Merge pull request #9791 from ArtemDzhereleiko/AD/imp/security-settings/password-length
Improvement for security settings
This commit is contained in:
commit
614d64dda2
@ -51,24 +51,37 @@
|
||||
<fieldset class="fields-group">
|
||||
<legend class="group-title" translate>admin.password-policy</legend>
|
||||
<section formGroupName="passwordPolicy">
|
||||
<mat-form-field class="mat-block">
|
||||
<mat-label translate>admin.minimum-password-length</mat-label>
|
||||
<input matInput type="number"
|
||||
formControlName="minimumLength"
|
||||
step="1"
|
||||
min="5"
|
||||
max="50"
|
||||
required/>
|
||||
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('required')">
|
||||
{{ 'admin.minimum-password-length-required' | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('min')">
|
||||
{{ 'admin.minimum-password-length-range' | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('max')">
|
||||
{{ 'admin.minimum-password-length-range' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
|
||||
<mat-form-field fxFlex class="mat-block">
|
||||
<mat-label translate>admin.minimum-password-length</mat-label>
|
||||
<input matInput type="number"
|
||||
formControlName="minimumLength"
|
||||
step="1"
|
||||
min="6"
|
||||
max="50"
|
||||
required/>
|
||||
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('required')">
|
||||
{{ 'admin.minimum-password-length-required' | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('min')">
|
||||
{{ 'admin.minimum-password-length-range' | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('max')">
|
||||
{{ 'admin.minimum-password-length-range' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field fxFlex class="mat-block" subscriptSizing="dynamic">
|
||||
<mat-label translate>admin.maximum-password-length</mat-label>
|
||||
<input matInput type="number" formControlName="maximumLength" step="1" min="6"/>
|
||||
<mat-hint></mat-hint>
|
||||
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.maximumLength').hasError('min')">
|
||||
{{ 'admin.maximum-password-length-min' | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.maximumLength').hasError('lessMin')">
|
||||
{{ 'admin.maximum-password-length-less-min' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
|
||||
<mat-form-field fxFlex class="mat-block">
|
||||
<mat-label translate>admin.minimum-uppercase-letters</mat-label>
|
||||
@ -140,9 +153,16 @@
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<mat-checkbox formControlName="allowWhitespaces" style="margin-bottom: 16px">
|
||||
<mat-label translate>admin.allow-whitespace</mat-label>
|
||||
</mat-checkbox>
|
||||
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
|
||||
<mat-checkbox fxFlex formControlName="allowWhitespaces" style="margin-bottom: 16px">
|
||||
<mat-label translate>admin.allow-whitespace</mat-label>
|
||||
</mat-checkbox>
|
||||
<mat-checkbox fxFlex formControlName="forceUserToResetPasswordIfNotValid" style="margin-bottom: 16px">
|
||||
<mat-label tb-hint-tooltip-icon="{{'admin.force-reset-password-if-no-valid-hint' | translate}}">
|
||||
{{'admin.force-reset-password-if-no-valid' | translate}}
|
||||
</mat-label>
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</section>
|
||||
</fieldset>
|
||||
<div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px" class="layout-wrap" style="margin-top: 16px">
|
||||
|
||||
@ -19,7 +19,14 @@ import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { PageComponent } from '@shared/components/page.component';
|
||||
import { Router } from '@angular/router';
|
||||
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import {
|
||||
AbstractControl,
|
||||
UntypedFormBuilder,
|
||||
UntypedFormControl,
|
||||
UntypedFormGroup, ValidationErrors,
|
||||
ValidatorFn,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { JwtSettings, SecuritySettings } from '@shared/models/settings.models';
|
||||
import { AdminService } from '@core/http/admin.service';
|
||||
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
|
||||
@ -28,7 +35,10 @@ import { randomAlphanumeric } from '@core/utils';
|
||||
import { AuthService } from '@core/auth/auth.service';
|
||||
import { DialogService } from '@core/services/dialog.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { forkJoin, Observable, of } from 'rxjs';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { AlarmInfo } from '@shared/models/alarm.models';
|
||||
import { QueueProcessingStrategyTypes, QueueProcessingStrategyTypesMap } from '@shared/models/queue.models';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-security-settings',
|
||||
@ -67,14 +77,16 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi
|
||||
userLockoutNotificationEmail: ['', []],
|
||||
passwordPolicy: this.fb.group(
|
||||
{
|
||||
minimumLength: [null, [Validators.required, Validators.min(5), Validators.max(50)]],
|
||||
minimumLength: [null, [Validators.required, Validators.min(6), Validators.max(50)]],
|
||||
maximumLength: [null, [Validators.min(6), this.maxPasswordValidation()]],
|
||||
minimumUppercaseLetters: [null, Validators.min(0)],
|
||||
minimumLowercaseLetters: [null, Validators.min(0)],
|
||||
minimumDigits: [null, Validators.min(0)],
|
||||
minimumSpecialCharacters: [null, Validators.min(0)],
|
||||
passwordExpirationPeriodDays: [null, Validators.min(0)],
|
||||
passwordReuseFrequencyDays: [null, Validators.min(0)],
|
||||
allowWhitespaces: [true]
|
||||
allowWhitespaces: [true],
|
||||
forceUserToResetPasswordIfNotValid: [false]
|
||||
}
|
||||
)
|
||||
});
|
||||
@ -113,6 +125,18 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi
|
||||
})).subscribe(() => {});
|
||||
}
|
||||
|
||||
private maxPasswordValidation(): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const value: string = control.value;
|
||||
if (value) {
|
||||
if (value < control.parent.value?.minimumLength) {
|
||||
return {lessMin: true};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
discardSetting() {
|
||||
this.securitySettingsFormGroup.reset(this.securitySettings);
|
||||
}
|
||||
@ -189,5 +213,4 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi
|
||||
confirmForm(): UntypedFormGroup {
|
||||
return this.securitySettingsFormGroup.dirty ? this.securitySettingsFormGroup : this.jwtSecuritySettingsFormGroup;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -44,10 +44,11 @@
|
||||
{{ 'security.password-requirement.incorrect-password-try-again' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block same-color" hideRequiredMarker appearance="fill" color="primary">
|
||||
<mat-form-field class="mat-block same-color" hideRequiredMarker appearance="fill" color="primary" subscriptSizing="dynamic">
|
||||
<mat-label translate>login.new-password</mat-label>
|
||||
<input matInput type="password" name="new-password" formControlName="newPassword" autocomplete="new-password" required/>
|
||||
<tb-toggle-password [fxShow]="changePassword.get('newPassword').dirty || changePassword.get('newPassword').touched" matSuffix></tb-toggle-password>
|
||||
<mat-hint></mat-hint>
|
||||
<mat-error *ngIf="changePassword.get('newPassword').errors
|
||||
&& !changePassword.get('newPassword').hasError('alreadyUsed')
|
||||
&& !changePassword.get('newPassword').hasError('hasWhitespaces')
|
||||
@ -115,6 +116,13 @@
|
||||
</tb-icon>
|
||||
{{ 'security.password-requirement.character' | translate : {count: passwordPolicy.minimumLength} }}
|
||||
</p>
|
||||
<div class="password-requirements" *ngIf="passwordPolicy.maximumLength > 0">
|
||||
<h4 class="mat-h4" translate>security.password-requirement.at-most</h4>
|
||||
<p class="mat-body">
|
||||
<mat-icon class="tb-mat-20" [svgIcon]="changePassword.get('newPassword').hasError('maxLength') ? 'mdi:circle-small' : 'mdi:check'"></mat-icon>
|
||||
{{ 'security.password-requirement.character' | translate : {count: passwordPolicy.maximumLength} }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div fxLayout="row" fxLayoutGap="8px" style="margin-top: 18px;" [fxShow]="changePassword.dirty || changePassword.touched">
|
||||
|
||||
@ -213,6 +213,10 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
|
||||
errors.minLength = true;
|
||||
}
|
||||
|
||||
if (!value.length || this.passwordPolicy.maximumLength > 0 && value.length > this.passwordPolicy.maximumLength) {
|
||||
errors.maxLength = true;
|
||||
}
|
||||
|
||||
return isEqual(errors, {}) ? null : errors;
|
||||
};
|
||||
}
|
||||
|
||||
@ -56,7 +56,8 @@
|
||||
<mat-icon matPrefix>lock</mat-icon>
|
||||
</mat-form-field>
|
||||
<div fxLayoutAlign="end center" class="forgot-password">
|
||||
<button class="tb-reset-password" mat-button type="button" routerLink="/login/resetPasswordRequest">{{ 'login.forgot-password' | translate }}
|
||||
<button class="tb-reset-password" mat-button type="button" routerLink="/login/resetPasswordRequest">
|
||||
{{ (passwordViolation ? 'login.reset-password' : 'login.forgot-password') | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div fxLayout="column" class="tb-action-button">
|
||||
|
||||
@ -32,6 +32,8 @@ import { OAuth2ClientInfo } from '@shared/models/oauth2.models';
|
||||
})
|
||||
export class LoginComponent extends PageComponent implements OnInit {
|
||||
|
||||
passwordViolation = false;
|
||||
|
||||
loginFormGroup = this.fb.group({
|
||||
username: '',
|
||||
password: ''
|
||||
@ -57,6 +59,8 @@ export class LoginComponent extends PageComponent implements OnInit {
|
||||
if (error && error.error && error.error.errorCode) {
|
||||
if (error.error.errorCode === Constants.serverErrorCode.credentialsExpired) {
|
||||
this.router.navigateByUrl(`login/resetExpiredPassword?resetToken=${error.error.resetToken}`);
|
||||
} else if (error.error.errorCode === Constants.serverErrorCode.passwordViolation) {
|
||||
this.passwordViolation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,7 +30,8 @@ export const Constants = {
|
||||
badRequestParams: 31,
|
||||
itemNotFound: 32,
|
||||
tooManyRequests: 33,
|
||||
tooManyUpdates: 34
|
||||
tooManyUpdates: 34,
|
||||
passwordViolation: 45
|
||||
},
|
||||
entryPoints: {
|
||||
login: '/api/auth/login',
|
||||
|
||||
@ -99,12 +99,14 @@ export type DeviceConnectivitySettings = Record<DeviceConnectivityProtocol, Devi
|
||||
|
||||
export interface UserPasswordPolicy {
|
||||
minimumLength: number;
|
||||
maximumLength: number;
|
||||
minimumUppercaseLetters: number;
|
||||
minimumLowercaseLetters: number;
|
||||
minimumDigits: number;
|
||||
minimumSpecialCharacters: number;
|
||||
passwordExpirationPeriodDays: number;
|
||||
allowWhitespaces: boolean;
|
||||
forceUserToResetPasswordIfNotValid: boolean;
|
||||
}
|
||||
|
||||
export interface SecuritySettings {
|
||||
|
||||
@ -182,7 +182,10 @@
|
||||
"password-policy": "Password policy",
|
||||
"minimum-password-length": "Minimum password length",
|
||||
"minimum-password-length-required": "Minimum password length is required",
|
||||
"minimum-password-length-range": "Minimum password length should be in a range from 5 to 50",
|
||||
"minimum-password-length-range": "Minimum password length should be in a range from 6 to 50",
|
||||
"maximum-password-length": "Maximum password length",
|
||||
"maximum-password-length-min": "Maximum password length should be at least 6",
|
||||
"maximum-password-length-less-min": "Maximum password length should be greater than minimum length",
|
||||
"minimum-uppercase-letters": "Minimum number of uppercase letters",
|
||||
"minimum-uppercase-letters-range": "Minimum number of uppercase letters can't be negative",
|
||||
"minimum-lowercase-letters": "Minimum number of lowercase letters",
|
||||
@ -196,6 +199,8 @@
|
||||
"password-reuse-frequency-days": "Password reuse frequency in days",
|
||||
"password-reuse-frequency-days-range": "Password reuse frequency in days can't be negative",
|
||||
"allow-whitespace": "Allow whitespace",
|
||||
"force-reset-password-if-no-valid": "Force to reset password if not valid",
|
||||
"force-reset-password-if-no-valid-hint": "Please be careful when enabling this feature: it will require users with not valid password to reset their password via email.",
|
||||
"general-policy": "General policy",
|
||||
"max-failed-login-attempts": "Maximum number of failed login attempts, before account is locked",
|
||||
"minimum-max-failed-login-attempts-range": "Maximum number of failed login attempts can't be negative",
|
||||
@ -3610,7 +3615,8 @@
|
||||
"password-requirements": "Password requirements",
|
||||
"password-should-difference": "New password should be different from current",
|
||||
"special-character": "{ count, plural, =1 {1 special character} other {# special characters} }",
|
||||
"uppercase-letter": "{ count, plural, =1 {1 uppercase letter} other {# uppercase letters} }"
|
||||
"uppercase-letter": "{ count, plural, =1 {1 uppercase letter} other {# uppercase letters} }",
|
||||
"at-most": "At most:"
|
||||
}
|
||||
},
|
||||
"relation": {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user