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>
110 lines
3.7 KiB
Python
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()
|