Browse Source

UI: Add security page for user

pull/6235/head
Vladyslav_Prykhodko 4 years ago
parent
commit
a80e83cb12
  1. 13
      ui-ngx/src/app/core/http/two-factor-authentication.service.ts
  2. 4
      ui-ngx/src/app/modules/home/models/services.map.ts
  3. 2
      ui-ngx/src/app/modules/home/pages/home-pages.module.ts
  4. 104
      ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.html
  5. 105
      ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.html
  6. 15
      ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.scss
  7. 97
      ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.html
  8. 15
      ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.scss
  9. 19
      ui-ngx/src/app/modules/home/pages/profile/profile-routing.module.ts
  10. 99
      ui-ngx/src/app/modules/home/pages/profile/profile.component.html
  11. 23
      ui-ngx/src/app/modules/home/pages/profile/profile.component.scss
  12. 99
      ui-ngx/src/app/modules/home/pages/profile/profile.component.ts
  13. 8
      ui-ngx/src/app/modules/home/pages/profile/profile.module.ts
  14. 77
      ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.component.scss
  15. 29
      ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.map.ts
  16. 102
      ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html
  17. 40
      ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts
  18. 105
      ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.html
  19. 40
      ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.ts
  20. 92
      ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.html
  21. 17
      ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts
  22. 84
      ui-ngx/src/app/modules/home/pages/security/security-routing.module.ts
  23. 80
      ui-ngx/src/app/modules/home/pages/security/security.component.html
  24. 115
      ui-ngx/src/app/modules/home/pages/security/security.component.scss
  25. 171
      ui-ngx/src/app/modules/home/pages/security/security.component.ts
  26. 39
      ui-ngx/src/app/modules/home/pages/security/security.module.ts
  27. 4
      ui-ngx/src/app/shared/components/user-menu.component.html
  28. 4
      ui-ngx/src/app/shared/components/user-menu.component.ts
  29. 12
      ui-ngx/src/app/shared/models/two-factor-auth.models.ts
  30. 100
      ui-ngx/src/assets/locale/locale.constant-en_US.json

13
ui-ngx/src/app/core/http/two-factor-authentication.service.ts

