TorbatYar/docs/services_contracts.md
Mortezakoohjani 800b0ba2c5 Deploy TorbatYar for torbatyar.ir with nginx multi-tenant routing.
Wire production domain, CORS for tenant subdomains, celery volume mounts, and nginx reverse proxy configs for apex, API, identity, auth, and wildcard tenants.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 21:43:33 +03:30

13 KiB
Raw Blame History

قراردادهای ارتباطی سرویس‌ها (Service Contracts)

۱. قوانین بنیادی

  1. ارتباط مستقیم بین دیتابیس سرویس‌ها ممنوع است. هیچ سرویسی نباید به دیتابیس سرویس دیگر کوئری بزند.
  2. هر سرویس فقط دیتابیس خودش را می‌شناسد (Database-per-service).
  3. ارتباط بین سرویس‌ها فقط از این چهار راه:
    • REST API (همزمان)
    • Webhook (اعلان به بیرون)
    • Async Event (message bus)
    • Outbox/Inbox Pattern (تحویل قابل اعتماد رویداد)
  4. همه درخواست‌های tenant-aware باید tenant_id را حمل کنند (هدر X-Tenant-ID یا داخل توکن/رویداد).

۲. احراز هویت بین سرویس‌ها

  • سرویس‌ها با Internal Service Token یکدیگر را احراز می‌کنند.
  • توکن به‌صورت hash در internal_service_tokens ذخیره می‌شود (خود توکن ذخیره نمی‌شود).
  • هر توکن دارای scopes است؛ سرویس فراخوان باید scope لازم را داشته باشد.
  • کاربر نهایی با JWT کیکلوک احراز می‌شود.

۳. قرارداد رویداد (Event Envelope)

همه رویدادها با ساختار مشترک shared.events.EventEnvelope منتشر می‌شوند:

{
  "event_id": "uuid",
  "event_type": "tenant.created",
  "aggregate_type": "tenant",
  "aggregate_id": "uuid",
  "tenant_id": "uuid|null",
  "source_service": "core-service",
  "payload": { },
  "occurred_at": "ISO-8601"
}

جریان Outbox → Inbox

  1. سرویس مبدأ رویداد را در outbox_events (در همان تراکنش) ذخیره می‌کند.
  2. worker رویدادهای pending را منتشر و processed می‌کند.
  3. سرویس مقصد رویداد را در inbox_events ثبت می‌کند (بر اساس event_id برای idempotency) و سپس پردازش می‌کند.

رویدادهای شناخته‌شده Core (نمونه)

event_type aggregate توضیح
tenant.created tenant ساخت مستأجر جدید
tenant.suspended tenant تعلیق مستأجر
tenant.activated tenant فعال‌سازی مستأجر
domain.created domain افزودن دامنه
subscription.created subscription ایجاد اشتراک
subscription.updated subscription تغییر اشتراک
feature_access.changed feature_access تغییر دسترسی قابلیت

۴. قرارداد Entitlement (بررسی دسترسی)

سرویس‌های دیگر پیش از اجرای یک قابلیت، دسترسی را از Core استعلام می‌کنند:

POST /api/v1/tenants/{tenant_id}/features/check
{ "feature_key": "accounting.invoice.create" }

→ { "tenant_id": "...", "feature_key": "...", "has_access": true, "reason": "plan_enabled" }

قرارداد نام‌گذاری feature_key: {service_key}.{resource}.{action} مثال: crm.lead.create, ecommerce.product.create.

۵. APIهای Core Platform (فاز ۱)

گروه متد و مسیر
Health GET /health
Tenants POST/GET /api/v1/tenants، GET/PATCH /api/v1/tenants/{id}، POST .../suspend، POST .../activate
Domains POST/GET /api/v1/tenants/{id}/domains، POST /api/v1/domains/resolve
Plans & Features POST/GET /api/v1/plans، POST/GET /api/v1/features، POST /api/v1/plans/{id}/features
Subscription POST/GET /api/v1/tenants/{id}/subscription، POST /api/v1/tenants/{id}/features/check
Service Registry POST/GET /api/v1/services، PATCH /api/v1/services/{id}/status

۶. قالب پاسخ استاندارد

  • خطاها با قالب shared.responses.ErrorResponse برگردانده می‌شوند:
{ "success": false, "error": { "code": "not_found", "message": "...", "details": null } }
  • لیست‌ها با قالب صفحه‌بندی Page (شامل items و meta).

۷. Onboarding و Tenant Context (فاز ۴ — Tenant Onboarding و Workspace Activation)

