Unverified Commit c1ebae42 authored by AlexKarpov's avatar AlexKarpov Committed by GitHub
Browse files

NAS-114608 / None / Refactor TwoFactorComponent to ix-forms (#6404)

* NAS-114608: Refactor TwoFactorComponent to ix-forms

* NAS-114608: Refactor TwoFactorComponent to ix-forms

* NAS-114608: Always show the QR code after Enable button is pressed

* NAS-114608: Rearranged the form

* NAS-114608: Fixed test errors
parent 7babeb76
Showing with 529 additions and 268 deletions
+529 -268
......@@ -3,6 +3,7 @@
<legend class="title">
{{title}}
</legend>
<tooltip *ngIf="tooltip" [header]="title" class="tooltip" [message]="tooltip"></tooltip>
</div>
<ng-content></ng-content>
<mat-divider *ngIf="divider" class="divider" inset></mat-divider>
......
......@@ -4,6 +4,8 @@ fieldset {
}
.title-container {
align-items: flex-end;
display: flex;
margin-bottom: 18px;
margin-top: 9px;
}
......@@ -16,6 +18,11 @@ legend {
padding: 0;
}
.tooltip {
margin-left: 8px;
vertical-align: -4px;
}
.divider {
margin: 16px 0 !important;
}
......@@ -9,4 +9,5 @@ export class IxFieldsetComponent {
@Input() disable: boolean;
@Input() title: string;
@Input() divider: boolean;
@Input() tooltip: string;
}
......@@ -6,7 +6,7 @@
<tooltip *ngIf="tooltip" [header]="label" class="tooltip" [message]="tooltip"></tooltip>
</div>
<div class="input-container" [class.disabled]="isDisabled">
<div class="input-container" [class.disabled]="isDisabled" [class.readonly]="readonly">
<span *ngIf="prefixIcon" class="prefix-icon">
<mat-icon>{{prefixIcon}}</mat-icon>
</span>
......@@ -17,10 +17,12 @@
[class.password-field]="isPasswordField()"
[class.has-reset-input-icon]="shouldShowResetInput()"
[required]="required"
[readonly]="readonly"
[disabled]="isDisabled"
[type]="getType()"
[value]="formatted"
(input)="input(ixInput)"
(focus)="focus(ixInput)"
(blur)="blur()"
[autocomplete]="autocomplete"
[autocapitalize]="autocapitalize"
......@@ -45,3 +47,4 @@
</div>
<ix-errors [control]="controlDirective.control" [label]="label"></ix-errors>
<mat-hint *ngIf="hint">{{hint}}</mat-hint>
......@@ -37,6 +37,14 @@
opacity: 1;
}
}
&.readonly {
background: var(--bg2);
input {
cursor: copy;
}
}
}
.label-container {
......
......@@ -19,6 +19,7 @@ export class IxInputComponent implements ControlValueAccessor {
@Input() hint: string;
@Input() tooltip: string;
@Input() required: boolean;
@Input() readonly: boolean;
@Input() type: string;
@Input() autocomplete = 'off';
@Input() autocapitalize = 'off';
......@@ -84,7 +85,7 @@ export class IxInputComponent implements ControlValueAccessor {
}
shouldShowResetInput(): boolean {
return !this.isDisabled && this.hasValue() && this.type !== 'password';
return !this.isDisabled && this.hasValue() && this.type !== 'password' && !this.readonly;
}
getType(): string {
......@@ -112,6 +113,13 @@ export class IxInputComponent implements ControlValueAccessor {
this.cdr.markForCheck();
}
focus(ixInput: HTMLInputElement): void {
this.onTouch();
if (this.readonly) {
ixInput.select();
}
}
blur(): void {
this.onTouch();
if (this.formatted) {
......
<mat-card class="form-card">
<mat-progress-bar *ngIf="isFormLoading" mode="indeterminate"></mat-progress-bar>
<mat-card-content>
<div class="status-content">
<p class="help-text">
{{ twoFactorStatusText | translate }}
</p>
<button
mat-button
[disabled]="isFormLoading"
color="primary"
type="button"
(click)="toggleTwoFactor()"
>{{ twoFactorButtonText | translate }}</button>
<button
mat-button
[disabled]="isFormLoading || !secret"
color="accent"
type="button"
(click)="openQrDialog()"
>{{ 'Show QR' | translate }}</button>
</div>
<form [formGroup]="form" class="ix-form-container" (submit)="onSubmit()">
<div class="two-columns">
<ix-fieldset
[title]="helptext.two_factor.title"
[tooltip]="helptext.two_factor.message"
>
<ix-select
formControlName="otp_digits"
[label]="labels.otp_digits"
[tooltip]="tooltips.otp_digits"
[options]="otpDigitOptions$"
[required]="true"
></ix-select>
<ix-input
formControlName="interval"
[label]="labels.interval"
[tooltip]="tooltips.interval"
[hint]="intervalHint"
type="number"
></ix-input>
<ix-input
formControlName="window"
[label]="labels.window"
[tooltip]="tooltips.window"
type="number"
></ix-input>
<ix-checkbox
formControlName="ssh"
[label]="labels.ssh"
[tooltip]="tooltips.ssh"
></ix-checkbox>
<button
mat-button
type="submit"
color="accent"
[disabled]="form.invalid || isFormLoading"
>{{ 'Save' | translate }}</button>
</ix-fieldset>
<ix-fieldset [title]="helptext.two_factor.sys">
<ix-input
formControlName="secret"
[label]="labels.secret"
[tooltip]="tooltips.secret"
[readonly]="true"
type="password"
></ix-input>
<ix-input
formControlName="uri"
[label]="labels.uri"
[tooltip]="tooltips.uri"
[readonly]="true"
type="password"
></ix-input>
<button
mat-button
color="accent"
type="button"
(click)="renewSecret()"
[disabled]="isFormLoading || !twoFactorEnabled"
>{{ 'Renew Secret' | translate }}</button>
</ix-fieldset>
</div>
</form>
</mat-card-content>
</mat-card>
\ No newline at end of file
.status-content {
padding-bottom: 24px !important;
button {
margin-right: 5px;
}
}
.two-columns {
display: flex;
> * {
border-top: 1px solid var(--lines);
display: block;
width: 50%;
}
}
.help-text {
color: var(--fg1);
font-size: 14px;
}
.card {
margin-left: auto;
margin-right: auto;
max-width: 960px;
}
@media (max-width: 600px) {
.two-columns {
flex-direction: column;
> * {
width: 100%;
}
}
}
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonHarness } from '@angular/material/button/testing';
import { MatDialog } from '@angular/material/dialog';
import {
createComponentFactory, mockProvider, Spectator,
} from '@ngneat/spectator/jest';
import { of } from 'rxjs';
import { MockWebsocketService } from 'app/core/testing/classes/mock-websocket.service';
import { mockCall, mockWebsocket } from 'app/core/testing/utils/mock-websocket.utils';
import { TwoFactorConfig } from 'app/interfaces/two-factor-config.interface';
import { IxInputHarness } from 'app/modules/ix-forms/components/ix-input/ix-input.harness';
import { IxFormsModule } from 'app/modules/ix-forms/ix-forms.module';
import { FormErrorHandlerService } from 'app/modules/ix-forms/services/form-error-handler.service';
import { IxFormHarness } from 'app/modules/ix-forms/testing/ix-form.harness';
import { QrDialogComponent } from 'app/pages/system/two-factor/qr-dialog/qr-dialog.component';
import { TwoFactorComponent } from 'app/pages/system/two-factor/two-factor.component';
import { DialogService, WebSocketService } from 'app/services';
describe('TwoFactorComponent', () => {
let spectator: Spectator<TwoFactorComponent>;
let loader: HarnessLoader;
let ws: WebSocketService;
let matDialog: MatDialog;
const createComponent = createComponentFactory({
component: TwoFactorComponent,
imports: [
IxFormsModule,
ReactiveFormsModule,
],
providers: [
mockWebsocket([
mockCall('auth.twofactor.config', {
enabled: false,
id: 1,
interval: 30,
otp_digits: 6,
secret: null,
services: { ssh: false },
window: 0,
} as TwoFactorConfig),
mockCall('auth.twofactor.provisioning_uri', 'otpauth://totp/iXsystems:truenas.local%40TrueNAS?secret=None&issuer=iXsystems'),
mockCall('auth.twofactor.renew_secret'),
mockCall('auth.twofactor.update'),
]),
mockProvider(FormErrorHandlerService),
mockProvider(DialogService, {
confirm: jest.fn(() => of(true)),
}),
],
});
beforeEach(() => {
spectator = createComponent();
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
ws = spectator.inject(WebSocketService);
matDialog = spectator.inject(MatDialog);
});
it('loads uri and puts it in the field', async () => {
const gidInput = await loader.getHarness(IxInputHarness.with({ label: 'Provisioning URI (includes Secret - Read only):' }));
const value = await gidInput.getValue();
expect(ws.call).toHaveBeenCalledWith('auth.twofactor.provisioning_uri');
expect(value).toBe('otpauth://totp/iXsystems:truenas.local%40TrueNAS?secret=None&issuer=iXsystems');
});
it('loads current config and and shows it.', async () => {
const form = await loader.getHarness(IxFormHarness);
const values = await form.getValues();
expect(ws.call).toHaveBeenCalledWith('auth.twofactor.config');
expect(values).toEqual({
'One-Time Password (OTP) Digits': '6',
Interval: '30',
Window: '0',
'Enable Two-Factor Auth for SSH': false,
'Secret (Read only)': '',
'Provisioning URI (includes Secret - Read only):': 'otpauth://totp/iXsystems:truenas.local%40TrueNAS?secret=None&issuer=iXsystems',
});
});
it('sends an update payload to websocket when settings are updated and saved', async () => {
const form = await loader.getHarness(IxFormHarness);
await form.fillForm({
'One-Time Password (OTP) Digits': '6',
Interval: '30',
Window: '0',
'Enable Two-Factor Auth for SSH': false,
});
const saveButton = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
await saveButton.click();
expect(ws.call).toHaveBeenCalledWith('auth.twofactor.update', [{
enabled: false,
interval: 30,
otp_digits: 6,
services: { ssh: false },
window: 0,
}]);
});
it('enable 2FA when click `Enable Two-Factor Authentication` button', async () => {
const enableButton = await loader.getHarness(MatButtonHarness.with({ text: 'Enable Two-Factor Authentication' }));
await enableButton.click();
expect(spectator.inject(DialogService).confirm).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Enable Two-Factor Authentication',
}),
);
expect(ws.call).toHaveBeenCalledWith('auth.twofactor.update', [{
enabled: true,
}]);
});
it('open QR dialog when click `Show QR` button', async () => {
jest.spyOn(matDialog, 'open').mockImplementation();
const showQrButton = await loader.getHarness(MatButtonHarness.with({ text: 'Show QR' }));
await showQrButton.click();
const gidInput = await loader.getHarness(IxInputHarness.with({ label: 'Provisioning URI (includes Secret - Read only):' }));
const value = await gidInput.getValue();
expect(matDialog.open).toHaveBeenCalledWith(QrDialogComponent, {
width: '300px',
data: { qrInfo: value },
});
});
it('renew 2FA Secret when click `Renew Secret` button', async () => {
const renewButton = await loader.getHarness(MatButtonHarness.with({ text: 'Renew Secret' }));
await renewButton.click();
expect(spectator.inject(DialogService).confirm).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Renew Secret',
}),
);
expect(ws.call).toHaveBeenCalledWith('auth.twofactor.renew_secret');
});
it('disable 2FA when click `Disable Two-Factor Authentication` button', async () => {
spectator.inject(MockWebsocketService).mockCall('auth.twofactor.config', {
enabled: true,
} as TwoFactorConfig);
spectator.component.ngOnInit();
const disableButton = await loader.getHarness(MatButtonHarness.with({ text: 'Disable Two-Factor Authentication' }));
await disableButton.click();
expect(spectator.inject(DialogService).confirm).not.toHaveBeenCalledWith();
expect(ws.call).toHaveBeenCalledWith('auth.twofactor.update', [{
enabled: false,
}]);
});
});
import { Component } from '@angular/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef, Component, OnInit,
} from '@angular/core';
import {
Validators,
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { FormBuilder } from '@ngneat/reactive-forms';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import * as _ from 'lodash';
import { of } from 'rxjs';
import { filter } from 'rxjs/operators';
import { helptext } from 'app/helptext/system/2fa';
import { FormConfiguration } from 'app/interfaces/entity-form.interface';
import { TwoFactorConfig } from 'app/interfaces/two-factor-config.interface';
import { EntityFormComponent } from 'app/modules/entity/entity-form/entity-form.component';
import { FieldConfig, FormParagraphConfig, FormInputConfig } from 'app/modules/entity/entity-form/models/field-config.interface';
import { FieldSet } from 'app/modules/entity/entity-form/models/fieldset.interface';
import { TwoFactorConfig, TwoFactorConfigUpdate } from 'app/interfaces/two-factor-config.interface';
import { EntityUtils } from 'app/modules/entity/utils';
import { FormErrorHandlerService } from 'app/modules/ix-forms/services/form-error-handler.service';
import { QrDialogComponent } from 'app/pages/system/two-factor/qr-dialog/qr-dialog.component';
import { WebSocketService, DialogService, AppLoaderService } from 'app/services/';
import { WebSocketService, DialogService } from 'app/services';
@UntilDestroy()
@Component({
selector: 'app-two-factor',
template: '<entity-form [conf]="this"></entity-form>',
templateUrl: './two-factor.component.html',
styleUrls: ['./two-factor.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TwoFactorComponent implements FormConfiguration {
queryCall = 'auth.twofactor.config' as const;
private entityEdit: EntityFormComponent;
private TwoFactorEnabled: boolean;
qrInfo: string;
private secret: string;
title = helptext.two_factor.formTitle;
export class TwoFactorComponent implements OnInit {
isFormLoading = false;
twoFactorEnabled: boolean;
secret: string;
intervalHint: string;
private digitsOnLoad: number;
private intervalOnLoad: number;
fieldConfig: FieldConfig[] = [];
fieldSets: FieldSet[] = [
{
name: helptext.two_factor.title,
width: '100%',
label: true,
config: [
{
type: 'paragraph',
name: 'instructions',
paraText: helptext.two_factor.message,
},
],
},
{ name: 'divider', divider: true },
{
name: helptext.two_factor.title,
width: '48%',
label: false,
config: [
{
type: 'select',
name: 'otp_digits',
placeholder: helptext.two_factor.otp.placeholder,
tooltip: helptext.two_factor.otp.tooltip,
options: [
{ label: '6', value: 6 },
{ label: '7', value: 7 },
{ label: '8', value: 8 },
],
required: true,
validation: helptext.two_factor.otp.validation,
},
{
type: 'input',
name: 'interval',
inputType: 'number',
placeholder: helptext.two_factor.interval.placeholder,
tooltip: helptext.two_factor.interval.tooltip,
validation: helptext.two_factor.interval.validation,
},
],
},
{
name: 'vertical-spacer',
width: '2%',
label: false,
config: [],
},
{
name: helptext.two_factor.title,
width: '48%',
label: false,
config: [
{
type: 'input',
name: 'window',
inputType: 'number',
placeholder: helptext.two_factor.window.placeholder,
tooltip: helptext.two_factor.window.tooltip,
validation: helptext.two_factor.window.validation,
},
{
type: 'checkbox',
name: 'ssh',
placeholder: helptext.two_factor.services.placeholder,
tooltip: helptext.two_factor.services.tooltip,
},
],
},
form = this.fb.group({
otp_digits: [null as number, [Validators.required, Validators.min(6), Validators.max(8)]],
interval: [null as number, [Validators.min(5)]],
window: [null as number, [Validators.min(0)]],
ssh: [false],
secret: [''],
uri: [''],
});
{ name: 'divider', divider: true },
readonly helptext = helptext;
{
name: helptext.two_factor.sys,
width: '100%',
label: true,
config: [
{
type: 'input',
name: 'secret',
inputType: 'password',
togglePw: true,
placeholder: helptext.two_factor.secret.placeholder,
tooltip: helptext.two_factor.secret.tooltip,
readonly: true,
},
{
type: 'input',
name: 'uri',
inputType: 'password',
togglePw: true,
placeholder: helptext.two_factor.uri.placeholder,
tooltip: helptext.two_factor.uri.tooltip,
readonly: true,
},
{
type: 'paragraph',
name: 'enabled_status',
paraText: '',
},
],
},
];
readonly labels = {
otp_digits: helptext.two_factor.otp.placeholder,
interval: helptext.two_factor.interval.placeholder,
window: helptext.two_factor.window.placeholder,
ssh: helptext.two_factor.services.placeholder,
secret: helptext.two_factor.secret.placeholder,
uri: helptext.two_factor.uri.placeholder,
};
custActions = [
{
id: 'enable_action',
name: helptext.two_factor.enable_button,
function: () => {
this.dialog.confirm({
title: helptext.two_factor.confirm_dialog.title,
message: helptext.two_factor.confirm_dialog.message,
hideCheckBox: true,
buttonMsg: helptext.two_factor.confirm_dialog.btn,
}).pipe(filter(Boolean), untilDestroyed(this)).subscribe(() => {
this.loader.open();
this.ws.call('auth.twofactor.update', [{ enabled: true }]).pipe(untilDestroyed(this)).subscribe(() => {
this.loader.close();
this.TwoFactorEnabled = true;
this.updateEnabledStatus();
this.updateSecretAndUri();
}, (err) => {
this.loader.close();
this.dialog.errorReport(helptext.two_factor.error,
err.reason, err.trace.formatted);
});
});
},
},
{
id: 'disable_action',
name: helptext.two_factor.disable_button,
function: () => {
this.loader.open();
this.ws.call('auth.twofactor.update', [{ enabled: false }]).pipe(untilDestroyed(this)).subscribe(() => {
this.loader.close();
this.TwoFactorEnabled = false;
this.updateEnabledStatus();
}, (err) => {
this.dialog.errorReport(helptext.two_factor.error,
err.reason, err.trace.formatted);
});
},
},
{
id: 'show_qr',
name: 'Show QR',
function: () => {
this.openQrDialog();
},
},
{
id: 'renew_secret',
name: 'Renew Secret',
function: () => {
this.renewSecret();
},
},
];
readonly tooltips = {
otp_digits: helptext.two_factor.otp.tooltip,
interval: helptext.two_factor.interval.tooltip,
window: helptext.two_factor.window.tooltip,
ssh: helptext.two_factor.services.tooltip,
secret: helptext.two_factor.secret.tooltip,
uri: helptext.two_factor.uri.tooltip,
};
constructor(protected ws: WebSocketService, protected dialog: DialogService,
protected loader: AppLoaderService,
protected mdDialog: MatDialog) { }
readonly otpDigitOptions$ = of([
{ label: '6', value: 6 },
{ label: '7', value: 7 },
{ label: '8', value: 8 },
]);
resourceTransformIncomingRestData(data: TwoFactorConfig): any {
this.secret = data.secret;
this.TwoFactorEnabled = data.enabled;
this.digitsOnLoad = data.otp_digits;
this.intervalOnLoad = data.interval;
this.updateEnabledStatus();
return {
...data,
ssh: data.services.ssh,
};
}
constructor(
private fb: FormBuilder,
protected ws: WebSocketService,
private cdr: ChangeDetectorRef,
private dialogService: DialogService,
private errorHandler: FormErrorHandlerService,
protected mdDialog: MatDialog,
) {}
isCustActionVisible(actionId: string): boolean {
if (actionId === 'enable_action' && this.TwoFactorEnabled) {
return false;
} if (actionId === 'disable_action' && !this.TwoFactorEnabled) {
return false;
}
return true;
}
ngOnInit(): void {
this.isFormLoading = true;
isCustActionDisabled(actionId: string): boolean {
// Disables the 'Enable 2F' & 'Show QR' buttons if there is no secret
if (actionId === 'renew_secret') {
return !this.TwoFactorEnabled;
} if (actionId === 'show_qr') {
return !(this.secret && this.secret !== '');
}
}
this.ws.call('auth.twofactor.config').pipe(untilDestroyed(this)).subscribe(
(config: TwoFactorConfig) => {
this.secret = config.secret;
this.twoFactorEnabled = config.enabled;
this.digitsOnLoad = config.otp_digits;
this.intervalOnLoad = config.interval;
this.form.patchValue({
...config,
ssh: config.services.ssh,
});
this.isFormLoading = false;
this.cdr.markForCheck();
},
(error) => {
this.isFormLoading = false;
new EntityUtils().handleWsError(null, error, this.dialogService);
this.cdr.markForCheck();
},
);
afterInit(entityEdit: EntityFormComponent): void {
this.entityEdit = entityEdit;
this.getUri();
const intervalValue: FormInputConfig = _.find(this.fieldConfig, { name: 'interval' }) as FormInputConfig;
entityEdit.formGroup.controls['interval'].valueChanges.pipe(untilDestroyed(this)).subscribe((val: string) => {
if (parseInt(val) !== 30) {
intervalValue.hint = helptext.two_factor.interval.hint;
this.getUri(false);
this.form.controls.interval.valueChanges.pipe(untilDestroyed(this)).subscribe((val: number | string) => {
if (this.form.controls.interval.valid && Number(val) !== 30) {
this.intervalHint = helptext.two_factor.interval.hint;
} else {
intervalValue.hint = null;
this.intervalHint = null;
}
});
}
getUri(): void {
this.ws.call('auth.twofactor.provisioning_uri').pipe(untilDestroyed(this)).subscribe((provisioningUri) => {
this.entityEdit.formGroup.controls['uri'].setValue(provisioningUri);
this.qrInfo = provisioningUri;
}, (err) => {
this.loader.close();
this.dialog.errorReport(helptext.two_factor.error, err.reason, err.trace.formatted);
});
}
updateEnabledStatus(): void {
const enabled = _.find(this.fieldConfig, { name: 'enabled_status' }) as FormParagraphConfig;
if (this.TwoFactorEnabled) {
enabled.paraText = helptext.two_factor.enabled_status_true;
} else {
enabled.paraText = helptext.two_factor.enabled_status_false;
}
}
onSubmit(): void {
const values = this.form.value;
const params = {
otp_digits: Number(values.otp_digits),
interval: Number(values.interval),
window: Number(values.window),
enabled: this.twoFactorEnabled,
services: { ssh: values.ssh },
};
customSubmit(data: any): void {
if (data.otp_digits === this.digitsOnLoad && data.interval === this.intervalOnLoad) {
this.doSubmit(data);
if (params.otp_digits === this.digitsOnLoad && params.interval === this.intervalOnLoad) {
this.doSubmit(params);
} else {
this.dialog.confirm({
this.dialogService.confirm({
title: helptext.two_factor.submitDialog.title,
message: helptext.two_factor.submitDialog.message,
hideCheckBox: true,
buttonMsg: helptext.two_factor.submitDialog.btn,
}).pipe(untilDestroyed(this)).subscribe(() => {
this.intervalOnLoad = data.interval;
this.digitsOnLoad = data.otp_digits;
this.doSubmit(data, true);
}).pipe(filter(Boolean), untilDestroyed(this)).subscribe(() => {
this.intervalOnLoad = params.interval;
this.digitsOnLoad = params.otp_digits;
this.doSubmit(params, true);
});
}
}
doSubmit(data: any, openQr = false): void {
data.enabled = this.TwoFactorEnabled;
data.services = { ssh: data.ssh };
const extras = ['instructions', 'enabled_status', 'secret', 'uri', 'ssh'];
extras.forEach((extra) => {
delete data[extra];
});
this.loader.open();
this.ws.call('auth.twofactor.update', [data]).pipe(untilDestroyed(this)).subscribe(() => {
this.loader.close();
doSubmit(params: TwoFactorConfigUpdate, openQr = false): void {
this.isFormLoading = true;
this.ws.call('auth.twofactor.update', [params]).pipe(untilDestroyed(this)).subscribe(() => {
this.isFormLoading = false;
this.cdr.markForCheck();
if (openQr) {
this.openQrDialog();
}
}, (err) => {
this.loader.close();
this.dialog.errorReport(helptext.two_factor.error,
err.reason, err.trace.formatted);
this.isFormLoading = false;
this.errorHandler.handleWsFormError(err, this.form);
this.cdr.markForCheck();
});
}
get twoFactorStatusText(): string {
return this.twoFactorEnabled
? helptext.two_factor.enabled_status_true
: helptext.two_factor.enabled_status_false;
}
get twoFactorButtonText(): string {
return this.twoFactorEnabled
? helptext.two_factor.disable_button
: helptext.two_factor.enable_button;
}
toggleTwoFactor(): void {
if (this.twoFactorEnabled) {
this.twoFactorEnabled = false;
this.ws.call('auth.twofactor.update', [{ enabled: false }])
.pipe(untilDestroyed(this)).subscribe(() => {
this.isFormLoading = false;
}, (err) => {
this.isFormLoading = false;
this.twoFactorEnabled = true;
this.dialogService.errorReport(helptext.two_factor.error,
err.reason, err.trace.formatted);
});
} else {
this.dialogService.confirm({
title: helptext.two_factor.confirm_dialog.title,
message: helptext.two_factor.confirm_dialog.message,
hideCheckBox: true,
buttonMsg: helptext.two_factor.confirm_dialog.btn,
}).pipe(filter(Boolean), untilDestroyed(this)).subscribe(() => {
this.isFormLoading = true;
this.ws.call('auth.twofactor.update', [{ enabled: true }])
.pipe(untilDestroyed(this)).subscribe(() => {
this.isFormLoading = false;
this.twoFactorEnabled = true;
this.updateSecretAndUri();
}, (err) => {
this.isFormLoading = false;
this.dialogService.errorReport(helptext.two_factor.error,
err.reason, err.trace.formatted);
});
});
}
}
openQrDialog(): void {
this.mdDialog.open(QrDialogComponent, {
width: '300px',
data: { qrInfo: this.qrInfo },
data: { qrInfo: this.form.controls.uri.value },
});
}
renewSecret(): void {
this.dialog.confirm({
this.dialogService.confirm({
title: helptext.two_factor.renewSecret.title,
message: helptext.two_factor.renewSecret.message,
hideCheckBox: true,
buttonMsg: helptext.two_factor.renewSecret.btn,
}).pipe(filter(Boolean), untilDestroyed(this)).subscribe(() => {
this.loader.open();
this.isFormLoading = true;
this.ws.call('auth.twofactor.renew_secret').pipe(untilDestroyed(this)).subscribe(() => {
this.loader.close();
this.isFormLoading = false;
this.updateSecretAndUri();
},
(err) => {
this.loader.close();
this.dialog.errorReport(helptext.two_factor.error,
this.isFormLoading = false;
this.dialogService.errorReport(helptext.two_factor.error,
err.reason, err.trace.formatted);
});
});
}
updateSecretAndUri(): void {
this.ws.call('auth.twofactor.config').pipe(untilDestroyed(this)).subscribe((config) => {
this.entityEdit.formGroup.controls['secret'].setValue(config.secret);
this.secret = config.secret;
this.getUri();
}, (err) => {
this.loader.close();
this.dialog.errorReport(helptext.two_factor.error,
err.reason, err.trace.formatted);
});
this.isFormLoading = true;
this.ws.call('auth.twofactor.config').pipe(untilDestroyed(this)).subscribe(
(config: TwoFactorConfig) => {
this.isFormLoading = false;
this.form.controls.secret.setValue(config.secret);
this.cdr.markForCheck();
this.secret = config.secret;
this.getUri();
}, (err) => {
this.isFormLoading = false;
this.dialogService.errorReport(helptext.two_factor.error,
err.reason, err.trace.formatted);
},
);
}
getUri(openQr = true): void {
this.isFormLoading = true;
this.ws.call('auth.twofactor.provisioning_uri').pipe(untilDestroyed(this)).subscribe(
(provisioningUri: string) => {
this.isFormLoading = false;
this.form.controls.uri.setValue(provisioningUri);
this.cdr.markForCheck();
if (this.secret && openQr) {
this.openQrDialog();
}
}, (err) => {
this.isFormLoading = false;
this.dialogService.errorReport(helptext.two_factor.error,
err.reason, err.trace.formatted);
},
);
}
}
......@@ -3249,6 +3249,7 @@
"Show Built-in Users": "",
"Show Console Messages": "",
"Show Extra Columns": "",
"Show QR": "",
"Show Text Console without Password Prompt": "",
"Show Tracebacks in Case of Fatal Error": "",
"Show all available groups, including those that do not have quotas set.": "",
......
......@@ -3249,6 +3249,7 @@
"Show Built-in Users": "",
"Show Console Messages": "",
"Show Extra Columns": "",
"Show QR": "",
"Show Text Console without Password Prompt": "",
"Show Tracebacks in Case of Fatal Error": "",
"Show all available groups, including those that do not have quotas set.": "",
......
......@@ -3249,6 +3249,7 @@
"Show Built-in Users": "",
"Show Console Messages": "",
"Show Extra Columns": "",
"Show QR": "",
"Show Text Console without Password Prompt": "",
"Show Tracebacks in Case of Fatal Error": "",
"Show all available groups, including those that do not have quotas set.": "",
......
......@@ -3249,6 +3249,7 @@
"Show Built-in Users": "",
"Show Console Messages": "",
"Show Extra Columns": "",
"Show QR": "",
"Show Text Console without Password Prompt": "",
"Show Tracebacks in Case of Fatal Error": "",
"Show all available groups, including those that do not have quotas set.": "",
......
......@@ -3249,6 +3249,7 @@
"Show Built-in Users": "",
"Show Console Messages": "",
"Show Extra Columns": "",
"Show QR": "",
"Show Text Console without Password Prompt": "",
"Show Tracebacks in Case of Fatal Error": "",
"Show all available groups, including those that do not have quotas set.": "",
......
......@@ -3249,6 +3249,7 @@
"Show Built-in Users": "",
"Show Console Messages": "",
"Show Extra Columns": "",
"Show QR": "",
"Show Text Console without Password Prompt": "",
"Show Tracebacks in Case of Fatal Error": "",
"Show all available groups, including those that do not have quotas set.": "",
......
......@@ -3249,6 +3249,7 @@
"Show Built-in Users": "",
"Show Console Messages": "",
"Show Extra Columns": "",
"Show QR": "",
"Show Text Console without Password Prompt": "",
"Show Tracebacks in Case of Fatal Error": "",
"Show all available groups, including those that do not have quotas set.": "",
......
......@@ -3249,6 +3249,7 @@
"Show Built-in Users": "",
"Show Console Messages": "",
"Show Extra Columns": "",
"Show QR": "",
"Show Text Console without Password Prompt": "",
"Show Tracebacks in Case of Fatal Error": "",
"Show all available groups, including those that do not have quotas set.": "",
......
......@@ -3249,6 +3249,7 @@
"Show Built-in Users": "",
"Show Console Messages": "",
"Show Extra Columns": "",
"Show QR": "",
"Show Text Console without Password Prompt": "",
"Show Tracebacks in Case of Fatal Error": "",
"Show all available groups, including those that do not have quotas set.": "",
......
......@@ -3249,6 +3249,7 @@
"Show Built-in Users": "",
"Show Console Messages": "",
"Show Extra Columns": "",
"Show QR": "",
"Show Text Console without Password Prompt": "",
"Show Tracebacks in Case of Fatal Error": "",
"Show all available groups, including those that do not have quotas set.": "",
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment