UI: move change password with profile to security

This commit is contained in:
Vladyslav_Prykhodko 2022-06-09 18:20:34 +03:00
parent 6173795580
commit fedd644516
16 changed files with 305 additions and 268 deletions

View File

@ -23,7 +23,7 @@ import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models';
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
import { defaultHttpOptions } from '../http/http-utils';
import { defaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from '../http/http-utils';
import { UserService } from '../http/user.service';
import { Store } from '@ngrx/store';
import { AppState } from '../core.state';
@ -47,6 +47,7 @@ import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.com
import { OAuth2ClientInfo, PlatformType } from '@shared/models/oauth2.models';
import { isMobileApp } from '@core/utils';
import { TwoFactorAuthProviderType, TwoFaProviderInfo } from '@shared/models/two-factor-auth.models';
import { UserPasswordPolicy } from '@shared/models/settings.models';
@Injectable({
providedIn: 'root'
@ -163,14 +164,18 @@ export class AuthService {
));
}
public changePassword(currentPassword: string, newPassword: string) {
return this.http.post('/api/auth/changePassword', {currentPassword, newPassword}, defaultHttpOptions()).pipe(
public changePassword(currentPassword: string, newPassword: string, config?: RequestConfig) {
return this.http.post('/api/auth/changePassword', {currentPassword, newPassword}, defaultHttpOptionsFromConfig(config)).pipe(
tap((loginResponse: LoginResponse) => {
this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, false);
}
));
}
public getUserPasswordPolicy() {
return this.http.get<UserPasswordPolicy>(`/api/noauth/userPasswordPolicy`, defaultHttpOptions());
}
public activateByEmailCode(emailCode: string): Observable<LoginResponse> {
return this.http.post<LoginResponse>(`/api/noauth/activateByEmailCode?emailCode=${emailCode}`,
null, defaultHttpOptions());

View File

@ -1,64 +0,0 @@
<!--
Copyright © 2016-2022 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.
-->
<form [formGroup]="changePassword" (ngSubmit)="onChangePassword()">
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>profile.change-password</h2>
<span fxFlex></span>
<button mat-icon-button
[mat-dialog-close]="false"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-form-field class="mat-block">
<mat-label translate>profile.current-password</mat-label>
<input matInput type="password" formControlName="currentPassword"/>
<mat-icon class="material-icons" matPrefix>lock</mat-icon>
<tb-toggle-password matSuffix></tb-toggle-password>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>login.new-password</mat-label>
<input matInput type="password" formControlName="newPassword"/>
<mat-icon class="material-icons" matPrefix>lock</mat-icon>
<tb-toggle-password matSuffix></tb-toggle-password>
</mat-form-field>
<mat-form-field class="mat-block">
<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-form-field>
</div>
<div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
[mat-dialog-close]="false" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || changePassword.invalid">
{{ 'profile.change-password' | translate }}
</button>
</div>
</form>

View File

@ -1,17 +0,0 @@
/**
* Copyright © 2016-2022 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.
*/
:host {
}

View File

@ -1,70 +0,0 @@
///
/// Copyright © 2016-2022 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, OnInit } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup } from '@angular/forms';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { AuthService } from '@core/auth/auth.service';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
@Component({
selector: 'tb-change-password-dialog',
templateUrl: './change-password-dialog.component.html',
styleUrls: ['./change-password-dialog.component.scss']
})
export class ChangePasswordDialogComponent extends DialogComponent<ChangePasswordDialogComponent> implements OnInit {
changePassword: FormGroup;
constructor(protected store: Store<AppState>,
protected router: Router,
private translate: TranslateService,
private authService: AuthService,
public dialogRef: MatDialogRef<ChangePasswordDialogComponent>,
public fb: FormBuilder) {
super(store, router, dialogRef);
}
ngOnInit(): void {
this.buildChangePasswordForm();
}
buildChangePasswordForm() {
this.changePassword = this.fb.group({
currentPassword: [''],
newPassword: [''],
newPassword2: ['']
});
}
onChangePassword(): void {
if (this.changePassword.get('newPassword').value !== this.changePassword.get('newPassword2').value) {
this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.passwords-mismatch-error'),
type: 'error' }));
} else {
this.authService.changePassword(
this.changePassword.get('currentPassword').value,
this.changePassword.get('newPassword').value).subscribe(() => {
this.dialogRef.close(true);
});
}
}
}

