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