إنتقل إلى المحتوى الرئيسي

بنية العروض التجريبية الخاصة

القرار المعماري

العرض ليس نسخة كاملة جديدة من المنصة ولا حساباً مشتركاً دائماً. هو tenant مؤقت داخل قاعدة الإنتاج يحمل is_demo=true ووقت انتهاء إلزامياً، وتحميه طبقات متراكبة من capability access وRLS والتحقق على مستوى API.

هذا يحقق تجربة واقعية ومخصصة دون تكلفة نشر stack مستقل لكل عميل محتمل، مع بقاء الإلغاء والتنظيف مركزيين.

تدفق الوصول

  1. ينشئ مشرف يملك demos.manage رمزاً عشوائياً base64url عالي العشوائية.
  2. ترسل لوحة الإدارة SHA-256 فقط إلى create_demo_tenant، ثم تنشئ هويات المدير والمسافر والسائق.
  3. تُشتق كلمات مرور الشخصيات بـHMAC من الرمز وDEMO_ACCESS_SECRET؛ لا تخزن كلمات المرور الخام.
  4. يوضع الرمز في URL fragment (#access=...) حتى لا يظهر في طلب الصفحة أو سجل Traefik.
  5. تقرأ صفحة bootstrap الجزء بعد اكتمال hydration، وتبقيه حتى يقبل الخادم capability، ثم تحذفه من شريط العنوان عبر history.replaceState. يمنع ذلك أن تفقد إعادة تركيب React للـeffect الرمز قبل اكتمال التحقق.
  6. يتحقق الخادم من البصمة، الشركة، الإلغاء، ووقت الانتهاء، ثم يصدر جلسة الشخصية المطلوبة. في تجربة المسافر تُثبت الجلسة في cookies من نوع HttpOnly فقط، وتُعاد ملاحة الصفحة كاملة حتى يتحقق CustomerAuthProvider منها من الخادم؛ لا تعاد رموز Supabase في JSON ولا تدخل JavaScript.
  7. يعيد proxy الخاص بنطاق العرض التحقق من المنحة لكل طلب لاحق، بينما تفرض RLS نفس حدود الشركة على الاستعلامات المباشرة.

يرجع bootstrap رمز مجال ثابتاً مثل DEMO_ACCESS_INVALID أو DEMO_PERSONAS_INCOMPLETE عند الفشل. تستخدم الواجهة الرمز لاختيار نص عربي أو إنجليزي، ولا تعرض رسالة الاستثناء الخام. تحمل demo_config رسالة ترحيب عربية وأخرى إنجليزية، بينما يحفظ اسم الشركة في name_ar وname_en.

يعرض /demo shell مستقلاً وثابتاً في أول render للخادم والمتصفح. لا يركب غلاف تنقل العميل ولا CustomerAuthProvider قبل إنشاء جلسة المسافر، لذلك لا تنتج صفحة التسليم طلبات /api/auth/customer/* مجهولة أو نصاً مختلفاً أثناء hydration.

طبقات العزل

الطبقةالضمان
DNS وTraefikنطاقات عرض مستقلة، TLS، وترويسات noindex وno-store
مخبأ المتصفحService Worker لا يخزن HTML أو RSC أو استجابات API الخاصة؛ يقتصر على الأصول العامة الثابة
حماية السعةTraefik يحد bursts حسب IP، وبحث الرحلات يطبق حداً موزعاً، بينما يملك service role الداخلي ميزانية Kong منفصلة عن anon
تهيئة العرضTOTP وصلاحية demos.manage، مع 10 عمليات لكل مشرف وIP خلال 15 دقيقة
تبادل الشخصية30 محاولة لكل IP خلال 15 دقيقة حتى لا يحجب فتح الشخصيات الثلاث شبكة مكتب كاملة
capability43 محرفاً تقريباً من entropy عشوائي؛ يفتح bootstrap الشخصيات فقط ولا يمنح مخزون الرحلات
جلسة الشخصيةJWT موثق يحمل demo_company_id مطابقاً للـcapability؛ لا يفتح tenant آخر أو بيانات حقيقية
APIالبحث يحتفظ بجلسة المتصل ويختار مجالاً واحداً فقط: public أو internal أو demo مطابق
RLScan_access_tenant يتطلب tenant مطابقاً ونشطاً وغير منتهٍ للعروض فقط
التجميع والمخبأالمسارات الشائعة تجمع المجال المطابق فقط، ولا تخزن نتائج internal/demo في مخبأ عام
التسويق وSEOlanding routes وsitemap وملفات الشركات ومراجعاتها تقرأ شركات الإنتاج العامة فقط
أوامر الحجزJourney quote وقفل المقاعد والحجز القديم تعيد «غير موجود» قبل لمس مخزون tenant غير مطابق
الآثار الخارجيةعناوين .invalid وأرقام +963900… وصندوق demo_outbox بدل الإرسال الحقيقي
دورة الحياةالإلغاء/الانتهاء يمنعان الوصول فوراً، والتنظيف يحذف البيانات وهويات Auth

الحساب العام أو التشغيلي العادي يستطيع الوصول إلى شركة إنتاج حقيقية وفق العضوية والسياسات السابقة. أما هوية INTERNAL أو DEMO الصالحة فهي محدد حصري لعالم المخزون وليست صلاحية إضافية فوق الإنتاج العام؛ لذلك يرفض can_access_tenant عبور أي منها إلى شركة إنتاج حقيقية أيضاً.

فصل بيانات الإنتاج الداخلي

شركة QA الإنتاجية ليست عرضاً مؤقتاً، لذلك تحمل is_internal=true بدلاً من is_demo. لا يمكن للحسابات العادية أو المجهولة اكتشاف رحلاتها أو فتحها أو حجز مقاعدها. تمنح البذرة الحسابات الداخلية علامة موثوقة في app_metadata، ولا تعتمد صلاحية الخادم على user_metadata القابل لتعديل المسافر. يمنع trigger الأدوار العميلة من تغيير is_internal، ولا يسمح بجمع is_internal وis_demo على الشركة نفسها. عند ترحيل هويات QA القديمة لا تكفي علامة user_metadata وحدها؛ يشترط backfill سجلاً موثوقاً في إدارة المنصة أو عضوية الشركة أو السائق أو حجزاً مرتبطاً بالشركة الداخلية قبل كتابة app_metadata.

يطبق search_journey_options مجالاً واحداً لكل طلب:

  • PUBLIC: شركات الإنتاج الحقيقية النشطة والمنشورة وغير الموقوفة فقط؛
  • INTERNAL: شركات QA الداخلية النشطة والمنشورة وغير الموقوفة فقط؛
  • DEMO: شركة العرض النشطة والمنشورة والمطابقة في JWT فقط، بشرط عدم انتهاء صلاحيتها.

رابط العرض ليس بديلاً عن حساب الشخصية. قبل قراءة أي Trip يجب أن تنشئ عملية passenger_session جلسة المسافر الموقعة، وأن يطابق demo_company_id في JWT الشركة التي قيدها رابط العرض. الرابط وحده أو حساب من مجال آخر يعيدان 401.

تطبق أوامر quote وقفل المقاعد والحجز المجال نفسه حتى لا يتحول تخمين UUID إلى تجاوز لمسار البحث. تستبعد عروض public_companies وpublic_trips والمراجعات والشهادات كلا المجالين الخاصين من الإحصاءات والتسويق. تطبق popular_routes_aggregate المجال نفسه قبل التجميع، ويمنع API التخزين المشترك لنتائج INTERNAL وDEMO كي لا تتحول طبقة CDN إلى قناة تسريب بين المجالات. كما تبقى مسارات landing وsitemap وملف الشركة العام ومراجعات الشركة إسقاطات عامة حصراً للشركات النشطة والمنشورة وغير الموقوفة؛ لا تتحول إلى استجابات خاصة عند وجود cookie، ولا تنشر معرفات شركات QA أو العرض حتى عند تخمين المعرّف أو الـslug.

البيانات والمقياس

يبني seed_demo_company_world عالماً صغيراً ثابت الحجم: 4 حافلات، حتى 12 مساراً، 264 رحلة، 12 مسافراً، 100 حجز، وبيانات دعم ورسائل محدودة. ينشئ trigger مقيد بالشركات التجريبية مخطط bus_layouts.layout_document قياسياً لكل حافلة بحيث يساوي عدد عناصر المقاعد سعتها، ولا يمس حافلات الشركات الحقيقية. يمنع demo_tenant_cap() وجود أكثر من 50 tenant تجريبياً فعالاً، وتستخدم عملية الإنشاء advisory lock لمنع تجاوز السقف عند الطلبات المتزامنة.

إعادة الضبط تقفل tenant داخل المعاملة قبل الحذف وإعادة البناء، ويعيد trigger إنشاء مخططات المقاعد تلقائياً مع الحافلات الجديدة. تحذف الدالة الخاصة purge_demo_tenant_operational_world عالم التشغيل والـJourney والتذاكر والحضور والمالية والتسويات والجودة والإشعارات بترتيب الاعتماديات، لكنها ترفض أي Journey أو Journey Order متصل بـtenant آخر. لا يملك anon أو authenticated أو service_role صلاحية استدعائها مباشرة؛ تستدعيها فقط أوامر reset/reaper المصرح بها. يتطلب أي bypass لحماية السجل immutable علامة transaction-local مع تحقق مستقل أن الشركة is_demo=true. مهمة الحصاد تستخدم FOR UPDATE SKIP LOCKED حتى تستطيع أكثر من نسخة صيانة العمل بأمان دون حذف مزدوج.

يثبت اختبار الإنتاج المحمي tests/e2e/production/demo-lifecycle.spec.ts هذه الحدود عبر دورة مؤقتة كاملة: تهيئة مخصصة، عزل مجهول، تبادل الشخصيات الثلاث، عدم إعادة capability في قائمة الإدارة، عدم وجود أخطاء hydration أو console في مركز العرض، إلغاء، حصاد، وتنظيف احتياطي. الاختبار دليل على هذا المسار المحدد ولا يحول العرض إلى ضمان شامل لكل ميزة داخل الشخصيات.

حدود الثقة

  • SUPABASE_SERVICE_ROLE_KEY يبقى داخل خوادم Next.js ومهمة الصيانة فقط.
  • DEMO_ACCESS_SECRET بطول 32 محرفاً على الأقل، مستقل عن JWT وservice-role وCRON_SECRET.
  • CRON_SECRET يحمي /api/maintenance ويُرسل كـBearer من حاوية الصيانة الداخلية.
  • لا يجوز تفعيل SHAMBUS_DEMO_ENVIRONMENT=true على stack الإنتاج المشترك؛ الأمان لكل tenant يعتمد على الهوية التجريبية والعناوين المحجوزة.
  • نطاق web-api.shambus.com يعرّض مسارات /api/ فقط لتطبيقات الجوال، حتى تبقى صفحة النطاق العام في وضع «قريباً» دون كسر واجهات API.

الخصوصية والمراقبة

تُسجل عمليات الإنشاء والتمديد وإعادة الضبط والتدوير والإلغاء في سجل الإدارة. تخزن منحة الوصول اسم جهة الاتصال وبريدها الاختياريين فقط، وتُحذف مع tenant بعد الانتهاء. لا تضع بيانات عميل حقيقية داخل بيانات العرض.

للتشغيل اليومي راجع دليل دورة حياة العروض.