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:
@@ -80,23 +80,71 @@ class UserAuthService:
|
||||
'timestamp': datetime.now()
|
||||
}
|
||||
logger.debug(f"Cached data for user {username}")
|
||||
|
||||
async def verify_user_credentials(self, username: str, password: str) -> bool:
|
||||
|
||||
async def get_username_by_email(
|
||||
self,
|
||||
email: str,
|
||||
server_id: Optional[str] = None
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Obține username-ul Oracle corespunzător unui email.
|
||||
|
||||
Necesar pentru login cu email - convertește email-ul în username-ul
|
||||
real din tabelul UTILIZATORI pentru autentificare cu pack_drepturi.
|
||||
|
||||
Args:
|
||||
email: Email-ul utilizatorului
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server mode)
|
||||
|
||||
Returns:
|
||||
Username-ul Oracle sau None dacă email-ul nu există
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT UTILIZATOR
|
||||
FROM CONTAFIN_ORACLE.UTILIZATORI
|
||||
WHERE LOWER(EMAIL) = :email
|
||||
AND INACTIV = 0
|
||||
AND STERS = 0
|
||||
""", {'email': email.lower().strip()})
|
||||
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
username = row[0]
|
||||
logger.info(f"Resolved email '{email}' to username '{username}' on server '{server_id}'")
|
||||
return username
|
||||
else:
|
||||
logger.warning(f"No username found for email '{email}' on server '{server_id}'")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Database error resolving email '{email}' to username: {str(e)}")
|
||||
return None
|
||||
|
||||
async def verify_user_credentials(
|
||||
self,
|
||||
username: str,
|
||||
password: str,
|
||||
server_id: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Verifică credențialele utilizatorului folosind pack_drepturi.verificautilizator
|
||||
|
||||
|
||||
Args:
|
||||
username: Numele utilizatorului
|
||||
password: Parola utilizatorului
|
||||
|
||||
server_id: ID-ul serverului Oracle (opțional, pentru multi-server mode)
|
||||
|
||||
Returns:
|
||||
True dacă credențialele sunt corecte, False altfel
|
||||
|
||||
|
||||
Raises:
|
||||
AuthenticationError: Dacă apar erori în procesul de verificare
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Apelarea procedurii pack_drepturi.verificautilizator
|
||||
# Această procedură returnează ID-ul utilizatorului cu checksum pentru succes, -1 pentru eșec
|
||||
@@ -110,7 +158,10 @@ class UserAuthService:
|
||||
|
||||
result = cursor.fetchone()
|
||||
verification_result = result[0] if result else -1
|
||||
|
||||
|
||||
# DEBUG: Log the exact result from Oracle
|
||||
logger.info(f"[DEBUG] verificautilizator('{username.upper()}', '***') on server '{server_id}' = {verification_result}")
|
||||
|
||||
# Interpretarea rezultatului conform logicii VFP:
|
||||
# -1 = invalid credentials
|
||||
# > 0 = valid user ID with checksum
|
||||
@@ -136,27 +187,33 @@ class UserAuthService:
|
||||
logger.error(f"Database error during authentication for user {username}: {str(e)}")
|
||||
raise AuthenticationError(f"Database authentication error: {str(e)}")
|
||||
|
||||
async def get_user_companies(self, username: str) -> List[str]:
|
||||
async def get_user_companies(
|
||||
self,
|
||||
username: str,
|
||||
server_id: Optional[str] = None
|
||||
) -> List[str]:
|
||||
"""
|
||||
Obține lista firmelor la care utilizatorul are acces din V_NOM_FIRME
|
||||
folosind ID-ul utilizatorului din UTILIZATORI
|
||||
|
||||
|
||||
Args:
|
||||
username: Numele utilizatorului
|
||||
|
||||
server_id: ID-ul serverului Oracle (opțional, pentru multi-server mode)
|
||||
|
||||
Returns:
|
||||
Lista codurilor firmelor la care utilizatorul are acces
|
||||
|
||||
|
||||
Raises:
|
||||
AuthenticationError: Dacă apar erori în procesul de obținere
|
||||
"""
|
||||
# Verifică cache-ul mai întâi
|
||||
cached_data = self._get_cached_user_data(username)
|
||||
# Verifică cache-ul mai întâi (include server_id în cheie pentru multi-server)
|
||||
cache_key_suffix = f"_{server_id}" if server_id else ""
|
||||
cached_data = self._get_cached_user_data(f"{username}{cache_key_suffix}")
|
||||
if cached_data and 'companies' in cached_data:
|
||||
return cached_data['companies']
|
||||
|
||||
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
try:
|
||||
# Debug: să vedem ce utilizatori există în tabela UTILIZATORI
|
||||
@@ -222,85 +279,111 @@ class UserAuthService:
|
||||
# În caz de eroare, returnăm listă goală în loc de TEST_COMPANY
|
||||
return []
|
||||
|
||||
# Cache rezultatul
|
||||
self._cache_user_data(username, {'companies': companies})
|
||||
|
||||
# Cache rezultatul (include server_id pentru multi-server)
|
||||
cache_key = f"{username}{cache_key_suffix}"
|
||||
self._cache_user_data(cache_key, {'companies': companies})
|
||||
|
||||
return companies
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Database error getting companies for user {username}: {str(e)}")
|
||||
raise AuthenticationError(f"Error retrieving user companies: {str(e)}")
|
||||
|
||||
async def get_user_permissions(self, username: str, company: str) -> List[str]:
|
||||
async def get_user_permissions(
|
||||
self,
|
||||
username: str,
|
||||
company: str,
|
||||
server_id: Optional[str] = None
|
||||
) -> List[str]:
|
||||
"""
|
||||
Obține permisiunile utilizatorului pentru o anumită firmă
|
||||
|
||||
|
||||
Args:
|
||||
username: Numele utilizatorului
|
||||
company: Codul firmei
|
||||
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server mode)
|
||||
|
||||
Returns:
|
||||
Lista permisiunilor pentru firma specificată
|
||||
"""
|
||||
# Implementare de bază - poate fi extinsă în viitor
|
||||
companies = await self.get_user_companies(username)
|
||||
|
||||
companies = await self.get_user_companies(username, server_id)
|
||||
|
||||
# Dacă nu există companii sau compania nu este în listă, returnează permisiuni minime
|
||||
if not companies or company not in companies:
|
||||
return ["read"] if not companies else []
|
||||
|
||||
|
||||
# Pentru moment, toți utilizatorii autentificați au permisiuni de citire
|
||||
# Acest sistem poate fi extins cu permisiuni granulare în viitor
|
||||
return ["read", "reports"]
|
||||
|
||||
async def authenticate_and_create_tokens(
|
||||
self,
|
||||
username: str,
|
||||
password: str
|
||||
self,
|
||||
username: str,
|
||||
password: str,
|
||||
server_id: Optional[str] = None
|
||||
) -> Tuple[bool, Optional[TokenResponse], Optional[str]]:
|
||||
"""
|
||||
Autentifică utilizatorul și creează token-urile JWT
|
||||
|
||||
|
||||
Suportă atât username clasic cât și email pentru login.
|
||||
Dacă input-ul conține '@', se tratează ca email și se convertește
|
||||
în username-ul Oracle corespunzător.
|
||||
|
||||
Args:
|
||||
username: Numele utilizatorului
|
||||
username: Numele utilizatorului sau email-ul
|
||||
password: Parola utilizatorului
|
||||
|
||||
server_id: ID-ul serverului Oracle (opțional, pentru multi-server mode)
|
||||
|
||||
Returns:
|
||||
Tuple cu (success, token_response, error_message)
|
||||
"""
|
||||
try:
|
||||
# Verifică credențialele
|
||||
is_valid = await self.verify_user_credentials(username, password)
|
||||
|
||||
# Detectăm dacă input-ul este email sau username clasic
|
||||
actual_username = username
|
||||
if '@' in username:
|
||||
# Este email - convertim în username Oracle
|
||||
resolved_username = await self.get_username_by_email(username, server_id)
|
||||
if not resolved_username:
|
||||
logger.warning(f"Could not resolve email '{username}' to username on server '{server_id}'")
|
||||
return False, None, "Invalid username or password"
|
||||
actual_username = resolved_username
|
||||
logger.info(f"Login with email '{username}' resolved to username '{actual_username}'")
|
||||
|
||||
# Verifică credențialele pe serverul specificat
|
||||
is_valid = await self.verify_user_credentials(actual_username, password, server_id)
|
||||
|
||||
if not is_valid:
|
||||
return False, None, "Invalid username or password"
|
||||
|
||||
# Obține firmele utilizatorului
|
||||
companies = await self.get_user_companies(username)
|
||||
|
||||
|
||||
# Obține firmele utilizatorului de pe serverul specificat
|
||||
companies = await self.get_user_companies(actual_username, server_id)
|
||||
|
||||
# Nu blocăm login-ul dacă utilizatorul nu are firme - îl lăsăm să vadă mesajul în frontend
|
||||
if not companies:
|
||||
logger.info(f"User {username} has no companies assigned - allowing login but with empty companies list")
|
||||
|
||||
logger.info(f"User {actual_username} has no companies assigned - allowing login but with empty companies list")
|
||||
|
||||
# Obține permisiunile (pentru prima firmă ca default sau lista goală)
|
||||
permissions = await self.get_user_permissions(username, companies[0] if companies else "")
|
||||
|
||||
permissions = await self.get_user_permissions(actual_username, companies[0] if companies else "", server_id)
|
||||
|
||||
# Creează token-urile folosind jwt_handler
|
||||
# Include server_id în JWT pentru ca request-urile ulterioare să știe pe care server să execute query-uri
|
||||
jwt_tokens = jwt_handler.create_token_response(
|
||||
username=username,
|
||||
username=actual_username,
|
||||
companies=companies,
|
||||
user_id=None, # Poate fi adăugat în viitor dacă avem user_id în DB
|
||||
permissions=permissions
|
||||
permissions=permissions,
|
||||
server_id=server_id
|
||||
)
|
||||
|
||||
|
||||
# Creează obiectul CurrentUser
|
||||
current_user = CurrentUser(
|
||||
username=username,
|
||||
username=actual_username,
|
||||
user_id=None,
|
||||
companies=companies,
|
||||
permissions=permissions
|
||||
)
|
||||
|
||||
|
||||
# Creează TokenResponse-ul complet cu user info
|
||||
token_response = TokenResponse(
|
||||
access_token=jwt_tokens.access_token,
|
||||
@@ -309,10 +392,10 @@ class UserAuthService:
|
||||
expires_in=jwt_tokens.expires_in,
|
||||
user=current_user
|
||||
)
|
||||
|
||||
logger.info(f"Successfully created tokens for user {username}")
|
||||
|
||||
logger.info(f"Successfully created tokens for user {actual_username} on server {server_id or 'default'}")
|
||||
return True, token_response, None
|
||||
|
||||
|
||||
except AuthenticationError as e:
|
||||
logger.error(f"Authentication error for user {username}: {str(e)}")
|
||||
return False, None, str(e)
|
||||
|
||||
Reference in New Issue
Block a user