Navigation utilisateur : avatar, paramétrage, maintenance, mot de passe

Réorganisation complète de la navigation suite au retour d'usage, plus
changement de mot de passe et affichage/masquage des mots de passe saisis.

Backend :
- Nouvel endpoint POST /api/auth/change-password (ChangePasswordDto,
  MotDePasseActuelInvalideException, validateur, AuthService.ChangePasswordAsync).
  Exige l'ancien mot de passe, distinct de la réinitialisation par e-mail.
- Endpoint placé dans SessionController (protégé par l'autorisation globale) et
  NON dans AuthController : AuthController porte [AllowAnonymous] au niveau
  classe, ce qui rend inopérant un [Authorize] posé sur une seule action (bug de
  sécurité détecté par test manuel — l'endpoint était accessible sans jeton —
  puis corrigé par ce déplacement).

Frontend :
- Page « Compte utilisateur » (ProfilComponent renommé) : section Maintenance
  extraite vers MaintenanceComponent (route /maintenance) ; nouvelle section de
  changement de mot de passe (ancien + nouveau + confirmation).
- ParametrageComponent (route /parametrage) : menu latéral Compte / Véhicules /
  Barème + contenu en routes enfants, réutilisant les composants existants.
  Compte et Véhicules en authGuard seul ; Barème garde profilConfigureGuard.
- Barre de navigation : Véhicules et Barème retirés (rejoignent Paramétrage) ;
  logo passé en format horizontal (icône + texte).
- Avatar utilisateur (initiales) à droite de la barre : sous-menu au survol
  (identité, Paramétrage, Maintenance, Se déconnecter). Menu « Specs » converti
  en dropdown survolable (Exigences / Conception / Tâches).
- Dropdowns : panneau collé au bouton (plus d'espace mort) pour que le survol
  reste continu jusqu'au panneau — un espace créait une fermeture prématurée.
- Barre masquée sur les pages publiques (login/register/forgot/reset-password)
  même avec un jeton résiduel expiré/invalide (détection via Router events, pas
  seulement la présence du jeton).
- PasswordInputComponent (shared) : bouton œil afficher/masquer, ControlValueAccessor,
  style aligné sur les champs texte classiques. Appliqué aux 8 champs mot de
  passe de l'application (login, register, reset-password, compte utilisateur).

Specs : requirements.md (exigences 20, 21 + glossaire) et design.md (endpoints,
DTO, composants) mis à jour. 54 tests backend au vert.
This commit is contained in:
2026-09-22 09:54:25 +02:00
parent 058481c192
commit ee47d04b90
36 changed files with 1232 additions and 118 deletions
+42 -6
View File
@@ -34,6 +34,8 @@
| 22 | Trajet aller/retour | Case à cocher A/R doublant la distance comptée (Distance_Effective) sans ligne supplémentaire ; badge A/R en liste et mention « (A/R) » sur la carte |
| 23 | Barème multi-tranches & recopie | Tranches paramétrables par puissance fiscale (dernière tranche illimitée), coefficient sur 3 décimales, bouton « Recopier le dernier barème connu » |
| 24 | Consultation des specs | Lien « Specs » vers une page dédiée (menu latéral Exigences / Conception / Tâches) affichant les documents Markdown rendus (dont les diagrammes Mermaid), en lecture seule et derrière l'authentification |
| 25 | Changement de mot de passe | Page Compte utilisateur : formulaire dédié (mot de passe actuel + nouveau + confirmation) ; endpoint protégé distinct de la réinitialisation par e-mail |
| 26 | Navigation utilisateur | Avatar (initiales) à droite de la barre, sous-menu au survol (Prénom Nom, Paramétrage, Maintenance, Se déconnecter) ; Page_Parametrage (Compte / Véhicules / Barème) et Page_Maintenance dédiées ; barre masquée sur les pages publiques ; contrôle afficher/masquer sur tous les champs mot de passe |
### Contraintes techniques
@@ -271,12 +273,24 @@ src/
- Infobulles au clic (popup) et au survol (tooltip) : commentaire en tête (si présent), adresse départ, adresse arrivée, distance suivie de « (A/R) » pour un aller/retour ; largeur bornée avec retour à la ligne pour ne pas déborder de la carte
- Fond de carte : OpenStreetMap (tile layer `https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`)
**`ProfilComponent`**
- Champs : nom (1–100 car.), prénom (1–100 car.), matricule (max 8 car., limite de saisie stricte), adresse de domicile (1–255 car.)
- Bouton "Enregistrer"
- Message d'avertissement si le géocodage de l'adresse a échoué
**`ProfilComponent`** (page « Compte utilisateur » — Exigences 10, 20)
- Titre affiché : « Compte utilisateur » (aligné avec les intitulés « Véhicules » et « Barème » du menu de la Page_Parametrage)
- Formulaire d'identité : nom (1–100 car.), prénom (1–100 car.), matricule (max 8 car., limite de saisie stricte), adresse de domicile (1–255 car.)
- Bouton "Enregistrer" ; message d'avertissement si le géocodage de l'adresse a échoué
- **Formulaire de changement de mot de passe** (distinct, Exigence 20) : mot de passe actuel + nouveau + confirmation (validateur de groupe d'égalité, même pattern que `RegisterComponent`) ; `POST /api/auth/change-password` via `AuthService.changePassword` ; une erreur 400 affiche « Le mot de passe actuel est incorrect » sur le formulaire
- La section « Maintenance » (recalcul des distances) a été extraite vers `MaintenanceComponent` (route `/maintenance`, accessible depuis l'Avatar_Utilisateur)
- Route : `/parametrage/compte` (au sein de la Page_Parametrage)
**`VehiculesComponent`**
**`MaintenanceComponent`** (route `/maintenance` — Exigence 21)
- Reprend l'action de recalcul des distances (`POST /api/trajets/recalculer-distances`) extraite de l'ancienne page Profil
- Accessible depuis le sous-menu de l'Avatar_Utilisateur, `authGuard` seul (pas de dépendance au profil configuré)
**`ParametrageComponent`** (route `/parametrage` — Exigence 21)
- Layout menu latéral + contenu, sur le même modèle que `SpecsComponent` : colonne gauche (« Compte », « Véhicules », « Barème »), colonne droite = `<router-outlet>` avec les routes enfants `parametrage/compte`, `parametrage/vehicules`, `parametrage/bareme`
- Réutilise `ProfilComponent`, `VehiculesComponent`, `BaremeComponent` sans duplication ; seul le point d'entrée change
- Gardes : `compte` et `vehicules` sur `authGuard` seul ; `bareme` ajoute `profilConfigureGuard` (Exigence 21.6)
**`VehiculesComponent`** (route `/parametrage/vehicules`)
- Liste de l'historique des véhicules (plaque, puissance fiscale, date d'effet), triée par date d'effet décroissante
- Formulaire d'ajout d'un véhicule : plaque, puissance fiscale, date d'effet (<= date du jour, sélecteur bloquant les dates futures)
- À l'enregistrement, si la réponse `VehiculeCreeDto.ReaffectationProposee` est true, ouvre `ReaffectationDialogComponent`
@@ -358,7 +372,7 @@ src/
> **Décision — stockage du JWT dans `localStorage`** : retenu pour sa simplicité, malgré le risque XSS, acceptable pour une application personnelle mono-utilisateur. Le cookie HttpOnly a été rejeté pour éviter la complexité de protection CSRF.
**`SpecsComponent`** (documents de spécification — Exigence 19)
- La barre de navigation principale expose un simple lien « Specs » vers `/specs` (redirigé vers le premier document) ; le choix du document se fait dans le **menu latéral** de la page, pas dans la barre.
- La barre de navigation principale expose un menu déroulant « Specs » (survol ou clic, même mécanisme que l'Avatar_Utilisateur) listant directement les 3 documents ; le choix détaillé (disponibilité, etc.) reste géré par le **menu latéral** de la page elle-même.
- Route paramétrée `/specs/:cle` ; **lazy-loadée** (`loadComponent`) pour sortir la librairie Markdown du bundle initial
- Colonne gauche : menu listant les documents (`GET /api/specs`), le document indisponible étant signalé ; colonne droite : contenu rendu
- Rendu : le Markdown (`GET /api/specs/{cle}`) est converti en HTML par **`marked`** (GFM : titres, listes, tableaux, code), puis **assaini par `DOMPurify`** avant injection (`[innerHTML]`) — Exigence 19.7
@@ -372,6 +386,18 @@ src/
- **Gestion d'erreur** : en cas d'échec de récupération de l'indicateur (backend injoignable / erreur réseau), le guard affiche un message d'erreur de connexion avec une action "Réessayer" plutôt que de rediriger arbitrairement, laissant l'utilisateur relancer la vérification
- Déclaré dans `app-routing.module.ts` sur les routes protégées (consultation, saisie, résumés, etc.) ; la route `/onboarding` reste accessible sans garde
**`AppComponent`** (coquille applicative — barre de navigation, Exigence 21)
- Barre de navigation : logo (format horizontal, icône + texte), liens métier (Trajets, Carnet d'adresses, Montants réglés, Résumé mensuel, Résumé annuel), menu déroulant « Specs », puis l'Avatar_Utilisateur poussé à l'extrémité droite (`margin-left: auto`)
- **Menus déroulants (Specs, Avatar)** : s'ouvrent au survol (`mouseenter`) ET restent utilisables au clic (tactile, clavier) ; se ferment au `mouseleave` du bloc conteneur ou au clic extérieur (`@HostListener('document:click')`)
- **Piège évité** : le panneau déroulant ne doit PAS être séparé du bouton par un espace (`top: calc(100% + Xpx)`), sous peine de déclencher un `mouseleave` prématuré lorsque le curseur traverse cet espace mort en descendant vers le panneau. Le panneau touche directement le bouton (`top: 100%`) et l'espacement visuel est reporté en `padding-top` À L'INTÉRIEUR du panneau (zone `__panneau-contenu` séparée pour le fond/l'ombre), de sorte que la zone de survol reste continue.
- **Affichage conditionnel** : la barre de navigation (avatar inclus) est masquée non seulement en l'absence de jeton, mais aussi sur les pages PUBLIQUES (`/login`, `/register`, `/forgot-password`, `/reset-password`), détectées via les événements du `Router` (`NavigationEnd`). Ce deuxième critère est nécessaire car un jeton résiduel expiré ou invalide peut rester en `localStorage` sans qu'aucun appel HTTP protégé ne le purge sur ces pages (aucune requête vers un endpoint protégé n'y est faite) ; se fier uniquement à la présence du jeton ferait réapparaître la barre à tort.
- **Avatar** : initiales calculées à partir du Profil (`ProfilService.getProfil()`, chargé via un `effect()` déclenché dès que l'utilisateur authentifié quitte une page publique) ; sous-menu = identité (Prénom Nom), « Paramétrage » (`/parametrage`), « Maintenance » (`/maintenance`), « Se déconnecter » (`AuthService.logout()`)
**`PasswordInputComponent`** (`shared/components/password-input/` — Exigence 21.8)
- Champ de mot de passe réutilisable avec bouton « œil » (SVG inline, pas d'icône externe) pour basculer l'affichage entre masqué et visible
- Implémente `ControlValueAccessor` : s'utilise comme un `<input>` classique avec `formControlName` (remplace directement `<input type="password">` dans les 4 pages d'authentification et la page Compte utilisateur, 8 occurrences au total)
- **Style aligné sur les champs texte classiques** (`padding: 0.5rem`, `border: 1px solid #bbb`, `border-radius: 6px`, `font-size: 0.95rem`, variante rouge si `aria-invalid`) : porté par le composant lui-même (pas par la page hôte, qui ne peut pas styler l'intérieur d'un composant enfant à travers l'encapsulation de vue Angular) afin que le rendu soit identique à celui d'un champ texte normal quelle que soit la page
#### Services Angular
**`TrajetService`**
@@ -1200,6 +1226,13 @@ public record ResetPasswordDto(
string MotDePasse // nouveau mot de passe, >= 8 caractères
);
// Changement de mot de passe par un utilisateur authentifié (Exigence 20). À la
// différence de ResetPasswordDto (jeton reçu par email), exige l'ancien mot de passe.
public record ChangePasswordDto(
string MotDePasseActuel,
string NouveauMotDePasse // >= 8 caractères
);
// --- Documents de spécification (Exigence 19) ---
// Métadonnées d'un document (élément du sous-menu)
@@ -1246,6 +1279,9 @@ public record SpecContenuDto(
| `GET` | `/api/auth/moi` | Valider la session courante (jeton valide + compte existant) — endpoint **protégé** | — | `200 OK` ; `401` sinon |
| `POST` | `/api/auth/forgot-password` | Demander un lien de réinitialisation (envoi email, sinon lien journalisé) | `ForgotPasswordDto` | `200 OK` |
| `POST` | `/api/auth/reset-password` | Réinitialiser le mot de passe via le jeton reçu | `ResetPasswordDto` | `200 OK` ; `400` si jeton invalide/expiré/déjà utilisé |
| `POST` | `/api/auth/change-password` | Changer le mot de passe d'un utilisateur **authentifié** (exige l'ancien mot de passe) — endpoint **protégé** | `ChangePasswordDto` | `200 OK` ; `400` si mot de passe actuel incorrect ; `401` sans jeton |
> **Placement de `change-password`** : cet endpoint est déclaré dans `SessionController` (route `api/auth`, SANS `[AllowAnonymous]` au niveau classe) et non dans `AuthController` (qui porte `[AllowAnonymous]` au niveau classe). En ASP.NET Core, `[AllowAnonymous]` posé sur une classe s'applique à TOUTES ses actions et ne peut pas être contredit par un `[Authorize]` posé sur une seule action : un premier essai avec `[Authorize]` sur l'action dans `AuthController` a laissé l'endpoint accessible sans jeton (vérifié par test manuel), d'où le déplacement vers `SessionController`, protégé par l'autorisation globale de l'application.
#### Résumés
@@ -63,6 +63,9 @@ L'application est composée d'un frontend Angular/TypeScript et d'un backend C#.
- **Aller_Retour** : Un indicateur booléen d'un Trajet signifiant que le déplacement est effectué dans les deux sens. Lorsqu'il est vrai, la Distance comptée du Trajet (Distance_Effective) est le double de la distance simple mesurée entre le départ et l'arrivée.
- **Distance_Effective** : La distance comptée d'un Trajet dans tous les agrégats (liste, totaux, carte, PDF, barème) : égale à la distance simple, ou à son double lorsque le Trajet est un Aller_Retour.
- **Document_Specification** : Un document de conception du projet consultable en lecture seule dans l'Application : les Exigences (requirements), la Conception technique (design) et le Plan de tâches (tasks), rendus depuis leur source Markdown.
- **Avatar_Utilisateur** : Le contrôle affiché à l'extrémité droite de la barre de navigation, représentant l'utilisateur par ses initiales (1ère lettre du Prénom + 1ère lettre du Nom). Un survol ou un clic ouvre un sous-menu proposant l'identité affichée, « Paramétrage », « Maintenance » et « Se déconnecter ».
- **Page_Parametrage** : La page accessible depuis l'Avatar_Utilisateur, présentant un menu latéral (Compte, Véhicules, Barème) et affichant à sa droite la donnée paramétrable sélectionnée.
- **Page_Maintenance** : La page accessible depuis l'Avatar_Utilisateur regroupant les actions techniques de maintenance de l'Application (ex. recalcul des distances routières).
---
@@ -454,3 +457,34 @@ L'application est composée d'un frontend Angular/TypeScript et d'un backend C#.
7. THE Frontend SHALL assainir le contenu HTML issu du rendu Markdown avant affichage, afin d'éviter l'injection de contenu actif.
8. IF un diagramme Mermaid ne peut pas être rendu (syntaxe non reconnue), THEN THE Frontend SHALL conserver l'affichage du bloc de code brut correspondant sans empêcher le rendu des autres diagrammes ni du reste du document.
---
### Requirement 20: Changement de mot de passe par un utilisateur authentifié
**User Story:** En tant qu'utilisateur authentifié, je veux changer mon mot de passe depuis mon compte, afin de le renouveler sans passer par le parcours de mot de passe oublié.
#### Acceptance Criteria
1. THE Frontend SHALL afficher, sur la page « Compte utilisateur », un formulaire de changement de Mot_De_Passe distinct du formulaire d'identité, comportant le mot de passe actuel, le nouveau Mot_De_Passe et sa confirmation.
2. THE Frontend SHALL exiger que le nouveau Mot_De_Passe comporte au moins 8 caractères et corresponde exactement à sa confirmation avant d'autoriser la soumission.
3. WHEN l'utilisateur soumet le changement de Mot_De_Passe, THE Backend SHALL vérifier que le mot de passe actuel fourni correspond au Compte AVANT de le remplacer ; cette vérification exige un utilisateur authentifié (jeton JWT valide).
4. IF le mot de passe actuel fourni ne correspond pas au Compte, THEN THE Backend SHALL rejeter le changement avec une erreur HTTP 400 explicite, sans modifier le Mot_De_Passe existant.
5. WHEN le mot de passe actuel est correct et le nouveau Mot_De_Passe valide, THE Backend SHALL remplacer le hachage du Mot_De_Passe du Compte et THE Frontend SHALL afficher une confirmation de succès.
6. THE Backend SHALL exposer l'endpoint de changement de mot de passe sur un contrôleur soumis à l'autorisation globale (utilisateur authentifié), et non sur un contrôleur autorisant les accès anonymes, afin qu'un `[Authorize]` posé sur une seule action ne puisse pas être contourné par une déclaration `[AllowAnonymous]` au niveau de la classe.
---
### Requirement 21: Navigation utilisateur (avatar, paramétrage, maintenance)
**User Story:** En tant qu'utilisateur, je veux accéder à mon compte, aux données paramétrables et aux actions de maintenance depuis un menu utilisateur unique, afin de garder la barre de navigation principale légère et centrée sur les fonctionnalités de saisie et de consultation.
#### Acceptance Criteria
1. THE Frontend SHALL afficher un Avatar_Utilisateur à l'extrémité droite de la barre de navigation, représentant l'utilisateur par ses initiales (1ère lettre du Prénom suivie de la 1ère lettre du Nom, en majuscules) ; à défaut d'identité connue, un caractère générique est affiché.
2. WHEN l'utilisateur survole ou clique sur l'Avatar_Utilisateur, THE Frontend SHALL afficher un sous-menu présentant le Prénom et le Nom de l'utilisateur, puis les options « Paramétrage », « Maintenance » et « Se déconnecter ».
3. THE Frontend SHALL fermer le sous-menu de l'Avatar_Utilisateur lorsque le curseur quitte la zone du bouton et du panneau déroulant réunis, sans espace mort entre les deux qui provoquerait une fermeture prématurée lors du déplacement du curseur vers le panneau.
4. WHEN l'utilisateur sélectionne « Paramétrage », THE Frontend SHALL naviguer vers la Page_Parametrage, affichant par défaut la donnée « Compte » ; WHEN l'utilisateur sélectionne « Maintenance », THE Frontend SHALL naviguer vers la Page_Maintenance ; WHEN l'utilisateur sélectionne « Se déconnecter », THE Frontend SHALL déconnecter l'utilisateur et naviguer vers la page de connexion.
5. THE Page_Parametrage SHALL présenter un menu latéral avec les entrées « Compte » (identité, matricule, adresse de domicile), « Véhicules » (historique des véhicules) et « Barème » (barème kilométrique), chacune affichant à droite le contenu correspondant sans rechargement complet de la page.
6. THE Backend SHALL continuer à exiger la configuration du Profil (Exigence 16) pour l'accès à la donnée « Barème » de la Page_Parametrage, mais PAS pour l'accès aux données « Compte » et « Véhicules », afin de ne pas bloquer un utilisateur dont le Profil n'est pas encore configuré.
7. THE Frontend SHALL masquer entièrement la barre de navigation (y compris l'Avatar_Utilisateur) sur les pages non authentifiées (connexion, inscription, mot de passe oublié, réinitialisation), MÊME SI un jeton résiduel expiré ou invalide est encore présent localement, la présence locale d'un jeton ne garantissant pas la validité réelle de la Session sur ces pages.
8. THE Frontend SHALL proposer, sur chaque champ de saisie d'un Mot_De_Passe (connexion, inscription, réinitialisation, changement de mot de passe), un contrôle permettant de basculer l'affichage entre masqué et visible, afin que l'utilisateur puisse vérifier la saisie ; ce contrôle SHALL présenter un rendu visuel identique à celui des autres champs de saisie du formulaire.