apps/hermes — back-office interne
@pambe/hermes est la console d'administration / ops de Pambe : une app
Next.js 16 séparée (port 3002), réservée à l'équipe. Contrairement à apps/web,
elle n'est pas internationalisée — elle utilise next/link et
next/navigation directement, car elle ne cible pas d'audience publique ni de
SEO.
Contrôle d'accès — la porte admin
L'accès est doublement contrôlé : par le frontend et par l'API.
Côté Hermes (layout)
app/layout.tsx appelle resolveAdminGate() (défini dans lib/admin-access.ts,
marqué server-only) à chaque rendu. Le résultat est une union discriminée :
gate.state | Comportement |
|---|---|
"anonymous" | Affiche un écran « Connexion requise » avec lien /auth/login. |
"forbidden" | Affiche un écran « Accès refusé » avec lien /auth/logout. |
"admin" | Monte la sidebar et rend {children} normalement. |
Lorsque gate.state === "admin", l'en-tête du shell affiche deux éléments
supplémentaires : l'email de l'administrateur connecté (tronqué en mobile,
visible à partir de sm:) et un lien Se déconnecter (→ /auth/logout).
lib/admin-emails.ts contient la logique pure (sans dépendance env/Auth0) :
parseAdminEmails(raw) et isAdminEmail({ allowlist, email }).
lib/admin-access.ts les enrobe avec getEnv().ADMIN_EMAILS et la session Auth0.
Côté API
Chaque appel HTTP passe également par adminMiddleware (voir
Slice admin). La porte frontend évite un aller-retour
inutile, mais l'API reste la source de vérité des droits.
Pages
Vue d'ensemble (app/page.tsx)
Dashboard KPI alimenté par GET /admin/stats. Affiche 5 cartes (utilisateurs,
services, demandes, missions, avis) et un tableau de répartition des missions par
statut de tracking (contacted / doing / done / refused). Données en temps réel
(export const dynamic = "force-dynamic").
La carte « Utilisateurs » tolère la valeur null de registeredUsers : elle
affiche "—" avec le hint "annuaire indisponible" lorsque la Management API
Auth0 est momentanément hors ligne (voir Réponse /admin/stats).
Utilisateurs (app/utilisateurs/)
Annuaire paginé des comptes (25 par page) alimenté par GET /admin/users.
Composant serveur initial + client interactif (utilisateurs-client.tsx) pour
la recherche et la pagination sans rechargement de page.
Chaque ligne affiche : nom / email, compteurs d'activité (demandes · services ·
avis · profil), date d'inscription, statut (Actif / Suspendu), et bouton
Suspendre / Réactiver (Server Action setUserBlockedAction →
POST /admin/users/:id/block).
Un clic sur un utilisateur charge le détail via getAdminUserAction →
GET /admin/users/:id et l'affiche dans une modale.
Imports CSV (app/import-csv/)
Upload des fichiers de services scrapés (Google Maps), avec historique d'import.
Pendant UI de POST /imports/services côté API
(voir Cold start & claim).
Architecture interne
Server Actions (app/admin-actions.ts)
Toutes les mutations et lectures passent par des Server Actions typées, qui
appellent assertAdmin() en défense avant tout appel API :
| Action | Endpoint API |
|---|---|
getAdminStatsAction() | GET /admin/stats |
listAdminUsersAction({ query, page, perPage }) | GET /admin/users |
getAdminUserAction({ id }) | GET /admin/users/:id |
setUserBlockedAction({ id, blocked }) | POST /admin/users/:id/block |
Client API (lib/api/)
| Fichier | Rôle |
|---|---|
lib/api/http.ts | apiFetch — fetch authentifié (Bearer), normalise les erreurs non-2xx en ApiError, valide la réponse avec un schéma Zod. |
lib/api/admin.ts | Factory admin(config) exposant getStats, listUsers, getUser, setUserBlocked. Schémas Zod miroir des réponses de apps/api. |
Hermes parle à l'API par HTTP (pas d'import direct de packages/lib) — la
dépendance est limitée à @pambe/ui pour le design system.
Favicon
app/icon.tsx génère le favicon de Hermes via ImageResponse de next/og
(convention App Router). Il rend la marque Pambe — dégradé vert→bleu
(#00FF9B → #03DBB0 → #0B64F4) sur fond blanc — en PNG 32 × 32. Le
chemin SVG de la marque est inliné directement dans le fichier pour que Hermes
reste auto-contenu (pas de dépendance sur apps/web/lib/brand.ts).
La variable ADMIN_EMAILS doit être configurée dans les deux apps :
apps/api (porte API) et apps/hermes (porte layout). En local, une valeur
vide désactive l'accès admin partout.