در بریف پروژه این مجموعه API با عنوان «Phase 3» شناخته می‌شود؛ در شماره‌گذاری داخلی مستندات فاز ۴ است (نگاه کنید به progress.md).

همهٔ endpointهای این بخش زیر API_V1_PREFIX (پیش‌فرض /api/v1) هستند و به احراز هویت نیاز دارند (Authorization: Bearer <token> — چه JWT محلی OTP و چه JWT کیکلوک SSO؛ هر دو با get_current_core_user resolve می‌شوند).

GET /api/v1/me

وضعیت کاربر جاری، عضویت‌ها و نیاز به onboarding.

پاسخ ۲۰۰:

{
  "user_id": "uuid",
  "mobile": "09xxxxxxxxx",
  "email": null,
  "platform_role": "user",
  "onboarding_required": true,
  "current_tenant_id": null,
  "memberships": [
    {
      "tenant_id": "uuid", "tenant_name": "...", "tenant_slug": "...",
      "tenant_status": "active", "onboarding_completed": true,
      "role": "tenant_owner", "status": "active", "is_owner": true
    }
  ]
}

onboarding_required=true اگر کاربر هیچ عضویتی نداشته باشد یا tenant جاری هنوز onboarding_completed=false باشد.

خطاها: 401 unauthorized (بدون توکن)، 403 forbidden (JWT کیکلوک بدون لینک به رکورد Core — نگاه کنید به architecture.md).

GET /api/v1/me/tenants

آرایه‌ای از MembershipSummary (همان ساختار memberships بالا) برای همهٔ tenantهای در دسترس کاربر جاری.

POST /api/v1/onboarding/tenant

ساخت tenant جدید توسط کاربر جاری (بدون نیاز به عضویت قبلی).

بدنه درخواست:

{
  "business_name": "کافه تربت",
  "slug": "cafe-torbat",
  "business_type": "cafe",
  "default_locale": "fa-IR",
  "timezone": "Asia/Tehran"
}

business_name و slug الزامی؛ بقیه اختیاری (پیش‌فرض‌ها بالا). slug باید فقط حروف کوچک انگلیسی/اعداد/خط‌تیره باشد.

رفتار: ساخت tenant با status=pending_activation → ساخت عضویت tenant_owner/is_owner=true برای کاربر جاری → اتصال پلن پیش‌فرض FREE با tenant_subscriptions.status=active → (در صورت تنظیم PLATFORM_BASE_DOMAIN) ساخت زیردامنهٔ {slug}.{base_domain} با verification_status=verified → اگر کاربر current_tenant_id نداشت، همین tenant به‌عنوان جاری تنظیم می‌شود.

پاسخ ۲۰۱: شیء TenantContextRead (نگاه کنید پایین).

خطاها: 409 slug_taken (slug تکراری در کل پلتفرم)، 401/403 مشابه بالا.

PATCH /api/v1/onboarding/tenant/{tenant_id}/branding

{ "primary_color": "#112233", "secondary_color": "#445566", "logo_url": null, "favicon_url": null }

همهٔ فیلدها اختیاری‌اند؛ فقط مقادیر ارسال‌شده به‌روزرسانی می‌شوند. نیازمند نقش tenant_owner/tenant_admin روی tenant مقصد (یا platform_admin). پاسخ ۲۰۰: TenantContextRead.

خطا: 403 forbidden (بدون نقش کافی).

PATCH /api/v1/onboarding/tenant/{tenant_id}/domain

{ "custom_domain": "cafe.example.com" }

اگر custom_domain ارسال شود، رکورد domains جدید با domain_type=custom_domain، is_primary=false، verification_status=pending ساخته می‌شود. پاسخ ۲۰۰: TenantContextRead.

خطاها: 409 domain_taken (دامنه تکراری در کل پلتفرم)، 403 forbidden.

POST /api/v1/onboarding/tenant/{tenant_id}/complete

بدون بدنه. اعتبارسنجی نهایی: نام/slug موجود باشد و حداقل یک owner فعال داشته باشد؛ در صورت موفقیت onboarding_completed=true و status=active می‌شود. پاسخ ۲۰۰: TenantContextRead.

خطاها: 422 onboarding_incomplete (نام/slug ناقص)، 422 owner_required (بدون owner فعال)، 403 forbidden (بدون نقش کافی).

GET /api/v1/tenant/current

tenant جاری کاربر را برمی‌گرداند (بر اساس current_tenant_id یا اولین عضویت در نبود آن). پاسخ ۲۰۰: TenantContextRead.

