feat(auth): add 2FA with OTP, backup codes and trusted devices

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-02-24 17:25:00 +00:00
parent b001b94e37
commit 1839285ac3
26 changed files with 2402 additions and 312 deletions

View File

@@ -56,6 +56,10 @@ class LoginRequest(BaseModel):
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):
@@ -83,17 +87,25 @@ 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,
default=None,
description="JWT refresh token (opțional)"
)
token_type: str = Field(
default="bearer",
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):
@@ -340,5 +352,70 @@ class CheckEmailResponse(BaseModel):
)
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()
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)"
)