Fix .gitignore and add missing authentication source files
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>
This commit is contained in:
395
shared/auth/auth_service.py
Normal file
395
shared/auth/auth_service.py
Normal file
@@ -0,0 +1,395 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user