Browse Source

fixed validation of change password fields

pull/14362/head
Maksym Tsymbarov 10 months ago
parent
commit
a62a41e299
  1. 10
      ui-ngx/src/app/modules/home/pages/security/security.component.html
  2. 82
      ui-ngx/src/app/modules/home/pages/security/security.component.ts
  3. 34
      ui-ngx/src/app/modules/login/login-routing.module.ts
  4. 4
      ui-ngx/src/app/modules/login/login.module.ts
  5. 22
      ui-ngx/src/app/modules/login/pages/login/create-password.component.html
  6. 62
      ui-ngx/src/app/modules/login/pages/login/create-password.component.ts
  7. 35
      ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.html
  8. 48
      ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.scss
  9. 57
      ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.ts
  10. 20
      ui-ngx/src/app/modules/login/pages/login/reset-password.component.html
  11. 62
      ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts
  12. 122
      ui-ngx/src/app/shared/models/password.models.ts
  13. 1
      ui-ngx/src/app/shared/models/public-api.ts
  14. 12
      ui-ngx/src/assets/locale/locale.constant-en_US.json

10
ui-ngx/src/app/modules/home/pages/security/security.component.html

@ -40,7 +40,7 @@
<mat-label translate>profile.current-password</mat-label>
<input matInput type="password" name="current-password" formControlName="currentPassword" autocomplete="current-password"/>
<tb-toggle-password [class.!hidden]="!changePassword.get('currentPassword').dirty && !changePassword.get('currentPassword').touched" matSuffix></tb-toggle-password>
<mat-error *ngIf="changePassword.get('currentPassword').hasError('differencePassword')">
<mat-error *ngIf="changePassword.get('currentPassword').hasError('passwordsNotMatch')">
{{ 'security.password-requirement.incorrect-password-try-again' | translate }}
</mat-error>
</mat-form-field>
@ -52,13 +52,13 @@
<mat-error *ngIf="changePassword.get('newPassword').errors
&& !changePassword.get('newPassword').hasError('alreadyUsed')
&& !changePassword.get('newPassword').hasError('hasWhitespaces')
&& !changePassword.get('newPassword').hasError('samePassword')">
&& !changePassword.get('newPassword').hasError('passwordSameAsOld')">
{{ 'security.password-requirement.password-not-meet-requirements' | translate }}
</mat-error>
<mat-error *ngIf="changePassword.get('newPassword').hasError('alreadyUsed')">
{{ changePassword.get('newPassword').getError('alreadyUsed') }}
</mat-error>
<mat-error *ngIf="changePassword.get('newPassword').hasError('samePassword')">
<mat-error *ngIf="changePassword.get('newPassword').hasError('passwordSameAsOld')">
{{ 'security.password-requirement.password-should-difference' | translate }}
</mat-error>
<mat-error *ngIf="changePassword.get('newPassword').hasError('hasWhitespaces')">
@ -72,7 +72,7 @@
<mat-label translate>login.new-password-again</mat-label>
<input matInput type="password" name="new-password" formControlName="newPassword2" autocomplete="new-password" required/>
<tb-toggle-password [class.!hidden]="!changePassword.get('newPassword2').dirty && !changePassword.get('newPassword2').touched" matSuffix></tb-toggle-password>
<mat-error *ngIf="changePassword.get('newPassword2').hasError('differencePassword')">
<mat-error *ngIf="changePassword.get('newPassword2').hasError('passwordsNotMatch')">
{{ 'security.password-requirement.new-passwords-not-match' | translate }}
</mat-error>
</mat-form-field>
@ -134,7 +134,7 @@
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async)">
[disabled]="(isLoading$ | async) || (!changePassword.valid && changePassword.touched)">
{{ 'profile.change-password' | translate }}
</button>
</div>

82
ui-ngx/src/app/modules/home/pages/security/security.component.ts

