fix telegram

This commit is contained in:
Claude Agent
2026-02-23 15:12:33 +00:00
parent 6c78fec8a7
commit 8bc567a9c5
426 changed files with 112478 additions and 1 deletions

View File

@@ -0,0 +1,681 @@
"""
Authentication Routes Template pentru ROA2WEB FastAPI Applications
Acest modul oferă rute predefinite pentru autentificare care pot fi integrate
în orice aplicație FastAPI din ecosistemul ROA2WEB.
Endpoints disponibile:
- POST /auth/login - Autentificare utilizator
- POST /auth/refresh - Refresh access token
- POST /auth/logout - Deconectare utilizator
- GET /auth/me - Informații utilizator curent
- GET /auth/companies - Firmele utilizatorului
- GET /auth/status - Status autentificare
"""
import logging
from typing import List, Optional
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status, Request, Response
from fastapi.security import HTTPAuthorizationCredentials
from .models import (
LoginRequest, TokenResponse, RefreshTokenRequest, LogoutRequest,
CurrentUser, UserCompany, CompanyAccessRequest, CompanyAccessResponse,
AuthError, AuthStats, CheckEmailRequest, CheckEmailResponse, ServerInfo,
CheckIdentityRequest, CheckIdentityResponse
)
from .auth_service import auth_service, AuthenticationError
from .jwt_handler import jwt_handler
from .dependencies import (
get_current_user, get_optional_user,
security_required, security_optional
)
from .middleware import default_rate_limiter, RateLimiter
logger = logging.getLogger(__name__)
def create_auth_router(
prefix: str = "/auth",
tags: Optional[List[str]] = None,
include_admin_routes: bool = False
) -> APIRouter:
"""
Creează un router FastAPI cu toate rutele de autentificare
Args:
prefix: Prefix-ul pentru toate rutele
tags: Tag-urile pentru documentația OpenAPI
include_admin_routes: Dacă să includă rutele de administrare
Returns:
Router-ul FastAPI configurat
"""
router = APIRouter(prefix=prefix, tags=tags or ["authentication"])
# Rate limiter pentru check-identity/check-email: 5 requests per minut per IP
check_identity_rate_limiter = RateLimiter(max_requests=5, time_window=60)
@router.post("/check-identity", response_model=CheckIdentityResponse, status_code=status.HTTP_200_OK)
async def check_identity(
check_data: CheckIdentityRequest,
request: Request
) -> CheckIdentityResponse:
"""
Verifică dacă un email sau username există în sistem și pe câte servere Oracle (US-013).
Acest endpoint suportă dual login:
- Input cu '@': tratează ca email și caută în EmailServerCache
- Input fără '@': tratează ca username și caută direct în Oracle
SECURITATE:
- Rate limited: max 5 requests/minut per IP
- NU expune serverele disponibile pentru identități invalide
- Identități invalide returnează {exists: false, servers: []}
Args:
check_data: Identitatea de verificat (email sau username)
request: Request-ul HTTP (pentru rate limiting)
Returns:
CheckIdentityResponse cu exists, servers[] și identity_type
Raises:
HTTPException 429: Rate limit exceeded
"""
# Rate limiting - 5 req/min per IP
client_ip = request.client.host if request.client else "unknown"
if not check_identity_rate_limiter.is_allowed(client_ip):
reset_time = check_identity_rate_limiter.get_reset_time(client_ip)
logger.warning(f"Rate limit exceeded for check-identity from IP {client_ip}")
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Too many requests. Please try again later.",
headers={
"X-RateLimit-Limit": "5",
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": str(reset_time),
"Retry-After": str(max(1, reset_time - int(__import__('time').time())))
}
)
try:
from .email_server_cache import email_server_cache
from backend.config import settings
identity = check_data.identity # Already normalized by validator
is_email = '@' in identity
identity_type = "email" if is_email else "username"
logger.info(f"Check-identity request for '{identity}' (type: {identity_type}) from IP {client_ip}")
# Get server IDs based on identity type
if is_email:
# Email lookup from cache
server_ids = email_server_cache.get_servers_for_email(identity)
else:
# Username lookup directly from Oracle (async)
server_ids = await email_server_cache.get_servers_for_username(identity)
if not server_ids:
# Identity not found - return empty response (don't expose available servers!)
logger.info(f"Identity '{identity}' not found in any server")
return CheckIdentityResponse(exists=False, servers=[], identity_type=identity_type)
# Build server info list with human-readable names
servers: List[ServerInfo] = []
for server_id in server_ids:
server_config = settings.get_oracle_server(server_id)
if server_config:
servers.append(ServerInfo(
id=server_config.id,
name=server_config.name
))
else:
# Fallback if server config not found (shouldn't happen)
logger.warning(f"Server '{server_id}' not found in config")
servers.append(ServerInfo(id=server_id, name=server_id))
logger.info(f"Identity '{identity}' found on {len(servers)} server(s): {[s.id for s in servers]}")
return CheckIdentityResponse(exists=True, servers=servers, identity_type=identity_type)
except Exception as e:
logger.error(f"Error checking identity '{check_data.identity}': {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error checking identity"
)
@router.post("/check-email", response_model=CheckEmailResponse, status_code=status.HTTP_200_OK)
async def check_email(
check_data: CheckEmailRequest,
request: Request
) -> CheckEmailResponse:
"""
Verifică dacă un email există în sistem și pe câte servere Oracle.
DEPRECATED: Folosește /check-identity pentru suport dual email/username.
Păstrat pentru backward compatibility.
Args:
check_data: Email-ul de verificat
request: Request-ul HTTP (pentru rate limiting)
Returns:
CheckEmailResponse cu exists și servers[]
"""
# Rate limiting - shared with check-identity
client_ip = request.client.host if request.client else "unknown"
if not check_identity_rate_limiter.is_allowed(client_ip):
reset_time = check_identity_rate_limiter.get_reset_time(client_ip)
logger.warning(f"Rate limit exceeded for check-email from IP {client_ip}")
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Too many requests. Please try again later.",
headers={
"X-RateLimit-Limit": "5",
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": str(reset_time),
"Retry-After": str(max(1, reset_time - int(__import__('time').time())))
}
)
try:
from .email_server_cache import email_server_cache
from backend.config import settings
email = check_data.email.lower().strip()
logger.info(f"Check-email request for '{email}' from IP {client_ip}")
# Get server IDs from cache
server_ids = email_server_cache.get_servers_for_email(email)
if not server_ids:
# Email not found - return empty response (don't expose available servers!)
logger.info(f"Email '{email}' not found in any server")
return CheckEmailResponse(exists=False, servers=[])
# Build server info list with human-readable names
servers: List[ServerInfo] = []
for server_id in server_ids:
server_config = settings.get_oracle_server(server_id)
if server_config:
servers.append(ServerInfo(
id=server_config.id,
name=server_config.name
))
else:
# Fallback if server config not found (shouldn't happen)
logger.warning(f"Server '{server_id}' not found in config")
servers.append(ServerInfo(id=server_id, name=server_id))
logger.info(f"Email '{email}' found on {len(servers)} server(s): {[s.id for s in servers]}")
return CheckEmailResponse(exists=True, servers=servers)
except Exception as e:
logger.error(f"Error checking email '{check_data.email}': {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error checking email"
)
@router.post("/login", response_model=TokenResponse, status_code=status.HTTP_200_OK)
async def login(
login_data: LoginRequest,
request: Request,
response: Response
) -> TokenResponse:
"""
Autentifică un utilizator și returnează token-urile JWT
Acest endpoint:
- Validează credențialele utilizatorului în Oracle
- Obține firmele la care utilizatorul are acces
- Generează access și refresh token-uri JWT
- Aplică rate limiting pentru securitate
- Suportă modul multi-server (server_id opțional)
Args:
login_data: Datele de autentificare (username, password, server_id opțional)
request: Request-ul HTTP (pentru rate limiting)
response: Response-ul HTTP (pentru header-e)
Returns:
Token-urile JWT și informațiile utilizatorului
Raises:
HTTPException 400: Pentru server_id invalid
HTTPException 401: Pentru credențiale invalide
HTTPException 500: Pentru erori de sistem
"""
try:
# Log tentativa de autentificare
client_ip = request.client.host if request.client else "unknown"
server_info = f" on server {login_data.server_id}" if login_data.server_id else ""
logger.info(f"Login attempt for user {login_data.username}{server_info} from IP {client_ip}")
# Validare server_id dacă specificat (multi-server mode)
if login_data.server_id:
from backend.config import settings
from shared.database.oracle_pool import oracle_pool
# Verifică dacă serverul există în configurație
server_config = settings.get_oracle_server(login_data.server_id)
if not server_config:
logger.warning(f"Invalid server_id '{login_data.server_id}' in login request")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid server_id: '{login_data.server_id}'. Server not found in configuration."
)
# Verifică dacă serverul este înregistrat în pool
if not oracle_pool.is_server_registered(login_data.server_id):
logger.warning(f"Server '{login_data.server_id}' not registered in pool")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Server '{login_data.server_id}' is not available."
)
# Autentifică și creează token-urile
success, token_response, error_message = await auth_service.authenticate_and_create_tokens(
login_data.username,
login_data.password,
login_data.server_id
)
if not success:
logger.warning(f"Failed login attempt for user {login_data.username}{server_info}: {error_message}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=error_message or "Authentication failed"
)
# token_response.user este deja populat corect de auth_service.authenticate_and_create_tokens
# cu username-ul Oracle rezolvat (nu email-ul) și lista de firme
# Header-e de securitate
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
logger.info(f"Successful login for user {login_data.username}{server_info}")
return token_response
except HTTPException:
# Re-raise HTTP exceptions as-is (e.g., 401 for invalid credentials)
raise
except AuthenticationError as e:
logger.error(f"Authentication error for user {login_data.username}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=str(e)
)
except Exception as e:
logger.error(f"Unexpected error during login for user {login_data.username}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal authentication error"
)
@router.post("/refresh", response_model=TokenResponse, status_code=status.HTTP_200_OK)
async def refresh_token(refresh_data: RefreshTokenRequest) -> TokenResponse:
"""
Reîmprospătează access token-ul folosind refresh token-ul
Args:
refresh_data: Refresh token-ul valid
Returns:
Noul access token și informațiile utilizatorului
Raises:
HTTPException: Pentru refresh token-uri invalide
"""
try:
# Validează refresh token-ul
token_data = jwt_handler.verify_token(refresh_data.refresh_token)
if not token_data or token_data.token_type != "refresh":
logger.warning("Invalid refresh token provided")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token"
)
# Obține datele actualizate ale utilizatorului
companies = await auth_service.get_user_companies(token_data.username)
permissions = ["read", "reports"] # Poate fi extins în viitor
# Creează noul access token
new_access_token = jwt_handler.create_access_token(
username=token_data.username,
companies=companies,
user_id=token_data.user_id,
permissions=permissions
)
# Informațiile utilizatorului
current_user = CurrentUser(
username=token_data.username,
user_id=token_data.user_id,
companies=companies,
permissions=permissions
)
token_response = TokenResponse(
access_token=new_access_token,
token_type="bearer",
expires_in=jwt_handler.access_token_expire_minutes * 60,
user=current_user
)
logger.info(f"Token refreshed for user {token_data.username}")
return token_response
except Exception as e:
logger.error(f"Error refreshing token: {str(e)}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token refresh failed"
)
@router.post("/logout", status_code=status.HTTP_200_OK)
async def logout(
logout_data: Optional[LogoutRequest] = None,
current_user: CurrentUser = Depends(get_current_user)
) -> dict:
"""
Deconectează utilizatorul (invalidează token-urile)
Note: În implementarea curentă, token-urile JWT sunt stateless,
deci nu pot fi invalidate direct. În viitor poate fi implementat
un blacklist pentru token-uri.
Args:
logout_data: Date pentru logout (opțional)
current_user: Utilizatorul curent autentificat
Returns:
Confirmarea deconectării
"""
logger.info(f"User {current_user.username} logged out")
# În viitor, aici se poate implementa:
# - Adăugarea token-ului într-un blacklist
# - Invalidarea tuturor sesiunilor utilizatorului
# - Notificări de securitate
return {
"message": "Successfully logged out",
"username": current_user.username,
"logout_time": datetime.now().isoformat()
}
@router.get("/me", response_model=CurrentUser)
async def get_current_user_info(
current_user: CurrentUser = Depends(get_current_user)
) -> CurrentUser:
"""
Returnează informațiile despre utilizatorul curent
Args:
current_user: Utilizatorul curent autentificat
Returns:
Informațiile complete ale utilizatorului
"""
logger.debug(f"User info requested for {current_user.username}")
return current_user
@router.get("/companies", response_model=List[UserCompany])
async def get_user_companies(
current_user: CurrentUser = Depends(get_current_user)
) -> List[UserCompany]:
"""
Returnează lista firmelor la care utilizatorul are acces
Args:
current_user: Utilizatorul curent autentificat
Returns:
Lista firmelor cu permisiunile asociate
"""
try:
# Obține firmele actualizate din baza de date
companies = await auth_service.get_user_companies(current_user.username)
user_companies = []
for i, company_code in enumerate(companies):
# Obține permisiunile pentru fiecare firmă
permissions = await auth_service.get_user_permissions(
current_user.username,
company_code
)
user_company = UserCompany(
code=company_code,
permissions=permissions,
is_default=(i == 0) # Prima firmă ca default
)
user_companies.append(user_company)
logger.debug(f"Returned {len(user_companies)} companies for user {current_user.username}")
return user_companies
except Exception as e:
logger.error(f"Error getting companies for user {current_user.username}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error retrieving user companies"
)
@router.post("/check-company-access", response_model=CompanyAccessResponse)
async def check_company_access(
access_request: CompanyAccessRequest,
current_user: CurrentUser = Depends(get_current_user)
) -> CompanyAccessResponse:
"""
Verifică dacă utilizatorul are acces la o firmă specifică
Args:
access_request: Request-ul de verificare acces
current_user: Utilizatorul curent autentificat
Returns:
Răspunsul cu informații despre acces
"""
try:
has_access = await auth_service.validate_user_company_access(
current_user.username,
access_request.company_code
)
if not has_access:
return CompanyAccessResponse(
has_access=False,
company=None,
missing_permissions=None
)
# Obține permisiunile pentru firmă
permissions = await auth_service.get_user_permissions(
current_user.username,
access_request.company_code
)
# Verifică permisiunile cerute
missing_permissions = []
if access_request.required_permissions:
missing_permissions = [
perm for perm in access_request.required_permissions
if perm not in permissions
]
user_company = UserCompany(
code=access_request.company_code,
permissions=permissions
)
return CompanyAccessResponse(
has_access=True,
company=user_company,
missing_permissions=missing_permissions if missing_permissions else None
)
except Exception as e:
logger.error(f"Error checking company access: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error checking company access"
)
@router.get("/my-servers", response_model=dict)
async def get_my_servers(
current_user: CurrentUser = Depends(get_current_user)
) -> dict:
"""
Returnează lista serverelor la care utilizatorul autentificat are acces (US-006).
Acest endpoint este folosit de frontend pentru a popula dropdown-ul de server switch.
Lookup-ul se face pe baza email-ului sau username-ului utilizatorului curent.
Args:
current_user: Utilizatorul curent autentificat
Returns:
Dict cu lista de servere: {servers: [{id: string, name: string}, ...]}
"""
try:
from .email_server_cache import email_server_cache
from backend.config import settings
logger.info(f"Get my-servers request for user '{current_user.username}'")
# Try email lookup first (faster, from cache)
server_ids: List[str] = []
if current_user.email:
server_ids = email_server_cache.get_servers_for_email(current_user.email)
logger.debug(f"Email lookup for '{current_user.email}': {server_ids}")
# If no email or no results, try username lookup (queries Oracle directly)
if not server_ids:
server_ids = await email_server_cache.get_servers_for_username(current_user.username)
logger.debug(f"Username lookup for '{current_user.username}': {server_ids}")
# Build server info list with human-readable names
servers: List[ServerInfo] = []
for server_id in server_ids:
server_config = settings.get_oracle_server(server_id)
if server_config:
servers.append(ServerInfo(
id=server_config.id,
name=server_config.name
))
else:
# Fallback if server config not found
logger.warning(f"Server '{server_id}' not found in config")
servers.append(ServerInfo(id=server_id, name=server_id))
logger.info(f"User '{current_user.username}' has access to {len(servers)} server(s)")
return {"servers": [s.model_dump() for s in servers]}
except Exception as e:
logger.error(f"Error getting servers for user '{current_user.username}': {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error retrieving user servers"
)
@router.get("/status")
async def get_auth_status(
current_user: Optional[CurrentUser] = Depends(get_optional_user)
) -> dict:
"""
Returnează statusul de autentificare (endpoint public)
Args:
current_user: Utilizatorul curent (opțional)
Returns:
Statusul de autentificare
"""
if current_user:
return {
"authenticated": True,
"username": current_user.username,
"companies_count": len(current_user.companies),
"permissions": current_user.permissions
}
else:
return {
"authenticated": False,
"username": None,
"companies_count": 0,
"permissions": []
}
# Rute de administrare (opționale)
if include_admin_routes:
@router.get("/admin/stats", response_model=AuthStats)
async def get_auth_stats(
current_user: CurrentUser = Depends(get_current_user)
) -> AuthStats:
"""
Returnează statistici despre sistemul de autentificare
Necesită permisiuni de admin.
"""
# Verifică permisiuni admin
if "admin" not in current_user.permissions:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin permissions required"
)
cache_stats = auth_service.get_cache_stats()
return AuthStats(
total_users=1, # Placeholder - poate fi implementat
active_sessions=1, # Placeholder - poate fi implementat
cache_hit_ratio=cache_stats.get('cache_hit_ratio', 0),
last_cleanup=datetime.now()
)
@router.post("/admin/refresh-cache")
async def refresh_user_cache(
username: Optional[str] = None,
current_user: CurrentUser = Depends(get_current_user)
) -> dict:
"""
Reîmprospătează cache-ul utilizatorilor
Necesită permisiuni de admin.
"""
if "admin" not in current_user.permissions:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin permissions required"
)
if username:
success = await auth_service.refresh_user_data(username)
return {
"message": f"Cache refreshed for user {username}",
"success": success
}
else:
auth_service.clear_cache()
return {"message": "All user cache cleared"}
return router
# Router implicit pentru folosire rapidă
auth_router = create_auth_router()
# Router cu rute de admin incluse
auth_router_with_admin = create_auth_router(include_admin_routes=True)