Story 1.7 - Implémente un système complet d'audit trail pour tracer
toutes les actions sensibles (authentification, modifications de données,
exports) avec immuabilité garantie par PostgreSQL.
Fonctionnalités principales:
- Table audit_log append-only avec contraintes PostgreSQL (RULE)
- AuditLogger centralisé avec injection automatique du contexte
- Correlation ID pour traçabilité distribuée (HTTP + async)
- Handlers pour événements d'authentification
- Commande d'archivage des logs anciens
- Pas de PII dans les logs (emails/IPs hashés)
Infrastructure:
- Middlewares Messenger pour propagation du Correlation ID
- HTTP middleware pour génération/propagation du Correlation ID
- Support multi-tenant avec TenantResolver
Permet aux utilisateurs de visualiser et gérer leurs sessions actives
sur différents appareils, avec la possibilité de révoquer des sessions
à distance en cas de suspicion d'activité non autorisée.
Fonctionnalités :
- Liste des sessions actives avec métadonnées (appareil, navigateur, localisation)
- Identification de la session courante
- Révocation individuelle d'une session
- Révocation de toutes les autres sessions
- Déconnexion avec nettoyage des cookies sur les deux chemins (legacy et actuel)
Sécurité :
- Cache frontend scopé par utilisateur pour éviter les fuites entre comptes
- Validation que le refresh token appartient à l'utilisateur JWT authentifié
- TTL des sessions Redis aligné sur l'expiration du refresh token
- Événements d'audit pour traçabilité (SessionInvalidee, ToutesSessionsInvalidees)
@see Story 1.6 - Gestion des sessions
Implémentation complète du flux de réinitialisation de mot de passe (Story 1.5):
Backend:
- Aggregate PasswordResetToken avec TTL 1h, UUID v7, usage unique
- Endpoint POST /api/password/forgot avec rate limiting (3/h par email, 10/h par IP)
- Endpoint POST /api/password/reset avec validation token
- Templates email (demande + confirmation)
- Repository Redis avec TTL 2h pour distinguer expiré/invalide
Frontend:
- Page /mot-de-passe-oublie avec message générique (anti-énumération)
- Page /reset-password/[token] avec validation temps réel des critères
- Gestion erreurs: token invalide, expiré, déjà utilisé
Tests:
- 14 tests unitaires PasswordResetToken
- 7 tests unitaires RequestPasswordResetHandler
- 7 tests unitaires ResetPasswordHandler
- Tests E2E Playwright pour le flux complet
L'installation du projet nécessitait plusieurs commandes manuelles et
le Makefile listait les commandes dans un bloc help statique, difficile
à maintenir et susceptible de devenir obsolète.
Le Makefile utilise maintenant le pattern "## description" qui génère
automatiquement l'aide à partir des commentaires inline. Le README
est simplifié avec un `make install` unique qui orchestre tout le setup.
Implémente la Story 1.4 du système d'authentification avec plusieurs
couches de protection contre les attaques par force brute.
Sécurité backend :
- Authentification JWT avec access token (15min) + refresh token (7j)
- Rotation automatique des refresh tokens avec détection de replay
- Rate limiting progressif par IP (délai Fibonacci après échecs)
- Intégration Cloudflare Turnstile CAPTCHA après 5 tentatives
- Alerte email à l'utilisateur après blocage temporaire
- Isolation multi-tenant (un utilisateur ne peut se connecter que sur
son établissement)
Frontend :
- Page de connexion avec feedback visuel des délais et erreurs
- Composant TurnstileCaptcha réutilisable
- Gestion d'état auth avec stockage sécurisé des tokens
- Tests E2E Playwright pour login, tenant isolation, et activation
Infrastructure :
- Configuration Symfony Security avec json_login + jwt
- Cache pools séparés (filesystem en test, Redis en prod)
- NullLoginRateLimiter pour environnement de test (évite blocage CI)
- Génération des clés JWT en CI après démarrage du backend
Plusieurs problèmes empêchaient les tests E2E de passer en CI :
1. Healthcheck : L'endpoint /api nécessite une authentification et
retournait 401, causant l'échec du healthcheck. Remplacé par
/api/docs qui est public.
2. Mailer : L'activation de compte déclenche l'envoi d'un email via
mailpit, qui n'est pas disponible en CI. Ajout d'une variable
d'environnement MAILER_DSN=null://null pour désactiver l'envoi.
3. Token partagé : Chaque navigateur (chromium, firefox, webkit)
consommait le même token, causant des échecs pour les suivants.
Maintenant chaque navigateur crée son propre token dans beforeAll
avec un email unique (e2e-{browser}@example.com).
4. Nettoyage : Suppression de test-utils.ts et global-setup simplifié
car la création de token est maintenant dans le fichier de test.
L'inscription Classeo se fait via invitation : un admin crée un compte,
l'utilisateur reçoit un lien d'activation par email pour définir son
mot de passe. Ce flow sécurisé évite les inscriptions non autorisées
et garantit que seuls les utilisateurs légitimes accèdent au système.
Points clés de l'implémentation :
- Tokens d'activation à usage unique stockés en cache (Redis/filesystem)
- Validation du consentement parental pour les mineurs < 15 ans (RGPD)
- L'échec d'activation ne consume pas le token (retry possible)
- Users dans un cache séparé sans TTL (pas d'expiration)
- Hot reload en dev (FrankenPHP sans mode worker)
Story: 1.3 - Inscription et activation de compte
Une application SaaS éducative nécessite une séparation stricte des données
entre établissements scolaires. L'architecture multi-tenant par sous-domaine
(ecole-alpha.classeo.local) permet cette isolation tout en utilisant une
base de code unique.
Le choix d'une résolution basée sur les sous-domaines plutôt que sur des
headers ou tokens facilite le routage au niveau infrastructure (reverse proxy)
et offre une UX plus naturelle où chaque école accède à "son" URL dédiée.
Configure l'environnement de développement complet avec Docker Compose,
structure DDD 4 Bounded Contexts, et pipeline CI/CD GitHub Actions.
Corrections compatibilité CI:
- Symfony 8 nécessite monolog-bundle ^4.0 (la v3.x ne supporte que jusqu'à Symfony 7)
- ESLint v9 nécessite flat config (eslint.config.js) - le format .eslintrc.cjs est obsolète