229 lines
6.3 KiB
Python
229 lines
6.3 KiB
Python
"""
|
|
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
|
|
from app.models.refresh_token import RefreshToken
|
|
|
|
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
|
|
from app.core.cache import cache
|
|
|
|
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)
|
|
|
|
# Conectar a Redis cache
|
|
await cache.connect()
|
|
logger.info("Caché Redis conectado")
|
|
|
|
if settings.ENVIRONMENT == "development":
|
|
await create_tables()
|
|
logger.info("Tablas de base de datos verificadas")
|
|
|
|
yield
|
|
|
|
# Shutdown
|
|
logger.info("Cerrando ServiceManagerWeb Backend")
|
|
await cache.disconnect()
|
|
logger.info("Caché Redis desconectado")
|
|
|
|
|
|
# Crear aplicación FastAPI
|
|
app = FastAPI(
|
|
title="ServiceManagerWeb API",
|
|
description="Mesa de Ayuda B2B multi-tenant para Aduanasoft",
|
|
version=settings.APP_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
|
|
|
|
if settings.is_production():
|
|
cors_allow_methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
|
|
cors_allow_headers = [
|
|
"Authorization",
|
|
"Content-Type",
|
|
"X-Tenant-ID",
|
|
"X-Tenant-Slug",
|
|
"X-Correlation-ID",
|
|
]
|
|
else:
|
|
cors_allow_methods = ["*"]
|
|
cors_allow_headers = ["*"]
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=cors_origins,
|
|
allow_credentials=True,
|
|
allow_methods=cors_allow_methods,
|
|
allow_headers=cors_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.APP_VERSION,
|
|
"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.APP_VERSION,
|
|
"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"
|
|
)
|