UI: Add 2FA general setting form

This commit is contained in:
Vladyslav_Prykhodko 2022-04-29 18:12:35 +03:00
parent d9a2495ea4
commit e29be2bded
7 changed files with 283 additions and 155 deletions

View File

@ -1,3 +1,19 @@
///
/// 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 { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';

View File

@ -364,7 +364,7 @@ export class MenuService {
name: 'admin.system-settings',
type: 'toggle',
path: '/settings',
height: '80px',
height: '120px',
icon: 'settings',
pages: [
{
@ -374,6 +374,14 @@ 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',
@ -510,6 +518,12 @@ 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',

View File

@ -1,3 +1,20 @@
<!--
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.
-->
<div>
<mat-card class="settings-card">
<mat-card-title>
@ -11,131 +28,146 @@
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<mat-card-content style="padding-top: 16px;">
<form [formGroup]="twoFaFormGroup" (ngSubmit)="save()" fxLayout="column">
<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>
<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>
<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>
<mat-label translate>admin.2fa.verification-code-send-rate-limit</mat-label>
<input matInput formControlName="verificationCodeSendRateLimit">
<mat-hint translate>admin.2fa.verification-code-send-rate-limit-hint</mat-hint>
<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>
<mat-label translate>admin.2fa.verification-code-check-rate-limit</mat-label>
<input matInput formControlName="verificationCodeCheckRateLimit">
<mat-hint translate>admin.2fa.verification-code-check-rate-limit-hint</mat-hint>
<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">Providers</div>
<ng-container formArrayName="providers">
<div class="container">
<mat-expansion-panel *ngFor="let provider of providersForm.controls; let j = index; trackBy: trackByParams"
class="registration-card mat-elevation-z0">
<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"
[disabled]="providersForm.controls.length < 2"
(click)="removeProviders($event, j)"
matTooltip="{{ 'admin.oauth2.delete-provider' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</mat-panel-description>
</mat-expansion-panel-header>
<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">
<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-template matExpansionPanelContent>
<section [formGroupName]="j">
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<section fxFlex formGroupName="additionalInfo" fxLayout="row">
<ng-template matExpansionPanelContent>
<section [formGroupName]="i">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.login-provider</mat-label>
<mat-select formControlName="providerName">
<mat-option *ngFor="let provider of templateProvider" [value]="provider">
{{ provider }}
<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>
</mat-select>
</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>
</ng-container>
</section>
<mat-form-field floatLabel="always" fxFlex class="mat-block">
<mat-label translate>admin.oauth2.allowed-platforms</mat-label>
<mat-select formControlName="platforms" multiple placeholder="{{ 'admin.oauth2.all-platforms' | translate }}">
<mat-option *ngFor="let platform of platformTypes" [value]="platform">
{{ platformTypeTranslations.get(platform) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.client-id</mat-label>
<input matInput formControlName="clientId" required>
<mat-error *ngIf="registration.get('clientId').hasError('required')">
{{ 'admin.oauth2.client-id-required' | translate }}
</mat-error>
<mat-error *ngIf="registration.get('clientId').hasError('maxlength')">
{{ 'admin.oauth2.client-id-max-length' | translate }}
</mat-error>
</mat-form-field>
</ng-template>
</mat-expansion-panel>
</mat-accordion>
</div>
</ng-container>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.client-secret</mat-label>
<input matInput formControlName="clientSecret" required>
<mat-error *ngIf="registration.get('clientSecret').hasError('required')">
{{ 'admin.oauth2.client-secret-required' | translate }}
</mat-error>
<mat-error *ngIf="registration.get('clientSecret').hasError('maxlength')">
{{ 'admin.oauth2.client-secret-max-length' | translate }}
</mat-error>
</mat-form-field>
</div>
</section>
</ng-template>
</mat-expansion-panel>
</div>
</ng-container>
</ng-container>
<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>
<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>action.add</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>
</mat-card>

View File

@ -0,0 +1,21 @@
/**
* 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.
*/
:host{
.container {
margin-bottom: 1em;
}
}

View File

@ -1,10 +1,26 @@
///
/// 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, 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 { 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';
@ -12,11 +28,7 @@ import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentica
import { AuthState } from '@core/auth/auth.models';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum';
import {
TwoFactorAuthProviderType,
TwoFactorAuthSettings,
TwoFactorAuthSettingsForm
} from '@shared/models/two-factor-auth.models';
import { TwoFactorAuthProviderType, TwoFactorAuthSettings } from '@shared/models/two-factor-auth.models';
@Component({
selector: 'tb-2fa-settings',
@ -29,6 +41,8 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
private authUser = this.authState.authUser;
twoFaFormGroup: FormGroup;
twoFactorAuthProviderTypes = Object.keys(TwoFactorAuthProviderType);
twoFactorAuthProviderType = TwoFactorAuthProviderType;
constructor(protected store: Store<AppState>,
private route: ActivatedRoute,
@ -43,7 +57,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
ngOnInit() {
this.build2faSettingsForm();
this.twoFaService.getTwoFaSettings().subscribe((setting) => {
console.log(this.formDataPreprocessing(setting));
this.initTwoFactorAuthForm(setting);
});
}
@ -60,12 +74,19 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
}
save() {
const setting = this.twoFaFormGroup.value;
this.twoFaService.saveTwoFaSettings(setting).subscribe(
(twoFactorAuthSettings) => {
this.twoFaFormGroup.patchValue(twoFactorAuthSettings, {emitEvent: false});
this.twoFaFormGroup.markAsUntouched();
this.twoFaFormGroup.markAsPristine();
}
);
}
private build2faSettingsForm(): void {
this.twoFaFormGroup = this.fb.group({
useSystemTwoFactorAuthSettings: [false],
useSystemTwoFactorAuthSettings: [this.isTenantAdmin()],
maxVerificationFailuresBeforeUserLockout: [30, [
Validators.required,
Validators.pattern(/^\d*$/),
@ -77,13 +98,20 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
Validators.min(1),
Validators.pattern(/^\d*$/)
]],
verificationCodeCheckRateLimit: ['', Validators.pattern(/^[1-9]\d*:[1-9]\d*$/)],
verificationCodeSendRateLimit: ['', Validators.pattern(/^[1-9]\d*:[1-9]\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*$/)]],
providers: this.fb.array([])
});
}
addProviders() {
private initTwoFactorAuthForm(settings: TwoFactorAuthSettings) {
settings.providers.forEach(() => {
this.addProvider();
});
this.twoFaFormGroup.patchValue(settings, {emitEvent: false});
}
addProvider() {
const newProviders = this.fb.group({
providerType: [TwoFactorAuthProviderType.TOTP],
issuerName: ['', Validators.required],
@ -119,8 +147,9 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
});
if (this.providersForm.length) {
const selectProvidersType = this.providersForm.value[0].providerType;
if (selectProvidersType !== TwoFactorAuthProviderType.TOTP) {
newProviders.get('providerType').patchValue(TwoFactorAuthProviderType.SMS, {emitEvents: true})
if (selectProvidersType === TwoFactorAuthProviderType.TOTP) {
newProviders.get('providerType').setValue(TwoFactorAuthProviderType.SMS);
newProviders.updateValueAndValidity();
}
}
this.providersForm.push(newProviders);
@ -140,16 +169,10 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
return this.twoFaFormGroup.get('providers') as FormArray;
}
private formDataPreprocessing(data: TwoFactorAuthSettings): TwoFactorAuthSettingsForm {
return data;
}
private formDataPostprocessing(data: TwoFactorAuthSettingsForm): TwoFactorAuthSettings{
return data;
}
trackByParams(index: number): number {
return index;
selectedTypes(type: TwoFactorAuthProviderType, index: number): boolean {
const selectedProviderTypes: TwoFactorAuthProviderType[] = this.providersForm.value.map(providers => providers.providerType);
selectedProviderTypes.splice(index, 1);
return selectedProviderTypes.includes(type);
}
}

View File

@ -1,3 +1,19 @@
///
/// 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.
///
export interface TwoFactorAuthSettings {
maxVerificationFailuresBeforeUserLockout: number;
providers: Array<TwoFactorAuthProviderConfig>;
@ -7,7 +23,7 @@ export interface TwoFactorAuthSettings {
verificationCodeSendRateLimit: string;
}
export type TwoFactorAuthProviderConfig = Partial<TotpTwoFactorAuthProviderConfig | SmsTwoFactorAuthProviderConfig>
export type TwoFactorAuthProviderConfig = Partial<TotpTwoFactorAuthProviderConfig | SmsTwoFactorAuthProviderConfig>;
export interface TotpTwoFactorAuthProviderConfig {
providerType: TwoFactorAuthProviderType;
@ -24,7 +40,3 @@ export enum TwoFactorAuthProviderType{
TOTP = 'TOTP',
SMS = 'SMS'
}
export interface TwoFactorAuthSettingsForm extends TwoFactorAuthSettings {
}

View File

@ -255,19 +255,29 @@
},
"2fa": {
"2fa": "Two-factor authentication",
"use-system-two-factor-auth-settings": "Use system two factor auth settings",
"total-allowed-time-for-verification": "Total allowed time for verification",
"total-allowed-time-for-verification-required": "Total allowed time is required.",
"total-allowed-time-for-verification-pattern": "Total allowed time must be a positive integer.",
"available-providers": "Available providers:",
"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-required": "Max verification failures is required.",
"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-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-hint": "If empty field, the limit not be apply",
"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-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-hint": "If empty field, the limit not be apply",
"verification-code-send-rate-limit-pattern": "Verification code send limit has invalid format"
"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."
}
},
"alarm": {