Merge pull request #2384 from alberto-art3ch/fix/user_change_password_validations
Change password view enhancement to support and display password vali…
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
|
||||
import { environment } from 'environments/environment';
|
||||
|
||||
export function passwordValidator(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const value = control.value;
|
||||
if (!value) return null; // No validation if the field is empty
|
||||
|
||||
const errors: ValidationErrors = {};
|
||||
|
||||
if (value.length < environment.minPasswordLength) {
|
||||
errors['minLength'] = 'Password must be at least ' + environment.minPasswordLength + ' characters long';
|
||||
}
|
||||
if (value.length > 50) {
|
||||
errors['maxLength'] = 'Password must be maximum 50 characters long';
|
||||
}
|
||||
if (!/[A-Z]/.test(value)) {
|
||||
errors['uppercase'] = 'Password must contain at least one uppercase letter';
|
||||
}
|
||||
if (!/[a-z]/.test(value)) {
|
||||
errors['lowercase'] = 'Password must contain at least one lowercase letter';
|
||||
}
|
||||
if (!/\d/.test(value)) {
|
||||
errors['number'] = 'Password must contain at least one number';
|
||||
}
|
||||
if (!/^(?:(.)(?!\1))+$/.test(value)) {
|
||||
errors['repeated'] = 'Password must have not consecutive repeating characters';
|
||||
}
|
||||
if (!/[@$!%*?&]/.test(value)) {
|
||||
errors['specialChar'] = 'Password must contain at least one special character (@$!%*?&)';
|
||||
}
|
||||
|
||||
return Object.keys(errors).length > 0 ? errors : null;
|
||||
};
|
||||
}
|
||||
@@ -1,21 +1,27 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AbstractControl, ValidationErrors, ValidatorFn, Validators } from '@angular/forms';
|
||||
import { environment } from 'environments/environment';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { passwordValidator } from './password.validator';
|
||||
import { environment } from 'environments/environment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class PasswordsUtility {
|
||||
minPasswordLength: number = environment.minPasswordLength | 12;
|
||||
// password regex pattern
|
||||
public static PASSWORD_REGEX = '^(?!.*(.)\\1{1,})(?!.*\\s)(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^\\w\\s]).{12,50}$';
|
||||
public static PASSWORD_REGEX =
|
||||
'^(?!.*(.)\\1{1,})(?!.*\\s)(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^\\w\\s]).{' +
|
||||
(environment.minPasswordLength | 12) +
|
||||
',50}$';
|
||||
|
||||
public getPasswordValidators(): ValidatorFn[] {
|
||||
return [
|
||||
Validators.required,
|
||||
Validators.pattern(PasswordsUtility.PASSWORD_REGEX),
|
||||
Validators.minLength(this.minPasswordLength),
|
||||
Validators.maxLength(50),
|
||||
Validators.minLength(environment.minPasswordLength)];
|
||||
passwordValidator()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,68 +3,40 @@
|
||||
<div mat-dialog-content>
|
||||
<form [formGroup]="changePasswordForm">
|
||||
<div fxLayout="column">
|
||||
<mat-form-field>
|
||||
<mat-label>{{ 'labels.inputs.Password' | translate }}</mat-label>
|
||||
<input matInput formControlName="password" type="{{ passwordInputType[0] }}" />
|
||||
<button
|
||||
type="button"
|
||||
mat-button
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
(mousedown)="togglePasswordVisibility(0)"
|
||||
(mouseup)="togglePasswordVisibility(0)"
|
||||
>
|
||||
<fa-icon *ngIf="passwordInputType[0] === 'password'" icon="eye"></fa-icon>
|
||||
<fa-icon *ngIf="passwordInputType[0] === 'text'" icon="eye-slash"></fa-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="changePasswordForm.controls.password.hasError('required')">
|
||||
{{ 'labels.inputs.Password' | translate }} <strong>{{ 'labels.commons.is required' | translate }}</strong>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="changePasswordForm.controls.password.hasError('minlength')">
|
||||
{{ 'labels.commons.Password should be at least' | translate }}
|
||||
<strong>{{ 'labels.commons.12 characters long' | translate }}</strong>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="changePasswordForm.controls.password.hasError('maxlength')">
|
||||
{{ 'labels.commons.Password should not be more than' | translate }}
|
||||
<strong>{{ 'labels.commons.50 characters long' | translate }}</strong>
|
||||
</mat-error>
|
||||
<mat-error
|
||||
*ngIf="
|
||||
changePasswordForm.controls.password.hasError('pattern') &&
|
||||
!(
|
||||
changePasswordForm.controls.password.hasError('minlength') ||
|
||||
changePasswordForm.controls.password.hasError('maxlength')
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ 'labels.commons.Password should include a' | translate }}
|
||||
<strong>{{ 'labels.commons.numeral' | translate }}</strong> {{ 'labels.commons.and at' | translate }}
|
||||
<strong>{{ 'labels.commons.least one uppercase' | translate }}</strong
|
||||
>, <strong>{{ 'labels.commons.lowercase and special character' | translate }}</strong>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mifosx-input-password
|
||||
#password
|
||||
formControlName="password"
|
||||
label="{{ 'Password' | translateKey: 'inputs' }}"
|
||||
></mifosx-input-password>
|
||||
<div class="error">
|
||||
<p *ngIf="changePasswordForm.get('password').errors?.['uppercase']">
|
||||
{{ changePasswordForm.get('password').errors?.['uppercase'] | translateKey: 'inputs' }}
|
||||
</p>
|
||||
<p *ngIf="changePasswordForm.get('password').errors?.['number']">
|
||||
{{ changePasswordForm.get('password').errors?.['number'] | translateKey: 'inputs' }}
|
||||
</p>
|
||||
<p *ngIf="changePasswordForm.get('password').errors?.['repeated']">
|
||||
{{ changePasswordForm.get('password').errors?.['repeated'] | translateKey: 'inputs' }}
|
||||
</p>
|
||||
<p *ngIf="changePasswordForm.get('password').errors?.['specialChar']">
|
||||
{{ changePasswordForm.get('password').errors?.['specialChar'] | translateKey: 'inputs' }}
|
||||
</p>
|
||||
<p *ngIf="changePasswordForm.get('password').errors?.['minlength']">
|
||||
{{ 'Password minimun length must be' | translateKey: 'inputs' }} {{ minPasswordLength }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>{{ 'labels.inputs.Repeat Password' | translate }}</mat-label>
|
||||
<input matInput formControlName="repeatPassword" type="{{ passwordInputType[1] }}" />
|
||||
<button
|
||||
type="button"
|
||||
mat-button
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
(mousedown)="togglePasswordVisibility(1)"
|
||||
(mouseup)="togglePasswordVisibility(1)"
|
||||
>
|
||||
<fa-icon *ngIf="passwordInputType[1] === 'password'" icon="eye"></fa-icon>
|
||||
<fa-icon *ngIf="passwordInputType[1] === 'text'" icon="eye-slash"></fa-icon>
|
||||
</button>
|
||||
<mat-error *ngIf="changePasswordForm.controls.repeatPassword.hasError('required')">
|
||||
{{ 'labels.inputs.Password' | translate }} <strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="changePasswordForm.controls.repeatPassword.hasError('notequal')">
|
||||
{{ 'labels.inputs.Password' | translate }} <strong>{{ 'labels.commons.does not match' | translate }}</strong>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mifosx-input-password
|
||||
#repeatPassword
|
||||
formControlName="repeatPassword"
|
||||
label="{{ 'Confirm Password' | translateKey: 'inputs' }}"
|
||||
></mifosx-input-password>
|
||||
</div>
|
||||
|
||||
<div class="error">
|
||||
<p *ngIf="changePasswordForm.get('repeatPassword').errors?.['notequal']">
|
||||
{{ 'Passwords do not match' | translateKey: 'inputs' }}
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
mat-dialog-content {
|
||||
min-width: 240px;
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { UntypedFormBuilder, Validators, ValidatorFn, AbstractControl, ValidationErrors } from '@angular/forms';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { PasswordsUtility } from 'app/core/utils/passwords-utility';
|
||||
import { environment } from 'environments/environment';
|
||||
|
||||
/**
|
||||
* Change Password Dialog component.
|
||||
@@ -14,6 +15,8 @@ import { PasswordsUtility } from 'app/core/utils/passwords-utility';
|
||||
styleUrls: ['./change-password-dialog.component.scss']
|
||||
})
|
||||
export class ChangePasswordDialogComponent implements OnInit {
|
||||
minPasswordLength: number = environment.minPasswordLength | 12;
|
||||
|
||||
/** Change Password Form */
|
||||
changePasswordForm: any;
|
||||
/** Password input field type. */
|
||||
@@ -37,15 +40,6 @@ export class ChangePasswordDialogComponent implements OnInit {
|
||||
this.createChangePasswordForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the visibility of the password input field.
|
||||
*
|
||||
* Changes the input type between 'password' and 'text'.
|
||||
*/
|
||||
togglePasswordVisibility(index: number) {
|
||||
this.passwordInputType[index] = this.passwordInputType[index] === 'password' ? 'text' : 'password';
|
||||
}
|
||||
|
||||
/** Change Password form */
|
||||
createChangePasswordForm() {
|
||||
this.changePasswordForm = this.formBuilder.group({
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<mat-form-field>
|
||||
<mat-label>{{ label }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[disabled]="disabled"
|
||||
[errorStateMatcher]="matcher"
|
||||
(focusout)="onFocusOut()"
|
||||
(input)="onInput($event)"
|
||||
[required]="required"
|
||||
[type]="type"
|
||||
[value]="value"
|
||||
/>
|
||||
<mat-icon matSuffix (click)="onVisibilityClick($event)">{{ icon }}</mat-icon>
|
||||
<mat-error *ngIf="ngControl.hasError('required')">{{ label }} is a required field.</mat-error>
|
||||
<mat-error *ngIf="ngControl.hasError(customErrorName)">{{ customErrorMessage }}</mat-error>
|
||||
</mat-form-field>
|
||||
@@ -0,0 +1,11 @@
|
||||
:host {
|
||||
width: 100%;
|
||||
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { InputPasswordComponent } from './input-password.component';
|
||||
|
||||
describe('InputPasswordComponent', () => {
|
||||
let component: InputPasswordComponent;
|
||||
let fixture: ComponentFixture<InputPasswordComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [InputPasswordComponent]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(InputPasswordComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Component, Input, Optional, Self, ViewChild } from '@angular/core';
|
||||
import {
|
||||
AbstractControl,
|
||||
ControlValueAccessor,
|
||||
FormGroupDirective,
|
||||
NgControl,
|
||||
NgForm,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { MatInput } from '@angular/material/input';
|
||||
import { ErrorStateMatcher } from '@angular/material/core';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-input-password',
|
||||
templateUrl: './input-password.component.html',
|
||||
styleUrls: ['./input-password.component.scss']
|
||||
})
|
||||
export class InputPasswordComponent implements ControlValueAccessor, ErrorStateMatcher {
|
||||
disabled = false;
|
||||
|
||||
icon = 'visibility_off';
|
||||
|
||||
@Input()
|
||||
label: string = null;
|
||||
|
||||
@Input()
|
||||
customErrorMessage: string = null;
|
||||
|
||||
@Input()
|
||||
customErrorName: string = null;
|
||||
|
||||
get matcher() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@ViewChild(MatInput)
|
||||
matInput: MatInput;
|
||||
|
||||
get required(): boolean {
|
||||
return this._required ?? this.ngControl?.control?.hasValidator(Validators.required) ?? false;
|
||||
}
|
||||
|
||||
@Input()
|
||||
set required(value: boolean) {
|
||||
this._required = value;
|
||||
}
|
||||
|
||||
touched = false;
|
||||
|
||||
type = 'password';
|
||||
|
||||
value: string = null;
|
||||
|
||||
private _required: boolean = null;
|
||||
|
||||
private _visible = false;
|
||||
|
||||
constructor(@Optional() @Self() public ngControl: NgControl) {
|
||||
if (ngControl !== null) {
|
||||
ngControl.valueAccessor = this;
|
||||
}
|
||||
}
|
||||
|
||||
isErrorState(control: AbstractControl | null, form: FormGroupDirective | NgForm | null): boolean {
|
||||
return this.touched && (this.ngControl?.control?.invalid ?? false);
|
||||
}
|
||||
|
||||
onChange = (value: any) => {};
|
||||
|
||||
onFocusOut(): void {
|
||||
this.touched = true;
|
||||
this.onTouched();
|
||||
this.matInput.updateErrorState();
|
||||
}
|
||||
|
||||
onInput($event: any): void {
|
||||
this.value = $event.currentTarget.value;
|
||||
this.touched = true;
|
||||
this.onChange(this.value);
|
||||
}
|
||||
|
||||
onTouched = () => {};
|
||||
|
||||
onVisibilityClick($event: Event): void {
|
||||
if (this._visible) {
|
||||
this.icon = 'visibility_off';
|
||||
this.type = 'password';
|
||||
} else {
|
||||
this.icon = 'visibility';
|
||||
this.type = 'text';
|
||||
}
|
||||
|
||||
// Invert the value.
|
||||
this._visible = !this._visible;
|
||||
|
||||
$event.stopPropagation();
|
||||
}
|
||||
|
||||
registerOnChange(fn: any) {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: any) {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean) {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
writeValue(obj: any): void {
|
||||
this.value = obj;
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ import { ThemeToggleComponent } from './theme-toggle/theme-toggle.component';
|
||||
import { LongTextComponent } from './long-text/long-text.component';
|
||||
import { DropdownComponent } from './dropdown/dropdown.component';
|
||||
import { InputAmountComponent } from './input-amount/input-amount.component';
|
||||
import { InputPasswordComponent } from './input-password/input-password.component';
|
||||
|
||||
/**
|
||||
* Shared Module
|
||||
@@ -106,7 +107,8 @@ import { InputAmountComponent } from './input-amount/input-amount.component';
|
||||
ThemeToggleComponent,
|
||||
LongTextComponent,
|
||||
DropdownComponent,
|
||||
InputAmountComponent
|
||||
InputAmountComponent,
|
||||
InputPasswordComponent
|
||||
],
|
||||
exports: [
|
||||
FileUploadComponent,
|
||||
|
||||
Reference in New Issue
Block a user