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
+10 -1
View File
@@ -21,7 +21,7 @@ from app.core.security import (
verify_password,
)
from app.domain.audit.models import AuditAction
from app.domain.identity.models import User
from app.domain.identity.models import PlatformRole, User
from app.infrastructure.repositories.identity import (
MembershipRepository,
OrganizationRepository,
@@ -73,6 +73,11 @@ class TokenResponse(BaseModel):
class MeResponse(BaseModel):
user: UserOut
organizations: list[OrganizationSummary]
#: True when this account is a manager with no organization yet: they landed
#: on the platform owner's invitation link and built nothing, so the only
#: useful thing the UI can do is open the creation wizard. The owner is
#: deliberately excluded — they administer the platform rather than joining it.
needs_onboarding: bool = False
def _to_user_out(user: User) -> UserOut:
@@ -170,12 +175,16 @@ async def me(
session: AsyncSession = Depends(get_session),
) -> MeResponse:
orgs = await OrganizationRepository(session).list_for_user(user.id)
needs_onboarding = (
user.platform_role == PlatformRole.MANAGER and len(orgs) == 0 and not user.is_platform_owner
)
return MeResponse(
user=_to_user_out(user),
organizations=[
OrganizationSummary(id=org.id, name=org.name, slug=org.slug, role=role.value)
for org, role in orgs
],
needs_onboarding=needs_onboarding,
)