""" 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 verify_user_credentials(self, username: str, password: str) -> bool: """ Verifică credențialele utilizatorului folosind pack_drepturi.verificautilizator Args: username: Numele utilizatorului password: Parola utilizatorului 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: 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 # 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) -> 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 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) if cached_data and 'companies' in cached_data: return cached_data['companies'] try: async with oracle_pool.get_connection() 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 self._cache_user_data(username, {'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]: """ Obține permisiunile utilizatorului pentru o anumită firmă Args: username: Numele utilizatorului company: Codul firmei Returns: Lista permisiunilor pentru firma specificată """ # Implementare de bază - poate fi extinsă în viitor companies = await self.get_user_companies(username) # 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 ) -> Tuple[bool, Optional[TokenResponse], Optional[str]]: """ Autentifică utilizatorul și creează token-urile JWT Args: username: Numele utilizatorului password: Parola utilizatorului Returns: Tuple cu (success, token_response, error_message) """ try: # Verifică credențialele is_valid = await self.verify_user_credentials(username, password) if not is_valid: return False, None, "Invalid username or password" # Obține firmele utilizatorului companies = await self.get_user_companies(username) # 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") # Obține permisiunile (pentru prima firmă ca default sau lista goală) permissions = await self.get_user_permissions(username, companies[0] if companies else "") # Creează token-urile folosind jwt_handler jwt_tokens = jwt_handler.create_token_response( username=username, companies=companies, user_id=None, # Poate fi adăugat în viitor dacă avem user_id în DB permissions=permissions ) # Creează obiectul CurrentUser current_user = CurrentUser( username=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 {username}") 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()