""" Authentication Schemas Pydantic models for auth request/response payloads. """ import uuid from datetime import datetime from typing import Optional from pydantic import Field from src.shared.base import BaseSchema class AuthSyncRequest(BaseSchema): """ Request payload for POST /auth/sync The client sends this after obtaining an OIDC token from Authentik. The access_token is validated against Authentik's userinfo endpoint. """ access_token: str = Field( ..., description="OIDC access token from Authentik", ) class RoleSchema(BaseSchema): """Role information in domain.category:action format""" id: uuid.UUID = Field(..., description="Role ID") name: str = Field(..., description="Role name (e.g., 'control-room.general:admin')") domain: str = Field(..., description="Permission domain (e.g., 'control-room')") category: str = Field(default="general", description="Permission category (e.g., 'general')") action: str = Field(..., description="Permission action (e.g., 'admin')") class UserPreferencesSchema(BaseSchema): """User preferences""" theme: str = Field(default="system", description="Theme preference: system, light, dark") default_room: str = Field(default="front-hall", description="Default room for housekeeping") preferences_json: dict = Field(default_factory=dict, description="Extended preferences") class UserSchema(BaseSchema): """User information returned from sync""" id: uuid.UUID = Field(..., description="Internal user ID") authentik_id: uuid.UUID = Field(..., description="Authentik user ID") email: str = Field(..., description="User email") name: str = Field(..., description="Display name") avatar_url: Optional[str] = Field(None, description="Profile picture URL") created_at: datetime = Field(..., description="Account creation timestamp") last_login: Optional[datetime] = Field(None, description="Last login timestamp") class AuthSyncResponse(BaseSchema): """ Response from POST /auth/sync Contains the synced user profile, roles, and preferences. """ user: UserSchema = Field(..., description="User profile") roles: list[RoleSchema] = Field(..., description="User's permission roles") preferences: UserPreferencesSchema = Field(..., description="User preferences") is_new_user: bool = Field(..., description="True if user was just created") class TokenInfoSchema(BaseSchema): """ Token information from Authentik userinfo endpoint This is what Authentik returns when validating an access token. """ sub: str = Field(..., description="Subject (Authentik user ID)") email: str = Field(..., description="User email") name: Optional[str] = Field(None, description="Display name") preferred_username: Optional[str] = Field(None, description="Username") groups: list[str] = Field(default_factory=list, description="Group memberships") picture: Optional[str] = Field(None, description="Profile picture URL") class UserListItemSchema(BaseSchema): """User item for list display""" id: uuid.UUID = Field(..., description="Internal user ID") email: str = Field(..., description="User email") name: str = Field(..., description="Display name") avatar_url: Optional[str] = Field(None, description="Profile picture URL") created_at: datetime = Field(..., description="Account creation timestamp") last_login: Optional[datetime] = Field(None, description="Last login timestamp") roles: list[str] = Field(default_factory=list, description="Role names") class UsersListResponse(BaseSchema): """Response from GET /auth/users""" items: list[UserListItemSchema] = Field(..., description="List of users") total: int = Field(..., description="Total count of users") class BulkSyncResultSchema(BaseSchema): """Result from bulk sync operation""" created: int = Field(..., description="Number of users created") updated: int = Field(..., description="Number of users updated") failed: int = Field(..., description="Number of users that failed to sync") total_in_authentik: int = Field(..., description="Total users in Authentik") errors: list[str] = Field(default_factory=list, description="Error messages for failed syncs") class GroupListItemSchema(BaseSchema): """Group item for list display""" id: uuid.UUID = Field(..., description="Internal group ID") authentik_id: uuid.UUID = Field(..., description="Authentik group ID") name: str = Field(..., description="Group name") is_superuser: bool = Field(default=False, description="Whether group has superuser privileges") parent_name: Optional[str] = Field(None, description="Parent group name") member_count: int = Field(default=0, description="Number of users in this group") synced_at: datetime = Field(..., description="Last sync timestamp") roles: list[str] = Field(default_factory=list, description="Assigned role names") class GroupsListResponse(BaseSchema): """Response from GET /auth/groups""" items: list[GroupListItemSchema] = Field(..., description="List of groups") total: int = Field(..., description="Total count of groups") class RolesListResponse(BaseSchema): """Response from GET /auth/roles""" items: list[RoleSchema] = Field(..., description="List of all roles") total: int = Field(..., description="Total count of roles") class GroupRoleAssignmentResponse(BaseSchema): """Response from group role assignment operations""" group_id: uuid.UUID = Field(..., description="Group ID") group_name: str = Field(..., description="Group name") roles: list[str] = Field(..., description="Currently assigned role names") # ============================================================================= # User Profile (Phase 4) # ============================================================================= class UserProfileResponse(BaseSchema): """Response from GET /users/me - full user profile""" user: UserSchema = Field(..., description="User profile") roles: list[RoleSchema] = Field(..., description="User's permission roles") preferences: UserPreferencesSchema = Field(..., description="User preferences") class PreferencesUpdateRequest(BaseSchema): """Request for PATCH /users/me/preferences""" theme: Optional[str] = Field(None, description="Theme preference: system, light, dark") default_room: Optional[str] = Field(None, description="Default room for housekeeping") preferences_json: Optional[dict] = Field(None, description="Extended preferences (merged)") # ============================================================================= # API Keys (Phase 4) # ============================================================================= class ApiKeyCreateRequest(BaseSchema): """Request for POST /users/me/api-keys""" name: str = Field(..., min_length=1, max_length=100, description="Human-readable key name") scopes: Optional[list[str]] = Field(None, description="Optional scope restriction (role names)") expires_in_days: Optional[int] = Field(None, ge=1, le=365, description="Days until expiration (optional)") class ApiKeyCreateResponse(BaseSchema): """Response from POST /users/me/api-keys - includes the key (shown only once)""" id: uuid.UUID = Field(..., description="API key ID") name: str = Field(..., description="Key name") key: str = Field(..., description="The API key (shown only once!)") key_prefix: str = Field(..., description="Key prefix for identification") scopes: Optional[list[str]] = Field(None, description="Scope restriction") expires_at: Optional[datetime] = Field(None, description="Expiration timestamp") created_at: datetime = Field(..., description="Creation timestamp") class ApiKeySchema(BaseSchema): """API key information (without the actual key)""" id: uuid.UUID = Field(..., description="API key ID") name: str = Field(..., description="Key name") key_prefix: str = Field(..., description="Key prefix for identification (e.g., 'tak_abc1')") scopes: Optional[list[str]] = Field(None, description="Scope restriction") expires_at: Optional[datetime] = Field(None, description="Expiration timestamp") last_used_at: Optional[datetime] = Field(None, description="Last usage timestamp") created_at: datetime = Field(..., description="Creation timestamp") is_expired: bool = Field(..., description="Whether the key has expired") class ApiKeysListResponse(BaseSchema): """Response from GET /users/me/api-keys""" items: list[ApiKeySchema] = Field(..., description="List of API keys") total: int = Field(..., description="Total count of keys")