Browse Source

UI: move change password with profile to security

pull/6675/head
Vladyslav_Prykhodko 4 years ago
parent
commit
fedd644516
  1. 11
      ui-ngx/src/app/core/auth/auth.service.ts
  2. 64
      ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.html
  3. 17
      ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.scss
  4. 70
      ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.ts
  5. 18
      ui-ngx/src/app/modules/home/pages/profile/profile.component.html
  6. 6
      ui-ngx/src/app/modules/home/pages/profile/profile.component.scss
  7. 55
      ui-ngx/src/app/modules/home/pages/profile/profile.component.ts
  8. 4
      ui-ngx/src/app/modules/home/pages/profile/profile.module.ts
  9. 115
      ui-ngx/src/app/modules/home/pages/security/security.component.html
  10. 55
      ui-ngx/src/app/modules/home/pages/security/security.component.scss
  11. 127
      ui-ngx/src/app/modules/home/pages/security/security.component.ts
  12. 21
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  13. 1
      ui-ngx/src/assets/locale/locale.constant-ru_RU.json
  14. 1
      ui-ngx/src/assets/locale/locale.constant-uk_UA.json
  15. 1
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json
  16. 7
      ui-ngx/src/theme.scss

11
ui-ngx/src/app/core/auth/auth.service.ts

@ -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());

64
ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.html

@ -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>

17
ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.scss

@ -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 {
}

70
ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.ts

@ -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);
});
}
}
}

18
ui-ngx/src/app/modules/home/pages/profile/profile.component.html

@ -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"

6
ui-ngx/src/app/modules/home/pages/profile/profile.component.scss

@ -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} {

55
ui-ngx/src/app/modules/home/pages/profile/profile.component.ts

@ -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'
}));
}
}
}

4
ui-ngx/src/app/modules/home/pages/profile/profile.module.ts

@ -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,

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

@ -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>

55
ui-ngx/src/app/modules/home/pages/security/security.component.scss

@ -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;
}
.change-password {
margin: 0;
.mat-divider.mat-divider-vertical {
margin-bottom: 25px;
}
.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;
}
}
.profile-last-login-ts {
font-size: 16px;
font-weight: 400;
.auth-title {
font-weight: 500;
margin: 0;
}
.profile-btn-subtext {
.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;
}
}

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

@ -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: ''
});
}
}

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

@ -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 you’re 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": {

1
ui-ngx/src/assets/locale/locale.constant-ru_RU.json

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

1
ui-ngx/src/assets/locale/locale.constant-uk_UA.json

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

1
ui-ngx/src/assets/locale/locale.constant-zh_CN.json

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

7
ui-ngx/src/theme.scss

@ -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 {

Loading…
Cancel
Save