Files
openmind/backend/app/main.py
snoopy 49870dcd56 Phase 2.1 : preparation au deploiement public
Rend une instance exposee sur Internet exploitable sans la transformer en
infrastructure : le produit reste local-first, sans compte et sans telemetrie.

Protection du service : limitation de debit en memoire par client et par
famille d'endpoint (health et ready jamais limites), interrupteur
SHARING_ENABLED, capacite maximale de partages, taille propre aux partages.
X-Forwarded-For n'est cru que si TRUST_PROXY_HEADERS l'autorise, et on lit alors
la derniere valeur, celle ajoutee par le proxy.

Expiration facultative des partages (24 h a 90 jours, ou jamais) : un lien
expire repond 410 et la page publique le dit, sans autre detail. Le proprietaire
peut prolonger ; aucune purge automatique.

Protection des liens non repertories : robots.txt, X-Robots-Tag et meta robots
sur /s/ et /api/, Referrer-Policy no-referrer sur tout le site, redaction des
jetons dans les journaux d'acces, share_token porte a 128 bits (les anciens
restent valides).

Schema gere par Alembic, applique au demarrage. Une base creee avant Alembic est
estampillee puis migree : verifie sur la base reelle de developpement, 4
partages inchanges et expires_at NULL.

Administration par CLI locale au conteneur (stats, liste, suppression, purge,
sauvegarde a chaud via l'API SQLite, restauration prudente) : aucune interface
/admin exposee.

Le jeton de gestion peut enfin etre exporte dans un fichier .openmind-share puis
reimporte depuis un autre navigateur, apres verification aupres du serveur. Il
n'est toujours jamais ajoute a un export .mmap.

Ajoute une page /privacy dont le texte est centralise, et corrige un bug trouve
en observant les logs : Alembic, lance dans le processus applicatif,
reappliquait la configuration de logs d'alembic.ini et desactivait les loggers
existants, ce qui faisait disparaitre les journaux d'acces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 09:22:17 +02:00

110 lines
3.7 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},
headers=exc.headers or None,
)
@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()