"""Service des fichiers statiques du frontend (SPA) par FastAPI. Le build Vite est copie dans `settings.static_dir`. Toute route qui n'est ni /api/... ni un fichier existant retourne index.html, afin que le routeur cote client puisse gerer /map/:id et /s/:token. """ from __future__ import annotations import logging from pathlib import Path from fastapi import APIRouter, FastAPI, Request from fastapi.responses import FileResponse, HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles logger = logging.getLogger(__name__) _PLACEHOLDER = """ Frontend non compile

Frontend non compile

L'API est disponible sur /api/health.

En developpement, lancez npm run dev dans frontend/.

""" # Fichiers servis a la racine et qui ne doivent pas etre mis en cache longtemps. _NO_CACHE_FILES = {"index.html", "sw.js", "manifest.webmanifest"} def _safe_resolve(static_dir: Path, relative_path: str) -> Path | None: """Empeche toute sortie du dossier statique (path traversal).""" candidate = (static_dir / relative_path.lstrip("/")).resolve() try: candidate.relative_to(static_dir.resolve()) except ValueError: return None return candidate def mount_frontend(app: FastAPI, static_dir_path: str) -> None: static_dir = Path(static_dir_path) router = APIRouter(include_in_schema=False) if (static_dir / "assets").is_dir(): # Les assets Vite sont hashes : cache long sans risque. app.mount( "/assets", StaticFiles(directory=static_dir / "assets"), name="assets", ) @router.api_route("/{full_path:path}", methods=["GET", "HEAD"]) def serve_spa(full_path: str, request: Request): # type: ignore[no-untyped-def] if full_path.startswith("api/"): return JSONResponse(status_code=404, content={"detail": "Ressource introuvable."}) if not static_dir.is_dir(): return HTMLResponse(_PLACEHOLDER, status_code=200) if full_path: candidate = _safe_resolve(static_dir, full_path) if candidate is not None and candidate.is_file(): headers = ( {"Cache-Control": "no-cache"} if candidate.name in _NO_CACHE_FILES else {"Cache-Control": "public, max-age=3600"} ) return FileResponse(candidate, headers=headers) index = static_dir / "index.html" if index.is_file(): return FileResponse(index, headers={"Cache-Control": "no-cache"}) return HTMLResponse(_PLACEHOLDER, status_code=200) app.include_router(router)