committed by
GitHub
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