Browse Source
# Conflicts: # ui-ngx/src/app/modules/home/pages/security/security.component.ts # ui-ngx/src/app/modules/login/login-routing.module.ts # ui-ngx/src/app/shared/shared.module.tspull/14449/head^2
15 changed files with 417 additions and 135 deletions
@ -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> |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
///
|
||||
|
/// 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, ViewEncapsulation } 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'; |
||||
|
import { POSITION_MAP } from '@shared/models/overlay.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-password-requirements-tooltip', |
||||
|
templateUrl: './password-requirements-tooltip.component.html', |
||||
|
styleUrl: './password-requirements-tooltip.component.scss', |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class PasswordRequirementsTooltipComponent { |
||||
|
@Input() passwordControl: AbstractControl; |
||||
|
@Input() passwordPolicy: UserPasswordPolicy; |
||||
|
@Input() trigger: CdkOverlayOrigin; |
||||
|
|
||||
|
passwordErrorRules = passwordErrorRules; |
||||
|
isTooltipOpen = false; |
||||
|
|
||||
|
overlayPositions: ConnectionPositionPair[] = [ |
||||
|
{...POSITION_MAP.top, offsetY: -20} |
||||
|
]; |
||||
|
|
||||
|
checkForError(errorName: string): boolean { |
||||
|
return this.passwordControl?.hasError(errorName) ?? false; |
||||
|
} |
||||
|
|
||||
|
onFocus(): void { |
||||
|
this.isTooltipOpen = true; |
||||
|
} |
||||
|
|
||||
|
onBlur(): void { |
||||
|
this.isTooltipOpen = false; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,106 @@ |
|||||
|
///
|
||||
|
/// 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 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', |
||||
|
hasWhitespaces = 'security.password-requirement.password-should-not-contain-spaces' |
||||
|
} |
||||
|
|
||||
|
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 }, |
||||
|
{ key: 'hasWhitespaces', policyProp: 'hasWhitespaces', translation: TooltipPasswordErrorMessageKey.hasWhitespaces }, |
||||
|
]; |
||||
|
|
||||
|
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 ?? ''; |
||||
|
|
||||
|
if ((newPass || confirm) && 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; |
||||
|
}; |
||||
|
} |
||||
Loading…
Reference in new issue