Lorsqu'un admin créait un élève de moins de 15 ans avec une date de
naissance, le compte ne pouvait pas être activé car le consentement
parental RGPD n'avait jamais été enregistré — aucun mécanisme ne le
permettait dans le parcours admin.
Ajout d'une case « Consentement parental obtenu » dans le formulaire
de création d'élève, affichée conditionnellement quand la date de
naissance indique un âge < 15 ans. L'admin confirme que l'établissement
a recueilli le consentement, qui est alors enregistré côté backend
lors de la création du compte.
Les listes paginées (utilisateurs, classes, matières, affectations,
invitations parents, droits à l'image) effectuaient des requêtes SQL
complètes à chaque chargement de page, sans aucun cache. Sur les
établissements avec plusieurs centaines d'enregistrements, cela causait
des temps de réponse perceptibles et une charge inutile sur PostgreSQL.
Cette refactorisation introduit un cache tag-aware (Redis en prod,
filesystem en dev) avec invalidation événementielle, et extrait les
requêtes de lecture dans des ports Application / implémentations DBAL
conformes à l'architecture hexagonale. Un middleware Messenger garantit
l'invalidation synchrone du cache même pour les événements routés en
asynchrone (envoi d'emails), évitant ainsi toute donnée périmée côté UI.
Les administrateurs ont besoin d'un moyen simple pour inviter les parents
à rejoindre la plateforme. Cette fonctionnalité permet de générer des codes
d'invitation uniques (8 caractères alphanumériques) avec une validité de
48h, de les envoyer par email, et de les activer via une page publique
dédiée qui crée automatiquement le compte parent.
L'interface d'administration offre l'envoi unitaire et en masse, le renvoi,
le filtrage par statut, ainsi que la visualisation de l'état de chaque
invitation (en attente, activée, expirée).
L'établissement a besoin d'importer en masse ses enseignants depuis les
exports des logiciels de vie scolaire (Pronote, EDT, etc.), comme c'est
déjà possible pour les élèves. Le wizard en 4 étapes (upload → mapping
→ aperçu → import) réutilise l'architecture de l'import élèves tout en
ajoutant la gestion des matières et des classes enseignées.
Corrections de la review #2 intégrées :
- La commande ImportTeachersCommand est routée en async via Messenger
pour ne pas bloquer la requête HTTP sur les gros fichiers.
- Le handler est protégé par un try/catch Throwable pour marquer le
batch en échec si une erreur inattendue survient, évitant qu'il
reste bloqué en statut "processing".
- Les domain events (UtilisateurInvite) sont dispatchés sur l'event
bus après chaque création d'utilisateur, déclenchant l'envoi des
emails d'invitation.
- L'option "mettre à jour les enseignants existants" (AC5) permet de
choisir entre ignorer ou mettre à jour nom/prénom et ajouter les
affectations manquantes pour les doublons détectés par email.
L'import manuel élève par élève est fastidieux pour les établissements
qui gèrent des centaines d'élèves. Un wizard d'import en 4 étapes
(upload → mapping → preview → confirmation) permet de traiter un
fichier complet en une seule opération, avec détection automatique
du format (Pronote, École Directe) et validation avant import.
L'import est traité de manière asynchrone via Messenger pour ne pas
bloquer l'interface, avec suivi de progression en temps réel et
réutilisation des mappings entre imports successifs.
Les administrateurs et secrétaires avaient besoin de pouvoir inscrire un
élève en cours d'année sans passer par un import CSV. Cette fonctionnalité
pose aussi les fondations du modèle élève↔classe (ClassAssignment) qui
sera réutilisé par l'import CSV en masse (Story 3.1).
L'email est désormais optionnel pour les élèves : si fourni, une invitation
est envoyée (User::inviter) ; sinon l'élève est créé avec le statut
INSCRIT sans accès compte (User::inscrire). La création de l'utilisateur
et l'affectation à la classe sont atomiques (transaction DBAL).
Côté frontend, la page /admin/students offre liste paginée, recherche,
filtrage par classe, création via modale (avec détection de doublons
côté serveur), et changement de classe avec optimistic update.
Les administrateurs peuvent désormais configurer l'identité visuelle
de leur établissement : upload d'un logo (PNG/JPG, redimensionné
automatiquement via Imagick) et choix d'une couleur principale
appliquée aux boutons et à la navigation.
La couleur est validée côté client et serveur pour garantir la
conformité WCAG AA (contraste ≥ 4.5:1 sur fond blanc). Les
personnalisations sont injectées dynamiquement via CSS variables
et visibles immédiatement après sauvegarde.
Les administrateurs et enseignants ont besoin de consulter et gérer
les autorisations de droit à l'image des élèves pour respecter
la réglementation lors de publications contenant des photos (FR82).
Cette fonctionnalité ajoute une page dédiée avec liste filtrable
par statut, modification individuelle via dropdown, export CSV
avec BOM UTF-8 pour Excel, et préparation du système d'avertissement
avant publication (query/handler prêts, intégration à faire quand
le module publication existera).
Le filtrage par classe (AC2) est bloqué en attente d'une table
d'affectation élève↔classe qui n'existe pas encore.
Les administrateurs d'établissement avaient besoin de gérer le calendrier
scolaire (FR80) pour que l'EDT et les devoirs respectent automatiquement
les jours non travaillés. Sans cette configuration centralisée, chaque
module devait gérer indépendamment les contraintes de dates.
Le calendrier s'appuie sur l'API data.education.gouv.fr pour importer
les vacances officielles par zone (A/B/C) et calcule les 11 jours fériés
français (dont les fêtes mobiles liées à Pâques). Les enseignants sont
notifiés par email lors de l'ajout d'une journée pédagogique. Un query
IsSchoolDay et une validation des dates d'échéance de devoirs permettent
aux autres modules de s'intégrer sans couplage direct.
Le super admin (table super_admins, master DB) ne pouvait pas se connecter
via /api/login car ce firewall n'utilisait que le provider tenant. De même,
le JWT n'était pas enrichi pour les super admins, l'endpoint /api/me/roles
les rejetait, et le frontend redirigeait systématiquement vers /dashboard.
Un chain provider (super_admin + tenant) résout l'authentification,
le JwtPayloadEnricher et MyRolesProvider gèrent désormais les deux types
d'utilisateurs, et le frontend redirige selon le rôle après login.
Couverture des processors (RefreshToken, RequestPasswordReset,
ResetPassword, SwitchRole, UpdateUserRoles), des query handlers
(HasGradesInPeriod, HasStudentsInClass), des messaging handlers
(SendActivationConfirmation, SendPasswordResetEmail), et côté
frontend des modules auth, roles, monitoring, types et E2E tokens.
Les utilisateurs étaient stockés uniquement dans Redis (CacheUserRepository),
ce qui exposait à une perte totale des comptes en cas de restart Redis,
FLUSHDB ou perte du volume Docker. Les tables student_guardians et
teacher_assignments référençaient des user IDs sans FK réelle.
PostgreSQL devient la source de vérité via DoctrineUserRepository (DBAL,
upsert ON CONFLICT). CachedUserRepository décore l'interface existante
avec le pattern cache-aside : lectures Redis d'abord → miss → PostgreSQL
→ populate Redis ; écritures PostgreSQL d'abord → mise à jour Redis.
Si Redis est indisponible, l'application continue via PostgreSQL seul.
Une commande de migration (app:migrate-users-to-postgres) permet de copier
les données Redis existantes vers PostgreSQL de manière idempotente.
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 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.
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
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
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