Journal d’activité (Audit Log)
Ce document présente le module de journalisation des actions critiques (apps/api/src/audit), qui alimente la page admin “Journal d’activité” du dashboard (apps/dashboard/app/(dashboard)/audit-log). Il répond à un besoin de traçabilité : savoir qui a fait quoi, quand, sur quelle cible, avec quel niveau de gravité — pour les actions administratives et vendeur qui affectent d’autres comptes ou la configuration de la marketplace.
1. Vue d’ensemble
Le module expose un unique service, AuditService, marqué @Global() : il est injectable depuis n’importe quel module métier (Users, Products, Orders, Payments, Certifications, Settings, Reviews, Sellers, Leads, Countries, Auth…) sans avoir à réimporter AuditModule partout. Chaque module métier qui déclenche une action jugée sensible appelle auditService.log(...) en plus de sa logique métier normale — l’écriture d’un log n’est jamais une fin en soi, c’est un effet de bord d’une action réelle.
Le résultat est consultable par les administrateurs sur la page “Journal d’activité” du dashboard, avec recherche, filtres et pagination.
2. Modèle de données
Champs (AuditLog, schema.prisma)
| Champ | Type | Description |
|---|---|---|
actorId | String? | Identifiant de l’utilisateur à l’origine de l’action (ou null/'system' pour un acteur serveur, voir §3.C). |
actorEmail | String | Email de l’acteur au moment de l’action. |
actorRole | UserRole | Rôle de l’acteur (ADMIN, SELLER, BUYER) au moment de l’action. |
action | String | Libellé humain de l’action, en français (ex: "Blocage de compte"). Sert aussi de valeur pour le filtre “Action” du dashboard. |
severity | AuditSeverity | INFO | WARNING | CRITICAL (voir §3.B). |
targetType | String? | Nom du modèle affecté (ex: "User", "Product", "Order"). |
targetId | String? | Identifiant de l’enregistrement affecté. |
targetLabel | String? | Libellé lisible de la cible (nom, email, numéro de commande…), capturé au moment de l’action. |
metadata | Json? | Détails contextuels libres (ex: { from: 'PENDING', to: 'APPROVED' }). Affiché en JSON brut dans le détail de la ligne côté dashboard. |
createdAt | DateTime | Horodatage de l’écriture. Indexé, ainsi que severity et actorRole, pour accélérer le filtrage. |
[!IMPORTANT] Aucune clé étrangère vers
User. Le commentaire du schéma est explicite : l’acteur et la cible sont dénormalisés (email, rôle, libellé capturés au moment de l’action) précisément pour que l’historique reste lisible même si le compte concerné est supprimé par la suite. UnDELETEsurUserne casse donc jamais le journal, et ne nécessite aucune règle de cascade ou deSET NULL— le log garde une photographie figée de l’acteur/cible tels qu’ils étaient à l’instantT.
3. Logiques métier clés
A. AuditService.log() ne fait jamais échouer l’action appelante
Extrait de audit.service.ts :
async log(input: AuditLogInput): Promise<void> {
try {
await this.prisma.auditLog.create({ data: { /* ... */ } });
} catch (err) {
this.logger.error(`Échec de l'écriture du journal d'audit: ${err}`);
}
}[!WARNING] Toute erreur d’écriture (base indisponible, contrainte violée, etc.) est avalée et seulement journalisée via le logger NestJS.
log()ne relance jamais l’exception. C’est un choix de conception assumé : la journalisation est secondaire par rapport à l’action métier réelle (bloquer un compte, valider un vendeur…) — elle ne doit jamais empêcher cette action de réussir. La contrepartie est qu’un incident d’écriture du journal est silencieux pour l’appelant et ne doit être détecté qu’via les logs serveur.
B. Sévérité : une convention, pas une règle automatique
AuditSeverity (INFO, WARNING, CRITICAL) est choisie au cas par cas par chaque service appelant, il n’existe pas de règle centrale qui déduit la sévérité depuis le type d’action. En pratique (voir §5), la convention observée dans le code est :
CRITICAL: actions difficilement réversibles ou à fort impact (suppression d’un compte utilisateur).WARNING: actions réversibles mais sensibles (blocage de compte, changement de rôle, annulation de commande, rejet vendeur, désactivation d’un pays/moyen de paiement, refus de sponsorisation).INFO: actions positives ou administratives courantes (déblocage, approbation, création, activation).
C. Acteur système (SYSTEM_ACTOR)
Pour les actions déclenchées côté serveur sans utilisateur humain authentifié (ex : un webhook de passerelle de paiement qui confirme un paiement), le module définit une constante conventionnelle :
export const SYSTEM_ACTOR: AuditActor = {
userId: 'system',
email: 'system@webhook',
role: UserRole.ADMIN,
};Utilisée aujourd’hui par payments.service.ts (résultat du webhook CinetPay) et par auth.service.ts (inscription vendeur, déclenchée par l’auto-inscription elle-même, pas par un admin).
D. Lecture : filtres, recherche, pagination
AuditService.findAll(filters) (audit.service.ts) construit dynamiquement une clause where Prisma :
role: filtre exact suractorRole.severity: filtre exact surseverity.action: filtre exact sur le libellé d’action (alimenté par la liste distincte, voir ci-dessous).days: ne garde que les entrées dontcreatedAt >= now - days.search: recherche texte insensible à la casse (contains,mode: 'insensitive') suractorEmailOUtargetLabelOUactionsimultanément.page/limit: pagination classique,limitbornée entre 1 et 100 (défaut 25), résultat trié parcreatedAt desc.
Le total et les données sont récupérés en une seule transaction Prisma ($transaction([count, findMany])) pour rester cohérents entre eux.
AuditService.getDistinctActions() renvoie la liste triée des libellés d’action déjà journalisés, sans liste codée en dur côté API — elle sert uniquement à peupler dynamiquement le filtre “Action” du dashboard au fil des actions réellement enregistrées.
4. Rôles et Autorisations (RBAC)
Le contrôleur (audit.controller.ts) est protégé dans son ensemble par @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) au niveau de la classe : tous les endpoints sont réservés aux administrateurs, sans exception.
| Endpoint | Méthode | Rôle requis | Description |
|---|---|---|---|
/audit-logs | GET | ADMIN | Liste paginée et filtrable des événements journalisés. |
/audit-logs/actions | GET | ADMIN | Liste des libellés d’action distincts déjà journalisés (alimente le filtre “Action”). |
[!NOTE] La route HTTP réelle est
/audit-logs(pluriel), et non/audit. La route dashboard (Next.js), elle, est/audit-log(singulier) — ne pas confondre les deux lors de l’intégration ou du débogage réseau.
Côté dashboard, la page apps/dashboard/app/(dashboard)/audit-log/page.tsx fait elle-même une vérification défensive du rôle décodé depuis le JWT (payload.role !== 'ADMIN' → redirect('/')), en plus du contrôle serveur côté API.
5. Actions journalisées aujourd’hui
Liste exhaustive des points d’appel de auditService.log(...) trouvés dans apps/api/src à ce jour. Toute nouvelle action métier sensible ajoutée dans le futur devrait suivre le même principe (log en effet de bord, jamais bloquant).
Libellé (action) | Sévérité | Cible (targetType) | Déclencheur / fichier |
|---|---|---|---|
Blocage de compte | CRITICAL | User | Admin bloque un compte (isActive → false) — users.service.ts |
Déblocage de compte | INFO | User | Admin débloque un compte (isActive → true) — users.service.ts |
Changement de rôle | WARNING | User | Admin modifie le rôle d’un utilisateur (metadata: { from, to }) — users.service.ts |
Libellé dynamique (validation/rejet vendeur via users.service.ts) | INFO ou WARNING (WARNING si REJECTED) | User | Changement de vendorStatus (metadata: { from, to }) — users.service.ts |
Suppression utilisateur | CRITICAL | User | Admin supprime définitivement un compte — users.service.ts |
Validation compte vendeur | INFO | User | Admin approuve une demande vendeur (vendorStatus → APPROVED) — sellers.service.ts |
Rejet compte vendeur | WARNING | User | Admin rejette une demande vendeur (metadata: { reason }) — sellers.service.ts |
Inscription vendeur (en attente de validation) | INFO | User | Acteur SYSTEM_ACTOR — auto-inscription d’un vendeur (vendorStatus = PENDING) — auth.service.ts |
Annulation commande | WARNING | Order | Vendeur/Admin passe une commande à CANCELLED — orders.service.ts |
Traitement demande Grands Comptes | INFO | EnterpriseLead | Admin change le statut d’un lead B2B (metadata: { from, to }) — leads.service.ts |
Création moyen de paiement | INFO | PaymentMethod | Admin crée un moyen de paiement — payments.service.ts |
Activation moyen de paiement / Désactivation moyen de paiement | INFO / WARNING | PaymentMethod | Admin (dés)active un moyen de paiement — payments.service.ts |
Paiement confirmé (webhook passerelle) / Paiement échoué (webhook passerelle) | INFO / WARNING | Payment | Acteur SYSTEM_ACTOR — résultat de la vérification serveur-à-serveur d’un paiement (CinetPay) — payments.service.ts |
Création certification | INFO | Certification | Admin crée une certification produit — certifications.service.ts |
Activation certification / Désactivation certification | INFO | Certification | Admin (dés)active une certification — certifications.service.ts |
Création pays | INFO | Country | Admin crée un pays — countries.service.ts |
Activation pays / Désactivation pays | INFO / WARNING | Country | Admin (dés)active un pays — countries.service.ts |
Activation module sponsorisation / Désactivation module sponsorisation | WARNING | SystemSettings | Admin (dés)active l’interrupteur global de sponsorisation — settings.service.ts |
Suppression avis (admin) | WARNING | Review | Admin supprime un avis client — reviews.service.ts |
Suppression produit | WARNING | Product | Suppression d’un produit (vendeur propriétaire ou admin) — products.service.ts |
Demande de sponsorisation | INFO | Product | Vendeur demande la sponsorisation d’un produit (metadata: { mode, price }) — products.service.ts |
Approbation sponsorisation / Refus/désactivation sponsorisation | INFO / WARNING | Product | Décision admin sur une demande de sponsorisation — products.service.ts |
[!NOTE] Cette liste reflète l’état du code au moment de la rédaction (recherche exhaustive de
auditService.log(dansapps/api/src). Le filtre “Action” du dashboard est peuplé dynamiquement depuis les données réellement en base (GET /audit-logs/actions) — il n’y a donc jamais de désynchronisation entre les actions listées ici et celles filtrables dans l’UI, tant que cette table reste à jour avec le code.
6. Interface d’administration (dashboard)
Page : apps/dashboard/app/(dashboard)/audit-log/page.tsx, protégée par une double vérification de rôle (JWT décodé côté page + garde ADMIN côté API).
Filtres disponibles (AuditLogFilters.tsx) :
- Recherche libre (email, cible, libellé d’action).
- Rôle de l’acteur (Admin / Vendeur / Acheteur).
- Action (liste dynamique via
GET /audit-logs/actions). - Fenêtre temporelle (24h / 7 jours / 30 jours / toute la période).
- Sévérité (boutons segmentés : Toutes / Critique / Attention / Info).
Tous les filtres sont reflétés dans l’URL (useSearchParams + router.push), ce qui rend chaque vue filtrée partageable par lien.
Table (AuditLogTable.tsx) : une ligne par événement (horodatage, acteur avec avatar d’initiales et badge de rôle, libellé d’action, cible, badge de sévérité coloré). Chaque ligne est dépliable (“Détails”) et affiche alors l’id de l’événement, le type/l’identifiant de la cible, et le metadata brut formaté en JSON.
Pagination : 25 entrées par page (valeur fixée côté dashboard, passée en limit à l’API), navigation par numéros de page classique.
7. Guide des Endpoints et Exemples
Lister les événements (GET /audit-logs)
- Accès : Admin uniquement (
Authorization: Bearer <token_admin>). - Requête :
GET /audit-logs?role=SELLER&severity=WARNING&days=7&page=1&limit=25 - Réponse (
200 OK) :{ "data": [ { "id": "log-uuid-1", "actorId": "usr-admin-uuid", "actorEmail": "admin@marketplace.cm", "actorRole": "ADMIN", "action": "Rejet compte vendeur", "severity": "WARNING", "targetType": "User", "targetId": "usr-seller-uuid", "targetLabel": "Jean Mballa (Mballa Electronics)", "metadata": { "reason": "Documents incomplets" }, "createdAt": "2026-09-20T14:32:00.000Z" } ], "total": 1, "page": 1, "limit": 25, "totalPages": 1 }
Lister les actions distinctes (GET /audit-logs/actions)
- Accès : Admin uniquement.
- Réponse (
200 OK) :[ "Annulation commande", "Approbation sponsorisation", "Blocage de compte", "Changement de rôle", "Déblocage de compte", "Rejet compte vendeur", "Suppression utilisateur", "Validation compte vendeur" ]
8. Limites connues et évolutions possibles
- Pas de granularité fine sur
metadata: le contenu est libre (Record<string, unknown>), donc chaque service décide de sa propre structure. Il n’y a pas de schéma central validé pour ce champ — un consommateur du journal (export, alerting) doit rester tolérant à des formes hétérogènes selon l’action. - Pas de purge/rétention automatique : le modèle ne prévoit aujourd’hui aucun mécanisme d’archivage ou de suppression des entrées anciennes ; la table
AuditLogcroît indéfiniment. - Pas d’export dans l’UI actuelle (contrairement à la page Rapports, voir reports.md) — seule la consultation filtrée/paginée est disponible.