Files
openmind/backend/app/main.py
snoopy 0b2e5e326d OpenMind v1 : cartes mentales libres, local-first et dockerisees
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>
2026-09-08 22:19:18 +02:00

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()