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>
60 lines
1.4 KiB
Python
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
|