Documentation

Gestion des utilisateurs (admin)

Notes internes, tags de segmentation, bulk actions, export RGPD admin-initié.

La fiche utilisateur dans /admin/users (clic sur une row) ouvre un sheet à 5 onglets : Info, Notes, Emails, Historique, Actions. Cette doc couvre les features admin qui s'y rattachent.

Notes internes

Champ libre admin par user, invisible côté user. Idéal pour le contexte support ("a réclamé un refund le 12/03", "VIP partenaire", etc.).

model UserNote {
  id        String   @id @default(cuid())
  userId    String
  authorId  String?
  content   String
  createdAt DateTime @default(now())
}

Routes :

GET    /api/admin/users/[id]/notes
POST   /api/admin/users/[id]/notes               { content: string }
DELETE /api/admin/users/[id]/notes/[noteId]

Auteur conservé via FK (SetNull si l'admin est supprimé — la note reste pour la trace). Toute action audit-loggée (user.note_added / user.note_deleted).

Modération d'avatar

Un modérateur ou admin peut supprimer l'avatar d'un utilisateur directement depuis le sheet de la fiche user (icône ✕ sur la photo de profil).

DELETE /api/admin/users/[id]/avatar
  • Guard : requireApiAuth({ moderator: true }) — accessible aux MODERATORs et ADMINs
  • Met user.image = null en DB
  • Audit user.avatar_removed
  • Invalide le cache TanStack Query ["users", id] côté client

L'avatar est physiquement supprimé de R2 uniquement si la feature de stockage R2 est activée et que l'image provient du bucket interne.

Timeline utilisateur

La fiche user (onglet Historique) affiche une timeline unifiée qui merge :

  • Connexions (LoginHistory) — device, OS, navigateur, pays/ville
  • Événements d'audit (AuditLog) — toutes les actions sensibles
  • Souscriptions — démarrages et résiliations
  • Coupons — redemptions

Comportement

  • Acteur : pour les événements d'audit déclenchés par un admin (ex. changement de rôle), le nom de l'admin est affiché dans l'accordéon sous "Par" avec un mini-popup UserEntityCell.
  • Auto-exclusion : les entités User correspondant au profil en cours de consultation sont masquées (ex. "2FA désactivée" sur son propre profil n'affiche pas un accordéon vide pointant sur lui-même).
  • Pas d'IP : l'adresse IP n'est jamais affichée dans la timeline (ni dans le hint ni dans l'accordéon) pour ne pas surcharger l'UI.

Tags

Tableau de strings (User.tags) éditable depuis la fiche user. Format : ^[a-z0-9][a-z0-9_-]{0,30}$ (lowercase, max 31 chars). Stocké trié + dédupé.

Cas d'usage :

  • Audience broadcasts (#beta-testers, #early-access)
  • Triage support (#vip, #high-volume)
  • Segmentation marketing
PATCH /api/admin/users/[id]/tags    { tags: string[] }

Bulk actions

API POST /api/admin/users/bulk (max 500 users / appel) :

actioneffet
blockbloque + bump tokenVersion
unblockdéblocage
revoke_sessionsbump tokenVersion + delete UserSession
add_tagpush tag dans tags[] (set-like)
remove_tagretire tag

Garde-fous :

  • Self-exclusion (un admin ne peut s'auto-bloquer)
  • block / unblock / revoke_sessionswhere: { role: { not: "ADMIN" } } → un admin compromis ne peut pas retirer les pouvoirs des autres admins

L'UI multi-select dans la table n'est pas encore wirée — l'API est prête, à brancher quand tu en auras l'usage.

Export RGPD

Bouton Export RGPD (JSON) dans l'onglet Actions de la fiche user.

GET /api/admin/users/[id]/export

Renvoie un JSON avec toutes les données rattachées à l'user :

  • Profil (sans password hash)
  • Accounts OAuth (sans tokens)
  • Sessions, login history, audit logs
  • Notifications
  • API keys (sans hash)
  • Subscription + redemptions de coupons
  • Memberships d'organisations
  • Notes admin
  • Email logs
  • Filleuls (referrals)

Limites : MAX_PER_RELATION = 5000 rows par relation pour éviter le DoS sur des users très actifs. Rate-limit 10/h par admin.

Le filename est sanitisé via regex (pas d'injection header). L'admin qui a déclenché l'export est loggé en audit (user.gdpr_export).

L'export user-initié (par l'utilisateur lui-même) est dans /api/user/export — même payload, sans certains champs admin (notes).

Allez plus loin

On this page