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:
@@ -12,7 +12,7 @@
|
|||||||
formControlName="new_password"
|
formControlName="new_password"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
/>
|
/>
|
||||||
<span class="auth-hint">{{ passwordHint }}</span>
|
<app-password-requirements [password]="password()" />
|
||||||
|
|
||||||
@if (errorMessage()) {
|
@if (errorMessage()) {
|
||||||
<p class="auth-error">{{ errorMessage() }}</p>
|
<p class="auth-error">{{ errorMessage() }}</p>
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { Component, OnInit, inject, signal } from '@angular/core';
|
import { Component, OnInit, inject, signal } from '@angular/core';
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
import { ReactiveFormsModule, FormBuilder } from '@angular/forms';
|
import { ReactiveFormsModule, FormBuilder } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { AuthService } from '../../../core/services/auth.service';
|
import { AuthService } from '../../../core/services/auth.service';
|
||||||
import { passwordValidators, PASSWORD_HINT } from '../../../shared/validators/password.validator';
|
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';
|
import { MOTIF_LIEN_RESET_INVALIDE } from '../../../shared/models/auth-redirect-reason';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-reset-password',
|
selector: 'app-reset-password',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ReactiveFormsModule, RouterLink],
|
imports: [ReactiveFormsModule, RouterLink, PasswordRequirementsChecklist],
|
||||||
templateUrl: './reset-password.html',
|
templateUrl: './reset-password.html',
|
||||||
styleUrl: './reset-password.scss',
|
styleUrl: './reset-password.scss',
|
||||||
})
|
})
|
||||||
@@ -30,6 +32,8 @@ export class ResetPassword implements OnInit {
|
|||||||
new_password: ['', passwordValidators],
|
new_password: ['', passwordValidators],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
password = toSignal(this.form.controls.new_password.valueChanges, { initialValue: '' });
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (!this.hasToken) {
|
if (!this.hasToken) {
|
||||||
this.redirigeVersLoginLienInvalide();
|
this.redirigeVersLoginLienInvalide();
|
||||||
|
|||||||
+8
@@ -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>
|
||||||
+29
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
+19
@@ -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.maxLength(PASSWORD_MAX_LENGTH),
|
||||||
Validators.pattern(PASSWORD_PATTERN),
|
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) },
|
||||||
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user