UI: Add secret for totp auth dialog

This commit is contained in:
ArtemDzhereleiko 2025-09-24 16:35:23 +03:00
parent b84d818b28
commit 4ddc8030cc
5 changed files with 37 additions and 21 deletions

View File

@ -14,7 +14,7 @@
/// 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 { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { Store } from '@ngrx/store';
@ -29,11 +29,10 @@ import {
TwoFactorAuthSettingsForm
} from '@shared/models/two-factor-auth.models';
import { isDefined, isNotEmptyStr } from '@core/utils';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { MatExpansionPanel } from '@angular/material/expansion';
import { NotificationTargetConfigType, NotificationTargetConfigTypeInfoMap } from '@shared/models/notification.models';
import { EntityType } from '@shared/models/entity-type.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
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 {
private readonly destroy$ = new Subject<void>();
private readonly posIntValidation = [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)];
twoFaFormGroup: UntypedFormGroup;
@ -62,7 +60,8 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
constructor(protected store: Store<AppState>,
private twoFaService: TwoFactorAuthenticationService,
private fb: UntypedFormBuilder) {
private fb: UntypedFormBuilder,
private destroyRef: DestroyRef) {
super(store);
}
@ -75,8 +74,6 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
ngOnDestroy() {
super.ngOnDestroy();
this.destroy$.next();
this.destroy$.complete();
}
confirmForm(): UntypedFormGroup {
@ -156,7 +153,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
this.buildProvidersSettingsForm(provider);
});
this.twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').valueChanges.pipe(
takeUntil(this.destroy$)
takeUntilDestroyed(this.destroyRef)
).subscribe(value => {
if (value) {
this.twoFaFormGroup.get('verificationCodeCheckRateLimitNumber').enable({emitEvent: false});
@ -167,7 +164,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
}
});
this.providersForm.valueChanges.pipe(
takeUntil(this.destroy$)
takeUntilDestroyed(this.destroyRef)
).subscribe((value: TwoFactorAuthProviderConfigForm[]) => {
const activeProvider = value.filter(provider => provider.enable);
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(
takeUntil(this.destroy$)
takeUntilDestroyed(this.destroyRef)
).subscribe(value => {
if (value) {
this.twoFaFormGroup.get('enforcedUsersFilter').enable({emitEvent: false});
@ -245,7 +242,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
}
const newProviders = this.fb.group(formControlConfig);
newProviders.get('enable').valueChanges.pipe(
takeUntil(this.destroy$)
takeUntilDestroyed(this.destroyRef)
).subscribe(value => {
if (value) {
newProviders.enable({emitEvent: false});

View File

@ -52,6 +52,19 @@
<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>
<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>
<mat-form-field class="mat-block code-container flex-1">
<input matInput formControlName="verificationCode"

View File

@ -42,6 +42,7 @@ export class TotpAuthDialogComponent extends DialogComponent<TotpAuthDialogCompo
totpConfigForm: UntypedFormGroup;
totpAuthURL: string;
totpAuthURLSecret: string;
@ViewChild('stepper', {static: false}) stepper: MatStepper;
@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.authAccountConfig = accountConfig as TotpTwoFactorAuthAccountConfig;
this.totpAuthURL = this.authAccountConfig.authUrl;
this.totpAuthURLSecret = new URL(this.totpAuthURL).searchParams.get('secret');
this.authAccountConfig.useByDefault = true;
import('qrcode').then((QRCode) => {
unwrapModule(QRCode).toCanvas(this.canvasRef.nativeElement, this.totpAuthURL);

View File

@ -31,12 +31,12 @@
<mat-card-content>
<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>
<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)">
<mat-icon class="tb-mat-18" svgIcon="{{ providersData.get(provider).icon }}"></mat-icon>
{{ providersData.get(provider).name | translate }}
</button>
</ng-container>
}
@if (config) {
<button type="button" mat-raised-button color="accent" class="navigation w-full" (click)="cancelLogin()">
{{ 'login.login' | translate }}
@ -63,7 +63,7 @@
<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>
<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>
<tb-copy-button
class="attribute-copy"
@ -203,9 +203,9 @@
<div mat-dialog-content tb-toast class="backup-code">
<p class="mat-body-2 description" translate>security.2fa.dialog.backup-code-description</p>
<div class="container">
<div *ngFor="let code of backupCode?.codes" class="code">
{{ code }}
</div>
@for (code of backupCode?.codes; track code) {
<div class="code">{{ code }}</div>
}
</div>
<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()">
@ -242,10 +242,14 @@
</mat-card-title>
</mat-card-header>
<mat-card-content>
<p class="mat-body">
<p class="mat-body inline-block">
{{ twoFactorAuthProvidersEnterCodeCardTranslate.get(providerType).description | translate }}
<ng-container *ngIf="providerType === TwoFactorAuthProviderType.SMS">{{ smsConfigForm.get('phone').value }}</ng-container>
<ng-container *ngIf="providerType === TwoFactorAuthProviderType.EMAIL">{{ emailConfigForm.get('email').value }}</ng-container>
@if (providerType === TwoFactorAuthProviderType.SMS) {
<span>{{ smsConfigForm.get('phone').value }}</span>
}
@if (providerType === TwoFactorAuthProviderType.EMAIL) {
<span>{{ emailConfigForm.get('email').value }}</span>
}
</p>
<form [formGroup]="configForm" class="flex flex-col items-center justify-start">
<mat-form-field class="mat-block w-full">

View File

@ -511,7 +511,7 @@
"verification-limitations": "Verification limitations",
"verification-message-template-pattern": "Verification message need to contains pattern: ${code}",
"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-required": "Time is required.",
"force-2fa": "Force two-factor authentication",