UI: move change password with profile to security
This commit is contained in:
parent
6173795580
commit
fedd644516
@ -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());
|
||||
|
||||
@ -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>
|
||||
@ -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 {
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -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} {
|
||||
|
||||
@ -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'
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -1289,7 +1289,6 @@
|
||||
"change-password": "Изменить пароль",
|
||||
"current-password": "Текущий пароль",
|
||||
"copy-jwt-token": "Копировать JWT токен",
|
||||
"valid-till": "Действителен до {{expirationData}}",
|
||||
"tokenCopiedMessage": "JWT токен скопирован в буфер обмена",
|
||||
"tokenCopiedWarnMessage": "JWT токен недействителен! Перезагрузите страницу."
|
||||
},
|
||||
|
||||
@ -1704,7 +1704,6 @@
|
||||
"change-password": "Змінити пароль",
|
||||
"current-password": "Поточний пароль",
|
||||
"copy-jwt-token": "Копіювати JWT токен",
|
||||
"valid-till": "Дійсний до {{expirationData}}",
|
||||
"tokenCopiedMessage": "JWT токен скопійовано в буфер обміну",
|
||||
"tokenCopiedWarnMessage": "JWT токен не є дійсним! Перезавантажте сторінку."
|
||||
},
|
||||
|
||||
@ -2009,7 +2009,6 @@
|
||||
"last-login-time": "最后登录",
|
||||
"profile": "属性",
|
||||
"copy-jwt-token": "复制 JWT 令牌",
|
||||
"valid-till": "有效期至 {{expirationData}}",
|
||||
"tokenCopiedSuccessMessage": "JWT 令牌已复制到剪贴板",
|
||||
"tokenCopiedWarnMessage": "JWT 令牌已过期!请刷新页面。"
|
||||
},
|
||||
|
||||
@ -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…
x
Reference in New Issue
Block a user