"""Authentication Pydantic models for the Aniworld web application. This module defines simple request/response shapes used by the auth API and by the authentication service. Keep models small and focused so they are easy to validate and test. """ from __future__ import annotations import re from datetime import datetime, timezone from typing import Optional from pydantic import BaseModel, Field, field_validator class LoginRequest(BaseModel): """Request body for a login attempt. Fields: - password: master password string (minimum 8 chars recommended) - remember: optional flag to request a long-lived session """ password: str = Field(..., min_length=1, description="Master password") remember: Optional[bool] = Field( False, description="Keep session alive" ) class LoginResponse(BaseModel): """Response returned after a successful login.""" access_token: str = Field(..., description="JWT access token") token_type: str = Field("bearer", description="Token type") expires_at: Optional[datetime] = Field(None, description="Optional expiry timestamp") class SetupRequest(BaseModel): """Request to initialize the master password during first-time setup. This request includes all configuration fields needed to set up the application. """ # Required fields master_password: str = Field( ..., min_length=8, description="New master password" ) anime_directory: Optional[str] = Field( None, description="Optional anime directory path" ) # Application settings name: Optional[str] = Field( default="Aniworld", description="Application name" ) data_dir: Optional[str] = Field( default="data", description="Data directory path" ) # Scheduler configuration scheduler_enabled: Optional[bool] = Field( default=True, description="Enable/disable scheduler" ) scheduler_interval_minutes: Optional[int] = Field( default=60, ge=1, description="Scheduler interval in minutes" ) # Logging configuration logging_level: Optional[str] = Field( default="INFO", description="Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)" ) logging_file: Optional[str] = Field( default=None, description="Log file path" ) logging_max_bytes: Optional[int] = Field( default=None, ge=0, description="Max log file size in bytes" ) logging_backup_count: Optional[int] = Field( default=3, ge=0, description="Number of backup log files" ) # Backup configuration backup_enabled: Optional[bool] = Field( default=False, description="Enable/disable backups" ) backup_path: Optional[str] = Field( default="data/backups", description="Backup directory path" ) backup_keep_days: Optional[int] = Field( default=30, ge=0, description="Days to keep backups" ) # NFO configuration nfo_tmdb_api_key: Optional[str] = Field( default=None, description="TMDB API key" ) nfo_auto_create: Optional[bool] = Field( default=True, description="Auto-create NFO files" ) nfo_update_on_scan: Optional[bool] = Field( default=True, description="Update NFO on scan" ) nfo_download_poster: Optional[bool] = Field( default=True, description="Download poster images" ) nfo_download_logo: Optional[bool] = Field( default=True, description="Download logo images" ) nfo_download_fanart: Optional[bool] = Field( default=True, description="Download fanart images" ) nfo_image_size: Optional[str] = Field( default="original", description="Image size preference (original or w500)" ) @field_validator("logging_level") @classmethod def validate_logging_level(cls, v: Optional[str]) -> Optional[str]: """Validate logging level.""" if v is None: return v allowed = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} lvl = v.upper() if lvl not in allowed: raise ValueError(f"Invalid logging level: {v}. Must be one of {allowed}") return lvl @field_validator("nfo_image_size") @classmethod def validate_image_size(cls, v: Optional[str]) -> Optional[str]: """Validate image size.""" if v is None: return v allowed = {"original", "w500"} size = v.lower() if size not in allowed: raise ValueError(f"Invalid image size: {v}. Must be 'original' or 'w500'") return size class AuthStatus(BaseModel): """Public status about whether auth is configured and the current user state.""" configured: bool = Field(..., description="Whether a master password is set") authenticated: bool = Field(False, description="Whether the caller is authenticated") class SessionModel(BaseModel): """Lightweight session representation stored/returned by the auth service. This model can be persisted if a persistent session store is used. """ session_id: str = Field(..., description="Unique session identifier") user: Optional[str] = Field(None, description="Username or identifier") created_at: datetime = Field( default_factory=lambda: datetime.now(timezone.utc) ) expires_at: Optional[datetime] = Field(None) class RegisterRequest(BaseModel): """Request to register a new user (for testing purposes).""" username: str = Field( ..., min_length=3, max_length=50, description="Username" ) password: str = Field(..., min_length=8, description="Password") email: str = Field(..., description="Email address") @field_validator("email") @classmethod def validate_email(cls, v: str) -> str: """Validate email format.""" # Basic email validation pattern = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$" if not re.match(pattern, v): raise ValueError("Invalid email address") return v @field_validator("username") @classmethod def validate_username(cls, v: str) -> str: """Validate username contains no special characters.""" if not re.match(r"^[a-zA-Z0-9_-]+$", v): raise ValueError( "Username can only contain letters, numbers, underscore, " "and hyphen" ) return v