""" Middleware personalizado para Anexo76 - Validación de licencias - Gestión de multi-tenancy - Logging de requests """ from fastapi import Request, HTTPException from starlette.middleware.base import BaseHTTPMiddleware from typing import Callable import logging import time from datetime import datetime from sqlalchemy.orm import Session from .database import CoreSessionLocal from .security import verify_token, get_tenant_from_token from .config import settings logger = logging.getLogger(__name__) class TenantMiddleware(BaseHTTPMiddleware): """ Middleware para identificar y validar el tenant en cada request """ async def dispatch(self, request: Request, call_next: Callable): # Rutas públicas que no requieren tenant public_paths = [ "/docs", "/redoc", "/openapi.json", "/v1/auth", "/v1/status", "/health", "/" ] # Verificar si la ruta es pública (comparación exacta o prefijo) is_public = False for path in public_paths: if request.url.path == path or (path != "/" and request.url.path.startswith(path)): is_public = True break if is_public: return await call_next(request) # Extraer token y obtener tenant auth_header = request.headers.get("Authorization") if not auth_header or not auth_header.startswith("Bearer "): raise HTTPException(status_code=401, detail="Missing or invalid authorization header") token = auth_header.split(" ")[1] try: user_info = verify_token(token) tenant_id = get_tenant_from_token(user_info) if not tenant_id: raise HTTPException(status_code=400, detail="Tenant ID not found in token") # Agregar tenant_id al state del request request.state.tenant_id = tenant_id request.state.user_info = user_info except Exception as e: logger.error(f"Tenant validation error: {str(e)}") raise HTTPException(status_code=401, detail="Invalid authentication") response = await call_next(request) return response class LicenseValidationMiddleware(BaseHTTPMiddleware): """ Middleware para validar la licencia del tenant antes de procesar requests """ async def dispatch(self, request: Request, call_next: Callable): if not settings.LICENSE_CHECK_ENABLED: return await call_next(request) # Rutas que no requieren validación de licencia exempt_paths = [ "/docs", "/redoc", "/openapi.json", "/v1/auth", "/v1/status", "/health", "/" ] # Verificar si la ruta está exenta (comparación exacta o prefijo) is_exempt = False for path in exempt_paths: if request.url.path == path or (path != "/" and request.url.path.startswith(path)): is_exempt = True break if is_exempt: return await call_next(request) # Obtener tenant_id del request state (debe ser seteado por TenantMiddleware) tenant_id = getattr(request.state, "tenant_id", None) if not tenant_id: return await call_next(request) # Dejamos que TenantMiddleware maneje esto # Validar licencia db = CoreSessionLocal() try: # Importar aquí para evitar imports circulares from api.v1.modules.licenses.service import LicenseService license_service = LicenseService(db) license_info = license_service.validate_license(tenant_id) if not license_info["is_valid"]: raise HTTPException( status_code=402, detail=f"License validation failed: {license_info['reason']}" ) # Agregar info de licencia al request state request.state.license_info = license_info except HTTPException: raise except Exception as e: logger.error(f"License validation error: {str(e)}") raise HTTPException(status_code=500, detail="License validation error") finally: db.close() response = await call_next(request) return response class RequestLoggingMiddleware(BaseHTTPMiddleware): """ Middleware para logging de requests """ async def dispatch(self, request: Request, call_next: Callable): start_time = time.time() # Log request logger.info(f"Request: {request.method} {request.url.path}") response = await call_next(request) # Log response process_time = time.time() - start_time logger.info( f"Response: {request.method} {request.url.path} " f"Status: {response.status_code} " f"Duration: {process_time:.3f}s" ) # Agregar header con tiempo de procesamiento response.headers["X-Process-Time"] = str(process_time) return response