Files
roa2web-service-auto/shared/auth/middleware.py
Marius Mutu 1378ee1e6a Implement hybrid two-tier cache system with full monitoring and Telegram bot enhancements
Cache System (Backend):
- Implemented two-tier hybrid cache: L1 (in-memory) + L2 (SQLite)
- L1 cache: Fast dictionary-based with 5-minute TTL for hot data
- L2 cache: Persistent SQLite with 1-hour TTL for warm data
- Cache decorator with automatic tier management and fallback
- Cache key generation with per-user isolation
- Event monitoring system for cache statistics
- Cache benchmarking utilities for performance testing
- Added cache management endpoints: /api/cache/stats, /api/cache/clear, /api/cache/benchmark
- Cache configuration via environment variables (CACHE_ENABLED, CACHE_L1_TTL, etc.)

Backend Services:
- Updated dashboard_service to use @cached decorator with request context
- Added cache support to invoice_service and treasury_service
- Integrated cache manager into main.py with lifespan events
- Added Request parameter to service methods for cache metadata

Frontend Enhancements:
- New CacheStatsView.vue for real-time cache monitoring dashboard
- Cache store (cacheStore.js) for state management
- Updated router to include /cache-stats route
- Navigation updates in DashboardHeader and HamburgerMenu
- Cache stats accessible from main navigation

Telegram Bot Improvements:
- Enhanced formatters with YTD comparison data
- Improved menu navigation and button layout
- Better error handling and user feedback
- Bot startup improvements with graceful shutdown

Auth & Middleware:
- Enhanced middleware with cache metadata injection
- Improved request state handling for cache source tracking

Development:
- Updated start-dev.sh with better error handling
- Added TELEGRAM_EMAIL_AUTH_PLAN.md documentation
- Updated requirements.txt with aiosqlite for async SQLite

Performance:
- L1 cache provides <1ms response for hot data
- L2 cache provides ~5ms response for warm data
- Database queries only for cold data or cache misses
- Cache hit rates tracked and displayed in real-time

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 22:42:00 +02:00

378 lines
13 KiB
Python

