Files
SchotzIAD/frontend/src/app/features/auth/reset-password/reset-password.component.ts
T
MadJax ee47d04b90 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.
2026-09-22 09:54:25 +02:00

117 lines
4.2 KiB
TypeScript

import { CommonModule } from '@angular/common';
import { Component, inject, signal } from '@angular/core';
import {
AbstractControl,
FormBuilder,
FormGroup,
ReactiveFormsModule,
ValidationErrors,
Validators
} from '@angular/forms';
import { HttpErrorResponse } from '@angular/common/http';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { AuthService } from '../../../core/services/auth.service';
import { PasswordInputComponent } from '../../../shared/components/password-input/password-input.component';
import { NotificationService } from '../../../core/services/notification.service';
import { ResetPasswordDto } from '../../../core/models';
/**
* Validateur de groupe : vérifie l'égalité entre `nouveauMotDePasse` et
* `confirmation`. Pose l'erreur `motsDePasseDifferents` sur le groupe lorsque
* les deux valeurs diffèrent.
*/
function egaliteMotsDePasse(control: AbstractControl): ValidationErrors | null {
const nouveau = control.get('nouveauMotDePasse')?.value ?? '';
const confirmation = control.get('confirmation')?.value ?? '';
return nouveau === confirmation ? null : { motsDePasseDifferents: true };
}
/**
* Écran de réinitialisation du mot de passe (`/reset-password`).
*
* Lit le paramètre de requête `token` depuis l'URL (via `ActivatedRoute`) et le
* conserve dans un signal. Si le token est absent, un message invite à
* redemander un lien. Le formulaire saisit le nouveau mot de passe (>= 8
* caractères) et sa confirmation (validateur d'égalité). Sur succès, une
* notification est émise puis le composant navigue vers `/login` (Exigence 7.6).
*
* Contrainte zoneless : tout état async est exposé via des signals
* (`soumissionEnCours`, `erreur`, `token`).
*
* _Requirements: 7.6_
*/
@Component({
selector: 'app-reset-password',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, RouterLink, PasswordInputComponent],
templateUrl: './reset-password.component.html',
styleUrl: './reset-password.component.scss'
})
export class ResetPasswordComponent {
private readonly fb = inject(FormBuilder);
private readonly authService = inject(AuthService);
private readonly router = inject(Router);
private readonly route = inject(ActivatedRoute);
private readonly notification = inject(NotificationService);
/** Token de réinitialisation lu depuis l'URL (`?token=...`), ou null si absent. */
readonly token = signal<string | null>(
this.route.snapshot.queryParamMap.get('token')
);
/** Vrai pendant l'appel de réinitialisation (désactive le bouton). */
readonly soumissionEnCours = signal(false);
/** Message d'erreur courant, ou null si aucune erreur. */
readonly erreur = signal<string | null>(null);
readonly form: FormGroup = this.fb.group(
{
nouveauMotDePasse: ['', [Validators.required, Validators.minLength(8)]],
confirmation: ['', [Validators.required]]
},
{ validators: egaliteMotsDePasse }
);
/** Tente la réinitialisation avec le nouveau mot de passe saisi. */
reinitialiser(): void {
const token = this.token();
if (this.form.invalid || token === null || this.soumissionEnCours()) {
this.form.markAllAsTouched();
return;
}
const valeur = this.form.getRawValue();
const dto: ResetPasswordDto = {
token,
nouveauMotDePasse: valeur.nouveauMotDePasse ?? ''
};
this.erreur.set(null);
this.soumissionEnCours.set(true);
this.authService.resetPassword(dto).subscribe({
next: () => {
this.soumissionEnCours.set(false);
this.notification.success('Votre mot de passe a été réinitialisé.');
this.router.navigate(['/login']);
},
error: (err: HttpErrorResponse) => {
this.soumissionEnCours.set(false);
let message: string;
if (err.status === 400) {
// ProblemDetails : le backend fournit un message précis
// (lien expiré / lien invalide) via `detail` le cas échéant.
message =
err.error?.detail ??
'Le lien de réinitialisation est invalide ou a expiré.';
} else {
message = 'La réinitialisation a échoué. Veuillez réessayer.';
}
this.erreur.set(message);
this.notification.error(message);
}
});
}
}