- Extraccion de helpers en backend: audit_helpers.py, helpers.py - Modularizacion de schemas en archivos individuales por dominio - Reduccion de audit.py en 953 lineas (74% del archivo) - Reduccion de tickets.py en 655 lineas (60% del archivo) - Expansion de auth.py con recuperacion de contrasenia y tokens - Nuevos modulos: core/email.py, core/cache.py - Reorganizacion de scripts a backend/scripts/ - Frontend: refactorizacion de audit page con array-driven components - Frontend: correccion de 11 errores ortograficos en tickets page - Frontend: proxy Docker corregido en vite.config.js - Frontend: nuevas rutas forgot-password, reset-password, organization, profile - Nuevas utilidades TS: colorUtils.ts, dateFormats.ts - 5 nuevos archivos de tests unitarios en backend/tests/unit/ - Eliminacion de 3 scripts temporales de prueba - Documentacion tecnica: CAMBIOS_v1.10.0.md, OPTIMIZACIONES_RENDIMIENTO.md
145 lines
4.7 KiB
Python
145 lines
4.7 KiB
Python
"""
|
|
Tenant Middleware - ServiceManagerWeb
|
|
|
|
Middleware para manejo de multi-tenancy
|
|
"""
|
|
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.requests import Request
|
|
from starlette.responses import Response, JSONResponse
|
|
from sqlalchemy import select
|
|
import structlog
|
|
|
|
from app.core.database import AsyncSessionLocal
|
|
from app.core.config import get_settings
|
|
from app.models.tenant import Tenant, TenantStatus
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
settings = get_settings()
|
|
|
|
|
|
class TenantMiddleware(BaseHTTPMiddleware):
|
|
"""
|
|
Middleware para extraer y validar información del tenant.
|
|
|
|
Extrae el tenant_id del header X-Tenant-ID o el slug del header
|
|
X-Tenant-Slug, valida que exista en la base de datos y que esté
|
|
activo, y almacena el objeto Tenant en request.state.tenant.
|
|
"""
|
|
|
|
# Rutas que no requieren tenant
|
|
EXCLUDED_PATHS = {
|
|
"/health",
|
|
"/",
|
|
"/api/v1/auth/login",
|
|
"/v1/auth/login",
|
|
"/api/v1/auth/refresh",
|
|
"/v1/auth/refresh",
|
|
"/api/v1/auth/forgot-password",
|
|
"/v1/auth/forgot-password",
|
|
"/api/v1/auth/reset-password",
|
|
"/v1/auth/reset-password",
|
|
"/docs",
|
|
"/api/v1/docs",
|
|
"/v1/docs",
|
|
"/openapi.json",
|
|
"/api/v1/openapi.json",
|
|
"/v1/openapi.json",
|
|
"/redoc",
|
|
"/api/v1/redoc",
|
|
"/v1/redoc",
|
|
}
|
|
|
|
async def dispatch(self, request: Request, call_next) -> Response:
|
|
"""Valida el tenant en cada request y lo almacena en request.state."""
|
|
|
|
# Inicializar state con valores por defecto
|
|
request.state.tenant = None
|
|
request.state.tenant_id = None
|
|
request.state.tenant_slug = None
|
|
|
|
# Saltar validación en rutas excluidas
|
|
if request.url.path in self.EXCLUDED_PATHS or request.url.path.startswith("/docs"):
|
|
return await call_next(request)
|
|
|
|
# Extraer headers de tenant
|
|
tenant_id = request.headers.get("X-Tenant-ID")
|
|
tenant_slug = request.headers.get("X-Tenant-Slug")
|
|
|
|
# Si no hay headers de tenant
|
|
if not tenant_id and not tenant_slug:
|
|
if settings.ENVIRONMENT == "production":
|
|
return JSONResponse(
|
|
status_code=400,
|
|
content={"detail": "Tenant information required (X-Tenant-ID or X-Tenant-Slug header)"}
|
|
)
|
|
# En desarrollo, continuar sin tenant con advertencia
|
|
logger.warning(
|
|
"Request without tenant information",
|
|
path=request.url.path,
|
|
method=request.method,
|
|
)
|
|
return await call_next(request)
|
|
|
|
# Validar tenant contra la base de datos
|
|
try:
|
|
async with AsyncSessionLocal() as session:
|
|
if tenant_id:
|
|
result = await session.execute(
|
|
select(Tenant).where(Tenant.id == tenant_id)
|
|
)
|
|
else:
|
|
result = await session.execute(
|
|
select(Tenant).where(Tenant.slug == tenant_slug)
|
|
)
|
|
tenant = result.scalars().first()
|
|
|
|
if tenant is None:
|
|
logger.warning(
|
|
"Tenant not found",
|
|
tenant_id=tenant_id,
|
|
tenant_slug=tenant_slug,
|
|
path=request.url.path,
|
|
)
|
|
return JSONResponse(
|
|
status_code=404,
|
|
content={"detail": "Tenant not found"}
|
|
)
|
|
|
|
if tenant.status != TenantStatus.ACTIVE:
|
|
logger.warning(
|
|
"Tenant is not active",
|
|
tenant_id=str(tenant.id),
|
|
tenant_slug=tenant.slug,
|
|
status=tenant.status,
|
|
path=request.url.path,
|
|
)
|
|
return JSONResponse(
|
|
status_code=403,
|
|
content={"detail": f"Tenant is {tenant.status.value}"}
|
|
)
|
|
|
|
# Almacenar tenant validado en el state
|
|
request.state.tenant = tenant
|
|
request.state.tenant_id = str(tenant.id)
|
|
request.state.tenant_slug = tenant.slug
|
|
|
|
logger.debug(
|
|
"Tenant validated",
|
|
tenant_id=str(tenant.id),
|
|
tenant_slug=tenant.slug,
|
|
path=request.url.path,
|
|
)
|
|
|
|
except Exception as exc:
|
|
logger.error(
|
|
"Error validating tenant",
|
|
error=str(exc),
|
|
path=request.url.path,
|
|
)
|
|
return JSONResponse(
|
|
status_code=503,
|
|
content={"detail": "Service temporarily unavailable"}
|
|
)
|
|
|
|
return await call_next(request) |