UI: Add secret for totp auth dialog
This commit is contained in:
parent
b84d818b28
commit
4ddc8030cc
@ -14,7 +14,7 @@
|
|||||||
/// limitations under the License.
|
/// limitations under the License.
|
||||||
///
|
///
|
||||||
|
|
||||||
import { Component, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core';
|
import { Component, DestroyRef, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core';
|
||||||
import { PageComponent } from '@shared/components/page.component';
|
import { PageComponent } from '@shared/components/page.component';
|
||||||
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
|
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
@ -29,11 +29,10 @@ import {
|
|||||||
TwoFactorAuthSettingsForm
|
TwoFactorAuthSettingsForm
|
||||||
} from '@shared/models/two-factor-auth.models';
|
} from '@shared/models/two-factor-auth.models';
|
||||||
import { isDefined, isNotEmptyStr } from '@core/utils';
|
import { isDefined, isNotEmptyStr } from '@core/utils';
|
||||||
import { Subject } from 'rxjs';
|
|
||||||
import { takeUntil } from 'rxjs/operators';
|
|
||||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
import { MatExpansionPanel } from '@angular/material/expansion';
|
||||||
import { NotificationTargetConfigType, NotificationTargetConfigTypeInfoMap } from '@shared/models/notification.models';
|
import { NotificationTargetConfigType, NotificationTargetConfigTypeInfoMap } from '@shared/models/notification.models';
|
||||||
import { EntityType } from '@shared/models/entity-type.models';
|
import { EntityType } from '@shared/models/entity-type.models';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-2fa-settings',
|
selector: 'tb-2fa-settings',
|
||||||
@ -42,7 +41,6 @@ import { EntityType } from '@shared/models/entity-type.models';
|
|||||||
})
|
})
|
||||||
export class TwoFactorAuthSettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy {
|
export class TwoFactorAuthSettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy {
|
||||||
|
|
||||||
private readonly destroy$ = new Subject<void>();
|
|
||||||
private readonly posIntValidation = [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)];
|
private readonly posIntValidation = [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)];
|
||||||
|
|
||||||
twoFaFormGroup: UntypedFormGroup;
|
twoFaFormGroup: UntypedFormGroup;
|
||||||
@ -62,7 +60,8 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
|||||||
|
|
||||||
constructor(protected store: Store<AppState>,
|
constructor(protected store: Store<AppState>,
|
||||||
private twoFaService: TwoFactorAuthenticationService,
|
private twoFaService: TwoFactorAuthenticationService,
|
||||||
private fb: UntypedFormBuilder) {
|
private fb: UntypedFormBuilder,
|
||||||
|
private destroyRef: DestroyRef) {
|
||||||
super(store);
|
super(store);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,8 +74,6 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
|||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
super.ngOnDestroy();
|
super.ngOnDestroy();
|
||||||
this.destroy$.next();
|
|
||||||
this.destroy$.complete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmForm(): UntypedFormGroup {
|
confirmForm(): UntypedFormGroup {
|
||||||
@ -156,7 +153,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
|||||||
this.buildProvidersSettingsForm(provider);
|
this.buildProvidersSettingsForm(provider);
|
||||||
});
|
});
|
||||||
this.twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').valueChanges.pipe(
|
this.twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').valueChanges.pipe(
|
||||||
takeUntil(this.destroy$)
|
takeUntilDestroyed(this.destroyRef)
|
||||||
).subscribe(value => {
|
).subscribe(value => {
|
||||||
if (value) {
|
if (value) {
|
||||||
this.twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').enable({emitEvent: false});
|
this.twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').enable({emitEvent: false});
|
||||||
@ -167,7 +164,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.providersForm.valueChanges.pipe(
|
this.providersForm.valueChanges.pipe(
|
||||||
takeUntil(this.destroy$)
|
takeUntilDestroyed(this.destroyRef)
|
||||||
).subscribe((value: TwoFactorAuthProviderConfigForm[]) => {
|
).subscribe((value: TwoFactorAuthProviderConfigForm[]) => {
|
||||||
const activeProvider = value.filter(provider => provider.enable);
|
const activeProvider = value.filter(provider => provider.enable);
|
||||||
const indexBackupCode = Object.values(TwoFactorAuthProviderType).indexOf(TwoFactorAuthProviderType.BACKUP_CODE);
|
const indexBackupCode = Object.values(TwoFactorAuthProviderType).indexOf(TwoFactorAuthProviderType.BACKUP_CODE);
|
||||||
@ -181,7 +178,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.twoFaFormGroup.get('enforceTwoFa').valueChanges.pipe(
|
this.twoFaFormGroup.get('enforceTwoFa').valueChanges.pipe(
|
||||||
takeUntil(this.destroy$)
|
takeUntilDestroyed(this.destroyRef)
|
||||||
).subscribe(value => {
|
).subscribe(value => {
|
||||||
if (value) {
|
if (value) {
|
||||||
this.twoFaFormGroup.get('enforcedUsersFilter').enable({emitEvent: false});
|
this.twoFaFormGroup.get('enforcedUsersFilter').enable({emitEvent: false});
|
||||||
@ -245,7 +242,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
|
|||||||
}
|
}
|
||||||
const newProviders = this.fb.group(formControlConfig);
|
const newProviders = this.fb.group(formControlConfig);
|
||||||
newProviders.get('enable').valueChanges.pipe(
|
newProviders.get('enable').valueChanges.pipe(
|
||||||
takeUntil(this.destroy$)
|
takeUntilDestroyed(this.destroyRef)
|
||||||
).subscribe(value => {
|
).subscribe(value => {
|
||||||
if (value) {
|
if (value) {
|
||||||
newProviders.enable({emitEvent: false});
|
newProviders.enable({emitEvent: false});
|
||||||
|
|||||||
@ -52,6 +52,19 @@
|
|||||||
<form [formGroup]="totpConfigForm" class="flex flex-col items-center justify-start" (ngSubmit)="onSaveConfig()">
|
<form [formGroup]="totpConfigForm" class="flex flex-col items-center justify-start" (ngSubmit)="onSaveConfig()">
|
||||||
<p class="mat-body qr-code-description" translate>security.2fa.dialog.scan-qr-code</p>
|
<p class="mat-body qr-code-description" translate>security.2fa.dialog.scan-qr-code</p>
|
||||||
<canvas class="flex-1" #canvas [style.display]="totpAuthURL ? 'block' : 'none'"></canvas>
|
<canvas class="flex-1" #canvas [style.display]="totpAuthURL ? 'block' : 'none'"></canvas>
|
||||||
|
<p class="mat-body qr-code-description" translate>login.enter-key-manually</p>
|
||||||
|
<div class="flex flex-row items-center w-full overflow-hidden max-w-[375px]">
|
||||||
|
<span tbTruncateWithTooltip class="w-full">{{ totpAuthURLSecret }}</span>
|
||||||
|
<tb-copy-button
|
||||||
|
class="attribute-copy"
|
||||||
|
[disabled]="isLoading$ | async"
|
||||||
|
[copyText]="totpAuthURLSecret"
|
||||||
|
tooltipText="{{ 'attribute.copy-key' | translate }}"
|
||||||
|
tooltipPosition="above"
|
||||||
|
icon="content_copy"
|
||||||
|
[style]="{'font-size': '24px'}">
|
||||||
|
</tb-copy-button>
|
||||||
|
</div>
|
||||||
<p class="mat-body qr-code-description" style="margin-top: 30px;" translate>security.2fa.dialog.enter-verification-code</p>
|
<p class="mat-body qr-code-description" style="margin-top: 30px;" translate>security.2fa.dialog.enter-verification-code</p>
|
||||||
<mat-form-field class="mat-block code-container flex-1">
|
<mat-form-field class="mat-block code-container flex-1">
|
||||||
<input matInput formControlName="verificationCode"
|
<input matInput formControlName="verificationCode"
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export class TotpAuthDialogComponent extends DialogComponent<TotpAuthDialogCompo
|
|||||||
|
|
||||||
totpConfigForm: UntypedFormGroup;
|
totpConfigForm: UntypedFormGroup;
|
||||||
totpAuthURL: string;
|
totpAuthURL: string;
|
||||||
|
totpAuthURLSecret: string;
|
||||||
|
|
||||||
@ViewChild('stepper', {static: false}) stepper: MatStepper;
|
@ViewChild('stepper', {static: false}) stepper: MatStepper;
|
||||||
@ViewChild('canvas', {static: false}) canvasRef: ElementRef<HTMLCanvasElement>;
|
@ViewChild('canvas', {static: false}) canvasRef: ElementRef<HTMLCanvasElement>;
|
||||||
@ -55,6 +56,7 @@ export class TotpAuthDialogComponent extends DialogComponent<TotpAuthDialogCompo
|
|||||||
this.twoFaService.generateTwoFaAccountConfig(TwoFactorAuthProviderType.TOTP).subscribe(accountConfig => {
|
this.twoFaService.generateTwoFaAccountConfig(TwoFactorAuthProviderType.TOTP).subscribe(accountConfig => {
|
||||||
this.authAccountConfig = accountConfig as TotpTwoFactorAuthAccountConfig;
|
this.authAccountConfig = accountConfig as TotpTwoFactorAuthAccountConfig;
|
||||||
this.totpAuthURL = this.authAccountConfig.authUrl;
|
this.totpAuthURL = this.authAccountConfig.authUrl;
|
||||||
|
this.totpAuthURLSecret = new URL(this.totpAuthURL).searchParams.get('secret');
|
||||||
this.authAccountConfig.useByDefault = true;
|
this.authAccountConfig.useByDefault = true;
|
||||||
import('qrcode').then((QRCode) => {
|
import('qrcode').then((QRCode) => {
|
||||||
unwrapModule(QRCode).toCanvas(this.canvasRef.nativeElement, this.totpAuthURL);
|
unwrapModule(QRCode).toCanvas(this.canvasRef.nativeElement, this.totpAuthURL);
|
||||||
|
|||||||
@ -31,12 +31,12 @@
|
|||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<div class="providers-container tb-default flex flex-col gap-2">
|
<div class="providers-container tb-default flex flex-col gap-2">
|
||||||
<p class="mat-body"> {{ (config ? 'login.set-up-verification-method-login' :'login.set-up-verification-method') | translate }}</p>
|
<p class="mat-body"> {{ (config ? 'login.set-up-verification-method-login' :'login.set-up-verification-method') | translate }}</p>
|
||||||
<ng-container *ngFor="let provider of allowProviders">
|
@for (provider of allowProviders; track provider) {
|
||||||
<button type="button" [disabled]="config?.configs?.[provider]" mat-stroked-button class="provider" (click)="updateState(provider)">
|
<button type="button" [disabled]="config?.configs?.[provider]" mat-stroked-button class="provider" (click)="updateState(provider)">
|
||||||
<mat-icon class="tb-mat-18" svgIcon="{{ providersData.get(provider).icon }}"></mat-icon>
|
<mat-icon class="tb-mat-18" svgIcon="{{ providersData.get(provider).icon }}"></mat-icon>
|
||||||
{{ providersData.get(provider).name | translate }}
|
{{ providersData.get(provider).name | translate }}
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
}
|
||||||
@if (config) {
|
@if (config) {
|
||||||
<button type="button" mat-raised-button color="accent" class="navigation w-full" (click)="cancelLogin()">
|
<button type="button" mat-raised-button color="accent" class="navigation w-full" (click)="cancelLogin()">
|
||||||
{{ 'login.login' | translate }}
|
{{ 'login.login' | translate }}
|
||||||
@ -63,7 +63,7 @@
|
|||||||
<p class="mat-body qr-code-description mb-4" translate>security.2fa.dialog.scan-qr-code</p>
|
<p class="mat-body qr-code-description mb-4" translate>security.2fa.dialog.scan-qr-code</p>
|
||||||
<canvas class="flex-1" #canvas [style.display]="totpAuthURL ? 'block' : 'none'"></canvas>
|
<canvas class="flex-1" #canvas [style.display]="totpAuthURL ? 'block' : 'none'"></canvas>
|
||||||
<p class="mat-body qr-code-description" translate>login.enter-key-manually</p>
|
<p class="mat-body qr-code-description" translate>login.enter-key-manually</p>
|
||||||
<div class="flex flex-row mb-8 w-full overflow-hidden" style="align-items: center">
|
<div class="flex flex-row items-center mb-8 w-full overflow-hidden">
|
||||||
<span tbTruncateWithTooltip class="w-full">{{ totpAuthURLSecret }}</span>
|
<span tbTruncateWithTooltip class="w-full">{{ totpAuthURLSecret }}</span>
|
||||||
<tb-copy-button
|
<tb-copy-button
|
||||||
class="attribute-copy"
|
class="attribute-copy"
|
||||||
@ -203,9 +203,9 @@
|
|||||||
<div mat-dialog-content tb-toast class="backup-code">
|
<div mat-dialog-content tb-toast class="backup-code">
|
||||||
<p class="mat-body-2 description" translate>security.2fa.dialog.backup-code-description</p>
|
<p class="mat-body-2 description" translate>security.2fa.dialog.backup-code-description</p>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div *ngFor="let code of backupCode?.codes" class="code">
|
@for (code of backupCode?.codes; track code) {
|
||||||
{{ code }}
|
<div class="code">{{ code }}</div>
|
||||||
</div>
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="action-buttons flex flex-row items-center justify-start gap-4">
|
<div class="action-buttons flex flex-row items-center justify-start gap-4">
|
||||||
<button type="button" mat-flat-button class="provider w-full" (click)="downloadFile()">
|
<button type="button" mat-flat-button class="provider w-full" (click)="downloadFile()">
|
||||||
@ -242,10 +242,14 @@
|
|||||||
</mat-card-title>
|
</mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<p class="mat-body">
|
<p class="mat-body inline-block">
|
||||||
{{ twoFactorAuthProvidersEnterCodeCardTranslate.get(providerType).description | translate }}
|
{{ twoFactorAuthProvidersEnterCodeCardTranslate.get(providerType).description | translate }}
|
||||||
<ng-container *ngIf="providerType === TwoFactorAuthProviderType.SMS">{{ smsConfigForm.get('phone').value }}</ng-container>
|
@if (providerType === TwoFactorAuthProviderType.SMS) {
|
||||||
<ng-container *ngIf="providerType === TwoFactorAuthProviderType.EMAIL">{{ emailConfigForm.get('email').value }}</ng-container>
|
<span>{{ smsConfigForm.get('phone').value }}</span>
|
||||||
|
}
|
||||||
|
@if (providerType === TwoFactorAuthProviderType.EMAIL) {
|
||||||
|
<span>{{ emailConfigForm.get('email').value }}</span>
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
<form [formGroup]="configForm" class="flex flex-col items-center justify-start">
|
<form [formGroup]="configForm" class="flex flex-col items-center justify-start">
|
||||||
<mat-form-field class="mat-block w-full">
|
<mat-form-field class="mat-block w-full">
|
||||||
|
|||||||
@ -511,7 +511,7 @@
|
|||||||
"verification-limitations": "Verification limitations",
|
"verification-limitations": "Verification limitations",
|
||||||
"verification-message-template-pattern": "Verification message need to contains pattern: ${code}",
|
"verification-message-template-pattern": "Verification message need to contains pattern: ${code}",
|
||||||
"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": "Within time",
|
||||||
"within-time-pattern": "Time must be a positive integer.",
|
"within-time-pattern": "Time must be a positive integer.",
|
||||||
"within-time-required": "Time is required.",
|
"within-time-required": "Time is required.",
|
||||||
"force-2fa": "Force two-factor authentication",
|
"force-2fa": "Force two-factor authentication",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user