"""
FastAPI Authentication Middleware pentru ROA2WEB
Acest modul oferă middleware pentru autentificarea automată în aplicațiile FastAPI,
incluzând extragerea token-urilor, validarea și injectarea datelor utilizatorului
în contextul request-ului.
Funcționalități:
- Extragere automată token JWT din header Authorization
- Validare token și user data injection
- Rate limiting pentru endpoint-urile de autentificare
- Logging pentru securitate și monitoring
"""
import logging
import time
from typing import Optional, Callable, Dict, Any, List, Set
from collections import defaultdict, deque
from datetime import datetime, timedelta
from fastapi import Request, Response, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
from .jwt_handler import jwt_handler, TokenData
from .auth_service import auth_service
from .models import CurrentUser, AuthError
logger = logging.getLogger(__name__)
class RateLimiter:
"""
Rate limiter pentru protejarea endpoint-urilor de autentificare
"""
def __init__(self, max_requests: int = 5, time_window: int = 300):
"""
Inițializează rate limiter
Args:
max_requests: Numărul maxim de request-uri permise
time_window: Fereastra de timp în secunde
"""
self.max_requests = max_requests
self.time_window = time_window
self.requests: Dict[str, deque] = defaultdict(deque)
def is_allowed(self, client_ip: str) -> bool:
"""
Verifică dacă request-ul este permis pentru acest IP
Args:
client_ip: Adresa IP a clientului
Returns:
True dacă request-ul este permis
"""
now = time.time()
client_requests = self.requests[client_ip]
# Șterge request-urile vechi
while client_requests and client_requests[0] < now - self.time_window:
client_requests.popleft()
# Verifică dacă putem accepta încă un request
if len(client_requests) >= self.max_requests:
return False
# Adaugă request-ul curent
client_requests.append(now)
return True
def get_reset_time(self, client_ip: str) -> int:
"""
Returnează timpul când rate limiting se resetează pentru acest IP
Args:
client_ip: Adresa IP a clientului
Returns:
Timestamp când se resetează
"""
client_requests = self.requests[client_ip]
if not client_requests:
return int(time.time())
return int(client_requests[0] + self.time_window)
class AuthenticationMiddleware(BaseHTTPMiddleware):
"""
Middleware pentru autentificarea automată în FastAPI
Acest middleware:
- Extrage token-ul JWT din header-ul Authorization
- Validează token-ul și obține datele utilizatorului
- Injectează utilizatorul curent în request.state
- Aplică rate limiting pentru endpoint-urile sensibile
"""
def __init__(
self,
app,
excluded_paths: Optional[List[str]] = None,
rate_limit_paths: Optional[List[str]] = None,
rate_limiter: Optional[RateLimiter] = None
):
"""
Inițializează middleware-ul
Args:
app: Aplicația FastAPI
excluded_paths: Căile care nu necesită autentificare
rate_limit_paths: Căile cu rate limiting
rate_limiter: Instance de rate limiter personalizat
"""
super().__init__(app)
self.excluded_paths = excluded_paths or [
"/docs", "/redoc", "/openapi.json", "/health", "/",
"/auth/login", "/auth/register"
]
self.rate_limit_paths = rate_limit_paths or [
"/auth/login", "/auth/register", "/auth/forgot-password"
]
self.rate_limiter = rate_limiter or RateLimiter(max_requests=5, time_window=300)
logger.info(f"Authentication middleware initialized with {len(self.excluded_paths)} excluded paths")
def _get_client_ip(self, request: Request) -> str:
"""Obține adresa IP a clientului"""
# Verifică header-ele proxy
forwarded_for = request.headers.get("X-Forwarded-For")
if forwarded_for:
return forwarded_for.split(",")[0].strip()
real_ip = request.headers.get("X-Real-IP")
if real_ip:
return real_ip
# Fallback la client IP direct
return request.client.host if request.client else "unknown"
def _should_exclude_path(self, path: str) -> bool:
"""Verifică dacă path-ul trebuie exclus de la autentificare"""
# Special case for root path to avoid excluding all paths that start with "/"
if "/" in self.excluded_paths and path == "/":
return True
# Check other excluded paths (excluding "/" to avoid matching all paths)
excluded_paths_no_root = [p for p in self.excluded_paths if p != "/"]
return any(path.startswith(excluded) for excluded in excluded_paths_no_root)
def _should_rate_limit_path(self, path: str) -> bool:
"""Verifică dacă path-ul necesită rate limiting"""
return any(path.startswith(limited) for limited in self.rate_limit_paths)
def _extract_token_from_header(self, request: Request) -> Optional[str]:
"""
Extrage token-ul JWT în header-ul Authorization
Args:
request: Request-ul HTTP
Returns:
Token-ul JWT sau None
"""
authorization = request.headers.get("Authorization")
if not authorization:
return None
if not authorization.startswith("Bearer "):
return None
return authorization[7:] # Elimină "Bearer "
async def _create_current_user(self, token_data: TokenData) -> CurrentUser:
"""
Creează obiectul CurrentUser din token data
Args:
token_data: Datele din token
Returns:
Obiectul CurrentUser
"""
return CurrentUser(
username=token_data.username,
user_id=token_data.user_id,
companies=token_data.companies,
permissions=token_data.permissions,
last_login=datetime.now()
)
async def _handle_rate_limiting(self, request: Request, path: str) -> Optional[Response]:
"""
Gestionează rate limiting pentru căile sensibile
Args:
request: Request-ul HTTP
path: Calea request-ului
Returns:
Response cu eroare dacă este rate limited, None altfel
"""
if not self._should_rate_limit_path(path):
return None
client_ip = self._get_client_ip(request)
if not self.rate_limiter.is_allowed(client_ip):
reset_time = self.rate_limiter.get_reset_time(client_ip)
logger.warning(f"Rate limit exceeded for IP {client_ip} on path {path}")
error = AuthError(
error="rate_limit_exceeded",
error_description="Too many requests. Please try again later.",
error_code="RATE_LIMIT_001"
)
return JSONResponse(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
content=error.dict(),
headers={
"X-RateLimit-Limit": str(self.rate_limiter.max_requests),
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": str(reset_time),
"Retry-After": str(reset_time - int(time.time()))
}
)
return None
async def dispatch(self, request: Request, call_next: Callable) -> Response:
"""
Procesează request-ul prin middleware
Args:
request: Request-ul HTTP
call_next: Următorul handler din pipeline
Returns:
Response-ul HTTP
"""
print(f"[ORIGINAL MIDDLEWARE] dispatch called for path: {request.url.path}")
start_time = time.time()
path = request.url.path
# IMPORTANT: Allow OPTIONS requests (CORS preflight) to pass through
if request.method == "OPTIONS":
response = await call_next(request)
return response
# Rate limiting pentru căile sensibile
rate_limit_response = await self._handle_rate_limiting(request, path)
if rate_limit_response:
return rate_limit_response
# Skip autentificare pentru căile excluse
if self._should_exclude_path(path):
request.state.user = None
request.state.is_authenticated = False
response = await call_next(request)
return response
# Extrage token-ul
print(f"[MIDDLEWARE DEBUG] Extracting token for path: {path}")
token = self._extract_token_from_header(request)
print(f"[MIDDLEWARE DEBUG] Extracted token: {token[:30] if token else 'None'}...")
if not token:
# Nu există token - pentru endpoint-urile protejate returnează 401
logger.warning(f"No token provided for protected path {path}")
error = AuthError(
error="authentication_required",
error_description="Authentication required",
error_code="AUTH_003"
)
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content=error.dict(),
headers={"WWW-Authenticate": "Bearer"}
)
# Validează token-ul
print(f"[MIDDLEWARE DEBUG] Validating token: {token[:30]}...")
token_data = jwt_handler.verify_token(token)
print(f"[MIDDLEWARE DEBUG] Token validation result: {token_data}")
if not token_data:
# Token invalid
logger.warning(f"Invalid token used for path {path}")
error = AuthError(
error="invalid_token",
error_description="The provided token is invalid or expired.",
error_code="AUTH_001"
)
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content=error.dict(),
headers={"WWW-Authenticate": "Bearer"}
)
# Token valid - creează utilizatorul curent
try:
current_user = await self._create_current_user(token_data)
request.state.user = current_user
request.state.is_authenticated = True
request.state.token_data = token_data
logger.debug(f"User {current_user.username} authenticated successfully for path {path}")
except Exception as e:
logger.error(f"Error creating current user: {str(e)}")
error = AuthError(
error="authentication_error",
error_description="Authentication processing error.",
error_code="AUTH_002"
)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=error.dict()
)
# Procesează request-ul
response = await call_next(request)
# Adaugă header-e de securitate
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
# Log timpul de procesare
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
class HTTPBearerOptional(HTTPBearer):
"""
Versiune opțională pentru autentificare care nu aruncă excepții
dacă token-ul lipsește - utile pentru endpoint-urile care
pot funcționa atât cu cât și fără autentificare
"""
async def __call__(self, request: Request) -> Optional[HTTPAuthorizationCredentials]:
"""
Extrage credențialele de autentificare fără să arunce excepții
Args:
request: Request-ul HTTP
Returns:
Credențialele sau None
"""
try:
return await super().__call__(request)
except HTTPException:
return None
# Instance predefinite pentru folosire rapidă
security_optional = HTTPBearerOptional(auto_error=False)
security_required = HTTPBearer()
# Rate limiter default
default_rate_limiter = RateLimiter(max_requests=5, time_window=300)