"""Point d'entree de l'application FastAPI. Ce fichier se limite a l'assemblage : configuration, middlewares, gestion des erreurs, montage des routeurs et du frontend statique. """ from __future__ import annotations import logging from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from app.api import api_router from app.config import get_settings from app.db.init_db import init_db from app.logging_config import configure_logging from app.middleware import MaxBodySizeMiddleware, SecurityHeadersMiddleware from app.services.errors import ShareError from app.web import mount_frontend logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): # type: ignore[no-untyped-def] settings = get_settings() configure_logging(settings.log_level) init_db() logger.info( "%s demarre (max_nodes=%s, max_map_size_mb=%s)", settings.app_name, settings.max_nodes, settings.max_map_size_mb, ) yield logger.info("Arret de %s", settings.app_name) def create_app() -> FastAPI: settings = get_settings() configure_logging(settings.log_level) app = FastAPI( title=f"{settings.app_name} API", version="1.0.0", description="API de partage de cartes. Les cartes privees restent dans le navigateur.", lifespan=lifespan, docs_url="/api/docs", openapi_url="/api/openapi.json", redoc_url=None, ) app.add_middleware(SecurityHeadersMiddleware) app.add_middleware(MaxBodySizeMiddleware, max_body_size=settings.max_map_size_bytes) # CORS restrictif : par defaut aucune origine externe, l'app etant servie # par ce meme serveur (meme origine). origins = settings.cors_origin_list if origins: app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=False, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["Content-Type", "X-Management-Token"], ) register_exception_handlers(app) app.include_router(api_router) mount_frontend(app, settings.static_dir) return app def register_exception_handlers(app: FastAPI) -> None: """Aucune erreur brute ne doit remonter au client.""" @app.exception_handler(ShareError) async def _share_error(_request: Request, exc: ShareError) -> JSONResponse: return JSONResponse(status_code=exc.status_code, content={"detail": exc.message}) @app.exception_handler(RequestValidationError) async def _validation_error(_request: Request, exc: RequestValidationError) -> JSONResponse: # On expose un message lisible, pas la structure interne de validation. first = exc.errors()[0] if exc.errors() else None detail = "Les donnees envoyees ne forment pas une carte valide." if first and first.get("msg"): reason = str(first["msg"]).replace("Value error, ", "") detail = f"{detail} ({reason})" return JSONResponse(status_code=422, content={"detail": detail}) @app.exception_handler(Exception) async def _unhandled(request: Request, exc: Exception) -> JSONResponse: # La traceback complete reste cote serveur. logger.exception("Erreur non geree sur %s %s", request.method, request.url.path) return JSONResponse( status_code=500, content={"detail": "Une erreur interne est survenue. Reessayez plus tard."}, ) app = create_app()