""" Entrypoint FastAPI – registra router, middleware, startup/shutdown. """ from contextlib import asynccontextmanager from typing import AsyncGenerator from fastapi import FastAPI, Request, status from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from slowapi.middleware import SlowAPIMiddleware from slowapi.util import get_remote_address from app.api.v1 import auth, labels, mailboxes, messages, notifications, permissions, send, tenants, users, virtual_boxes, ws from app.api.v1 import settings as settings_router from app.config import get_settings from app.core.logging import get_logger, setup_logging from app.database import engine from app.websocket.manager import redis_subscriber_loop settings = get_settings() logger = get_logger(__name__) @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: """Gestione ciclo di vita dell'applicazione.""" import asyncio setup_logging() logger.info( "🚀 PEChub Backend avviato", extra={"env": settings.app_env, "debug": settings.app_debug}, ) # Avvia il subscriber Redis per il forward degli eventi WebSocket redis_task = asyncio.create_task( redis_subscriber_loop(settings.redis_url), name="redis-ws-subscriber", ) yield # Cleanup redis_task.cancel() try: await redis_task except asyncio.CancelledError: pass # Chiudi pool arq se aperto from app.services.send_service import close_arq_pool await close_arq_pool() await engine.dispose() logger.info("🛑 PEChub Backend fermato") # ─── Applicazione FastAPI ───────────────────────────────────────────────────── limiter = Limiter(key_func=get_remote_address, default_limits=["200/minute"]) app = FastAPI( title="PEChub API", description="API per la gestione PEC SaaS multi-tenant", version="1.0.0", docs_url="/docs" if not settings.is_production else None, redoc_url="/redoc" if not settings.is_production else None, lifespan=lifespan, ) # ─── Middleware ─────────────────────────────────────────────────────────────── app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_middleware(SlowAPIMiddleware) app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins_list, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ─── Router ─────────────────────────────────────────────────────────────────── API_PREFIX = "/api/v1" app.include_router(auth.router, prefix=API_PREFIX) app.include_router(users.router, prefix=API_PREFIX) app.include_router(tenants.router, prefix=API_PREFIX) app.include_router(permissions.router, prefix=API_PREFIX) app.include_router(mailboxes.router, prefix=API_PREFIX) app.include_router(messages.router, prefix=API_PREFIX) app.include_router(send.router, prefix=API_PREFIX) app.include_router(ws.router, prefix=API_PREFIX) app.include_router(virtual_boxes.router, prefix=API_PREFIX) app.include_router(notifications.router, prefix=API_PREFIX) app.include_router(labels.router, prefix=API_PREFIX) app.include_router(settings_router.router, prefix=API_PREFIX) # ─── Health check ───────────────────────────────────────────────────────────── @app.get("/health", tags=["Health"], include_in_schema=False) async def health_check() -> dict: """Endpoint di health check per Docker/Kubernetes.""" return { "status": "ok", "version": "1.0.0", "env": settings.app_env, } @app.get("/health/db", tags=["Health"], include_in_schema=False) async def health_db() -> dict: """Verifica connessione al database.""" from sqlalchemy import text from app.database import AsyncSessionLocal try: async with AsyncSessionLocal() as session: await session.execute(text("SELECT 1")) return {"status": "ok", "database": "connected"} except Exception as e: return JSONResponse( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, content={"status": "error", "database": str(e)}, ) # ─── Error handler globale ──────────────────────────────────────────────────── @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception) -> JSONResponse: logger.exception(f"Errore non gestito: {exc}") return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={"detail": "Errore interno del server"}, )