View File

@ -78,24 +78,6 @@
{{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
</mat-checkbox>
</section>
<div fxLayout="row" fxLayoutGap="16px" style="padding-bottom: 16px; margin-top: 20px;">
<div>
<button mat-button mat-raised-button color="primary"
type="button"
[disabled]="(isLoading$ | async)" (click)="changePassword()">
{{'profile.change-password' | translate}}
</button>
</div>
<div>
<button mat-raised-button
type="button"
(click)="copyToken()">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
<span>{{ 'profile.copy-jwt-token' | translate }}</span>
</button>
<div class="profile-btn-subtext">{{ expirationJwtData }}</div>
</div>
</div>
<div fxLayout="row" fxLayoutAlign="end start">
<button mat-button mat-raised-button color="primary"
type="submit"

View File

@ -38,12 +38,6 @@
font-size: 16px;
font-weight: 400;
}
.profile-btn-subtext {
font: 400 14px / 16px Roboto, "Helvetica Neue", sans-serif;
letter-spacing: 0.25px;
opacity: 0.6;
padding: 8px 0;
}
.tb-home-dashboard {
tb-dashboard-autocomplete {
@media #{$mat-gt-sm} {

View File

@ -27,16 +27,9 @@ import { ActionAuthUpdateUserDetails } from '@core/auth/auth.actions';
import { environment as env } from '@env/environment';
import { TranslateService } from '@ngx-translate/core';
import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions';
import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { AuthService } from '@core/auth/auth.service';
import { ActivatedRoute } from '@angular/router';
import { isDefinedAndNotNull } from '@core/utils';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DatePipe } from '@angular/common';
import { ClipboardService } from 'ngx-clipboard';
@Component({
selector: 'tb-profile',
@ -51,29 +44,11 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
languageList = env.supportedLangs;
private readonly authUser: AuthUser;
get jwtToken(): string {
return `Bearer ${localStorage.getItem('jwt_token')}`;
}
get jwtTokenExpiration(): string {
return localStorage.getItem('jwt_token_expiration');
}
get expirationJwtData(): string {
const expirationData = this.datePipe.transform(this.jwtTokenExpiration, 'yyyy-MM-dd HH:mm:ss');
return this.translate.instant('profile.valid-till', { expirationData });
}
constructor(protected store: Store<AppState>,
private route: ActivatedRoute,
private userService: UserService,
private authService: AuthService,
private translate: TranslateService,
public dialog: MatDialog,
public dialogService: DialogService,
public fb: FormBuilder,
private datePipe: DatePipe,
private clipboardService: ClipboardService) {
public fb: FormBuilder) {
super(store);
this.authUser = getCurrentAuthUser(this.store);
}
@ -121,13 +96,6 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
);
}
changePassword(): void {
this.dialog.open(ChangePasswordDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
});
}
private userLoaded(user: User) {
this.user = user;
this.profile.reset(user);
@ -158,25 +126,4 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
isSysAdmin(): boolean {
return this.authUser.authority === Authority.SYS_ADMIN;
}
copyToken() {
if (+this.jwtTokenExpiration < Date.now()) {
this.store.dispatch(new ActionNotificationShow({
message: this.translate.instant('profile.tokenCopiedWarnMessage'),
type: 'warn',
duration: 1500,
verticalPosition: 'bottom',
horizontalPosition: 'right'
}));
} else {
this.clipboardService.copyFromContent(this.jwtToken);
this.store.dispatch(new ActionNotificationShow({
message: this.translate.instant('profile.tokenCopiedSuccessMessage'),
type: 'success',
duration: 750,
verticalPosition: 'bottom',
horizontalPosition: 'right'
}));
}
}
}

View File

@ -19,12 +19,10 @@ import { CommonModule } from '@angular/common';
import { ProfileComponent } from './profile.component';
import { SharedModule } from '@shared/shared.module';
import { ProfileRoutingModule } from './profile-routing.module';
import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component';
@NgModule({
declarations: [
ProfileComponent,
ChangePasswordDialogComponent
ProfileComponent
],
imports: [
CommonModule,

View File

@ -17,34 +17,117 @@
-->
<div class="profile-container" fxLayout="column" fxLayoutGap="8px">
<mat-card class="profile-card" fxLayout="column">
<mat-card-title>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.xs="8px"
fxLayoutAlign="space-between start" fxLayoutAlign.xs="start start">
<div fxFlex class="mat-headline" translate>
security.security
</div>
<div fxLayout="column">
<span class="mat-subheader" translate>profile.last-login-time</span>
<span class="profile-last-login-ts" style='opacity: 0.7;'>{{ user?.additionalInfo?.lastLoginTs | date:'yyyy-MM-dd HH:mm:ss' }}</span>
</div>
</div>
<mat-card-title style="margin-bottom: 8px;">
<span class="mat-headline card-title" translate>profile.jwt-token</span>
</mat-card-title>
<mat-card-content>
<div>
<button mat-stroked-button
<div fxLayout="row" fxLayoutAlign="space-between center">
<div class="token-text">{{ 'profile.token-valid-till' | translate }} <span class="date">{{ jwtTokenExpiration | date: 'yyyy-MM-dd HH:mm:ss' }}</span></div>
<button mat-raised-button
color="primary"
type="button"
(click)="copyToken()">
<mat-icon class="material-icons">add_circle_outline</mat-icon>
<span>{{ 'profile.copy-jwt-token' | translate }}</span>
</button>
<div class="profile-btn-subtext">{{ expirationJwtData }}</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="profile-card" fxLayout="column">
<mat-card-content class="change-password" tb-toast toastTarget="changePassword">
<form #changePasswordForm="ngForm" [formGroup]="changePassword" (ngSubmit)="onChangePassword(changePasswordForm)">
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap="25px" fxLayoutGap.xs="0">
<div fxFlex="290px" fxFlex.sm="250px" fxFlex.xs="100">
<h3 class="card-title" translate>profile.change-password</h3>
<mat-form-field class="mat-block same-color" hideRequiredMarker appearance="fill" color="primary">
<mat-label translate>profile.current-password</mat-label>
<input matInput type="password" name="current-password" formControlName="currentPassword" autocomplete="current-password" required/>
<tb-toggle-password [fxShow]="changePassword.get('currentPassword').dirty || changePassword.get('currentPassword').touched" matSuffix></tb-toggle-password>
<mat-error *ngIf="changePassword.get('currentPassword').hasError('differencePassword')">
{{ 'security.password-requirement.incorrect-password-try-again' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block same-color" hideRequiredMarker appearance="fill" color="primary">
<mat-label translate>login.new-password</mat-label>
<input matInput type="password" name="new-password" formControlName="newPassword" autocomplete="new-password" required/>
<tb-toggle-password [fxShow]="changePassword.get('newPassword').dirty || changePassword.get('newPassword').touched" matSuffix></tb-toggle-password>
<mat-error *ngIf="changePassword.get('newPassword').errors
&& !changePassword.get('newPassword').hasError('alreadyUsed')
&& !changePassword.get('newPassword').hasError('hasWhitespaces')
&& !changePassword.get('newPassword').hasError('samePassword')">
{{ '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')">
{{ 'security.password-requirement.password-should-difference' | translate }}
</mat-error>
<mat-error *ngIf="changePassword.get('newPassword').hasError('hasWhitespaces')">
{{ 'security.password-requirement.password-should-not-contain-spaces' | translate }}
</mat-error>
</mat-form-field>
<div fxFlex fxHide fxShow.xs fxLayoutAlign="start center">
<ng-container *ngTemplateOutlet="passwordRequirements"></ng-container>
</div>
<mat-form-field class="mat-block same-color" hideRequiredMarker appearance="fill" color="primary">
<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 [fxShow]="changePassword.get('newPassword2').dirty || changePassword.get('newPassword2').touched" matSuffix></tb-toggle-password>
<mat-error *ngIf="changePassword.get('newPassword2').hasError('differencePassword')">
{{ 'security.password-requirement.new-passwords-not-match' | translate }}
</mat-error>
</mat-form-field>
</div>
<mat-divider [vertical]="true"></mat-divider>
<div fxFlex fxHide.xs fxLayoutAlign="start start">
<ng-container *ngTemplateOutlet="passwordRequirements"></ng-container>
</div>
</div>
<ng-template #passwordRequirements>
<div class="password-requirements" *ngIf="passwordPolicy">
<h3 class="card-title" translate>security.password-requirement.password-requirements</h3>
<h4 class="mat-h4" translate>security.password-requirement.at-least</h4>
<p class="mat-body" *ngIf="passwordPolicy.minimumUppercaseLetters > 0">
<mat-icon class="tb-mat-20" [svgIcon]="changePassword.get('newPassword').hasError('notUpperCase') ? 'mdi:circle-small' : 'mdi:check'"></mat-icon>
{{ 'security.password-requirement.uppercase-letter' | translate : {count: passwordPolicy.minimumUppercaseLetters} }}
</p>
<p class="mat-body" *ngIf="passwordPolicy.minimumLowercaseLetters > 0">
<mat-icon class="tb-mat-20" [svgIcon]="changePassword.get('newPassword').hasError('notLowerCase') ? 'mdi:circle-small' : 'mdi:check'"></mat-icon>
{{ 'security.password-requirement.lowercase-letter' | translate : {count: passwordPolicy.minimumLowercaseLetters} }}
</p>
<p class="mat-body" *ngIf="passwordPolicy.minimumDigits > 0">
<mat-icon class="tb-mat-20" [svgIcon]="changePassword.get('newPassword').hasError('notNumeric') ? 'mdi:circle-small' : 'mdi:check'"></mat-icon>
{{ 'security.password-requirement.digit' | translate : {count: passwordPolicy.minimumDigits} }}
</p>
<p class="mat-body" *ngIf="passwordPolicy.minimumSpecialCharacters > 0">
<mat-icon class="tb-mat-20" [svgIcon]="changePassword.get('newPassword').hasError('notSpecial') ? 'mdi:circle-small' : 'mdi:check'"></mat-icon>
{{ 'security.password-requirement.special-character' | translate : {count: passwordPolicy.minimumSpecialCharacters} }}
</p>
<p class="mat-body" *ngIf="passwordPolicy.minimumLength > 0">
<mat-icon class="tb-mat-20" [svgIcon]="changePassword.get('newPassword').hasError('minLength') ? 'mdi:circle-small' : 'mdi:check'"></mat-icon>
{{ 'security.password-requirement.character' | translate : {count: passwordPolicy.minimumLength} }}
</p>
</div>
</ng-template>
<div fxLayout="row" fxLayoutGap="8px" style="margin-top: 18px;" [fxShow]="changePassword.dirty || changePassword.touched">
<button mat-button color="primary"
type="button"
(click)="discardChanges(changePasswordForm, $event)"
[disabled]="(isLoading$ | async)">
{{ 'action.discard-changes' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async)">
{{ 'profile.change-password' | translate }}
</button>
</div>
</form>
</mat-card-content>
</mat-card>
<mat-card class="profile-card" *ngIf="allowTwoFactorProviders.length">
<mat-card-title style="margin-bottom: 20px;">
<span class="mat-headline" translate>admin.2fa.2fa</span>
<span class="mat-headline card-title" translate>admin.2fa.2fa</span>
</mat-card-title>
<mat-card-subtitle style="margin-bottom: 40px;">
<div class="mat-body-1 description" translate>security.2fa.2fa-description</div>

View File

@ -21,6 +21,7 @@
}
mat-card.profile-card {
padding: 24px;
@media #{$mat-gt-sm} {
width: 70%;
}
@ -31,23 +32,54 @@
width: 45%;
}
.mat-subheader {
line-height: 24px;
color: rgba(0, 0, 0, 0.54);
.card-title {
font: 500 18px / 24px Roboto, "Helvetica Neue", sans-serif;
letter-spacing: 0.15px;
margin-top: 0;
}
.mat-h4 {
font-weight: 500;
font-size: 14px;
font-weight: 400;
letter-spacing: 0.25px;
margin: 0 0 4px;
}
.profile-last-login-ts {
font-size: 16px;
font-weight: 400;
.change-password {
margin: 0;
.mat-divider.mat-divider-vertical {
margin-bottom: 25px;
}
.profile-btn-subtext {
.mat-form-field {
margin-bottom: 4px;
}
.password-requirements > p {
margin: 0 0 8px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.mat-icon[data-mat-icon-name="check"] {
color: #24A148;
}
}
.auth-title {
font-weight: 500;
margin: 0;
}
.token-text {
font: 400 14px / 16px Roboto, "Helvetica Neue", sans-serif;
letter-spacing: 0.25px;
opacity: 0.6;
padding: 8px 0;
> .date {
opacity: .7;
}
}
}
@ -91,3 +123,8 @@
}
}
}
:host ::ng-deep {
.mat-form-field-appearance-fill .mat-form-field-underline::before {
background-color: transparent;
}
}

View File

@ -19,7 +19,15 @@ import { User } from '@shared/models/user.model';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup } from '@angular/forms';
import {
AbstractControl,
FormBuilder,
FormGroup, FormGroupDirective,
NgForm,
ValidationErrors,
ValidatorFn,
Validators
} from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
@ -39,7 +47,9 @@ 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 } from '@core/utils';
import { isDefinedAndNotNull, isEqual } from '@core/utils';
import { AuthService } from '@core/auth/auth.service';
import { UserPasswordPolicy } from '@shared/models/settings.models';
@Component({
selector: 'tb-security',
@ -52,7 +62,11 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
private accountConfig: AccountTwoFaSettings;
twoFactorAuth: FormGroup;
changePassword: FormGroup;
user: User;
passwordPolicy: UserPasswordPolicy;
allowTwoFactorProviders: TwoFactorAuthProviderType[] = [];
providersData = twoFactorAuthProvidersData;
twoFactorAuthProviderType = TwoFactorAuthProviderType;
@ -80,6 +94,7 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
public dialogService: DialogService,
public fb: FormBuilder,
private datePipe: DatePipe,
private authService: AuthService,
private clipboardService: ClipboardService) {
super(store);
}
@ -88,6 +103,8 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
this.buildTwoFactorForm();
this.user = this.route.snapshot.data.user;
this.twoFactorLoad(this.route.snapshot.data.providers);
this.buildChangePasswordForm();
this.loadPasswordPolicy();
}
ngOnDestroy() {
@ -142,6 +159,75 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
});
}
private buildChangePasswordForm() {
this.changePassword = this.fb.group({
currentPassword: [''],
newPassword: ['', Validators.required],
newPassword2: ['', this.samePasswordValidation(false, 'newPassword')]
});
}
private loadPasswordPolicy() {
this.authService.getUserPasswordPolicy().subscribe(policy => {
this.passwordPolicy = policy;
this.changePassword.get('newPassword').setValidators([
this.passwordStrengthValidator(),
this.samePasswordValidation(true, 'currentPassword'),
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;
}
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];
if (isSame) {
return value === keyValue ? {samePassword: true} : null;
}
return value !== keyValue ? {differencePassword: true} : null;
};
}
trackByProvider(i: number, provider: TwoFactorAuthProviderType) {
return provider;
}
@ -256,4 +342,41 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
}
return info;
}
onChangePassword(form: FormGroupDirective): void {
if (this.changePassword.valid) {
this.authService.changePassword(this.changePassword.get('currentPassword').value,
this.changePassword.get('newPassword').value, {ignoreErrors: true}).subscribe(() => {
this.discardChanges(form);
},
(error) => {
if (error.status === 400 && error.error.message === 'Current password doesn\'t match!') {
this.changePassword.get('currentPassword').setErrors({differencePassword: true});
} else if (error.status === 400 && error.error.message.startsWith('Password must')) {
this.loadPasswordPolicy();
} else if (error.status === 400 && error.error.message.startsWith('Password was already used')) {
this.changePassword.get('newPassword').setErrors({alreadyUsed: error.error.message});
} else {
this.store.dispatch(new ActionNotificationShow({
message: error.error.message,
type: 'error',
target: 'changePassword'
}));
}
});
} else {
this.changePassword.markAllAsTouched();
}
}
discardChanges(form: FormGroupDirective, event?: MouseEvent) {
if (event) {
event.stopPropagation();
}
form.resetForm({
currentPassword: '',
newPassword: '',
newPassword2: ''
});
}
}

