Les listes admin (utilisateurs, classes, matières, affectations) chargeaient
toutes les données d'un coup, ce qui dégradait l'expérience avec un volume
croissant. La pagination côté serveur existait dans la config API Platform
mais aucun Provider ne l'exploitait.
Cette implémentation ajoute la pagination serveur (30 items/page, max 100)
avec recherche textuelle sur toutes les sections, des composants frontend
réutilisables (Pagination + SearchInput avec debounce), et la synchronisation
URL pour le partage de liens filtrés.
Les Query valident leurs paramètres (clamp page/limit, trim search) pour
éviter les abus. Les affectations utilisent des lookup maps pour résoudre
les noms sans N+1 queries. Les pages admin gèrent les race conditions
via AbortController.
Permet aux administrateurs d'associer un enseignant à une classe pour une
matière donnée au sein d'une année scolaire. Cette brique est nécessaire
pour construire les emplois du temps et les carnets de notes par la suite.
Le modèle impose l'unicité du triplet enseignant × classe × matière par
année scolaire, avec réactivation automatique d'une affectation retirée
plutôt que duplication. L'isolation multi-tenant est garantie au niveau
du repository (findById/get filtrent par tenant_id).
Les helpers loginAs* utilisaient un pattern séquentiel (click → wait)
qui crée une race condition : la navigation peut se terminer avant que
le listener soit en place. Firefox sur CI est particulièrement sensible.
Le fix remplace ce pattern par Promise.all([waitForURL, click]) dans
les 14 fichiers E2E concernés, alignant le code sur le pattern robuste
déjà utilisé dans login.spec.ts.
L'ensemble du frontend utilisait un mix de @media (max-width) desktop-first
et @media (min-width) mobile-first, en contradiction avec la spec UX qui
impose une stratégie mobile-first radicale (78% des parents/élèves sur mobile).
Cette conversion uniformise les 8 fichiers restants vers @media (min-width)
avec les breakpoints de la spec UX (sm: 640px, md: 768px) pour garantir
une expérience progressive enhancement cohérente.
Les parents doivent pouvoir suivre la scolarité de leurs enfants (notes,
emploi du temps, devoirs). Cela nécessite un lien formalisé entre le
compte parent et le compte élève, géré par les administrateurs.
Le lien est établi soit manuellement via l'interface d'administration,
soit automatiquement lors de l'activation du compte parent lorsque
l'invitation inclut un élève cible. Ce lien conditionne l'accès aux
données scolaires de l'enfant (autorisations vérifiées par un voter
dédié).
Les utilisateurs Classeo étaient limités à un seul rôle, alors que
dans la réalité scolaire un directeur peut aussi être enseignant,
ou un parent peut avoir un rôle vie scolaire. Cette limitation
obligeait à créer des comptes distincts par fonction.
Le modèle User supporte désormais plusieurs rôles simultanés avec
basculement via le header. L'admin peut attribuer/retirer des rôles
depuis l'interface de gestion, avec des garde-fous : pas d'auto-
destitution, pas d'escalade de privilèges (seul SUPER_ADMIN peut
attribuer SUPER_ADMIN), vérification du statut actif pour le
switch de rôle, et TTL explicite sur le cache de rôle actif.
Les événements métier (emails d'invitation, reset password, activation)
bloquaient la réponse API en étant traités de manière synchrone. Ce commit
route ces événements vers un transport AMQP asynchrone avec un worker
dédié, garantissant des réponses API rapides et une gestion robuste des
échecs.
Le retry utilise une stratégie Fibonacci (1s, 1s, 2s, 3s, 5s, 8s, 13s)
qui offre un bon compromis entre réactivité et protection des services
externes. Les messages qui épuisent leurs tentatives arrivent dans une
dead-letter queue Doctrine avec alerte email à l'admin.
La commande console CreateTestActivationTokenCommand détecte désormais
les comptes déjà actifs et génère un token de réinitialisation de mot
de passe au lieu d'un token d'activation, évitant une erreur bloquante
lors de la ré-invitation par un admin.
Permet aux administrateurs d'un établissement de gérer le cycle de vie
des comptes utilisateurs : inviter de nouveaux membres, bloquer/débloquer
des comptes actifs, et renvoyer des invitations en attente.
Chaque mutation vérifie l'appartenance au tenant courant pour empêcher
les accès cross-tenant. Le blocage est restreint aux comptes actifs
uniquement et un administrateur ne peut pas bloquer son propre compte.
Les comptes suspendus reçoivent une erreur 403 spécifique au login
(sans déclencher l'escalade du rate limiting) et les tentatives sont
tracées dans les métriques Prometheus.
Les établissements scolaires utilisent des systèmes d'évaluation variés
(notes /20, /10, lettres, compétences, sans notes). Jusqu'ici l'application
imposait implicitement le mode notes /20, ce qui ne correspondait pas
à la réalité pédagogique de nombreuses écoles.
Cette configuration permet à chaque établissement de choisir son mode
de notation par année scolaire, avec verrouillage automatique dès que
des notes ont été saisies pour éviter les incohérences. Le Score Sérénité
adapte ses pondérations selon le mode choisi (les compétences sont
converties via un mapping, le mode sans notes exclut la composante notes).
L'administration d'un établissement nécessite de découper l'année
scolaire en trimestres ou semestres avant de pouvoir saisir les notes
et générer les bulletins.
Ce module permet de configurer les périodes par année scolaire
(current/previous/next résolus en UUID v5 déterministes), de modifier
les dates individuelles avec validation anti-chevauchement, et de
consulter la période en cours avec le décompte des jours restants.
Les dates par défaut de février s'adaptent aux années bissextiles.
Le repository utilise UPSERT transactionnel pour garantir l'intégrité
lors du changement de mode (trimestres ↔ semestres). Les domain events
de Subject sont étendus pour couvrir toutes les mutations (code,
couleur, description) en plus du renommage.
Les établissements ont besoin de définir leur référentiel de matières
pour pouvoir ensuite les associer aux enseignants et aux classes.
Cette fonctionnalité permet aux administrateurs de créer, modifier et
archiver les matières avec leurs propriétés (nom, code court, couleur).
L'architecture suit le pattern DDD avec des Value Objects utilisant
les property hooks PHP 8.5 pour garantir l'immutabilité et la validation.
L'isolation multi-tenant est assurée par vérification dans les handlers.
Permet aux administrateurs de créer, modifier et supprimer des classes
pour organiser les élèves par niveau. L'archivage soft-delete préserve
l'historique tout en masquant les classes obsolètes.
Inclut la validation des noms (2-50 caractères), les niveaux scolaires
du CP à la Terminale, et les contrôles d'accès par rôle.
Permet aux parents de visualiser une démo du Score Sérénité dès leur
première connexion, avant même que les données réelles soient disponibles.
Les autres rôles (enseignant, élève, admin) ont également leur dashboard
adapté avec des sections placeholder.
La landing page redirige automatiquement vers /dashboard si l'utilisateur
est déjà authentifié, offrant un accès direct au tableau de bord.
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