Premiere version fonctionnelle, demarrable avec `docker compose up -d --build` sur http://localhost:8080. Architecture - Frontend React 18 / TypeScript strict / Vite / Tailwind, canvas @xyflow/react, etat Zustand, persistance IndexedDB via Dexie, auto-layout dagre. - Backend FastAPI / SQLAlchemy 2 / Pydantic v2 / SQLite, qui sert aussi les fichiers statiques du frontend compile. - Image Docker multi-stage, un seul service, bind mount ./data pour SQLite. Local-first Les cartes restent dans le navigateur. Le serveur ne recoit une carte que sur demande explicite de partage. Partage Deux secrets distincts : share_token public en lecture seule dans l'URL, et management_token conserve uniquement cote client, transmis en en-tete pour les modifications et stocke hashe en base. Fonctionnalites Tableau de bord, editeur complet (undo/redo, copier/coller, raccourcis, branches repliables, recherche), 9 themes, 4 dispositions automatiques, panneau de proprietes, import/export .mmap, export PNG/SVG/PDF, impression, page publique en lecture seule, PWA. Qualite 25 tests pytest et 43 tests vitest au vert, ruff et ESLint sans erreur, TypeScript strict sans erreur. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
106 lines
3.6 KiB
Python
106 lines
3.6 KiB
Python
"""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()
|