""" Authentication Pydantic Models pentru ROA2WEB Acest modul definește toate modelele de date folosite în sistemul de autentificare, incluzând request/response models și modele pentru user data. Modelele acoperă: - Login request și response - Token data și management - User information și permisiuni - Company access control """ from pydantic import BaseModel, Field, validator, EmailStr from typing import List, Optional, Dict, Any from datetime import datetime from enum import Enum class PermissionType(str, Enum): """Tipurile de permisiuni disponibile în sistem""" READ = "read" WRITE = "write" DELETE = "delete" ADMIN = "admin" REPORTS = "reports" EXPORT = "export" class TokenType(str, Enum): """Tipurile de token-uri JWT""" ACCESS = "access" REFRESH = "refresh" class LoginRequest(BaseModel): """Model pentru request-ul de login""" username: str = Field( ..., min_length=3, max_length=50, description="Numele utilizatorului", example="admin" ) password: str = Field( ..., min_length=1, description="Parola utilizatorului" ) remember_me: bool = Field( default=False, description="Dacă să păstreze utilizatorul autentificat mai mult timp" ) server_id: Optional[str] = Field( default=None, description="ID-ul serverului Oracle pentru autentificare (opțional în modul single-server)", example="romfast" ) trusted_device_token: Optional[str] = Field( default=None, description="Token de trusted device din localStorage (pentru skip 2FA)" ) @validator('username') def username_alphanumeric(cls, v): """Validează că username-ul conține doar caractere permise (inclusiv email-uri) Pentru backward compatibility: - Permite username-uri clasice: litere, cifre, spații, _, - - Permite email-uri pentru noul flow multi-server: @, . """ # Permitem litere, cifre, spații, _, -, @, și . (pentru email-uri) allowed_chars = v.replace(' ', '').replace('_', '').replace('-', '').replace('@', '').replace('.', '') if not allowed_chars.isalnum(): raise ValueError('Username-ul poate conține doar litere, cifre, spații, _, -, @ și .') # Detectăm dacă este email sau username clasic if '@' in v: # Email: păstrăm lowercase pentru consistență cu email-urile return v.lower().strip() else: # Username clasic: uppercase pentru consistență cu Oracle return v.upper().strip() class TokenResponse(BaseModel): """Model pentru răspunsul de autentificare cu token-uri""" access_token: str = Field(description="JWT access token") refresh_token: Optional[str] = Field( default=None, description="JWT refresh token (opțional)" ) token_type: str = Field( default="bearer", description="Tipul token-ului (întotdeauna 'bearer')" ) expires_in: int = Field( description="Timpul de expirare al access token-ului în secunde" ) user: 'CurrentUser' = Field(description="Informațiile utilizatorului autentificat") trusted_device_token: Optional[str] = Field( default=None, description="Token de stocat în localStorage (prezent doar dacă trust_device=True)" ) backup_codes: Optional[list[str]] = Field( default=None, description="Coduri de backup generate la primul 2FA reușit (afișați utilizatorului o singură dată!)" ) class RefreshTokenRequest(BaseModel): """Model pentru request-ul de refresh token""" refresh_token: str = Field(description="Refresh token-ul valid") class LogoutRequest(BaseModel): """Model pentru request-ul de logout""" refresh_token: Optional[str] = Field( default=None, description="Refresh token de invalidat (opțional)" ) class CurrentUser(BaseModel): """Model pentru utilizatorul curent autentificat""" username: str = Field(description="Numele utilizatorului") user_id: Optional[int] = Field( default=None, description="ID-ul utilizatorului în baza de date" ) email: Optional[EmailStr] = Field( default=None, description="Email-ul utilizatorului" ) companies: List[str] = Field( default_factory=list, description="Lista codurilor firmelor la care utilizatorul are acces" ) permissions: List[PermissionType] = Field( default_factory=lambda: [PermissionType.READ], description="Lista permisiunilor utilizatorului" ) is_active: bool = Field( default=True, description="Dacă utilizatorul este activ" ) last_login: Optional[datetime] = Field( default=None, description="Data ultimei autentificări" ) @validator('companies') def companies_not_empty_if_active(cls, v, values): """Validează că utilizatorii activi au cel puțin o firmă""" if values.get('is_active', True) and not v: raise ValueError('Utilizatorii activi trebuie să aibă acces la cel puțin o firmă') return v class UserCompany(BaseModel): """Model pentru o firmă la care utilizatorul are acces""" code: str = Field(description="Codul firmei (schema Oracle)") name: Optional[str] = Field( default=None, description="Numele firmei (dacă este disponibil)" ) permissions: List[PermissionType] = Field( default_factory=lambda: [PermissionType.READ], description="Permisiunile utilizatorului pentru această firmă" ) is_default: bool = Field( default=False, description="Dacă aceasta este firma implicită pentru utilizator" ) class CompanyAccessRequest(BaseModel): """Model pentru verificarea accesului la o firmă""" company_code: str = Field(description="Codul firmei de verificat") required_permissions: Optional[List[PermissionType]] = Field( default=None, description="Permisiunile necesare (opțional)" ) class CompanyAccessResponse(BaseModel): """Model pentru răspunsul de verificare acces firmă""" has_access: bool = Field(description="Dacă utilizatorul are acces") company: Optional[UserCompany] = Field( default=None, description="Detaliile firmei dacă utilizatorul are acces" ) missing_permissions: Optional[List[PermissionType]] = Field( default=None, description="Permisiunile lipsă (dacă aplicabil)" ) class AuthError(BaseModel): """Model pentru erorile de autentificare""" error: str = Field(description="Tipul erorii") error_description: str = Field(description="Descrierea detaliată a erorii") error_code: Optional[str] = Field( default=None, description="Codul de eroare pentru procesare automată" ) class AuthStats(BaseModel): """Model pentru statisticile de autentificare""" total_users: int = Field(description="Numărul total de utilizatori") active_sessions: int = Field(description="Sesiuni active curente") cache_hit_ratio: float = Field( description="Rata de hit a cache-ului pentru date utilizatori" ) last_cleanup: Optional[datetime] = Field( default=None, description="Ultima curățare a cache-ului" ) class PasswordChangeRequest(BaseModel): """Model pentru schimbarea parolei (pentru viitor)""" current_password: str = Field(description="Parola curentă") new_password: str = Field( min_length=8, description="Noua parolă (minim 8 caractere)" ) confirm_password: str = Field(description="Confirmarea noii parole") @validator('confirm_password') def passwords_match(cls, v, values): """Validează că parolele coincid""" if 'new_password' in values and v != values['new_password']: raise ValueError('Parolele nu coincid') return v class SessionInfo(BaseModel): """Model pentru informațiile despre sesiune""" session_id: str = Field(description="ID-ul sesiunii") username: str = Field(description="Numele utilizatorului") created_at: datetime = Field(description="Data creării sesiunii") last_activity: datetime = Field(description="Ultima activitate") ip_address: Optional[str] = Field( default=None, description="Adresa IP a utilizatorului" ) user_agent: Optional[str] = Field( default=None, description="User agent-ul browserului" ) is_active: bool = Field( default=True, description="Dacă sesiunea este încă activă" ) # ============================================================================ # MULTI-ORACLE IDENTITY CHECK MODELS (US-004, US-013) # ============================================================================ class CheckIdentityRequest(BaseModel): """ Model pentru verificarea identității în sistemul multi-Oracle (US-013) Suportă atât email cât și username: - Cu '@': tratează ca email și caută în EmailServerCache - Fără '@': tratează ca username și caută în Oracle pe toate serverele """ identity: str = Field( ..., min_length=2, max_length=100, description="Email sau username de verificat", example="user@example.com sau MARIUS" ) @validator('identity') def validate_identity(cls, v): """Validează și normalizează identitatea""" stripped = v.strip() if not stripped: raise ValueError('Identitatea nu poate fi goală') # Pentru email-uri, normalizăm la lowercase if '@' in stripped: return stripped.lower() # Pentru username-uri, normalizăm la uppercase (convenție Oracle) return stripped.upper() class CheckEmailRequest(BaseModel): """ Model pentru verificarea email-ului în sistemul multi-Oracle (US-004) DEPRECATED: Folosește CheckIdentityRequest pentru suport dual email/username Păstrat pentru backward compatibility. """ email: EmailStr = Field( ..., description="Adresa email a utilizatorului de verificat", example="user@example.com" ) class ServerInfo(BaseModel): """Informații despre un server Oracle disponibil pentru utilizator""" id: str = Field(description="ID-ul serverului (ex: 'romfast')") name: str = Field(description="Numele human-readable al serverului (ex: 'Romfast - Producție')") class CheckIdentityResponse(BaseModel): """ Răspunsul pentru verificarea identității (email sau username) (US-013). SECURITATE: - Pentru identitate validă: returnează exists=True și lista serverelor - Pentru identitate invalidă: returnează exists=False și listă goală de servere (NU expunem serverele disponibile pentru a preveni enumerarea!) """ exists: bool = Field( description="True dacă identitatea există în sistem pe cel puțin un server" ) servers: List[ServerInfo] = Field( default_factory=list, description="Lista serverelor pe care există identitatea (goală pentru identitate invalidă)" ) identity_type: str = Field( default="unknown", description="Tipul identității: 'email' sau 'username'" ) class CheckEmailResponse(BaseModel): """ Răspunsul pentru verificarea email-ului (US-004). DEPRECATED: Folosește CheckIdentityResponse pentru suport dual email/username Păstrat pentru backward compatibility. SECURITATE: - Pentru email valid: returnează exists=True și lista serverelor - Pentru email invalid: returnează exists=False și listă goală de servere (NU expunem serverele disponibile pentru a preveni enumerarea!) """ exists: bool = Field( description="True dacă email-ul există în sistem pe cel puțin un server" ) servers: List[ServerInfo] = Field( default_factory=list, description="Lista serverelor pe care există email-ul (goală pentru email invalid)" ) class VerifyBackupCodeRequest(BaseModel): """Request pentru POST /auth/verify-backup-code""" code: str = Field(..., min_length=6, max_length=12, description="Codul de recuperare (ex: AB3K9PQR)") email: str = Field(..., description="Email sau username") server_id: Optional[str] = Field(default=None, description="ID server Oracle") trust_device: bool = Field(default=False, description="Ține minte dispozitivul 30 de zile") # Update la forward references pentru TokenResponse TokenResponse.model_rebuild() # ============================================================================ # MODELE 2FA WEB LOGIN # ============================================================================ class LoginRequires2FAResponse(BaseModel): """ Răspuns returnat de POST /auth/login când 2FA este necesar. Frontend-ul detectează câmpul requires_2fa=True și afișează pasul de cod. Email-ul complet se trimite la /auth/verify-2fa-code. """ requires_2fa: bool = Field( default=True, description="Întotdeauna True când se solicită 2FA" ) masked_email: str = Field( description="Emailul mascat pentru afișare (ex: m***@romfast.ro)" ) email: str = Field( description="Emailul complet — de trimis la /auth/verify-2fa-code" ) class Verify2FARequest(BaseModel): """Request pentru POST /auth/verify-2fa-code""" code: str = Field( ..., min_length=6, max_length=6, description="Codul OTP de 6 cifre primit pe email" ) email: str = Field( ..., description="Emailul primit în răspunsul de la /auth/login (câmpul 'email')" ) server_id: Optional[str] = Field( default=None, description="ID-ul serverului Oracle (pentru multi-server mode)" ) trust_device: bool = Field( default=False, description="Dacă utilizatorul vrea să fie ținut minte pe acest dispozitiv" ) class Resend2FARequest(BaseModel): """Request pentru POST /auth/resend-2fa-code""" email: str = Field( ..., description="Emailul unde se retrimite codul OTP" ) server_id: Optional[str] = Field( default=None, description="ID-ul serverului Oracle (pentru multi-server mode)" )