خطا: 404 not_found (کاربر هیچ عضویتی ندارد).

POST /api/v1/tenant/switch

{ "tenant_id": "uuid" }

current_tenant_id کاربر را به این tenant تغییر می‌دهد (فقط اگر عضویت active روی آن داشته باشد). پاسخ ۲۰۰: TenantContextRead.

خطا: 403 forbidden (عضو نیست یا عضویت غیرفعال).

ساختار مشترک TenantContextRead

{
  "tenant": { "id": "...", "name": "...", "slug": "...", "status": "active",
              "business_type": null, "default_locale": "fa-IR", "timezone": "Asia/Tehran",
              "primary_color": "#0284c7", "secondary_color": "#0f172a",
              "logo_url": null, "favicon_url": null, "onboarding_completed": true,
              "created_at": "...", "updated_at": "..." },
  "role": "tenant_owner",
  "is_owner": true,
  "plan_code": "FREE",
  "plan_name": "رایگان",
  "subscription_status": "active",
  "domains": [ { "id": "...", "tenant_id": "...", "domain": "cafe-torbat.example.com",
                 "domain_type": "subdomain", "is_verified": true, "verified_at": null,
                 "created_at": "..." } ],
  "primary_domain": "cafe-torbat.example.com"
}

۸. Identity & Access Service (فاز ۲)

Base URL: http://identity-access-service:8001 (Docker) یا http://localhost:8001

گروه متد و مسیر احراز هویت
Health GET /health عمومی
Auth Config GET /api/v1/auth/config عمومی
Login URL GET /api/v1/auth/login-url عمومی
Token Exchange POST /api/v1/auth/token عمومی (code)
Current User GET /api/v1/auth/me JWT
Register (SSO+mobile) POST /api/v1/auth/register عمومی
Verify register mobile POST /api/v1/auth/register/verify-mobile عمومی
Register mobile only POST /api/v1/auth/register/mobile عمومی
Complete mobile register POST /api/v1/auth/register/mobile/verify عمومی
Unified mobile start POST /api/v1/auth/mobile/start عمومی
Unified mobile complete POST /api/v1/auth/mobile/complete عمومی
Session handoff redeem POST /api/v1/auth/session/redeem عمومی (یکبارمصرف)
SSO mobile verify request POST /api/v1/auth/mobile/request JWT
SSO mobile verify POST /api/v1/auth/mobile/verify JWT
Users POST /api/v1/users platform_admin
Members POST/GET /api/v1/tenants/{id}/members platform_admin

جریان SSO (Frontend)

  1. GET /api/v1/auth/login-url → redirect به Keycloak
  2. Callback با codePOST /api/v1/auth/token
  3. GET /api/v1/auth/me → اگر requires_mobile_verification=true/auth/verify-mobile
  4. استفاده از access_token در هدر Authorization: Bearer ...

ورود مرکزی (Keycloak — تنها UX کاربر)

  1. همه لینک‌های «ورود» → GET /auth/login-url → Keycloak
  2. تب موبایل در Keycloak → mobile/start + mobile/complete?create_handoff=true
  3. GET /auth/callback?handoff=...POST /session/redeem → dashboard

ورود/ثبت‌نام یکپارچه (API)

  1. POST /auth/mobile/start با { mobile }{ intent: "login"|"register", expires_in }
  2. کاربر OTP را وارد می‌کند
  3. POST /auth/mobile/complete با { mobile, code, display_name?, email?, username?, password? }
    • اگر intent=login: فقط mobile + code
    • اگر intent=register: فیلدهای پروفایل الزامی (یا ثبت‌نام ساده بدون آن‌ها برای موبایل-only)
  4. پاسخ: AuthSessionResponse (access_token + refresh_token) → redirect به /dashboard

ثبت‌نام legacy

  • SSO + موبایل: POST /register → OTP → POST /register/verify-mobile
  • فقط موبایل: POST /register/mobile → OTP → POST /register/mobile/verify
  • Identity برای OTP به Core متصل می‌شود: POST core /api/v1/auth/otp/* با context=public

رویدادهای Identity

event_type توضیح
user.registered ثبت کاربر جدید
tenant_member.added افزودن عضو به tenant
tenant_member.removed حذف عضو از tenant

۹. Service Registry

سرویس‌های داخلی آینده باید خود را در service_registry ثبت کنند (service_key, base_url, health_check_url, status) تا discovery و health-check ممکن باشد.