feat(account): add password strength and match validators
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
|
import { passwordStrengthValidator, passwordMatchValidator } from './password.validators';
|
||||||
|
|
||||||
|
describe('passwordStrengthValidator', () => {
|
||||||
|
const validate = (value: string) =>
|
||||||
|
passwordStrengthValidator()(new FormControl(value));
|
||||||
|
|
||||||
|
it('returns null for an empty value (required handles emptiness)', () => {
|
||||||
|
expect(validate('')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for a strong password', () => {
|
||||||
|
expect(validate('Str0ng!Pass')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flags a password that is too short', () => {
|
||||||
|
const errors = validate('Ab1!');
|
||||||
|
expect(errors?.['passwordStrength']?.['minlength']).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flags a missing uppercase letter', () => {
|
||||||
|
const errors = validate('weak1234!');
|
||||||
|
expect(errors?.['passwordStrength']?.['uppercase']).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flags a missing special character', () => {
|
||||||
|
const errors = validate('Weak1234');
|
||||||
|
expect(errors?.['passwordStrength']?.['special']).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('passwordMatchValidator', () => {
|
||||||
|
const buildGroup = (current: string, next: string, confirm: string) =>
|
||||||
|
new FormGroup({
|
||||||
|
currentPassword: new FormControl(current),
|
||||||
|
newPassword: new FormControl(next),
|
||||||
|
confirmPassword: new FormControl(confirm),
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when new matches confirm and differs from current', () => {
|
||||||
|
const group = buildGroup('Old1234!', 'New1234!', 'New1234!');
|
||||||
|
expect(passwordMatchValidator()(group)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flags a confirm mismatch', () => {
|
||||||
|
const group = buildGroup('Old1234!', 'New1234!', 'Different1!');
|
||||||
|
expect(passwordMatchValidator()(group)?.['mismatch']).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flags a new password equal to the current password', () => {
|
||||||
|
const group = buildGroup('Same1234!', 'Same1234!', 'Same1234!');
|
||||||
|
expect(passwordMatchValidator()(group)?.['sameAsCurrent']).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors the ASP.NET Identity password policy enforced on the server:
|
||||||
|
* at least 8 characters with an uppercase, a lowercase, a digit, and a
|
||||||
|
* non-alphanumeric character. Client-side only — the server stays authoritative.
|
||||||
|
* Returns null for an empty value so the `required` validator owns emptiness.
|
||||||
|
*/
|
||||||
|
export function passwordStrengthValidator(): ValidatorFn {
|
||||||
|
return (control: AbstractControl): ValidationErrors | null => {
|
||||||
|
const value = control.value as string;
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors: ValidationErrors = {};
|
||||||
|
if (value.length < 8) {
|
||||||
|
errors['minlength'] = true;
|
||||||
|
}
|
||||||
|
if (!/[A-Z]/.test(value)) {
|
||||||
|
errors['uppercase'] = true;
|
||||||
|
}
|
||||||
|
if (!/[a-z]/.test(value)) {
|
||||||
|
errors['lowercase'] = true;
|
||||||
|
}
|
||||||
|
if (!/[0-9]/.test(value)) {
|
||||||
|
errors['digit'] = true;
|
||||||
|
}
|
||||||
|
if (!/[^a-zA-Z0-9]/.test(value)) {
|
||||||
|
errors['special'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(errors).length ? { passwordStrength: errors } : null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group-level validator: the confirm field must match the new password, and the
|
||||||
|
* new password must differ from the current one.
|
||||||
|
*/
|
||||||
|
export function passwordMatchValidator(): ValidatorFn {
|
||||||
|
return (group: AbstractControl): ValidationErrors | null => {
|
||||||
|
const current = group.get('currentPassword')?.value;
|
||||||
|
const next = group.get('newPassword')?.value;
|
||||||
|
const confirm = group.get('confirmPassword')?.value;
|
||||||
|
|
||||||
|
const errors: ValidationErrors = {};
|
||||||
|
if (next && confirm && next !== confirm) {
|
||||||
|
errors['mismatch'] = true;
|
||||||
|
}
|
||||||
|
if (next && current && next === current) {
|
||||||
|
errors['sameAsCurrent'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(errors).length ? errors : null;
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user