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. /// 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});

View File

@ -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"

View File

@ -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);

View File

@ -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">

View File

@ -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",