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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user