From a62a41e299967e7725279ea9a20be90925f6d56e Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 17 Nov 2025 10:32:16 +0200 Subject: [PATCH 1/5] fixed validation of change password fields --- .../pages/security/security.component.html | 10 +- .../home/pages/security/security.component.ts | 82 +++++------- .../app/modules/login/login-routing.module.ts | 34 ++++- ui-ngx/src/app/modules/login/login.module.ts | 4 +- .../login/create-password.component.html | 22 +++- .../pages/login/create-password.component.ts | 62 +++++---- ...ssword-requirements-tooltip.component.html | 35 +++++ ...ssword-requirements-tooltip.component.scss | 48 +++++++ ...password-requirements-tooltip.component.ts | 57 ++++++++ .../pages/login/reset-password.component.html | 20 ++- .../pages/login/reset-password.component.ts | 62 +++++---- .../src/app/shared/models/password.models.ts | 122 ++++++++++++++++++ ui-ngx/src/app/shared/models/public-api.ts | 1 + .../assets/locale/locale.constant-en_US.json | 12 ++ 14 files changed, 458 insertions(+), 113 deletions(-) create mode 100644 ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.html create mode 100644 ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.scss create mode 100644 ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.ts create mode 100644 ui-ngx/src/app/shared/models/password.models.ts diff --git a/ui-ngx/src/app/modules/home/pages/security/security.component.html b/ui-ngx/src/app/modules/home/pages/security/security.component.html index 5b661963c6..4d7af5bfd0 100644 --- a/ui-ngx/src/app/modules/home/pages/security/security.component.html +++ b/ui-ngx/src/app/modules/home/pages/security/security.component.html @@ -40,7 +40,7 @@ profile.current-password - + {{ 'security.password-requirement.incorrect-password-try-again' | translate }} @@ -52,13 +52,13 @@ + && !changePassword.get('newPassword').hasError('passwordSameAsOld')"> {{ 'security.password-requirement.password-not-meet-requirements' | translate }} {{ changePassword.get('newPassword').getError('alreadyUsed') }} - + {{ 'security.password-requirement.password-should-difference' | translate }} @@ -72,7 +72,7 @@ login.new-password-again - + {{ 'security.password-requirement.new-passwords-not-match' | translate }} @@ -134,7 +134,7 @@ diff --git a/ui-ngx/src/app/modules/home/pages/security/security.component.ts b/ui-ngx/src/app/modules/home/pages/security/security.component.ts index 32094d9677..4bad58a764 100644 --- a/ui-ngx/src/app/modules/home/pages/security/security.component.ts +++ b/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; }; } diff --git a/ui-ngx/src/app/modules/login/login-routing.module.ts b/ui-ngx/src/app/modules/login/login-routing.module.ts index 3000d81fb8..da4ac05def 100644 --- a/ui-ngx/src/app/modules/login/login-routing.module.ts +++ b/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 = (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', diff --git a/ui-ngx/src/app/modules/login/login.module.ts b/ui-ngx/src/app/modules/login/login.module.ts index 35dbfad7e2..a7d8731f9d 100644 --- a/ui-ngx/src/app/modules/login/login.module.ts +++ b/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, diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.html b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html index e5b287df16..79b601d099 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html @@ -32,15 +32,28 @@ common.password - + lock + + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} + login.password-again - + lock + + {{ 'security.password-requirement.new-passwords-not-match' | translate }} +
+ + diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts index 4cb8fa7cba..e7aa68cba0 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts +++ b/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, 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, diff --git a/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.html b/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.html new file mode 100644 index 0000000000..f54b8c64ef --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.html @@ -0,0 +1,35 @@ + + +
+ @for (rule of passwordErrorRules; track $index) { + @if (!rule.policyProp || passwordPolicy[rule.policyProp] > 0) { +

+ + {{ checkForError(rule.key) ? 'mdi:close' : 'mdi:check' }} + + {{ rule.translation | translate : passwordPolicy }} +

+ } + } +
+
+
diff --git a/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.scss b/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.scss new file mode 100644 index 0000000000..10ed07030d --- /dev/null +++ b/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; + } +} diff --git a/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.ts b/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.ts new file mode 100644 index 0000000000..9d8c4d9b66 --- /dev/null +++ b/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; + } +} diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html index 5233189902..b4be2ecc9d 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html @@ -35,15 +35,28 @@ login.new-password - + lock + + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} + login.new-password-again lock + + {{ 'security.password-requirement.new-passwords-not-match' | translate }} +
+ + diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts index 202722cf15..4a2c0bc75d 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts +++ b/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, 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, diff --git a/ui-ngx/src/app/shared/models/password.models.ts b/ui-ngx/src/app/shared/models/password.models.ts new file mode 100644 index 0000000000..4bba3d6737 --- /dev/null +++ b/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; + }; +} diff --git a/ui-ngx/src/app/shared/models/public-api.ts b/ui-ngx/src/app/shared/models/public-api.ts index bd7fe18262..5f930ff180 100644 --- a/ui-ngx/src/app/shared/models/public-api.ts +++ b/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'; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index ec3ef79ef4..8f3ef7e3d8 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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", From 8a3d9048b2138d630a3ff622a00d92d72dfc2f1d Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 25 Nov 2025 11:39:12 +0200 Subject: [PATCH 2/5] moved password requirements tooltip to shared module --- ui-ngx/src/app/modules/login/login.module.ts | 4 +--- .../components}/password-requirements-tooltip.component.html | 0 .../components}/password-requirements-tooltip.component.scss | 0 .../components}/password-requirements-tooltip.component.ts | 0 ui-ngx/src/app/shared/shared.module.ts | 3 +++ 5 files changed, 4 insertions(+), 3 deletions(-) rename ui-ngx/src/app/{modules/login/pages/login => shared/components}/password-requirements-tooltip.component.html (100%) rename ui-ngx/src/app/{modules/login/pages/login => shared/components}/password-requirements-tooltip.component.scss (100%) rename ui-ngx/src/app/{modules/login/pages/login => shared/components}/password-requirements-tooltip.component.ts (100%) diff --git a/ui-ngx/src/app/modules/login/login.module.ts b/ui-ngx/src/app/modules/login/login.module.ts index a7d8731f9d..35dbfad7e2 100644 --- a/ui-ngx/src/app/modules/login/login.module.ts +++ b/ui-ngx/src/app/modules/login/login.module.ts @@ -25,7 +25,6 @@ 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: [ @@ -34,8 +33,7 @@ import { PasswordRequirementsTooltipComponent } from '@modules/login/pages/login ResetPasswordComponent, CreatePasswordComponent, TwoFactorAuthLoginComponent, - LinkExpiredComponent, - PasswordRequirementsTooltipComponent + LinkExpiredComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.html b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.html similarity index 100% rename from ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.html rename to ui-ngx/src/app/shared/components/password-requirements-tooltip.component.html diff --git a/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.scss b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.scss similarity index 100% rename from ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.scss rename to ui-ngx/src/app/shared/components/password-requirements-tooltip.component.scss diff --git a/ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.ts b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts similarity index 100% rename from ui-ngx/src/app/modules/login/pages/login/password-requirements-tooltip.component.ts rename to ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 9fd6e4a71e..c69776ef1d 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -228,6 +228,7 @@ import { JsFuncModuleRowComponent } from '@shared/components/js-func-module-row. import { EntityKeyAutocompleteComponent } from '@shared/components/entity/entity-key-autocomplete.component'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { MqttVersionSelectComponent } from '@shared/components/mqtt-version-select.component'; +import { PasswordRequirementsTooltipComponent } from '@shared/components/password-requirements-tooltip.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -443,6 +444,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ScadaSymbolInputComponent, EntityKeyAutocompleteComponent, MqttVersionSelectComponent, + PasswordRequirementsTooltipComponent ], imports: [ CommonModule, @@ -707,6 +709,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ScadaSymbolInputComponent, EntityKeyAutocompleteComponent, MqttVersionSelectComponent, + PasswordRequirementsTooltipComponent ] }) export class SharedModule { } From 7921162d337d82a31a93cc8d6e017f90e670bcee Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 26 Nov 2025 15:52:06 +0200 Subject: [PATCH 3/5] cleanup --- .../login/create-password.component.html | 2 +- .../pages/login/create-password.component.ts | 37 ++++++------------- .../pages/login/reset-password.component.html | 2 +- .../pages/login/reset-password.component.ts | 35 ++++++------------ .../src/app/shared/models/password.models.ts | 24 ++---------- .../assets/locale/locale.constant-en_US.json | 6 --- 6 files changed, 30 insertions(+), 76 deletions(-) diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.html b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html index 79b601d099..e3e43b6d29 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html @@ -42,7 +42,7 @@ formControlName="newPassword"/> lock - + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts index e7aa68cba0..ec8a0efa0c 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts @@ -16,15 +16,11 @@ 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, UntypedFormGroup, Validators } from '@angular/forms'; -import { TranslateService } from '@ngx-translate/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { UserPasswordPolicy } from '@shared/models/settings.models'; -import { combineLatest } from 'rxjs'; import { passwordsMatchValidator, passwordStrengthValidator @@ -37,26 +33,21 @@ import { }) export class CreatePasswordComponent extends PageComponent { - activateToken = ''; - createPassword: UntypedFormGroup; passwordPolicy: UserPasswordPolicy; + createPassword: FormGroup; - constructor(protected store: Store, - private route: ActivatedRoute, + private activateToken: string; + + constructor(private route: ActivatedRoute, private authService: AuthService, - private translate: TranslateService, - private fb: UntypedFormBuilder) { - super(store); + private fb: FormBuilder) { + super(); + + this.activateToken = this.route.snapshot.queryParams['activateToken'] || ''; - combineLatest([ - this.route.queryParams, - this.route.data - ]) + this.route.data .pipe(takeUntilDestroyed()) - .subscribe(([params, data]) => { - this.activateToken = params['activateToken'] || ''; - this.passwordPolicy = data['passwordPolicy']; - }); + .subscribe((data) => this.passwordPolicy = data['passwordPolicy']); this.buildCreatePasswordForm(); } @@ -72,17 +63,13 @@ export class CreatePasswordComponent extends PageComponent { }); } - get passwordErrorsLength(): number { - return Object.keys(this.createPassword.get('newPassword')?.errors ?? {}).length; - } - onCreatePassword() { if (this.createPassword.invalid) { this.createPassword.markAllAsTouched(); } else { this.authService.activate( this.activateToken, - this.createPassword.get('password').value, true).subscribe(); + this.createPassword.get('newPassword').value, true).subscribe(); } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html index b4be2ecc9d..95584f9a75 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.html @@ -45,7 +45,7 @@ formControlName="newPassword"/> lock - + {{ 'security.password-requirement.password-not-meet-requirements' | translate }} diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts index 4a2c0bc75d..bfc8986625 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts @@ -16,13 +16,9 @@ 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, UntypedFormGroup, Validators } from '@angular/forms'; -import { TranslateService } from '@ngx-translate/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { combineLatest } from 'rxjs'; import { UserPasswordPolicy } from '@shared/models/settings.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { @@ -39,25 +35,22 @@ export class ResetPasswordComponent extends PageComponent { isExpiredPassword: boolean; - resetToken = ''; - - resetPassword: UntypedFormGroup; + resetPassword: FormGroup; passwordPolicy: UserPasswordPolicy; - constructor(protected store: Store, - private route: ActivatedRoute, + private resetToken: string; + + constructor(private route: ActivatedRoute, private router: Router, private authService: AuthService, - private translate: TranslateService, - private fb: UntypedFormBuilder) { - super(store); - combineLatest([ - this.route.queryParams, - this.route.data - ]) + private fb: FormBuilder) { + super(); + + this.resetToken = this.route.snapshot.queryParams['resetToken'] || ''; + + this.route.data .pipe(takeUntilDestroyed()) - .subscribe(([params, data]) => { - this.resetToken = params['resetToken'] || ''; + .subscribe((data) => { this.passwordPolicy = data['passwordPolicy']; this.isExpiredPassword = data['expiredPassword'] ?? false; }); @@ -76,10 +69,6 @@ export class ResetPasswordComponent extends PageComponent { }); } - get passwordErrorsLength(): number { - return Object.keys(this.resetPassword.get('newPassword')?.errors ?? {}).length; - } - onResetPassword() { if (this.resetPassword.invalid) { this.resetPassword.markAllAsTouched(); diff --git a/ui-ngx/src/app/shared/models/password.models.ts b/ui-ngx/src/app/shared/models/password.models.ts index 4bba3d6737..dbffb35e92 100644 --- a/ui-ngx/src/app/shared/models/password.models.ts +++ b/ui-ngx/src/app/shared/models/password.models.ts @@ -18,24 +18,14 @@ 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' + notSpecial = 'security.password-requirement.password-tooltip-special-characters', + hasWhitespaces = 'security.password-requirement.password-should-not-contain-spaces' } export const passwordErrorRules = [ @@ -45,6 +35,7 @@ export const passwordErrorRules = [ { key: 'notNumeric', policyProp: 'minimumDigits', translation: TooltipPasswordErrorMessageKey.notNumeric }, { key: 'notSpecial', policyProp: 'minimumSpecialCharacters', translation: TooltipPasswordErrorMessageKey.notSpecial }, { key: 'maxLength', policyProp: 'maximumLength', translation: TooltipPasswordErrorMessageKey.maxLength }, + { key: 'hasWhitespaces', policyProp: 'hasWhitespaces', translation: TooltipPasswordErrorMessageKey.hasWhitespaces }, ]; export const passwordsMatchValidator = (firstControlName: string, secondControlName: string): ValidatorFn =>{ @@ -59,14 +50,7 @@ export const passwordsMatchValidator = (firstControlName: string, secondControlN const newPass = newPassControl.value ?? ''; const confirm = confirmControl.value ?? ''; - const userInteracted = - confirmControl.touched || confirmControl.dirty || group.touched; - - if (!userInteracted) { - return null; - } - - if (newPass && confirm !== newPass) { + if ((newPass || confirm) && confirm !== newPass) { confirmControl.setErrors({ passwordsNotMatch: true }); return { passwordsNotMatch: true }; } else { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 8f3ef7e3d8..ad8d0aa54f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4418,12 +4418,6 @@ "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", From 1d9892c9783ada1823905c4df0e644b066682ab5 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 26 Nov 2025 16:20:51 +0200 Subject: [PATCH 4/5] changed ViewEncapsulation cleanup --- .../modules/login/pages/login/create-password.component.ts | 2 +- .../components/password-requirements-tooltip.component.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts index ec8a0efa0c..6e3dcc0f63 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts @@ -55,7 +55,7 @@ export class CreatePasswordComponent extends PageComponent { private buildCreatePasswordForm() { this.createPassword = this.fb.group({ newPassword: ['', [Validators.required, passwordStrengthValidator(this.passwordPolicy)]], - newPassword2:[''] + newPassword2: [''] }, { validators: [ passwordsMatchValidator('newPassword', 'newPassword2'), diff --git a/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts index 9d8c4d9b66..e279708184 100644 --- a/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts +++ b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Input } from '@angular/core'; +import { Component, Input, ViewEncapsulation } from '@angular/core'; import { CdkOverlayOrigin, ConnectionPositionPair } from '@angular/cdk/overlay'; import { passwordErrorRules } from '@shared/models/password.models'; import { AbstractControl } from '@angular/forms'; @@ -23,7 +23,8 @@ 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' + styleUrl: './password-requirements-tooltip.component.scss', + encapsulation: ViewEncapsulation.None }) export class PasswordRequirementsTooltipComponent { @Input() passwordControl: AbstractControl; From 8da810aa0be1d0f046feb241ec63892c58e5d6ca Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 28 Nov 2025 16:29:09 +0200 Subject: [PATCH 5/5] clean up --- ui-ngx/src/app/core/auth/auth.service.ts | 4 ++-- ui-ngx/src/app/modules/login/login-routing.module.ts | 2 +- .../login/pages/login/create-password.component.ts | 6 +----- .../login/pages/login/reset-password.component.ts | 10 ++-------- .../password-requirements-tooltip.component.ts | 9 ++------- 5 files changed, 8 insertions(+), 23 deletions(-) diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index f8ffd5e8b6..a70a8baa47 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -164,8 +164,8 @@ export class AuthService { )); } - public getUserPasswordPolicy() { - return this.http.get(`/api/noauth/userPasswordPolicy`, defaultHttpOptions()); + public getUserPasswordPolicy(config?: RequestConfig) { + return this.http.get(`/api/noauth/userPasswordPolicy`, defaultHttpOptionsFromConfig(config)); } public activateByEmailCode(emailCode: string): Observable { diff --git a/ui-ngx/src/app/modules/login/login-routing.module.ts b/ui-ngx/src/app/modules/login/login-routing.module.ts index da4ac05def..3608f425a9 100644 --- a/ui-ngx/src/app/modules/login/login-routing.module.ts +++ b/ui-ngx/src/app/modules/login/login-routing.module.ts @@ -34,7 +34,7 @@ const passwordPolicyResolver: ResolveFn = (route: ActivatedR state: RouterStateSnapshot, router = inject(Router), authService = inject(AuthService)) => { - return authService.getUserPasswordPolicy().pipe( + return authService.getUserPasswordPolicy({ignoreErrors: true}).pipe( catchError(() => { return of({} as UserPasswordPolicy); }) diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts index 6e3dcc0f63..417df10b18 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.ts @@ -19,7 +19,6 @@ import { AuthService } from '@core/auth/auth.service'; import { PageComponent } from '@shared/components/page.component'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { UserPasswordPolicy } from '@shared/models/settings.models'; import { passwordsMatchValidator, @@ -44,10 +43,7 @@ export class CreatePasswordComponent extends PageComponent { super(); this.activateToken = this.route.snapshot.queryParams['activateToken'] || ''; - - this.route.data - .pipe(takeUntilDestroyed()) - .subscribe((data) => this.passwordPolicy = data['passwordPolicy']); + this.passwordPolicy = this.route.snapshot.data['passwordPolicy']; this.buildCreatePasswordForm(); } diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts index bfc8986625..7305acb4e6 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts @@ -20,7 +20,6 @@ import { PageComponent } from '@shared/components/page.component'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { UserPasswordPolicy } from '@shared/models/settings.models'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { passwordsMatchValidator, passwordStrengthValidator @@ -47,13 +46,8 @@ export class ResetPasswordComponent extends PageComponent { super(); this.resetToken = this.route.snapshot.queryParams['resetToken'] || ''; - - this.route.data - .pipe(takeUntilDestroyed()) - .subscribe((data) => { - this.passwordPolicy = data['passwordPolicy']; - this.isExpiredPassword = data['expiredPassword'] ?? false; - }); + this.passwordPolicy = this.route.snapshot.data['passwordPolicy']; + this.isExpiredPassword = this.route.snapshot.data['expiredPassword'] ?? false; this.buildResetPasswordForm(); } diff --git a/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts index e279708184..1e2f97dfa7 100644 --- a/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts +++ b/ui-ngx/src/app/shared/components/password-requirements-tooltip.component.ts @@ -19,6 +19,7 @@ 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'; +import { POSITION_MAP } from '@shared/models/overlay.models'; @Component({ selector: 'tb-password-requirements-tooltip', @@ -35,13 +36,7 @@ export class PasswordRequirementsTooltipComponent { isTooltipOpen = false; overlayPositions: ConnectionPositionPair[] = [ - { - originX: 'center', - originY: 'top', - overlayX: 'center', - overlayY: 'bottom', - offsetY: -20 - } + {...POSITION_MAP.top, offsetY: -20} ]; checkForError(errorName: string): boolean {