This commit fixes overly broad .gitignore patterns that were excluding important source code files from version control. Previously, wildcard patterns like *auth*, *token*, *secret*, *connection*, and *credential* were excluding ALL files containing these words, including critical application code. Changes: - Updated .gitignore with specific patterns for sensitive config files (*.json, *.txt, *.yml, *.yaml extensions only) - Removed broad wildcards that excluded source code files Added missing source files: - shared/auth/ (9 files): Complete authentication system - JWT handler, middleware, auth service, models, routes - reports-app/backend/app/routers/auth.py: Authentication API router - reports-app/backend/app/auth_middleware_wrapper.py: Middleware wrapper - reports-app/frontend/src/stores/auth.js: Vue.js auth store - reports-app/frontend/tests/: E2E tests and fixtures for auth - reports-app/telegram-bot/app/auth/: Telegram auth linking module - deployment/windows/scripts/Setup-ClaudeAuth.ps1: Windows deployment script - security/secrets_scanner.py: Security scanning utility These files are essential for the application to function and should have been included in the initial commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
395 lines
16 KiB
Python
395 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
|
|
import sys
|
|
import os
|
|
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
|
|
|
from 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() |