# app/schemas/users.py from datetime import datetime from typing import Any from uuid import UUID from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator from app.schemas.validators import validate_password_strength, validate_phone_number class UserBase(BaseModel): email: EmailStr first_name: str last_name: str | None = None phone_number: str | None = None @field_validator("phone_number") @classmethod def validate_phone(cls, v: str | None) -> str | None: return validate_phone_number(v) class UserCreate(UserBase): password: str is_superuser: bool = False is_active: bool = True @field_validator("password") @classmethod def password_strength(cls, v: str) -> str: """Enterprise-grade password strength validation""" return validate_password_strength(v) class UserUpdate(BaseModel): first_name: str | None = None last_name: str | None = None phone_number: str | None = None password: str | None = None preferences: dict[str, Any] | None = None locale: str | None = Field( None, max_length=10, pattern=r"^[a-z]{2}(-[A-Z]{2})?$", description="User's preferred locale (BCP 47 format: en, it, en-US, it-IT)", examples=["en", "it", "en-US", "it-IT"], ) is_active: bool | None = ( None # Changed default from True to None to avoid unintended updates ) is_superuser: bool | None = None # Explicitly reject privilege escalation attempts @field_validator("phone_number") @classmethod def validate_phone(cls, v: str | None) -> str | None: return validate_phone_number(v) @field_validator("password") @classmethod def password_strength(cls, v: str | None) -> str | None: """Enterprise-grade password strength validation""" if v is None: return v return validate_password_strength(v) @field_validator("locale") @classmethod def validate_locale(cls, v: str | None) -> str | None: """Validate locale against supported locales.""" if v is None: return v # Only support English and Italian for template showcase # Note: Locales stored in lowercase for case-insensitive matching supported_locales = {"en", "it", "en-us", "en-gb", "it-it"} # Normalize to lowercase for comparison and storage v_lower = v.lower() if v_lower not in supported_locales: raise ValueError( f"Unsupported locale '{v}'. Supported locales: {sorted(supported_locales)}" ) # Return normalized lowercase version for consistency return v_lower @field_validator("is_superuser") @classmethod def prevent_superuser_modification(cls, v: bool | None) -> bool | None: """Prevent users from modifying their superuser status via this schema.""" if v is not None: raise ValueError("Cannot modify superuser status through user update") return v class UserInDB(UserBase): id: UUID is_active: bool is_superuser: bool created_at: datetime updated_at: datetime | None = None locale: str | None = None model_config = ConfigDict(from_attributes=True) class UserResponse(UserBase): id: UUID is_active: bool is_superuser: bool created_at: datetime updated_at: datetime | None = None locale: str | None = None model_config = ConfigDict(from_attributes=True) class Token(BaseModel): access_token: str refresh_token: str | None = None token_type: str = "bearer" user: "UserResponse" # Forward reference since UserResponse is defined above expires_in: int | None = None # Token expiration in seconds class TokenPayload(BaseModel): sub: str # User ID exp: int # Expiration time iat: int | None = None # Issued at jti: str | None = None # JWT ID is_superuser: bool | None = False first_name: str | None = None email: str | None = None type: str | None = None # Token type (access/refresh) class TokenData(BaseModel): user_id: UUID is_superuser: bool = False class PasswordChange(BaseModel): """Schema for changing password (requires current password).""" current_password: str new_password: str @field_validator("new_password") @classmethod def password_strength(cls, v: str) -> str: """Enterprise-grade password strength validation""" return validate_password_strength(v) class PasswordReset(BaseModel): """Schema for resetting password (via email token).""" token: str new_password: str @field_validator("new_password") @classmethod def password_strength(cls, v: str) -> str: """Enterprise-grade password strength validation""" return validate_password_strength(v) class LoginRequest(BaseModel): email: EmailStr password: str class RefreshTokenRequest(BaseModel): refresh_token: str class PasswordResetRequest(BaseModel): """Schema for requesting a password reset.""" email: EmailStr = Field(..., description="Email address of the account") model_config = {"json_schema_extra": {"example": {"email": "user@example.com"}}} class PasswordResetConfirm(BaseModel): """Schema for confirming a password reset with token.""" token: str = Field(..., description="Password reset token from email") new_password: str = Field(..., min_length=8, description="New password") @field_validator("new_password") @classmethod def password_strength(cls, v: str) -> str: """Enterprise-grade password strength validation""" return validate_password_strength(v) model_config = { "json_schema_extra": { "example": { "token": "eyJwYXlsb2FkIjp7ImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImV4cCI6MTcxMjM0NTY3OH19", "new_password": "NewSecurePassword123", } } }