UI: Add 2FA profile setting form and added support Email 2FA provider

This commit is contained in:
Vladyslav_Prykhodko 2022-05-06 17:52:22 +03:00
parent 4480badd78
commit 70cbe29a5d
20 changed files with 932 additions and 27 deletions

View File

@ -18,7 +18,12 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { TwoFactorAuthSettings } from '@shared/models/two-factor-auth.models'; import {
AccountTwoFaSettings,
TwoFactorAuthAccountConfig,
TwoFactorAuthProviderType,
TwoFactorAuthSettings
} from '@shared/models/two-factor-auth.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -38,4 +43,37 @@ export class TwoFactorAuthenticationService {
return this.http.post<TwoFactorAuthSettings>(`/api/2fa/settings`, settings, defaultHttpOptionsFromConfig(config)); return this.http.post<TwoFactorAuthSettings>(`/api/2fa/settings`, settings, defaultHttpOptionsFromConfig(config));
} }
getAvailableTwoFaProviders(config?: RequestConfig): Observable<Array<TwoFactorAuthProviderType>> {
return this.http.get<Array<TwoFactorAuthProviderType>>(`/api/2fa/providers`, defaultHttpOptionsFromConfig(config));
}
generateTwoFaAccountConfig(providerType: TwoFactorAuthProviderType, config?: RequestConfig): Observable<TwoFactorAuthAccountConfig> {
return this.http.post<TwoFactorAuthAccountConfig>(`/api/2fa/account/config/generate?providerType=${providerType}`,
defaultHttpOptionsFromConfig(config));
}
getAccountTwoFaSettings(config?: RequestConfig): Observable<AccountTwoFaSettings> {
return this.http.get<AccountTwoFaSettings>(`/api/2fa/account/settings`, defaultHttpOptionsFromConfig(config));
}
updateTwoFaAccountConfig(providerType: TwoFactorAuthProviderType, useByDefault: boolean,
config?: RequestConfig): Observable<AccountTwoFaSettings> {
return this.http.put<AccountTwoFaSettings>(`/api/2fa/account/config?providerType=${providerType}`, {useByDefault},
defaultHttpOptionsFromConfig(config));
}
submitTwoFaAccountConfig(authConfig: TwoFactorAuthAccountConfig, config?: RequestConfig): Observable<any> {
return this.http.post(`/api/2fa/account/config/submit`, authConfig, defaultHttpOptionsFromConfig(config));
}
verifyAndSaveTwoFaAccountConfig(authConfig: TwoFactorAuthAccountConfig, verificationCode: number,
config?: RequestConfig): Observable<any> {
return this.http.post(`/api/2fa/account/config?verificationCode=${verificationCode}`, authConfig, defaultHttpOptionsFromConfig(config));
}
deleteTwoFaAccountConfig(providerType: TwoFactorAuthProviderType, config?: RequestConfig): Observable<AccountTwoFaSettings> {
return this.http.delete<AccountTwoFaSettings>(`/api/2fa/account/config?providerType=${providerType}`,
defaultHttpOptionsFromConfig(config));
}
} }

View File

