""" Authentication Service - Oracle Database Integration pentru ROA2WEB Acest modul integrează sistemul de autentificare JWT cu baza de date Oracle, reutilizând funcționalitatea existentă din aplicația Flask originală. Funcționalități: - Verificare utilizatori prin pack_drepturi.verificautilizator - Obținere lista firmelor din vdef_util_grup - Gestionarea sesiunilor și permisiunilor utilizatorilor - Caching pentru performanță optimă """ import logging import hashlib from typing import Optional, List, Dict, Any, Tuple from datetime import datetime, timedelta import asyncio from contextlib import asynccontextmanager # Import shared database pool # IMPORTANT: Use shared.database.oracle_pool to ensure singleton consistency # DO NOT use relative imports like 'database.oracle_pool' from shared.database.oracle_pool import oracle_pool from .jwt_handler import jwt_handler from .models import TokenResponse, CurrentUser logger = logging.getLogger(__name__) class AuthenticationError(Exception): """Excepție pentru erorile de autentificare""" pass class UserAuthService: """ Serviciu pentru autentificarea utilizatorilor folosind Oracle Database Acest serviciu integrează: - Verificarea credențialelor prin pack_drepturi.verificautilizator - Obținerea listei de firme prin vdef_util_grup - Generarea token-urilor JWT - Cache pentru performanță """ def __init__(self): """Inițializează serviciul de autentificare""" self._user_cache: Dict[str, Dict[str, Any]] = {} self._cache_ttl = timedelta(minutes=15) # Cache 15 minute def _get_cache_key(self, username: str) -> str: """Generează cheia de cache pentru utilizator""" return f"auth_user_{username.lower()}" def _is_cache_valid(self, cache_entry: Dict[str, Any]) -> bool: """Verifică dacă entry-ul din cache este încă valid""" if not cache_entry or 'timestamp' not in cache_entry: return False cache_time = cache_entry['timestamp'] return datetime.now() - cache_time < self._cache_ttl def _get_cached_user_data(self, username: str) -> Optional[Dict[str, Any]]: """Obține datele utilizatorului din cache dacă sunt valide""" cache_key = self._get_cache_key(username) cache_entry = self._user_cache.get(cache_key) if self._is_cache_valid(cache_entry): logger.debug(f"Cache hit for user {username}") return cache_entry['data'] return None def _cache_user_data(self, username: str, data: Dict[str, Any]) -> None: """Salvează datele utilizatorului în cache""" cache_key = self._get_cache_key(username) self._user_cache[cache_key] = { 'data': data, 'timestamp': datetime.now() } logger.debug(f"Cached data for user {username}") 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(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 cursor.execute(""" SELECT pack_drepturi.verificautilizator(:username, :password) FROM DUAL """, { 'username': username.upper(), 'password': password }) 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 # < -1000000 = admin/super user is_valid = verification_result != -1 if is_valid: # Extrage ID-ul real al utilizatorului conform logicii VFP if verification_result < -1000000: # Admin/Super user user_id = verification_result + 1000000 logger.info(f"Admin/Super user {username} authenticated successfully (ID: {user_id})") else: # User normal - extrage ID-ul din checksum user_id = int(verification_result / 100) logger.info(f"User {username} authenticated successfully (ID: {user_id}, verification: {verification_result})") else: logger.warning(f"Authentication failed for user {username}") return is_valid except Exception as e: 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, 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 (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(server_id) as connection: with connection.cursor() as cursor: try: # Debug: să vedem ce utilizatori există în tabela UTILIZATORI cursor.execute(""" SELECT ID_UTIL, UTILIZATOR FROM UTILIZATORI WHERE UPPER(UTILIZATOR) LIKE '%MARIUS%' ORDER BY UTILIZATOR """) debug_users = cursor.fetchall() logger.info(f"DEBUG: Users with MARIUS in name: {debug_users}") # Primul pas: obținem ID-ul utilizatorului din UTILIZATORI cursor.execute(""" SELECT ID_UTIL, UTILIZATOR FROM UTILIZATORI WHERE UPPER(UTILIZATOR) = :username """, {'username': username.upper()}) user_row = cursor.fetchone() if not user_row: logger.warning(f"User {username} not found in UTILIZATORI table") # Să încercăm să găsim utilizatori similari cursor.execute(""" SELECT ID_UTIL, UTILIZATOR FROM UTILIZATORI WHERE UPPER(UTILIZATOR) LIKE :username_pattern ORDER BY UTILIZATOR """, {'username_pattern': f'%{username.upper()}%'}) similar_users = cursor.fetchall() logger.info(f"Similar users found: {similar_users}") return [] user_id = user_row[0] actual_name = user_row[1] logger.info(f"Found user {username} with ID: {user_id}, actual name: {actual_name}") # Al doilea pas: obținem firmele folosind query-ul corect (cu ID_FIRMA) cursor.execute(""" SELECT A.ID_FIRMA, A.FIRMA FROM V_NOM_FIRME A WHERE A.ID_FIRMA IN ( SELECT ID_FIRMA FROM VDEF_UTIL_FIRME WHERE ID_PROGRAM = 2 AND ID_UTIL = :user_id ) ORDER BY A.FIRMA """, {'user_id': user_id}) companies_rows = cursor.fetchall() companies = [str(row[0]) for row in companies_rows if row[0]] if not companies: logger.warning(f"No companies found for user {username} (ID: {user_id})") return [] logger.info(f"User {username} has access to {len(companies)} companies: {companies}") except Exception as e: logger.error(f"Could not query companies for user {username}: {e}") # În caz de eroare, returnăm listă goală în loc de TEST_COMPANY return [] # 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, 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, 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, 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 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: # 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 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 {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(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=actual_username, companies=companies, user_id=None, # Poate fi adăugat în viitor dacă avem user_id în DB permissions=permissions, server_id=server_id ) # Creează obiectul CurrentUser current_user = CurrentUser( 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, refresh_token=jwt_tokens.refresh_token, token_type=jwt_tokens.token_type, expires_in=jwt_tokens.expires_in, user=current_user ) 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) except Exception as e: logger.error(f"Unexpected error during authentication for user {username}: {str(e)}") return False, None, "Internal authentication error" async def validate_user_company_access(self, username: str, company: str) -> bool: """ Validează dacă utilizatorul are acces la o anumită firmă Args: username: Numele utilizatorului company: Codul firmei de verificat Returns: True dacă utilizatorul are acces, False altfel """ try: companies = await self.get_user_companies(username) has_access = company in companies if not has_access: logger.warning(f"User {username} attempted to access unauthorized company {company}") return has_access except Exception as e: logger.error(f"Error validating company access for user {username}: {str(e)}") return False async def refresh_user_data(self, username: str) -> bool: """ Reîmprospătează datele utilizatorului din cache Args: username: Numele utilizatorului Returns: True dacă refresh-ul a fost cu succes """ try: # Șterge din cache cache_key = self._get_cache_key(username) if cache_key in self._user_cache: del self._user_cache[cache_key] # Reîncarcă datele await self.get_user_companies(username) logger.info(f"Refreshed user data for {username}") return True except Exception as e: logger.error(f"Error refreshing user data for {username}: {str(e)}") return False def clear_cache(self) -> None: """Șterge tot cache-ul utilizatorilor""" self._user_cache.clear() logger.info("User cache cleared") def get_cache_stats(self) -> Dict[str, Any]: """Returnează statistici despre cache""" total_entries = len(self._user_cache) valid_entries = sum( 1 for entry in self._user_cache.values() if self._is_cache_valid(entry) ) return { 'total_entries': total_entries, 'valid_entries': valid_entries, 'cache_hit_ratio': valid_entries / total_entries if total_entries > 0 else 0 } # Instance globală pentru folosire în toate aplicațiile auth_service = UserAuthService()