Skip to content

feat(admin): poser le socle, la connexion, les rôles et le journal - #363

Open
Nascimento95 wants to merge 24 commits into
mainfrom
issue/52-admin-foundation
Open

Nascimento95 wants to merge 24 commits into
mainfrom
issue/52-admin-foundation

Conversation

@Nascimento95

@Nascimento95 Nascimento95 commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Résultat et problème

L'admin web n'existait pas (apps/admin était une coquille vide). Cette PR pose son socle : app Next.js, connexion, rôles, coque, journal des actions, compteurs et composants communs.

Les quatre critères d'acceptation sont tenus. La double authentification a été ajoutée le 2026-10-13, une fois le projet widoo-staging configuré (détail sur le ticket).

Ce qui est fait :

  • packages/shared : les 12 sections de l'admin et la matrice des rôles du wiki, source unique pour le menu et l'API ; codes mfa_required et account_suspended.
  • packages/ui (nouveau) : thème shadcn tiré de tokens.json, bouton, champs, encadré, menu, info-bulle, tableau, tri, pagination, filtre, pastille, panneau de détail, confirmation, toasts.
  • apps/admin : socle Next.js repris des choix du site, en-têtes noindex et no-store, connexion Google et lien par e-mail (A-01), refus d'un compte sans rôle ou suspendu, retour à la page demandée, double authentification TOTP (activation et saisie du code), coque avec menu par rôle et compteurs, état d'une liste dans l'adresse, dates relatives.
  • apps/api : garde requireAdminSection (rôle par section, session de 12 h, second facteur), compte suspendu refusé partout, table admin_actions et recordAdminAction, GET /admin/counters.
  • infra/admin.Dockerfile : image Cloud Run, construite et lancée en local.

Limites :

  • Double authentification : faite pour la connexion (activation à la première connexion avec QR code ou clé, puis code à chaque connexion). Aucune planche pour ces deux écrans : ils suivent la carte de connexion A-01. Le TOTP s'active sur un projet par l'API d'administration, pas dans la console : à refaire sur le projet de production.
  • Session de 12 h : l'API la refuse au-delà (401), mais l'admin ne ramène pas encore de lui-même à A-01 quand ça arrive en cours d'usage.
  • Second facteur non exigé avec l'émulateur : isSecondFactorRequired est faux seulement quand FIREBASE_AUTH_EMULATOR_HOST est défini, ce que la config refuse déjà en production. Choix à valider.
  • Vérification du code avant une action sensible (10 min) : non faite, aucune action sensible n'existe encore.
  • Journal : aucune route d'écriture admin dans ce ticket ; recordAdminAction est testé seul, son premier appelant arrive avec Construire la file de modération et ses endpoints (A-02) #53.
  • Compteurs : seule la modération est comptée ; le retard de 72 h se mesure sur routes.updated_at en attendant moderation_actions (Construire la file de modération et ses endpoints (A-02) #53). Signalements, support, brouillons IA et traductions viendront avec leurs tables.
  • Dimensions hors tokens.json : menu 248 et 72 px, en-tête 64 px, champ 40 px, panneau 400 px, fenêtre 520 px, carte de connexion 440 px, deux tailles de texte et l'ombre flottante sont dans le preset de packages/ui, marquées « à reporter dans tokens.json », comme le fait le site.
  • Écarts avec les planches : pas de bouton « Réduire le menu » sous 1 280 px ; compteur du menu réduit sans disque sombre ; toasts sans disque derrière l'icône ; cases à cocher et boutons radio non faits (aucun écran du ticket ne s'en sert).
  • Une adresse inconnue affiche « Page introuvable » mais répond 200.

Fixes #52
Écrans : A-01, coque et modèles communs
Dépendance : #364 (environnement du build de l'admin en CI), à fusionner avant. Le site (#256) n'est pas fusionné : .gitignore, .prettierignore, .dockerignore, eslint.config.js et package.json reçoivent ici des lignes proches des siennes, conflits à prévoir sur la seconde PR fusionnée.

Comment vérifier

En local, sans toucher au projet Firebase ni à la base de Widoo :

docker compose up -d
cp apps/api/.env.example apps/api/.env      # décommenter FIREBASE_AUTH_EMULATOR_HOST
cp apps/admin/.env.example apps/admin/.env.local
pnpm --filter @widoo/api db:migrate && pnpm --filter @widoo/api db:seed
firebase emulators:start --only auth --project demo-widoo
pnpm --filter @widoo/api dev
pnpm --filter @widoo/admin dev              # http://localhost:5173

