**📚 مرجع كامل لواجهة برمجة التطبيقات API Reference**

📚 مرجع كامل لواجهة برمجة التطبيقات API Reference

📑 فهرس المحتويات

  1. المصادقة والصلاحيات
  2. جدول كل المسارات
  3. فحص الحالة
  4. التدوينات
  5. الوسائط
  6. التصنيفات
  7. المجموعات
  8. حقول ميتا المجموعات
  9. ملء حقول الميتا من التدوينة
  10. رموز الأخطاء
  11. سيناريوهات كاملة
  12. إدارة المجتمع والمستخدمين

1️⃣ المصادقة والصلاحيات

طرق المصادقة

كل الطلبات (عدا /health) تتطلب واحدًا من:

الطريقة الصيغة
Header (موصى به) Authorization: Bearer sk_xxxxxxxxxxxx
Header بديل X-API-Key: sk_xxxxxxxxxxxx
Query (غير مستحسن) ?api_key=sk_xxxxxxxxxxxx

الصلاحيات المتاحة

الصلاحية تسمح بـ
* كل الصلاحيات (لا يشمل الحذف — غير موجود أصلًا)
posts:read قراءة التدوينات (مع بيانات المحررين)
posts:write إنشاء/تحديث التدوينات (مع تعيين المحررين)
media:read قراءة الوسائط
media:write رفع الوسائط
categories:read قراءة التصنيفات
categories:write إنشاء/تحديث التصنيفات
groups:read قراءة المجموعات وحقولها
groups:write إنشاء/تحديث المجموعات
groups:meta:write إدارة حقول ميتا المجموعات
community:read قراءة المجتمع
community:write إنشاء/تحديث المجتمع
community:moderate إدارة النشر والاعتماد

⚠️ مهم: لا يوجد حذف في API. كل عمليات الحذف تتم من لوحة الإدارة (/admin/posts, /admin/media, إلخ).

إنشاء مفتاح API

  1. افتح /admin/api-keys
  2. أدخل اسم المفتاح
  3. اختر الصلاحيات
  4. اضغط إنشاء المفتاح
  5. انسخ المفتاح فورًا (يظهر مرة واحدة فقط)

2️⃣ جدول كل المسارات

