Browse Source

UI: Refactoring 2FA system admin settings

pull/6235/head
Vladyslav_Prykhodko 4 years ago
parent
commit
664d337a99
  1. 14
      ui-ngx/src/app/core/services/menu.service.ts
  2. 2
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  3. 304
      ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html
  4. 63
      ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss
  5. 251
      ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts
  6. 52
      ui-ngx/src/app/shared/models/two-factor-auth.models.ts
  7. 17
      ui-ngx/src/assets/locale/locale.constant-en_US.json

14
ui-ngx/src/app/core/services/menu.service.ts

@ -374,14 +374,6 @@ export class MenuService {
path: '/settings/home',
icon: 'settings_applications'
},
{
id: guid(),
name: 'admin.2fa.2fa',
type: 'link',
path: '/settings/2fa',
icon: 'mdi:two-factor-authentication',
isMdiIcon: true
},
{
id: guid(),
name: 'resource.resources-library',
@ -518,12 +510,6 @@ export class MenuService {
icon: 'settings_applications',
path: '/settings/home'
},
{
name: 'admin.2fa.2fa',
icon: 'mdi:two-factor-authentication',
isMdiIcon: true,
path: '/settings/2fa'
},
{
name: 'resource.resources-library',
icon: 'folder',

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

@ -190,7 +190,7 @@ const routes: Routes = [
component: TwoFactorAuthSettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
auth: [Authority.SYS_ADMIN],
title: 'admin.2fa.2fa',
breadcrumb: {
label: 'admin.2fa.2fa',

304
ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html

@ -27,159 +27,171 @@
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<mat-card-content style="padding-top: 16px;">
<mat-card-content>
<form [formGroup]="twoFaFormGroup" (ngSubmit)="save()">
<fieldset [disabled]="isLoading$ | async">
<mat-checkbox *ngIf="isTenantAdmin()" formControlName="useSystemTwoFactorAuthSettings" style="padding-bottom: 16px;">
{{ 'admin.2fa.use-system-two-factor-auth-settings' | translate }}
</mat-checkbox>
<ng-container *ngIf="!isTenantAdmin() || !twoFaFormGroup.get('useSystemTwoFactorAuthSettings').value">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.total-allowed-time-for-verification</mat-label>
<input matInput required formControlName="totalAllowedTimeForVerification" type="number" step="1" min="1">
<mat-error *ngIf="twoFaFormGroup.get('totalAllowedTimeForVerification').hasError('required')">
{{ 'admin.2fa.total-allowed-time-for-verification-required' | translate }}
</mat-error>
<mat-error *ngIf="twoFaFormGroup.get('totalAllowedTimeForVerification').hasError('pattern')
|| twoFaFormGroup.get('totalAllowedTimeForVerification').hasError('min')">
{{ 'admin.2fa.total-allowed-time-for-verification-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.max-verification-failures-before-user-lockout</mat-label>
<input matInput required formControlName="maxVerificationFailuresBeforeUserLockout" type="number" step="1" min="0" max="65535">
<mat-error *ngIf="twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('required')">
{{ 'admin.2fa.max-verification-failures-before-user-lockout-required' | translate }}
</mat-error>
<mat-error *ngIf="twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('pattern')
|| twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('min')
|| twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('max')">
{{ 'admin.2fa.max-verification-failures-before-user-lockout-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.verification-code-send-rate-limit</mat-label>
<input matInput formControlName="verificationCodeSendRateLimit" required>
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeSendRateLimit').hasError('required')">
{{ 'admin.2fa.verification-code-send-rate-limit-required' | translate }}
</mat-error>
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeSendRateLimit').hasError('pattern')">
{{ 'admin.2fa.verification-code-send-rate-limit-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.verification-code-check-rate-limit</mat-label>
<input matInput formControlName="verificationCodeCheckRateLimit" required>
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimit').hasError('required')">
{{ 'admin.2fa.verification-code-check-rate-limit-required' | translate }}
</mat-error>
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimit').hasError('pattern')">
{{ 'admin.2fa.verification-code-check-rate-limit-pattern' | translate }}
</mat-error>
</mat-form-field>
<div class="mat-h3" translate>admin.2fa.available-providers</div>
<ng-container formArrayName="providers">
<div class="container">
<mat-accordion multi>
<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 }}
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end center">
<button mat-icon-button
type="button"
(click)="removeProviders($event, i)"
matTooltip="{{ 'admin.oauth2.delete-provider' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</mat-panel-description>
</mat-expansion-panel-header>
<ng-container>
<fieldset class="fields-group">
<legend class="group-title" translate>admin.2fa.general-setting</legend>
<div class="input-row" fxLayout="row" fxLayout.sm="column" fxLayoutGap="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.total-allowed-time-for-verification</mat-label>
<input matInput required formControlName="totalAllowedTimeForVerification" type="number" step="1" min="1">
<mat-error *ngIf="twoFaFormGroup.get('totalAllowedTimeForVerification').hasError('required')">
{{ 'admin.2fa.total-allowed-time-for-verification-required' | translate }}
</mat-error>
<mat-error *ngIf="twoFaFormGroup.get('totalAllowedTimeForVerification').hasError('pattern')
|| twoFaFormGroup.get('totalAllowedTimeForVerification').hasError('min')">
{{ 'admin.2fa.total-allowed-time-for-verification-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.max-verification-failures-before-user-lockout</mat-label>
<input matInput required formControlName="maxVerificationFailuresBeforeUserLockout" type="number" step="1" min="0" max="65535">
<mat-error *ngIf="twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('required')">
{{ 'admin.2fa.max-verification-failures-before-user-lockout-required' | translate }}
</mat-error>
<mat-error *ngIf="twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('pattern')
|| twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('min')
|| twoFaFormGroup.get('maxVerificationFailuresBeforeUserLockout').hasError('max')">
{{ 'admin.2fa.max-verification-failures-before-user-lockout-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
<mat-slide-toggle class="rate-limit-toggle" formControlName="verificationCodeSendRateLimitEnable">
{{ 'admin.2fa.verification-code-send-rate-limit' | translate }}
</mat-slide-toggle>
<div class="input-row" fxLayout="row" fxLayout.sm="column" fxLayoutGap="8px"
*ngIf="twoFaFormGroup.get('verificationCodeSendRateLimitEnable').value">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.number-of-attempts</mat-label>
<input matInput formControlName="verificationCodeSendRateLimitNumber" type="number" step="1" min="1" required>
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeSendRateLimitNumber').hasError('required')">
{{ 'admin.2fa.number-of-attempts-required' | translate }}
</mat-error>
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeSendRateLimitNumber').hasError('pattern')
|| twoFaFormGroup.get('verificationCodeSendRateLimitNumber').hasError('min')">
{{ 'admin.2fa.number-of-attempts-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.within-time</mat-label>
<input matInput formControlName="verificationCodeSendRateLimitTime" type="number" step="1" min="1" required>
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeSendRateLimitTime').hasError('required')">
{{ 'admin.2fa.within-time-required' | translate }}
</mat-error>
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeSendRateLimitTime').hasError('pattern')
|| twoFaFormGroup.get('verificationCodeSendRateLimitTime').hasError('min')">
{{ 'admin.2fa.within-time-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
<mat-slide-toggle class="rate-limit-toggle" formControlName="verificationCodeCheckRateLimitEnable">
{{ 'admin.2fa.verification-code-check-rate-limit' | translate }}
</mat-slide-toggle>
<div class="input-row" fxLayout="row" fxLayout.sm="column" fxLayoutGap="8px"
*ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').value">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.number-of-attempts</mat-label>
<input matInput formControlName="verificationCodeCheckRateLimitNumber" required type="number" step="1" min="1">
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('required')">
{{ 'admin.2fa.number-of-attempts-required' | translate }}
</mat-error>
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('pattern')
|| twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').hasError('min')">
{{ 'admin.2fa.number-of-attempts-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.within-time</mat-label>
<input matInput formControlName="verificationCodeCheckRateLimitTime" required type="number" step="1" min="1">
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitTime').hasError('required')">
{{ 'admin.2fa.within-time-required' | translate }}
</mat-error>
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeCheckRateLimitTime').hasError('pattern')
|| twoFaFormGroup.get('verificationCodeCheckRateLimitTime').hasError('min')">
{{ 'admin.2fa.within-time-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
</fieldset>
<fieldset class="fields-group" formArrayName="providers">
<legend class="group-title" translate>admin.2fa.available-providers</legend>
<ng-container *ngFor="let provider of providersForm.controls; let i = index; let $last = last; trackBy: trackByElement">
<mat-expansion-panel class="provider" [formGroupName]="i">
<mat-expansion-panel-header>
<mat-panel-title fxLayoutAlign="start center">
<mat-slide-toggle
(click)="toggleProviders($event)"
formControlName="enable">
</mat-slide-toggle>
{{ provider.value.providerType }}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<section [formGroupName]="i">
<ng-template matExpansionPanelContent>
<ng-container [ngSwitch]="provider.get('providerType').value">
<ng-container *ngSwitchCase="twoFactorAuthProviderType.TOTP">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.provider</mat-label>
<mat-select formControlName="providerType">
<mat-option *ngFor="let provider of twoFactorAuthProviderTypes; trackBy: trackByElement"
[value]="provider"
[disabled]="selectedTypes(twoFactorAuthProviderType[provider], i)">
{{ provider }}
</mat-option>
</mat-select>
<mat-label translate>admin.2fa.issuer-name</mat-label>
<input matInput formControlName="issuerName" required>
<mat-error *ngIf="provider.get('issuerName').hasError('required')">
{{ "admin.2fa.issuer-name-required" | translate }}
</mat-error>
</mat-form-field>
</ng-container>
<div *ngSwitchCase="twoFactorAuthProviderType.SMS"
fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.verification-message-template</mat-label>
<input matInput formControlName="smsVerificationMessageTemplate" required>
<mat-error *ngIf="provider.get('smsVerificationMessageTemplate').hasError('required')">
{{ "admin.2fa.verification-message-template-required" | translate }}
</mat-error>
<mat-error *ngIf="provider.get('smsVerificationMessageTemplate').hasError('pattern')">
{{ "admin.2fa.verification-message-template-pattern" | translate }}
</mat-error>
</mat-form-field>
<ng-container [ngSwitch]="provider.get('providerType').value">
<ng-container *ngSwitchCase="twoFactorAuthProviderType.TOTP">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.issuer-name</mat-label>
<input matInput formControlName="issuerName" required>
<mat-error *ngIf="provider.get('issuerName').hasError('required')">
{{ "admin.2fa.issuer-name-required" | translate }}
</mat-error>
</mat-form-field>
</ng-container>
<div *ngSwitchCase="twoFactorAuthProviderType.SMS"
fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.verification-message-template</mat-label>
<input matInput formControlName="smsVerificationMessageTemplate" required>
<mat-error *ngIf="provider.get('smsVerificationMessageTemplate').hasError('required')">
{{ "admin.2fa.verification-message-template-required" | translate }}
</mat-error>
<mat-error *ngIf="provider.get('smsVerificationMessageTemplate').hasError('pattern')">
{{ "admin.2fa.verification-message-template-pattern" | translate }}
</mat-error>
</mat-form-field>
<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>
<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>
</mat-expansion-panel>
</mat-accordion>
</div>
</ng-container>
<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>
<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-template>
</mat-expansion-panel>
<mat-divider *ngIf="!$last"></mat-divider>
</ng-container>
</fieldset>
<div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button mat-raised-button color="primary"
[disabled]="(isLoading$ | async) || twoFaFormGroup.invalid || !twoFaFormGroup.dirty"
type="submit">
{{'action.save' | translate}}
</button>
</div>
</ng-container>
<div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px">
<button type="button" mat-raised-button color="primary"
[disabled]="twoFaFormGroup.get('useSystemTwoFactorAuthSettings').value
|| providersForm.length == twoFactorAuthProviderTypes.length || (isLoading$ | async)"
(click)="addProvider()">
<mat-icon>add</mat-icon>
<span translate>admin.2fa.add-provider</span>
</button>
<button mat-button mat-raised-button color="primary"
[disabled]="(isLoading$ | async) || twoFaFormGroup.invalid || !twoFaFormGroup.dirty"
type="submit">
{{'action.save' | translate}}
</button>
</div>
</fieldset>
</form>
</mat-card-content>

63
ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss

@ -15,7 +15,66 @@
*/
:host{
.container {
margin-bottom: 1em;
.fields-group {
margin: 8px 0;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
position: relative;
padding-bottom: 8px;
legend {
color: rgba(0, 0, 0, .7);
width: fit-content;
}
&:not(:last-of-type) {
padding: 8px;
}
&:last-of-type {
margin-bottom: 24px;
legend {
margin: 0 8px;
}
}
.rate-limit-toggle {
display: block;
margin-bottom: 8px;
}
}
.mat-expansion-panel {
box-shadow: none;
margin: 1px 0 0;
&.provider {
overflow: inherit;
.mat-expansion-panel-header {
padding: 0 24px 0 8px;
&.mat-expanded {
height: 48px;
}
.mat-slide-toggle {
margin-right: 8px;
}
}
.mat-expansion-panel-header-title {
height: 40px;
}
}
}
}
:host ::ng-deep {
.mat-expansion-panel {
&.provider {
.mat-expansion-panel-header > .mat-content {
overflow: inherit;
}
.mat-expansion-panel-body {
padding: 0 16px 8px 8px;
}
}
}
}

251
ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts

@ -14,21 +14,21 @@
/// limitations under the License.
///
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
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 { 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';
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service';
import { AuthState } from '@core/auth/auth.models';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum';
import { TwoFactorAuthProviderType, TwoFactorAuthSettings } from '@shared/models/two-factor-auth.models';
import {
TwoFactorAuthProviderType,
TwoFactorAuthSettings,
TwoFactorAuthSettingsForm
} from '@shared/models/two-factor-auth.models';
import { deepClone, isNotEmptyStr } from '@core/utils';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'tb-2fa-settings',
@ -37,56 +37,72 @@ import { TwoFactorAuthProviderType, TwoFactorAuthSettings } from '@shared/models
})
export class TwoFactorAuthSettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy {
private authState: AuthState = getCurrentAuthState(this.store);
private authUser = this.authState.authUser;
private readonly destroy$ = new Subject<void>();
twoFaFormGroup: FormGroup;
twoFactorAuthProviderTypes = Object.keys(TwoFactorAuthProviderType);
twoFactorAuthProviderType = TwoFactorAuthProviderType;
constructor(protected store: Store<AppState>,
private route: ActivatedRoute,
private twoFaService: TwoFactorAuthenticationService,
private fb: FormBuilder,
private dialogService: DialogService,
private translate: TranslateService,
@Inject(WINDOW) private window: Window) {
private fb: FormBuilder) {
super(store);
}
ngOnInit() {
this.build2faSettingsForm();
this.twoFaService.getTwoFaSettings().subscribe((setting) => {
this.initTwoFactorAuthForm(setting);
this.setAuthConfigFormValue(setting);
});
}
ngOnDestroy() {
super.ngOnDestroy();
this.destroy$.next();
this.destroy$.complete();
}
confirmForm(): FormGroup {
return this.twoFaFormGroup;
}
isTenantAdmin(): boolean {
return this.authUser.authority === Authority.TENANT_ADMIN;
save() {
if (this.twoFaFormGroup.valid) {
const setting = this.twoFaFormGroup.value as TwoFactorAuthSettingsForm;
this.joinRateLimit(setting, 'verificationCodeCheckRateLimit');
this.joinRateLimit(setting, 'verificationCodeSendRateLimit');
const providers = setting.providers.filter(provider => provider.enable);
providers.forEach(provider => delete provider.enable);
const config = Object.assign(setting, {providers});
this.twoFaService.saveTwoFaSettings(config).subscribe(
() => {
this.twoFaFormGroup.markAsUntouched();
this.twoFaFormGroup.markAsPristine();
}
);
} else {
Object.keys(this.twoFaFormGroup.controls).forEach(field => {
const control = this.twoFaFormGroup.get(field);
control.markAsTouched({onlySelf: true});
});
}
}
save() {
const setting = this.twoFaFormGroup.value;
this.twoFaService.saveTwoFaSettings(setting).subscribe(
(twoFactorAuthSettings) => {
this.twoFaFormGroup.patchValue(twoFactorAuthSettings, {emitEvent: false});
this.twoFaFormGroup.markAsUntouched();
this.twoFaFormGroup.markAsPristine();
}
);
toggleProviders($event: Event): void {
if ($event) {
$event.stopPropagation();
}
}
trackByElement(i: number, item: any) {
return item;
}
get providersForm(): FormArray {
return this.twoFaFormGroup.get('providers') as FormArray;
}
private build2faSettingsForm(): void {
this.twoFaFormGroup = this.fb.group({
useSystemTwoFactorAuthSettings: [this.isTenantAdmin()],
maxVerificationFailuresBeforeUserLockout: [30, [
Validators.required,
Validators.pattern(/^\d*$/),
@ -98,91 +114,122 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
Validators.min(1),
Validators.pattern(/^\d*$/)
]],
verificationCodeCheckRateLimit: ['3:900', [Validators.required, Validators.pattern(/^[1-9]\d*:[1-9]\d*$/)]],
verificationCodeSendRateLimit: ['1:60', [Validators.required, Validators.pattern(/^[1-9]\d*:[1-9]\d*$/)]],
verificationCodeCheckRateLimitEnable: [false],
verificationCodeCheckRateLimitNumber: ['3', [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]],
verificationCodeCheckRateLimitTime: ['900', [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]],
verificationCodeSendRateLimitEnable: [false],
verificationCodeSendRateLimitNumber: ['1', [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]],
verificationCodeSendRateLimitTime: ['60', [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]],
providers: this.fb.array([])
});
}
private initTwoFactorAuthForm(settings: TwoFactorAuthSettings) {
settings.providers.forEach(() => {
this.addProvider();
Object.values(TwoFactorAuthProviderType).forEach(provider => {
this.buildProvidersSettingsForm(provider);
});
this.twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(value => {
if (value) {
this.twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').enable({emitEvent: false});
this.twoFaFormGroup.get('verificationCodeCheckRateLimitTime').enable({emitEvent: false});
} else {
this.twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').disable({emitEvent: false});
this.twoFaFormGroup.get('verificationCodeCheckRateLimitTime').disable({emitEvent: false});
}
});
this.twoFaFormGroup.get('verificationCodeSendRateLimitEnable').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(value => {
if (value) {
this.twoFaFormGroup.get('verificationCodeSendRateLimitNumber').enable({emitEvent: false});
this.twoFaFormGroup.get('verificationCodeSendRateLimitTime').enable({emitEvent: false});
} else {
this.twoFaFormGroup.get('verificationCodeSendRateLimitNumber').disable({emitEvent: false});
this.twoFaFormGroup.get('verificationCodeSendRateLimitTime').disable({emitEvent: false});
}
});
this.twoFaFormGroup.patchValue(settings);
this.twoFaFormGroup.markAsPristine();
}
addProvider() {
const newProviders = this.fb.group({
providerType: [TwoFactorAuthProviderType.TOTP],
issuerName: ['ThingsBoard', Validators.required],
smsVerificationMessageTemplate: [{
value: 'Verification code: ${verificationCode}',
disabled: true
}, [
Validators.required,
Validators.pattern(/\${verificationCode}/)
]],
verificationCodeLifetime: [{
value: 120,
disabled: true
}, [
Validators.required,
Validators.min(1),
Validators.pattern(/^\d*$/)
]]
private setAuthConfigFormValue(settings: TwoFactorAuthSettings) {
const [checkRateLimitNumber, checkRateLimitTime] = this.splitRateLimit(settings.verificationCodeCheckRateLimit);
const [sendRateLimitNumber, sendRateLimitTime] = this.splitRateLimit(settings.verificationCodeSendRateLimit);
const allowProvidersConfig = settings.providers.map(provider => provider.providerType);
const processFormValue: TwoFactorAuthSettingsForm = Object.assign(deepClone(settings), {
verificationCodeCheckRateLimitEnable: checkRateLimitNumber > 0,
verificationCodeCheckRateLimitNumber: checkRateLimitNumber || 3,
verificationCodeCheckRateLimitTime: checkRateLimitTime || 900,
verificationCodeSendRateLimitEnable: sendRateLimitNumber > 0,
verificationCodeSendRateLimitNumber: sendRateLimitNumber || 1,
verificationCodeSendRateLimitTime: sendRateLimitTime || 60,
providers: []
});
newProviders.get('providerType').valueChanges.subscribe(type => {
switch (type) {
case TwoFactorAuthProviderType.SMS:
newProviders.get('issuerName').disable({emitEvent: false});
newProviders.get('smsVerificationMessageTemplate').enable({emitEvent: false});
newProviders.get('verificationCodeLifetime').enable({emitEvent: false});
break;
case TwoFactorAuthProviderType.TOTP:
newProviders.get('issuerName').enable({emitEvent: false});
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;
Object.values(TwoFactorAuthProviderType).forEach(provider => {
const index = allowProvidersConfig.indexOf(provider);
if (index > -1) {
processFormValue.providers.push(Object.assign(settings.providers[index], {enable: true}));
} else {
processFormValue.providers.push({enable: false});
}
});
if (this.providersForm.length) {
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();
this.twoFaFormGroup.patchValue(processFormValue);
}
removeProviders($event: Event, index: number): void {
if ($event) {
$event.stopPropagation();
$event.preventDefault();
private buildProvidersSettingsForm(provider: TwoFactorAuthProviderType) {
const formControlConfig: {[key: string]: any} = {
providerType: [provider],
enable: [false]
};
switch (provider) {
case TwoFactorAuthProviderType.TOTP:
formControlConfig.issuerName = [{value: 'ThingsBoard', disabled: true}, Validators.required];
break;
case TwoFactorAuthProviderType.SMS:
formControlConfig.smsVerificationMessageTemplate = [{value: 'Verification code: ${verificationCode}', disabled: true}, [
Validators.required,
Validators.pattern(/\${verificationCode}/)
]];
formControlConfig.verificationCodeLifetime = [{value: 120, disabled: true}, [
Validators.required,
Validators.min(1),
Validators.pattern(/^\d*$/)
]];
break;
case TwoFactorAuthProviderType.EMAIL:
formControlConfig.verificationCodeLifetime = [{value: 120, disabled: true}, [
Validators.required,
Validators.min(1),
Validators.pattern(/^\d*$/)
]];
break;
}
this.providersForm.removeAt(index);
this.providersForm.markAsTouched();
this.providersForm.markAsDirty();
}
get providersForm(): FormArray {
return this.twoFaFormGroup.get('providers') as FormArray;
const newProviders = this.fb.group(formControlConfig);
newProviders.get('enable').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(value => {
if (value) {
newProviders.enable({emitEvent: false});
} else {
newProviders.disable({emitEvent: false});
newProviders.get('enable').enable({emitEvent: false});
newProviders.get('providerType').enable({emitEvent: false});
}
});
this.providersForm.push(newProviders);
}
trackByElement(i: number, item: any) {
return item;
private splitRateLimit(setting: string): [number, number] {
if (isNotEmptyStr(setting)) {
const [attemptNumber, time] = setting.split(':');
return [parseInt(attemptNumber, 10), parseInt(time, 10)];
}
return [0, 0];
}
selectedTypes(type: TwoFactorAuthProviderType, index: number): boolean {
const selectedProviderTypes: TwoFactorAuthProviderType[] = this.providersForm.value.map(providers => providers.providerType);
selectedProviderTypes.splice(index, 1);
return selectedProviderTypes.includes(type);
private joinRateLimit(processFormValue: TwoFactorAuthSettingsForm, property: string) {
if (processFormValue[`${property}Enable`]) {
processFormValue[property] = [processFormValue[`${property}Number`], processFormValue[`${property}Time`]].join(':');
}
delete processFormValue[`${property}Enable`];
delete processFormValue[`${property}Number`];
delete processFormValue[`${property}Time`];
}
}

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

@ -23,9 +23,22 @@ export interface TwoFactorAuthSettings {
verificationCodeSendRateLimit: string;
}
export interface TwoFactorAuthSettingsForm extends TwoFactorAuthSettings{
providers: Array<TwoFactorAuthProviderConfigForm>;
verificationCodeCheckRateLimitEnable: boolean;
verificationCodeCheckRateLimitNumber: number;
verificationCodeCheckRateLimitTime: number;
verificationCodeSendRateLimitEnable: boolean;
verificationCodeSendRateLimitNumber: number;
verificationCodeSendRateLimitTime: number;
}
export type TwoFactorAuthProviderConfig = Partial<TotpTwoFactorAuthProviderConfig | SmsTwoFactorAuthProviderConfig |
EmailTwoFactorAuthProviderConfig>;
export type TwoFactorAuthProviderConfigForm = Partial<TotpTwoFactorAuthProviderConfig | SmsTwoFactorAuthProviderConfig |
EmailTwoFactorAuthProviderConfig> & TwoFactorAuthProviderFormConfig;
export interface TotpTwoFactorAuthProviderConfig {
providerType: TwoFactorAuthProviderType;
issuerName: string;
@ -42,6 +55,10 @@ export interface EmailTwoFactorAuthProviderConfig {
verificationCodeLifetime: number;
}
export interface TwoFactorAuthProviderFormConfig {
enable: boolean;
}
export enum TwoFactorAuthProviderType{
TOTP = 'TOTP',
SMS = 'SMS',
@ -69,5 +86,38 @@ export type TwoFactorAuthAccountConfig = TotpTwoFactorAuthAccountConfig | SmsTwo
export interface AccountTwoFaSettings {
configs?: {TwoFactorAuthProviderType: TwoFactorAuthAccountConfig};
configs: {TwoFactorAuthProviderType: TwoFactorAuthAccountConfig};
}
export interface TwoFaProviderInfo {
type: TwoFactorAuthProviderType;
default: boolean;
}
export interface TwoFactorAuthProviderData {
name: string;
description: string;
}
export const twoFactorAuthProvidersData = new Map<TwoFactorAuthProviderType, TwoFactorAuthProviderData>(
[
[
TwoFactorAuthProviderType.TOTP, {
name: 'Authentication app',
description: 'Use apps like Google Authenticator, Authy, or Duo on your phone to authenticate. It will generate a security code for logging in.'
}
],
[
TwoFactorAuthProviderType.SMS, {
name: 'SMS',
description: 'Use your phone to authenticate. We\'ll send you a security code via SMS message when you log in.'
}
],
[
TwoFactorAuthProviderType.EMAIL, {
name: 'Email',
description: 'Use a security code sent to your email address to authenticate.'
}
],
]
);

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

@ -123,6 +123,7 @@
"number-from-required": "Phone Number From is required.",
"number-to": "Phone Number To",
"number-to-required": "Phone Number To is required.",
"phone-number": "Phone Number",
"phone-number-hint": "Phone Number in E.164 format, ex. +19995550123",
"phone-number-hint-twilio": "Phone Number in E.164 format/Phone Number's SID/Messaging Service SID, ex. +19995550123/PNXXX/MGXXX",
"phone-number-pattern": "Invalid phone number. Should be in E.164 format, ex. +19995550123.",
@ -314,30 +315,32 @@
},
"2fa": {
"2fa": "Two-factor authentication",
"add-provider": "Add provider",
"available-providers": "Available providers:",
"available-providers": "Available providers",
"general-setting": "General setting",
"issuer-name": "Issuer name",
"issuer-name-required": "Issuer name is required.",
"max-verification-failures-before-user-lockout": "Max verification failures before user lockout",
"max-verification-failures-before-user-lockout-pattern": "Max verification failures must be a positive integer.",
"max-verification-failures-before-user-lockout-required": "Max verification failures is required.",
"number-of-attempts": "Number of attempts",
"number-of-attempts-pattern": "Number of attempts must be a positive integer.",
"number-of-attempts-required": "Number of attempts is required.",
"provider": "Provider",
"total-allowed-time-for-verification": "Total allowed time for verification (sec)",
"total-allowed-time-for-verification-pattern": "Total allowed time must be a positive integer.",
"total-allowed-time-for-verification-required": "Total allowed time is required.",
"use-system-two-factor-auth-settings": "Use system two factor auth settings",
"verification-code-check-rate-limit": "Verification code check rate limit",
"verification-code-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 (sec)",
"verification-code-lifetime-pattern": "Verification code lifetime must be a positive integer.",
"verification-code-lifetime-required": "Verification code lifetime is required.",
"verification-code-send-rate-limit": "Verification code send rate limit",
"verification-code-send-rate-limit-pattern": "Verification code send limit has invalid format",
"verification-code-send-rate-limit-required": "Verification code send rate limit is required.",
"verification-message-template": "Verification message template",
"verification-message-template-pattern": "Verification message need to contains pattern: ${verificationCode}",
"verification-message-template-required": "Verification message template is required."
"verification-message-template-required": "Verification message template is required.",
"within-time": "Within time (sec)",
"within-time-pattern": "Time must be a positive integer.",
"within-time-required": "Time is required."
}
},
"alarm": {

Loading…
Cancel
Save