@ -81,7 +81,7 @@
<ng-container formArrayName="providers"> <ng-container formArrayName="providers">
<div class="container"> <div class="container">
<mat-accordion multi> <mat-accordion multi>
<mat-expansion-panel *ngFor="let provider of providersForm.controls; let i = index"> <mat-expansion-panel *ngFor="let provider of providersForm.controls; let i = index; trackBy: trackByElement">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title fxLayoutAlign="start center"> <mat-panel-title fxLayoutAlign="start center">
{{ provider.value.providerType }} {{ provider.value.providerType }}
@ -102,10 +102,10 @@
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.provider</mat-label> <mat-label translate>admin.2fa.provider</mat-label>
<mat-select formControlName="providerType"> <mat-select formControlName="providerType">
<mat-option *ngFor="let twoFactorAuthProviderType of twoFactorAuthProviderTypes" <mat-option *ngFor="let provider of twoFactorAuthProviderTypes; trackBy: trackByElement"
[value]="twoFactorAuthProviderType" [value]="provider"
[disabled]="selectedTypes(twoFactorAuthProviderType[twoFactorAuthProviderType], i)"> [disabled]="selectedTypes(twoFactorAuthProviderType[provider], i)">
{{ twoFactorAuthProviderType }} {{ provider }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
@ -144,6 +144,19 @@
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<div *ngSwitchCase="twoFactorAuthProviderType.EMAIL">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.verification-code-lifetime</mat-label>
<input matInput formControlName="verificationCodeLifetime" type="number" step="1" min="1" required>
<mat-error *ngIf="provider.get('verificationCodeLifetime').hasError('required')">
{{ "admin.2fa.verification-code-lifetime-required" | translate }}
</mat-error>
<mat-error *ngIf="provider.get('verificationCodeLifetime').hasError('min') ||
provider.get('verificationCodeLifetime').hasError('pattern')">
{{ "admin.2fa.verification-code-lifetime-pattern" | translate }}
</mat-error>
</mat-form-field>
</div>
</ng-container> </ng-container>
</section> </section>
</ng-template> </ng-template>
@ -159,7 +172,7 @@
|| providersForm.length == twoFactorAuthProviderTypes.length || (isLoading$ | async)" || providersForm.length == twoFactorAuthProviderTypes.length || (isLoading$ | async)"
(click)="addProvider()"> (click)="addProvider()">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
<span translate>action.add</span> <span translate>admin.2fa.add-provider</span>
</button> </button>
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"
[disabled]="(isLoading$ | async) || twoFaFormGroup.invalid || !twoFaFormGroup.dirty" [disabled]="(isLoading$ | async) || twoFaFormGroup.invalid || !twoFaFormGroup.dirty"

View File

@ -20,7 +20,7 @@ import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
@ -108,13 +108,14 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
settings.providers.forEach(() => { settings.providers.forEach(() => {
this.addProvider(); this.addProvider();
}); });
this.twoFaFormGroup.patchValue(settings, {emitEvent: false}); this.twoFaFormGroup.patchValue(settings);
this.twoFaFormGroup.markAsPristine();
} }
addProvider() { addProvider() {
const newProviders = this.fb.group({ const newProviders = this.fb.group({
providerType: [TwoFactorAuthProviderType.TOTP], providerType: [TwoFactorAuthProviderType.TOTP],
issuerName: ['', Validators.required], issuerName: ['ThingsBoard', Validators.required],
smsVerificationMessageTemplate: [{ smsVerificationMessageTemplate: [{
value: 'Verification code: ${verificationCode}', value: 'Verification code: ${verificationCode}',
disabled: true disabled: true
@ -143,16 +144,21 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
newProviders.get('smsVerificationMessageTemplate').disable({emitEvent: false}); newProviders.get('smsVerificationMessageTemplate').disable({emitEvent: false});
newProviders.get('verificationCodeLifetime').disable({emitEvent: false}); newProviders.get('verificationCodeLifetime').disable({emitEvent: false});
break; break;
case TwoFactorAuthProviderType.EMAIL:
newProviders.get('issuerName').disable({emitEvent: false});
newProviders.get('smsVerificationMessageTemplate').disable({emitEvent: false});
newProviders.get('verificationCodeLifetime').enable({emitEvent: false});
break;
} }
}); });
if (this.providersForm.length) { if (this.providersForm.length) {
const selectProvidersType = this.providersForm.value[0].providerType; const selectedProviderTypes = this.providersForm.value.map(providers => providers.providerType);
if (selectProvidersType === TwoFactorAuthProviderType.TOTP) { const allowProviders = this.twoFactorAuthProviderTypes.filter(provider => !selectedProviderTypes.includes(provider));
newProviders.get('providerType').setValue(TwoFactorAuthProviderType.SMS); newProviders.get('providerType').setValue(allowProviders[0]);
newProviders.updateValueAndValidity(); newProviders.updateValueAndValidity();
}
} }
this.providersForm.push(newProviders); this.providersForm.push(newProviders);
this.providersForm.markAsDirty();
} }
removeProviders($event: Event, index: number): void { removeProviders($event: Event, index: number): void {
@ -169,6 +175,10 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
return this.twoFaFormGroup.get('providers') as FormArray; return this.twoFaFormGroup.get('providers') as FormArray;
} }
trackByElement(i: number, item: any) {
return item;
}
selectedTypes(type: TwoFactorAuthProviderType, index: number): boolean { selectedTypes(type: TwoFactorAuthProviderType, index: number): boolean {
const selectedProviderTypes: TwoFactorAuthProviderType[] = this.providersForm.value.map(providers => providers.providerType); const selectedProviderTypes: TwoFactorAuthProviderType[] = this.providersForm.value.map(providers => providers.providerType);
selectedProviderTypes.splice(index, 1); selectedProviderTypes.splice(index, 1);

View File

@ -0,0 +1,104 @@
<!--
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 dont 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>

View File

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

View File

@ -0,0 +1,93 @@
///
/// 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, Inject, ViewChild } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service';
import { TwoFactorAuthAccountConfig, TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models';
import { MatStepper } from '@angular/material/stepper';
export interface EmailAuthDialogData {
email: string;
}
@Component({
selector: 'tb-email-auth-dialog',
templateUrl: './email-auth-dialog.component.html',
styleUrls: ['./email-auth-dialog.component.scss']
})
export class EmailAuthDialogComponent extends DialogComponent<EmailAuthDialogComponent> {
private authAccountConfig: TwoFactorAuthAccountConfig;
emailConfigForm: FormGroup;
emailVerificationForm: FormGroup;
@ViewChild('stepper', {static: false}) stepper: MatStepper;
constructor(protected store: Store<AppState>,
protected router: Router,
private twoFaService: TwoFactorAuthenticationService,
@Inject(MAT_DIALOG_DATA) public data: EmailAuthDialogData,
public dialogRef: MatDialogRef<EmailAuthDialogComponent>,
public fb: FormBuilder) {
super(store, router, dialogRef);
this.emailConfigForm = this.fb.group({
email: [this.data.email, [Validators.required, Validators.email]]
});
this.emailVerificationForm = this.fb.group({
verificationCode: ['', [
Validators.required,
Validators.minLength(6),
Validators.maxLength(6),
Validators.pattern(/^\d*$/)
]]
});
}
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();
});
break;
case 1:
this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig,
this.emailVerificationForm.get('verificationCode').value).subscribe(() => {
this.stepper.next();
});
break;
}
}
closeDialog() {
return this.dialogRef.close(this.stepper.selectedIndex > 1);
}
}

