## Résumé
Implémente le journal d'audit append-only couvrant les 5 tickets de `doc/audit-log.md` et embarque au passage plusieurs corrections périphériques (sidebar Admin/Mon compte, drawer RBAC, Swagger, schema_filter Doctrine) ainsi que l'initialisation de la suite e2e Playwright. Toutes les mutations Doctrine sur les entités portant `#[Auditable]` sont tracées dans une table PostgreSQL dédiée, exposée en lecture seule via API Platform et consultable par les admins dans une page dédiée.
## Ce qui change
### Audit log — cœur de la PR
**Backend**
- Migration : table `audit_log` (UUID v7 natif Postgres en PK, `jsonb changes`, 3 index pour tri chrono, par entité et par utilisateur).
- `AuditLogWriter` : service bas-niveau, écrit via une connexion DBAL dédiée `audit` (même DSN que `default`, service séparé) pour sortir de la transaction ORM en batch. Blacklist defense-in-depth `password`/`plainPassword`/`token`/`secret`.
- `RequestIdProvider` : UUID v4 généré au `kernel.request` principal, injecté dans chaque ligne d'audit de la requête.
- Attributs `#[Auditable]` / `#[AuditIgnore]` dans `src/Shared/Domain/Attribute/` (accessibles par tous les modules).
- `AuditListener` : capture `onFlush` / écriture `postFlush` avec pattern swap-and-clear contre les flushes ré-entrants. Erreurs loguées, jamais propagées. Entité `User` annotée (password / plainPassword ignorés).
- API Platform read-only `/api/audit-logs` (permission RBAC `core.audit_log.view`) : `GET` collection paginée + `GET` item, pas de POST/PUT/PATCH/DELETE. Filtres `entity_type`, `entity_id`, `action`, `performed_by`, `performed_at[after]`/`[before]`.
- `DbalPaginator` implémentant `PaginatorInterface` : `hydra:view` généré automatiquement par API Platform, pas de construction manuelle.
- Ressource `AuditLogEntityTypesResource` + provider dédié pour peupler le filtre par type d'entité côté UI (réponse cachée, pas de requête à chaque ouverture du drawer).
- Permission `core.audit_log.view` déclarée dans `CoreModule::permissions()`.
- `audit_log` exclu du `schema_filter` Doctrine : plus de faux diff sur `make migration-diff`.
**Frontend**
- Page admin `/admin/audit-log` : tableau paginé, filtres locaux (état dans le composant, non persistés dans l'URL — conforme règle CLAUDE.md « Tableaux : pas de persistance URL »), drawer de détail (diff + timeline complète de l'entité), badges colorés par action.
- Composable partagé `useAuditLog` avec `resetAuditLog()` auto-enregistré sur `onAuthSessionCleared` (règle CLAUDE.md composables singletons).
- Composant réutilisable `<AuditTimeline :entity-type :entity-id>` : garde permission (pas d'appel API sans le droit), lazy loading (10 items + bouton « Voir plus »), dates relatives FR via `Intl.RelativeTimeFormat`, skeleton loader.
- Entrée sidebar « Journal d'audit » gated sur `core.audit_log.view` + clés i18n imbriquées dans `fr.json`.
### Fixes embarqués
- **Review fixes audit-log** (commits `37eafd2`, `1505e84`, `99c77eb`) : précision des timestamps, `ESCAPE` sur les `LIKE`, plafond pagination, diverses remarques du 1er tour de review.
- **Sidebar** (`701a480`, `e2fbf51`) : nouvelle section « Administration » + groupe « Mon compte », gate de section sur permissions, « Tableau de bord » déplacé dans « Mon compte ». Convention admin documentée.
- **Drawer RBAC utilisateurs** (`617ee31`, `5f5afcc`) : corrige l'affichage des sites et l'écrasement via merge-patch (garde anti-écrasement + spec `GET /users/{id}/rbac` documentée).
- **Swagger UI** (`6db955f`) : réactivé en ajoutant `symfony/twig-bundle` aux deps (régression depuis l'arrivée d'API Platform 4.2).
- **`phpunit.dist.xml`** : `<env APP_ENV=dev>` forçait la suite à tourner sous `framework.test=false` (→ `test.service_container` introuvable) ; `JWT_PASSPHRASE` ne matchait pas les clés de dev. Corrigés pour débloquer la suite.
### E2E Playwright (nouveau, commit `4603ab2`)
- `playwright.config.ts` + structure `frontend/tests/e2e/` (personas, helpers `loginAs`, page objects `LoginPage` + `SidebarComponent`).
- Specs : `auth/login.spec.ts` + `permissions/sidebar-visibility.spec.ts` (vérifie la visibilité de la sidebar par rôle RBAC).
- Commande `SeedE2ECommand` pour préparer un jeu de données déterministe côté backend.
- `make e2e` ajouté au Makefile.
## Décisions techniques
- **UUID v7 natif Postgres** (16 octets vs 36 en varchar) : index `performed_at` ~40 % plus petit sur une table append-only à croissance infinie.
- **`entity_type` format `module.Entity`** (ex: `core.User`) : évite les collisions si deux modules ont des entités de même nom.
- **`performed_by` dénormalisé** (string, pas FK) : le nom persiste même après suppression de l'utilisateur.
- **Connexion DBAL dédiée `audit`** : évite l'entanglement transactionnel entre audit et ORM en batch.
- **`ManyToMany` non audité** : limitation connue (`getEntityChangeSet()` ne couvre pas les collections) ; extension future via `getScheduledCollectionUpdates()` si besoin.
- **Filtres locaux non persistés dans l'URL** : choix assumé (cf. CLAUDE.md) pour éviter le couplage table ↔ routeur.
## Test plan
- [x] `make test` : 218 tests passent (writer unitaires + listener intégration + API fonctionnels + UserRbacProcessor).
- [x] `npm run lint` + `npm run test` + `npm run build` (frontend).
- [x] Migration appliquée sur dev + test, `audit_log` ignoré par `schema_filter`.
- [x] Permissions synchronisées (`app:sync-permissions`).
- [x] Swagger `/api/docs` accessible de nouveau.
- [ ] Playwright : `make e2e` vert en local (login + sidebar-visibility).
- [ ] Vérifier en local : création/modif/suppression d'un user apparaît dans `/admin/audit-log`.
- [ ] Vérifier : user sans `core.audit_log.view` → 403 sur l'endpoint + item absent de la sidebar.
- [ ] Vérifier : expansion d'une ligne affiche la timeline de l'entité avec dates relatives FR.
- [ ] Vérifier : drawer RBAC utilisateur n'écrase plus la liste des sites au `PATCH`.
## Points d'attention pour le review
- `AuditListener` : pattern swap-and-clear sur `postFlush` — relire la gestion des flushes ré-entrants.
- `DbalPaginator` : vérifier que l'absence d'`Iterator` custom ne casse pas la normalisation API Platform sur collections vides.
- `UserRbacProcessor` : logique merge-patch + garde anti-écrasement des sites (régression corrigée dans `617ee31`).
- Playwright : nouvelle dépendance de dev, s'assurer que `make e2e` ne fait pas partie du pipeline CI par défaut (à brancher explicitement).
Co-authored-by: Matthieu <mtholot19@gmail.com>
Reviewed-on: #9
Co-authored-by: matthieu <matthieu@yuno.malio.fr>
Co-committed-by: matthieu <matthieu@yuno.malio.fr>
425 lines
17 KiB
Markdown
425 lines
17 KiB
Markdown
# Audit Log — Specification technique
|
|
|
|
## Objectif
|
|
|
|
Tracer l'historique de toutes les modifications BDD dans une table `audit_log` append-only. L'audit est opt-in via l'attribut `#[Auditable]` sur les entites, expose en lecture seule via API Platform (permission RBAC `core.audit_log.view`), et visualise dans le frontend via une page dediee et un composant timeline reutilisable.
|
|
|
|
**Regle projet** : toute entite (nouvelle ou existante) doit etre annotee `#[Auditable]` avec `#[AuditIgnore]` sur les champs sensibles. L'audit n'est pas optionnel — il est obligatoire sur toutes les entites metier.
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
src/
|
|
Shared/
|
|
Domain/
|
|
Attribute/
|
|
Auditable.php # Attribut classe — active le tracking
|
|
AuditIgnore.php # Attribut propriete — exclut un champ
|
|
Module/
|
|
Core/
|
|
CoreModule.php # + permission core.audit_log.view
|
|
Application/
|
|
DTO/
|
|
AuditLogOutput.php # DTO lecture seule
|
|
Infrastructure/
|
|
Audit/
|
|
AuditLogWriter.php # Ecrit via DBAL (pas Doctrine ORM)
|
|
RequestIdProvider.php # UUID v4 par requete HTTP
|
|
Doctrine/
|
|
AuditListener.php # Listener onFlush/postFlush
|
|
Migrations/ # (migration dans migrations/ racine — cf. bug tri FQCN)
|
|
ApiPlatform/
|
|
Resource/
|
|
AuditLogResource.php # ApiResource read-only
|
|
State/
|
|
Provider/
|
|
AuditLogProvider.php # Provider DBAL
|
|
|
|
frontend/
|
|
shared/
|
|
composables/
|
|
useAuditLog.ts # Composable partage (page + timeline)
|
|
components/
|
|
audit/
|
|
AuditTimeline.vue # Timeline verticale reutilisable
|
|
types/
|
|
index.ts # + AuditLogEntry, AuditLogFilters, HydraView
|
|
utils/
|
|
api.ts # + support hydra:view pagination
|
|
modules/
|
|
core/
|
|
pages/
|
|
admin/
|
|
audit-log.vue # Page globale admin
|
|
```
|
|
|
|
---
|
|
|
|
## Table PostgreSQL `audit_log`
|
|
|
|
Table non geree par Doctrine ORM (pas d'entite). Ecriture via DBAL uniquement pour eviter la recursion des listeners.
|
|
|
|
### Schema
|
|
|
|
| Colonne | Type | Contrainte | Description |
|
|
|---------|------|-----------|-------------|
|
|
| `id` | uuid | PK | UUID v7 genere en PHP (`Uuid::v7()->toRfc4122()`) — type natif PG (16 octets vs 36 en varchar) |
|
|
| `entity_type` | varchar(100) | NOT NULL | Format `module.Entity` (ex: `core.User`, `commercial.Client`) — evite les collisions inter-modules |
|
|
| `entity_id` | varchar(64) | NOT NULL | ID de l'entite (supporte int et UUID) |
|
|
| `action` | varchar(10) | NOT NULL | `create`, `update`, `delete` |
|
|
| `changes` | jsonb | NOT NULL DEFAULT '{}' | Changements (format selon action) |
|
|
| `performed_by` | varchar(100) | NOT NULL | Username denormalise (survit a la suppression du user) |
|
|
| `performed_at` | timestamptz | NOT NULL | Horodatage de l'action |
|
|
| `ip_address` | varchar(45) | NULL | Adresse IP (null en CLI) |
|
|
| `request_id` | varchar(36) | NULL | UUID v4 par requete HTTP (null en CLI) |
|
|
|
|
### Index
|
|
|
|
- `idx_audit_entity_time` : `(entity_type, entity_id, performed_at)` — recherche par entite
|
|
- `idx_audit_performer` : `(performed_by, performed_at)` — recherche par utilisateur
|
|
- `idx_audit_time` : `(performed_at)` — tri chronologique global
|
|
|
|
### Regles
|
|
|
|
- **Append-only** : pas d'UPDATE, pas de DELETE
|
|
- **Colonnes en minuscules** (convention PostgreSQL du projet)
|
|
- **Champs sensibles exclus** : `password`, `plainPassword`, `token`, `secret` ne doivent jamais apparaitre dans `changes`
|
|
- **`performed_by` denormalise** : string, pas FK — le nom persiste meme si l'utilisateur est supprime
|
|
- **Migration** : dans `migrations/` (namespace racine `DoctrineMigrations`) a cause du bug de tri alphabetique FQCN de Doctrine Migrations 3.x entre namespaces
|
|
|
|
### Contrat : ce que `audit_log` garantit (et ne garantit pas)
|
|
|
|
`audit_log` enregistre les **tentatives de modification** capturees par le `postFlush` Doctrine, ecrites via une connexion DBAL dediee (`audit_connection`). Ce choix est intentionnel : les lignes d'audit survivent au rollback eventuel de la transaction metier principale, ce qui permet de tracer les tentatives meme en cas d'echec applicatif.
|
|
|
|
**Conséquence à connaître** : si un controller enveloppe plusieurs operations dans une transaction explicite sur la connexion `default` et que cette transaction outermost rollback apres un flush intermediaire reussi, la ligne audit correspondante **persiste** sur la connexion `audit` alors que la modification metier a ete annulee. L'audit log peut donc contenir des lignes decrivant un etat qui n'existe pas en base metier.
|
|
|
|
En pratique :
|
|
- Ce cas est rare dans un CRM interne (les rollbacks explicites outermost sont marginaux par rapport aux flushes atomiques).
|
|
- La ligne audit garde son `request_id` qui permet une correlation post-mortem avec les logs applicatifs pour distinguer une tentative avortee d'un commit reussi.
|
|
- Le comportement est volontaire — pas un bug. Pour un besoin de garantie « audit = reflet exact du commit outermost », il faudrait basculer l'audit sur la meme connexion que le metier (voir `AuditLogWriter`), au prix de perdre la resilience au rollback partiel.
|
|
|
|
L'audit est donc un **journal des intentions appliquees par l'ORM**, pas une source de verite transactionnelle sur l'etat final de la DB.
|
|
|
|
---
|
|
|
|
## Composants backend
|
|
|
|
### `AuditLogWriter`
|
|
|
|
**Emplacement** : `src/Module/Core/Infrastructure/Audit/AuditLogWriter.php`
|
|
|
|
Service responsable de l'ecriture dans `audit_log` via `Connection::executeStatement()`.
|
|
|
|
**Dependances** :
|
|
- `Doctrine\DBAL\Connection` — connexion DBAL dediee `audit` (meme DSN, service separe) pour eviter l'entanglement transactionnel avec l'ORM. Config : `doctrine.dbal.connections.audit` dans `doctrine.yaml`. Injection via `#[Autowire(service: 'doctrine.dbal.audit_connection')]`.
|
|
- `Symfony\Bundle\SecurityBundle\Security`
|
|
- `Symfony\Component\HttpFoundation\RequestStack`
|
|
- `RequestIdProvider`
|
|
|
|
**Methode principale** :
|
|
```php
|
|
public function log(
|
|
string $entityType,
|
|
string $entityId,
|
|
string $action,
|
|
array $changes,
|
|
): void
|
|
```
|
|
|
|
**Comportement** :
|
|
- Genere `id` via `Uuid::v7()->toRfc4122()`
|
|
- `performed_by` = `$security->getUser()?->getUserIdentifier() ?? 'system'`
|
|
- `ip_address` = `$requestStack->getCurrentRequest()?->getClientIp()`
|
|
- `request_id` = `$requestIdProvider->getRequestId()`
|
|
- `performed_at` = `new \DateTimeImmutable('now', new \DateTimeZone('UTC'))`
|
|
- Filtre les cles sensibles (`password`, `plainPassword`, `token`, `secret`) de `$changes`
|
|
- INSERT SQL brut via DBAL
|
|
|
|
**Necessite** : `composer require symfony/uid`
|
|
|
|
### `RequestIdProvider`
|
|
|
|
**Emplacement** : `src/Module/Core/Infrastructure/Audit/RequestIdProvider.php`
|
|
|
|
Service singleton qui genere un UUID v4 unique par requete HTTP principale.
|
|
|
|
**Comportement** :
|
|
- Ecoute `kernel.request` via `#[AsEventListener]`
|
|
- Ignore les sub-requests : `if (!$event->isMainRequest()) return;`
|
|
- Genere `Uuid::v4()->toRfc4122()` a chaque requete principale
|
|
- Expose `getRequestId(): ?string` (null en CLI)
|
|
|
|
### Attributs `#[Auditable]` et `#[AuditIgnore]`
|
|
|
|
**Emplacement** : `src/Shared/Domain/Attribute/` (dans Shared, pas Core — tous les modules doivent y acceder)
|
|
|
|
- `#[Auditable]` : attribut de classe, marqueur vide. Active le tracking sur l'entite.
|
|
- `#[AuditIgnore]` : attribut de propriete, marqueur vide. Exclut un champ du tracking.
|
|
|
|
### `AuditListener`
|
|
|
|
**Emplacement** : `src/Module/Core/Infrastructure/Doctrine/AuditListener.php`
|
|
|
|
Listener Doctrine (pas EventSubscriber — deprecie Symfony 8) utilisant `#[AsDoctrineListener]`.
|
|
|
|
**Evenements** :
|
|
- `onFlush` : collecte les changesets (aucune ecriture)
|
|
- `postFlush` : ecrit via `AuditLogWriter` (hors transaction Doctrine)
|
|
|
|
**Dependances** :
|
|
- `AuditLogWriter`
|
|
- `LoggerInterface`
|
|
|
|
**Logique `onFlush`** :
|
|
1. Recupere `UnitOfWork`
|
|
2. Parcourt insertions, updates, deletions
|
|
3. Pour chaque entite : verifie `#[Auditable]` via `ReflectionClass::getAttributes()`
|
|
4. Filtre les proprietes `#[AuditIgnore]` + blacklist hardcodee
|
|
5. Formate les changements :
|
|
- **create** : snapshot complet de toutes les proprietes non-ignorees
|
|
- **update** : `{champ: {old: x, new: y}}` via `getEntityChangeSet()`
|
|
- **delete** : snapshot complet
|
|
6. ManyToOne : log l'ID via `?->getId()` (null-safe pour les relations nullable), pas l'objet
|
|
7. Stocke dans `$pendingLogs` (propriete privee)
|
|
|
|
**Logique `postFlush`** — pattern swap-and-clear (protection contre flush re-entrant) :
|
|
1. Copie `$pendingLogs` dans variable locale, vide immediatement `$this->pendingLogs = []`
|
|
2. Pour chaque log copie → `AuditLogWriter::log()`
|
|
3. Try/catch : erreur → `$logger->error(...)`, jamais de crash
|
|
|
|
**Cas particuliers** :
|
|
- Flush sans changement → rien
|
|
- Entite sans `#[Auditable]` → ignoree
|
|
- Batch (fixtures, import) → chaque entite auditee, groupees par `request_id`
|
|
- Console → `performed_by = 'system'`, `ip_address = null`, `request_id = null`
|
|
- ManyToMany / OneToMany : tracees via `UnitOfWork::getScheduledCollectionUpdates()` et `getScheduledCollectionDeletions()` (cf. `AuditListener::captureCollectionChange`). Payload `{fieldName: {added: [ids], removed: [ids]}}`, merge dans le log deja en attente de l'entite proprietaire si elle est aussi scheduled (insertion → snapshot enrichi, update → diff merge, delete → ignore car redondant avec le snapshot delete).
|
|
|
|
---
|
|
|
|
## API Platform — Lecture seule
|
|
|
|
### `AuditLogResource`
|
|
|
|
**Emplacement** : `src/Module/Core/Infrastructure/ApiPlatform/Resource/AuditLogResource.php`
|
|
|
|
**Operations** :
|
|
- `GET /api/audit-logs` — collection paginee (30 items/page), tri `performed_at DESC`
|
|
- `GET /api/audit-logs/{id}` — detail
|
|
|
|
**Securite** : `is_granted('core.audit_log.view')` — permission RBAC, 403 sinon
|
|
|
|
**Pas d'endpoints d'ecriture** : POST, PUT, PATCH, DELETE → 405
|
|
|
|
### `AuditLogOutput`
|
|
|
|
**Emplacement** : `src/Module/Core/Application/DTO/AuditLogOutput.php`
|
|
|
|
DTO readonly avec les champs : `id`, `entityType`, `entityId`, `action`, `changes`, `performedBy`, `performedAt`, `ipAddress`, `requestId`.
|
|
|
|
### `AuditLogProvider`
|
|
|
|
**Emplacement** : `src/Module/Core/Infrastructure/ApiPlatform/State/Provider/AuditLogProvider.php`
|
|
|
|
Provider DBAL (pas Doctrine ORM).
|
|
|
|
**Filtres** (query params, combinables en AND) :
|
|
- `entity_type` : filtre exact
|
|
- `entity_id` : filtre exact
|
|
- `action` : filtre exact
|
|
- `performed_by` : filtre exact
|
|
- `performed_at[after]` : date minimum (incluse)
|
|
- `performed_at[before]` : date maximum (incluse)
|
|
|
|
**Pagination** : via un `DbalPaginator` implementant `ApiPlatform\State\Pagination\PaginatorInterface` (`getCurrentPage()`, `getLastPage()`, `getTotalItems()`, `getItemsPerPage()`, `count()`, `getIterator()`). Le provider retourne ce paginator — API Platform genere automatiquement `hydra:view`. Pas de construction manuelle de la pagination.
|
|
|
|
### Permission RBAC
|
|
|
|
Ajouter dans `CoreModule::permissions()` :
|
|
- `core.audit_log.view`
|
|
|
|
---
|
|
|
|
## Frontend
|
|
|
|
### Composable `useAuditLog.ts`
|
|
|
|
**Emplacement** : `frontend/shared/composables/useAuditLog.ts`
|
|
|
|
Composable partage, reutilise par la page globale (ticket 4) et le composant timeline (ticket 5).
|
|
|
|
**Methodes** :
|
|
- `fetchLogs(filters?: AuditLogFilters): Promise<HydraCollection<AuditLogEntry>>`
|
|
- `fetchLogById(id: string): Promise<AuditLogEntry>`
|
|
- `fetchEntityLogs(entityType: string, entityId: string, page?: number): Promise<HydraCollection<AuditLogEntry>>`
|
|
|
|
Utilise `useApi().get()`.
|
|
|
|
Si le composable maintient du state singleton (refs module-level pour cache), il doit exposer `resetAuditLog()` et etre reinitialise au logout (regle CLAUDE.md).
|
|
|
|
### Types
|
|
|
|
Ajouter dans `frontend/shared/types/index.ts` :
|
|
|
|
```typescript
|
|
export interface AuditLogEntry {
|
|
id: string
|
|
entityType: string
|
|
entityId: string
|
|
action: 'create' | 'update' | 'delete'
|
|
changes: Record<string, unknown>
|
|
performedBy: string
|
|
performedAt: string
|
|
ipAddress: string | null
|
|
requestId: string | null
|
|
}
|
|
|
|
export interface AuditLogFilters {
|
|
entityType?: string
|
|
entityId?: string
|
|
action?: string
|
|
performedBy?: string
|
|
performedAtAfter?: string
|
|
performedAtBefore?: string
|
|
page?: number
|
|
}
|
|
|
|
interface HydraView {
|
|
'hydra:first'?: string
|
|
'hydra:last'?: string
|
|
'hydra:next'?: string
|
|
'hydra:previous'?: string
|
|
}
|
|
```
|
|
|
|
Le type `HydraView` doit etre ajoute dans `frontend/shared/utils/api.ts` (a cote de `HydraCollection`) et `HydraCollection` doit etre etendu avec un champ optionnel `'hydra:view'?: HydraView`.
|
|
|
|
### Page `admin/audit-log.vue`
|
|
|
|
**Emplacement** : `frontend/modules/core/pages/admin/audit-log.vue`
|
|
|
|
**Acces** : permission RBAC `core.audit_log.view` (verifie via `usePermissions().can('core.audit_log.view')`)
|
|
|
|
**Elements** :
|
|
- Tableau pagine avec style projet (header `bg-tertiary-500`, rows hover)
|
|
- Filtres : plage dates, type entite (select), utilisateur (input), action (checkboxes), bouton reset
|
|
- Filtres persistes dans les query params URL
|
|
- Ligne expandable au clic :
|
|
- update : tableau champ / ancienne valeur / nouvelle valeur
|
|
- create/delete : snapshot complet
|
|
- Badges action :
|
|
- create : `bg-green-100 text-green-800`
|
|
- update : `bg-yellow-100 text-yellow-800`
|
|
- delete : `bg-red-100 text-red-800`
|
|
- Pagination prev/next via `hydra:view`
|
|
- Etat vide : message i18n "Aucune activite enregistree"
|
|
- Chargement initial : 30 dernieres entrees sans filtre
|
|
|
|
### Sidebar
|
|
|
|
Ajouter entree dans `config/sidebar.php` :
|
|
- Label : `sidebar.core.audit_log`
|
|
- Route : `/admin/audit-log`
|
|
- Icon : a definir (ex: `mdi:clipboard-text-clock`)
|
|
- Module : `core`
|
|
- Permission : `core.audit_log.view` — filtre automatiquement cote SidebarProvider
|
|
|
|
### Composant `AuditTimeline.vue`
|
|
|
|
**Emplacement** : `frontend/shared/components/audit/AuditTimeline.vue`
|
|
|
|
Composant reutilisable, auto-importe par Nuxt.
|
|
|
|
**Props** :
|
|
- `entityType: string`
|
|
- `entityId: string | number`
|
|
|
|
**Comportement** :
|
|
- Garde permission : si `!usePermissions().can('core.audit_log.view')` → rendu vide, aucun appel API
|
|
- Timeline verticale : bordure gauche (`border-l-2 border-gray-200`) + dots colores par action
|
|
- Chaque entree : icone + date relative FR (`Intl.RelativeTimeFormat('fr')`) + date absolue en tooltip + utilisateur + resume
|
|
- Update : affiche old → new par champ
|
|
- Lazy loading : 10 items initiaux + bouton "Voir plus"
|
|
- Skeleton loader pendant le chargement
|
|
- Etat vide : "Aucun historique"
|
|
|
|
**Premiere integration** : sur la page `admin/audit-log.vue`
|
|
|
|
---
|
|
|
|
## i18n
|
|
|
|
Cles a ajouter dans `frontend/i18n/locales/fr.json` :
|
|
|
|
Structure imbriquee (respecte le format existant de `fr.json`) :
|
|
|
|
```json
|
|
{
|
|
"sidebar": {
|
|
"core": {
|
|
"audit_log": "Journal d'audit"
|
|
}
|
|
},
|
|
"audit": {
|
|
"action": {
|
|
"create": "Création",
|
|
"update": "Modification",
|
|
"delete": "Suppression"
|
|
},
|
|
"entity": {
|
|
"user": "Utilisateur"
|
|
},
|
|
"empty": "Aucune activité enregistrée",
|
|
"no_results": "Aucun résultat pour ces filtres",
|
|
"timeline": {
|
|
"empty": "Aucun historique",
|
|
"load_more": "Voir plus"
|
|
},
|
|
"filters": {
|
|
"reset": "Réinitialiser",
|
|
"date_from": "Du",
|
|
"date_to": "Au",
|
|
"entity_type": "Type d'entité",
|
|
"user": "Utilisateur",
|
|
"action": "Action"
|
|
},
|
|
"detail": {
|
|
"field": "Champ",
|
|
"old_value": "Ancienne valeur",
|
|
"new_value": "Nouvelle valeur"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Ordre d'implementation
|
|
|
|
```
|
|
Ticket 1 ────► Ticket 2 ────► Ticket 3 ────┬──► Ticket 4
|
|
Table + Attributs + API │ Page admin
|
|
Writer Listener read-only │
|
|
└──► Ticket 5
|
|
Timeline
|
|
(4 et 5 en parallele)
|
|
```
|
|
|
|
---
|
|
|
|
## Decisions techniques (issues reviews)
|
|
|
|
- **Connexion DBAL dediee** : `AuditLogWriter` utilise une connexion separee `audit` (meme DSN) pour eviter l'entanglement transactionnel avec l'ORM en batch
|
|
- **PaginatorInterface** : le provider retourne un `DbalPaginator` implementant l'interface API Platform — pas de construction manuelle `hydra:view`
|
|
- **Type natif `uuid` PG** : 16 octets vs 36 en varchar, index 40% plus petit sur table append-only a croissance infinie
|
|
- **Pattern swap-and-clear** dans `postFlush` : protection contre flush re-entrant
|
|
- **Blacklist exact-match** sur noms de proprietes (`password`, `plainPassword`, `token`, `secret`) — en defense-in-depth avec `#[AuditIgnore]`
|
|
- **Collections to-many auditees** : tracees via `getScheduledCollectionUpdates` / `getScheduledCollectionDeletions`, payload `{added, removed}` merge dans le changeset de l'entite proprietaire (cf. `AuditListener::captureCollectionChange`)
|
|
- **Erreur audit silencieuse** : loguee, jamais propagee — pas de retry/dead-letter (acceptable pour CRM interne)
|
|
- **`entity_type` format `module.Entity`** : evite collisions si deux modules ont des entites de meme nom
|
|
|
|
## Dependances externes
|
|
|
|
- `symfony/uid` : generation UUID v7 (id) et v4 (request_id)
|