Skip to content

feat(admin): gérer les utilisateurs et leurs rôles (A-06) - #499

Open
Nascimento95 wants to merge 12 commits into
issue/54-reports-and-ratingsfrom
issue/341-users-and-roles
Open

Nascimento95 wants to merge 12 commits into
issue/54-reports-and-ratingsfrom
issue/341-users-and-roles

Conversation

@Nascimento95

Copy link
Copy Markdown
Collaborator

Résultat et problème

L'équipe ne pouvait ni retrouver un compte, ni le suspendre, ni changer un rôle, ni offrir Premium sans passer par la base. A-06 « Utilisateurs » le fait depuis l'admin, avec le droit de chaque rôle vérifié par l'API et chaque écriture dans admin_actions.

  • Liste et fiche : recherche par e-mail, prénom ou identifiant ; panneau de détail (formule et origine, certification, parcours débloqués et créés, signalements faits et reçus, messages de support).
  • Actions : changer le rôle, suspendre et réactiver (motif), offrir Premium jusqu'à une date ou un parcours, attribuer ou retirer Créateur certifié, exporter les données (JSON, comme l'app), supprimer le compte (règle de l'app, D-027).
  • Compte suspendu : 403 account_suspended partout, profil public masqué, parcours publiés gardés.
  • Droits (allowedUserActions, une seule règle partagée par l'API et l'admin) : admin = tout ; moderator = suspendre un compte user ; personne n'agit sur son propre compte ; un compte d'équipe est rétrogradé en user avant d'être supprimé.
  • Corrections au passage : la route POST /admin/users/:id/certification, citée « existe déjà » par le wiki, n'existait pas : elle est écrite ici. DetailPanel reste en vue quand la liste défile (commit fix(ui)).

Limites

  • Avis et signalements de lieux : tables absentes (route_ratings #64, signalements de lieux #49). La fiche affiche des listes vides, à remplacer à un seul endroit (readUserDetail, apps/api/src/admin/users-read.ts).
  • Offrir un parcours : l'identifiant se colle à la main, pas de sélecteur (hors ticket).
  • Le webhook RevenueCat (#67) devra laisser plan_source = gift tranquille.
  • Question à Ilan : le wiki dit « lecture, suspension » pour le moderator sans préciser qui. Choix retenu : un compte user seulement ; un éditeur, un modérateur ou un admin se suspend par un admin.
  • Le wiki annonce une nouvelle saisie du code TOTP avant une action sensible (plus de 10 min) : absente du socle, signalée sur l'Inbox.

Fixes #341
Écrans : A-06
Dépendance : PR empilée sur #479 (#54), elle-même sur #398, #365 et #363 (base issue/54-reports-and-ratings).

Comment vérifier

  1. pnpm install, docker-compose up -d, pnpm --filter @widoo/api db:migrate && pnpm --filter @widoo/api db:seed, pnpm dev.
  2. Connecté en admin : Utilisateurs, chercher un compte, ouvrir sa fiche ; offrir Premium jusqu'à demain (la formule devient « Offert, jusqu'au… ») ; offrir un parcours Premium ; attribuer le badge ; changer le rôle ; suspendre avec motif puis réactiver ; exporter ; supprimer un compte user.
  3. Un abonnement du store actif refuse l'offre (« Cet utilisateur a déjà un abonnement ou un pass actif. »).
  4. Connecté en moderator : seul « Suspendre » sur un compte user, aucune action sur un éditeur, un modérateur ou un admin.

Tests : 16 tests d'API (admin-users.test.ts) couvrent la matrice des rôles, l'arrêt du Premium offert à sa date, la suspension (403 account_suspended, profil masqué), le journal, l'export et la suppression ; allowedUserActions et GiftBody côté shared ; affichage et adresses côté admin.

  • pnpm lint && pnpm typecheck && pnpm test verts en local (chaque commit passe lint et types)

Formatage mécanique : non

Risque

Niveau : 3 — changement de rôle, suspension, suppression de compte, droit Premium donné à la main, migration.

Taille : tous les commits sous 400 lignes utiles (le plus gros : 360). Le snapshot de la migration (meta/0017_snapshot.json, 3 278 lignes) est généré par drizzle-kit.

Revue OWASP (édition en cours)

  • A01 Contrôle d'accès : le rôle vient de la base à chaque appel ; section users fermée à l'éditeur ; allowedUserActions rejoué sur la ligne verrouillée (FOR UPDATE) de chaque écriture, pas seulement dans l'interface ; personne n'agit sur son propre compte ; un moderator ne touche pas à un compte d'équipe. Testé (403 pour chaque couple acteur/cible).
  • A04 Conception non sécurisée : l'offre refuse un abonnement du store en cours, une date passée, les deux champs ou aucun ; un parcours gratuit, déjà débloqué ou écrit par le compte.
  • A09 Journalisation : chaque écriture et chaque export (lecture des données d'un tiers) écrit admin_actions, dans la transaction du changement.
  • A03 Injection : requêtes paramétrées ; % et _ de la recherche échappés ; corps validés (plainText, strictObject, 422).
  • Données personnelles : export limité à l'admin, limité à 5 par heure et par membre, cache-control: no-store, rien d'écrit sur disque.
  • A07 Authentification : la suspension coupe toutes les sessions (403 account_suspended). La nouvelle saisie du TOTP avant une action sensible n'existe pas dans le socle (voir plus haut).
  • Aucune nouvelle dépendance.

Retour arrière

Revert de la PR. Migration 0017 additive (deux colonnes plan_source/source avec défaut store, un DROP NOT NULL) : un retour arrière du code laisse les colonnes sans effet. L'annuler complètement demande de supprimer d'abord les cadeaux (route_unlocks où source = 'gift', sans transaction) pour rétablir le NOT NULL. Les rôles, suspensions et cadeaux déjà accordés sont conservés. Une suppression de compte est définitive.

🤖 Generated with Claude Code

Nascimento95 and others added 12 commits October 16, 2026 11:57
…d route unlock (A-06)

Add users.plan_source and route_unlocks.source (store or gift). A gift has no store
transaction, so route_unlocks.store_transaction_id becomes nullable, required for the store.
Additive migration 0017.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…do to an account (A-06)

Schemas of the list, the card and the actions, and allowedUserActions, the one place of the
rule: the admin changes roles, offers Premium and deletes; a moderator suspends a user.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
GET /admin/users and GET /admin/users/:id, for moderators and admins: search by part of an
e-mail or first name or by whole identifier, newest first, with the plan and its origin, the
routes unlocked and created, the reports made and received, and the support messages. The
ratings are empty until route_ratings exists (#64).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
PATCH /admin/users/:id/role (admin only) and POST /admin/users/:id/suspend and /reactivate with
a reason. The rule is allowedUserActions, checked on the locked row: nobody acts on their own
account, a moderator suspends a user only. Each write is journaled in the transaction of its
change. A suspended account keeps its published routes but its public profile is masked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
POST /admin/users/:id/gift, admin only: Premium until a future date (plan_source gift, which
ends by itself through canAccessRoute) or one published Premium route (route_unlocks, source
gift). Refused with a reason in details when the account has a running store plan, already
has the route, is its author, or the route is free.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… delete it (A-06)

POST /admin/users/:id/certification (the team's choice wins over the nightly computation),
GET /admin/users/:id/export (the file of the app, rate limited, journaled as it reads the data
of someone else) and DELETE /admin/users/:id with the rule of the app (softDeleteUser). Admin
only; an account of the team is demoted to user before it is deleted.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The panel grew with the page, so scrolling a long card scrolled the list away. It now sticks
to the top and scrolls inside its own height (max-h-detail-panel, to move to tokens.json).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…status (A-06)

The table of Utilisateurs, its badges and texts, how an account is named and its plan read, the
search address of the list, and the id of the member of the session.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…(A-06)

Utilisateurs for moderators and admins: a search by e-mail, first name or identifier, and the
card of the account in the detail panel (plan and its origin, certification, routes unlocked
and created, reports, support messages).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… suspension, toasts (A-06)

The buttons of the card (only what allowedUserActions gives the role), the window that asks a
reason to suspend or reactivate, the texts of every action and of each refusal, and the hook
that sends an action and tells its result in a toast.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d delete from the card (A-06)

The windows of the other actions and the container that opens them from the buttons of the
card: a role, Premium until a date or one route, the Créateur certifié choice, the export as a
JSON file, and the deletion with its confirmation.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

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:3 Risque niveau 3 (SECURITY.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant