ساختار مالکیت پلتفرم: یک مالک، مدیرانِ ساختهٔ مالک، سازمان‌های مدیریت‌شده

- users.is_platform_owner → users.platform_role (owner|manager|user) با مهاجرت
  d4e5f6a7b8c9 و backfill. ایندکس یکتای جزئی uq_single_platform_owner تضمین
  می‌کند دقیقاً یک مالک وجود داشته باشد — در سطح دیتابیس، نه فقط در کد.
- ثبت‌نام عمومی حذف شد (POST /auth/signup). حساب‌ها فقط با POST /admin/users
  (فقط مالک، با رمز موقت یک‌بارمصرف) یا با پذیرش لینک دعوت ساخته می‌شوند.
  در مسیر دعوت، اگر حساب وجود نداشته باشد خودِ دعوت‌شده رمز تعیین می‌کند؛ اگر
  وجود داشته باشد لینک هرگز جای reset رمز را نمی‌گیرد.
- ساخت سازمان فقط برای مالک و مدیران؛ کاربر عادی با دعوت عضو می‌شود.
- تنزل یا حذف مدیر، سازمان‌هایش را به مالک سامانه منتقل می‌کند (مالکیت + عضویت،
  با audit). حساب مالک از API تغییرناپذیر است؛ جابه‌جایی صندلی فقط با
  POST /admin/transfer-ownership و در یک تراکنش انجام می‌شود.
- افزودن POST /auth/change-password برای تبدیل رمز موقت به رمز خصوصی.
- فرانت: صفحهٔ /admin/users، صفحهٔ /account، بازنویسی /accept-invite، حذف /signup،
  گارد /onboarding برای کاربر عادی، و نشان نقش پلتفرم در پوستهٔ اپ.
- PRAGMA foreign_keys=ON برای SQLite تا ON DELETE CASCADE در dev و prod یکسان
  رفتار کند (پیش‌تر حذف کاربر، عضویت‌هایش را یتیم می‌گذاشت).
- تست: ۳۰۰ تست بک‌اند (شامل tests/test_platform_hierarchy.py با ۲۲ تست) و ۶۴ تست
  فرانت‌اند؛ ruff، mypy، tsc، eslint و build تمیز.