View File

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

View File

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

View File

@ -0,0 +1,91 @@
///
/// 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, ViewChild } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service';
import { TwoFactorAuthAccountConfig, TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models';
import { phoneNumberPattern } from '@shared/models/settings.models';
import { MatStepper } from '@angular/material/stepper';
@Component({
selector: 'tb-sms-auth-dialog',
templateUrl: './sms-auth-dialog.component.html',
styleUrls: ['./sms-auth-dialog.component.scss']
})
export class SMSAuthDialogComponent extends DialogComponent<SMSAuthDialogComponent> {
private authAccountConfig: TwoFactorAuthAccountConfig;
phoneNumberPattern = phoneNumberPattern;
smsConfigForm: FormGroup;
smsVerificationForm: FormGroup;
@ViewChild('stepper', {static: false}) stepper: MatStepper;
constructor(protected store: Store<AppState>,
protected router: Router,
private twoFaService: TwoFactorAuthenticationService,
public dialogRef: MatDialogRef<SMSAuthDialogComponent>,
public fb: FormBuilder) {
super(store, router, dialogRef);
this.smsConfigForm = this.fb.group({
phone: ['', [Validators.required, Validators.pattern(phoneNumberPattern)]]
});
this.smsVerificationForm = this.fb.group({
verificationCode: ['', [
Validators.required,
Validators.minLength(6),
Validators.maxLength(6),
Validators.pattern(/^\d*$/)
]]
});
}
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();
});
break;
case 1:
this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig,
this.smsVerificationForm.get('verificationCode').value).subscribe(() => {
this.stepper.next();
});
break;
}
}
closeDialog() {
return this.dialogRef.close(this.stepper.selectedIndex > 1);
}
}

