Ne plus servir un index.html périmé, et nommer les sessions expirées
Build & Deploy / build (push) Successful in 23s
Build & Deploy / build (push) Successful in 23s
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) <noreply@anthropic.com>
This commit is contained in:
+22
-8
@@ -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' } }))
|
||||
|
||||
Reference in New Issue
Block a user