- Implemented SvelteKit frontend with authentication callback handling. - Created demo routes and paraglide localization functionality. - Added health check and entrypoint scripts for backend services. - Established PostgreSQL and Keycloak initialization scripts with health checks. - Introduced models for database schema using SQLAlchemy. - Configured Vite and SvelteKit for development and testing environments. - Added health check script to verify service statuses and resource usage. - Created Docker entrypoint scripts for seamless service startup.
166 lines
5.3 KiB
Python
166 lines
5.3 KiB
Python
"""
|
|
Middleware personalizado para Anexo76
|
|
- Validación de licencias
|
|
- Gestión de multi-tenancy
|
|
- Logging de requests
|
|
"""
|
|
from fastapi import Request, HTTPException
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from typing import Callable
|
|
import logging
|
|
import time
|
|
from datetime import datetime
|
|
from sqlalchemy.orm import Session
|
|
from .database import CoreSessionLocal
|
|
from .security import verify_token, get_tenant_from_token
|
|
from .config import settings
|
|
|
|
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
|
|
public_paths = [
|
|
"/docs",
|
|
"/redoc",
|
|
"/openapi.json",
|
|
"/v1/auth",
|
|
"/v1/status",
|
|
"/health",
|
|
"/"
|
|
]
|
|
|
|
# Verificar si la ruta es pública (comparación exacta o prefijo)
|
|
is_public = False
|
|
for path in public_paths:
|
|
if request.url.path == path or (path != "/" and request.url.path.startswith(path)):
|
|
is_public = True
|
|
break
|
|
|
|
if is_public:
|
|
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)
|
|
|
|
if not tenant_id:
|
|
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
|
|
|
|
# Agregar tenant_id al state del request
|
|
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")
|
|
|
|
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 = [
|
|
"/docs",
|
|
"/redoc",
|
|
"/openapi.json",
|
|
"/v1/auth",
|
|
"/v1/status",
|
|
"/health",
|
|
"/"
|
|
]
|
|
|
|
# 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.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()
|
|
|
|
# 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
|