@ -39,8 +39,8 @@ export class TwoFactorAuthenticationService {
return this.http.get<TwoFactorAuthSettings>(`/api/2fa/settings`, defaultHttpOptionsFromConfig(config));
}
saveTwoFaSettings(settings: TwoFactorAuthSettings, config?: RequestConfig): Observable<TwoFactorAuthSettings> {
return this.http.post<TwoFactorAuthSettings>(`/api/2fa/settings`, settings, defaultHttpOptionsFromConfig(config));
saveTwoFaSettings(settings: TwoFactorAuthSettings, config?: RequestConfig): Observable<any> {
return this.http.post(`/api/2fa/settings`, settings, defaultHttpOptionsFromConfig(config));
}
getAvailableTwoFaProviders(config?: RequestConfig): Observable<Array<TwoFactorAuthProviderType>> {
@ -67,8 +67,9 @@ export class TwoFactorAuthenticationService {
}
verifyAndSaveTwoFaAccountConfig(authConfig: TwoFactorAuthAccountConfig, verificationCode: number,
config?: RequestConfig): Observable<any> {
return this.http.post(`/api/2fa/account/config?verificationCode=${verificationCode}`, authConfig, defaultHttpOptionsFromConfig(config));
config?: RequestConfig): Observable<AccountTwoFaSettings> {
return this.http.post<AccountTwoFaSettings>(`/api/2fa/account/config?verificationCode=${verificationCode}`,
authConfig, defaultHttpOptionsFromConfig(config));
}
deleteTwoFaAccountConfig(providerType: TwoFactorAuthProviderType, config?: RequestConfig): Observable<AccountTwoFaSettings> {
@ -76,4 +77,8 @@ export class TwoFactorAuthenticationService {
defaultHttpOptionsFromConfig(config));
}
requestTwoFaVerificationCodeSend(providerType: TwoFactorAuthProviderType, config?: RequestConfig) {
return this.http.post(`/api/auth/2fa/verification/send?providerType=${providerType}`, defaultHttpOptionsFromConfig(config));
}
}

4
ui-ngx/src/app/modules/home/models/services.map.ts

@ -36,6 +36,7 @@ import { BroadcastService } from '@core/services/broadcast.service';
import { ImportExportService } from '@home/components/import-export/import-export.service';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { OtaPackageService } from '@core/http/ota-package.service';
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service';
export const ServicesMap = new Map<string, Type<any>>(
[
@ -59,6 +60,7 @@ export const ServicesMap = new Map<string, Type<any>>(
['router', Router],
['importExport', ImportExportService],
['deviceProfileService', DeviceProfileService],
['otaPackageService', OtaPackageService]
['otaPackageService', OtaPackageService],
['twoFactorAuthenticationService', TwoFactorAuthenticationService]
]
);

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

@ -19,6 +19,7 @@ import { NgModule } from '@angular/core';
import { AdminModule } from './admin/admin.module';
import { HomeLinksModule } from './home-links/home-links.module';
import { ProfileModule } from './profile/profile.module';
import { SecurityModule } from '@home/pages/security/security.module';
import { TenantModule } from '@modules/home/pages/tenant/tenant.module';
import { CustomerModule } from '@modules/home/pages/customer/customer.module';
import { AuditLogModule } from '@modules/home/pages/audit-log/audit-log.module';
@ -42,6 +43,7 @@ import { OtaUpdateModule } from '@home/pages/ota-update/ota-update.module';
AdminModule,
HomeLinksModule,
ProfileModule,
SecurityModule,
TenantProfileModule,
TenantModule,
DeviceProfileModule,

104
ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.html

@ -1,104 +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.
-->
<mat-toolbar fxLayout="row" color="primary">
<h2>Enable email authenticator</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="closeDialog()"
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 style="padding: 0">
<mat-stepper labelPosition="bottom" linear #stepper>
<ng-template matStepperIcon="edit">
<mat-icon>done</mat-icon>
</ng-template>
<mat-step [stepControl]="emailConfigForm">
<ng-template matStepLabel>Add email</ng-template>
<form [formGroup]="emailConfigForm" style="display: block">
<mat-form-field class="mat-block" appearance="fill" style="max-width: 250px;">
<mat-label translate>user.email</mat-label>
<input matInput formControlName="email" required/>
<mat-error *ngIf="emailConfigForm.get('email').hasError('required')">
{{ 'user.email-required' | translate }}
</mat-error>
<mat-error *ngIf="emailConfigForm.get('email').hasError('email')">
{{ 'user.invalid-email-format' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayoutAlign="end center">
<button mat-button
type="button"
[mat-dialog-close]="false">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
type="button"
color="primary"
[disabled]="(isLoading$ | async) || emailConfigForm.invalid"
(click)="nextStep()">
Send code
</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="emailVerificationForm">
<ng-template matStepLabel>Authentication verification</ng-template>
<form [formGroup]="emailVerificationForm" style="display: block">
<p class="mat-body-strong">Enter the 6-digit code here</p>
<p class="mat-body" style="max-width: 480px;">Enter the 6 digit verification code we just sent to {{ emailConfigForm.get('email').value }}.</p>
<mat-form-field class="mat-block" appearance="fill" style="max-width: 200px;">
<mat-label>6-digit code</mat-label>
<input matInput formControlName="verificationCode" required maxlength="6" type="text" pattern="\d*">
</mat-form-field>
<div fxLayout="row" fxLayoutAlign="end center">
<button mat-button
type="button"
[mat-dialog-close]="false">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
type="button"
color="primary"
[disabled]="(isLoading$ | async) || emailVerificationForm.invalid"
(click)="nextStep()">
Activate
</button>
</div>
</form>
</mat-step>
<mat-step>
<ng-template matStepLabel matStepperIcon="done">Two-factor authentication activated</ng-template>
<div>
<h2 class="mat-h2">Email authentication enabled</h2>
<p class="mat-body" style="max-width: 480px;">If we notice an attempted login from a device or browser we don’t recognize, you will be prompted to enter the security code that will be sent to your email address.</p>
<div fxLayout="row" fxLayoutAlign="end center">
<button mat-button
type="button"
[mat-dialog-close]="true">
Done
</button>
</div>
</div>
</mat-step>
</mat-stepper>
</div>

105
ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.html

@ -1,105 +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.
-->
<mat-toolbar fxLayout="row" color="primary">
<h2>Enable SMS authenticator</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="closeDialog()"
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 style="padding: 0">
<mat-stepper labelPosition="bottom" linear #stepper>
<ng-template matStepperIcon="edit">
<mat-icon>done</mat-icon>
</ng-template>
<mat-step [stepControl]="smsConfigForm">
<ng-template matStepLabel>Add phone number</ng-template>
<form [formGroup]="smsConfigForm" style="display: block">
<p class="mat-body-1">Enter the phone number including the area code.</p>
<mat-form-field class="mat-block" appearance="fill" style="max-width: 250px;">
<mat-label>Phone number</mat-label>
<input type="tel" required [pattern]="phoneNumberPattern" matInput formControlName="phone">
<mat-error *ngIf="smsConfigForm.get('phone').hasError('required')">
{{ 'admin.number-to-required' | translate }}
</mat-error>
<mat-error *ngIf="smsConfigForm.get('phone').hasError('pattern')">
{{ 'admin.phone-number-pattern' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayoutAlign="end center">
<button mat-button
type="button"
[mat-dialog-close]="false">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
type="button"
color="primary"
[disabled]="(isLoading$ | async) || smsConfigForm.invalid"
(click)="nextStep()">
Send code
</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="smsVerificationForm">
<ng-template matStepLabel>Authentication verification</ng-template>
<form [formGroup]="smsVerificationForm" style="display: block">
<p class="mat-body-strong">Enter the 6-digit code here</p>
<p class="mat-body" style="max-width: 480px;">Enter the 6 digit verification code we just sent to {{ smsConfigForm.get('phone').value }}.</p>
<mat-form-field class="mat-block" appearance="fill" style="max-width: 200px;">
<mat-label>6-digit code</mat-label>
<input matInput formControlName="verificationCode" required maxlength="6" type="text" pattern="\d*">
</mat-form-field>
<div fxLayout="row" fxLayoutAlign="end center">
<button mat-button
type="button"
[mat-dialog-close]="false">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
type="button"
color="primary"
[disabled]="(isLoading$ | async) || smsVerificationForm.invalid"
(click)="nextStep()">
Activate
</button>
</div>
</form>
</mat-step>
<mat-step>
<ng-template matStepLabel matStepperIcon="done">Two-factor authentication activated</ng-template>
<div>
<h2 class="mat-h2">SMS authentication enabled</h2>
<p class="mat-body" style="max-width: 480px;">If we notice an attempted login from a device or browser we don’t recognize, you will be prompted to enter the security code that will be sent to the phone number.</p>
<div fxLayout="row" fxLayoutAlign="end center">
<button mat-button
type="button"
[mat-dialog-close]="true">
Done
</button>
</div>
</div>
</mat-step>
</mat-stepper>
</div>

15
ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.scss

@ -1,15 +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.
*/

97
ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.html

@ -1,97 +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.
-->
<mat-toolbar fxLayout="row" color="primary">
<h2>Enable authenticator app</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="closeDialog()"
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 style="padding: 0">
<mat-stepper labelPosition="bottom" linear #stepper>
<ng-template matStepperIcon="edit">
<mat-icon>done</mat-icon>
</ng-template>
<mat-step>
<ng-template matStepLabel>Get app</ng-template>
<div>
<p class="mat-body-1">You'll need to use a verification app such as Google Authenticator, Authy, or Duo. Install from your app store</p>
<div fxLayout="row" fxLayoutAlign="end center">
<button mat-button
type="button"
[mat-dialog-close]="false">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
type="button"
color="primary"
matStepperNext>
Next
</button>
</div>
</div>
</mat-step>
<mat-step [stepControl]="totpConfigForm">
<ng-template matStepLabel>Authentication verification</ng-template>
<form [formGroup]="totpConfigForm" style="display: block">
<p class="mat-body-strong">1. Scan this QR code with your verification app</p>
<p class="mat-body" style="margin-bottom: 8px">Once your app reads the QR code, you'll get a 6-digit code.</p>
<canvas fxFlex #canvas [ngStyle]="{display: totpAuthURL ? 'block' : 'none'}"></canvas>
<p class="mat-body-strong">2. Enter the 6-digit code here</p>
<p class="mat-body" style="max-width: 480px;">Enter the code from the app below. Once connected, we'll remember your phone so you can use it each time you log in.</p>
<mat-form-field class="mat-block" appearance="fill" style="max-width: 200px;">
<mat-label>6-digit code</mat-label>
<input matInput formControlName="verificationCode" required maxlength="6" type="text" pattern="\d*">
</mat-form-field>
<div fxLayout="row" fxLayoutAlign="end center">
<button mat-button
type="button"
[mat-dialog-close]="false">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
type="button"
color="primary"
[disabled]="(isLoading$ | async) || totpConfigForm.invalid"
(click)="onSaveConfig()">
Next
</button>
</div>
</form>
</mat-step>
<mat-step>
<ng-template matStepLabel matStepperIcon="done">Two-factor authentication activated</ng-template>
<div>
<h2 class="mat-h2">Success</h2>
<p class="mat-body" style="max-width: 480px;">The next time you login from an unrecognized browser or device, you will need to provide a two-factor authentication code.</p>
<div fxLayout="row" fxLayoutAlign="end center">
<button mat-button
type="button"
[mat-dialog-close]="true">
Done
</button>
</div>
</div>
</mat-step>
</mat-stepper>
</div>

15
ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.scss

@ -1,15 +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.
*/

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

@ -26,8 +26,6 @@ import { AppState } from '@core/core.state';
import { UserService } from '@core/http/user.service';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Observable } from 'rxjs';
import { TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models';
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service';
@Injectable()
export class UserProfileResolver implements Resolve<User> {
@ -42,17 +40,6 @@ export class UserProfileResolver implements Resolve<User> {
}
}
@Injectable()
export class UserTwoFAProvidersResolver implements Resolve<Array<TwoFactorAuthProviderType>> {
constructor(private twoFactorAuthService: TwoFactorAuthenticationService) {
}
resolve(): Observable<Array<TwoFactorAuthProviderType>> {
return this.twoFactorAuthService.getAvailableTwoFaProviders();
}
}
const routes: Routes = [
{
path: 'profile',
@ -67,8 +54,7 @@ const routes: Routes = [
}
},
resolve: {
user: UserProfileResolver,
providers: UserTwoFAProvidersResolver
user: UserProfileResolver
}
}
];
@ -77,8 +63,7 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: [
UserProfileResolver,
UserTwoFAProvidersResolver
UserProfileResolver
]
})
export class ProfileRoutingModule { }

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

@ -18,12 +18,13 @@
<div>
<mat-card class="profile-card">
<mat-card-title>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.xs="8px">
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.xs="8px"
fxLayoutAlign="space-between start" fxLayoutAlign.xs="start start">
<div fxFlex fxLayout="column">
<span class="mat-headline" translate>profile.profile</span>
<span class="profile-email" style='opacity: 0.7;'>{{ profile ? profile.get('email').value : '' }}</span>
</div>
<div fxFlex fxLayout="column">
<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>
@ -77,21 +78,23 @@
{{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
</mat-checkbox>
</section>
<div fxLayout="row" style="padding-bottom: 16px;">
<button mat-button mat-raised-button color="primary"
type="button"
[disabled]="(isLoading$ | async)" (click)="changePassword()">
{{'profile.change-password' | translate}}
</button>
</div>
<div fxLayout="row" fxLayoutAlign=" center" style="padding-bottom: 16px;">
<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>
<span class="profile-btn-subtext">{{ expirationJwtData }}</span>
<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"
@ -104,66 +107,4 @@
</form>
</mat-card-content>
</mat-card>
<mat-card class="profile-card" *ngIf="allowTwoFactorAuth">
<mat-card-title>
<span class="mat-title">Two-factor authentication</span>
</mat-card-title>
<mat-card-content>
<div class="mat-body-1 description">
Two-Factor Authentication (2FA) can be used to help protect your account from unauthorized access by requiring you to enter a security code when you sign in.
</div>
<div class="mat-body-2">Available authentication methods:</div>
<form [formGroup]="twoFactorAuth">
<fieldset [disabled]="isLoading$ | async">
<mat-radio-group formControlName="useByDefault">
<div *ngIf="allowTOTP2faProvider" class="provider">
<h3 class="mat-h3">Third-Party authentication app</h3>
<div fxLayout="row" fxLayoutAlign="space-between start">
<div class="mat-body-1 description">
Use an Authenticator App as your Two-Factor Authentication. When you sign in you’ll be required to use the security code provided by your Authenticator App.
</div>
<mat-slide-toggle formControlName="TOTP"
(click)="confirm2FAChange($event, twoFactorAuthProviderType.TOTP)">
</mat-slide-toggle>
</div>
<mat-radio-button [value]="twoFactorAuthProviderType.TOTP"
*ngIf="twoFactorAuth.get('TOTP').value">
<span class="checkbox-label">Make this my primary Two-Factor authentication method</span>
</mat-radio-button>
</div>
<div *ngIf="allowSMS2faProvider" class="provider">
<h3 class="mat-h3">SMS authentication</h3>
<div fxLayout="row" fxLayoutAlign="space-between start">
<div class="mat-body-1 description">
Use your phone as your Two-Factor Authentication when you sign in you’ll be required to use the security code we send you via SMS message.
</div>
<mat-slide-toggle formControlName="SMS"
(click)="confirm2FAChange($event, twoFactorAuthProviderType.SMS)">
</mat-slide-toggle>
</div>
<mat-radio-button [value]="twoFactorAuthProviderType.SMS"
*ngIf="twoFactorAuth.get('SMS').value">
<span class="checkbox-label">Make this my primary Two-Factor authentication method</span>
</mat-radio-button>
</div>
<div *ngIf="allowEmail2faProvider" class="provider">
<h3 class="mat-h3">Email authentication</h3>
<div fxLayout="row" fxLayoutAlign="space-between start">
<div class="mat-body-1 description">
Use a security code sent to your email address as your Two-Factor Authentication (2FA). The security code will be sent to the address associated with your account. You’ll need to use it in when you sign in.
</div>
<mat-slide-toggle formControlName="EMAIL"
(click)="confirm2FAChange($event, twoFactorAuthProviderType.EMAIL)">
</mat-slide-toggle>
</div>
<mat-radio-button [value]="twoFactorAuthProviderType.EMAIL"
*ngIf="twoFactorAuth.get('EMAIL').value">
<span class="checkbox-label">Make this my primary Two-Factor authentication method</span>
</mat-radio-button>
</div>
</mat-radio-group>
</fieldset>
</form>
</mat-card-content>
</mat-card>
</div>

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

@ -39,8 +39,10 @@
font-weight: 400;
}
.profile-btn-subtext {
opacity: 0.7;
padding: 10px;
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 {
@ -59,21 +61,4 @@
}
}
}
.description {
padding-bottom: 8px;
color: #808080;
margin-right: 8px;
}
.provider {
padding: 14px 0;
.mat-h3 {
margin-bottom: 8px;
}
.checkbox-label {
font-size: 14px;
}
}
}

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

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, OnInit, ViewChild } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { UserService } from '@core/http/user.service';
import { AuthUser, User } from '@shared/models/user.model';
import { Authority } from '@shared/models/authority.enum';
@ -37,12 +37,6 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DatePipe } from '@angular/common';
import { ClipboardService } from 'ngx-clipboard';
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service';
import { AccountTwoFaSettings, TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models';
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { TotpAuthDialogComponent } from '@home/pages/profile/authentication-dialog/totp-auth-dialog.component';
import { SMSAuthDialogComponent } from '@home/pages/profile/authentication-dialog/sms-auth-dialog.component';
import { EmailAuthDialogComponent, } from '@home/pages/profile/authentication-dialog/email-auth-dialog.component';
@Component({
selector: 'tb-profile',
@ -53,25 +47,8 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
authorities = Authority;
profile: FormGroup;
twoFactorAuth: FormGroup;
user: User;
languageList = env.supportedLangs;
allowTwoFactorAuth = false;
allowSMS2faProvider = false;
allowTOTP2faProvider = false;
allowEmail2faProvider = false;
twoFactorAuthProviderType = TwoFactorAuthProviderType;
@ViewChild('totp') totp: MatSlideToggle;
private authDialogMap = new Map<TwoFactorAuthProviderType, any>(
[
[TwoFactorAuthProviderType.TOTP, TotpAuthDialogComponent],
[TwoFactorAuthProviderType.SMS, SMSAuthDialogComponent],
[TwoFactorAuthProviderType.EMAIL, EmailAuthDialogComponent]
]
);
private readonly authUser: AuthUser;
get jwtToken(): string {
@ -92,7 +69,6 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
private userService: UserService,
private authService: AuthService,
private translate: TranslateService,
private twoFaService: TwoFactorAuthenticationService,
public dialog: MatDialog,
public dialogService: DialogService,
public fb: FormBuilder,
@ -104,9 +80,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
ngOnInit() {
this.buildProfileForm();
this.buildTwoFactorForm();
this.userLoaded(this.route.snapshot.data.user);
this.twoFactorLoad(this.route.snapshot.data.providers);
}
private buildProfileForm() {
@ -120,19 +94,6 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
});
}
private buildTwoFactorForm() {
this.twoFactorAuth = this.fb.group({
TOTP: [false],
SMS: [false],
EMAIL: [false],
useByDefault: [null]
});
this.twoFactorAuth.get('useByDefault').valueChanges.subscribe(value => {
this.twoFaService.updateTwoFaAccountConfig(value, true, {ignoreLoading: true})
.subscribe(data => this.processTwoFactorAuthConfig(data));
});
}
save(): void {
this.user = {...this.user, ...this.profile.value};
if (!this.user.additionalInfo) {
@ -190,37 +151,6 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
this.profile.get('homeDashboardHideToolbar').setValue(homeDashboardHideToolbar);
}
private twoFactorLoad(providers: TwoFactorAuthProviderType[]) {
if (providers.length) {
this.allowTwoFactorAuth = true;
this.twoFaService.getAccountTwoFaSettings().subscribe(data => this.processTwoFactorAuthConfig(data));
providers.forEach(provider => {
switch (provider) {
case TwoFactorAuthProviderType.SMS:
this.allowSMS2faProvider = true;
break;
case TwoFactorAuthProviderType.TOTP:
this.allowTOTP2faProvider = true;
break;
case TwoFactorAuthProviderType.EMAIL:
this.allowEmail2faProvider = true;
break;
}
});
}
}
private processTwoFactorAuthConfig(setting?: AccountTwoFaSettings) {
if (setting) {
Object.values(setting.configs).forEach(config => {
this.twoFactorAuth.get(config.providerType).setValue(true);
if (config.useByDefault) {
this.twoFactorAuth.get('useByDefault').setValue(config.providerType, {emitEvent: false});
}
});
}
}
confirmForm(): FormGroup {
return this.profile;
}
@ -249,31 +179,4 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
}));
}
}
confirm2FAChange(event: MouseEvent, provider: TwoFactorAuthProviderType) {
event.stopPropagation();
event.preventDefault();
const providerName = provider === TwoFactorAuthProviderType.TOTP ? 'authenticator app' : `${provider.toLowerCase()} authentication`;
if (this.twoFactorAuth.get(provider).value) {
this.dialogService.confirm(`Are you sure you want to disable ${providerName}?`,
`Disabling ${providerName} will make your account less secure`).subscribe(res => {
if (res) {
this.twoFaService.deleteTwoFaAccountConfig(provider).subscribe(data => this.processTwoFactorAuthConfig(data));
}
});
} else {
this.dialog.open(this.authDialogMap.get(provider), {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
email: this.user.email
}
}).afterClosed().subscribe(res => {
if (res) {
this.twoFactorAuth.get(provider).setValue(res);
this.twoFactorAuth.get('useByDefault').setValue(provider, {emitEvent: false});
}
});
}
}
}

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

