421 lines
14 KiB
Python
421 lines
14 KiB
Python
"""
|
|
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)"
|
|
) |