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