Files
SIRH/doc/audit-logging.md
T
tristan 832751d1ed
Auto Tag Develop / tag (push) Successful in 9s
feat(audit) : contexte forensique dans le journal d'activité (IP, appareil, device id) (#33)
## Contexte

Certains comptes sont **partagés** par plusieurs personnes (ex. compte « Usine »), y compris depuis des smartphones. Le journal d'activité ne stockait que le `username` → impossible de distinguer les intervenants. Cette PR ajoute un **contexte forensique automatique** à chaque entrée du journal.

## Ce qui est ajouté (capté automatiquement, sans friction utilisateur)

- **Adresse IP** de la requête
- **User-Agent brut** (borné à 1024 caractères)
- **Libellé appareil lisible** dérivé du User-Agent : `Type · OS · Navigateur` (ex. `Mobile · Android · Chrome`)
- **Identifiant d'appareil persistant** envoyé par le front (header `X-Device-Id`, stocké en `localStorage`, borné à 64 car.) — distingue les **appareils** derrière un compte partagé

## Implémentation

- `UserAgentParser` (service maison, sans dépendance) — détection ordonnée OS/navigateur, testée
- 4 colonnes **nullable** sur `audit_logs` + migration réversible (pas de backfill, rétro-compatible)
- Capture **centralisée** dans `AuditLogger::log()` via `RequestStack` — aucun processor modifié
- Champs exposés dans l'API lecture (`AuditLogProvider` + DTO TS aligné) via `AuditLogReadRepositoryInterface` (suit le pattern existant des autres read-repos)
- Front : `useDeviceId` + injection du header `X-Device-Id` dans `useApi` (sur toutes les requêtes, SSR-safe)
- `framework.trusted_proxies` documenté (commenté) pour une IP correcte derrière un reverse proxy
- Docs : `doc/audit-logging.md` + `CLAUDE.md`

## Hors périmètre (étapes suivantes)

- **Écran du journal (`audit-logs.vue`) non modifié** — l'affichage des nouvelles colonnes fera l'objet d'une refonte séparée. Les données sont prêtes côté API.
- La doc in-app (`documentation-content.ts`) n'est pas touchée : le journal est un outil caché `ROLE_SUPER_ADMIN` sans article existant ni niveau de doc super-admin.

## À noter pour le déploiement

- L'IP n'est fiable derrière un reverse proxy qu'une fois `framework.trusted_proxies` activé (livré commenté).

## Tests

`OK (249 tests, 533 assertions)` — sortie PHPUnit propre (aucune notice).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #33
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-24 11:56:42 +00:00

4.0 KiB

Journal des actions (Audit Log)

Objectif

Tracer les actions utilisateurs pour diagnostiquer rapidement les problèmes de calcul signalés. Quand un utilisateur signale une incohérence dans ses heures, RTT ou congés, le journal permet de voir exactement ce qui a été modifié, par qui, et quand.

Accès

  • Rôle requis : ROLE_SUPER_ADMIN (rôle caché, non visible dans l'interface de gestion des utilisateurs)
  • Ajout du rôle : directement en base de données
    UPDATE users SET roles = '["ROLE_ADMIN","ROLE_SUPER_ADMIN"]' WHERE username = 'xxx';
    
  • Page : /audit-logs (lien "Journal" dans la sidebar, visible uniquement avec le rôle)

Actions tracées

Processor Entité Actions
AbsenceWriteProcessor Absence create, delete
WorkHourBulkUpsertProcessor WorkHour create, update, delete
WorkHourSiteValidationProcessor WorkHour site_validate
WorkHourBulkValidationProcessor WorkHour validate
WorkHourBulkSiteValidationProcessor WorkHour site_validate
EmployeeWriteProcessor Employee create, update (changement contrat)
ContractSuspensionWriteProcessor ContractSuspension create, update
EmployeeRttPaymentProcessor EmployeeRttPayment update
EmployeeFractionedDaysProcessor EmployeeLeaveBalance update

Données stockées

Chaque entrée contient :

  • employee : l'employé concerné (FK, nullable)
  • username : l'utilisateur qui a effectué l'action
  • action : type d'action (create, update, delete, validate, site_validate)
  • entityType : type d'entité (work_hour, absence, employee, etc.)
  • description : description lisible en français
  • changes : diff JSON {old: {...}, new: {...}} avec les anciennes/nouvelles valeurs
  • affectedDate : date de travail ou début d'absence (pour filtrage par période)
  • createdAt : horodatage de l'action
  • ipAddress : IP source de la requête (Request::getClientIp()) — nécessite framework.trusted_proxies derrière un reverse proxy, sinon IP du proxy
  • userAgent : User-Agent brut de la requête
  • deviceLabel : libellé lisible dérivé du User-Agent (Type · OS · Navigateur, ex. Mobile · Android · Chrome), via App\Service\UserAgentParser
  • deviceId : identifiant d'appareil persistant envoyé par le front (header X-Device-Id, stocké en localStorage['sirh-device-id']). Distingue les appareils derrière un compte partagé (ex. « Usine »), pas les personnes.

Capture : automatique et centralisée dans AuditLogger::log() (via RequestStack) — aucun processor à modifier. En contexte CLI/cron (pas de requête), ces 4 champs restent null.

⚠️ CORS : le front et l'API sont sur des origines distinctes ; le header X-Device-Id ajouté à chaque requête déclenche un préflight CORS. Il doit figurer dans nelmio_cors.allow_headers (config/packages/nelmio_cors.yaml), sinon le navigateur bloque toutes les requêtes API.

Filtres disponibles

  • Par employé (affecté) — champ texte, recherche partielle sur nom/prénom (insensible à la casse)
  • Par période (date affectée) — sélecteur de plage
  • Par type(s) d'entité (multi-sélection)
  • Par action(s) (multi-sélection)
  • Par utilisateur / compte — champ texte, recherche partielle (insensible à la casse)
  • Par IP (recherche partielle)
  • Par appareil (recherche partielle sur le libellé ou le device id)

Pagination : perPage (10 / 25 / 50 / 100, défaut 10) + page.

L'écran utilise un MalioDataTable, un drawer de filtre (bouton « Filtrer » avec compteur de filtres actifs, état brouillon/appliqué, Réinitialiser/Appliquer) et un drawer de détail ouvert au clic sur une ligne (méta + contexte technique IP/appareil/User-Agent/device id + diff lisible des changements).

Convention

Tout nouveau processor traitant des entités impactant les calculs (heures, absences, contrats, RTT) doit intégrer le service AuditLogger et logger les actions create/update/delete.