**📚 مرجع كامل لواجهة برمجة التطبيقات API Reference**
📑 فهرس المحتويات
- المصادقة والصلاحيات
- جدول كل المسارات
- فحص الحالة
- التدوينات
- الوسائط
- التصنيفات
- المجموعات
- حقول ميتا المجموعات
- ملء حقول الميتا من التدوينة
- رموز الأخطاء
- سيناريوهات كاملة
- إدارة المجتمع والمستخدمين
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
- افتح
/admin/api-keys - أدخل اسم المفتاح
- اختر الصلاحيات
- اضغط إنشاء المفتاح
- انسخ المفتاح فورًا (يظهر مرة واحدة فقط)
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_found422 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 |
| 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 خطوات
GET /api/v1/groups/{id}/meta-fields— اعرف الحقولPOST /api/v1/postsمعgroups+meta- استخدم
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️⃣ لتأكيد البنية الدقيقة
أرسل لي:
-
database/community-schema.sql— البنية الكاملة (إلزامي) -
app/Modules/Community/CommunityUserRepository.php— للتأكد من الأعمدة المستخدمة في الكود -
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 إن أردت جلب المحررين) وابدأ الإرسال.