View File

@ -0,0 +1,97 @@
<!--
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>

View File

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

View File

@ -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.
///
import { Component, ElementRef, ViewChild } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service';
import { TotpTwoFactorAuthAccountConfig, TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models';
import { MatStepper } from '@angular/material/stepper';
@Component({
selector: 'tb-totp-auth-dialog',
templateUrl: './totp-auth-dialog.component.html',
styleUrls: ['./totp-auth-dialog.component.scss']
})
export class TotpAuthDialogComponent extends DialogComponent<TotpAuthDialogComponent> {
private authAccountConfig: TotpTwoFactorAuthAccountConfig;
totpConfigForm: FormGroup;
totpAuthURL: string;
@ViewChild('stepper', {static: false}) stepper: MatStepper;
@ViewChild('canvas', {static: false}) canvasRef: ElementRef<HTMLCanvasElement>;
constructor(protected store: Store<AppState>,
protected router: Router,
private twoFaService: TwoFactorAuthenticationService,
public dialogRef: MatDialogRef<TotpAuthDialogComponent>,
public fb: FormBuilder) {
super(store, router, dialogRef);
this.twoFaService.generateTwoFaAccountConfig(TwoFactorAuthProviderType.TOTP).subscribe(accountConfig => {
this.authAccountConfig = accountConfig as TotpTwoFactorAuthAccountConfig;
this.totpAuthURL = this.authAccountConfig.authUrl;
this.authAccountConfig.useByDefault = true;
import('qrcode').then((QRCode) => {
QRCode.toCanvas(this.canvasRef.nativeElement, this.totpAuthURL);
this.canvasRef.nativeElement.style.width = 'auto';
this.canvasRef.nativeElement.style.height = 'auto';
});
});
this.totpConfigForm = this.fb.group({
verificationCode: ['', [
Validators.required,
Validators.minLength(6),
Validators.maxLength(6),
Validators.pattern(/^\d*$/)
]]
});
}
onSaveConfig() {
this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig,
this.totpConfigForm.get('verificationCode').value).subscribe((res) => {
this.stepper.next();
});
}
closeDialog() {
return this.dialogRef.close(this.stepper.selectedIndex > 1);
}
}

View File

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

View File

@ -93,8 +93,7 @@
</button> </button>
<span class="profile-btn-subtext">{{ expirationJwtData }}</span> <span class="profile-btn-subtext">{{ expirationJwtData }}</span>
</div> </div>
<div fxLayout="row" class="layout-wrap"> <div fxLayout="row" fxLayoutAlign="end start">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || profile.invalid || !profile.dirty"> [disabled]="(isLoading$ | async) || profile.invalid || !profile.dirty">
@ -105,4 +104,66 @@
</form> </form>
</mat-card-content> </mat-card-content>
</mat-card> </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 youll 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 youll 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. Youll 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> </div>

View File

@ -59,4 +59,21 @@
} }
} }
} }
.description {
padding-bottom: 8px;
color: #808080;
margin-right: 8px;
}
.provider {
padding: 14px 0;
.mat-h3 {
margin-bottom: 8px;
}
.checkbox-label {
font-size: 14px;
}
}
} }

