Kyle Pope 3a456e56dd Show date of birth with calculated age in IAM user detail
Adds date_of_birth to UserDetailResponse schema, AdminUserDetail
TypeScript type, and the User Information card in UserDetailSection.
Displays formatted date with age in parentheses (e.g. "3/02/2000 (26)").

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 19:58:21 +08:00

180 lines
5.1 KiB
Python

"""
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
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
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
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
# ---------------------------------------------------------------------------
# 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