feat: Funcion de sistema tenants

This commit is contained in:
2026-02-23 13:01:24 -07:00
parent ceea67eb2b
commit 1ccc39732b
58 changed files with 1889 additions and 315 deletions

View File

@@ -9,8 +9,9 @@ from starlette.requests import Request
from starlette.responses import Response, JSONResponse
from sqlalchemy import select
import structlog
import uuid
from app.core.database import AsyncSessionLocal
from app.core.database import AsyncSessionLocal, get_db
from app.core.config import get_settings
from app.models.tenant import Tenant, TenantStatus
@@ -30,11 +31,17 @@ class TenantMiddleware(BaseHTTPMiddleware):
# Rutas que no requieren tenant
EXCLUDED_PATHS = {
"/health",
"/api/v1/health",
"/v1/health",
"/api/v1/health/detailed",
"/v1/health/detailed",
"/",
"/api/v1/auth/login",
"/v1/auth/login",
"/api/v1/auth/refresh",
"/v1/auth/refresh",
"/api/v1/auth/logout",
"/v1/auth/logout",
"/api/v1/auth/forgot-password",
"/v1/auth/forgot-password",
"/api/v1/auth/reset-password",
@@ -66,33 +73,58 @@ class TenantMiddleware(BaseHTTPMiddleware):
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":
tenant_uuid: uuid.UUID | None = None
if tenant_id:
try:
tenant_uuid = uuid.UUID(tenant_id)
except ValueError:
return JSONResponse(
status_code=400,
content={"detail": "Tenant information required (X-Tenant-ID or X-Tenant-Slug header)"}
content={"detail": "Invalid X-Tenant-ID header (must be UUID)"},
)
# En desarrollo, continuar sin tenant con advertencia
logger.warning(
"Request without tenant information",
path=request.url.path,
method=request.method,
# Si no hay headers de tenant (requerido para aislamiento multi-tenant)
if not tenant_id and not tenant_slug:
return JSONResponse(
status_code=400,
content={"detail": "Tenant information required (X-Tenant-ID or X-Tenant-Slug header)"},
)
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()
# Prefer DB session coming from dependency overrides (tests) when available.
# Guard: in unit tests request.app may be a MagicMock, not a real FastAPI app.
dependency_overrides = getattr(request.app, "dependency_overrides", None)
override_get_db = None
if isinstance(dependency_overrides, dict):
override_get_db = dependency_overrides.get(get_db)
if override_get_db is not None:
agen = override_get_db()
session = await agen.__anext__()
try:
if tenant_uuid is not None:
result = await session.execute(
select(Tenant).where(Tenant.id == tenant_uuid)
)
else:
result = await session.execute(
select(Tenant).where(Tenant.slug == tenant_slug)
)
tenant = result.scalars().first()
finally:
await agen.aclose()
else:
async with AsyncSessionLocal() as session:
if tenant_uuid is not None:
result = await session.execute(
select(Tenant).where(Tenant.id == tenant_uuid)
)
else:
result = await session.execute(
select(Tenant).where(Tenant.slug == tenant_slug)
)
tenant = result.scalars().first()
if tenant is None:
logger.warning(