feat: multi-Oracle server support with runtime switching

Complete implementation of multi-server Oracle database support:

Backend:
- Multi-pool Oracle with lazy loading per server
- Email-to-server cache for automatic server discovery
- JWT tokens include server_id claim
- /auth/check-identity and /auth/check-email endpoints
- /auth/my-servers endpoint for listing user's accessible servers
- Server switch with password re-authentication

Frontend:
- New ServerSelector component for header dropdown
- Multi-step login flow (identity → server → password)
- Server switching from header with password modal
- Mobile drawer menu with server selection
- Dark mode support for all new components
- URL bookmark support with ?server= query param

Scripts:
- Unified start.sh replacing start-prod.sh/start-test.sh
- Unified ssh-tunnel.sh with multi-server support
- Updated status.sh for new architecture

Tests:
- E2E tests for multi-server and single-server login flows
- Backend unit tests for all new endpoints
- Oracle multi-pool integration tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-01-26 22:39:06 +00:00
parent 5f99ee2fd0
commit b137e80b71
102 changed files with 9398 additions and 2787 deletions

View File

@@ -36,14 +36,14 @@ class TokenType(str, Enum):
class LoginRequest(BaseModel):
"""Model pentru request-ul de login"""
username: str = Field(
...,
min_length=3,
...,
min_length=3,
max_length=50,
description="Numele utilizatorului",
example="admin"
)
password: str = Field(
...,
...,
min_length=1,
description="Parola utilizatorului"
)
@@ -51,15 +51,32 @@ class LoginRequest(BaseModel):
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"
)
@validator('username')
def username_alphanumeric(cls, v):
"""Validează că username-ul conține doar caractere permise (inclusiv spații)"""
# Permitem litere, cifre, spații, _, și -
allowed_chars = v.replace(' ', '').replace('_', '').replace('-', '')
"""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 -')
return v.upper() # Convertim la uppercase pentru consistență cu Oracle
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):
@@ -227,5 +244,101 @@ class SessionInfo(BaseModel):
)
# ============================================================================
# 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)"
)
# Update la forward references pentru TokenResponse
TokenResponse.model_rebuild()