Merge remote-tracking branch 'origin/dev' into feat/design-system
# Conflicts: # apps/frontend/src/app/features/auth/change-password/change-password.html # apps/frontend/src/app/features/auth/change-password/change-password.ts # apps/frontend/src/app/features/auth/login/login.html # apps/frontend/src/app/features/auth/login/login.ts
This commit is contained in:
+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()),
|
||||
})),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const MOTIF_LIEN_RESET_INVALIDE = 'lien-expire';
|
||||
export const MESSAGE_LIEN_RESET_INVALIDE =
|
||||
'Ce lien de réinitialisation est invalide ou a expiré. Connectez-vous ou redemandez-en un.';
|
||||
@@ -10,6 +10,15 @@ export interface PasswordChangeRequest {
|
||||
new_password: string;
|
||||
}
|
||||
|
||||
export interface ForgotPasswordRequest {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface ResetPasswordRequest {
|
||||
token: string;
|
||||
new_password: string;
|
||||
}
|
||||
|
||||
export interface Principal {
|
||||
id: string;
|
||||
email: string;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { passwordValidators } from './password.validator';
|
||||
|
||||
function estValide(motDePasse: string): boolean {
|
||||
return new FormControl(motDePasse, passwordValidators).valid;
|
||||
}
|
||||
|
||||
describe('passwordValidators', () => {
|
||||
it('accepte un mot de passe couvrant les quatre classes', () => {
|
||||
expect(estValide('Un-mot-de-passe1!')).toBe(true);
|
||||
});
|
||||
|
||||
it('accepte un mot de passe accentué (alignement avec le backend, ex: "Sécurité1")', () => {
|
||||
expect(estValide('Sécurité1!')).toBe(true);
|
||||
});
|
||||
|
||||
it('refuse un mot de passe sans majuscule même avec un "×" ou un "÷"', () => {
|
||||
expect(estValide('abcdefg1×')).toBe(false);
|
||||
expect(estValide('abcdefg1÷')).toBe(false);
|
||||
});
|
||||
|
||||
it('refuse un mot de passe sans minuscule même avec un "×" ou un "÷"', () => {
|
||||
expect(estValide('ABCDEFG1×')).toBe(false);
|
||||
expect(estValide('ABCDEFG1÷')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
// Contrainte : `PASSWORD_PATTERN` doit rester identique au validateur Pydantic de
|
||||
// `app/schemas/auth.py` côté backend (mêmes plages de majuscules/minuscules, excluant
|
||||
// × et ÷, mêmes chiffres 0-9, même jeu de caractères spéciaux). `\w`/`\d` divergent entre
|
||||
// JavaScript (ASCII) et Python (Unicode) : une négation aurait accepté ou rejeté un même
|
||||
// mot de passe différemment d'un côté à l'autre (ex. "Sécurité1").
|
||||
|
||||
import { Validators } from '@angular/forms';
|
||||
|
||||
export const PASSWORD_MIN_LENGTH = 8;
|
||||
export const PASSWORD_MAX_LENGTH = 128;
|
||||
export const PASSWORD_HINT =
|
||||
'8 à 128 caractères, avec au moins 1 majuscule, 1 minuscule, 1 chiffre et 1 caractère spécial';
|
||||
|
||||
const SPECIAL_CHARACTERS = '!@#$%^&*()\\-_=+[\\]{};:,.?';
|
||||
const PASSWORD_PATTERN = new RegExp(
|
||||
`^(?=.*[A-ZÀ-ÖØ-Þ])(?=.*[a-zà-öø-þ])` +
|
||||
`(?=.*[0-9])(?=.*[${SPECIAL_CHARACTERS}]).*$`,
|
||||
);
|
||||
|
||||
export const passwordValidators = [
|
||||
Validators.required,
|
||||
Validators.minLength(PASSWORD_MIN_LENGTH),
|
||||
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) },
|
||||
];
|
||||
Reference in New Issue
Block a user