Aller au contenu principal

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.stateComportement
"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 setUserBlockedActionPOST /admin/users/:id/block).

Un clic sur un utilisateur charge le détail via getAdminUserActionGET /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 :

ActionEndpoint 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/)

FichierRôle
lib/api/http.tsapiFetch — fetch authentifié (Bearer), normalise les erreurs non-2xx en ApiError, valide la réponse avec un schéma Zod.
lib/api/admin.tsFactory 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).

ADMIN_EMAILS

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.