Afficher l'IP client (x-forwarded-for) dans les logs des requêtes API
This commit is contained in:
@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user