video-accessibility/backend/app/models/invitation.py
Vadym Samoilenko 00fb1aacc6 feat(saas): Phase 2 — invitation flow, email templates, MS SSO zero-membership
Backend:
- models/invitation.py — Invitation model + create/accept/preview schemas
- routes_invitations.py — org-scoped POST/GET/DELETE + public preview/accept endpoints
  Single-use token via find_one_and_update; sha256(token) stored in DB, plaintext in email URL
- emailer.py — _send() helper; send_invitation_email, send_welcome_email, send_password_reset_email
  send_completion_email refactored to use _send()
- migration_2026-04-28-000002 — creates invitations collection with TTL index (30d audit trail)
- routes_auth.py — new MS SSO users provisioned with zero memberships instead of role=PRODUCTION;
  they land on "no access" page until an admin invites them
- main.py — registers invitations_org_router and invitations_router

Frontend:
- routes/AcceptInvite.tsx — public page at /accept-invite?token=...
  Four states: new user (name+password), existing user (confirm), MS user, already-member
- App.tsx — /accept-invite route outside RequireAuth
- types/api.ts — Invitation, InvitationCreate, InvitationPreview, InvitationAcceptRequest/Response
- lib/api.ts — listInvitations, createInvitation, revokeInvitation, previewInvitation, acceptInvitation
- hooks/useClients.ts — useInvitations, useCreateInvitation, useRevokeInvitation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 16:52:08 +01:00

60 lines
1.4 KiB
Python

from datetime import datetime
from typing import Optional
from pydantic import BaseModel, EmailStr
from .organization import OrgRole
class Invitation(BaseModel):
id: Optional[str] = None
email: str
organization_id: str
role_in_org: OrgRole
target_team_ids: list[str] = []
token_hash: str
invited_by_user_id: str
expires_at: datetime
accepted_at: Optional[datetime] = None
revoked_at: Optional[datetime] = None
created_at: Optional[datetime] = None
class InvitationCreate(BaseModel):
email: EmailStr
role_in_org: OrgRole = OrgRole.MEMBER
target_team_ids: list[str] = []
expires_in_days: int = 7
class InvitationPreviewResponse(BaseModel):
org_name: str
org_slug: str
inviter_name: str
role_in_org: OrgRole
target_email: str
requires_password_setup: bool
requires_microsoft_signin: bool
is_existing_member: bool
class InvitationAcceptRequest(BaseModel):
token: str
full_name: Optional[str] = None
password: Optional[str] = None
ms_id_token: Optional[str] = None
class InvitationResponse(BaseModel):
id: str
email: str
organization_id: str
role_in_org: OrgRole
invited_by_user_id: str
expires_at: datetime
accepted_at: Optional[datetime] = None
revoked_at: Optional[datetime] = None
created_at: Optional[datetime] = None
is_expired: bool = False
is_accepted: bool = False
is_revoked: bool = False