@ -23,7 +23,6 @@ import {
AbstractControl,
UntypedFormBuilder,
UntypedFormGroup, FormGroupDirective,
NgForm,
ValidationErrors,
ValidatorFn,
Validators
@ -48,10 +47,14 @@ import {
import { authenticationDialogMap } from '@home/pages/security/authentication-dialog/authentication-dialog.map';
import { takeUntil, tap } from 'rxjs/operators';
import { Observable, of, Subject } from 'rxjs';
import { isDefinedAndNotNull, isEqual } from '@core/utils';
import { isDefinedAndNotNull } from '@core/utils';
import { AuthService } from '@core/auth/auth.service';
import { UserPasswordPolicy } from '@shared/models/settings.models';
import { MatCheckboxChange } from '@angular/material/checkbox';
import {
passwordsMatchValidator,
passwordStrengthValidator
} from '@shared/models/password.models';
@Component({
selector: 'tb-security',
@ -164,7 +167,12 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
this.changePassword = this.fb.group({
currentPassword: [''],
newPassword: ['', Validators.required],
newPassword2: ['', this.samePasswordValidation(false, 'newPassword')]
newPassword2: ['']
}, {
validators: [
this.passwordNotSameAsOld(),
passwordsMatchValidator('newPassword', 'newPassword2'),
]
});
}
@ -172,64 +180,36 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
this.authService.getUserPasswordPolicy().subscribe(policy => {
this.passwordPolicy = policy;
this.changePassword.get('newPassword').setValidators([
this.passwordStrengthValidator(),
this.samePasswordValidation(true, 'currentPassword'),
passwordStrengthValidator(this.passwordPolicy),
Validators.required
]);
this.changePassword.get('newPassword').updateValueAndValidity({emitEvent: false});
});
}
private passwordStrengthValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value: string = control.value;
const errors: any = {};
if (this.passwordPolicy.minimumUppercaseLetters > 0 &&
!new RegExp(`(?:.*?[A-Z]){${this.passwordPolicy.minimumUppercaseLetters}}`).test(value)) {
errors.notUpperCase = true;
}
if (this.passwordPolicy.minimumLowercaseLetters > 0 &&
!new RegExp(`(?:.*?[a-z]){${this.passwordPolicy.minimumLowercaseLetters}}`).test(value)) {
errors.notLowerCase = true;
}
if (this.passwordPolicy.minimumDigits > 0
&& !new RegExp(`(?:.*?\\d){${this.passwordPolicy.minimumDigits}}`).test(value)) {
errors.notNumeric = true;
}
if (this.passwordPolicy.minimumSpecialCharacters > 0 &&
!new RegExp(`(?:.*?[\\W_]){${this.passwordPolicy.minimumSpecialCharacters}}`).test(value)) {
errors.notSpecial = true;
}
if (!this.passwordPolicy.allowWhitespaces && /\s/.test(value)) {
errors.hasWhitespaces = true;
}
if (this.passwordPolicy.minimumLength > 0 && value.length < this.passwordPolicy.minimumLength) {
errors.minLength = true;
}
if (!value.length || this.passwordPolicy.maximumLength > 0 && value.length > this.passwordPolicy.maximumLength) {
errors.maxLength = true;
}
passwordNotSameAsOld(): ValidatorFn {
return (group: AbstractControl): ValidationErrors | null => {
const currentPassControl = group.get('currentPassword');
const newPassControl = group.get('newPassword');
return isEqual(errors, {}) ? null : errors;
};
}
private samePasswordValidation(isSame: boolean, key: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value: string = control.value;
const keyValue = control.parent?.value[key];
const current = currentPassControl?.value ?? '';
const newPass = newPassControl?.value ?? '';
if (isSame) {
return value === keyValue ? {samePassword: true} : null;
if (current && newPass && current === newPass) {
newPassControl?.setErrors({
...newPassControl.errors,
passwordSameAsOld: true
});
return { passwordSameAsOld: true };
} else {
const currentErrors = newPassControl?.errors;
if (currentErrors?.passwordSameAsOld) {
const { passwordSameAsOld, ...rest } = currentErrors;
newPassControl.setErrors(Object.keys(rest).length ? rest : null);
}
return null;
}
return value !== keyValue ? {differencePassword: true} : null;
};
}

34
ui-ngx/src/app/modules/login/login-routing.module.ts

@ -14,8 +14,8 @@
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { inject, NgModule } from '@angular/core';
import { ActivatedRouteSnapshot, ResolveFn, Router, RouterModule, RouterStateSnapshot, Routes } from '@angular/router';
import { LoginComponent } from './pages/login/login.component';
import { AuthGuard } from '@core/guards/auth.guard';
@ -25,6 +25,21 @@ import { CreatePasswordComponent } from '@modules/login/pages/login/create-passw
import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component';
import { Authority } from '@shared/models/authority.enum';
import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.component';
import { of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AuthService } from '@core/auth/auth.service';
import { UserPasswordPolicy } from '@shared/models/settings.models';
const passwordPolicyResolver: ResolveFn<UserPasswordPolicy> = (route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
router = inject(Router),
authService = inject(AuthService)) => {
return authService.getUserPasswordPolicy().pipe(
catchError(() => {
return of({} as UserPasswordPolicy);
})
);
};
const routes: Routes = [
{
@ -52,7 +67,10 @@ const routes: Routes = [
title: 'login.reset-password',
module: 'public'
},
canActivate: [AuthGuard]
canActivate: [AuthGuard],
resolve: {
passwordPolicy: passwordPolicyResolver
}
},
{
path: 'login/resetExpiredPassword',
@ -62,7 +80,10 @@ const routes: Routes = [
module: 'public',
expiredPassword: true
},
canActivate: [AuthGuard]
canActivate: [AuthGuard],
resolve: {
passwordPolicy: passwordPolicyResolver
}
},
{
path: 'login/createPassword',
@ -71,7 +92,10 @@ const routes: Routes = [
title: 'login.create-password',
module: 'public'
},
canActivate: [AuthGuard]
canActivate: [AuthGuard],
resolve: {
passwordPolicy: passwordPolicyResolver
}
},
{
path: 'login/mfa',

4
ui-ngx/src/app/modules/login/login.module.ts

@ -25,6 +25,7 @@ import { ResetPasswordComponent } from '@modules/login/pages/login/reset-passwor
import { CreatePasswordComponent } from '@modules/login/pages/login/create-password.component';
import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component';
import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.component';
import { PasswordRequirementsTooltipComponent } from '@modules/login/pages/login/password-requirements-tooltip.component';
@NgModule({
declarations: [
@ -33,7 +34,8 @@ import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.co
ResetPasswordComponent,
CreatePasswordComponent,
TwoFactorAuthLoginComponent,
LinkExpiredComponent
LinkExpiredComponent,
PasswordRequirementsTooltipComponent
],
imports: [
CommonModule,

22
ui-ngx/src/app/modules/login/pages/login/create-password.component.html

@ -32,15 +32,28 @@
<span style="height: 50px;"></span>
<mat-form-field class="mat-block tb-appearance-transparent">
<mat-label translate>common.password</mat-label>
<input matInput type="password" autofocus formControlName="password"/>
<input matInput
type="password"
autofocus
cdkOverlayOrigin
#passwordTrigger="cdkOverlayOrigin"
(focus)="passwordTooltip.onFocus()"
(blur)="passwordTooltip.onBlur()"
formControlName="newPassword"/>
<mat-icon class="material-icons" matPrefix>lock</mat-icon>
<tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="passwordErrorsLength > 0">
{{ 'security.password-requirement.password-not-meet-requirements' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block tb-appearance-transparent">
<mat-label translate>login.password-again</mat-label>
<input matInput type="password" formControlName="password2"/>
<input matInput type="password" formControlName="newPassword2"/>
<mat-icon class="material-icons" matPrefix>lock</mat-icon>
<tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="createPassword.get('newPassword2').hasError('passwordsNotMatch')">
{{ 'security.password-requirement.new-passwords-not-match' | translate }}
</mat-error>
</mat-form-field>
<div class="flex flex-col items-center justify-start gap-4 gt-xs:flex-row gt-xs:items-start gt-xs:justify-center">
<button mat-raised-button color="accent" type="submit" [disabled]="(isLoading$ | async)">
@ -57,3 +70,8 @@
</mat-card-content>
</mat-card>
</div>
<tb-password-requirements-tooltip #passwordTooltip
[passwordControl]="createPassword.get('newPassword')"
[passwordPolicy]="passwordPolicy"
[trigger]="passwordTrigger">
</tb-password-requirements-tooltip>

62
ui-ngx/src/app/modules/login/pages/login/create-password.component.ts

@ -14,57 +14,71 @@
/// limitations under the License.
///
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component } from '@angular/core';
import { AuthService } from '@core/auth/auth.service';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
import { UntypedFormBuilder } from '@angular/forms';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { UserPasswordPolicy } from '@shared/models/settings.models';
import { combineLatest } from 'rxjs';
import {
passwordsMatchValidator,
passwordStrengthValidator
} from '@shared/models/password.models';
@Component({
selector: 'tb-create-password',
templateUrl: './create-password.component.html',
styleUrls: ['./create-password.component.scss']
})
export class CreatePasswordComponent extends PageComponent implements OnInit, OnDestroy {
export class CreatePasswordComponent extends PageComponent {
activateToken = '';
sub: Subscription;
createPassword = this.fb.group({
password: [''],
password2: ['']
});
createPassword: UntypedFormGroup;
passwordPolicy: UserPasswordPolicy;
constructor(protected store: Store<AppState>,
private route: ActivatedRoute,
private authService: AuthService,
private translate: TranslateService,
public fb: UntypedFormBuilder) {
private fb: UntypedFormBuilder) {
super(store);
}
ngOnInit() {
this.sub = this.route
.queryParams
.subscribe(params => {
this.activateToken = params.activateToken || '';
combineLatest([
this.route.queryParams,
this.route.data
])
.pipe(takeUntilDestroyed())
.subscribe(([params, data]) => {
this.activateToken = params['activateToken'] || '';
this.passwordPolicy = data['passwordPolicy'];
});
this.buildCreatePasswordForm();
}
private buildCreatePasswordForm() {
this.createPassword = this.fb.group({
newPassword: ['', [Validators.required, passwordStrengthValidator(this.passwordPolicy)]],
newPassword2:['']
}, {
validators: [
passwordsMatchValidator('newPassword', 'newPassword2'),
]
});
}
ngOnDestroy(): void {
super.ngOnDestroy();
this.sub.unsubscribe();
get passwordErrorsLength(): number {
return Object.keys(this.createPassword.get('newPassword')?.errors ?? {}).length;
}
onCreatePassword() {
if (this.createPassword.get('password').value !== this.createPassword.get('password2').value) {
this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.passwords-mismatch-error'),
type: 'error' }));
if (this.createPassword.invalid) {
this.createPassword.markAllAsTouched();
} else {
this.authService.activate(
this.activateToken,

35
ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.html

@ -0,0 +1,35 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-template cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayOpen]="isTooltipOpen"
[cdkConnectedOverlayPositions]="overlayPositions">
<div class="password-checklist-card">
@for (rule of passwordErrorRules; track $index) {
@if (!rule.policyProp || passwordPolicy[rule.policyProp] > 0) {
<p class="mat-body flex text-sm">
<tb-icon class="tb-mat-20">
{{ checkForError(rule.key) ? 'mdi:close' : 'mdi:check' }}
</tb-icon>
{{ rule.translation | translate : passwordPolicy }}
</p>
}
}
<div class="tooltip-arrow"></div>
</div>
</ng-template>

48
ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.scss

@ -0,0 +1,48 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.password-checklist-card {
background-color: #0000009E;
backdrop-filter: blur(8px);
color: white;
padding: 12px 16px;
border-radius: 8px;
position: relative;
min-width: 220px;
display: flex;
gap: 8px;
flex-direction: column;
& > tb-icon {
color: white;
}
& > p {
margin: 0;
}
& > .tooltip-arrow {
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid #002b36;
}
}

57
ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.ts

@ -0,0 +1,57 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Input } from '@angular/core';
import { CdkOverlayOrigin, ConnectionPositionPair } from '@angular/cdk/overlay';
import { passwordErrorRules } from '@shared/models/password.models';
import { AbstractControl } from '@angular/forms';
import { UserPasswordPolicy } from '@shared/models/settings.models';
@Component({
selector: 'tb-password-requirements-tooltip',
templateUrl: './password-requirements-tooltip.component.html',
styleUrl: './password-requirements-tooltip.component.scss'
})
export class PasswordRequirementsTooltipComponent {
@Input() passwordControl: AbstractControl;
@Input() passwordPolicy: UserPasswordPolicy;
@Input() trigger: CdkOverlayOrigin;
passwordErrorRules = passwordErrorRules;
isTooltipOpen = false;
overlayPositions: ConnectionPositionPair[] = [
{
originX: 'center',
originY: 'top',
overlayX: 'center',
overlayY: 'bottom',
offsetY: -20
}
];
checkForError(errorName: string): boolean {
return this.passwordControl?.hasError(errorName) ?? false;
}
onFocus(): void {
this.isTooltipOpen = true;
}
onBlur(): void {
this.isTooltipOpen = false;
}
}

20
ui-ngx/src/app/modules/login/pages/login/reset-password.component.html

@ -35,15 +35,28 @@
<span style="height: 50px;"></span>
<mat-form-field class="mat-block tb-appearance-transparent">
<mat-label translate>login.new-password</mat-label>
<input matInput type="password" autofocus formControlName="newPassword"/>
<input matInput
type="password"
autofocus
cdkOverlayOrigin
#passwordTrigger="cdkOverlayOrigin"
(focus)="passwordTooltip.onFocus()"
(blur)="passwordTooltip.onBlur()"
formControlName="newPassword"/>
<mat-icon class="material-icons" matPrefix>lock</mat-icon>
<tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="passwordErrorsLength > 0">
{{ 'security.password-requirement.password-not-meet-requirements' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block tb-appearance-transparent">
<mat-label translate>login.new-password-again</mat-label>
<input matInput type="password" formControlName="newPassword2"/>
<mat-icon class="material-icons" matPrefix>lock</mat-icon>
<tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="resetPassword.get('newPassword2').hasError('passwordsNotMatch')">
{{ 'security.password-requirement.new-passwords-not-match' | translate }}
</mat-error>
</mat-form-field>
<div class="flex flex-col items-center justify-start gap-4 gt-sm:flex-row gt-sm:items-start gt-sm:justify-center">
<button mat-raised-button color="accent" type="submit" [disabled]="(isLoading$ | async)">
@ -60,3 +73,8 @@
</mat-card-content>
</mat-card>
</div>
<tb-password-requirements-tooltip #passwordTooltip
[passwordControl]="resetPassword.get('newPassword')"
[passwordPolicy]="passwordPolicy"
[trigger]="passwordTrigger">
</tb-password-requirements-tooltip>

62
ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts

@ -14,61 +14,75 @@
/// limitations under the License.
///
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component } from '@angular/core';
import { AuthService } from '@core/auth/auth.service';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
import { UntypedFormBuilder } from '@angular/forms';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { combineLatest } from 'rxjs';
import { UserPasswordPolicy } from '@shared/models/settings.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
passwordsMatchValidator,
passwordStrengthValidator
} from '@shared/models/password.models';
@Component({
selector: 'tb-reset-password',
templateUrl: './reset-password.component.html',
styleUrls: ['./reset-password.component.scss']
})
export class ResetPasswordComponent extends PageComponent implements OnInit, OnDestroy {
export class ResetPasswordComponent extends PageComponent {
isExpiredPassword: boolean;
resetToken = '';
sub: Subscription;
resetPassword = this.fb.group({
newPassword: [''],
newPassword2: ['']
});
resetPassword: UntypedFormGroup;
passwordPolicy: UserPasswordPolicy;
constructor(protected store: Store<AppState>,
private route: ActivatedRoute,
private router: Router,
private authService: AuthService,
private translate: TranslateService,
public fb: UntypedFormBuilder) {
private fb: UntypedFormBuilder) {
super(store);
combineLatest([
this.route.queryParams,
this.route.data
])
.pipe(takeUntilDestroyed())
.subscribe(([params, data]) => {
this.resetToken = params['resetToken'] || '';
this.passwordPolicy = data['passwordPolicy'];
this.isExpiredPassword = data['expiredPassword'] ?? false;
});
this.buildResetPasswordForm();
}
ngOnInit() {
this.isExpiredPassword = this.route.snapshot.data.expiredPassword;
this.sub = this.route
.queryParams
.subscribe(params => {
this.resetToken = params.resetToken || '';
});
private buildResetPasswordForm() {
this.resetPassword = this.fb.group({
newPassword: ['', [Validators.required, passwordStrengthValidator(this.passwordPolicy)]],
newPassword2: ['']
}, {
validators: [
passwordsMatchValidator('newPassword', 'newPassword2'),
]
});
}
ngOnDestroy(): void {
super.ngOnDestroy();
this.sub.unsubscribe();
get passwordErrorsLength(): number {
return Object.keys(this.resetPassword.get('newPassword')?.errors ?? {}).length;
}
onResetPassword() {
if (this.resetPassword.get('newPassword').value !== this.resetPassword.get('newPassword2').value) {
this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.passwords-mismatch-error'),
type: 'error' }));
if (this.resetPassword.invalid) {
this.resetPassword.markAllAsTouched();
} else {
this.authService.resetPassword(
this.resetToken,

122
ui-ngx/src/app/shared/models/password.models.ts

@ -0,0 +1,122 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { UserPasswordPolicy } from '@shared/models/settings.models';
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
import { isEqual } from '@core/utils';
export enum PasswordErrorMessageKey {
minLength = 'security.password-requirement.password-min-length',
maxLength = 'security.password-requirement.password-max-length',
notUpperCase = 'security.password-requirement.password-uppercase',
notLowerCase = 'security.password-requirement.password-lowercase',
notNumeric = 'security.password-requirement.password-digit',
notSpecial = 'security.password-requirement.password-special-characters',
hasWhitespaces = 'security.password-requirement.password-should-not-contain-spaces',
default = 'security.password-requirement.password-not-meet-requirements'
}
export enum TooltipPasswordErrorMessageKey {
minLength = 'security.password-requirement.password-tooltip-min-length',
maxLength = 'security.password-requirement.password-tooltip-max-length',
notUpperCase = 'security.password-requirement.password-tooltip-uppercase',
notLowerCase = 'security.password-requirement.password-tooltip-lowercase',
notNumeric = 'security.password-requirement.password-tooltip-digit',
notSpecial = 'security.password-requirement.password-tooltip-special-characters'
}
export const passwordErrorRules = [
{ key: 'minLength', policyProp: 'minimumLength', translation: TooltipPasswordErrorMessageKey.minLength },
{ key: 'notUpperCase', policyProp: 'minimumUppercaseLetters', translation: TooltipPasswordErrorMessageKey.notUpperCase },
{ key: 'notLowerCase', policyProp: 'minimumLowercaseLetters', translation: TooltipPasswordErrorMessageKey.notLowerCase },
{ key: 'notNumeric', policyProp: 'minimumDigits', translation: TooltipPasswordErrorMessageKey.notNumeric },
{ key: 'notSpecial', policyProp: 'minimumSpecialCharacters', translation: TooltipPasswordErrorMessageKey.notSpecial },
{ key: 'maxLength', policyProp: 'maximumLength', translation: TooltipPasswordErrorMessageKey.maxLength },
];
export const passwordsMatchValidator = (firstControlName: string, secondControlName: string): ValidatorFn =>{
return (group: AbstractControl): ValidationErrors | null => {
const newPassControl = group.get(firstControlName);
const confirmControl = group.get(secondControlName);
if (!newPassControl || !confirmControl) {
return null;
}
const newPass = newPassControl.value ?? '';
const confirm = confirmControl.value ?? '';
const userInteracted =
confirmControl.touched || confirmControl.dirty || group.touched;
if (!userInteracted) {
return null;
}
if (newPass && confirm !== newPass) {
confirmControl.setErrors({ passwordsNotMatch: true });
return { passwordsNotMatch: true };
} else {
const currentErrors = confirmControl?.errors;
if (currentErrors?.['passwordsNotMatch']) {
const { passwordsNotMatch, ...rest } = currentErrors;
confirmControl?.setErrors(Object.keys(rest).length ? rest : null);
}
return null;
}
};
}
export const passwordStrengthValidator = (passwordPolicy: UserPasswordPolicy): ValidatorFn => {
return (control: AbstractControl): ValidationErrors | null => {
const value: string = control.value;
const errors: any = {};
if (passwordPolicy.minimumUppercaseLetters > 0 &&
!new RegExp(`(?:.*?[A-Z]){${passwordPolicy.minimumUppercaseLetters}}`).test(value)) {
errors.notUpperCase = true;
}
if (passwordPolicy.minimumLowercaseLetters > 0 &&
!new RegExp(`(?:.*?[a-z]){${passwordPolicy.minimumLowercaseLetters}}`).test(value)) {
errors.notLowerCase = true;
}
if (passwordPolicy.minimumDigits > 0
&& !new RegExp(`(?:.*?\\d){${passwordPolicy.minimumDigits}}`).test(value)) {
errors.notNumeric = true;
}
if (passwordPolicy.minimumSpecialCharacters > 0 &&
!new RegExp(`(?:.*?[\\W_]){${passwordPolicy.minimumSpecialCharacters}}`).test(value)) {
errors.notSpecial = true;
}
if (!passwordPolicy.allowWhitespaces && /\s/.test(value)) {
errors.hasWhitespaces = true;
}
if (passwordPolicy.minimumLength > 0 && value.length < passwordPolicy.minimumLength) {
errors.minLength = true;
}
if (!value.length || passwordPolicy.maximumLength > 0 && value.length > passwordPolicy.maximumLength) {
errors.maxLength = true;
}
return isEqual(errors, {}) ? null : errors;
};
}

1
ui-ngx/src/app/shared/models/public-api.ts

@ -71,3 +71,4 @@ export * from './query/query.models';
export * from './regex.constants';
export * from './trendz-settings.models';
export * from './ai-model.models';
export * from './password.models';

12
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -4412,6 +4412,18 @@
"at-least": "At least:",
"character": "{ count, plural, =1 {1 character} other {# characters} }",
"digit": "{ count, plural, =1 {1 digit} other {# digits} }",
"password-tooltip-min-length": "At least {{minimumLength}} characters long",
"password-tooltip-max-length": "At most {{maximumLength}} characters long",
"password-tooltip-uppercase": "{{minimumUppercaseLetters}} uppercase character",
"password-tooltip-lowercase": "{{minimumLowercaseLetters}} lowercase character",
"password-tooltip-digit": "{{minimumDigits}} number",
"password-tooltip-special-characters": "{{minimumSpecialCharacters}} special character",
"password-min-length": "Password must be {{minimumLength}} or more characters in length",
"password-max-length": "Password should be less than {{maximumLength}}",
"password-uppercase": "Password must contain {{minimumUppercaseLetters}} or more uppercase characters",
"password-lowercase": "Password must contain {{minimumLowercaseLetters}} or more lowercase characters",
"password-digit": "Password must contain {{minimumDigits}} or more digit characters",
"password-special-characters": "Password must contain {{minimumSpecialCharacters}} or more special characters",
"incorrect-password-try-again": "Incorrect password. Try again",
"lowercase-letter": "{ count, plural, =1 {1 lowercase letter} other {# lowercase letters} }",
"new-passwords-not-match": "New password didn't match",

Loading…
Cancel
Save