Files
plantillas-proyectos/backend/core/middleware.py
acazares b68c4316ff Refactor backend and frontend code for improved structure and functionality
- Rearranged imports in multiple files for consistency and clarity.
- Updated logging middleware to exclude specific paths from logging.
- Enhanced security module by cleaning up token handling and improving tenant validation.
- Added tenant and company scoped mixins for better database model management.
- Implemented generic CRUD routes for tenant-scoped resources.
- Improved error handling and response management in API routes.
- Cleaned up login and logout processes to ensure proper session management.
- Introduced mechanisms to clear local storage and cookies on tenant change.
- Enhanced company store to detect tenant changes and clear data accordingly.
- Added new DTO mixins for currency and value affect flags.
2025-11-11 17:20:47 -06:00

191 lines
6.2 KiB
Python

"""
Middleware personalizado para Anexo76
- Validación de licencias
- Gestión de multi-tenancy
- Logging de requests
"""
import logging
import time
from typing import Callable
from fastapi import HTTPException, Request
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):
"""
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
# Permitir acceso sin autenticación a rutas de documentación y salud
doc_prefixes = ["/api/redoc", "/api/openapi.json"]
public_prefixes = ["/api/v1/auth", "/api/v1/status", "/api/health", "/api/"]
path = request.url.path
# Permitir cualquier subruta de docs/redoc/openapi.json (por ejemplo, /api/docs, /api/docs/, /api/docs/oauth2-redirect)
if any(
path == prefix or path.startswith(prefix + "/") for prefix in doc_prefixes
):
return await call_next(request)
# Permitir rutas públicas exactas o con prefijo
if any(
path == prefix or (prefix != "/" and path.startswith(prefix))
for prefix in public_prefixes
):
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)
# ⚠️ NOTA: tenant_id puede ser None para usuarios SSO que aún no tienen tenant asignado
# En ese caso, el endpoint específico deberá manejarlo
if not tenant_id:
logger.warning(
f"⚠️ Token sin tenant_id para usuario: {user_info.get('sub', 'unknown')}"
)
# No lanzamos error aquí, dejamos que el endpoint decida qué hacer
# Agregar tenant_id al state del request (puede ser None)
request.state.tenant_id = tenant_id
request.state.user_info = user_info
except HTTPException:
# Re-lanzar HTTPException directamente
raise
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 = [
"/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.a76.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