""" ServiceManagerWeb Backend - FastAPI Application Mesa de Ayuda B2B multi-tenant con Clean Architecture """ from fastapi import FastAPI, Request, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware from fastapi.responses import JSONResponse from contextlib import asynccontextmanager import structlog import time import uuid from app.core.config import get_settings from app.core.database import engine, create_tables # Import models to register them with SQLAlchemy from app.models.tenant import Tenant from app.models.system import System from app.models.category import Category from app.models.user import User from app.models.ticket import Ticket from app.models.comment import TicketComment from app.models.attachment import TicketAttachment from app.models.audit import AuditLog # Nuevo modelo para auditoría from app.models.refresh_token import RefreshToken # Modelo para refresh tokens from app.core.logging import setup_logging from app.api.v1.router import api_router from app.middleware.tenant import TenantMiddleware from app.middleware.correlation_id import CorrelationIDMiddleware settings = get_settings() setup_logging() logger = structlog.get_logger() @asynccontextmanager async def lifespan(app: FastAPI): """Lifecycle manager para la aplicación.""" # Startup logger.info("Iniciando ServiceManagerWeb Backend", version=settings.API_VERSION) if settings.ENVIRONMENT == "development": await create_tables() logger.info("Tablas de base de datos verificadas") yield # Shutdown logger.info("Cerrando ServiceManagerWeb Backend") # Crear aplicación FastAPI app = FastAPI( title="ServiceManagerWeb API", description="Mesa de Ayuda B2B multi-tenant para Aduanasoft", version=settings.API_VERSION, lifespan=lifespan, docs_url=f"/{settings.API_VERSION}/docs" if settings.ENVIRONMENT == "development" else None, redoc_url=f"/{settings.API_VERSION}/redoc" if settings.ENVIRONMENT == "development" else None, openapi_url=f"/{settings.API_VERSION}/openapi.json" ) # =================================== # MIDDLEWARE # =================================== # CORS cors_origins = settings.CORS_ORIGINS.split(",") if isinstance(settings.CORS_ORIGINS, str) else settings.CORS_ORIGINS app.add_middleware( CORSMiddleware, allow_origins=cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Compression app.add_middleware(GZipMiddleware, minimum_size=1000) # Custom middleware app.add_middleware(CorrelationIDMiddleware) app.add_middleware(TenantMiddleware) # Request logging middleware @app.middleware("http") async def request_logging_middleware(request: Request, call_next): """Log todas las requests con métricas de performance.""" start_time = time.time() correlation_id = getattr(request.state, "correlation_id", str(uuid.uuid4())) # Log request logger.info( "Request iniciada", method=request.method, url=str(request.url), correlation_id=correlation_id, user_agent=request.headers.get("user-agent"), remote_addr=request.client.host if request.client else None ) # Process request response = await call_next(request) # Log response duration = time.time() - start_time logger.info( "Request completada", method=request.method, url=str(request.url), status_code=response.status_code, duration=f"{duration:.3f}s", correlation_id=correlation_id ) # Add correlation ID to response headers response.headers["X-Correlation-ID"] = correlation_id return response # =================================== # EXCEPTION HANDLERS # =================================== @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): """Handler global para excepciones no capturadas.""" correlation_id = getattr(request.state, "correlation_id", str(uuid.uuid4())) logger.error( "Excepción no manejada", error=str(exc), correlation_id=correlation_id, url=str(request.url), method=request.method, exc_info=True ) return JSONResponse( status_code=500, content={ "success": False, "error": { "code": "INTERNAL_ERROR", "message": "Error interno del servidor" }, "correlation_id": correlation_id } ) # =================================== # ROUTES # =================================== # Health check endpoint @app.get("/health") async def health_check(): """Health check para load balancer y monitoring.""" return { "status": "healthy", "service": "ServiceManagerWeb API", "version": settings.API_VERSION, "environment": settings.ENVIRONMENT } # Root endpoint @app.get("/") async def root(): """Endpoint raíz con información básica.""" return { "service": "ServiceManagerWeb API", "version": settings.API_VERSION, "docs": f"/{settings.API_VERSION}/docs", "environment": settings.ENVIRONMENT } # API routes app.include_router( api_router, prefix=f"/{settings.API_VERSION}", responses={ 400: {"description": "Bad Request"}, 401: {"description": "Unauthorized"}, 403: {"description": "Forbidden"}, 404: {"description": "Not Found"}, 422: {"description": "Validation Error"}, 500: {"description": "Internal Server Error"} } ) if __name__ == "__main__": import uvicorn uvicorn.run( "app.main:app", host="0.0.0.0", port=8000, reload=settings.ENVIRONMENT == "development" )