Audit log
Tracer les actions sensibles — auth, billing, coupons, plans, users.
Toute action sensible passe par logAudit(). Lecture via
/admin/audit (DataList).
Modèle Prisma
model AuditLog {
id String @id @default(cuid())
userId String?
action String // namespacé "domain.verb"
entity String? // "User", "Plan", "Coupon"...
entityId String?
metadata Json? // payload libre
ip String?
userAgent String?
createdAt DateTime @default(now())
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
}Actions trackées
Auth
user.registeruser.loginuser.logoutuser.password_changedauth.password_reset_requestedauth.password_resetuser.2fa_enableduser.2fa_disabled
User & admin
user.impersonateuser.unlockuser.delete_scheduleduser.delete_canceleduser.exportuser.connection_linked/user.connection_unlinkeduser.avatar_removed— suppression d'avatar par un modérateur/admin
Billing
subscription.created— aucheckout.session.completedStripesubscription.updated— à chaque renouvellement (invoice.payment_succeeded)subscription.canceled— quand l'user résilie viaPOST /api/billing/cancelsubscription.reactivated— quand l'user annule sa résiliation viaPOST /api/billing/reactivatesubscription.ended— après expiration effectivebilling.payment_failedbilling.refundbilling.comp_dayscredits.purchased— achat one-time de credit pack (webhook Stripe)
Coupons
coupon.createdcoupon.updatedcoupon.disabledcoupon.redeemed
Plans
plan.createdplan.updatedplan.deleted
Logger une action
import { logAudit } from "@/lib/audit";
await logAudit({
userId: session.user.id,
action: "myfeature.something_happened",
entity: "MyEntity",
entityId: row.id,
metadata: { foo: "bar" },
});L'IP et user-agent sont récupérés automatiquement depuis headers() si
disponibles.
Convention de naming
<domaine>.<verbe> :
- domaine =
user,auth,subscription,plan,coupon, ou la feature - verbe = action passée (
created,deleted,updated,redeemed, etc.)
Évite les actions trop génériques (user.update) — préfère
user.password_changed, user.locale_changed etc. pour pouvoir filtrer
côté admin.
Page admin /admin/audit
DataList avec :
- Pagination cursor (50 lignes / page, lazy)
- Filtre par
action(autocomplete sur les actions distinctes) - Filtre par user
- Cards montrent : action, user (avec link vers
/admin/users/[id]), timestamp relatif, badge entity, metadata expandable
API : GET /api/admin/audit?cursor=...&action=...&userId=....
Rétention
Pas de purge automatique. Si tu accumules des millions de lignes, ajoute un job Trigger.dev qui delete les rows plus vieilles que 90 jours :
await prisma.auditLog.deleteMany({
where: { createdAt: { lt: ninetyDaysAgo } },
});Allez plus loin
- Dashboard admin — section Audit
- DataList — composant utilisé pour la liste
- 2FA / Lockout — actions auth tracées