View File

@ -2498,7 +2498,7 @@
"password-reset": "Password reset",
"expired-password-reset-message": "Your credentials has been expired! Please create new password.",
"new-password": "New password",
"new-password-again": "New password again",
"new-password-again": "Confirm new password",
"password-link-sent-message": "Reset link has been sent",
"email": "Email",
"login-with": "Login with {{name}}",
@ -2601,12 +2601,13 @@
"change-password": "Change Password",
"current-password": "Current password",
"copy-jwt-token": "Copy JWT token",
"valid-till": "Valid till {{expirationData}}",
"jwt-token": "JWT token",
"token-valid-till": "Token is valid till",
"tokenCopiedSuccessMessage": "JWT token has been copied to clipboard",
"tokenCopiedWarnMessage": "JWT token is expired! Please, refresh the page."
},
"security": {
"security": "Security",
"security": "Password and authentication",
"2fa": {
"2fa": "Two-factor authentication",
"2fa-description": "Two-factor authentication protects your account from unauthorized access. All you have to do is enter a security code when you log in.",
@ -2660,6 +2661,20 @@
"backup-code-description": "These printable one-time passcodes allow you to sign in when away from your phone, like when youre traveling.",
"backup-code-hint": "{{ info }} single-use codes are active at this time"
}
},
"password-requirement": {
"at-least": "At least:",
"character": "{ count, plural, 1 {1 character} other {# characters} }",
"digit": "{ count, plural, 1 {1 digit} other {# digits} }",
"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",
"password-should-not-contain-spaces": "Your password should not contain spaces",
"password-not-meet-requirements": "Password didn't meet requirements",
"password-requirements": "Password requirements",
"password-should-difference": "New password should be different from current",
"special-character": "{ count, plural, 1 {1 special character} other {# special characters} }",
"uppercase-letter": "{ count, plural, 1 {1 uppercase letter} other {# uppercase letters} }"
}
},
"relation": {

View File

@ -1289,7 +1289,6 @@
"change-password": "Изменить пароль",
"current-password": "Текущий пароль",
"copy-jwt-token": "Копировать JWT токен",
"valid-till": "Действителен до {{expirationData}}",
"tokenCopiedMessage": "JWT токен скопирован в буфер обмена",
"tokenCopiedWarnMessage": "JWT токен недействителен! Перезагрузите страницу."
},

