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

250 lines
9.8 KiB
TypeScript
Raw Normal View History

2022-04-29 18:12:35 +03:00
///
/// 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, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
2022-04-28 23:03:37 +03:00
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 { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
2022-04-28 23:03:37 +03:00
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service';
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';
import { MatStepper } from '@angular/material/stepper';
import { MatExpansionPanel } from '@angular/material/expansion';
2022-04-28 23:03:37 +03:00
@Component({
selector: 'tb-2fa-settings',
templateUrl: './two-factor-auth-settings.component.html',
styleUrls: ['./two-factor-auth-settings.component.scss', './settings-card.scss']
})
export class TwoFactorAuthSettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy {
private readonly destroy$ = new Subject<void>();
2022-04-28 23:03:37 +03:00
twoFaFormGroup: FormGroup;
2022-04-29 18:12:35 +03:00
twoFactorAuthProviderType = TwoFactorAuthProviderType;
2022-04-28 23:03:37 +03:00
@ViewChildren(MatExpansionPanel) expansionPanel: QueryList<MatExpansionPanel>;
2022-04-28 23:03:37 +03:00
constructor(protected store: Store<AppState>,
private twoFaService: TwoFactorAuthenticationService,
private fb: FormBuilder) {
2022-04-28 23:03:37 +03:00
super(store);
}
ngOnInit() {
this.build2faSettingsForm();
this.twoFaService.getTwoFaSettings().subscribe((setting) => {
this.setAuthConfigFormValue(setting);
2022-04-28 23:03:37 +03:00
});
}
ngOnDestroy() {
super.ngOnDestroy();
this.destroy$.next();
this.destroy$.complete();
2022-04-28 23:03:37 +03:00
}
confirmForm(): FormGroup {
return this.twoFaFormGroup;
}
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});
});
}
2022-04-28 23:03:37 +03:00
}
toggleProviders($event: Event, i: number): void {
if ($event) {
$event.stopPropagation();
}
if (this.providersForm.at(i).get('enable').value) {
this.getByIndexPanel(i).close();
} else {
this.getByIndexPanel(i).open();
}
}
trackByElement(i: number, item: any) {
return item;
}
get providersForm(): FormArray {
return this.twoFaFormGroup.get('providers') as FormArray;
2022-04-28 23:03:37 +03:00
}
private build2faSettingsForm(): void {
this.twoFaFormGroup = this.fb.group({
maxVerificationFailuresBeforeUserLockout: [30, [
Validators.required,
Validators.pattern(/^\d*$/),
Validators.min(0),
Validators.max(65535)
]],
totalAllowedTimeForVerification: [3600, [
Validators.required,
Validators.min(1),
Validators.pattern(/^\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*$/)]],
2022-04-28 23:03:37 +03:00
providers: this.fb.array([])
});
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});
}
2022-04-29 18:12:35 +03:00
});
}
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: []
2022-04-28 23:03:37 +03:00
});
Object.values(TwoFactorAuthProviderType).forEach((provider, index) => {
const findIndex = allowProvidersConfig.indexOf(provider);
if (findIndex > -1) {
processFormValue.providers.push(Object.assign(settings.providers[findIndex], {enable: true}));
this.getByIndexPanel(index).open();
} else {
processFormValue.providers.push({enable: false});
2022-04-28 23:03:37 +03:00
}
});
this.twoFaFormGroup.patchValue(processFormValue);
2022-04-28 23:03:37 +03:00
}
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;
2022-04-28 23:03:37 +03:00
}
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);
2022-04-28 23:03:37 +03:00
}
private getByIndexPanel(index: number) {
return this.expansionPanel.find((_, i) => i === index);
}
private splitRateLimit(setting: string): [number, number] {
if (isNotEmptyStr(setting)) {
const [attemptNumber, time] = setting.split(':');
return [parseInt(attemptNumber, 10), parseInt(time, 10)];
}
return [0, 0];
}
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`];
2022-04-28 23:03:37 +03:00
}
}