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

- 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
+81
View File
@@ -10,6 +10,30 @@ export type Role = "owner" | "admin" | "manager" | "member" | "viewer";
export type MembershipStatus = "active" | "invited" | "suspended";
/**
* Platform-wide role — a different axis from the in-organization `Role`.
*
* - `owner` — the single owner of the whole system. Exactly one exists; only
* they may enter the admin console, create or demote managers, and move the
* owner seat.
* - `manager` — an account the owner creates. May build its own organizations
* and join other managers' organizations by invitation.
* - `user` — a plain account. Cannot create organizations; works only inside
* organizations it was invited to.
*/
export type PlatformRole = "owner" | "manager" | "user";
/** Roles that may create an organization. */
export function canCreateOrganization(role: PlatformRole | undefined | null): boolean {
return role === "owner" || role === "manager";
}
export const PLATFORM_ROLE_LABELS: Record<PlatformRole, string> = {
owner: "مالک سامانه",
manager: "مدیر",
user: "کاربر",
};
/**
* Provider kinds the AI Control Plane can talk to. agy (Google Antigravity)
* is the only one — the platform no longer has a provider picker.
@@ -160,6 +184,10 @@ export interface User {
email: string;
name: string;
avatar_url?: string | null;
/** Platform-wide role. Exactly one account in the system is `owner`. */
platform_role: PlatformRole;
/** True for the manager tier (may create organizations). */
is_manager: boolean;
is_platform_owner: boolean;
}
@@ -202,6 +230,11 @@ export interface InvitePreview {
expires_at: string;
/** True when the signed-in user is already a member of that organization. */
already_member: boolean;
/**
* True when an account exists for the invited email. The invitee then logs
* in first; setting a password anonymously is only for brand-new accounts.
*/
has_account: boolean;
}
export interface AcceptInviteResult {
@@ -305,6 +338,54 @@ export interface AdminUsageRow {
total_price: number;
}
/* Account administration (platform owner only) */
/** One row in the owner's account list. */
export interface AdminUser {
id: string;
email: string;
name: string;
platform_role: PlatformRole;
is_manager: boolean;
is_platform_owner: boolean;
is_active: boolean;
created_at: string;
/** How many organizations this account owns — what moves if it is demoted. */
owned_organizations: number;
}
/**
* A created account (or a password reset). `temp_password` is shown exactly
* once and never retrievable again.
*/
export interface AdminUserCreated {
user: AdminUser;
temp_password: string;
}
export interface CreateUserRequest {
email: string;
name?: string;
/** `owner` is refused: the seat moves only through transfer-ownership. */
platform_role: "manager" | "user";
}
export interface UpdateUserRequest {
platform_role?: "manager" | "user";
is_active?: boolean;
}
export interface OrganizationTransfer {
organization_id: string;
organization_name: string;
}
export interface DeleteUserResult {
deleted_user_id: string;
/** Organizations that moved to the platform owner. */
transferred_organizations: OrganizationTransfer[];
}
export interface PreviewDomain {
id: string;
host: string;