Page admin : appelle l'API au lieu du front, et resynchronise le rôle
Build & Deploy / build (push) Successful in 20s

Admin.tsx faisait ses 8 appels en relatif (`/admin/users?token=…`), donc vers
le nginx du front et non vers l'API. Le fallback SPA répondait index.html en
HTTP 200 : le res.json() partait en erreur de parsing — « The string did not
match the expected pattern » sous Safari. Les appels passent désormais par
API_URL, comme le reste du front.

App.tsx ne lisait le user que depuis localStorage, instantané pris au login :
une promotion en admin n'y apparaissait jamais et AdminRoute éjectait le compte
de /admin jusqu'à reconnexion. Le profil est resynchronisé depuis /me au
montage.

Restent 404 côté API : /admin/scrapers/logs et /admin/scrapers/run, que la page
appelle mais que le backend n'expose pas (les scrapers tournent via le cron
hôte). Le panneau affichera une erreur franche au lieu d'une erreur de parsing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-15 16:13:55 +02:00
co-authored by Claude Opus 5
parent b5f362fde1
commit 0c1b9f5670
2 changed files with 26 additions and 9 deletions
+17 -1
View File
@@ -21,7 +21,7 @@ import BackToTop from "./components/BackToTop";
import styles from "./App.module.css"; import styles from "./App.module.css";
import { getDocuments, type ApiDocument } from "./services/theapi"; import { getDocuments, API_URL, type ApiDocument } from "./services/theapi";
type NavItem = type NavItem =
| "home" | "home"
@@ -104,6 +104,22 @@ export default function App() {
: location.pathname === "/admin"? "admin" : location.pathname === "/admin"? "admin"
: "home"; : "home";
// Le user de localStorage est un instantané pris au login : un changement de
// rôle en base n'y apparaît jamais. On le resynchronise depuis /me au montage,
// sinon un compte promu admin reste bloqué hors de /admin jusqu'à reconnexion.
useEffect(() => {
const token = localStorage.getItem("authToken");
if (!token) return;
fetch(`${API_URL}/me?token=${encodeURIComponent(token)}`)
.then((res) => (res.ok ? res.json() : Promise.reject(res.status)))
.then((freshUser: User) => {
localStorage.setItem("user", JSON.stringify(freshUser));
setAuthState((prev) => ({ ...prev, user: freshUser }));
})
.catch((err) => console.error("Rafraîchissement du profil impossible :", err));
}, []);
useEffect(() => { useEffect(() => {
if (authState.isAuthenticated) { if (authState.isAuthenticated) {
getDocuments() getDocuments()
+9 -8
View File
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import styles from '../styles/Admin.module.css' import styles from '../styles/Admin.module.css'
import { API_URL } from '../services/theapi'
import { FileText, Database, Users } from 'lucide-react' import { FileText, Database, Users } from 'lucide-react'
type User = { id: string; email: string; full_name?: string; role: 'admin' | 'user' } type User = { id: string; email: string; full_name?: string; role: 'admin' | 'user' }
@@ -35,23 +36,23 @@ export default function Admin() {
setLoading(true) setLoading(true)
try{ try{
if(tab==='users'){ if(tab==='users'){
const res = await fetch(`/admin/users?token=${token}`) const res = await fetch(`${API_URL}/admin/users?token=${token}`)
if(!res.ok) throw new Error('Impossible de charger les utilisateurs') if(!res.ok) throw new Error('Impossible de charger les utilisateurs')
const data = await res.json() const data = await res.json()
setUsers(data || []) setUsers(data || [])
} else if(tab==='documents'){ } else if(tab==='documents'){
const res = await fetch('/documents') const res = await fetch(`${API_URL}/documents`)
if(!res.ok) throw new Error('Impossible de charger les documents') if(!res.ok) throw new Error('Impossible de charger les documents')
const data = await res.json() const data = await res.json()
setDocs(data || []) setDocs(data || [])
} else if(tab==='stats'){ } else if(tab==='stats'){
const res = await fetch(`/admin/stats?token=${token}`) const res = await fetch(`${API_URL}/admin/stats?token=${token}`)
if(!res.ok) throw new Error('Impossible de charger les statistiques') if(!res.ok) throw new Error('Impossible de charger les statistiques')
const data = await res.json() const data = await res.json()
setStats(data) setStats(data)
} else if(tab==='scrapers'){ } else if(tab==='scrapers'){
// logs endpoint (fallback) // logs endpoint (fallback)
const res = await fetch(`/admin/scrapers/logs?token=${token}`) const res = await fetch(`${API_URL}/admin/scrapers/logs?token=${token}`)
if(!res.ok) throw new Error('Impossible de charger les logs de scrapers') if(!res.ok) throw new Error('Impossible de charger les logs de scrapers')
const data = await res.json() const data = await res.json()
setLogs(data || []) setLogs(data || [])
@@ -74,7 +75,7 @@ export default function Admin() {
async function deleteUser(id: string){ async function deleteUser(id: string){
try{ try{
setLoading(true) setLoading(true)
const res = await fetch(`/admin/users/${id}?token=${token}`, { method: 'DELETE' }) const res = await fetch(`${API_URL}/admin/users/${id}?token=${token}`, { method: 'DELETE' })
if(!res.ok) throw new Error('Suppression impossible') if(!res.ok) throw new Error('Suppression impossible')
setUsers(u=>u.filter(x=>x.id!==id)) setUsers(u=>u.filter(x=>x.id!==id))
window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: 'Utilisateur supprimé', type: 'success' } })) window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: 'Utilisateur supprimé', type: 'success' } }))
@@ -89,7 +90,7 @@ export default function Admin() {
try{ try{
setLoading(true) setLoading(true)
const newRole = u.role === 'admin' ? 'user' : 'admin' const newRole = u.role === 'admin' ? 'user' : 'admin'
const res = await fetch(`/admin/users/${u.id}/role?token=${token}`, { method: 'PATCH', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ role: newRole }) }) 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') if(!res.ok) throw new Error('Impossible de modifier le rôle')
setUsers(list=>list.map(x=> x.id===u.id ? {...x, role:newRole} : x)) 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' } })) window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: `Rôle mis à jour: ${newRole}`, type: 'success' } }))
@@ -99,7 +100,7 @@ export default function Admin() {
async function deleteDocument(id:string){ async function deleteDocument(id:string){
try{ try{
setLoading(true) setLoading(true)
const res = await fetch(`/admin/documents/${id}?token=${token}`, { method: 'DELETE' }) const res = await fetch(`${API_URL}/admin/documents/${id}?token=${token}`, { method: 'DELETE' })
if(!res.ok) throw new Error('Suppression impossible') if(!res.ok) throw new Error('Suppression impossible')
setDocs(d=>d.filter(x=>x.id!==id)) setDocs(d=>d.filter(x=>x.id!==id))
window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: 'Document supprimé', type: 'success' } })) window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: 'Document supprimé', type: 'success' } }))
@@ -109,7 +110,7 @@ export default function Admin() {
async function runScraper(source: 'CIR'|'SCCS'){ async function runScraper(source: 'CIR'|'SCCS'){
try{ try{
setLoading(true) setLoading(true)
const res = await fetch(`/admin/scrapers/run?source=${source}&token=${token}`, { method: 'POST' }) 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') if(!res.ok) throw new Error('Erreur lors du lancement')
const entry = await res.json() const entry = await res.json()
setLogs(l=>[entry, ...l].slice(0,5)) setLogs(l=>[entry, ...l].slice(0,5))