View File

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { UserService } from '@core/http/user.service'; import { UserService } from '@core/http/user.service';
import { AuthUser, User } from '@shared/models/user.model'; import { AuthUser, User } from '@shared/models/user.model';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
@ -37,6 +37,12 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { ClipboardService } from 'ngx-clipboard'; 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({ @Component({
selector: 'tb-profile', selector: 'tb-profile',
@ -47,8 +53,25 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
authorities = Authority; authorities = Authority;
profile: FormGroup; profile: FormGroup;
twoFactorAuth: FormGroup;
user: User; user: User;
languageList = env.supportedLangs; 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; private readonly authUser: AuthUser;
get jwtToken(): string { get jwtToken(): string {
@ -69,6 +92,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
private userService: UserService, private userService: UserService,
private authService: AuthService, private authService: AuthService,
private translate: TranslateService, private translate: TranslateService,
private twoFaService: TwoFactorAuthenticationService,
public dialog: MatDialog, public dialog: MatDialog,
public dialogService: DialogService, public dialogService: DialogService,
public fb: FormBuilder, public fb: FormBuilder,
@ -80,10 +104,12 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
ngOnInit() { ngOnInit() {
this.buildProfileForm(); this.buildProfileForm();
this.buildTwoFactorForm();
this.userLoaded(this.route.snapshot.data.user); this.userLoaded(this.route.snapshot.data.user);
this.twoFactorLoad(this.route.snapshot.data.providers);
} }
buildProfileForm() { private buildProfileForm() {
this.profile = this.fb.group({ this.profile = this.fb.group({
email: ['', [Validators.required, Validators.email]], email: ['', [Validators.required, Validators.email]],
firstName: [''], firstName: [''],
@ -94,6 +120,19 @@ 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 { save(): void {
this.user = {...this.user, ...this.profile.value}; this.user = {...this.user, ...this.profile.value};
if (!this.user.additionalInfo) { if (!this.user.additionalInfo) {
@ -128,7 +167,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
}); });
} }
userLoaded(user: User) { private userLoaded(user: User) {
this.user = user; this.user = user;
this.profile.reset(user); this.profile.reset(user);
let lang; let lang;
@ -151,6 +190,37 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
this.profile.get('homeDashboardHideToolbar').setValue(homeDashboardHideToolbar); 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 { confirmForm(): FormGroup {
return this.profile; return this.profile;
} }
@ -179,4 +249,31 @@ 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});
}
});
}
}
} }

View File

@ -20,11 +20,17 @@ 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'; 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({ @NgModule({
declarations: [ declarations: [
ProfileComponent, ProfileComponent,
ChangePasswordDialogComponent ChangePasswordDialogComponent,
TotpAuthDialogComponent,
SMSAuthDialogComponent,
EmailAuthDialogComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

View File

@ -63,6 +63,7 @@ export const HelpLinks = {
smsProviderSettings: helpBaseUrl + '/docs/user-guide/ui/sms-provider-settings', smsProviderSettings: helpBaseUrl + '/docs/user-guide/ui/sms-provider-settings',
securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings', securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings',
oauth2Settings: helpBaseUrl + '/docs/user-guide/oauth-2-support/', oauth2Settings: helpBaseUrl + '/docs/user-guide/oauth-2-support/',
twoFactorAuthSettings: helpBaseUrl + '/docs/',
ruleEngine: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/overview/', ruleEngine: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/overview/',
ruleNodeCheckRelation: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node', ruleNodeCheckRelation: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node',
ruleNodeCheckExistenceFields: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-existence-fields-node', ruleNodeCheckExistenceFields: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-existence-fields-node',

View File

@ -23,7 +23,8 @@ export interface TwoFactorAuthSettings {
verificationCodeSendRateLimit: string; verificationCodeSendRateLimit: string;
} }
export type TwoFactorAuthProviderConfig = Partial<TotpTwoFactorAuthProviderConfig | SmsTwoFactorAuthProviderConfig>; export type TwoFactorAuthProviderConfig = Partial<TotpTwoFactorAuthProviderConfig | SmsTwoFactorAuthProviderConfig |
EmailTwoFactorAuthProviderConfig>;
export interface TotpTwoFactorAuthProviderConfig { export interface TotpTwoFactorAuthProviderConfig {
providerType: TwoFactorAuthProviderType; providerType: TwoFactorAuthProviderType;
@ -36,7 +37,37 @@ export interface SmsTwoFactorAuthProviderConfig {
verificationCodeLifetime: number; verificationCodeLifetime: number;
} }
export interface EmailTwoFactorAuthProviderConfig {
providerType: TwoFactorAuthProviderType;
verificationCodeLifetime: number;
}
export enum TwoFactorAuthProviderType{ export enum TwoFactorAuthProviderType{
TOTP = 'TOTP', TOTP = 'TOTP',
SMS = 'SMS' SMS = 'SMS',
EMAIL = 'EMAIL'
}
interface GeneralTwoFactorAuthAccountConfig {
providerType: TwoFactorAuthProviderType;
useByDefault: boolean;
}
export interface TotpTwoFactorAuthAccountConfig extends GeneralTwoFactorAuthAccountConfig {
authUrl: string;
}
export interface SmsTwoFactorAuthAccountConfig extends GeneralTwoFactorAuthAccountConfig {
phoneNumber: string;
}
export interface EmailTwoFactorAuthAccountConfig extends GeneralTwoFactorAuthAccountConfig {
email: string;
}
export type TwoFactorAuthAccountConfig = TotpTwoFactorAuthAccountConfig | SmsTwoFactorAuthAccountConfig | EmailTwoFactorAuthAccountConfig;
export interface AccountTwoFaSettings {
configs?: {TwoFactorAuthProviderType: TwoFactorAuthAccountConfig};
} }

View File

@ -255,6 +255,7 @@
}, },
"2fa": { "2fa": {
"2fa": "Two-factor authentication", "2fa": "Two-factor authentication",
"add-provider": "Add provider",
"available-providers": "Available providers:", "available-providers": "Available providers:",
"issuer-name": "Issuer name", "issuer-name": "Issuer name",
"issuer-name-required": "Issuer name is required.", "issuer-name-required": "Issuer name is required.",
@ -262,14 +263,14 @@
"max-verification-failures-before-user-lockout-pattern": "Max verification failures must be a positive integer.", "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.", "max-verification-failures-before-user-lockout-required": "Max verification failures is required.",
"provider": "Provider", "provider": "Provider",
"total-allowed-time-for-verification": "Total allowed time for verification", "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-pattern": "Total allowed time must be a positive integer.",
"total-allowed-time-for-verification-required": "Total allowed time is required.", "total-allowed-time-for-verification-required": "Total allowed time is required.",
"use-system-two-factor-auth-settings": "Use system two factor auth settings", "use-system-two-factor-auth-settings": "Use system two factor auth settings",
"verification-code-check-rate-limit": "Verification code check rate limit", "verification-code-check-rate-limit": "Verification code check rate limit",
"verification-code-check-rate-limit-pattern": "Verification code check limit has invalid format", "verification-code-check-rate-limit-pattern": "Verification code check limit has invalid format",
"verification-code-check-rate-limit-required": "Verification code check rate limit is required.", "verification-code-check-rate-limit-required": "Verification code check rate limit is required.",
"verification-code-lifetime": "Verification code lifetime", "verification-code-lifetime": "Verification code lifetime (sec)",
"verification-code-lifetime-pattern": "Verification code lifetime must be a positive integer.", "verification-code-lifetime-pattern": "Verification code lifetime must be a positive integer.",
"verification-code-lifetime-required": "Verification code lifetime is required.", "verification-code-lifetime-required": "Verification code lifetime is required.",
"verification-code-send-rate-limit": "Verification code send rate limit", "verification-code-send-rate-limit": "Verification code send rate limit",