Backend :
- AuditLogWriter::stripSensitive rendu reellement recursif (matche doc).
- Tests GET /api/permissions/{id} non-admin pour chaque branche OR (gap Codex).
- Gardes non-regression UserRbacProcessor : PATCH /rbac sans clef sites ne
doit ni auto-selectionner currentSite ni exiger sites.manage.
Frontend :
- useAuditLog : renomme export trompeur fetchLogs -> fetchLogsCached, le
nom reflete desormais le comportement (cache pollue sinon).
- RoleDrawer / UserRbacDrawer : catch explicite + message d'erreur +
bouton save disabled si le chargement des referentiels a echoue (evite
un ecrasement silencieux des droits).
- AuditTimeline / AuditLogDetail : `oui`/`non` passent par common.yes/no.
- AuditTimeline : Intl.RelativeTimeFormat et toLocaleString suivent la
locale i18n courante (plus de hardcode 'fr').
E2E :
- sidebar-visibility.spec : remplace waitForLoadState('networkidle')
fragile par attente semantique sur accountDashboardLink (stable en CI).
Tests : 237/237 green, eslint clean, php-cs-fixer clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issues remontees par la seconde passe de review de la PR #9 :
- Regression `GET /api/permissions` 403 silencieux sur les drawers RBAC
(UserRbacDrawer, RoleDrawer) apres le fix precedent qui imposait
`core.permissions.view`. Les users porteurs de `core.users.manage` /
`core.roles.manage` ne voyaient plus le catalogue pour hydrater leurs
checkboxes. Elargit la security expression sur Permission en OR avec
ces deux codes : les gestionnaires ont par nature besoin du catalogue
(codes/libelles seuls, pas de secret expose).
- Race condition dans UserRbacProcessor : `restoreAbsentCollections()`
lisait le snapshot Doctrine hors transaction, puis `wrapInTransaction()`
flushait plus tard. Fenetre courte mais reelle ou une modification
concurrente aurait pu etre annulee par une restauration depuis un
snapshot stale. Deplace l'appel a l'interieur de la transaction.
- Stale-data sur les pages admin users / roles / sites : meme pattern
try/finally sans catch que sur audit-log (deja corrige). Aligne les
trois pages avec un catch qui reset la liste locale.
- Tests manquants : garde de non-regression sur PATCH /rbac sans `sites`
(assure que la collection elle-meme est preservee, pas seulement le
currentSite). Couverture positive sur GET /api/permissions pour les
trois branches OR de la security expression (permissions.view,
users.manage, roles.manage) via des users non-admin.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Permission entity : remplace le guard `ROLE_USER` par `core.permissions.view`
sur GetCollection/Get. Le catalogue complet des permissions RBAC etait
accessible a tout utilisateur authentifie. Ajoute la permission manquante
dans CoreModule::permissions() et inverse les tests standardUser*
(attendent maintenant un 403 pour un user sans la permission).
- UserRbacProcessor::restoreAbsentCollections() : force
PersistentCollection::initialize() avant de lire le snapshot. Pour une
association fetch=LAZY (ex: User::$sites), le snapshot est vide tant que
la collection n'est pas materialisee, ce qui faisait vider silencieusement
tous les sites d'un user sur un PATCH ne contenant pas la cle `sites`.
- admin/audit-log.vue : ajoute un catch sur loadEntries() qui reset
entries/totalItems pour ne pas afficher de donnees stale si le fetch echoue
(reseau coupe, 403 inopinee...). Le toast d'erreur reste gere par useApi.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- TIMESTAMP(6) WITH TIME ZONE + tie-breaker id DESC sur l'ORDER BY pour
garantir un tri deterministe quand plusieurs lignes partagent la meme
timestamp (batch fixture, bulk flush < 1µs).
- Suppression de la clause ESCAPE '\\' redondante (`\` est deja
l'echappement LIKE par defaut en PostgreSQL) et fragile sur
standard_conforming_strings. Le str_replace des wildcards reste.
- paginationMaximumItemsPerPage : 100 -> 50. Reduit le pire cas de
reponse lourde sur un endpoint admin (changes JSONB volumineux).
Le drawer RBAC de /admin/users initialisait l'etat des sites a partir du payload
/api/users (groupe user:list) qui n'expose pas la collection sites. Consequence :
la section "Sites autorises" affichait toujours 0 case cochee, et la sauvegarde
ecrasait silencieusement les sites existants en BDD.
- Ajout d'une operation GET /users/{id}/rbac (groupe user:rbac:read) dediee au
chargement du detail pour l'edition : payload list reste leger, detail riche
sur une URI symetrique au PATCH existant.
- Drawer charge desormais GET /users/{id}/rbac pour initialiser sites, roles
et directPermissions ; UserListItem ne contient plus sites (inutilise).
- Colonne "Sites" retiree de la table /admin/users : l'info est consultee via
le drawer, pas la liste (evite aussi la fuite cross-site pour les users avec
core.users.view mais sans sites.bypass_scope).
- Garde anti-ecrasement dans UserRbacProcessor : respect de la semantique
merge-patch+json (cle absente = preservee, cle = [] = vidage explicite).
Restaure les collections ManyToMany absentes du payload a partir du snapshot
Doctrine. Couvre roles, directPermissions et sites.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resout les 5 findings de la review automatique + couverture ManyToMany
annoncee dans CLAUDE.md :
- AuditListener : resolution de la classe via ClassMetadata plutot que
`$entity::class` direct (defense proxy Doctrine : sous ORM 2 les lazies
sont des `Proxies\__CG__\...`). Test de regression via getReference().
- AuditListener : capture des modifications de collections to-many
(OneToMany / ManyToMany) via getScheduledCollectionUpdates /
getScheduledCollectionDeletions. Les diffs sont mergees dans le
changeset existant ou creent une entree "update" dediee.
- AuditLogResource + Provider : filtre multi-valeurs
`entity_type[]=X&entity_type[]=Y` (IN clause DBAL via
ArrayParameterType::STRING), endpoint `/audit-log-entity-types` pour
alimenter le MalioSelectCheckbox cote front.
- audit-log.vue : refonte complete. Passage a `MalioDataTable`,
composants `Malio*` (MalioInputText, MalioSelectCheckbox, MalioButton),
suppression complete de la persistance URL (`readQuery` / `syncQuery`
/ `route.query`). `datetime-local` conserve avec TODO pointant
l'exception CLAUDE.md.
- AuditTimeline : fix du saut d'items 11-30. `PAGE_SIZE = 10` aligne
avec un `itemsPerPage=10` passe au backend. Token anti-race pour
ignorer les reponses tardives quand l'entite affichee change.
- AuditLogDetail : affichage des diffs de collections to-many (+ / -)
dans le tableau field/old/new existant.
- logout.vue : ajout du `resetAuditLog()` au logout pour eviter qu'un
user suivant (meme onglet) voie l'etat audit de l'ancien.
- Permission / Role / Site : marquage `#[Auditable]`.
- Version bump 0.1.32 → 0.1.34.
Tests : 228 / 228 (221 assertions → 851, dont regressions proxy + M2M).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Blocker
- Frontend attendait `hydra:member` / `hydra:totalItems` / `hydra:view` mais
API Platform 4 sert `member` / `totalItems` / `view` (sans prefixe) sous
ld+json, et un tableau plat sous json. Consequence : tableau admin et
timeline silencieusement vides.
Fix : `useAuditLog` force `Accept: application/ld+json` (necessaire pour
obtenir l'objet Hydra avec pagination), types `HydraCollection`/`HydraView`
renommes, composants accedent aux proprietes sans prefixe. Nouveau test
fonctionnel verrouille le format.
Should-fix
- `AuditLogWriter` : ajout de `'id' => Types::GUID` pour expliciter le type
natif PG `uuid` (fonctionnait par cast implicite mais l'intention etait
floue).
- `AuditListener` docblock : documente que le DQL bulk DELETE/UPDATE et
`Connection::executeStatement()` bypassent le listener (onFlush non
appele). Piege pour les futures commandes de purge.
- `AuditLogResource` : ajout d'une regex UUID dans `requirements` de
l'operation Get — un `GET /api/audit-logs/not-a-uuid` produisait un 500
(cast PG rejete) au lieu d'un 404.
- `audit-log.vue` : le watcher des filtres faisait `filters.page = 1` ce
qui declenchait le watcher de `page`, causant deux `loadEntries()` en
parallele. Fusionne : la navigation page appelle `loadEntries()`
directement depuis `goPrevious`/`goNext`, plus de watcher dedie.
- `useAuditLog.fetchEntityLogs` : bypass du cache `lastCollection` pour ne
pas polluer la reference page-level quand la timeline est ouverte.
- `AuditTimeline.vue` : remplacement du `<div v-if="!canView"/>` vide par
un `v-if` sur le wrapper — aucun DOM quand l'utilisateur n'a pas le droit.
- `AuditListenerTest` tag : retire le `_` (wildcard LIKE SQL) du prefix
pour eviter un faux negatif de match cross-test.
- `AuditLogApiTest` : proprietes `auditConnection` / `runTag` nullable et
tearDown guarde, sinon un echec setUp provoquait un fatal typed-property
au lieu de propager l'exception d'origine.
Stabilite suite de tests
- `doctrine.yaml when@test` : `idle_connection_ttl: 1` sur les deux
connexions pour eviter l'accumulation de connexions orphelines.
- tearDown des tests audit : `close()` explicite sur la connexion audit
apres chaque test.
- `docker-compose.yml` : `max_connections=300` sur la DB dev (defaut PG=100
insuffisant pour 220+ tests * 2 connexions/test).
Implemente le journal d'audit append-only sur toutes les mutations Doctrine
des entites portant #[Auditable]. Couvre les 5 tickets de doc/audit-log.md :
1. Table PG audit_log (uuid PK, jsonb changes, index entity/time/performer)
+ AuditLogWriter (DBAL connexion dediee audit, blacklist defense-in-depth
sur password/plainPassword/token/secret) + RequestIdProvider (UUID v4 par
requete HTTP principale).
2. Attributs Auditable / AuditIgnore dans Shared/Domain/Attribute/
+ AuditListener (onFlush capture + postFlush ecriture hors transaction ORM,
pattern swap-and-clear, erreurs loguees jamais propagees). User annote.
3. API Platform read-only /api/audit-logs (permission core.audit_log.view)
avec filtres entity_type / entity_id / action / performed_by / plage
performed_at + DbalPaginator implementant PaginatorInterface (hydra:view
genere automatiquement).
4. Page admin /admin/audit-log : tableau pagine, filtres persistes en query
params, row expandable (diff + timeline de l'entite), entree sidebar avec
permission. Composable useAuditLog avec resetAuditLog() auto-enregistre
sur onAuthSessionCleared.
5. Composant AuditTimeline reutilisable : garde permission, lazy loading,
dates relatives FR, skeleton loader.
Fix connexe : phpunit.dist.xml forcait APP_ENV=dev via <env> ce qui cablait
framework.test=false et rendait test.service_container indisponible ; le
JWT_PASSPHRASE ne matchait pas non plus les cles dev. Corrige en meme temps
pour debloquer la suite de tests.