# الطريقة المسار الصلاحية الوصف
1 GET /api/v1/health — فحص الحالة
التدوينات
2 GET /api/v1/posts posts:read قائمة التدوينات (مع editor_names)
3 GET /api/v1/posts/{id} posts:read تفاصيل تدوينة (مع editors[])
4 POST /api/v1/posts posts:write ⭐ إنشاء تدوينة (يدعم editors[])
5 POST /api/v1/posts/{id} posts:write ⭐ تحديث (PATCH semantics)
6 POST /api/v1/posts/{id}/delete posts:write نقل للمهملات
الوسائط
7 GET /api/v1/media media:read قائمة الوسائط
8 GET /api/v1/media/{id} media:read تفاصيل ملف
9 GET /api/v1/media/{id}/embed media:read HTML جاهز
10 POST /api/v1/media media:write رفع ملف
التصنيفات
11 GET /api/v1/categories categories:read قائمة/شجرة
12 GET /api/v1/categories/{id} categories:read تفاصيل
13 POST /api/v1/categories categories:write إنشاء
14 POST /api/v1/categories/{id} categories:write تحديث
المجموعات
15 GET /api/v1/groups groups:read قائمة
16 GET /api/v1/groups/{id} groups:read تفاصيل
17 POST /api/v1/groups groups:write إنشاء
18 POST /api/v1/groups/{id} groups:write تحديث
حقول الميتا
19 GET /api/v1/groups/{id}/meta-fields groups:read قائمة الحقول
20 POST /api/v1/groups/{id}/meta-fields groups:meta:write إنشاء حقل
21 POST /api/v1/groups/{id}/meta-fields/{fid} groups:meta:write تحديث حقل
22 POST /api/v1/groups/{id}/meta-fields/reorder groups:meta:write إعادة ترتيب
المجتمع
23–39 … /api/v1/community/* community:* انظر القسم 12

3️⃣ فحص الحالة

GET /api/v1/health

بدون مصادقة.

bash

curl https://soufyanex.net/api/v1/health

الاستجابة 200 OK:

json

{"ok": true, "version": "1.2", "time": "2026-11-02T10:00:00+00:00"}


4️⃣ التدوينات

🆕 4.0 — المحررون (Editors) — نظام جديد

الفكرة

كل تدوينة يمكن ربطها بمحرر واحد أو أكثر من /admin/editors. الـ API تدعم:

  • editors[] — قائمة IDs / slugs / أسماء (موصى به)
  • editor_ids[] — نفس السلوك (alias)
  • editor_id — مفرد (للتوافق الخلفي)

Resolver — كيف تُفسَّر القيم؟

كل عنصر في editors[] يُفسَّر بالترتيب:

القيمة تُفسَّر كـ
1 (رقم) editors.id = 1
"1" (نص رقمي) editors.id = 1
"sami" editors.slug = 'sami'
"سامي" editors.name = 'سامي' (يجب أن يكون فريدًا)
"1,2,3" CSV → [1, 2, 3]

القواعد

# القاعدة
1 editors: [] → يحذف كل المحررين
2 عدم إرسال أي حقل محرر في تحديث → لا تغيير (PATCH semantics)
3 editor_id (مفرد) في التحديث → يستبدل القائمة كلها بـ [editor_id]
4 أول محرر في editors[] يُحفظ تلقائيًا في posts.editor_id (توافق خلفي)
5 كل المحررين يُحفظون في جدول post_editors (many-to-many)
6 محرر غير موجود → 422 create_failed

4.1 GET /api/v1/posts

قائمة التدوينات مع اسم المحررين وتاريخ التعديل.

Query Parameters:

المعامل النوع الافتراضي الوصف
page int 1 الصفحة
per_page int 20 العدد (أقصى 100)
status string — draft, published, scheduled, archived
search string — بحث في العنوان أو slug

bash

curl "https://soufyanex.net/api/v1/posts?status=published&per_page=10&page=1" \

-H "Authorization: Bearer sk_xxxxx"

الاستجابة 200 OK (🆕 يشمل editor_names و edited_at):

json

{
"ok": true,
"items": [
{
"id": 15,
"title": "عنوان التدوينة",
"slug": "post-slug",
"excerpt": "ملخص",
"status": "published",
"published_at": "2026-11-01 12:00:00",
"edited_at": "2026-11-02 10:46:11",
"created_at": "2026-11-01 10:00:00",
"thumbnail_url": "/uploads/post-thumbnail.webp",
"editor_names": "سامي، قيس"
}
],
"total": 42,
"page": 1,
"per_page": 10,
"pages": 5

}


4.2 GET /api/v1/posts/{id}

تفاصيل تدوينة كاملة، بما فيها content_html، العلاقات، الميتا، والمحررون.

bash

curl https://soufyanex.net/api/v1/posts/15 \

-H "Authorization: Bearer sk_xxxxx"

الاستجابة 200 OK (🆕):

json

{
"ok": true,
"post": {
"id": 15,
"title": "عنوان التدوينة",
"slug": "post-slug",
"excerpt": "ملخص",
"content_html": "<p>محتوى</p>",
"status": "published",
"published_at": "2026-11-01 12:00:00",
"edited_at": "2026-11-02 10:46:11",
"primary_category_id": 1,
"parent_post_id": null,
"featured_media_id": 42,
"categories": [1, 3],
"groups": [2],
"meta": {"5": "قيمة", "6": "قيمة أخرى"},
"editors": [
{
"id": 1,
"name": "سامي",
"slug": "sami",
"photo_path": "/uploads/editors/sami.jpg"
},
{
"id": 3,
"name": "قيس",
"slug": "qais",
"photo_path": null
}
],
"editor_ids": [1, 3],
"editor_names": "سامي، قيس",
"author_id": 1,
"created_at": "2026-11-01 10:00:00",
"updated_at": "2026-11-02 10:46:11"
}

}

أخطاء: 404 not_found إن لم توجد التدوينة.


4.3 POST /api/v1/posts ⭐ إنشاء تدوينة

Body (JSON) — الحقول المقبولة

الحقل النوع إلزامي الوصف
title string ✅ العنوان
slug string ❌ الرابط (يُولَّد تلقائيًا)
excerpt string ❌ ملخص (يُولَّد تلقائيًا)
status string ❌ draft (افتراضي), published, scheduled, archived
published_at string ❌ مطلوب إذا status=scheduled
categories array | string ❌ IDs/slugs/أسماء/CSV
primary_category_id | primary_category int | string ❌ التصنيف الأساسي
parent_post_id | parent_post int | string ❌ التدوينة الأب
featured_media_id | featured_media int | string ❌ الصورة البارزة
editors array | string ❌ 🆕 IDs/slugs/أسماء/CSV
editor_ids array | string ❌ alias لـ editors
editor_id int | string ❌ مفرد (توافق خلفي)
groups array | string ❌ IDs/slugs/أسماء/CSV
meta object ❌ قيم حقول الميتا
content_html string ❌ HTML جاهز
blocks array ❌ بديل عن content_html

أنواع blocks المدعومة

النوع الحقول
paragraph text
heading text, level (2-6)
list items[], ordered (bool)
image media_id, alt, caption, link
video media_id, caption
audio media_id, caption
pdf media_id, display_name, filename
html html (raw)
media media_id (يكتشف النوع تلقائيًا)

مثال 1: بسيط (بدون محرر)

bash

curl -X POST https://soufyanex.net/api/v1/posts \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \

-d '{"title": "تدوينة جديدة", "status": "draft"}'

الاستجابة 201 Created:

json

{
"ok": true,
"post": {
"id": 16,
"title": "تدوينة جديدة",
"slug": "tdwynh-jdydh",
"content_html": "",
"status": "draft",
"editors": [],
"editor_ids": [],
"editor_names": "",
"edited_at": null,
"categories": [],
"groups": [],
"meta": {}
}

}


🆕 مثال 2: بثلاثة محررين (IDs)

bash

curl -X POST https://soufyanex.net/api/v1/posts \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"title": "منشور بثلاثة محررين",
"status": "published",
"editors": [1, 3, 5]

}'

الاستجابة 201 Created:

json

{
"ok": true,
"post": {
"id": 17,
"title": "منشور بثلاثة محررين",
"editors": [
{"id": 1, "name": "سامي", "slug": "sami", "photo_path": "/uploads/editors/sami.jpg"},
{"id": 3, "name": "قيس", "slug": "qais", "photo_path": null},
{"id": 5, "name": "نور", "slug": "nour", "photo_path": null}
],
"editor_ids": [1, 3, 5],
"editor_names": "سامي، قيس، نور"
}

}


🆕 مثال 3: بأسماء أو slugs المحررين

bash

curl -X POST https://soufyanex.net/api/v1/posts \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"title": "منشور بأسماء المحررين",
"editors": ["sami", "قيس", "نور"]

}'

كل عنصر يُفسَّر: "sami" → slug، "قيس" → name، "نور" → name.

🆕 مثال 4: بمحرر واحد (توافق خلفي)

bash

curl -X POST https://soufyanex.net/api/v1/posts \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"title": "منشور بمحرر واحد",
"editor_id": 1

}'

editor_id: 1 ≈ editors: [1]

🆕 مثال 5: CSV

bash

curl -X POST https://soufyanex.net/api/v1/posts \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"title": "منشور بمحررين (CSV)",
"editors": "sami,qais"

}'


🆕 مثال 6: كامل — بثلاثة محررين + تصنيفات + ميتا + blocks

bash

curl -X POST https://soufyanex.net/api/v1/posts \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"title": "منشور متكامل",
"slug": "complete-post",
"excerpt": "ملخص قصير",
"status": "published",
"categories": ["الوزارة", "تقنية"],
"primary_category": "الوزارة",
"editors": [1, 3, 5],
"featured_media_id": 42,
"groups": ["الوظائف"],
"meta": {
"عدد المناصب": "5",
"الإدارة": "تقنية المعلومات"
},
"blocks": [
{"type": "paragraph", "text": "مقدمة المنشور."},
{"type": "heading", "level": 2, "text": "قسم فرعي"},
{"type": "image", "media_id": 42, "alt": "صورة", "caption": "شرح"}
]

}'


أخطاء:

الحالة الاستجابة
title مفقود 422 validation
محرر غير موجود 422 create_failed: "المحرر المحدد غير موجود."
اسم محرر مكرر 422 create_failed: "المحرر 'X' غير فريد — استخدم slug أو ID."
slug مكرر 422 create_failed

4.4 POST /api/v1/posts/{id} ⭐ تحديث (PATCH semantics)

Body: أي حقل من حقول الإنشاء. كلها اختيارية.

🆕 PATCH semantics — كيف تعمل؟

ما ترسله النتيجة
{"title": "جديد"} فقط العنوان يتغير، المحررون يبقون
{"editors": [2, 4]} فقط المحررون يتغيرون، باقي الحقول تبقى
{"editors": []} تُحذف كل المحررين
{"editor_id": 5} يُستبدلون بـ [5]
عدم إرسال أي حقل محرر لا تغيير على المحررين

مثال 1: تغيير العنوان فقط (المحررون يبقون)

bash

curl -X POST https://soufyanex.net/api/v1/posts/15 \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \

-d '{"title": "عنوان محدّث"}'

المحررون لم يُلمسوا.

🆕 مثال 2: تحديث المحررين فقط

bash

curl -X POST https://soufyanex.net/api/v1/posts/15 \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \

-d '{"editors": [2, 4]}'

العنوان والمحتوى والتصنيفات لم تُلمس.

🆕 مثال 3: حذف كل المحررين

bash

curl -X POST https://soufyanex.net/api/v1/posts/15 \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \

-d '{"editors": []}'


🆕 مثال 4: إضافة محرر واحد

bash

curl -X POST https://soufyanex.net/api/v1/posts/15 \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \

-d '{"editor_id": 7}'

يستبدل القائمة كلها بـ [7]. إن أردت الإضافة دون استبدال، اقرأ القائمة الحالية أولًا ثم أرسل القائمة الكاملة.

مثال 5: نشر مسودة

bash

curl -X POST https://soufyanex.net/api/v1/posts/15 \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \

-d '{"status": "published"}'


مثال 6: تحديث الميتا (يتطلب groups)

bash

curl -X POST https://soufyanex.net/api/v1/posts/15 \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"groups": ["الوظائف"],
"meta": {"عدد المناصب": "10"}

}'

⚠️ مهم: groups تستبدل العلاقات السابقة بالكامل. أرسل القائمة الكاملة.
⚠️ مهم: meta يتطلب groups معها.


الاستجابة 200 OK: التفاصيل الكاملة بعد التحديث (بنفس بنية GET /posts/{id}).

أخطاء:

  • 404 not_found
  • 422 update_failed (محرر غير موجود، إلخ)

🆕 4.5 POST /api/v1/posts/{id}/delete

نقل التدوينة إلى المهملات (soft delete).

bash

curl -X POST https://soufyanex.net/api/v1/posts/15/delete \

-H "Authorization: Bearer sk_xxxxx"

لا يحذف المحررين — ON DELETE CASCADE في post_editors يعتني بهم تلقائيًا عند الحذف النهائي فقط.

5️⃣ الوسائط

5.1 GET /api/v1/media

Query Parameters: page, per_page, kind (image/video/audio/pdf), q

bash

curl "https://soufyanex.net/api/v1/media?kind=image&per_page=20" \

-H "Authorization: Bearer sk_xxxxx"

json

{
"ok": true,
"items": [
{
"id": 42,
"kind": "image",
"original_name": "photo.jpg",
"storage_path": "/uploads/photo.jpg",
"mime_type": "image/jpeg",
"file_size": 123456,
"width": 1024,
"height": 768,
"created_at": "2026-11-01 10:00:00",
"thumbnail_path": "/uploads/photo-thumbnail.webp"
}
],
"total": 15,
"page": 1,
"per_page": 20,
"pages": 1

}

5.2 GET /api/v1/media/{id}

bash

curl https://soufyanex.net/api/v1/media/42 \

-H "Authorization: Bearer sk_xxxxx"

5.3 GET /api/v1/media/{id}/embed

HTML جاهز للإدراج.

json

{
"ok": true,
"html": "<figure class=\"cms-image\" data-media-id=\"42\"...>...</figure>",
"media": { ... }

}

5.4 POST /api/v1/media — رفع ملف

Body (multipart/form-data):

الحقل النوع إلزامي
file ملف ✅

الأنواع المسموحة:

النوع الصيغ الحد
صور jpg, png, gif, webp, avif 15 MB
فيديو mp4, webm, ogg, mov 120 MB
صوت mp3, m4a, ogg, wav, weba, flac 40 MB
PDF pdf 25 MB

bash

curl -X POST https://soufyanex.net/api/v1/media \
-H "Authorization: Bearer sk_xxxxx" \

-F "file=@photo.jpg"

الاستجابة 201 (للصور):

json

{
"ok": true,
"media": {
"id": 42,
"kind": "image",
"url": "/uploads/photo.jpg",
"mime": "image/jpeg",
"size": 123456,
"width": 1024,
"height": 768,
"webp_srcset": "/uploads/photo-thumbnail.webp 300w, ...",
"url_webp": "/uploads/photo.webp"
}

}


6️⃣ التصنيفات

6.1 GET /api/v1/categories

تدعم ?tree=1 لشجرة كاملة، و?active=1 للمفعّلة فقط.

bash

# قائمة مسطحة
curl https://soufyanex.net/api/v1/categories \
-H "Authorization: Bearer sk_xxxxx"

# شجرة
curl "https://soufyanex.net/api/v1/categories?tree=1" \

-H "Authorization: Bearer sk_xxxxx"

الاستجابة:

json

{
"ok": true,
"items": [
{"id": 1, "name": "تقنية", "slug": "tech", "parent_id": null, "full_path": "tech", "position": 0}
]

}

6.2 GET /api/v1/categories/{id}

json

{
"ok": true,
"category": {
"id": 1,
"name": "تقنية",
"slug": "tech",
"parent_id": null,
"full_path": "tech",
"position": 0,
"is_active": 1,
"usage_count": 5
}

}

6.3 POST /api/v1/categories — إنشاء

الحقل النوع إلزامي الوصف
name string ✅ الاسم
slug string ❌ يُولَّد
parent | parent_id int | string ❌ الأب (id/slug/اسم)
description string ❌ وصف
content_html string ❌ محتوى
position int ❌ ترتيب
is_active bool ❌ (افتراضي true)

bash

curl -X POST https://soufyanex.net/api/v1/categories \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \

-d '{"name": "تقنية"}'

6.4 POST /api/v1/categories/{id} — تحديث

Body: أي حقل من حقول الإنشاء (كلها اختيارية).

⚠️ حماية: الحقول غير المرسلة تحتفظ بقيمها. parent: null = نقل للجذر. parent غير موجود = إبقاء الأب. حماية من الحلقات (cycle).


7️⃣ المجموعات

7.1 GET /api/v1/groups

json

{
"ok": true,
"items": [
{"id": 2, "name": "الوظائف", "slug": "jobs", "color": "#0a7"}
]

}

7.2 GET /api/v1/groups/{id}

json

{
"ok": true,
"group": {
"id": 2,
"name": "الوظائف",
"slug": "jobs",
"description": "إعلانات الوظائف",
"color": "#0a7",
"is_active": 1,
"position": 0,
"post_count": 5,
"hook_count": 2
},
"post_ids": [15, 16, 20],
"hooks": {}

}

7.3 POST /api/v1/groups — إنشاء

الحقل النوع إلزامي
name string ✅
slug string ❌
description string ❌
color string ❌
position int ❌
is_active bool ❌
post_ids int[] ❌
hooks object ❌

7.4 POST /api/v1/groups/{id} — تحديث

⚠️ إذا لم ترسل post_ids أو hooks، تبقى قيمها الحالية.


8️⃣ حقول ميتا المجموعات

8.1 GET /api/v1/groups/{id}/meta-fields

json

{
"ok": true,
"group_id": 2,
"fields": [
{
"id": 5,
"group_id": 2,
"field_key": "vacancies",
"field_label": "عدد المناصب",
"field_type": "number",
"is_required": 1,
"sort_order": 0,
"is_active": 1
}
]

}

8.2 POST /api/v1/groups/{id}/meta-fields — إنشاء حقل

الحقل النوع إلزامي
field_label string ✅
field_key string ❌
field_type string ❌ (text, number, date, url, textarea, select)
field_options string ❌ (سطر لكل خيار)
is_required bool ❌
sort_order int ❌
is_active bool ❌

8.3 POST /api/v1/groups/{id}/meta-fields/{fid} — تحديث حقل

Body: أي حقل من حقول الإنشاء (عدا field_key — محمي).

8.4 POST /api/v1/groups/{id}/meta-fields/reorder

json

{"ids": [7, 5, 6]}


9️⃣ ملء حقول الميتا من التدوينة

🎯 3 خطوات

  1. GET /api/v1/groups/{id}/meta-fields — اعرف الحقول
  2. POST /api/v1/posts مع groups + meta
  3. استخدم field_label (عربي) أو field_key أو field_id

🔑 الخيارات الثلاثة للمفتاح

المفتاح مثال متى
field_label "عدد المناصب" الأسهل
field_key "vacancies" مستقر
field_id 5 الأدق

⚠️ القاعدة الذهبية

  • meta بدون groups → مرفوض
  • groups بدون meta → حذف كل القيم السابقة

🔟 رموز الأخطاء

HTTP error المعنى
401 unauthorized مفتاح مفقود/خاطئ/منتهي
403 forbidden المفتاح لا يملك الصلاحية
404 not_found العنصر غير موجود
422 validation حقل مفقود/غير صالح
422 create_failed خطأ في الإنشاء (slug مكرر، حقل غير معروف، محرر غير موجود، اسم محرر مكرر...)
422 update_failed خطأ في التحديث
422 cycle حلقة في هرمية التصنيفات
422 upload_failed نوع/حجم/صورة غير صالحة
422 reorder_failed خطأ في إعادة ترتيب الحقول
500 — خطأ خادم

1️⃣1️⃣ سيناريوهات كاملة

🆕 السيناريو 1: نشر تدوينة بثلاثة محررين + صورة + ميتا

bash

# 1) اعرف المحررين المتاحين
curl https://soufyanex.net/api/v1/community/editors \
-H "Authorization: Bearer sk_xxxxx"
# → [{"id":1,"name":"سامي","slug":"sami"}, {"id":3,"name":"قيس","slug":"qais"}, ...]

# 2) ارفع صورة
curl -X POST https://soufyanex.net/api/v1/media \
-H "Authorization: Bearer sk_xxxxx" \
-F "file=@banner.jpg"
# → {"media": {"id": 42, "url": "/uploads/banner.jpg"}}

# 3) أنشئ التدوينة
curl -X POST https://soufyanex.net/api/v1/posts \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"title": "مهندس DevOps — وزارة التقنية",
"slug": "devops-engineer-2026",
"status": "published",
"editors": ["sami", "qais", "نور"],
"categories": ["الوزارة"],
"primary_category": "الوزارة",
"featured_media_id": 42,
"groups": ["الوظائف"],
"meta": {"عدد المناصب": "3"},
"blocks": [
{"type": "paragraph", "text": "نبحث عن مهندس DevOps"}
]

}'

الاستجابة:

json

{
"ok": true,
"post": {
"id": 20,
"editors": [
{"id": 1, "name": "سامي", "slug": "sami", "photo_path": "/uploads/editors/sami.jpg"},
{"id": 3, "name": "قيس", "slug": "qais", "photo_path": null},
{"id": 5, "name": "نور", "slug": "nour", "photo_path": null}
],
"editor_ids": [1, 3, 5],
"editor_names": "سامي، قيس، نور"
}

}


🆕 السيناريو 2: تحديث محررين دون لمس باقي الحقول

bash

# اجلب الحالة الحالية
curl https://soufyanex.net/api/v1/posts/20 \
-H "Authorization: Bearer sk_xxxxx"
# → {"post": {"editors": [{"id":1}, {"id":3}, {"id":5}]}}

# استبدلهم بمحررين آخرين — العنوان والمحتوى يبقيان كما هما
curl -X POST https://soufyanex.net/api/v1/posts/20 \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \

-d '{"editors": [2, 4]}'


🆕 السيناريو 3: إضافة محرر دون حذف الباقين

بما أن editors[] يستبدل القائمة كاملة، فأضف الجديد إلى الحالية:

bash

# 1) اقرأ الحاليين
curl https://soufyanex.net/api/v1/posts/20 \
-H "Authorization: Bearer sk_xxxxx" | jq '.post.editor_ids'
# → [1, 3, 5]

# 2) أرسل القائمة الجديدة = القديمة + إضافة 7
curl -X POST https://soufyanex.net/api/v1/posts/20 \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \

-d '{"editors": [1, 3, 5, 7]}'


السيناريو 4: من الصفر إلى النشر (بدون محرر)

(انظر النسخة السابقة — نفس الخطوات لكن بدون editors)


1️⃣2️⃣ إدارة المجتمع والمستخدمين

(بدون تغيير عن النسخة السابقة — انظر القسم الكامل أدناه للتفاصيل)

إدارة المحررين في المجتمع

  • GET /api/v1/community/editors — قائمة المحررين المتاحين
  • POST /api/v1/community/editors/sync — مزامنة من editors (القاعدة الرئيسية)

📊 ملخص التغييرات في هذه النسخة (v1.2)

العنصر v1.1 v1.2
editors[] في POST /posts ❌ ✅
editors[] في POST /posts/{id} ❌ ✅ (PATCH semantics)
editor_id مفرد (توافق) ❌ ✅
editors[] في GET /posts/{id} ❌ ✅ كاملة
editor_ids[] في GET /posts/{id} ❌ ✅
editor_names في GET /posts ❌ ✅
edited_at في كل الاستجابات ❌ ✅
Resolver مرن (ID/slug/name/CSV) ❌ ✅
post_editors (many-to-many) ❌ ✅
PATCH semantics للمحررين ❌ ✅

جدول community_users — الشرح الكامل

⚠️ تنبيه: ليس لديّ ملف الـ schema

لم أستلم database/community-schema.sql في المحادثة. البنية أدناه مستنتجة من:

  • الـ API documentation السابق

  • CommunityUserRepository.php (7.3 KB — في شجرة الموقع)

  • CommunityEmailHasher.php (2.2 KB)

  • CommunityDatabase.php (7 KB)

للتأكد 100%: أرسل لي database/community-schema.sql وسأعطيك البنية الدقيقة.

---

1️⃣ ما هو community_users؟

جدول منفصل تمامًا عن users (مستخدمي الإدارة)، يحوي زوار المجتمع الذين يطرحون أسئلة.

الخصائص users community_users
الغرض مشرفو الموقع زوار المجتمع
القاعدة motavajj_pure_php_cms motavajj_community
الدخول تسجيل دخول بكلمة سر + 2FA لا تسجيل دخول
البريد نص صريح مشفَّر (hash + AES-256)
الظهور في /admin/users /admin/community/users
الحذف من /admin/users من /admin/community
API ❌ لا ✅ كاملة

---

2️⃣ البنية المتوقعة (مستنتجة)

sql

CREATE TABLE `community_users` (

`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,

-- الهوية

`name` VARCHAR(60) NOT NULL,

`profile_slug` VARCHAR(190) NOT NULL, -- عربي مسموح: "سارة-أحمد"

-- البريد المشفَّر (لا يمكن استرجاعه)

`email_hash` CHAR(64) NULL, -- SHA-256 للبحث

`email_encrypted` TEXT NULL, -- AES-256-GCM للاسترجاع من الإدارة

`email_verified` TINYINT(1) NOT NULL DEFAULT 0,

-- الحالة

`status` ENUM('draft','published','removed') NOT NULL DEFAULT 'draft',

`noindex` TINYINT(1) NOT NULL DEFAULT 1, -- 1 = لا تُفهرس (مسودة)

-- الأمان

`fingerprint` CHAR(64) NULL, -- بصمة الجهاز (rate limit)

`ip_hash` CHAR(64) NULL, -- IP مشفَّر

-- التوقيتات

`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

`published_at` DATETIME NULL,

PRIMARY KEY (`id`),

UNIQUE KEY `uq_cu_slug` (`profile_slug`),

UNIQUE KEY `uq_cu_email` (`email_hash`),

KEY `idx_cu_status` (`status`, `published_at`),

KEY `idx_cu_created` (`created_at`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

⚠️ هذا تقدير. قد يكون فيه اختلافات (حقول إضافية، أنواع مختلفة). أرسل community-schema.sql للتأكد.

---

3️⃣ الشرح التفصيلي لكل حقل

id

  • BIGINT UNSIGNED — مطابق لـ community_questions.user_id و community_answers (FK)

  • يُستخدم في URLs: /community/u/{profile_slug}

name

  • 2–60 حرفًا (حسب API validation)

  • مثال: "أحمد محمد"

profile_slug (وليس slug!)

  • مميز: يبدأ بـ profile_ لتفادي الالتباس مع slug (الأسئلة/التصنيفات)

  • عربي مسموح: "سارة-أحمد" (UTF-8)

  • UNIQUE: لا يمكن تكراره

email_hash — سرّ الخصوصية

  • SHA-256 للبريد (hex, 64 حرفًا)

  • الغرض: البحث السريع "هل البريد موجود؟" دون تخزين البريد صريحًا

  • مثال: sara@example.com → 8f14e45fceea167a5a36dedd4bea2543...

  • فائدة: لو سُرِّبت القاعدة، لا يمكن استخراج البريد الأصلي بسهولة

email_encrypted

  • AES-256-GCM (تشفير متماثل)

  • المفتاح: يُحفظ في .env (COMMUNITY_EMAIL_KEY)

  • المتى يُفكّ؟: فقط في /admin/community/users/{id}/edit (عرض البريد)

  • لماذا؟: حماية الخصوصية — حتى لو سُرِّبت القاعدة، البريد غير مقروء

status

القيمة المعنى
draft مسودة — لا يظهر في القوائم العامة، noindex=1
published منشور — يظهر في /community/u/{slug}
removed محذوف من الإدارة (soft) — يبقى مرتبطًا بأسئلته

noindex

  • 1 = noindex (Google يتجاهله) — للـ draft

  • 0 = قابل للفهرسة — للـ published

  • الغرض: منع الأسئلة/المستخدمين غير المنشورين من الظهور في SEO

fingerprint + ip_hash

  • للحماية من spam: CommunityRateLimiter يستخدمها

  • ip_hash = SHA-256 للـ IP (لا يُخزَّن IP صريحًا)

---

4️⃣ العلاقات مع الجداول الأخرى

text

┌─────────────────────────┐

│ community_users │

│ id, name, profile_slug, │

│ email_hash, status │

└────────────┬─────────────┘

│

│ 1:N

▼

┌─────────────────────────┐

│ community_questions │

│ id, user_id, title, │

│ slug, status, noindex │

└────────────┬─────────────┘

│

│ 1:N

▼

┌─────────────────────────┐

│ community_answers │

│ id, question_id, │

│ editor_id (→ community_ │

│ editors), body, votes │

└─────────────────────────┘

ملاحظة مهمة: حذف مستخدم من الإدارة لا يحذف أسئلته تلقائيًا — يبقى status = 'removed' مع أسئلته، لأن الإجابات مرتبطة بها.

---

5️⃣ العلاقة مع community_editors

text

┌─────────────────────┐ ┌─────────────────────┐

│ editors │ │ community_editors │

│ (القاعدة الرئيسية) │ sync │ (snapshot للعرض) │

│ id, name, slug, │ ───────────► │ id, source_editor_ │

│ photo_path, bio │ │ id, name, slug │

└─────────────────────┘ └─────────────────────┘

  • المحررون يُدارون في القاعدة الرئيسية (/admin/editors)

  • نسخة snapshot في community_editors للعرض فقط

  • المزامنة تلقائية عبر POST /api/v1/community/editors/sync

---

6️⃣ أمثلة استعلامات (phpMyAdmin)

أ) عدد المستخدمين حسب الحالة

sql

SELECT status, COUNT(*) AS n

FROM community_users

GROUP BY status;

ب) المستخدمون النشطون مع عدد أسئلتهم

sql

SELECT u.id, u.name, u.profile_slug,

COUNT(q.id) AS questions_count

FROM community_users u

LEFT JOIN community_questions q

ON q.user_id = u.id AND q.status = 'published'

WHERE u.status = 'published'

GROUP BY u.id

ORDER BY questions_count DESC;

ج) البحث بالبريد

sql

-- 1) احسب hash في PHP أو shell:

-- php -r "echo hash('sha256', 'sara@example.com');"

-- 2) استعلم:

SELECT id, name, profile_slug, status

FROM community_users

WHERE email_hash = '<hash_هنا>';

د) مستخدمون بلا أسئلة (لتنظيف)

sql

SELECT u.id, u.name

FROM community_users u

WHERE NOT EXISTS (

SELECT 1 FROM community_questions q

WHERE q.user_id = u.id AND q.status = 'published'

)

AND u.status = 'draft';

---

7️⃣ ما يمكنك فعله من API

العملية المسار الصلاحية
جلب القائمة GET /api/v1/community/users community:read
تفاصيل + أسئلة GET /api/v1/community/users/{id} community:read
إنشاء POST /api/v1/community/users community:write
تحديث POST /api/v1/community/users/{id} community:write
حذف ❌ من /admin/community فقط

⚠️ ما لا يمكن فعله من API

العملية السبب
قراءة email الأصلي مشفَّر — فقط من /admin/community/users/{id}/edit
تغيير profile_slug بعد الإنشاء ثابت (SEO)
حذف المستخدم لحماية الأسئلة/الإجابات

---

8️⃣ الفرق المهم: /users مقابل /questions

عند إرسال بريد موجود:

في السلوك
POST /community/users ❌ يفشل بـ 422: "البريد مستخدم من قبل"
POST /community/questions مع user: {...} ✅ يُعيد استخدام المستخدم الموجود

السبب: /questions يستخدم CommunityUserRepository::findOrCreateByEmail() — بينما /users يستخدم create() المباشر.

---

9️⃣ لتأكيد البنية الدقيقة

أرسل لي:

  1. database/community-schema.sql — البنية الكاملة (إلزامي)

  2. app/Modules/Community/CommunityUserRepository.php — للتأكد من الأعمدة المستخدمة في الكود

  3. app/Modules/Community/CommunityEmailHasher.php — لفهم آلية التشفير

حالما تصلني، سأعطيك:

  • البنية الفعلية (وليس المتوقعة)

  • الـ indexes الحقيقية

  • قواعد العمل الحقيقية (هل profile_slug ثابت؟ هل email_verified موجود؟)

  • أي حقول إضافية لم أتوقعها

---

📌 الخلاصة

السؤال الجواب
ما هو community_users؟ جدول زوار المجتمع، منفصل عن users
لماذا البريد مشفَّر؟ خصوصية — لا يمكن استرجاعه من API
كيف أبحث بالبريد؟ احسب SHA-256 ثم استعلم email_hash
هل يمكن حذف مستخدم؟ ❌ من API — فقط من /admin/community
ما الفرق عن /users؟ /questions يُعيد الاستخدام، /users يفشل

✅ قواعد ذهبية جديدة (للمحررين)

# القاعدة
1 استخدم editors: [...] دائمًا (متعدد)
2 editor_id مفرد للتوافق فقط
3 editors: [] = حذف الكل
4 عدم إرسال أي حقل محرر في التحديث = لا تغيير
5 كل حقل يُفسَّر: رقم → ID، نص → slug ثم name
6 أول محرر في editors[] يُحفظ في posts.editor_id
7 post_editors يُحدَّث تلقائيًا عند كل تغيير
8 edited_at يُحدَّث فقط عند تغيير (العنوان/المقتطف/المحتوى) و التدوينة منشورة
9 عند حذف محرر: ON DELETE CASCADE يحذفه من post_editors تلقائيًا
10 للحصول على قائمة المحررين الحالية: GET /posts/{id} → .post.editor_ids

🔧 مثال JavaScript (fetch) — نشر مع محررين

javascript

async function publishPostWithEditors({ title, editorSlugs, ministry, imageId }) {
const res = await fetch('https://soufyanex.net/api/v1/posts', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_xxxxxxxxxxxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
title,
status: 'published',
editors: editorSlugs, // ["sami", "qais", "نور"]
categories: [ministry],
primary_category: ministry,
featured_media_id: imageId,
blocks: [
{ type: 'paragraph', text: 'مقدمة المنشور.' },
],
}),
});

const data = await res.json();
if (!data.ok) throw new Error(data.message || data.error);

console.log('Editor IDs:', data.post.editor_ids);
console.log('Editor Names:', data.post.editor_names);
return data.post;

}


🧪 اختبار سريع

bash

# 1) المحررون المتاحون
curl https://soufyanex.net/api/v1/community/editors \
-H "Authorization: Bearer sk_xxxxx"

# 2) إنشاء تدوينة بمحررين
curl -X POST https://soufyanex.net/api/v1/posts \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \
-d '{"title": "اختبار", "editors": ["sami", "qais"], "status": "draft"}'

# 3) تحقق
curl https://soufyanex.net/api/v1/posts/{id} \
-H "Authorization: Bearer sk_xxxxx" | jq '.post.editors'

# 4) تحديث المحررين فقط
curl -X POST https://soufyanex.net/api/v1/posts/{id} \
-H "Authorization: Bearer sk_xxxxx" \
-H "Content-Type: application/json" \

-d '{"editors": [2, 4]}'


✅ الخلاصة

ما تغيّر التفاصيل
API اكتسبت إمكانية تعيين محرر أو أكثر لكل تدوينة
الاستجابات تعيد editors[] كاملة + editor_ids + editor_names + edited_at
PATCH semantics عدم إرسال حقل محرر = لا تغيير
التوافق editor_id مفرد لا يزال مدعومًا
جدول post_editors many-to-many مع position للترتيب

كل ما تحتاجه للإنتاج: أنشئ مفتاحًا بـ posts:write (و community:read إن أردت جلب المحررين) وابدأ الإرسال.