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 @@
+
+ @for (requirement of requirements(); track requirement.label) {
+ -
+ {{ requirement.met ? '✓' : '○' }}
+ {{ requirement.label }}
+
+ }
+
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) },
+];