feat(frontend): checklist de complexite du mot de passe sur reset-password

Remplace l'indice statique sous le champ nouveau mot de passe par une
checklist qui coche chaque regle (longueur, majuscule, minuscule,
chiffre, caractere special) au fur et a mesure de la saisie. Les regles
individuelles (PASSWORD_REQUIREMENTS) sont exposees depuis le meme
validateur que PASSWORD_PATTERN pour rester la seule source de verite.
This commit is contained in:
Johan LEROY
2026-09-17 14:57:05 +02:00
parent 7674955637
commit e381e0de09
7 changed files with 116 additions and 2 deletions
@@ -0,0 +1,8 @@
<ul class="password-requirements">
@for (requirement of requirements(); track requirement.label) {
<li [class.met]="requirement.met" [class.unmet]="!requirement.met">
<span class="password-requirements-icon">{{ requirement.met ? '✓' : '○' }}</span>
{{ requirement.label }}
</li>
}
</ul>
@@ -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;
}
}
@@ -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);
});
});
@@ -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()),
})),
);
}
@@ -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) },
];