Bug preexistente: LicenseValidationMiddleware se registraba después de
TenantMiddleware, pero add_middleware() de Starlette invierte el orden de
ejecución (el último registrado corre primero) — así que License corría ANTES
que Tenant en la práctica, contradiciendo el comentario en middleware.py que
asumía lo contrario ("TenantMiddleware corre antes... dejó tenant en
user_info").
Efecto real: request.state.user_info nunca existía cuando License intentaba
usarlo como fallback para tenant_override → el caché de licencia (agregado en
a1bbb6b1) nunca podía activarse para requests sin X-Tenant-Override
header/cookie explícito. Confirmado en vivo contra el Hub real
(workspace.aduanasoft.com): verify-license se repetía en cada request pese al
caché. Con el orden corregido, la segunda llamada dentro del TTL ya no golpea
al Hub.
137 lines
3.9 KiB
Python
137 lines
3.9 KiB
Python
"""
|
|
Mi Aplicación
|
|
Backend API con FastAPI + Keycloak + SQLAlchemy
|
|
"""
|
|
|
|
import logging
|
|
import subprocess
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.staticfiles import StaticFiles
|
|
from pathlib import Path
|
|
from contextlib import asynccontextmanager
|
|
|
|
# Core Modules (Secondary)
|
|
import core.celery_app # Initialize Celery App
|
|
from api.v1.router import router as api_v1_router
|
|
from core.config import settings
|
|
from core.storage_s3 import ensure_s3_bucket
|
|
from core.paths import layout_path
|
|
from core.error_handlers import register_exception_handlers
|
|
from core.middleware import (
|
|
LicenseValidationMiddleware,
|
|
RequestLoggingMiddleware,
|
|
TenantMiddleware,
|
|
)
|
|
|
|
# Configurar logging
|
|
logging.basicConfig(
|
|
level=logging.INFO if not settings.DEBUG else logging.DEBUG,
|
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
)
|
|
|
|
# Crear aplicación FastAPI
|
|
app = FastAPI(
|
|
title="Mi Aplicación API",
|
|
version=settings.APP_VERSION,
|
|
description="Aplicación web multi-tenant con autenticación Workspace",
|
|
docs_url="/api/docs" if settings.DEBUG else None,
|
|
redoc_url="/api/redoc" if settings.DEBUG else None,
|
|
openapi_url="/api/openapi.json" if settings.DEBUG else None,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Registrar manejadores de excepciones
|
|
register_exception_handlers(app)
|
|
|
|
def run_migrations():
|
|
subprocess.run(["alembic", "upgrade", "head"], check=True)
|
|
|
|
# Inicializar la base de datos
|
|
async def on_startup():
|
|
"""Evento de inicio de la aplicación"""
|
|
logger.info("Iniciando la aplicación...")
|
|
#init_db()
|
|
run_migrations()
|
|
if settings.use_s3_object_storage:
|
|
ensure_s3_bucket()
|
|
logger.info("Base de datos inicializada correctamente.")
|
|
|
|
|
|
# Agregar middlewares personalizados
|
|
if settings.DEBUG:
|
|
app.add_middleware(RequestLoggingMiddleware)
|
|
|
|
app.add_middleware(LicenseValidationMiddleware)
|
|
app.add_middleware(TenantMiddleware)
|
|
|
|
# CORS debe ser el último en añadirse para que sea el más externo
|
|
# y cubra todas las respuestas, incluyendo las de los middlewares internos
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.cors_origins_list,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
# Centraliza startup para evitar on_event() (deprecated en FastAPI)
|
|
await on_startup()
|
|
yield
|
|
|
|
app.router.lifespan_context = lifespan
|
|
|
|
|
|
# Crear directorio de uploads si no existe y montar archivos estáticos
|
|
uploads_dir = Path("uploads").resolve()
|
|
uploads_dir.mkdir(parents=True, exist_ok=True)
|
|
app.mount("/api/uploads", StaticFiles(directory=str(uploads_dir)), name="uploads")
|
|
|
|
# Directorios para importación CSV (layouts: temp y errors)
|
|
Path(layout_path("imports", "temp")).mkdir(parents=True, exist_ok=True)
|
|
Path(layout_path("imports", "errors")).mkdir(parents=True, exist_ok=True)
|
|
|
|
# Registrar routers
|
|
app.include_router(api_v1_router, prefix="/api/v1")
|
|
|
|
|
|
@app.get("/api/")
|
|
async def root():
|
|
"""Root endpoint"""
|
|
return {
|
|
"name": "Mi Aplicación API",
|
|
"version": settings.APP_VERSION,
|
|
"status": "running",
|
|
"docs": "/api/docs" if settings.DEBUG else "disabled in production",
|
|
}
|
|
|
|
|
|
@app.get("/api/health")
|
|
async def health_check():
|
|
"""Health check endpoint"""
|
|
return {"status": "healthy", "environment": settings.ENVIRONMENT}
|
|
|
|
|
|
@app.get("/api/version")
|
|
async def get_version():
|
|
"""
|
|
Endpoint de versión de la aplicación
|
|
|
|
Retorna la versión de la aplicación que fue incrustada en la imagen Docker
|
|
durante el proceso de CI/CD. La versión se genera automáticamente según la rama:
|
|
- development: YY.MM.1.<short-git-hash>
|
|
- main: YY.MM.0.<commit-count>
|
|
|
|
Returns:
|
|
dict: Información de versión y entorno
|
|
"""
|
|
return {
|
|
"service": settings.APP_NAME,
|
|
"version": settings.APP_VERSION,
|
|
"environment": settings.ENVIRONMENT,
|
|
"debug": settings.DEBUG,
|
|
}
|