View File

@ -1704,7 +1704,6 @@
"change-password": "Змінити пароль",
"current-password": "Поточний пароль",
"copy-jwt-token": "Копіювати JWT токен",
"valid-till": "Дійсний до {{expirationData}}",
"tokenCopiedMessage": "JWT токен скопійовано в буфер обміну",
"tokenCopiedWarnMessage": "JWT токен не є дійсним! Перезавантажте сторінку."
},

View File

@ -2009,7 +2009,6 @@
"last-login-time": "最后登录",
"profile": "属性",
"copy-jwt-token": "复制 JWT 令牌",
"valid-till": "有效期至 {{expirationData}}",
"tokenCopiedSuccessMessage": "JWT 令牌已复制到剪贴板",
"tokenCopiedWarnMessage": "JWT 令牌已过期!请刷新页面。"
},

View File

@ -221,6 +221,7 @@ $tb-dark-theme: get-tb-dark-theme(
@mixin tb-components-theme($theme) {
$primary: map-get($theme, primary);
$warn: map-get($theme, warn);
mat-toolbar{
&.mat-hue-3 {
@ -233,6 +234,12 @@ $tb-dark-theme: get-tb-dark-theme(
div.tb-dashboard-page.mobile-app {
@include mat-fab-toolbar-inverse-theme($tb-theme);
}
.same-color.mat-form-field-invalid {
.mat-form-field-suffix {
color: mat.get-color-from-palette($warn, text);
}
}
}
.tb-default {