From d5703c644288b64d539828605d4b38ad5961b8bc Mon Sep 17 00:00:00 2001 From: neckfire Date: Sat, 15 Aug 2026 16:44:17 +0200 Subject: [PATCH] =?UTF-8?q?Ne=20plus=20servir=20un=20index.html=20p=C3=A9r?= =?UTF-8?q?im=C3=A9,=20et=20nommer=20les=20sessions=20expir=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nginx ne posait aucun Cache-Control sur index.html. Le navigateur appliquait son cache heuristique et gardait l'ancien index.html, qui référence les anciens assets fingerprintés — eux marqués immutable un an. Conséquence : un déploiement pouvait rester invisible pendant une durée indéterminée, sans Ctrl+Shift+R. no-cache ne désactive pas le cache, il impose une revalidation : 304 tant que rien ne change, nouveau build dès qu'il change. Côté page admin, un 401 (jeton absent ou expiré au bout de 30 min) produisait « Impossible de charger les utilisateurs » — un message qui accuse le serveur alors que la session est simplement morte, l'interface continuant de croire l'utilisateur connecté. Les 401 et 403 sont désormais distingués et nommés. Co-Authored-By: Claude Opus 5 (1M context) --- nginx.conf | 8 ++++++++ src/pages/Admin.tsx | 30 ++++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/nginx.conf b/nginx.conf index ceaab6e..e122599 100644 --- a/nginx.conf +++ b/nginx.conf @@ -9,6 +9,14 @@ server { try_files $uri $uri/ /index.html; } + # index.html référence les assets fingerprintés : s'il est mis en cache, le + # navigateur continue de charger l'ancien build (assets marqués immutable) + # pendant une durée indéterminée. no-cache = il le garde mais revalide, donc + # un 304 tant que rien ne change, et le nouveau build dès le déploiement. + location = /index.html { + add_header Cache-Control "no-cache, must-revalidate"; + } + # Cache long pour les assets fingerprintés par Vite. location /assets/ { expires 1y; diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx index fa2275e..c1f0fdd 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -9,6 +9,20 @@ type Document = { id: string; title: string; source: 'CIR' | 'SCCS'; ingredient? type ScraperLog = { id: string; when: string; source: string; status: 'pending' | 'running' | 'success' | 'error' } type Stats = { total_documents?: number; cir_documents?: number; sccs_documents?: number; total_users?: number } +// Un jeton absent ou expiré (30 min de validité) renvoie 401. Le message +// générique laissait croire à une panne du serveur alors qu'il suffit de se +// reconnecter — l'interface, elle, croit toujours la session ouverte. +function assertOk(res: Response, fallback: string){ + if(res.ok) return + if(res.status === 401){ + throw new Error('Session expirée — déconnecte-toi puis reconnecte-toi.') + } + if(res.status === 403){ + throw new Error('Accès refusé : ce compte n\'est pas administrateur.') + } + throw new Error(fallback) +} + export default function Admin() { const [tab, setTab] = useState<'users'|'documents'|'stats'|'scrapers'>('users') @@ -38,23 +52,23 @@ export default function Admin() { try{ if(tab==='users'){ const res = await fetch(`${API_URL}/admin/users?token=${token}`) - if(!res.ok) throw new Error('Impossible de charger les utilisateurs') + assertOk(res, 'Impossible de charger les utilisateurs') const data = await res.json() setUsers(data || []) } else if(tab==='documents'){ const res = await fetch(`${API_URL}/documents`) - if(!res.ok) throw new Error('Impossible de charger les documents') + assertOk(res, 'Impossible de charger les documents') const data = await res.json() setDocs(data || []) } else if(tab==='stats'){ const res = await fetch(`${API_URL}/admin/stats?token=${token}`) - if(!res.ok) throw new Error('Impossible de charger les statistiques') + assertOk(res, 'Impossible de charger les statistiques') const data = await res.json() setStats(data) } else if(tab==='scrapers'){ // logs endpoint (fallback) const res = await fetch(`${API_URL}/admin/scrapers/logs?token=${token}`) - if(!res.ok) throw new Error('Impossible de charger les logs de scrapers') + assertOk(res, 'Impossible de charger les logs de scrapers') const data = await res.json() setLogs(data || []) } @@ -77,7 +91,7 @@ export default function Admin() { try{ setLoading(true) const res = await fetch(`${API_URL}/admin/users/${id}?token=${token}`, { method: 'DELETE' }) - if(!res.ok) throw new Error('Suppression impossible') + assertOk(res, 'Suppression impossible') setUsers(u=>u.filter(x=>x.id!==id)) window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: 'Utilisateur supprimé', type: 'success' } })) }catch(e: unknown){ @@ -92,7 +106,7 @@ export default function Admin() { setLoading(true) const newRole = u.role === 'admin' ? 'user' : 'admin' const res = await fetch(`${API_URL}/admin/users/${u.id}/role?token=${token}`, { method: 'PATCH', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ role: newRole }) }) - if(!res.ok) throw new Error('Impossible de modifier le rôle') + assertOk(res, 'Impossible de modifier le rôle') setUsers(list=>list.map(x=> x.id===u.id ? {...x, role:newRole} : x)) window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: `Rôle mis à jour: ${newRole}`, type: 'success' } })) }catch(e: unknown){ const msg = e instanceof Error ? e.message : String(e); setError(msg||'Erreur'); window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: msg||'Erreur', type: 'error' } })) }finally{ setLoading(false) } @@ -102,7 +116,7 @@ export default function Admin() { try{ setLoading(true) const res = await fetch(`${API_URL}/admin/documents/${id}?token=${token}`, { method: 'DELETE' }) - if(!res.ok) throw new Error('Suppression impossible') + assertOk(res, 'Suppression impossible') setDocs(d=>d.filter(x=>x.id!==id)) window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: 'Document supprimé', type: 'success' } })) }catch(e: unknown){ const msg = e instanceof Error ? e.message : String(e); setError(msg||'Erreur'); window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: msg||'Erreur', type: 'error' } })) }finally{ setLoading(false) } @@ -112,7 +126,7 @@ export default function Admin() { try{ setLoading(true) const res = await fetch(`${API_URL}/admin/scrapers/run?source=${source}&token=${token}`, { method: 'POST' }) - if(!res.ok) throw new Error('Erreur lors du lancement') + assertOk(res, 'Erreur lors du lancement') const entry = await res.json() setLogs(l=>[entry, ...l].slice(0,5)) window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: `Scraper ${source} lancé`, type: 'success' } }))