import logging import time from typing import Callable from fastapi import HTTPException, Request, Response from starlette.middleware.base import BaseHTTPMiddleware from .config import settings from .database import CoreSessionLocal from .security import get_tenant_from_token, verify_token logger = logging.getLogger(__name__) class TenantMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next: Callable): # No validamos token, no buscamos tenant. # Esto permite que CORSMiddleware haga su trabajo. if request.method == "OPTIONS": return await call_next(request) # 2. Definición de rutas (tal cual las tenías) doc_prefixes = ["/api/redoc", "/api/openapi.json", "/api/docs"] public_prefixes = ["/api/v1/auth", "/api/v1/status", "/api/health", "/api/"] path = request.url.path # 3. Bypass para rutas públicas y docs if any(path == prefix or path.startswith(prefix + "/") for prefix in doc_prefixes): return await call_next(request) if any(path == prefix or (prefix != "/" and path.startswith(prefix)) for prefix in public_prefixes): return await call_next(request) # 4. Validación estricta de Token (solo para lo que no es público ni OPTIONS) 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) 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") # 5. Continuar con la petición real return await call_next(request) 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 = [ "/api/docs", "/api/redoc", "/openapi.json", "/api/v1/auth", "/api/v1/auth", "/api/v1/status", "/api/v1/status", "/api/health", "/api/", ] # 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.core.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() excluded_paths = [ "/api/docs", "/api/redoc", "/openapi.json", "/api/v1/status", "/api/health", ] if any( request.url.path == path or request.url.path.startswith(path + "/") for path in excluded_paths ): return await call_next(request) # 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