feat: Version 1.10.0 - Refactorizacion, optimizacion UI y mejoras de seguridad
- 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
This commit is contained in:
@@ -4,28 +4,41 @@ Tenant Middleware - ServiceManagerWeb
|
||||
Middleware para manejo de multi-tenancy
|
||||
"""
|
||||
|
||||
from fastapi import Request, HTTPException, status
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.responses import Response
|
||||
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 y lo almacena
|
||||
en el estado de la request para uso posterior.
|
||||
|
||||
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",
|
||||
@@ -34,46 +47,99 @@ class TenantMiddleware(BaseHTTPMiddleware):
|
||||
"/v1/openapi.json",
|
||||
"/redoc",
|
||||
"/api/v1/redoc",
|
||||
"/v1/redoc"
|
||||
"/v1/redoc",
|
||||
}
|
||||
|
||||
|
||||
async def dispatch(self, request: Request, call_next) -> Response:
|
||||
"""Process request and add tenant information."""
|
||||
|
||||
# Skip tenant validation for excluded paths
|
||||
"""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)
|
||||
|
||||
# Extract tenant from header
|
||||
|
||||
# Extraer headers de tenant
|
||||
tenant_id = request.headers.get("X-Tenant-ID")
|
||||
tenant_slug = request.headers.get("X-Tenant-Slug")
|
||||
|
||||
# For now, we'll be more permissive in development
|
||||
# In production, tenant should be strictly required
|
||||
|
||||
# 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
|
||||
method=request.method,
|
||||
)
|
||||
# For now, continue without tenant for development
|
||||
# raise HTTPException(
|
||||
# status_code=status.HTTP_400_BAD_REQUEST,
|
||||
# detail="Tenant information required (X-Tenant-ID or X-Tenant-Slug header)"
|
||||
# )
|
||||
|
||||
# Store tenant info in request state
|
||||
request.state.tenant_id = tenant_id
|
||||
request.state.tenant_slug = tenant_slug
|
||||
|
||||
# TODO: Validate tenant exists and is active
|
||||
# This would involve a database query which we'll implement later
|
||||
|
||||
logger.debug(
|
||||
"Tenant middleware processed",
|
||||
tenant_id=tenant_id,
|
||||
tenant_slug=tenant_slug,
|
||||
path=request.url.path
|
||||
)
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user