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 { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models';
|
||||||
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
|
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 { UserService } from '../http/user.service';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppState } from '../core.state';
|
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 { OAuth2ClientInfo, PlatformType } from '@shared/models/oauth2.models';
|
||||||
import { isMobileApp } from '@core/utils';
|
import { isMobileApp } from '@core/utils';
|
||||||
import { TwoFactorAuthProviderType, TwoFaProviderInfo } from '@shared/models/two-factor-auth.models';
|
import { TwoFactorAuthProviderType, TwoFaProviderInfo } from '@shared/models/two-factor-auth.models';
|
||||||
|
import { UserPasswordPolicy } from '@shared/models/settings.models';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -163,14 +164,18 @@ export class AuthService {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public changePassword(currentPassword: string, newPassword: string) {
|
public changePassword(currentPassword: string, newPassword: string, config?: RequestConfig) {
|
||||||
return this.http.post('/api/auth/changePassword', {currentPassword, newPassword}, defaultHttpOptions()).pipe(
|
return this.http.post('/api/auth/changePassword', {currentPassword, newPassword}, defaultHttpOptionsFromConfig(config)).pipe(
|
||||||
tap((loginResponse: LoginResponse) => {
|
tap((loginResponse: LoginResponse) => {
|
||||||
this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, false);
|
this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, false);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getUserPasswordPolicy() {
|
||||||
|
return this.http.get<UserPasswordPolicy>(`/api/noauth/userPasswordPolicy`, defaultHttpOptions());
|
||||||
|
}
|
||||||
|
|
||||||
public activateByEmailCode(emailCode: string): Observable<LoginResponse> {
|
public activateByEmailCode(emailCode: string): Observable<LoginResponse> {
|
||||||
return this.http.post<LoginResponse>(`/api/noauth/activateByEmailCode?emailCode=${emailCode}`,
|
return this.http.post<LoginResponse>(`/api/noauth/activateByEmailCode?emailCode=${emailCode}`,
|
||||||
null, defaultHttpOptions());
|
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 }}
|
{{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
</section>
|
</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">
|
<div fxLayout="row" fxLayoutAlign="end start">
|
||||||
<button mat-button mat-raised-button color="primary"
|
<button mat-button mat-raised-button color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@ -38,12 +38,6 @@
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 400;
|
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-home-dashboard {
|
||||||
tb-dashboard-autocomplete {
|
tb-dashboard-autocomplete {
|
||||||
@media #{$mat-gt-sm} {
|
@media #{$mat-gt-sm} {
|
||||||
|
|||||||
@ -27,16 +27,9 @@ import { ActionAuthUpdateUserDetails } from '@core/auth/auth.actions';
|
|||||||
import { environment as env } from '@env/environment';
|
import { environment as env } from '@env/environment';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions';
|
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 { ActivatedRoute } from '@angular/router';
|
||||||
import { isDefinedAndNotNull } from '@core/utils';
|
import { isDefinedAndNotNull } from '@core/utils';
|
||||||
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
|
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({
|
@Component({
|
||||||
selector: 'tb-profile',
|
selector: 'tb-profile',
|
||||||
@ -51,29 +44,11 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
|
|||||||
languageList = env.supportedLangs;
|
languageList = env.supportedLangs;
|
||||||
private readonly authUser: AuthUser;
|
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>,
|
constructor(protected store: Store<AppState>,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
private authService: AuthService,
|
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
public dialog: MatDialog,
|
public fb: FormBuilder) {
|
||||||
public dialogService: DialogService,
|
|
||||||
public fb: FormBuilder,
|
|
||||||
private datePipe: DatePipe,
|
|
||||||
private clipboardService: ClipboardService) {
|
|
||||||
super(store);
|
super(store);
|
||||||
this.authUser = getCurrentAuthUser(this.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) {
|
private userLoaded(user: User) {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.profile.reset(user);
|
this.profile.reset(user);
|
||||||
@ -158,25 +126,4 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
|
|||||||
isSysAdmin(): boolean {
|
isSysAdmin(): boolean {
|
||||||
return this.authUser.authority === Authority.SYS_ADMIN;
|
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 { ProfileComponent } from './profile.component';
|
||||||
import { SharedModule } from '@shared/shared.module';
|
import { SharedModule } from '@shared/shared.module';
|
||||||
import { ProfileRoutingModule } from './profile-routing.module';
|
import { ProfileRoutingModule } from './profile-routing.module';
|
||||||
import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component';
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
ProfileComponent,
|
ProfileComponent
|
||||||
ChangePasswordDialogComponent
|
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
|||||||
@ -17,34 +17,117 @@
|
|||||||
-->
|
-->
|
||||||
<div class="profile-container" fxLayout="column" fxLayoutGap="8px">
|
<div class="profile-container" fxLayout="column" fxLayoutGap="8px">
|
||||||
<mat-card class="profile-card" fxLayout="column">
|
<mat-card class="profile-card" fxLayout="column">
|
||||||
<mat-card-title>
|
<mat-card-title style="margin-bottom: 8px;">
|
||||||
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.xs="8px"
|
<span class="mat-headline card-title" translate>profile.jwt-token</span>
|
||||||
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>
|
</mat-card-title>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<div>
|
<div fxLayout="row" fxLayoutAlign="space-between center">
|
||||||
<button mat-stroked-button
|
<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"
|
color="primary"
|
||||||
type="button"
|
type="button"
|
||||||
(click)="copyToken()">
|
(click)="copyToken()">
|
||||||
<mat-icon class="material-icons">add_circle_outline</mat-icon>
|
|
||||||
<span>{{ 'profile.copy-jwt-token' | translate }}</span>
|
<span>{{ 'profile.copy-jwt-token' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="profile-btn-subtext">{{ expirationJwtData }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</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 class="profile-card" *ngIf="allowTwoFactorProviders.length">
|
||||||
<mat-card-title style="margin-bottom: 20px;">
|
<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-title>
|
||||||
<mat-card-subtitle style="margin-bottom: 40px;">
|
<mat-card-subtitle style="margin-bottom: 40px;">
|
||||||
<div class="mat-body-1 description" translate>security.2fa.2fa-description</div>
|
<div class="mat-body-1 description" translate>security.2fa.2fa-description</div>
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
mat-card.profile-card {
|
mat-card.profile-card {
|
||||||
|
padding: 24px;
|
||||||
@media #{$mat-gt-sm} {
|
@media #{$mat-gt-sm} {
|
||||||
width: 70%;
|
width: 70%;
|
||||||
}
|
}
|
||||||
@ -31,23 +32,54 @@
|
|||||||
width: 45%;
|
width: 45%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-subheader {
|
.card-title {
|
||||||
line-height: 24px;
|
font: 500 18px / 24px Roboto, "Helvetica Neue", sans-serif;
|
||||||
color: rgba(0, 0, 0, 0.54);
|
letter-spacing: 0.15px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-h4 {
|
||||||
|
font-weight: 500;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 400;
|
letter-spacing: 0.25px;
|
||||||
|
margin: 0 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-last-login-ts {
|
.change-password {
|
||||||
font-size: 16px;
|
margin: 0;
|
||||||
font-weight: 400;
|
|
||||||
|
.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;
|
font: 400 14px / 16px Roboto, "Helvetica Neue", sans-serif;
|
||||||
letter-spacing: 0.25px;
|
letter-spacing: 0.25px;
|
||||||
opacity: 0.6;
|
|
||||||
padding: 8px 0;
|
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 { PageComponent } from '@shared/components/page.component';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { AppState } from '@core/core.state';
|
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 { TranslateService } from '@ngx-translate/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { DialogService } from '@core/services/dialog.service';
|
import { DialogService } from '@core/services/dialog.service';
|
||||||
@ -39,7 +47,9 @@ import {
|
|||||||
import { authenticationDialogMap } from '@home/pages/security/authentication-dialog/authentication-dialog.map';
|
import { authenticationDialogMap } from '@home/pages/security/authentication-dialog/authentication-dialog.map';
|
||||||
import { takeUntil, tap } from 'rxjs/operators';
|
import { takeUntil, tap } from 'rxjs/operators';
|
||||||
import { Observable, of, Subject } from 'rxjs';
|
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({
|
@Component({
|
||||||
selector: 'tb-security',
|
selector: 'tb-security',
|
||||||
@ -52,7 +62,11 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
|
|||||||
private accountConfig: AccountTwoFaSettings;
|
private accountConfig: AccountTwoFaSettings;
|
||||||
|
|
||||||
twoFactorAuth: FormGroup;
|
twoFactorAuth: FormGroup;
|
||||||
|
changePassword: FormGroup;
|
||||||
|
|
||||||
user: User;
|
user: User;
|
||||||
|
passwordPolicy: UserPasswordPolicy;
|
||||||
|
|
||||||
allowTwoFactorProviders: TwoFactorAuthProviderType[] = [];
|
allowTwoFactorProviders: TwoFactorAuthProviderType[] = [];
|
||||||
providersData = twoFactorAuthProvidersData;
|
providersData = twoFactorAuthProvidersData;
|
||||||
twoFactorAuthProviderType = TwoFactorAuthProviderType;
|
twoFactorAuthProviderType = TwoFactorAuthProviderType;
|
||||||
@ -80,6 +94,7 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
|
|||||||
public dialogService: DialogService,
|
public dialogService: DialogService,
|
||||||
public fb: FormBuilder,
|
public fb: FormBuilder,
|
||||||
private datePipe: DatePipe,
|
private datePipe: DatePipe,
|
||||||
|
private authService: AuthService,
|
||||||
private clipboardService: ClipboardService) {
|
private clipboardService: ClipboardService) {
|
||||||
super(store);
|
super(store);
|
||||||
}
|
}
|
||||||
@ -88,6 +103,8 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
|
|||||||
this.buildTwoFactorForm();
|
this.buildTwoFactorForm();
|
||||||
this.user = this.route.snapshot.data.user;
|
this.user = this.route.snapshot.data.user;
|
||||||
this.twoFactorLoad(this.route.snapshot.data.providers);
|
this.twoFactorLoad(this.route.snapshot.data.providers);
|
||||||
|
this.buildChangePasswordForm();
|
||||||
|
this.loadPasswordPolicy();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
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) {
|
trackByProvider(i: number, provider: TwoFactorAuthProviderType) {
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
@ -256,4 +342,41 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
|
|||||||
}
|
}
|
||||||
return info;
|
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",
|
"password-reset": "Password reset",
|
||||||
"expired-password-reset-message": "Your credentials has been expired! Please create new password.",
|
"expired-password-reset-message": "Your credentials has been expired! Please create new password.",
|
||||||
"new-password": "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",
|
"password-link-sent-message": "Reset link has been sent",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"login-with": "Login with {{name}}",
|
"login-with": "Login with {{name}}",
|
||||||
@ -2601,12 +2601,13 @@
|
|||||||
"change-password": "Change Password",
|
"change-password": "Change Password",
|
||||||
"current-password": "Current password",
|
"current-password": "Current password",
|
||||||
"copy-jwt-token": "Copy JWT token",
|
"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",
|
"tokenCopiedSuccessMessage": "JWT token has been copied to clipboard",
|
||||||
"tokenCopiedWarnMessage": "JWT token is expired! Please, refresh the page."
|
"tokenCopiedWarnMessage": "JWT token is expired! Please, refresh the page."
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
"security": "Security",
|
"security": "Password and authentication",
|
||||||
"2fa": {
|
"2fa": {
|
||||||
"2fa": "Two-factor authentication",
|
"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.",
|
"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-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"
|
"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": {
|
"relation": {
|
||||||
|
|||||||
@ -1289,7 +1289,6 @@
|
|||||||
"change-password": "Изменить пароль",
|
"change-password": "Изменить пароль",
|
||||||
"current-password": "Текущий пароль",
|
"current-password": "Текущий пароль",
|
||||||
"copy-jwt-token": "Копировать JWT токен",
|
"copy-jwt-token": "Копировать JWT токен",
|
||||||
"valid-till": "Действителен до {{expirationData}}",
|
|
||||||
"tokenCopiedMessage": "JWT токен скопирован в буфер обмена",
|
"tokenCopiedMessage": "JWT токен скопирован в буфер обмена",
|
||||||
"tokenCopiedWarnMessage": "JWT токен недействителен! Перезагрузите страницу."
|
"tokenCopiedWarnMessage": "JWT токен недействителен! Перезагрузите страницу."
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1704,7 +1704,6 @@
|
|||||||
"change-password": "Змінити пароль",
|
"change-password": "Змінити пароль",
|
||||||
"current-password": "Поточний пароль",
|
"current-password": "Поточний пароль",
|
||||||
"copy-jwt-token": "Копіювати JWT токен",
|
"copy-jwt-token": "Копіювати JWT токен",
|
||||||
"valid-till": "Дійсний до {{expirationData}}",
|
|
||||||
"tokenCopiedMessage": "JWT токен скопійовано в буфер обміну",
|
"tokenCopiedMessage": "JWT токен скопійовано в буфер обміну",
|
||||||
"tokenCopiedWarnMessage": "JWT токен не є дійсним! Перезавантажте сторінку."
|
"tokenCopiedWarnMessage": "JWT токен не є дійсним! Перезавантажте сторінку."
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2009,7 +2009,6 @@
|
|||||||
"last-login-time": "最后登录",
|
"last-login-time": "最后登录",
|
||||||
"profile": "属性",
|
"profile": "属性",
|
||||||
"copy-jwt-token": "复制 JWT 令牌",
|
"copy-jwt-token": "复制 JWT 令牌",
|
||||||
"valid-till": "有效期至 {{expirationData}}",
|
|
||||||
"tokenCopiedSuccessMessage": "JWT 令牌已复制到剪贴板",
|
"tokenCopiedSuccessMessage": "JWT 令牌已复制到剪贴板",
|
||||||
"tokenCopiedWarnMessage": "JWT 令牌已过期!请刷新页面。"
|
"tokenCopiedWarnMessage": "JWT 令牌已过期!请刷新页面。"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -221,6 +221,7 @@ $tb-dark-theme: get-tb-dark-theme(
|
|||||||
|
|
||||||
@mixin tb-components-theme($theme) {
|
@mixin tb-components-theme($theme) {
|
||||||
$primary: map-get($theme, primary);
|
$primary: map-get($theme, primary);
|
||||||
|
$warn: map-get($theme, warn);
|
||||||
|
|
||||||
mat-toolbar{
|
mat-toolbar{
|
||||||
&.mat-hue-3 {
|
&.mat-hue-3 {
|
||||||
@ -233,6 +234,12 @@ $tb-dark-theme: get-tb-dark-theme(
|
|||||||
div.tb-dashboard-page.mobile-app {
|
div.tb-dashboard-page.mobile-app {
|
||||||
@include mat-fab-toolbar-inverse-theme($tb-theme);
|
@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 {
|
.tb-default {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user