@ -20,17 +20,11 @@ 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';
import { TotpAuthDialogComponent } from './authentication-dialog/totp-auth-dialog.component';
import { SMSAuthDialogComponent } from '@home/pages/profile/authentication-dialog/sms-auth-dialog.component';
import { EmailAuthDialogComponent } from '@home/pages/profile/authentication-dialog/email-auth-dialog.component';
@NgModule({
declarations: [
ProfileComponent,
ChangePasswordDialogComponent,
TotpAuthDialogComponent,
SMSAuthDialogComponent,
EmailAuthDialogComponent
ChangePasswordDialogComponent
],
imports: [
CommonModule,

77
ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.component.scss

@ -0,0 +1,77 @@
/**
* 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{
.mat-toolbar > h2 {
font-weight: 400;
letter-spacing: 0.25px;
}
form {
display: block;
}
.mat-body-1 {
margin-bottom: 0;
letter-spacing: 0.25px;
&:not(:first-of-type) {
margin: 0 0 8px;
}
}
.input-container {
max-width: 290px;
}
.code-container {
max-width: 170px;
}
.result-title {
font: 500 18px / 24px Roboto, "Helvetica Neue", sans-serif;
letter-spacing: 0.1px;
margin: 8px 0;
text-align: center;
}
.result-description {
text-align: center;
margin: 0 0 16px;
letter-spacing: 0.25px;
max-width: 500px;
}
.step-description {
max-width: 450px;
&.input {
margin: 12px 0 0;
}
}
.qr-code-description {
text-align: center;
max-width: 180px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
margin-bottom: 0;
}
& ::ng-deep {
.mat-horizontal-stepper-header{
pointer-events: none !important;
}
}
}

29
ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.map.ts

@ -0,0 +1,29 @@
///
/// 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 { Type } from '@angular/core';
import { TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models';
import { TotpAuthDialogComponent } from './totp-auth-dialog.component';
import { SMSAuthDialogComponent } from './sms-auth-dialog.component';
import { EmailAuthDialogComponent } from './email-auth-dialog.component';
export const authenticationDialogMap = new Map<TwoFactorAuthProviderType, Type<any>>(
[
[TwoFactorAuthProviderType.TOTP, TotpAuthDialogComponent],
[TwoFactorAuthProviderType.SMS, SMSAuthDialogComponent],
[TwoFactorAuthProviderType.EMAIL, EmailAuthDialogComponent]
]
);

102
ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html

@ -0,0 +1,102 @@
<!--
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.
-->
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>security.2fa.dialog.enable-email-title</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="closeDialog()"
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 tb-toast style="padding: 0">
<mat-stepper [linear]="true" #stepper>
<ng-template matStepperIcon="edit">
<mat-icon>done</mat-icon>
</ng-template>
<mat-step [stepControl]="emailConfigForm">
<ng-template matStepLabel>{{ 'security.2fa.dialog.email-step-label' | translate }}</ng-template>
<form [formGroup]="emailConfigForm" (ngSubmit)="nextStep()">
<p class="mat-body step-description input" translate>security.2fa.dialog.email-step-description</p>
<div fxLayout="row" fxLayoutAlign="space-between center">
<mat-form-field fxFlex class="mat-block input-container" floatLabel="always">
<mat-label></mat-label>
<input matInput formControlName="email"
placeholder="{{ 'security.2fa.dialog.email-step-label' | translate }}" />
<mat-error *ngIf="emailConfigForm.get('email').hasError('required')">
{{ 'user.email-required' | translate }}
</mat-error>
<mat-error *ngIf="emailConfigForm.get('email').hasError('email')">
{{ 'user.invalid-email-format' | translate }}
</mat-error>
</mat-form-field>
<button mat-raised-button
type="submit"
color="primary"
[disabled]="(isLoading$ | async) || emailConfigForm.invalid">
{{ 'security.2fa.dialog.send-code' | translate }}
</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="emailVerificationForm">
<ng-template matStepLabel>{{ 'security.2fa.dialog.verification-step-label' | translate }}</ng-template>
<form [formGroup]="emailVerificationForm" (ngSubmit)="nextStep()">
<p class="mat-body step-description input">
{{ 'security.2fa.dialog.verification-step-description' | translate : {address: emailConfigForm.get('email').value} }}
</p>
<div fxLayout="row" fxLayoutAlign="space-between center">
<mat-form-field fxFlex class="mat-block code-container" floatLabel="always">
<mat-label></mat-label>
<input matInput formControlName="verificationCode"
maxlength="6" type="text"
pattern="\d*"
placeholder="{{ 'security.2fa.dialog.verification-code' | translate }}">
<mat-error *ngIf="emailVerificationForm.get('verificationCode').invalid">
{{ 'security.2fa.dialog.verification-code-invalid' | translate }}
</mat-error>
</mat-form-field>
<button mat-raised-button
type="submit"
color="primary"
[disabled]="(isLoading$ | async) || emailVerificationForm.invalid">
{{ 'security.2fa.dialog.activate' | translate }}
</button>
</div>
</form>
</mat-step>
<mat-step>
<ng-template matStepLabel matStepperIcon="done">{{ 'security.2fa.dialog.activation-step-label' | translate }}</ng-template>
<div>
<h3 class="result-title" translate>security.2fa.dialog.success</h3>
<p class="mat-body result-description" translate>security.2fa.dialog.activation-step-description-email</p>
<div fxLayout="row" fxLayoutAlign="end center">
<button mat-raised-button
type="button"
color="primary"
[mat-dialog-close]="true">
{{ 'import.stepper-text.done' | translate }}
</button>
</div>
</div>
</mat-step>
</mat-stepper>
</div>

40
ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.ts → ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts

@ -32,7 +32,7 @@ export interface EmailAuthDialogData {
@Component({
selector: 'tb-email-auth-dialog',
templateUrl: './email-auth-dialog.component.html',
styleUrls: ['./email-auth-dialog.component.scss']
styleUrls: ['./authentication-dialog.component.scss']
})
export class EmailAuthDialogComponent extends DialogComponent<EmailAuthDialogComponent> {
@ -68,20 +68,28 @@ export class EmailAuthDialogComponent extends DialogComponent<EmailAuthDialogCom
nextStep() {
switch (this.stepper.selectedIndex) {
case 0:
this.authAccountConfig = {
providerType: TwoFactorAuthProviderType.EMAIL,
useByDefault: true,
email: this.emailConfigForm.get('email').value as string
};
this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => {
this.stepper.next();
});
if (this.emailConfigForm.valid) {
this.authAccountConfig = {
providerType: TwoFactorAuthProviderType.EMAIL,
useByDefault: true,
email: this.emailConfigForm.get('email').value as string
};
this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => {
this.stepper.next();
});
} else {
this.showFormErrors(this.emailConfigForm);
}
break;
case 1:
this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig,
this.emailVerificationForm.get('verificationCode').value).subscribe(() => {
this.stepper.next();
});
if (this.emailVerificationForm.valid) {
this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig,
this.emailVerificationForm.get('verificationCode').value).subscribe(() => {
this.stepper.next();
});
} else {
this.showFormErrors(this.emailVerificationForm);
}
break;
}
}
@ -90,4 +98,10 @@ export class EmailAuthDialogComponent extends DialogComponent<EmailAuthDialogCom
return this.dialogRef.close(this.stepper.selectedIndex > 1);
}
private showFormErrors(form: FormGroup) {
Object.keys(form.controls).forEach(field => {
const control = form.get(field);
control.markAsTouched({onlySelf: true});
});
}
}

105
ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.html

@ -0,0 +1,105 @@
<!--
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.
-->
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>security.2fa.dialog.enable-sms-title</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="closeDialog()"
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 tb-toast style="padding: 0">
<mat-stepper linear #stepper>
<ng-template matStepperIcon="edit">
<mat-icon>done</mat-icon>
</ng-template>
<mat-step [stepControl]="smsConfigForm">
<ng-template matStepLabel>{{ 'security.2fa.dialog.sms-step-label' | translate }}</ng-template>
<form [formGroup]="smsConfigForm" (ngSubmit)="nextStep()">
<p class="mat-body step-description input" translate>security.2fa.dialog.sms-step-description</p>
<div fxLayout="row" fxLayoutAlign="space-between center">
<mat-form-field fxFlex class="mat-block input-container" floatLabel="always">
<mat-label></mat-label>
<input type="tel"
[pattern]="phoneNumberPattern"
matInput formControlName="phone"
placeholder="{{ 'security.2fa.dialog.sms-step-label' | translate }}">
<mat-error *ngIf="smsConfigForm.get('phone').hasError('required')">
{{ 'admin.number-to-required' | translate }}
</mat-error>
<mat-error *ngIf="smsConfigForm.get('phone').hasError('pattern')">
{{ 'admin.phone-number-pattern' | translate }}
</mat-error>
<mat-hint innerHTML="{{ 'admin.phone-number-hint' | translate }}"></mat-hint>
</mat-form-field>
<button mat-raised-button
type="submit"
color="primary"
[disabled]="(isLoading$ | async) || smsConfigForm.invalid">
{{ 'security.2fa.dialog.send-code' | translate }}
</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="smsVerificationForm">
<ng-template matStepLabel>{{ 'security.2fa.dialog.verification-step-label' | translate }}</ng-template>
<form [formGroup]="smsVerificationForm" (ngSubmit)="nextStep()">
<p class="mat-body step-description input">
{{ 'security.2fa.dialog.verification-step-description' | translate : {address: smsConfigForm.get('phone').value} }}
</p>
<div fxLayout="row" fxLayoutAlign="space-between center">
<mat-form-field fxFlex class="mat-block code-container" floatLabel="always">
<mat-label></mat-label>
<input matInput formControlName="verificationCode"
maxlength="6" type="text"
pattern="\d*"
placeholder="{{ 'security.2fa.dialog.verification-code' | translate }}">
<mat-error *ngIf="smsVerificationForm.get('verificationCode').invalid">
{{ 'security.2fa.dialog.verification-code-invalid' | translate }}
</mat-error>
</mat-form-field>
<button mat-raised-button
type="submit"
color="primary"
[disabled]="(isLoading$ | async) || smsVerificationForm.invalid">
{{ 'security.2fa.dialog.activate' | translate }}
</button>
</div>
</form>
</mat-step>
<mat-step>
<ng-template matStepLabel matStepperIcon="done">{{ 'security.2fa.dialog.activation-step-label' | translate }}</ng-template>
<div>
<h3 class="result-title" translate>security.2fa.dialog.success</h3>
<p class="mat-body result-description" translate>security.2fa.dialog.activation-step-description-sms</p>
<div fxLayout="row" fxLayoutAlign="end center">
<button mat-raised-button
type="button"
color="primary"
[mat-dialog-close]="true">
{{ 'import.stepper-text.done' | translate }}
</button>
</div>
</div>
</mat-step>
</mat-stepper>
</div>

40
ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/sms-auth-dialog.component.ts → ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.ts

@ -29,7 +29,7 @@ import { MatStepper } from '@angular/material/stepper';
@Component({
selector: 'tb-sms-auth-dialog',
templateUrl: './sms-auth-dialog.component.html',
styleUrls: ['./sms-auth-dialog.component.scss']
styleUrls: ['./authentication-dialog.component.scss']
})
export class SMSAuthDialogComponent extends DialogComponent<SMSAuthDialogComponent> {
@ -66,20 +66,28 @@ export class SMSAuthDialogComponent extends DialogComponent<SMSAuthDialogCompone
nextStep() {
switch (this.stepper.selectedIndex) {
case 0:
this.authAccountConfig = {
providerType: TwoFactorAuthProviderType.SMS,
useByDefault: true,
phoneNumber: this.smsConfigForm.get('phone').value as string
};
this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => {
this.stepper.next();
});
if (this.smsConfigForm.valid) {
this.authAccountConfig = {
providerType: TwoFactorAuthProviderType.SMS,
useByDefault: true,
phoneNumber: this.smsConfigForm.get('phone').value as string
};
this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => {
this.stepper.next();
});
} else {
this.showFormErrors(this.smsConfigForm);
}
break;
case 1:
this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig,
this.smsVerificationForm.get('verificationCode').value).subscribe(() => {
this.stepper.next();
});
if (this.smsVerificationForm.valid) {
this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig,
this.smsVerificationForm.get('verificationCode').value).subscribe(() => {
this.stepper.next();
});
} else {
this.showFormErrors(this.smsVerificationForm);
}
break;
}
}
@ -88,4 +96,10 @@ export class SMSAuthDialogComponent extends DialogComponent<SMSAuthDialogCompone
return this.dialogRef.close(this.stepper.selectedIndex > 1);
}
private showFormErrors(form: FormGroup) {
Object.keys(form.controls).forEach(field => {
const control = form.get(field);
control.markAsTouched({onlySelf: true});
});
}
}

92
ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.html

@ -0,0 +1,92 @@
<!--
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.
-->
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>security.2fa.dialog.enable-totp-title</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="closeDialog()"
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 tb-toast style="padding: 0">
<mat-stepper linear #stepper>
<ng-template matStepperIcon="edit">
<mat-icon>done</mat-icon>
</ng-template>
<mat-step>
<ng-template matStepLabel>{{ 'security.2fa.dialog.totp-step-label' | translate }}</ng-template>
<div>
<p class="mat-body-1" translate>security.2fa.dialog.totp-step-description-open</p>
<p class="mat-body-1" translate>security.2fa.dialog.totp-step-description-install</p>
<div fxLayout="row" fxLayoutAlign="end center">
<button mat-raised-button
type="button"
color="primary"
matStepperNext>
{{ 'security.2fa.dialog.next' | translate }}
</button>
</div>
</div>
</mat-step>
<mat-step [stepControl]="totpConfigForm">
<ng-template matStepLabel>{{ 'security.2fa.dialog.verification-step-label' | translate }}</ng-template>
<form [formGroup]="totpConfigForm" fxLayoutAlign="start center" fxLayout="column" (ngSubmit)="onSaveConfig()">
<p class="mat-body qr-code-description" translate>security.2fa.dialog.scan-qr-code</p>
<canvas fxFlex #canvas [ngStyle]="{display: totpAuthURL ? 'block' : 'none'}"></canvas>
<p class="mat-body qr-code-description" style="margin-top: 30px;" translate>security.2fa.dialog.enter-verification-code</p>
<mat-form-field fxFlex class="mat-block code-container" floatLabel="always">
<mat-label></mat-label>
<input matInput formControlName="verificationCode"
maxlength="6" type="text"
pattern="\d*"
placeholder="{{ 'security.2fa.dialog.verification-code' | translate }}">
<mat-error *ngIf="totpConfigForm.get('verificationCode').invalid">
{{ 'security.2fa.dialog.verification-code-invalid' | translate }}
</mat-error>
</mat-form-field>
</form>
<div fxLayout="row" fxLayoutAlign="end center">
<button mat-raised-button
type="submit"
color="primary"
[disabled]="(isLoading$ | async) || totpConfigForm.invalid">
{{ 'security.2fa.dialog.next' | translate }}
</button>
</div>
</mat-step>
<mat-step>
<ng-template matStepLabel matStepperIcon="done">{{ 'security.2fa.dialog.activation-step-label' | translate }}</ng-template>
<div>
<h3 class="result-title" translate>security.2fa.dialog.success</h3>
<p class="mat-body result-description" translate>security.2fa.dialog.activation-step-description-totp</p>
<div fxLayout="row" fxLayoutAlign="end center">
<button mat-raised-button
type="button"
color="primary"
[mat-dialog-close]="true">
{{ 'import.stepper-text.done' | translate }}
</button>
</div>
</div>
</mat-step>
</mat-stepper>
</div>

17
ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/totp-auth-dialog.component.ts → ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts

@ -28,7 +28,7 @@ import { MatStepper } from '@angular/material/stepper';
@Component({
selector: 'tb-totp-auth-dialog',
templateUrl: './totp-auth-dialog.component.html',
styleUrls: ['./totp-auth-dialog.component.scss']
styleUrls: ['./authentication-dialog.component.scss']
})
export class TotpAuthDialogComponent extends DialogComponent<TotpAuthDialogComponent> {
@ -67,10 +67,17 @@ export class TotpAuthDialogComponent extends DialogComponent<TotpAuthDialogCompo
}
onSaveConfig() {
this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig,
this.totpConfigForm.get('verificationCode').value).subscribe((res) => {
this.stepper.next();
});
if (this.totpConfigForm.valid) {
this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig,
this.totpConfigForm.get('verificationCode').value).subscribe(() => {
this.stepper.next();
});
} else {
Object.keys(this.totpConfigForm.controls).forEach(field => {
const control = this.totpConfigForm.get(field);
control.markAsTouched({onlySelf: true});
});
}
}
closeDialog() {

84
ui-ngx/src/app/modules/home/pages/security/security-routing.module.ts

@ -0,0 +1,84 @@
///
/// 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 { Injectable, NgModule } from '@angular/core';
import { Resolve, RouterModule, Routes } from '@angular/router';
import { SecurityComponent } from './security.component';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { Authority } from '@shared/models/authority.enum';
import { User } from '@shared/models/user.model';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UserService } from '@core/http/user.service';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Observable } from 'rxjs';
import { TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models';
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service';
@Injectable()
export class UserProfileResolver implements Resolve<User> {
constructor(private store: Store<AppState>,
private userService: UserService) {
}
resolve(): Observable<User> {
const userId = getCurrentAuthUser(this.store).userId;
return this.userService.getUser(userId);
}
}
@Injectable()
export class UserTwoFAProvidersResolver implements Resolve<Array<TwoFactorAuthProviderType>> {
constructor(private twoFactorAuthService: TwoFactorAuthenticationService) {
}
resolve(): Observable<Array<TwoFactorAuthProviderType>> {
return this.twoFactorAuthService.getAvailableTwoFaProviders();
}
}
const routes: Routes = [
{
path: 'security',
component: SecurityComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'security.security',
breadcrumb: {
label: 'security.security',
icon: 'lock'
}
},
resolve: {
user: UserProfileResolver,
providers: UserTwoFAProvidersResolver
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: [
UserProfileResolver,
UserTwoFAProvidersResolver
]
})
export class SecurityRoutingModule { }

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

@ -0,0 +1,80 @@
<!--
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.
-->
<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>
<mat-card-content>
<div>
<button mat-stroked-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" *ngIf="allowTwoFactorProviders.length">
<mat-card-title style="margin-bottom: 20px;">
<span class="mat-headline" 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>
</mat-card-subtitle>
<mat-card-content>
<h3 class="mat-h3 auth-title" translate>security.2fa.authenticate-with</h3>
<form [formGroup]="twoFactorAuth">
<fieldset [disabled]="isLoading$ | async">
<mat-radio-group formControlName="useByDefault">
<ng-container *ngFor="let provider of allowTwoFactorProviders; let $last = last; trackBy: trackByProvider">
<div class="provider">
<h4 class="provider-title">{{ providersData.get(provider).name | translate }}</h4>
<div fxLayout="row" fxLayoutAlign="space-between start">
<div class="mat-body-1 description">
{{ providersData.get(provider).description | translate }}
</div>
<mat-slide-toggle [formControlName]="provider"
(click)="confirm2FAChange($event, provider)">
</mat-slide-toggle>
</div>
<mat-radio-button [value]="provider"
*ngIf="twoFactorAuth.get(provider).value">
<span class="checkbox-label" translate>security.2fa.main-2fa-method</span>
</mat-radio-button>
</div>
<mat-divider *ngIf="!$last"></mat-divider>
</ng-container>
</mat-radio-group>
</fieldset>
</form>
</mat-card-content>
</mat-card>
</div>

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

@ -0,0 +1,115 @@
/**
* 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 "../../../../../scss/constants";
:host {
.profile-container {
padding: 8px;
}
mat-card.profile-card {
@media #{$mat-gt-sm} {
width: 70%;
}
@media #{$mat-gt-md} {
width: 50%;
}
@media #{$mat-gt-xl} {
width: 45%;
}
.title-container {
margin: 0;
}
.profile-email {
font-size: 16px;
font-weight: 400;
}
.mat-subheader {
line-height: 24px;
color: rgba(0,0,0,0.54);
font-size: 14px;
font-weight: 400;
}
.profile-last-login-ts {
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} {
padding-right: 12px;
}
@media #{$mat-lt-md} {
padding-bottom: 12px;
}
}
mat-checkbox {
@media #{$mat-gt-sm} {
margin-top: 16px;
}
}
}
}
.description {
color: rgba(0, 0, 0, 0.54);
letter-spacing: 0.25px;
margin-right: 8px;
}
.auth-title {
font-weight: 500;
line-height: 20px;
letter-spacing: 0.25px;
margin: 0;
}
.mat-divider-horizontal {
left: 16px;
right: 16px;
width: auto;
}
.provider {
padding: 24px 0;
.provider-title {
font: 400 14px / 16px Roboto, "Helvetica Neue", sans-serif;
letter-spacing: 0.25px;
margin: 0 0 8px;
}
.description {
max-width: 85%;
}
.checkbox-label {
font-size: 14px;
letter-spacing: 0.25px;
opacity: 0.87;
}
.mat-radio-button {
margin-top: 8px;
}
}
}

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

@ -0,0 +1,171 @@
///
/// 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 { 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 { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { ActivatedRoute } from '@angular/router';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DatePipe } from '@angular/common';
import { ClipboardService } from 'ngx-clipboard';
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service';
import {
AccountTwoFaSettings,
twoFactorAuthProvidersData,
TwoFactorAuthProviderType
} from '@shared/models/two-factor-auth.models';
import { authenticationDialogMap } from '@home/pages/security/authentication-dialog/authentication-dialog.map';
@Component({
selector: 'tb-security',
templateUrl: './security.component.html',
styleUrls: ['./security.component.scss']
})
export class SecurityComponent extends PageComponent implements OnInit {
twoFactorAuth: FormGroup;
user: User;
allowTwoFactorProviders: TwoFactorAuthProviderType[] = [];
providersData = twoFactorAuthProvidersData;
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 translate: TranslateService,
private twoFaService: TwoFactorAuthenticationService,
public dialog: MatDialog,
public dialogService: DialogService,
public fb: FormBuilder,
private datePipe: DatePipe,
private clipboardService: ClipboardService) {
super(store);
}
ngOnInit() {
this.buildTwoFactorForm();
this.user = this.route.snapshot.data.user;
this.twoFactorLoad(this.route.snapshot.data.providers);
}
private buildTwoFactorForm() {
this.twoFactorAuth = this.fb.group({
TOTP: [false],
SMS: [false],
EMAIL: [false],
useByDefault: [null]
});
this.twoFactorAuth.get('useByDefault').valueChanges.subscribe(value => {
this.twoFaService.updateTwoFaAccountConfig(value, true, {ignoreLoading: true})
.subscribe(data => this.processTwoFactorAuthConfig(data));
});
}
private twoFactorLoad(providers: TwoFactorAuthProviderType[]) {
if (providers.length) {
this.twoFaService.getAccountTwoFaSettings().subscribe(data => this.processTwoFactorAuthConfig(data));
Object.values(TwoFactorAuthProviderType).forEach(type => {
if (providers.includes(type)) {
this.allowTwoFactorProviders.push(type);
}
});
}
}
private processTwoFactorAuthConfig(setting: AccountTwoFaSettings) {
const configs = setting.configs;
Object.values(TwoFactorAuthProviderType).forEach(provider => {
if (configs[provider]) {
this.twoFactorAuth.get(provider).setValue(true);
if (configs[provider].useByDefault) {
this.twoFactorAuth.get('useByDefault').setValue(provider, {emitEvent: false});
}
} else {
this.twoFactorAuth.get(provider).setValue(false);
}
});
}
trackByProvider(i: number, provider: TwoFactorAuthProviderType) {
return provider;
}
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'
}));
}
}
confirm2FAChange(event: MouseEvent, provider: TwoFactorAuthProviderType) {
event.stopPropagation();
event.preventDefault();
if (this.twoFactorAuth.get(provider).value) {
const providerName = this.translate.instant(`security.2fa.provider.${provider.toLowerCase()}`);
this.dialogService.confirm(
this.translate.instant('security.2fa.disable-2fa-provider-title', {name: providerName}),
this.translate.instant('security.2fa.disable-2fa-provider-text', {name: providerName}),
).subscribe(res => {
if (res) {
this.twoFaService.deleteTwoFaAccountConfig(provider).subscribe(data => this.processTwoFactorAuthConfig(data));
}
});
} else {
const dialogData = provider === TwoFactorAuthProviderType.EMAIL ? {email: this.user.email} : {};
this.dialog.open(authenticationDialogMap.get(provider), {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: dialogData
}).afterClosed().subscribe(res => {
if (res) {
this.twoFactorAuth.get(provider).setValue(res);
this.twoFactorAuth.get('useByDefault').setValue(provider, {emitEvent: false});
}
});
}
}
}

39
ui-ngx/src/app/modules/home/pages/security/security.module.ts

@ -0,0 +1,39 @@
///
/// 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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SecurityComponent } from './security.component';
import { SharedModule } from '@shared/shared.module';
import { SecurityRoutingModule } from './security-routing.module';
import { TotpAuthDialogComponent } from './authentication-dialog/totp-auth-dialog.component';
import { SMSAuthDialogComponent } from '@home/pages/security/authentication-dialog/sms-auth-dialog.component';
import { EmailAuthDialogComponent } from '@home/pages/security/authentication-dialog/email-auth-dialog.component';
@NgModule({
declarations: [
SecurityComponent,
TotpAuthDialogComponent,
SMSAuthDialogComponent,
EmailAuthDialogComponent
],
imports: [
CommonModule,
SharedModule,
SecurityRoutingModule
]
})
export class SecurityModule { }

4
ui-ngx/src/app/shared/components/user-menu.component.html

@ -32,6 +32,10 @@
<mat-icon class="material-icons">account_circle</mat-icon>
<span translate>home.profile</span>
</button>
<button mat-menu-item (click)="openSecurity()">
<mat-icon class="material-icons">lock</mat-icon>
<span translate>security.security</span>
</button>
<button mat-menu-item (click)="logout()">
<mat-icon class="material-icons">exit_to_app</mat-icon>
<span translate>home.logout</span>

4
ui-ngx/src/app/shared/components/user-menu.component.ts

@ -106,6 +106,10 @@ export class UserMenuComponent implements OnInit, OnDestroy {
this.router.navigate(['profile']);
}
openSecurity(): void {
this.router.navigate(['security']);
}
logout(): void {
this.authService.logout();
}

12
ui-ngx/src/app/shared/models/two-factor-auth.models.ts

@ -103,20 +103,20 @@ export const twoFactorAuthProvidersData = new Map<TwoFactorAuthProviderType, Two
[
[
TwoFactorAuthProviderType.TOTP, {
name: 'Authentication app',
description: 'Use apps like Google Authenticator, Authy, or Duo on your phone to authenticate. It will generate a security code for logging in.'
name: 'security.2fa.provider.totp',
description: 'security.2fa.provider.totp-description'
}
],
[
TwoFactorAuthProviderType.SMS, {
name: 'SMS',
description: 'Use your phone to authenticate. We\'ll send you a security code via SMS message when you log in.'
name: 'security.2fa.provider.sms',
description: 'security.2fa.provider.sms-description'
}
],
[
TwoFactorAuthProviderType.EMAIL, {
name: 'Email',
description: 'Use a security code sent to your email address to authenticate.'
name: 'security.2fa.provider.email',
description: 'security.2fa.provider.email-description'
}
],
]

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

@ -123,7 +123,6 @@
"number-from-required": "Phone Number From is required.",
"number-to": "Phone Number To",
"number-to-required": "Phone Number To is required.",
"phone-number": "Phone Number",
"phone-number-hint": "Phone Number in E.164 format, ex. +19995550123",
"phone-number-hint-twilio": "Phone Number in E.164 format/Phone Number's SID/Messaging Service SID, ex. +19995550123/PNXXX/MGXXX",
"phone-number-pattern": "Invalid phone number. Should be in E.164 format, ex. +19995550123.",
@ -314,33 +313,33 @@
"scheme-korean-graphic-character-set": "14 - Korean Graphic Character Set (KS C 5601/KS X 1001)"
},
"2fa": {
"2fa": "Two-factor authentication",
"available-providers": "Available providers",
"general-setting": "General setting",
"issuer-name": "Issuer name",
"issuer-name-required": "Issuer name is required.",
"max-verification-failures-before-user-lockout": "Max verification failures before user lockout",
"max-verification-failures-before-user-lockout-pattern": "Max verification failures must be a positive integer.",
"max-verification-failures-before-user-lockout-required": "Max verification failures is required.",
"number-of-attempts": "Number of attempts",
"number-of-attempts-pattern": "Number of attempts must be a positive integer.",
"number-of-attempts-required": "Number of attempts is required.",
"provider": "Provider",
"total-allowed-time-for-verification": "Total allowed time for verification (sec)",
"total-allowed-time-for-verification-pattern": "Total allowed time must be a positive integer.",
"total-allowed-time-for-verification-required": "Total allowed time is required.",
"use-system-two-factor-auth-settings": "Use system two factor auth settings",
"verification-code-check-rate-limit": "Verification code check rate limit",
"verification-code-lifetime": "Verification code lifetime (sec)",
"verification-code-lifetime-pattern": "Verification code lifetime must be a positive integer.",
"verification-code-lifetime-required": "Verification code lifetime is required.",
"verification-code-send-rate-limit": "Verification code send rate limit",
"verification-message-template": "Verification message template",
"verification-message-template-pattern": "Verification message need to contains pattern: ${verificationCode}",
"verification-message-template-required": "Verification message template is required.",
"within-time": "Within time (sec)",
"within-time-pattern": "Time must be a positive integer.",
"within-time-required": "Time is required."
"2fa": "Two-factor authentication",
"available-providers": "Available providers",
"general-setting": "General setting",
"issuer-name": "Issuer name",
"issuer-name-required": "Issuer name is required.",
"max-verification-failures-before-user-lockout": "Max verification failures before user lockout",
"max-verification-failures-before-user-lockout-pattern": "Max verification failures must be a positive integer.",
"max-verification-failures-before-user-lockout-required": "Max verification failures is required.",
"number-of-attempts": "Number of attempts",
"number-of-attempts-pattern": "Number of attempts must be a positive integer.",
"number-of-attempts-required": "Number of attempts is required.",
"provider": "Provider",
"total-allowed-time-for-verification": "Total allowed time for verification (sec)",
"total-allowed-time-for-verification-pattern": "Total allowed time must be a positive integer.",
"total-allowed-time-for-verification-required": "Total allowed time is required.",
"use-system-two-factor-auth-settings": "Use system two factor auth settings",
"verification-code-check-rate-limit": "Verification code check rate limit",
"verification-code-lifetime": "Verification code lifetime (sec)",
"verification-code-lifetime-pattern": "Verification code lifetime must be a positive integer.",
"verification-code-lifetime-required": "Verification code lifetime is required.",
"verification-code-send-rate-limit": "Verification code send rate limit",
"verification-message-template": "Verification message template",
"verification-message-template-pattern": "Verification message need to contains pattern: ${verificationCode}",
"verification-message-template-required": "Verification message template is required.",
"within-time": "Within time (sec)",
"within-time-pattern": "Time must be a positive integer.",
"within-time-required": "Time is required."
}
},
"alarm": {
@ -2564,6 +2563,51 @@
"tokenCopiedSuccessMessage": "JWT token has been copied to clipboard",
"tokenCopiedWarnMessage": "JWT token is expired! Please, refresh the page."
},
"security": {
"security": "Security",
"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.",
"authenticate-with": "You can authenticate with:",
"disable-2fa-provider-text": "Disabling {{name}} will make your account less secure",
"disable-2fa-provider-title": "Are you sure you want to disable {{name}}?",
"main-2fa-method": "Use as main two-factor authentication method",
"dialog": {
"activate": "Activate",
"activation-step-description-email": "The next time you login in, you will be prompted to enter the security code that will be sent to your email address.",
"activation-step-description-sms": "The next time you login in, you will be prompted to enter the security code that will be sent to the phone number.",
"activation-step-description-totp": "The next time you login in, you will need to provide a two-factor authentication code.",
"activation-step-label": "Activation",
"email-step-description": "Enter an email to use as your authenticator.",
"email-step-label": "Email",
"enable-email-title": "Enable email authenticator",
"enable-sms-title": "Enable SMS authenticator",
"enable-totp-title": "Enable authenticator app",
"enter-verification-code": "Enter the 6-digit code here",
"next": "Next",
"scan-qr-code": "Scan this QR code with your verification app",
"send-code": "Send code",
"sms-step-description": "Enter a phone number to use as your authenticator.",
"sms-step-label": "Phone Number",
"success": "Success!",
"totp-step-description-install": "You can install apps like Google Authenticator, Authy, or Duo.",
"totp-step-description-open": "Open the authenticator app on your mobile phone.",
"totp-step-label": "Get app",
"verification-code": "6-digit code",
"verification-code-invalid": "Invalid verification code format",
"verification-step-description": "Enter a 6-digit code we just sent to {{address}}",
"verification-step-label": "Verification"
},
"provider": {
"email": "Email",
"email-description": "Use a security code sent to your email address to authenticate.",
"sms": "SMS",
"sms-description": "Use your phone to authenticate. We'll send you a security code via SMS message when you log in.",
"totp": "Authentication app",
"totp-description": "Use apps like Google Authenticator, Authy, or Duo on your phone to authenticate. It will generate a security code for logging in."
}
}
},
"relation": {
"relations": "Relations",
"direction": "Direction",

Loading…
Cancel
Save