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;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .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-btn-subtext {
 | 
			
		||||
    .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