From e6c8381b3cd35572848c0c14dac66dae219f6411 Mon Sep 17 00:00:00 2001 From: matthieu Date: Wed, 13 May 2026 08:29:30 +0000 Subject: [PATCH] feat : audit log (table + writer + listener + API + admin UI + timeline) (#9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 `` : 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`** : `` 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 Reviewed-on: https://gitea.malio.fr/MALIO-DEV/Coltura/pulls/9 Co-authored-by: matthieu Co-committed-by: matthieu --- .claude/rules/architecture.md | 66 + .claude/rules/backend.md | 55 + .claude/rules/frontend.md | 69 + .claude/rules/git.md | 39 + .claude/rules/naming.md | 18 + .claude/rules/testing.md | 36 + .claude/rules/workflow.md | 66 + .dockerignore | 1 + .gitignore | 16 + CLAUDE.md | 307 +-- README.md | 27 +- REVIEW.md | 730 +++++++ TICKETS.md | 1026 +++++++++ composer.json | 2 + composer.lock | 274 ++- config/bundles.php | 2 + config/packages/api_platform.yaml | 3 + config/packages/doctrine.yaml | 48 +- config/packages/twig.yaml | 6 + config/reference.php | 1911 ----------------- config/services.yaml | 3 + config/sidebar.php | 100 +- config/version.yaml | 2 +- doc/audit-log-review-backlog.md | 466 ++++ doc/audit-log.md | 424 ++++ docker-compose.yml | 5 +- docs/rbac/ticket-345-spec.md | 75 + docs/sites/ticket-02-spec.md | 109 + frontend/i18n/locales/fr.json | 68 +- .../modules/core/components/RoleDrawer.vue | 38 +- .../core/components/UserRbacDrawer.vue | 91 +- .../modules/core/pages/admin/audit-log.vue | 422 ++++ frontend/modules/core/pages/admin/roles.vue | 4 + frontend/modules/core/pages/admin/users.vue | 32 +- frontend/modules/core/pages/logout.vue | 4 +- frontend/modules/sites/pages/admin/sites.vue | 4 + frontend/package-lock.json | 64 + frontend/package.json | 5 +- frontend/playwright.config.ts | 42 + frontend/public/robots.txt | 2 +- .../components/audit/AuditLogDetail.vue | 100 + .../shared/components/audit/AuditTimeline.vue | 252 +++ frontend/shared/composables/useAuditLog.ts | 144 ++ frontend/shared/composables/useModules.ts | 16 +- frontend/shared/composables/useSidebar.ts | 17 +- frontend/shared/types/index.ts | 41 + frontend/shared/types/rbac.ts | 14 +- .../shared/utils/__tests__/debounce.test.ts | 52 + frontend/shared/utils/api.ts | 31 +- frontend/shared/utils/debounce.ts | 15 + frontend/tests/e2e/_fixtures/personas.ts | 112 + frontend/tests/e2e/auth/login.spec.ts | 65 + frontend/tests/e2e/helpers/loginAs.ts | 45 + frontend/tests/e2e/helpers/pages/LoginPage.ts | 32 + .../e2e/helpers/pages/SidebarComponent.ts | 33 + .../permissions/sidebar-visibility.spec.ts | 84 + frontend/vitest.config.ts | 4 + makefile | 95 +- migrations/Version20260420202749.php | 63 + phpunit.dist.xml | 40 + .../Core/Application/DTO/AuditLogOutput.php | 30 + src/Module/Core/CoreModule.php | 2 + src/Module/Core/Domain/Entity/Permission.php | 13 +- src/Module/Core/Domain/Entity/Role.php | 2 + src/Module/Core/Domain/Entity/User.php | 39 +- .../ApiPlatform/Pagination/DbalPaginator.php | 74 + .../Resource/AuditLogEntityTypesResource.php | 34 + .../ApiPlatform/Resource/AuditLogResource.php | 56 + .../Processor/UserPasswordHasherProcessor.php | 2 +- .../State/Processor/UserRbacProcessor.php | 120 ++ .../Provider/AuditLogEntityTypesProvider.php | 35 + .../State/Provider/AuditLogProvider.php | 240 +++ .../ApiPlatform/State/Provider/MeProvider.php | 2 +- .../Infrastructure/Audit/AuditLogWriter.php | 111 + .../Audit/RequestIdProvider.php | 42 + .../Infrastructure/Console/SeedE2ECommand.php | 216 ++ .../DataFixtures/AppFixtures.php | 10 +- .../Infrastructure/Doctrine/AuditListener.php | 513 +++++ src/Module/Sites/Domain/Entity/Site.php | 2 + .../Repository/SiteRepositoryInterface.php | 3 +- .../Doctrine/DoctrineSiteRepository.php | 2 +- src/Shared/Domain/Attribute/AuditIgnore.php | 19 + src/Shared/Domain/Attribute/Auditable.php | 19 + .../Domain/Contract/SiteProviderInterface.php | 21 + .../ApiPlatform/State/SidebarProvider.php | 19 +- templates/base.html.twig | 23 + tests/Module/Core/Api/AuditLogApiTest.php | 451 ++++ tests/Module/Core/Api/PermissionApiTest.php | 91 +- .../Module/Core/Api/UserRbacSitesApiTest.php | 121 ++ .../State/Processor/UserRbacProcessorTest.php | 10 + .../Audit/AuditLogWriterTest.php | 190 ++ .../Doctrine/AuditListenerTest.php | 423 ++++ 92 files changed, 8543 insertions(+), 2309 deletions(-) create mode 100644 .claude/rules/architecture.md create mode 100644 .claude/rules/backend.md create mode 100644 .claude/rules/frontend.md create mode 100644 .claude/rules/git.md create mode 100644 .claude/rules/naming.md create mode 100644 .claude/rules/testing.md create mode 100644 .claude/rules/workflow.md create mode 100644 .dockerignore create mode 100644 REVIEW.md create mode 100644 TICKETS.md create mode 100644 config/packages/twig.yaml delete mode 100644 config/reference.php create mode 100644 doc/audit-log-review-backlog.md create mode 100644 doc/audit-log.md create mode 100644 frontend/modules/core/pages/admin/audit-log.vue create mode 100644 frontend/playwright.config.ts create mode 100644 frontend/shared/components/audit/AuditLogDetail.vue create mode 100644 frontend/shared/components/audit/AuditTimeline.vue create mode 100644 frontend/shared/composables/useAuditLog.ts create mode 100644 frontend/shared/utils/__tests__/debounce.test.ts create mode 100644 frontend/shared/utils/debounce.ts create mode 100644 frontend/tests/e2e/_fixtures/personas.ts create mode 100644 frontend/tests/e2e/auth/login.spec.ts create mode 100644 frontend/tests/e2e/helpers/loginAs.ts create mode 100644 frontend/tests/e2e/helpers/pages/LoginPage.ts create mode 100644 frontend/tests/e2e/helpers/pages/SidebarComponent.ts create mode 100644 frontend/tests/e2e/permissions/sidebar-visibility.spec.ts create mode 100644 migrations/Version20260420202749.php create mode 100644 src/Module/Core/Application/DTO/AuditLogOutput.php create mode 100644 src/Module/Core/Infrastructure/ApiPlatform/Pagination/DbalPaginator.php create mode 100644 src/Module/Core/Infrastructure/ApiPlatform/Resource/AuditLogEntityTypesResource.php create mode 100644 src/Module/Core/Infrastructure/ApiPlatform/Resource/AuditLogResource.php create mode 100644 src/Module/Core/Infrastructure/ApiPlatform/State/Provider/AuditLogEntityTypesProvider.php create mode 100644 src/Module/Core/Infrastructure/ApiPlatform/State/Provider/AuditLogProvider.php create mode 100644 src/Module/Core/Infrastructure/Audit/AuditLogWriter.php create mode 100644 src/Module/Core/Infrastructure/Audit/RequestIdProvider.php create mode 100644 src/Module/Core/Infrastructure/Console/SeedE2ECommand.php create mode 100644 src/Module/Core/Infrastructure/Doctrine/AuditListener.php create mode 100644 src/Shared/Domain/Attribute/AuditIgnore.php create mode 100644 src/Shared/Domain/Attribute/Auditable.php create mode 100644 src/Shared/Domain/Contract/SiteProviderInterface.php create mode 100644 templates/base.html.twig create mode 100644 tests/Module/Core/Api/AuditLogApiTest.php create mode 100644 tests/Module/Core/Infrastructure/Audit/AuditLogWriterTest.php create mode 100644 tests/Module/Core/Infrastructure/Doctrine/AuditListenerTest.php diff --git a/.claude/rules/architecture.md b/.claude/rules/architecture.md new file mode 100644 index 0000000..1ddd203 --- /dev/null +++ b/.claude/rules/architecture.md @@ -0,0 +1,66 @@ +# Architecture — Modular Monolith DDD + +## Principe fondamental +Le **backend est la source de verite unique**. Il dicte : +- Quels modules sont actifs (`config/modules.php`) +- L'organisation de la sidebar (`config/sidebar.php`, decouplee des modules) + +Le frontend scanne `modules/*/` comme layers Nuxt et consomme l'API pour la navigation. Il ne decide rien. + +## Endpoints API cles + +- `GET /api/version` (public) — version de l'app +- `GET /api/modules` (public) — IDs des modules actifs +- `GET /api/sidebar` (public) — sections filtrees par modules actifs + `disabledRoutes` (items dont le module owner est inactif) +- `GET /api/me` (auth) — user courant + +## Arborescence minimale (detail complet : @README.md) + +``` +src/ + Shared/ # Noyau technique partage (Domain/, Application/Bus/, Infrastructure/ApiPlatform/) + Module/ + Core/ # Module obligatoire (auth, users) + CoreModule.php # ID, LABEL, REQUIRED, permissions() + Domain/ Application/ Infrastructure/ + Commercial/ # Exemple d'autre module +frontend/ + app/ # Shell (layouts, middlewares) + shared/ # Code inter-modules (composables, stores, utils) + modules/ # Layers Nuxt auto-detectes + core/ commercial/ +``` + +## Declaration d'un module + +Chaque module expose un `*Module.php` avec : +- `ID` (snake_case, ex: `commercial`, `gestion_rh`) +- `LABEL` +- `REQUIRED` (bool) +- Methode statique `permissions()` retournant les RBAC du module + +## Activer / desactiver un module + +Editer uniquement `config/modules.php` (commenter la ligne). Cascade automatique : +1. `/api/modules` ne retourne plus l'ID +2. `/api/sidebar` filtre les items `module: ''` et supprime les sections vides +3. Middleware front `modules.global.ts` redirige toute navigation vers une route desactivee +4. Le code reste dans le bundle (layer auto-detecte) → reactivation instantanee sans rebuild + +## Reorganiser la sidebar + +Editer uniquement `config/sidebar.php`. Le code des modules n'est pas touche — seule la place des items change. Chaque item reference son module owner via la cle `module`. + +## Communication inter-modules + +**Interdit** : import direct d'une classe d'un autre module. +**Autorise** : +- Via `Shared/Domain/Contract/` (interfaces : `UserResolverInterface`, `TenantAwareInterface`...) +- Via domain events (`Shared/Domain/Event/DomainEventInterface`) + +## Migrations + +- **Par defaut** : `src/Module//Infrastructure/Doctrine/Migrations/` (namespace modulaire) +- **Exception** : les migrations d'initialisation critiques (setup user, RBAC, seed de base) vivent au namespace racine `DoctrineMigrations` dans `migrations/`. + - Raison : avec plusieurs `migrations_paths`, Doctrine Migrations 3.x trie par FQCN alphabetique et non par version timestamp → ordre incorrect entre namespaces sur base vide. + - A supprimer quand un `MigrationsComparator` custom ou un upgrade Doctrine resoudra le tri. diff --git a/.claude/rules/backend.md b/.claude/rules/backend.md new file mode 100644 index 0000000..40c6a05 --- /dev/null +++ b/.claude/rules/backend.md @@ -0,0 +1,55 @@ +# Backend — Regles PHP / Symfony / API Platform + +## Structure de fichier + +- Toujours `declare(strict_types=1);` en tete de tout fichier PHP +- PHP CS Fixer : regles Symfony + PSR-12 + strict types (commande : `make php-cs-fixer-allow-risky`) +- Commentaires (docblock, inline, bloc) **en francais** ; code (classes, methodes, variables) en anglais + +## API Platform (pas de controllers) + +- Toujours utiliser `#[ApiResource]` + Providers + Processors — pas de controllers Symfony classiques +- Routes prefixees `/api` (via `config/routes/api_platform.yaml`) +- Le login `/login_check` est **hors** prefix `/api` (nginx reecrit `REQUEST_URI` vers `/login_check`) +- **Exception** : si tu dois creer un controller custom sous `/api/`, mettre `priority: 1` sur `#[Route]` pour eviter le conflit avec API Platform `{id}` + +## Repositories + +- Interface : `*RepositoryInterface` dans `Domain/Repository/` +- Implementation Doctrine : `Doctrine*Repository` dans `Infrastructure/Doctrine/` +- Le domaine garde les attributs ORM (approche pragmatique) + +## RBAC (permissions) + +Format obligatoire : `module.resource[.subresource].action` en snake_case. +- Exemples : `core.users.view`, `commercial.clients.contacts.edit`, `core.audit_log.view` +- Declarees via la methode statique `permissions()` des `*Module.php` +- Synchronisation : `app:sync-permissions` +- Verification API Platform : `is_granted('module.resource.action')` +- Verification front : `usePermissions()` + +## Roles + +- Hierarchie dans `config/packages/security.yaml` : `ROLE_ADMIN`, `ROLE_USER` +- Le role ne remplace pas la permission RBAC — deux niveaux complementaires + +## Audit (obligatoire) + +- Toute entite metier (nouvelle ou existante) : `#[Auditable]` (de `Shared/Domain/Attribute/`) +- Champs sensibles (password, token, secret) : `#[AuditIgnore]` +- Audit ManyToMany : trace automatiquement `{fieldName: {added: [ids], removed: [ids]}}` — aucune action supplementaire +- Spec complete : @doc/audit-log.md + +## Serialization + +Pour embarquer une relation dans le JSON (au lieu d'un IRI Hydra), ajouter le groupe du parent sur les proprietes de l'entite cible. + +Exemple : pour qu'`User.profile` soit embarque au lieu d'un lien IRI sous le groupe `user:read`, annoter `Profile.$firstName` avec `#[Groups(['user:read'])]`. + +## Upload de fichiers + +- Valider cote serveur avec `$file->getMimeType()` — **jamais** `getClientMimeType()` (spoofable par le client) + +## PostgreSQL + +- Noms de colonnes toujours en **minuscules** dans le SQL brut (commun a tous les projets MALIO) diff --git a/.claude/rules/frontend.md b/.claude/rules/frontend.md new file mode 100644 index 0000000..6c34164 --- /dev/null +++ b/.claude/rules/frontend.md @@ -0,0 +1,69 @@ +# Frontend — Regles Nuxt 4 / Vue 3 / @malio/layer-ui + +## Base + +- TypeScript strict +- 4 espaces d'indentation +- Commentaires (JSDoc, inline, bloc) **en francais** ; code (variables, types) en anglais +- Chaque module front = un layer Nuxt auto-detecte (`frontend/modules/*/nuxt.config.ts` minimal) + +## Appels API + +- Toujours `useApi()` — jamais `$fetch`, `ofetch`, `axios` en direct +- `useApi()` gere : cookies JWT, erreurs, toasts i18n, parsing Hydra + +## Stores (Pinia) + +- `useAuthStore` pour l'authentification +- `useUiStore` pour l'etat UI global (sidebar, modales, etc.) +- Composables avec state singleton (refs module-level) : exposer une fonction `reset*()` et la rappeler au logout (ex: `useSidebar().resetSidebar()`) + +## Middlewares globaux + +- `auth.global.ts` protege les routes + charge la sidebar apres login +- `modules.global.ts` redirige si la route demandee est dans `disabledRoutes` + +## i18n et sidebar + +- Labels de sidebar = cles i18n `sidebar..*`, jamais du texte brut +- Le layout `default.vue` applique `t()` sur les labels retournes par `/api/sidebar` +- Traductions dans `frontend/i18n/locales/` + +## Composants formulaires — @malio/layer-ui obligatoire + +Tout champ de formulaire / filtre doit utiliser les composants `Malio*` plutot que `` / `` / `