Files
roa2web-service-auto/shared/auth/auth_service.py
Marius Mutu c5e051ad80 feat: Migrate to ultrathin monolith architecture
Consolidate 3 separate applications (reports-app, data-entry-app, telegram-bot) into a unified
architecture with single backend and frontend:

Backend Changes:
- Unified FastAPI backend at backend/ with modular structure
- Modules: reports, data_entry, telegram in backend/modules/
- Centralized config.py and main.py with all routers registered
- Single worker mode (--workers 1) for Telegram bot compatibility
- Shared Oracle connection pool and JWT authentication
- Unified requirements.txt and environment configuration

Frontend Changes:
- Single Vue.js SPA with module-based routing
- Unified frontend at src/ with modules in src/modules/{reports,data-entry}/
- Shared components and stores in src/shared/
- Error boundaries for module isolation
- Dual API proxy in Vite for module communication

Infrastructure:
- New unified startup scripts: start-prod.sh, start-test.sh, start-backend.sh
- Environment templates: .env.dev.example, .env.test.example, .env.prod.example
- Updated deployment scripts for Windows IIS
- Simplified SSH tunnel management

Documentation:
- Comprehensive CLAUDE.md with architecture overview
- Module-specific docs in docs/{data-entry,telegram}/
- Architecture decision records in docs/ARCHITECTURE-DECISIONS.md
- Deployment guides consolidated in deployment/windows/docs/

This migration reduces complexity, improves maintainability, and enables easier
deployment while maintaining all existing functionality.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 23:48:14 +02:00

393 lines
16 KiB
Python

"""
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()