Afficher l'IP client (x-forwarded-for) dans les logs des requêtes API

This commit is contained in:
Antoine Jacquin
2026-09-14 19:30:08 +02:00
parent df462ea86e
commit cea671c0ac
4 changed files with 219 additions and 21 deletions

View File

@ -37,6 +37,12 @@ le téléchargement IGN). Endpoints :
fichier en téléchargement. Marche aussi sur la webapp
légère : les tuiles viennent du cache local, aucune
délégation à la machine de traitement.
GET /api/presets → presets de couches personnalisés (couches visibles,
opacités, ordre, fond OSM) stockés sur le serveur et
partagés par tous les navigateurs connectés
POST /api/presets → créer/remplacer un preset de couches (même identifiant
= remplacement)
DELETE /api/presets/{id} → supprimer un preset de couches
Fichiers statiques : /assets (interface), /index_thumbs, /index_subtiles,
/visualisations, /DTM.
@ -128,6 +134,17 @@ else:
logger.info("Backend de génération des tuiles : local — consultation "
"autonome du cache, sans LIDAR_GENERATION_URL")
@app.middleware("http")
async def _log_client_ip(request: Request, call_next):
xff = request.headers.get("x-forwarded-for", "").split(",")[0].strip()
peer = request.client.host if request.client else None
ip = xff or peer
path = request.url.path
# Ne logguer que les requêtes API (pas les fichiers statiques)
if path.startswith("/api/") or path == "/":
logger.info(f"{request.method} {path} — client : {ip or '?'}")
return await call_next(request)
# assets/ (CSS/JS de l'interface) est créé dès le démarrage pour que le monteur
# statique soit actif même avant la première génération de l'index.
_assets_dir = OUTPUT_DIR / "assets"
@ -524,6 +541,99 @@ def _load_job_state():
_load_job_state()
# --- Presets de couches (partagés entre navigateurs) --------------------------
# Stockés dans OUTPUT_DIR/.presets.json : un preset est un jeu nommé de
# couches visibles + opacités + ordre de pile + fond OSM, créé depuis
# l'interface (bouton 💾) et partagé par tous les navigateurs connectés à
# cette webapp (y compris le Pi en mode deux machines). Le localStorage du
# navigateur reste le repli quand l'interface est servie sans webapp.
PRESETS_FILE = OUTPUT_DIR / ".presets.json"
_presets_lock = threading.Lock()
def _load_presets():
"""Lis la liste des presets depuis disque (vide si absent/corrompu)."""
try:
data = json.loads(PRESETS_FILE.read_text(encoding="utf-8"))
if isinstance(data, list):
return data
except (OSError, ValueError):
pass
return []
def _save_presets(presets):
"""Écrit la liste des presets atomiquement (fichier temporaire + replace)."""
tmp = PRESETS_FILE.with_suffix(".tmp")
tmp.write_text(json.dumps(presets, ensure_ascii=False), encoding="utf-8")
os.replace(tmp, PRESETS_FILE)
class PresetRequest(BaseModel):
id: str = Field(..., description="identifiant du preset (attribué par l'interface)")
label: str = Field(..., description="nom affiché dans l'interface")
on: list = Field(default_factory=list, description="couches visibles (clés, ordre de pile)")
opacity: dict = Field(default_factory=dict, description="opacité de chaque couche, 0–1")
osm: dict = Field(default_factory=dict, description="fond de carte OSM : on, opacity, dark")
def _sanitize_preset(req: PresetRequest) -> dict:
"""Preset à stocker : label assaini, opacités bornées entre 0 et 1."""
on = [str(k) for k in (req.on or []) if str(k).strip()]
opacity = {}
for k, v in (req.opacity or {}).items():
try:
opacity[str(k)] = min(1.0, max(0.0, float(v)))
except (TypeError, ValueError):
continue
osm_in = req.osm if isinstance(req.osm, dict) else {}
osm = {"on": bool(osm_in.get("on", False))}
try:
osm["opacity"] = min(1.0, max(0.0, float(osm_in["opacity"])))
except (KeyError, TypeError, ValueError):
pass
if "dark" in osm_in:
osm["dark"] = bool(osm_in["dark"])
pid = re.sub(r"[^a-z0-9_-]", "", str(req.id).lower())
return {
"id": pid or "p" + str(int(time.time() * 1000)),
"label": (str(req.label).strip() or "Preset")[:80],
"on": on,
"opacity": opacity,
"osm": osm,
}
@app.get("/api/presets")
def list_presets():
"""Presets de couches personnalisés (partagés entre tous les navigateurs)."""
with _presets_lock:
return {"presets": _load_presets()}
@app.post("/api/presets", dependencies=[Depends(_require_token)])
def upsert_preset(req: PresetRequest):
"""Crée ou remplace un preset de couches (même identifiant = remplacement)."""
p = _sanitize_preset(req)
with _presets_lock:
presets = [x for x in _load_presets() if x.get("id") != p["id"]]
presets.append(p)
_save_presets(presets)
return p
@app.delete("/api/presets/{preset_id}", dependencies=[Depends(_require_token)])
def delete_preset(preset_id: str):
"""Supprime un preset de couches (404 s'il est inconnu)."""
with _presets_lock:
presets = _load_presets()
kept = [x for x in presets if x.get("id") != preset_id]
if len(kept) == len(presets):
raise HTTPException(404, "preset inconnu")
_save_presets(kept)
return {"ok": True}
def bbox_to_cells(w, s, e, n):
"""Cellules L93 de 1 km (col, row) intersectant une bbox WGS84.