UI: Add 2FA profile setting form and added support Email 2FA provider
This commit is contained in:
parent
4480badd78
commit
70cbe29a5d
@ -18,7 +18,12 @@ import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
|
||||
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({
|
||||
providedIn: 'root'
|
||||
@ -38,4 +43,37 @@ export class TwoFactorAuthenticationService {
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@
|
||||
<ng-container formArrayName="providers">
|
||||
<div class="container">
|
||||
<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-panel-title fxLayoutAlign="start center">
|
||||
{{ provider.value.providerType }}
|
||||
@ -102,10 +102,10 @@
|
||||
<mat-form-field fxFlex class="mat-block">
|
||||
<mat-label translate>admin.2fa.provider</mat-label>
|
||||
<mat-select formControlName="providerType">
|
||||
<mat-option *ngFor="let twoFactorAuthProviderType of twoFactorAuthProviderTypes"
|
||||
[value]="twoFactorAuthProviderType"
|
||||
[disabled]="selectedTypes(twoFactorAuthProviderType[twoFactorAuthProviderType], i)">
|
||||
{{ twoFactorAuthProviderType }}
|
||||
<mat-option *ngFor="let provider of twoFactorAuthProviderTypes; trackBy: trackByElement"
|
||||
[value]="provider"
|
||||
[disabled]="selectedTypes(twoFactorAuthProviderType[provider], i)">
|
||||
{{ provider }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@ -144,6 +144,19 @@
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</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>
|
||||
</section>
|
||||
</ng-template>
|
||||
@ -159,7 +172,7 @@
|
||||
|| providersForm.length == twoFactorAuthProviderTypes.length || (isLoading$ | async)"
|
||||
(click)="addProvider()">
|
||||
<mat-icon>add</mat-icon>
|
||||
<span translate>action.add</span>
|
||||
<span translate>admin.2fa.add-provider</span>
|
||||
</button>
|
||||
<button mat-button mat-raised-button color="primary"
|
||||
[disabled]="(isLoading$ | async) || twoFaFormGroup.invalid || !twoFaFormGroup.dirty"
|
||||
|
||||
@ -20,7 +20,7 @@ import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
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 { TranslateService } from '@ngx-translate/core';
|
||||
import { WINDOW } from '@core/services/window.service';
|
||||
@ -108,13 +108,14 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
||||
settings.providers.forEach(() => {
|
||||
this.addProvider();
|
||||
});
|
||||
this.twoFaFormGroup.patchValue(settings, {emitEvent: false});
|
||||
this.twoFaFormGroup.patchValue(settings);
|
||||
this.twoFaFormGroup.markAsPristine();
|
||||
}
|
||||
|
||||
addProvider() {
|
||||
const newProviders = this.fb.group({
|
||||
providerType: [TwoFactorAuthProviderType.TOTP],
|
||||
issuerName: ['', Validators.required],
|
||||
issuerName: ['ThingsBoard', Validators.required],
|
||||
smsVerificationMessageTemplate: [{
|
||||
value: 'Verification code: ${verificationCode}',
|
||||
disabled: true
|
||||
@ -143,16 +144,21 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
||||
newProviders.get('smsVerificationMessageTemplate').disable({emitEvent: false});
|
||||
newProviders.get('verificationCodeLifetime').disable({emitEvent: false});
|
||||
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) {
|
||||
const selectProvidersType = this.providersForm.value[0].providerType;
|
||||
if (selectProvidersType === TwoFactorAuthProviderType.TOTP) {
|
||||
newProviders.get('providerType').setValue(TwoFactorAuthProviderType.SMS);
|
||||
newProviders.updateValueAndValidity();
|
||||
}
|
||||
const selectedProviderTypes = this.providersForm.value.map(providers => providers.providerType);
|
||||
const allowProviders = this.twoFactorAuthProviderTypes.filter(provider => !selectedProviderTypes.includes(provider));
|
||||
newProviders.get('providerType').setValue(allowProviders[0]);
|
||||
newProviders.updateValueAndValidity();
|
||||
}
|
||||
this.providersForm.push(newProviders);
|
||||
this.providersForm.markAsDirty();
|
||||
}
|
||||
|
||||
removeProviders($event: Event, index: number): void {
|
||||
@ -169,6 +175,10 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
||||
return this.twoFaFormGroup.get('providers') as FormArray;
|
||||
}
|
||||
|
||||
trackByElement(i: number, item: any) {
|
||||
return item;
|
||||
}
|
||||
|
||||
selectedTypes(type: TwoFactorAuthProviderType, index: number): boolean {
|
||||
const selectedProviderTypes: TwoFactorAuthProviderType[] = this.providersForm.value.map(providers => providers.providerType);
|
||||
selectedProviderTypes.splice(index, 1);
|
||||
|
||||
@ -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 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>
|
||||
@ -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.
|
||||
*/
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 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>
|
||||
@ -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.
|
||||
*/
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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>
|
||||
@ -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.
|
||||
*/
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -26,6 +26,8 @@ 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> {
|
||||
@ -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 = [
|
||||
{
|
||||
path: 'profile',
|
||||
@ -54,7 +67,8 @@ const routes: Routes = [
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
user: UserProfileResolver
|
||||
user: UserProfileResolver,
|
||||
providers: UserTwoFAProvidersResolver
|
||||
}
|
||||
}
|
||||
];
|
||||
@ -63,7 +77,8 @@ const routes: Routes = [
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
providers: [
|
||||
UserProfileResolver
|
||||
UserProfileResolver,
|
||||
UserTwoFAProvidersResolver
|
||||
]
|
||||
})
|
||||
export class ProfileRoutingModule { }
|
||||
|
||||
@ -93,8 +93,7 @@
|
||||
</button>
|
||||
<span class="profile-btn-subtext">{{ expirationJwtData }}</span>
|
||||
</div>
|
||||
<div fxLayout="row" class="layout-wrap">
|
||||
<span fxFlex></span>
|
||||
<div fxLayout="row" fxLayoutAlign="end start">
|
||||
<button mat-button mat-raised-button color="primary"
|
||||
type="submit"
|
||||
[disabled]="(isLoading$ | async) || profile.invalid || !profile.dirty">
|
||||
@ -105,4 +104,66 @@
|
||||
</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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
/// 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 { AuthUser, User } from '@shared/models/user.model';
|
||||
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 { 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',
|
||||
@ -47,8 +53,25 @@ 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 {
|
||||
@ -69,6 +92,7 @@ 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,
|
||||
@ -80,10 +104,12 @@ 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);
|
||||
}
|
||||
|
||||
buildProfileForm() {
|
||||
private buildProfileForm() {
|
||||
this.profile = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
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 {
|
||||
this.user = {...this.user, ...this.profile.value};
|
||||
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.profile.reset(user);
|
||||
let lang;
|
||||
@ -151,6 +190,37 @@ 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;
|
||||
}
|
||||
@ -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});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,11 +20,17 @@ 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
|
||||
ChangePasswordDialogComponent,
|
||||
TotpAuthDialogComponent,
|
||||
SMSAuthDialogComponent,
|
||||
EmailAuthDialogComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@ -63,6 +63,7 @@ export const HelpLinks = {
|
||||
smsProviderSettings: helpBaseUrl + '/docs/user-guide/ui/sms-provider-settings',
|
||||
securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings',
|
||||
oauth2Settings: helpBaseUrl + '/docs/user-guide/oauth-2-support/',
|
||||
twoFactorAuthSettings: helpBaseUrl + '/docs/',
|
||||
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',
|
||||
ruleNodeCheckExistenceFields: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-existence-fields-node',
|
||||
|
||||
@ -23,7 +23,8 @@ export interface TwoFactorAuthSettings {
|
||||
verificationCodeSendRateLimit: string;
|
||||
}
|
||||
|
||||
export type TwoFactorAuthProviderConfig = Partial<TotpTwoFactorAuthProviderConfig | SmsTwoFactorAuthProviderConfig>;
|
||||
export type TwoFactorAuthProviderConfig = Partial<TotpTwoFactorAuthProviderConfig | SmsTwoFactorAuthProviderConfig |
|
||||
EmailTwoFactorAuthProviderConfig>;
|
||||
|
||||
export interface TotpTwoFactorAuthProviderConfig {
|
||||
providerType: TwoFactorAuthProviderType;
|
||||
@ -36,7 +37,37 @@ export interface SmsTwoFactorAuthProviderConfig {
|
||||
verificationCodeLifetime: number;
|
||||
}
|
||||
|
||||
export interface EmailTwoFactorAuthProviderConfig {
|
||||
providerType: TwoFactorAuthProviderType;
|
||||
verificationCodeLifetime: number;
|
||||
}
|
||||
|
||||
export enum TwoFactorAuthProviderType{
|
||||
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};
|
||||
}
|
||||
|
||||
@ -255,6 +255,7 @@
|
||||
},
|
||||
"2fa": {
|
||||
"2fa": "Two-factor authentication",
|
||||
"add-provider": "Add provider",
|
||||
"available-providers": "Available providers:",
|
||||
"issuer-name": "Issuer name",
|
||||
"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-required": "Max verification failures is required.",
|
||||
"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-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-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-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-required": "Verification code lifetime is required.",
|
||||
"verification-code-send-rate-limit": "Verification code send rate limit",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user