This commit is contained in:
mjalmoudy94
2026-10-13 21:58:41 +03:30
parent 6937b88ca7
commit 8eb631aedf
41 changed files with 2952 additions and 454 deletions
+11 -5
View File
@@ -162,15 +162,19 @@ CORS را تنظیم می‌کند.
Next.js App Router با دو ناحیهٔ مسیر:
**ناحیهٔ عمومی (بدون احراز هویت):** `/login`, `/signup`, `/onboarding`
**ناحیهٔ عمومی (بدون احراز هویت):** `/login`, `/accept-invite`, `/onboarding`
> `/signup` وجود ندارد: ثبت‌نام عمومی بسته است و حساب‌ها فقط توسط مالک سامانه یا
> با پذیرش لینک دعوت ساخته می‌شوند.
**ناحیهٔ اپلیکیشن (`(app)` — نیازمند نشست):**
`/dashboard`, `/workforce`, `/workforce/[id]`, `/manager`, `/tasks`,
`/tasks/[id]`, `/projects`, `/projects/[id]`, `/knowledge`, `/billing`,
`/settings`, `/traces`
**ناحیهٔ مالک سامانه:** `/admin`, `/admin/providers`, `/admin/models`,
`/admin/routing`, `/admin/rate-cards`, `/admin/organizations`, `/admin/errors`
**ناحیهٔ مالک سامانه:** `/admin`, `/admin/users`, `/admin/agy-accounts`,
`/admin/models`, `/admin/routing`, `/admin/rate-cards`, `/admin/organizations`,
`/admin/usage`, `/admin/errors`
**دو نقطهٔ اتصال مرکزی:**
@@ -236,11 +240,13 @@ storage/artifact، tool call) یک رکورد در ledger می‌گذارد با
## ۹. تست
**بک‌اند (`apps/api/tests/`)** — ۲۱۶ تست:
**بک‌اند (`apps/api/tests/`)** — ۳۰۰ تست:
| فایل | چه چیزی را می‌سنجد |
| --- | --- |
| `test_auth.py` | ثبت‌نام، ورود، نشست، نشت‌نداشتن hash |
| `test_auth.py` | ورود، نشست، تغییر رمز، بسته‌بودن ثبت‌نام، نشت‌نداشتن hash |
| `test_platform_hierarchy.py` | یکتایی مالک، گارد ساخت سازمان، ساخت مدیر توسط مالک، انتقال سازمان هنگام تنزل/حذف مدیر، انتقال مالکیت |
| `test_invites.py` | دعوت، پیش‌نمایش ناشناس، ساخت حساب از لینک، رد reset رمز با لینک |
| `test_tenant_isolation.py` | جداسازی مستأجر، ۴۰۴ به‌جای ۴۰۳، دعوت |
| `test_admin_control_plane.py` | دسترسی مالک سامانه، mask شدن کلید |
| `test_platform_rules.py` | برچسب دامنه، ضدتزریق host، ضدSSRF، پورت‌ها |
+35 -3
View File
@@ -34,7 +34,7 @@
- **چگونه:** argon2-cffi (برندهٔ PHC، مقاوم به GPU)؛ هیچ‌جا `md5`/`sha1`/`sha256`
خام برای رمز استفاده نشده است.
- **تست مدافع:**
- `test_signup_never_returns_password_hash` — پاسخ ثبت‌نام هیچ `password_hash` ندارد.
- `test_signup_helper_never_returns_password_hash` — هیچ پاسخی `password_hash` ندارد.
- `test_login_fails_with_wrong_password` — تأیید مسیر verify.
### ✅ JWT secret از env می‌آید و در production اعتبارسنجی می‌شود
@@ -120,9 +120,41 @@
### ✅ تست منفی برای unauthorized / forbidden
- **محل:** `tests/test_admin_control_plane.py`، `tests/test_auth.py`،
`tests/test_tenant_isolation.py`.
`tests/test_tenant_isolation.py`، `tests/test_platform_hierarchy.py`.
- **تست مدافع:** ۴ تست ۴۰۱ در `test_auth.py`، ۳ تست ۴۰۳/۴۰۴ در
`test_admin_control_plane.py`، ۶ تست cross-tenant در `test_tenant_isolation.py`.
`test_admin_control_plane.py`، ۶ تست cross-tenant در `test_tenant_isolation.py`،
و کل `test_platform_hierarchy.py` (۲۲ تست).
### ✅ سلسله‌مراتب پلتفرم: یک مالک، مدیرانِ ساختهٔ مالک
- **محل:** `apps/api/app/domain/identity/models.py` (`PlatformRole`)،
`apps/api/app/infrastructure/db/models.py` (ایندکس یکتا)،
`apps/api/app/interfaces/http/routers/admin_users.py`،
`apps/api/app/application/services/platform_users.py`.
- **چگونه:** چهار قاعده در کد و دیتابیس قفل شده‌اند:
1. **دقیقاً یک مالک.** ایندکس یکتای جزئی
(`uq_single_platform_owner ... WHERE platform_role = 'owner'`) در سطح دیتابیس
جلوی مالک دوم را می‌گیرد؛ API هم ساخت/تنزل مالک را رد می‌کند. جابه‌جایی صندلی
فقط با `POST /admin/transfer-ownership` و در یک تراکنش است (اول تنزل مالک فعلی،
بعد ارتقای جانشین — تا ایندکس هرگز دو مالک نبیند).
2. **ثبت‌نام عمومی وجود ندارد.** حساب‌ها فقط از `POST /admin/users` (مالک) یا از
پذیرش دعوت ساخته می‌شوند. مسیرهای `/api/invites/*` تنها فراخوانی‌های ناشناس
مجاز در لایهٔ RPC هستند (`router.py`)، چون خود توکن دعوت مجوز است.
3. **لینک دعوت هرگز reset رمز نمی‌شود.** اگر برای ایمیل دعوت‌شده حساب وجود داشته
باشد، پذیرش ناشناس با رمز `403` می‌گیرد و کاربر باید اول وارد شود
(`has_account` در preview به UI می‌گوید کدام مسیر است).
4. **سازمان‌های مدیرِ رفته به مالک می‌رسند.** تنزل یا حذف مدیر،
`owner_user_id` و عضویت‌ها را جابه‌جا می‌کند؛ داده هرگز یتیم یا حذف نمی‌شود.
- **تست مدافع:** `tests/test_platform_hierarchy.py`:
- `test_only_one_platform_owner_may_exist` — درج مستقیم مالک دوم `IntegrityError`.
- `test_owner_cannot_be_created_through_the_api` / `test_owner_account_is_immutable_from_the_api`
- `test_transfer_ownership_moves_the_seat_and_keeps_exactly_one`
- `test_plain_user_cannot_create_an_organization`
- `test_manager_cannot_create_accounts` / `test_manager_cannot_reach_the_admin_console`
- `test_demoting_a_manager_transfers_organizations_to_the_owner`
- `test_deleting_a_manager_transfers_organizations_and_memberships`
- `test_a_link_cannot_reset_an_existing_account` (در `test_invites.py`)
- `test_every_seat_move_is_audited`
### ✅ کاربر عادی مدل/provider خام نمی‌بیند