From da24d2a8a1b975e4a87973dd1caf64d7b61e5852 Mon Sep 17 00:00:00 2001 From: Nicolas FRYDER Date: Thu, 20 Aug 2026 16:44:07 +0200 Subject: [PATCH] =?UTF-8?q?fix(admin):=20XSS=20stock=C3=A9=20dans=20l'acti?= =?UTF-8?q?vit=C3=A9,=20timeout=20des=20requ=C3=AAtes,=20d=C3=A9blocage=20?= =?UTF-8?q?manuel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit activitySummary() injectait crawl_run.error en innerHTML sans échappement. Ce message provient d'exceptions du crawler, qui contiennent du contenu scrapé sur Metal Archives — donc influençable depuis l'extérieur. api() n'avait aucun timeout : une API muette (pool DB saturé, upstream nginx silencieux) laissait la vue bloquée sur « Chargement… » indéfiniment. reset-errors vise désormais aussi les lignes coincées en 'processing', en forçage manuel du mécanisme d'auto-réparation du worker. Co-Authored-By: Claude Opus 5 --- apps/admin/site/app.js | 22 ++++++++++++++++------ apps/api/src/adminRoutes.js | 5 +++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/apps/admin/site/app.js b/apps/admin/site/app.js index 3cfcee8..f57bf88 100644 --- a/apps/admin/site/app.js +++ b/apps/admin/site/app.js @@ -33,11 +33,21 @@ const state = { // API // ------------------------------------------------------------------ async function api(path, opts = {}) { - const res = await fetch(path, { - credentials: "include", - headers: { "Content-Type": "application/json", ...(opts.headers || {}) }, - ...opts, - }); + // Timeout explicite : sans lui, une API qui ne répond jamais (pool DB saturé, + // upstream nginx muet) laisse la vue bloquée sur « Chargement… » indéfiniment. + const ctrl = new AbortController(); + const t = setTimeout(() => ctrl.abort(), opts.timeoutMs || 20000); + let res; + try { + res = await fetch(path, { + credentials: "include", + headers: { "Content-Type": "application/json", ...(opts.headers || {}) }, + signal: ctrl.signal, + ...opts, + }); + } finally { + clearTimeout(t); + } if (res.status === 401) { showLogin(); throw new Error("unauthorized"); @@ -1168,7 +1178,7 @@ function activitySummary(row) { if (sum.bands_new) parts.push(`${fmtNum(sum.bands_new)} nouveaux`); if (sum.bands_updated) parts.push(`${fmtNum(sum.bands_updated)} MAJ`); if (sum.bands_enriched) parts.push(`${fmtNum(sum.bands_enriched)} enrichis`); - if (sum.error) parts.push(`erreur : ${sum.error}`); + if (sum.error) parts.push(`erreur : ${esc(sum.error)}`); return parts.join(" · ") || "—"; } return `${esc(sum.target_table || "")} #${sum.target_id ?? ""}`; diff --git a/apps/api/src/adminRoutes.js b/apps/api/src/adminRoutes.js index c968cc0..05f96c8 100644 --- a/apps/api/src/adminRoutes.js +++ b/apps/api/src/adminRoutes.js @@ -872,6 +872,10 @@ export default async function adminRoutes(fastify, opts) { // ------------------------------------------------------------------ // Géocodage — actions sur band_locations (nouveau pipeline) // ------------------------------------------------------------------ + // Débloque les lignes coincées en 'processing' (worker tué en plein + // traitement) en plus de l'ancien statut 'error' (legacy). Le worker se répare + // aussi tout seul au bout de GEOCODE_STUCK_PROCESSING_MIN ; ce bouton est un + // forçage manuel pour ne pas attendre. fastify.post("/admin/api/locations/reset-errors", async (req, reply) => { try { const r = await pool.query(` @@ -879,6 +883,7 @@ export default async function adminRoutes(fastify, opts) { SET geocode_status='queued', geocode_tries_geo=0, geocode_error=NULL, geocode_next_at=now(), updated_at=now() WHERE geocode_status = 'error' + OR (geocode_status = 'processing' AND updated_at < now() - interval '5 minutes') `); const count = r.rowCount; await pool.query(