diff --git a/apps/frontend/src/app/features/auth/reset-password/reset-password.html b/apps/frontend/src/app/features/auth/reset-password/reset-password.html index 57d202e..acb0090 100644 --- a/apps/frontend/src/app/features/auth/reset-password/reset-password.html +++ b/apps/frontend/src/app/features/auth/reset-password/reset-password.html @@ -12,7 +12,7 @@ formControlName="new_password" autocomplete="new-password" /> - {{ passwordHint }} + @if (errorMessage()) {

{{ errorMessage() }}

diff --git a/apps/frontend/src/app/features/auth/reset-password/reset-password.ts b/apps/frontend/src/app/features/auth/reset-password/reset-password.ts index 3fa56fa..b70975e 100644 --- a/apps/frontend/src/app/features/auth/reset-password/reset-password.ts +++ b/apps/frontend/src/app/features/auth/reset-password/reset-password.ts @@ -1,15 +1,17 @@ import { Component, OnInit, inject, signal } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; import { AuthService } from '../../../core/services/auth.service'; import { passwordValidators, PASSWORD_HINT } from '../../../shared/validators/password.validator'; +import { PasswordRequirementsChecklist } from '../../../shared/components/password-requirements/password-requirements'; import { MOTIF_LIEN_RESET_INVALIDE } from '../../../shared/models/auth-redirect-reason'; @Component({ selector: 'app-reset-password', standalone: true, - imports: [ReactiveFormsModule, RouterLink], + imports: [ReactiveFormsModule, RouterLink, PasswordRequirementsChecklist], templateUrl: './reset-password.html', styleUrl: './reset-password.scss', }) @@ -30,6 +32,8 @@ export class ResetPassword implements OnInit { new_password: ['', passwordValidators], }); + password = toSignal(this.form.controls.new_password.valueChanges, { initialValue: '' }); + ngOnInit(): void { if (!this.hasToken) { this.redirigeVersLoginLienInvalide(); diff --git a/apps/frontend/src/app/shared/components/password-requirements/password-requirements.html b/apps/frontend/src/app/shared/components/password-requirements/password-requirements.html new file mode 100644 index 0000000..bc7ed54 --- /dev/null +++ b/apps/frontend/src/app/shared/components/password-requirements/password-requirements.html @@ -0,0 +1,8 @@ + diff --git a/apps/frontend/src/app/shared/components/password-requirements/password-requirements.scss b/apps/frontend/src/app/shared/components/password-requirements/password-requirements.scss new file mode 100644 index 0000000..b5cb32b --- /dev/null +++ b/apps/frontend/src/app/shared/components/password-requirements/password-requirements.scss @@ -0,0 +1,29 @@ +:host { + display: block; +} + +.password-requirements { + list-style: none; + margin: 0.25rem 0 0; + padding: 0; + font-size: 0.8rem; + line-height: 1.5; + + li { + display: flex; + align-items: center; + gap: 0.4rem; + } + + .password-requirements-icon { + font-weight: 700; + } + + .unmet { + color: #9ca3af; + } + + .met { + color: #16a34a; + } +} diff --git a/apps/frontend/src/app/shared/components/password-requirements/password-requirements.spec.ts b/apps/frontend/src/app/shared/components/password-requirements/password-requirements.spec.ts new file mode 100644 index 0000000..23c4bbc --- /dev/null +++ b/apps/frontend/src/app/shared/components/password-requirements/password-requirements.spec.ts @@ -0,0 +1,39 @@ +import { TestBed } from '@angular/core/testing'; +import { PasswordRequirementsChecklist } from './password-requirements'; + +describe('PasswordRequirementsChecklist', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PasswordRequirementsChecklist], + }).compileComponents(); + }); + + it('ne coche aucune règle pour un mot de passe vide', () => { + const fixture = TestBed.createComponent(PasswordRequirementsChecklist); + fixture.componentRef.setInput('password', ''); + fixture.detectChanges(); + + expect(fixture.componentInstance.requirements().every((r) => !r.met)).toBe(true); + }); + + it('ne coche que les règles satisfaites pour un mot de passe partiel', () => { + const fixture = TestBed.createComponent(PasswordRequirementsChecklist); + fixture.componentRef.setInput('password', 'abcdefgh'); + fixture.detectChanges(); + + const parLabel = new Map(fixture.componentInstance.requirements().map((r) => [r.label, r.met])); + expect(parLabel.get('8 caractères minimum')).toBe(true); + expect(parLabel.get('1 minuscule')).toBe(true); + expect(parLabel.get('1 majuscule')).toBe(false); + expect(parLabel.get('1 chiffre')).toBe(false); + expect(parLabel.get('1 caractère spécial')).toBe(false); + }); + + it('coche toutes les règles pour un mot de passe conforme', () => { + const fixture = TestBed.createComponent(PasswordRequirementsChecklist); + fixture.componentRef.setInput('password', 'Un-nouveau-mot-de-passe1!'); + fixture.detectChanges(); + + expect(fixture.componentInstance.requirements().every((r) => r.met)).toBe(true); + }); +}); diff --git a/apps/frontend/src/app/shared/components/password-requirements/password-requirements.ts b/apps/frontend/src/app/shared/components/password-requirements/password-requirements.ts new file mode 100644 index 0000000..36a4413 --- /dev/null +++ b/apps/frontend/src/app/shared/components/password-requirements/password-requirements.ts @@ -0,0 +1,19 @@ +import { Component, computed, input } from '@angular/core'; +import { PASSWORD_REQUIREMENTS } from '../../validators/password.validator'; + +@Component({ + selector: 'app-password-requirements', + standalone: true, + templateUrl: './password-requirements.html', + styleUrl: './password-requirements.scss', +}) +export class PasswordRequirementsChecklist { + password = input(''); + + requirements = computed(() => + PASSWORD_REQUIREMENTS.map((requirement) => ({ + label: requirement.label, + met: requirement.test(this.password()), + })), + ); +} diff --git a/apps/frontend/src/app/shared/validators/password.validator.ts b/apps/frontend/src/app/shared/validators/password.validator.ts index 9f95863..78d4066 100644 --- a/apps/frontend/src/app/shared/validators/password.validator.ts +++ b/apps/frontend/src/app/shared/validators/password.validator.ts @@ -23,3 +23,18 @@ export const passwordValidators = [ Validators.maxLength(PASSWORD_MAX_LENGTH), Validators.pattern(PASSWORD_PATTERN), ]; + +export interface PasswordRequirement { + label: string; + test: (value: string) => boolean; +} + +const SPECIAL_REGEX = new RegExp(`[${SPECIAL_CHARACTERS}]`); + +export const PASSWORD_REQUIREMENTS: PasswordRequirement[] = [ + { label: `${PASSWORD_MIN_LENGTH} caractères minimum`, test: (v) => v.length >= PASSWORD_MIN_LENGTH }, + { label: '1 majuscule', test: (v) => /[A-ZÀ-ÖØ-Þ]/.test(v) }, + { label: '1 minuscule', test: (v) => /[a-zà-öø-þ]/.test(v) }, + { label: '1 chiffre', test: (v) => /[0-9]/.test(v) }, + { label: '1 caractère spécial', test: (v) => SPECIAL_REGEX.test(v) }, +];