Se connecter crée un compte user ; lui donner un rôle dans la base locale (update users set role = 'admin' where email = '…'). Le lien de connexion par e-mail se lit dans http://127.0.0.1:9099/emulator/v1/projects/demo-widoo/oobCodes.

Testé à la main avec quatre comptes fictifs, face aux planches du wiki (Marco dans Chrome, et l'agent dans son navigateur de test) :

  • compte sans rôle : « Ce compte n'a pas accès à l'admin. » avec l'adresse ;
  • admin, éditeur, modérateur : chacun voit le menu de son rôle ; menu du compte et déconnexion ;
  • /moderation sans session : renvoi vers A-01, puis retour à la page demandée ;
  • lien par e-mail : « Vérifiez votre boîte mail », renvoi après 60 s, connexion par le lien ;
  • compteur de la modération, rouge au-delà de 72 h, absent pour l'éditeur ;
  • sous 1 280 px, menu réduit aux icônes ; à 375 px, pas de défilement horizontal ;
  • tableau, tri et filtre dans l'adresse, panneau de détail, confirmation avec motif, toasts, sur une page de démonstration retirée avant la PR.

Testé par Marco le 2026-10-13, dans Chrome, admin et API en local (commit 2d93ecc) branchés sur le projet Firebase widoo-staging, base de données locale :

  • connexion Google réelle avec un compte sans rôle : « Ce compte n'a pas accès à l'admin. » avec l'adresse ;
  • même compte passé admin dans la base locale : écran « Activez la double authentification », QR code scanné dans une application d'authentification, code saisi, entrée dans l'admin avec le menu complet et le compteur de modération (l'API accepte donc le jeton avec second facteur dès l'activation) ;
  • déconnexion puis reconnexion Google : écran « Code de vérification » ; un mauvais code affiche « Ce code est incorrect ou a expiré. » ; le bon code ouvre l'admin.

Non vérifié contre le vrai projet : le lien de connexion par e-mail (testé seulement avec l'émulateur), la saisie de la clé à la main à la place du QR code, et tout déploiement.

  • pnpm lint && pnpm typecheck && pnpm test verts en local (1 075 tests, dont 372 pour l'API)
  • Testé sur iOS / Android (préciser) — sans objet, aucun écran mobile

Formatage mécanique : non

Risque

Niveau : 2 — migration additive (admin_actions, deux colonnes sur users), nouvel endpoint authentifié, garde d'accès par rôle et configuration de dépendances ; réversible et borné.

Taille : cinq commits entre 400 et 460 lignes utiles, tests compris. efb4cc5 (460) et 22ee626 (433) : composants de packages/ui, un fichier par composant, sans découpage plus fin qui tienne debout seul. fa2a3d6 (456) : fichiers de configuration du socle et leurs tests. 7bad428 (415) et c68d3e2 (428) : écran de connexion et coque, composants d'affichage et leurs conteneurs. Fichiers générés à part : migration 0015_add_admin_actions.sql et son instantané.

Revue OWASP (Top 10) :

  • Contrôle d'accès : le rôle vient de la base à chaque requête, jamais du jeton ; requireAdminSection applique la matrice du wiki ; un compte hors équipe reçoit 403 sans rien apprendre du second facteur. L'admin ne fait que cacher. Tests : routes/admin.test.ts, admin-sections.test.ts.
  • Authentification et session : session admin de 12 h au plus (auth_time), second facteur TOTP exigé hors émulateur et activé avant toute entrée, compte suspendu refusé partout. Le secret TOTP n'est ni stocké ni journalisé par l'admin : il vit le temps de l'écran d'activation. Le lien de connexion ne s'ouvre que sur l'appareil qui l'a demandé, et son code quitte l'adresse après usage.
  • Redirection ouverte : la page de retour est limitée à un chemin d'une section de l'admin (return-path.test.ts).
  • Configuration : CSP fermée (l'admin, l'API, Firebase Auth), frame-ancestors 'none', noindex, no-store ; l'émulateur n'est accepté qu'avec un projet demo-*. Aucun secret : seules des valeurs NEXT_PUBLIC_.
  • Journalisation : admin_actions ne reçoit que des insertions, dans la transaction du changement ; l'acteur purgé laisse la ligne.
  • Injection : requêtes Drizzle paramétrées ; l'état de liste lu dans l'adresse ignore tout filtre ou colonne inconnu.

Changement de configuration à relire : la règle pnpm react-dom: '-' interdisait react-dom dans tout le dépôt ; elle est limitée à @rnmapbox/maps, sans quoi aucune app web ne s'installe. Le lockfile du mobile bouge ; ses 269 tests passent. Le site aura le même besoin.

Nouvelles dépendances :

  • next 16.3.8 (le site est en 16.3.6, qui porte l'alerte haute GHSA-cjq9-62q9-8jv4), react-dom, tailwindcss 3.4.19, autoprefixer, postcss, @next/eslint-plugin-next : mêmes choix que le site.
  • @firebase/app et @firebase/auth : connexion Google et lien par e-mail, client officiel de Firebase Auth ; le méta-paquet firebase est écarté, il tire Firestore et @grpc/grpc-js (alerte haute GHSA-m9gg-hp2v-232j).
  • @radix-ui/react-slot (1.4, la 1.5 avait moins d'un jour), react-dropdown-menu, react-tooltip, react-dialog : primitives accessibles sur lesquelles shadcn/ui est bâti ; écarté : les réécrire.
  • class-variance-authority, clsx : variantes de composants, comme shadcn/ui ; tailwind-merge écarté, il ne connaît pas les classes des tokens.
  • sonner : le toast demandé par la planche.
  • qrcode : dessine le QR code de l'activation du second facteur dans le navigateur, sans appel à un service externe ; écarté : un service d'images de QR code, qui recevrait le secret.
  • @phosphor-icons/react : les icônes du design system, version web du paquet du mobile.

Retour arrière

Le revert de la PR suffit pour le code. La migration 0015 est additive : après un revert, la table admin_actions et les colonnes users.suspended_at et users.suspension_reason restent en base sans effet ; les supprimer demande une migration à part et perd les lignes du journal déjà écrites.

🤖 Generated with Claude Code

@Nascimento95

Copy link
Copy Markdown
Collaborator Author

Trois corrections poussées après le premier passage de la CI :

  • e8eb291 : next.config.ts lisait l'environnement au chargement, ce qui faisait échouer next typegen (typecheck) en CI, sans fichier .env. Il le lit maintenant quand les en-têtes sont demandés ; le build le refuse toujours s'il manque.
  • 089a23b : deux alertes de gravité haute apportées par mes dépendances. next 16.3.6 → 16.3.8 (GHSA-cjq9-62q9-8jv4, SSRF dans l'optimisation d'images) ; et le méta-paquet firebase remplacé par @firebase/app et @firebase/auth, seuls utilisés, ce qui retire @grpc/grpc-js tiré par Firestore (GHSA-m9gg-hp2v-232j). pnpm audit --audit-level=high ne montre plus que les deux exceptions déjà acceptées. Connexion par lien revérifiée de bout en bout en local après le changement de paquet. Les PR du site sont en next 16.3.6 et auront la même alerte.
  • L'étape « Build API and admin » de la CI construit l'admin sans son environnement : réglé à part dans chore(process): donner son environnement au build de l'admin en CI #364 (area:process), à fusionner avant celle-ci. D'ici là, cette étape échouera ici.

@IlanAMG

IlanAMG commented Oct 13, 2026

Copy link
Copy Markdown
Member

Revue du 13 octobre 2026 — session d'Ilan, relecture par un agent du code au commit 3bdf63e. Rien n'a été lancé pour cette relecture ; Ilan fait sa recette visuelle en local à part.

Contrôle d'accès : rien à redire. Le rôle est relu en base à chaque appel, le second facteur est exigé par l'API hors émulateur, l'émulateur est refusé en production, pas de redirection ouverte par retour, aucun secret dans le diff.

À faire avant fusion

  1. Rebase sur main, et migration à régénérer, pas à renommer. main a reçu aujourd'hui 0003_add_direction_cache (when 1791887020675). 0003_add_admin_actions porte un when antérieur (1791790539879). Renommée en 0004 avec son horodatage, Drizzle la sauterait sur une base déjà à 0003 (lastDbMigration.created_at < migration.folderMillis) : staging afficherait « Database migrated » sans créer users.suspended_at, et chaque appel authentifié répondrait 500, app mobile comprise. La CI ne le verrait pas, sa base est neuve. Après le rebase : supprimer la migration, db:generate, vérifier que les when du journal sont croissants.
  2. CI. Le build de l'admin a maintenant son environnement sur main (chore(process): donner son environnement au build de l'admin en CI #364, fusionnée) : le rebase suffit. L'audit des dépendances n'a encore jamais tourné sur cette PR.
  3. react-dom et l'app mobile. Avec '@rnmapbox/maps>react-dom': '-', le lockfile donne quand même react-dom@19.2.3 en pair à @rnmapbox/maps, expo et react-i18next : pnpm lie la version que déclare l'app web. C'est la version du React de l'app, donc a priori sans effet, mais le commentaire de pnpm-workspace.yaml dit l'inverse et personne n'a lancé l'app mobile sur ce lockfile. La pile du site fait le même changement de règle (feat(infra): poser le socle du site web (Next.js, FR/EN, SEO) #256) : la seconde des deux piles à fusionner aura un conflit d'une ligne sur ce commentaire.

Écarts mineurs

  • Textes de A-01 : il manque « Ouvrez-le sur cet ordinateur. » (lien envoyé), la phrase « Vous étiez connecté avec… L'admin est réservé aux comptes de l'équipe Widoo. » (compte sans accès) et « Votre session a expiré. ».
  • Après 12 h, l'API refuse mais l'admin reste affiché sans rien dire (useAdminCounters, .catch(() => undefined)).
  • Compte supprimé : « L'admin ne répond pas pour le moment. », sans moyen de changer de compte (lib/auth/session.ts).

Durcissements conseillés, non bloquants

  • Exiger aussi un projet demo-* côté API quand l'émulateur est actif (apps/api/src/config.ts), comme le fait déjà l'admin.
  • Refuser un jeton sans auth_time lisible ; limiter le second facteur accepté à totp.
  • Un test « aucune route /admin/* sans garde ».

En attente d'une décision d'Ilan : la fermeture de #52 alors que le retour à la connexion après 12 h, la nouvelle vérification avant une action sensible et la preuve du journal ne sont pas faits ; la façon de nommer le premier admin en staging puis en production.

@Nascimento95
Nascimento95 force-pushed the issue/52-admin-foundation branch from 3bdf63e to 03d6216 Compare October 14, 2026 04:23
@Nascimento95

Copy link
Copy Markdown
Collaborator Author

Réponse à la revue du 13 octobre, point par point. Branche rebasée sur main (--force-with-lease), tête 03d6216.

À faire avant fusion

  1. Migration : rebasée sur main, l'ancienne 0003_add_admin_actions supprimée, puis régénérée par db:generate : c'est maintenant 0005_add_admin_actions, après 0003_add_direction_cache et 0004_add_profile. Les when du journal sont croissants. Vérifié en local sur une base jetable : migrations de main appliquées d'abord, puis celles de la branche → admin_actions, users.suspended_at et users.suspension_reason créés, 6 migrations au journal.
  2. CI : le rebase apporte chore(process): donner son environnement au build de l'admin en CI #364.
  3. react-dom : le commentaire de pnpm-workspace.yaml décrivait mal la règle. Il dit maintenant que pnpm lie le react-dom 19.2.3 de l'admin comme pair de @rnmapbox/maps, expo et react-i18next (03d6216). Vérifié : expo export --platform ios avec carte des sources ne contient aucun module du paquet react-dom (0 sur 3 466). L'app n'a pas été lancée sur un appareil. Le conflit d'une ligne avec feat(infra): poser le socle du site web (Next.js, FR/EN, SEO) #256 reste à résoudre par la seconde PR fusionnée.

Écarts mineurs (f56ab37)

  • Textes de A-01 : « Ouvrez-le sur cet ordinateur. », « Vous étiez connecté avec {adresse}. » et « L'admin est réservé aux comptes de l'équipe Widoo. », et « Votre session a expiré. » au-dessus de « Reconnectez-vous pour revenir… ».
  • Après 12 h : le client API signale un 401 qui persiste avec un jeton rafraîchi (onUnauthorized, nouveau dans @widoo/api-client) ; l'admin déconnecte alors le membre et le renvoie à A-01 avec la page demandée et « Votre session a expiré. ».
  • Compte supprimé : refusé comme un compte sans accès (« Ce compte n'a pas accès à l'admin. »), on peut se connecter avec un autre compte. L'écran « L'admin ne répond pas pour le moment. » propose aussi « Utiliser un autre compte ».

Durcissements (fb660c2, 5601a4f)

  • L'API refuse l'émulateur Auth sans projet demo-*, quel que soit l'environnement.
  • Un jeton sans auth_time lisible est refusé (401) ; seul totp vaut second facteur sur /admin/* (phone → 403 mfa_required).
  • Nouveau test : chaque route /v1/admin/* du document OpenAPI répond 401 sans jeton et 403 à un compte hors équipe.

Vérification humaine : Marco a testé le 2026-10-14 en local (base locale, connexion par le Firebase de staging, commit 03d6216) : refus d'un compte sans rôle avec l'adresse utilisée, connexion Google avec code TOTP, « Ouvrez-le sur cet ordinateur. » après l'envoi d'un lien, refus d'un compte supprimé puis connexion avec un autre compte. Non vérifiés à l'écran : l'expiration après 12 h (couverte par les tests du client API et de l'API) et l'écran « L'admin ne répond pas ».

pnpm lint && pnpm typecheck && pnpm test verts en local : 408 tests API, 78 admin, 24 client API.

Toujours en attente de ta décision : la fermeture de #52 sans la nouvelle vérification avant une action sensible (aucune n'existe encore dans le socle) et sans la preuve du journal ; la façon de nommer le premier admin en staging puis en production.

@Nascimento95

Copy link
Copy Markdown
Collaborator Author

Complément à la vérification humaine : Marco a aussi testé le 2026-10-14 en local, au commit 03d6216, l'écran affiché quand l'API ne répond pas : « L'admin ne répond pas pour le moment. », avec « Réessayer » et « Utiliser un autre compte », qui ramène à la connexion. Seule l'expiration après 12 h reste non vérifiée à l'écran (couverte par les tests).

@Nascimento95

Copy link
Copy Markdown
Collaborator Author

Branche remise à jour sur main (tête a850abb) après les fusions de Paul du 14 octobre : ses migrations 0005 à 0013 occupaient déjà le numéro de la nôtre. 0005_add_admin_actions est supprimée et régénérée par db:generate en 0014_add_admin_actions ; les when du journal sont croissants. Vérifié sur une base de test recréée de zéro.

Autres adaptations au rebase, sans changement de comportement : httpError garde le code fin (mfa_required, account_suspended) par-dessus le nouveau errors.ts de main ; admin_actions.actor_id est classé dans le test d'export des données (journal de l'équipe, conservé à la purge d'un compte) ; le client API reprend celui de main avec getAdminCounters.

pnpm lint && pnpm typecheck && pnpm test : tout vert en local. Trois fichiers de l'API (editor-edit, editor-submit, export) dépassaient le délai de 5 s quand toute la suite tourne ; ils passent seuls, comme Paul l'a relevé dans l'Inbox.

Nascimento95 and others added 10 commits October 16, 2026 10:30
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the tokens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…urn to

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d the API

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…account

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_suspended codes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…account

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… counters

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nascimento95 and others added 14 commits October 16, 2026 10:31
…ed account

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oasts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d ask for a reason

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at typecheck

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
next 16.3.6 has a high advisory (GHSA-cjq9-62q9-8jv4), and the firebase
meta-package pulls @grpc/grpc-js through Firestore (GHSA-m9gg-hp2v-232j).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y provider

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…087)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dmin tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… deleted account

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…TP factor only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…team

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Nascimento95
Nascimento95 force-pushed the issue/52-admin-foundation branch from a850abb to b181be3 Compare October 16, 2026 03:33
@Nascimento95

Copy link
Copy Markdown
Collaborator Author

Branche remise à jour sur main (tête b181be3) après les fusions de Paul du 15 octobre : sa migration 0014_free_deleted_firebase_uid prenait le numéro de la nôtre. 0014_add_admin_actions est supprimée et régénérée par db:generate en 0015_add_admin_actions ; when du journal croissants, base de test recréée de zéro.

Autres adaptations au rebase, sans changement de comportement : la garde d'authentification reprend la version de main (compte supprimé : findOrCreateUser répond signed_out, D-027) et y remet le second facteur (secondFactor dans l'identité vérifiée, requireAdminSection, 403 mfa_required) et le refus d'un compte suspendu (403 account_suspended). authTime existait déjà sur main. Lint, types et tests verts en local ; quelques fichiers de l'API (éditeur, programmations, signalements) dépassent le délai de 5 s quand toute la suite tourne et passent en séquence.

@IlanAMG

IlanAMG commented Oct 16, 2026

Copy link
Copy Markdown
Member

Chef de projet (16/10/2026) : Ilan désigne Paul (@PaulThiberville) comme relecteur des PR de Marco. Ordre proposé : PR #363 (#52) d'abord, puis la pile du site depuis PR #256 (PR #270 à #276), puis les PR empilées de l'admin (#365, #398, #479, #499, #500, #501). Paul traite aussi les PR Dependabot #252 à #255 (fusionner ou fermer).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

level:2 Risque niveau 2 (SECURITY.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Admin : socle Next.js, coque, connexion, rôles et journal (A-01)

2 participants