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.
|
|
|
|
|
///
|
|
|
|
|
|
2022-05-25 12:32:42 +03:00
|
|
|
import { Component, OnDestroy, OnInit, QueryList, 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';
|
2022-05-16 17:21:04 +03:00
|
|
|
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';
|
2022-05-16 17:21:04 +03:00
|
|
|
import {
|
2022-05-25 12:32:42 +03:00
|
|
|
twoFactorAuthProvidersData,
|
2022-05-16 17:21:04 +03:00
|
|
|
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';
|
2022-05-17 18:06:35 +03:00
|
|
|
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',
|
2022-05-20 17:19:21 +03:00
|
|
|
styleUrls: [ './settings-card.scss', './two-factor-auth-settings.component.scss']
|
2022-04-28 23:03:37 +03:00
|
|
|
})
|
|
|
|
|
export class TwoFactorAuthSettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy {
|
|
|
|
|
|
2022-05-16 17:21:04 +03:00
|
|
|
private readonly destroy$ = new Subject<void>();
|
2022-05-25 12:32:42 +03:00
|
|
|
private readonly posIntValidation = [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)];
|
2022-04-28 23:03:37 +03:00
|
|
|
|
|
|
|
|
twoFaFormGroup: FormGroup;
|
2022-04-29 18:12:35 +03:00
|
|
|
twoFactorAuthProviderType = TwoFactorAuthProviderType;
|
2022-05-25 12:32:42 +03:00
|
|
|
twoFactorAuthProvidersData = twoFactorAuthProvidersData;
|
2022-04-28 23:03:37 +03:00
|
|
|
|
2022-05-17 18:06:35 +03:00
|
|
|
@ViewChildren(MatExpansionPanel) expansionPanel: QueryList<MatExpansionPanel>;
|
|
|
|
|
|
2022-04-28 23:03:37 +03:00
|
|
|
constructor(protected store: Store<AppState>,
|
|
|
|
|
private twoFaService: TwoFactorAuthenticationService,
|
2022-05-16 17:21:04 +03:00
|
|
|
private fb: FormBuilder) {
|
2022-04-28 23:03:37 +03:00
|
|
|
super(store);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ngOnInit() {
|
|
|
|
|
this.build2faSettingsForm();
|
|
|
|
|
this.twoFaService.getTwoFaSettings().subscribe((setting) => {
|
2022-05-16 17:21:04 +03:00
|
|
|
this.setAuthConfigFormValue(setting);
|
2022-04-28 23:03:37 +03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ngOnDestroy() {
|
|
|
|
|
super.ngOnDestroy();
|
2022-05-16 17:21:04 +03:00
|
|
|
this.destroy$.next();
|
|
|
|
|
this.destroy$.complete();
|
2022-04-28 23:03:37 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
confirmForm(): FormGroup {
|
|
|
|
|
return this.twoFaFormGroup;
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-16 17:21:04 +03:00
|
|
|
save() {
|
|
|
|
|
if (this.twoFaFormGroup.valid) {
|
|
|
|
|
const setting = this.twoFaFormGroup.value as TwoFactorAuthSettingsForm;
|
|
|
|
|
this.joinRateLimit(setting, 'verificationCodeCheckRateLimit');
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2022-05-20 17:19:21 +03:00
|
|
|
toggleExtensionPanel($event: Event, index: number, currentState: boolean) {
|
2022-05-16 17:21:04 +03:00
|
|
|
if ($event) {
|
|
|
|
|
$event.stopPropagation();
|
|
|
|
|
}
|
2022-05-18 17:49:25 +03:00
|
|
|
if (currentState) {
|
2022-05-20 17:19:21 +03:00
|
|
|
this.getByIndexPanel(index).close();
|
2022-05-17 18:06:35 +03:00
|
|
|
} else {
|
2022-05-20 17:19:21 +03:00
|
|
|
this.getByIndexPanel(index).open();
|
2022-05-17 18:06:35 +03:00
|
|
|
}
|
2022-05-16 17:21:04 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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*$/)
|
|
|
|
|
]],
|
2022-05-16 17:21:04 +03:00
|
|
|
verificationCodeCheckRateLimitEnable: [false],
|
2022-05-25 12:32:42 +03:00
|
|
|
verificationCodeCheckRateLimitNumber: ['3', this.posIntValidation],
|
|
|
|
|
verificationCodeCheckRateLimitTime: ['900', this.posIntValidation],
|
2022-05-20 17:19:21 +03:00
|
|
|
minVerificationCodeSendPeriod: ['30', [Validators.required, Validators.min(5), Validators.pattern(/^\d*$/)]],
|
2022-04-28 23:03:37 +03:00
|
|
|
providers: this.fb.array([])
|
|
|
|
|
});
|
2022-05-16 17:21:04 +03:00
|
|
|
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});
|
|
|
|
|
}
|
|
|
|
|
});
|
2022-04-29 18:12:35 +03:00
|
|
|
}
|
|
|
|
|
|
2022-05-16 17:21:04 +03:00
|
|
|
private setAuthConfigFormValue(settings: TwoFactorAuthSettings) {
|
|
|
|
|
const [checkRateLimitNumber, checkRateLimitTime] = this.splitRateLimit(settings.verificationCodeCheckRateLimit);
|
|
|
|
|
const allowProvidersConfig = settings.providers.map(provider => provider.providerType);
|
|
|
|
|
const processFormValue: TwoFactorAuthSettingsForm = Object.assign(deepClone(settings), {
|
|
|
|
|
verificationCodeCheckRateLimitEnable: checkRateLimitNumber > 0,
|
|
|
|
|
verificationCodeCheckRateLimitNumber: checkRateLimitNumber || 3,
|
|
|
|
|
verificationCodeCheckRateLimitTime: checkRateLimitTime || 900,
|
|
|
|
|
providers: []
|
2022-04-28 23:03:37 +03:00
|
|
|
});
|
2022-05-18 17:53:54 +03:00
|
|
|
if (checkRateLimitNumber > 0) {
|
2022-05-25 12:32:42 +03:00
|
|
|
this.getByIndexPanel(this.providersForm.length).open();
|
2022-05-18 17:53:54 +03:00
|
|
|
}
|
2022-05-17 18:06:35 +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}));
|
2022-05-20 17:19:21 +03:00
|
|
|
this.getByIndexPanel(index).open();
|
2022-05-16 17:21:04 +03:00
|
|
|
} else {
|
|
|
|
|
processFormValue.providers.push({enable: false});
|
2022-04-28 23:03:37 +03:00
|
|
|
}
|
|
|
|
|
});
|
2022-05-16 17:21:04 +03:00
|
|
|
this.twoFaFormGroup.patchValue(processFormValue);
|
2022-04-28 23:03:37 +03:00
|
|
|
}
|
|
|
|
|
|
2022-05-16 17:21:04 +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:
|
2022-05-20 17:19:21 +03:00
|
|
|
formControlConfig.smsVerificationMessageTemplate = [{value: 'Verification code: ${code}', disabled: true}, [
|
2022-05-16 17:21:04 +03:00
|
|
|
Validators.required,
|
2022-05-20 17:19:21 +03:00
|
|
|
Validators.pattern(/\${code}/)
|
2022-05-16 17:21:04 +03:00
|
|
|
]];
|
2022-05-25 12:32:42 +03:00
|
|
|
formControlConfig.verificationCodeLifetime = [{value: 120, disabled: true}, this.posIntValidation];
|
2022-05-16 17:21:04 +03:00
|
|
|
break;
|
|
|
|
|
case TwoFactorAuthProviderType.EMAIL:
|
2022-05-25 12:32:42 +03:00
|
|
|
formControlConfig.verificationCodeLifetime = [{value: 120, disabled: true}, this.posIntValidation];
|
|
|
|
|
break;
|
|
|
|
|
case TwoFactorAuthProviderType.BACKUP_CODE:
|
|
|
|
|
formControlConfig.codesQuantity = [{value: 10, disabled: true}, this.posIntValidation];
|
2022-05-16 17:21:04 +03:00
|
|
|
break;
|
2022-04-28 23:03:37 +03:00
|
|
|
}
|
2022-05-16 17:21:04 +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
|
|
|
}
|
|
|
|
|
|
2022-05-17 18:06:35 +03:00
|
|
|
private getByIndexPanel(index: number) {
|
|
|
|
|
return this.expansionPanel.find((_, i) => i === index);
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-16 17:21:04 +03:00
|
|
|
private splitRateLimit(setting: string): [number, number] {
|
|
|
|
|
if (isNotEmptyStr(setting)) {
|
|
|
|
|
const [attemptNumber, time] = setting.split(':');
|
|
|
|
|
return [parseInt(attemptNumber, 10), parseInt(time, 10)];
|
|
|
|
|
}
|
|
|
|
|
return [0, 0];
|
2022-05-06 17:52:22 +03:00
|
|
|
}
|
|
|
|
|
|
2022-05-16 17:21:04 +03:00
|
|
|
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
|
|
|
}
|
|
|
|
|
}
|