feat(org): platform invites, roles, org sharing (v2 phase A1-1)

- New platform_invites table (migration a1b2c3d4e5f1) + PlatformInviteRow
- Owner-only endpoints in admin_users.py: issue/list/revoke platform invites
- invites.py: two-kind resolution (org + platform), email-supplied accept path
- auth.py /me gains needs_onboarding (manager, 0 orgs, not owner)
- organizations.py: transfer-ownership, leave (last owner guard), list/revoke org invites
- contracts + web UI: admin/users invite modal, accept-invite platform variant, onboarding gating
- Tests: test_org_platform_invites.py (18), org-sharing.test.tsx; report under docs/plan/reports

Note: (app)/settings "اشتراک‌گذاری سازمان" section (1.10) remains presentational work;
endpoints are done and tested.
This commit is contained in:
mjalmoudy94
2026-10-14 09:14:09 +03:30
parent 3c8296e354
commit 5095a29df9
20 changed files with 1933 additions and 37 deletions
+35 -1
View File
@@ -221,8 +221,16 @@ export interface Invite {
invite_url: string;
}
/** What the accept-invite screen shows before the invitee commits. */
/**
* What the accept-invite screen shows before the invitee commits.
*
* Two kinds share one endpoint: a `platform` invite mints a new manager account
* (v2 D12) and has no organization yet, while an `organization` invite joins an
* existing one. The screen shows a different title for each — "ساخت حساب مدیر"
* versus joining a team — so the caller has to be able to tell them apart.
*/
export interface InvitePreview {
kind: "platform" | "organization";
organization_id: string;
organization_name: string;
email: string;
@@ -235,9 +243,12 @@ export interface InvitePreview {
* in first; setting a password anonymously is only for brand-new accounts.
*/
has_account: boolean;
/** For a platform invite: the role the redeemed account will receive. */
platform_role: PlatformRole;
}
export interface AcceptInviteResult {
kind: "platform" | "organization";
organization_id: string;
organization_name: string;
role: Role;
@@ -252,6 +263,29 @@ export interface TokenResponse {
export interface MeResponse {
user: User;
organizations: OrganizationWithRole[];
/**
* True for a manager who belongs to no organization yet — they arrived on the
* platform owner's invitation link and have built nothing, so the only useful
* thing the UI can do is open the creation wizard. The owner is excluded.
*/
needs_onboarding: boolean;
}
/** An invitation the platform owner issued, as it appears in the console. */
export interface PlatformInvite {
id: string;
email: string | null;
platform_role: string;
note: string;
expires_at: string;
created_at: string;
created_by_email: string;
}
export interface IssuedPlatformInvite {
invite: PlatformInvite;
/** The raw URL. Shown exactly once, at creation. */
invite_url: string;
}
export interface Project {