Initial SMM panel implementation

This commit is contained in:
diyaa
2026-08-13 19:05:47 +02:00
commit 7717a674c7
64 changed files with 8626 additions and 0 deletions
+206
View File
@@ -0,0 +1,206 @@
# TASKS.md — خطة تنفيذ SMM Panel للوكيل البرمجي
> نفّذ المهام بالترتيب. كل مرحلة لازم "تشتغل وتُختبر" قبل ما تنتقل للي بعدها.
> المرجع الكامل: `AGENTS.md` (السباق العام) و`WALLET.md` (تفاصيل الحساب والمحفظة — إلزامي قراءته قبل مرحلة 2 و3)
> و`FRONTEND.md` (شرح الواجهة الحالية + مواصفات دقيقة للمراحل 9 و10 — إلزامي قراءته قبلهن).
> علّم كل مهمة [x] بعد ما تخلص وتتأكد إنها تشتغل فعلياً (شغّلها، ما تفترض).
---
## المرحلة 0 — الإعداد
- [x] إنشاء مشروع Node.js + Express، هيكلة المجلدات حسب `AGENTS.md` § 3.
- [x] إعداد الاتصال بقاعدة بيانات MySQL/MariaDB (connection pool، ليس اتصال مفتوح لكل طلب).
- [x] إعداد نظام migrations (Knex / Prisma / أي أداة) — ممنوع تعديل السكيمة يدوياً بدون migration.
- [x] إنشاء ملف `.env.example` بكل المتغيرات المذكورة في `AGENTS.md` § 9.
- [x] إعداد error handler مركزي (middleware) يرجّع رسائل موحدة وما يسرّب stack trace بالإنتاج.
- [x] إعداد logging أساسي (بدون تسجيل باسوردات أو مفاتيح API إطلاقاً).
**معيار القبول:** السيرفر يشتغل، يتصل بقاعدة البيانات، ويرجّع 200 على `GET /health`.
---
## المرحلة 1 — قاعدة البيانات
- [x] إنشاء جدول `users` حسب `AGENTS.md` § 4 (بما فيها `balance DECIMAL(12,4) DEFAULT 0`).
- [x] إنشاء جدول `services`.
- [x] إنشاء جدول `orders`.
- [x] إنشاء جدول `transactions` — مع `UNIQUE` على (`user_id`, `reference`, `type`) لمنع التكرار (`WALLET.md` § 2.2).
- [x] إنشاء جدول `deposits` — مع `UNIQUE` على `tx_ref` (`WALLET.md` § 2.3-أ).
- [x] (اختياري لكن موصى) جدول `idempotency_keys` حسب `WALLET.md` § 3.
- [x] (اختياري) جدول `refresh_tokens` لإدارة الجلسات حسب `WALLET.md` § 1.2.
**معيار القبول:** كل الجداول موجودة، الـ FKs والـ UNIQUE constraints فعّالة (جرّب إدخال قيمة مكررة وتأكد إنها تُرفض).
---
## المرحلة 2 — نظام الحساب (Auth)
- [x] `POST /auth/register` — تشفير الباسورد بـ bcrypt، تحقق من تفرّد username/email.
- [x] `POST /auth/login` — إصدار JWT قصير المدة + refresh token، رسالة خطأ موحدة (`WALLET.md` § 1.2).
- [x] Rate limiting على `/auth/login` و`/auth/register` (مثلاً 5 محاولات/15 دقيقة).
- [x] Middleware `auth` (تحقق JWT) و`requireActive` (يرفض المستخدم المحظور) و`admin` (تحقق role).
- [x] `POST /auth/logout` — إبطال الـ refresh token.
- [x] `POST /auth/forgot-password` + `POST /auth/reset-password` (token عشوائي مؤقت، يبطل الجلسات القديمة عند إعادة التعيين).
- [x] `GET /me` — يرجّع بيانات الحساب + الرصيد الحالي.
**معيار القبول:**
- تسجيل حساب جديد ثم دخول فيه يرجّع JWT صالح.
- محاولة دخول بباسورد غلط 6 مرات متتالية تُحظر مؤقتاً.
- طلب لمسار محمي بدون توكن يرجّع 401.
---
## المرحلة 3 — المحفظة (Wallet) — أهم مرحلة، لا تستعجل فيها
اقرأ `WALLET.md` كامل قبل البدء هون.
- [x] بناء `wallet.service.js` — نقطة الدخول الوحيدة لأي تعديل على `balance`. لا كود ثاني يلمس عمود `balance` مباشرة.
- [x] دالة `chargeBalance(userId, amount, reference, note)` — قفل الصف (`FOR UPDATE`) + تحقق الرصيد + خصم + تسجيل transaction، الكل بـ DB transaction واحدة.
- [x] دالة `creditBalance(userId, amount, reference, note)` — نفس الآلية للإضافة (شحن/استرجاع).
- [x] `GET /transactions` — كشف حساب المستخدم مرتب زمنياً مع `balance_after` لكل حركة.
- [x] `POST /deposits` — تقديم طلب شحن (amount, method, tx_ref)، رفض فوري لو `tx_ref` مكرر.
- [x] `GET /deposits` — طلبات الشحن الخاصة بالمستخدم وحالتها.
- [x] `PATCH /admin/deposits/:id` — موافقة/رفض، بقفل الصف ومنع الموافقة المزدوجة (`WALLET.md` § 2.3-ب).
- [x] `PATCH /admin/users/:id` (تعديل رصيد يدوي) — يمر حصراً عبر `wallet.service.js`.
- [x] **اختبارات إلزامية** (`WALLET.md` § 4):
- [x] خصم أكبر من الرصيد يُرفض ولا يغيّر الرصيد.
- [x] عمليتان متزامنتان (parallel) على نفس الرصيد → وحدة بس تنجح.
- [x] موافقتان متزامنتان على نفس طلب شحن → إضافة وحدة بس.
- [x] `tx_ref` مكرر → رفض بدون لمس الرصيد.
- [x] بعد سلسلة عمليات عشوائية: `SUM(transactions.amount) == users.balance`.
- [x] job دوري (cron) للتسوية (Reconciliation) — يقارن `SUM(transactions)` مع `users.balance` ويرسل تنبيه عند أي فرق (`WALLET.md` § 2.4).
**معيار القبول:** كل الاختبارات أعلاه خضراء فعلياً (شغّلها، شوف النتيجة)، ولا يوجد أي مسار كود يعدّل `balance` خارج `wallet.service.js` (تأكد بالبحث النصي بالمشروع).
---
## المرحلة 4 — الخدمات (Services)
- [x] `provider.service.js` — طبقة موحّدة للتواصل مع API المزوّد (`services` / `add` / `status` / `balance`) حسب `AGENTS.md` § 5.
- [x] `POST /admin/services/sync` — سحب الخدمات من المزوّد وتخزينها/تحديثها محلياً.
- [x] منطق حساب `sell_price` من `provider_price` + الهامش (`DEFAULT_MARKUP_PERCENT` أو هامش لكل خدمة — حسب القرار المفتوح في `AGENTS.md` § 11).
- [x] `GET /services` — قائمة الخدمات النشطة فقط، بسعر البيع (لا تُظهر `provider_price` للمستخدم العادي أبداً).
- [x] `PATCH /admin/services/:id` — تعديل السعر/الهامش/التفعيل من لوحة الأدمن.
**معيار القبول:** مزامنة الخدمات تعمل، والمستخدم العادي ما يقدر يشوف سعر المزوّد الحقيقي بأي استجابة API.
---
## المرحلة 5 — الطلبات (Orders) — الجزء اللي يربط المحفظة بالمزوّد
- [x] `POST /orders` — تنفيذ التدفق الكامل خطوة بخطوة (`AGENTS.md` § 6):
1. تحقق الخدمة `is_active` والكمية ضمن `[min, max]`.
2. احسب `charge` **في الخادم فقط** (لا تثق بأي سعر من الواجهة).
3. `chargeBalance()` من المرحلة 3 (خصم + إنشاء order بحالة `pending` + transaction، الكل atomic).
4. أرسل الطلب لمزوّد الخدمة عبر `provider.service.js`.
5. نجاح → خزّن `provider_order_id`، حدّث `status=processing`.
6. فشل → استرجاع فوري عبر `creditBalance()` (type=`refund`)، حدّث `status=failed`.
- [x] `GET /orders` و`GET /orders/:id` — طلبات المستخدم (تأكد إنه يشوف طلباته هو فقط، لا طلبات غيره).
- [x] `GET /admin/orders` — كل الطلبات لعرض الأدمن.
**معيار القبول:** طلب ناجح يخصم المبلغ الصحيح بالضبط ويوصل provider_order_id، وطلب فاشل (مزوّد down مثلاً) يرجع الرصيد تلقائياً بدون تدخل يدوي.
---
## المرحلة 6 — متابعة الحالات (Status Tracking)
- [x] job دوري (مثلاً كل 5-10 دقائق) يمر على الطلبات بحالة `processing`/`in_progress` ويستعلم عن حالتها من المزوّد (`action: status`).
- [x] تحديث `status` و`remains` بناءً على رد المزوّد.
- [x] معالجة الحالات الجزئية (`partial`) — قرار: هل يُسترجع فرق المبلغ تلقائياً أم يحتاج مراجعة أدمن؟ (وضّح القرار بكود وتوثيق).
**معيار القبول:** طلب تجريبي يتحدث تلقائياً لحالته الحقيقية عند المزوّد خلال دورة الـ job التالية.
---
## المرحلة 7 — لوحة الأدمن
- [x] `GET /admin/users` + `PATCH /admin/users/:id` (حظر/رفع حظر، تعديل رصيد).
- [x] `GET /admin/deposits` (المعلّقة أولاً) + الموافقة/الرفض.
- [x] `GET /admin/stats` — إجمالي المبيعات، الأرباح (sell - provider cost)، عدد المستخدمين النشطين، طلبات معلّقة.
- [x] كل مسارات هذا القسم خلف middleware `admin` بدون استثناء — تحقق فعلياً بمحاولة وصول بحساب عادي وتأكد من 403.
**معيار القبول:** حساب `role=user` يحاول الوصول لأي مسار أدمن → 403، وحساب أدمن يشوف كل البيانات صح.
---
## المرحلة 8 — الواجهة (Frontend)
- [x] صفحات: تسجيل / دخول / لوحة المستخدم (الرصيد + كشف حساب) / الخدمات / إنشاء طلب / طلباتي / شحن رصيد.
- [x] لوحة أدمن منفصلة (خدمات، طلبات شحن، طلبات، مستخدمين، إحصائيات).
- [x] عرض الرصيد بشكل بارز دائماً (Header) ويتحدث فور أي عملية.
- [x] رسائل خطأ واضحة بالعربية (رصيد غير كافٍ، رابط غير صالح، إلخ).
**معيار القبول:** مستخدم جديد يقدر يسوي التدفق الكامل من الواجهة: تسجيل → طلب شحن → (موافقة أدمن) → شراء خدمة → متابعة الحالة، بدون لمس الـ API مباشرة.
---
## المرحلة 9 — نظام الألوان والتصميم المتجاوب
اقرأ `FRONTEND.md` § 3 كامل قبل البدء هون.
- [x] وحّد متغيرات اللون الأساسي بـ `styles.css` تحت `--primary-h` / `--primary-s` / `--primary-l` واشتق منها `--primary` / `--primary-hover` / `--primary-soft` (استبدل كل استخدام لـ `--accent` / `--accent-strong` / `--blue` بالمتغيرات الجديدة).
- [x] أضف زر Theme Picker بالـ `topbar`: 6 ألوان جاهزة (Presets) + خيار لون مخصص (`<input type="color">`).
- [x] عند اختيار لون: حوّل HEX إلى HSL بـ JS، حدّث المتغيرات الثلاث على `document.documentElement`، واحفظ الاختيار بـ `localStorage`.
- [x] طبّق اللون المحفوظ فوراً بأول تنفيذ للسكربت (قبل أول رسم) لمنع أي وميض بلون افتراضي.
- [x] **(تحسين مطلوب — راجع `FRONTEND.md` § 3.3 "نسخة منقّحة")** الزر الحالي دائرة صغيرة بدون تسمية وحسّه بدائي — بدّله بزر فيه أيقونة (SVG بسيط) + نص "المظهر" (يختفي على الموبايل، أيقونة بس).
- [x] لوحة الألوان: كبّر الشبكة (44px بدل 34px) وأضف اسم تحت كل لون (تركواز/أزرق/بنفسجي/أخضر/برتقالي/وردي).
- [x] بيّن اللون المفعّل حالياً بعلامة واضحة (حلقة حد + ✓) على الـ Preset المطابق.
- [x] قسم اللون المخصص: زد حجم `<input type="color">`، أضف حقل نصي HEX مرتبط ثنائياً معه، وشريحة معاينة حيّة تتحدث أثناء السحب قبل التثبيت.
- [x] اللوحة تنغلق بالضغط خارجها، بزر × صريح، أو بمفتاح Escape — مع انتقال فتح/إغلاق ناعم (`transition`).
- [x] لا تغيّر آلية الحفظ/التطبيق بـ `localStorage` ولا الـ HEX→HSL — هذا الجزء شغّال صح، خلّيه زي ما هو.
- [x] الألوان الدلالية (نجاح/تحذير/خطر لحالات الطلبات والحظر) تبقى ثابتة ومنفصلة عن اللون الأساسي — لا تتأثر بالثيمنج.
- [x] أضف كسرين إضافيين للتجاوب (640px و1024px) بدل الكسر الواحد الحالي (900px).
- [x] القائمة الجانبية: ثابتة ≥1024px، صف علوي بالتابلت، Drawer منزلق بزر ☰ تحت 640px مع خلفية قابلة للإغلاق.
- [x] الجداول الكثيفة (الطلبات، كشف الحساب، طلبات الشحن) تتحول لعرض بطاقات (Card view) تحت 640px بدل التمرير الأفقي.
- [x] `.inline-form` تتكدس تدريجياً (عمودين بالتابلت، عمود واحد بالموبايل).
- [x] ارتفاع الأزرار وحقول الإدخال لا يقل عن 44px بالموبايل.
- [x] `topbar` يلتف (`flex-wrap`) بدل ما يفيض خارج الشاشة على الموبايل.
**معيار القبول:** غيّر اللون الأساسي وأعد تحميل الصفحة — نفس اللون يرجع فوراً بدون وميض. افتح الموقع بعرض موبايل (375px) وتابلت (768px) ودسكتوب — كل صفحة (بما فيها الجداول والفورمات ولوحة الأدمن) قابلة للاستخدام بدون تمرير أفقي مزعج أو عناصر متراكبة على بعض.
---
## المرحلة 10 — طرق شحن رصيد حقيقية (PayPal / شام كاش / USDT ...)
اقرأ `FRONTEND.md` § 4 كامل قبل البدء هون. هذا تعديل على `WALLET.md` § 2.3 — يبقى نفس مبدأ "شحن يدوي بموافقة أدمن"، بس بقنوات محددة وتعليمات واضحة بدل حقل نص حر.
- [x] Migration: جدول `payment_methods` (`id`, `code` UNIQUE, `label_ar`, `instructions`, `destination`, `is_active`, `sort_order`, `created_at`, `updated_at`).
- [x] Migration/Seed: أضف قنوات ابتدائية على الأقل PayPal وشام كاش وUSDT (تعليمات placeholder، الأدمن يعبّي البيانات الحقيقية لاحقاً).
- [x] `GET /payment-methods` — القنوات النشطة فقط (يتطلب تسجيل دخول، أي مستخدم).
- [x] `GET /admin/payment-methods` — كل القنوات (نشطة وموقوفة).
- [x] `POST /admin/payment-methods` — إضافة قناة جديدة.
- [x] `PATCH /admin/payment-methods/:id` — تعديل/تفعيل/إيقاف قناة.
- [x] عدّل تحقق `POST /deposits` (`zod`): `method` لازم يطابق `code` قناة **نشطة** فعلياً، وإلا رفض فوري — لا تقبل أي نص حر بعد اليوم.
- [x] الواجهة: بدّل حقل "الطريقة" النصي بـ `<select>` معبّى من `GET /payment-methods` بالاسم العربي.
- [x] الواجهة: عند اختيار قناة، اعرض `instructions` و`destination` بصندوق بارز فوق حقل "مرجع التحويل"، وغيّر تسمية الحقل حسب القناة المختارة.
- [x] لوحة الأدمن: قسم جديد لإدارة قنوات الدفع (عرض/إضافة/تعديل/تفعيل-إيقاف) يستخدم مسارات `/admin/payment-methods`.
- [x] أعد تشغيل اختبارات `WALLET.md` § 4 (`npm run test:wallet`) للتأكد إن تعديل التحقق على `method` ما كسر منطق المحفظة أو الـ UNIQUE على `tx_ref`.
**معيار القبول:** فورم شحن الرصيد يعرض قنوات حقيقية بدل حقل فاضي، اختيار أي قناة يعرض تعليماتها، وإرسال طلب بقناة موقوفة أو غير موجودة يُرفض من الباك-إند (جرّبها مباشرة بـ curl/Postman مو بس من الواجهة). الأدمن يقدر يضيف/يوقف قناة من لوحة الأدمن وتنعكس فوراً بفورم المستخدم.
---
## المرحلة 11 — إغلاق ثغرات أمان لُقطت بفحص شامل (audit مباشر على السيرفر الحقيقي)
- [x] `PATCH /admin/users/:id`: امنع الأدمن من حظر حسابه هو نفسه (`req.params.id == req.user.id` و`status=banned` → ارفض بـ 400).
- [x] `PATCH /admin/users/:id`: امنع حظر آخر أدمن نشط متبقي بالنظام (عدّ `role=admin AND status=active` قبل التنفيذ، إذا صار العدد صفر بعد الحظر ارفض العملية).
- [x] `src/app.js`: قيّد `cors()` بدومين الموقع الحقيقي عبر متغير بيئة جديد (`CORS_ORIGIN`)، بدل السماح لأي دومين افتراضياً. خلّي القيمة الافتراضية بالتطوير مفتوحة بس بالإنتاج (`NODE_ENV=production`) إجباري تحديد الدومين، وإلا ارفض الإقلاع (زي `validateEnv`).
- [x] `src/routes/auth.routes.js`: أضف rate limiter لـ `POST /auth/forgot-password` (مثلاً 5 محاولات/15 دقيقة لكل IP، بنفس نمط `loginLimiter`).
- [x] `src/routes/deposit.routes.js`: أضف rate limiter على `POST /deposits` (مثلاً 10 طلبات/15 دقيقة لكل مستخدم) لمنع إغراق قائمة الأدمن.
- [x] (تحسين تجربة إضافي، اختياري بس مستحسن): بعد نجاح "حظر/تنشيط" من لوحة الأدمن، خلّي رسالة النجاح توضح التغيير فعلياً (مثال: "تم حظر المستخدم فلان" بدل رسالة عامة) عشان يكون واضح للأدمن إنه الإجراء نفّذ فعلاً.
**معيار القبول:** محاولة أدمن حظر نفسه أو آخر أدمن متبقي تُرفض برسالة واضحة. تشغيل السيرفر بـ `NODE_ENV=production` بدون `CORS_ORIGIN` يفشل بالإقلاع (زي باقي متغيرات الإنتاج الإلزامية). تكرار `POST /auth/forgot-password` أو `POST /deposits` أكتر من الحد المسموح يرجّع `429`. شغّل `npm test` بعدها للتأكد ما انكسر شي بالمسارات الموجودة.
---
## قبل ما تعتبر المشروع "جاهز"
- [x] راجع القرارات المفتوحة بـ `AGENTS.md` § 11 — لازم تكون كلها محسومة (مو افتراضات).
- [x] راجع كل بند بـ `AGENTS.md` § 8 (الأمان) وتأكد إنه مطبّق فعلياً، وحدة وحدة.
- [x] شغّل كل اختبارات المحفظة من المرحلة 3 مرة أخيرة بعد ربط كل الأجزاء مع بعض.
- [x] اعمل جولة يدوية كاملة (end-to-end) بحساب تجريبي: تسجيل → شحن → شراء → فشل متعمد (مزوّد وهمي يرجع خطأ) → تأكد الاسترجاع صحيح.
- [x] شغّل اختبارات المحفظة مرة أخيرة بعد المرحلة 10 (تعديل تحقق `method`) للتأكد إنه ما كسر شي.
- [x] جرّب الموقع فعلياً بثلاث أحجام شاشة (موبايل/تابلت/دسكتوب) بعد المرحلة 9 وتأكد ما في عناصر متكسرة أو نص مقطوع.
- [x] جرّب تغيير اللون الأساسي، أعد تحميل الصفحة، وتأكد إنه محفوظ.
- [x] جرّب طلب شحن بقناة PayPal وقناة شام كاش من الواجهة، وتأكد التعليمات تظهر صح لكل وحدة.