""" Admin API schemas — Pydantic v2. All admin-facing request/response shapes live here to keep the admin router clean and testable in isolation. """ import re from datetime import date, datetime from typing import Optional, Literal from pydantic import BaseModel, ConfigDict, Field, field_validator from app.schemas.auth import _validate_username, _validate_password_strength, _validate_email_format, _validate_name_field # --------------------------------------------------------------------------- # User list / detail # --------------------------------------------------------------------------- class UserListItem(BaseModel): id: int username: str umbral_name: str = "" email: Optional[str] = None first_name: Optional[str] = None last_name: Optional[str] = None role: str is_active: bool last_login_at: Optional[datetime] = None last_password_change_at: Optional[datetime] = None totp_enabled: bool mfa_enforce_pending: bool passwordless_enabled: bool = False created_at: datetime active_sessions: int = 0 model_config = ConfigDict(from_attributes=True) class UserListResponse(BaseModel): users: list[UserListItem] total: int class UserDetailResponse(UserListItem): preferred_name: Optional[str] = None date_of_birth: Optional[date] = None must_change_password: bool = False locked_until: Optional[datetime] = None # --------------------------------------------------------------------------- # Mutating user requests # --------------------------------------------------------------------------- class CreateUserRequest(BaseModel): """Admin-created user — allows role selection (unlike public RegisterRequest).""" model_config = ConfigDict(extra="forbid") username: str password: str role: Literal["admin", "standard", "public_event_manager"] = "standard" email: Optional[str] = Field(None, max_length=254) first_name: Optional[str] = Field(None, max_length=100) last_name: Optional[str] = Field(None, max_length=100) preferred_name: Optional[str] = Field(None, max_length=100) @field_validator("username") @classmethod def validate_username(cls, v: str) -> str: return _validate_username(v) @field_validator("password") @classmethod def validate_password(cls, v: str) -> str: return _validate_password_strength(v) @field_validator("email") @classmethod def validate_email(cls, v: str | None) -> str | None: return _validate_email_format(v) @field_validator("first_name", "last_name", "preferred_name") @classmethod def validate_name_fields(cls, v: str | None) -> str | None: return _validate_name_field(v) class UpdateUserRoleRequest(BaseModel): model_config = ConfigDict(extra="forbid") role: Literal["admin", "standard", "public_event_manager"] class ToggleActiveRequest(BaseModel): model_config = ConfigDict(extra="forbid") is_active: bool class ToggleMfaEnforceRequest(BaseModel): model_config = ConfigDict(extra="forbid") enforce: bool # --------------------------------------------------------------------------- # System config # --------------------------------------------------------------------------- class SystemConfigResponse(BaseModel): allow_registration: bool enforce_mfa_new_users: bool allow_passwordless: bool = False model_config = ConfigDict(from_attributes=True) class SystemConfigUpdate(BaseModel): model_config = ConfigDict(extra="forbid") allow_registration: Optional[bool] = None enforce_mfa_new_users: Optional[bool] = None allow_passwordless: Optional[bool] = None class TogglePasswordlessRequest(BaseModel): model_config = ConfigDict(extra="forbid") enabled: bool # --------------------------------------------------------------------------- # Admin dashboard # --------------------------------------------------------------------------- class RecentLoginItem(BaseModel): username: str last_login_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class RecentAuditItem(BaseModel): action: str actor_username: Optional[str] = None target_username: Optional[str] = None created_at: datetime model_config = ConfigDict(from_attributes=True) class AdminDashboardResponse(BaseModel): total_users: int active_users: int admin_count: int active_sessions: int mfa_adoption_rate: float recent_logins: list[RecentLoginItem] recent_audit_entries: list[RecentAuditItem] # --------------------------------------------------------------------------- # Password reset # --------------------------------------------------------------------------- class ResetPasswordResponse(BaseModel): message: str temporary_password: str class DeleteUserResponse(BaseModel): message: str deleted_username: str # --------------------------------------------------------------------------- # Audit log # --------------------------------------------------------------------------- class AuditLogEntry(BaseModel): id: int actor_username: Optional[str] = None target_username: Optional[str] = None action: str detail: Optional[str] = None ip_address: Optional[str] = None created_at: datetime class AuditLogResponse(BaseModel): entries: list[AuditLogEntry] total: int