Documentation

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.register
  • user.login
  • user.logout
  • user.password_changed
  • auth.password_reset_requested
  • auth.password_reset
  • user.2fa_enabled
  • user.2fa_disabled

User & admin

  • user.impersonate
  • user.unlock
  • user.delete_scheduled
  • user.delete_canceled
  • user.export
  • user.connection_linked / user.connection_unlinked
  • user.avatar_removed — suppression d'avatar par un modérateur/admin

Billing

  • subscription.created — au checkout.session.completed Stripe
  • subscription.updated — à chaque renouvellement (invoice.payment_succeeded)
  • subscription.canceled — quand l'user résilie via POST /api/billing/cancel
  • subscription.reactivated — quand l'user annule sa résiliation via POST /api/billing/reactivate
  • subscription.ended — après expiration effective
  • billing.payment_failed
  • billing.refund
  • billing.comp_days
  • credits.purchased — achat one-time de credit pack (webhook Stripe)

Coupons

  • coupon.created
  • coupon.updated
  • coupon.disabled
  • coupon.redeemed

Plans

  • plan.created
  • plan.updated
  • plan.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

On this page