committed by
GitHub
18 changed files with 1165 additions and 187 deletions
@ -0,0 +1,304 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2025 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 class="tb-two-factor-auth-login-content mat-app-background tb-dark flex flex-row items-center justify-center" |
|||
style="width: 100%;"> |
|||
@switch (state()) { |
|||
@case (ForceTwoFAState.SETUP) { |
|||
<mat-card appearance="raised" class="tb-two-factor-auth-login-card flex-initial"> |
|||
<mat-card-header> |
|||
<mat-card-title class="mat-headline-5 flex flex-row items-center justify-start"> |
|||
<button mat-icon-button type="button" (click)="cancelLogin()"> |
|||
<mat-icon>chevron_left</mat-icon> |
|||
</button> |
|||
{{ (config ? 'login.two-fa' :'login.two-fa-required') | translate }} |
|||
</mat-card-title> |
|||
</mat-card-header> |
|||
<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> |
|||
@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> |
|||
} |
|||
@if (config) { |
|||
<button type="button" mat-raised-button color="accent" class="navigation w-full" (click)="cancelLogin()"> |
|||
{{ 'login.login' | translate }} |
|||
</button> |
|||
} |
|||
</div> |
|||
</mat-card-content> |
|||
</mat-card> |
|||
} |
|||
@case (ForceTwoFAState.AUTHENTICATOR_APP) { |
|||
@switch (appState()) { |
|||
@case (ProvidersState.INPUT) { |
|||
<mat-card appearance="raised" class="tb-two-factor-auth-login-card flex-initial"> |
|||
<mat-card-header> |
|||
<mat-card-title class="mat-headline-5 flex flex-row items-center justify-start"> |
|||
<button mat-icon-button type="button" (click)="state.set(ForceTwoFAState.SETUP)"> |
|||
<mat-icon>chevron_left</mat-icon> |
|||
</button> |
|||
{{ 'login.enable-authenticator-app' | translate }} |
|||
</mat-card-title> |
|||
</mat-card-header> |
|||
<mat-card-content> |
|||
<div class="flex flex-col items-center justify-start"> |
|||
<p class="mat-body qr-code-description mb-4" translate>login.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 mb-8 w-full overflow-hidden"> |
|||
<span tbTruncateWithTooltip class="w-full">{{ totpAuthURLSecret }}</span> |
|||
<tb-copy-button |
|||
class="attribute-copy" |
|||
[disabled]="isLoading$ | async" |
|||
[copyText]="totpAuthURLSecret" |
|||
tooltipText="{{ 'login.copy-key' | translate }}" |
|||
tooltipPosition="above" |
|||
icon="content_copy" |
|||
[style]="{'font-size': '24px', color: 'rgba(255,255,255,.8)'}" |
|||
> |
|||
</tb-copy-button> |
|||
</div> |
|||
<div class="flex flex-col items-center justify-start gap-2 w-full"> |
|||
<button type="button" mat-stroked-button class="navigation w-full" (click)="appState.set(ProvidersState.ENTER_CODE)"> |
|||
{{ 'login.continue' | translate }} |
|||
</button> |
|||
<button type="button" mat-flat-button class="navigation w-full" (click)="tryAnotherWay(TwoFactorAuthProviderType.TOTP)"> |
|||
{{ 'login.try-another-way' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</mat-card-content> |
|||
</mat-card> |
|||
} |
|||
@case (ProvidersState.ENTER_CODE) { |
|||
<ng-container *ngTemplateOutlet="enterCodeTemplateCard; context: {providerType: TwoFactorAuthProviderType.TOTP}"></ng-container> |
|||
} |
|||
@case (ProvidersState.SUCCESS) { |
|||
<ng-container *ngTemplateOutlet="successTemplateCard; context: {providerType: TwoFactorAuthProviderType.TOTP}"></ng-container> |
|||
} |
|||
} |
|||
} |
|||
@case (ForceTwoFAState.SMS) { |
|||
@switch (smsState()) { |
|||
@case (ProvidersState.INPUT) { |
|||
<mat-card appearance="raised" class="tb-two-factor-auth-login-card flex-initial"> |
|||
<mat-card-header> |
|||
<mat-card-title class="mat-headline-5 flex flex-row items-center justify-start"> |
|||
<button mat-icon-button type="button" (click)="state.set(ForceTwoFAState.SETUP)"> |
|||
<mat-icon>chevron_left</mat-icon> |
|||
</button> |
|||
{{ 'login.enable-authenticator-sms' | translate }} |
|||
</mat-card-title> |
|||
</mat-card-header> |
|||
<mat-card-content> |
|||
<div class="flex flex-col items-center justify-start"> |
|||
<form [formGroup]="smsConfigForm" class="mb-12"> |
|||
<p class="mat-body step-description input" translate>login.sms-description</p> |
|||
<div class="flex flex-row items-center justify-between gap-3.75"> |
|||
<tb-phone-input class="flex-1" |
|||
label="{{ 'login.phone-input.phone-input-label' | translate }}" |
|||
hint="login.phone-input.phone-input-hint" |
|||
requiredErrorText="{{ 'login.phone-input.phone-input-required' | translate }}" |
|||
validationErrorText="{{ 'login.phone-input.phone-input-validation' | translate }}" |
|||
formControlName="phone" |
|||
[floatLabel]="'auto'"> |
|||
</tb-phone-input> |
|||
</div> |
|||
</form> |
|||
<div class="flex flex-col items-center justify-start gap-2 w-full"> |
|||
<button type="button" mat-stroked-button [disabled]="(isLoading$ | async) || smsConfigForm.invalid || !smsConfigForm.dirty" class="navigation w-full" (click)="sendSmsCode()"> |
|||
{{ 'login.send-code' | translate }} |
|||
</button> |
|||
<button type="button" mat-flat-button class="navigation w-full" (click)="tryAnotherWay(TwoFactorAuthProviderType.SMS)"> |
|||
{{ 'login.try-another-way' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</mat-card-content> |
|||
</mat-card> |
|||
} |
|||
@case (ProvidersState.ENTER_CODE) { |
|||
<ng-container *ngTemplateOutlet="enterCodeTemplateCard; context: {providerType: TwoFactorAuthProviderType.SMS}"></ng-container> |
|||
} |
|||
@case (ProvidersState.SUCCESS) { |
|||
<ng-container *ngTemplateOutlet="successTemplateCard; context: {providerType: TwoFactorAuthProviderType.SMS}"></ng-container> |
|||
} |
|||
} |
|||
} |
|||
@case (ForceTwoFAState.EMAIL) { |
|||
@switch (emailState()) { |
|||
@case (ProvidersState.INPUT) { |
|||
<mat-card appearance="raised" class="tb-two-factor-auth-login-card flex-initial"> |
|||
<mat-card-header> |
|||
<mat-card-title class="mat-headline-5 flex flex-row items-center justify-start"> |
|||
<button mat-icon-button type="button" (click)="state.set(ForceTwoFAState.SETUP)"> |
|||
<mat-icon>chevron_left</mat-icon> |
|||
</button> |
|||
{{ 'login.enable-authenticator-email' | translate }} |
|||
</mat-card-title> |
|||
</mat-card-header> |
|||
<mat-card-content> |
|||
<div class="flex flex-col items-center justify-start"> |
|||
<form [formGroup]="emailConfigForm" class="mb-8"> |
|||
<p class="mat-body step-description input" translate>login.email-description</p> |
|||
<mat-form-field class="mat-block input-container flex-1"> |
|||
<input matInput formControlName="email" |
|||
type="email" required |
|||
placeholder="{{ 'login.email-label' | translate }}" /> |
|||
<mat-error *ngIf="emailConfigForm.get('email').hasError('required')"> |
|||
{{ 'login.email-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="emailConfigForm.get('email').hasError('email')"> |
|||
{{ 'login.invalid-email-format' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</form> |
|||
<div class="flex flex-col items-center justify-start gap-2 w-full"> |
|||
<button type="button" mat-stroked-button [disabled]="(isLoading$ | async) || emailConfigForm.invalid" class="navigation w-full" (click)="sendEmailCode()"> |
|||
{{ 'login.send-code' | translate }} |
|||
</button> |
|||
<button type="button" mat-flat-button class="navigation w-full" (click)="tryAnotherWay(TwoFactorAuthProviderType.EMAIL)"> |
|||
{{ 'login.try-another-way' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</mat-card-content> |
|||
</mat-card> |
|||
} |
|||
@case (ProvidersState.ENTER_CODE) { |
|||
<ng-container *ngTemplateOutlet="enterCodeTemplateCard; context: {providerType: TwoFactorAuthProviderType.EMAIL}"></ng-container> |
|||
} |
|||
@case (ProvidersState.SUCCESS) { |
|||
<ng-container *ngTemplateOutlet="successTemplateCard; context: {providerType: TwoFactorAuthProviderType.EMAIL}"></ng-container> |
|||
} |
|||
} |
|||
} |
|||
@case (ForceTwoFAState.BACKUP_CODE) { |
|||
@switch (backupCodeState()) { |
|||
@case (BackupCodeState.CODE) { |
|||
<mat-card appearance="raised" class="tb-two-factor-auth-login-card flex-initial"> |
|||
<mat-card-header> |
|||
<mat-card-title class="mat-headline-5 flex flex-row items-center justify-start"> |
|||
<button mat-icon-button type="button" (click)="state.set(ForceTwoFAState.SETUP)"> |
|||
<mat-icon>chevron_left</mat-icon> |
|||
</button> |
|||
{{ 'login.get-backup-code' | translate }} |
|||
</mat-card-title> |
|||
</mat-card-header> |
|||
<mat-card-content> |
|||
<div mat-dialog-content tb-toast class="backup-code"> |
|||
<p class="mat-body-2 description" translate>login.backup-code-description</p> |
|||
<div class="container"> |
|||
@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()"> |
|||
{{ 'login.download-txt' | translate }} |
|||
</button> |
|||
<button type="button" mat-stroked-button class="provider w-full" (click)="printCode()"> |
|||
{{ 'login.print' | translate }} |
|||
</button> |
|||
</div> |
|||
<p class="mat-body-2 description" translate>login.backup-code-warn</p> |
|||
<button type="button" mat-raised-button color="accent" class="navigation w-full" (click)="backupCodeState.set(BackupCodeState.SUCCESS)"> |
|||
{{ 'login.continue' | translate }} |
|||
</button> |
|||
</div> |
|||
</mat-card-content> |
|||
</mat-card> |
|||
} |
|||
@case (BackupCodeState.SUCCESS) { |
|||
<ng-container *ngTemplateOutlet="successTemplateCard; context: {providerType: TwoFactorAuthProviderType.BACKUP_CODE}"></ng-container> |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</div> |
|||
|
|||
<ng-template #enterCodeTemplateCard let-providerType="providerType"> |
|||
<mat-card appearance="raised" class="tb-two-factor-auth-login-card flex-initial"> |
|||
<mat-card-header> |
|||
<mat-card-title class="mat-headline-5 flex flex-row items-center justify-start"> |
|||
<button mat-icon-button type="button" (click)="goBackByType(providerType)"> |
|||
<mat-icon>chevron_left</mat-icon> |
|||
</button> |
|||
{{ twoFactorAuthProvidersEnterCodeCardTranslate.get(providerType).name | translate }} |
|||
</mat-card-title> |
|||
</mat-card-header> |
|||
<mat-card-content> |
|||
<p class="mat-body inline-block"> |
|||
{{ twoFactorAuthProvidersEnterCodeCardTranslate.get(providerType).description | translate }} |
|||
@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"> |
|||
<input matInput formControlName="verificationCode" |
|||
maxlength="6" type="text" required |
|||
inputmode="numeric" pattern="[0-9]*" |
|||
autocomplete="off" |
|||
placeholder="{{ 'login.verification-code' | translate }}"> |
|||
<mat-error *ngIf="configForm.get('verificationCode').invalid"> |
|||
{{ 'login.verification-code-invalid' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<div class="flex flex-col items-center justify-start gap-2 w-full"> |
|||
<button type="button" mat-flat-button color="accent" [disabled]="(isLoading$ | async) || configForm.invalid || !configForm.dirty" class="navigation w-full" (click)="saveConfig(providerType)"> |
|||
{{ 'login.confirm' | translate }} |
|||
</button> |
|||
<button type="button" mat-flat-button class="navigation w-full" (click)="tryAnotherWay(providerType)"> |
|||
{{ 'login.try-another-way' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
</mat-card-content> |
|||
</mat-card> |
|||
</ng-template> |
|||
<ng-template #successTemplateCard let-providerType="providerType"> |
|||
<mat-card appearance="raised" class="tb-two-factor-auth-login-card flex-initial"> |
|||
<mat-card-header> |
|||
<mat-card-title class="mat-headline-5 flex flex-row items-center justify-start pl-10"> |
|||
{{ twoFactorAuthProvidersSuccessCardTranslate.get(providerType).name | translate }} |
|||
</mat-card-title> |
|||
</mat-card-header> |
|||
<mat-card-content> |
|||
<p class="mat-body mb-16" translate>{{ twoFactorAuthProvidersSuccessCardTranslate.get(providerType).description | translate }}</p> |
|||
<div class="flex flex-col items-center justify-start"> |
|||
<div class="flex flex-col items-center justify-start gap-2 w-full"> |
|||
<button type="button" mat-raised-button color="accent" class="navigation w-full" (click)="cancelLogin()"> |
|||
{{ 'login.login' | translate }} |
|||
</button> |
|||
@if (isAnyProviderAvailable) { |
|||
<button type="button" mat-flat-button class="navigation w-full" (click)="tryAnotherWay(providerType)"> |
|||
{{ 'login.add-verification-method' | translate }} |
|||
</button> |
|||
} |
|||
</div> |
|||
</div> |
|||
</mat-card-content> |
|||
</mat-card> |
|||
</ng-template> |
|||
@ -0,0 +1,110 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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 '../../../../../scss/constants'; |
|||
|
|||
:host { |
|||
display: flex; |
|||
flex: 1 1 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
|
|||
.tb-two-factor-auth-login-content { |
|||
background-color: #eee; |
|||
|
|||
.tb-two-factor-auth-login-card { |
|||
max-height: 100vh; |
|||
overflow: auto; |
|||
padding: 48px 48px 48px 16px; |
|||
|
|||
@media #{$mat-xs} { |
|||
height: 100%; |
|||
} |
|||
|
|||
@media #{$mat-gt-xs} { |
|||
width: 450px !important; |
|||
} |
|||
|
|||
.mat-mdc-card-title { |
|||
font: 400 28px / 36px Roboto, "Helvetica Neue", sans-serif; |
|||
} |
|||
|
|||
.mat-mdc-card-header { |
|||
padding: 0; |
|||
} |
|||
|
|||
.mat-mdc-card-content { |
|||
margin-top: 34px; |
|||
margin-left: 40px; |
|||
padding: 0; |
|||
} |
|||
|
|||
.mat-body { |
|||
letter-spacing: 0.25px; |
|||
line-height: 16px; |
|||
} |
|||
|
|||
.backup-code { |
|||
p { |
|||
text-align: justify; |
|||
} |
|||
|
|||
.container { |
|||
border: 1px solid; |
|||
border-radius: 4px; |
|||
gap: 16px; |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr; |
|||
justify-items: center; |
|||
padding: 16px 0; |
|||
margin-bottom: 16px; |
|||
|
|||
.code { |
|||
letter-spacing: 0.25px; |
|||
font-family: Roboto Mono, "Helvetica Neue", monospace; |
|||
} |
|||
} |
|||
|
|||
.action-buttons { |
|||
margin-bottom: 40px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
::ng-deep { |
|||
.tb-two-factor-auth-login-content { |
|||
.tb-two-factor-auth-login-card { |
|||
button.mat-mdc-icon-button { |
|||
.mat-icon { |
|||
color: rgba(255, 255, 255, 0.8); |
|||
} |
|||
} |
|||
} |
|||
.mat-mdc-form-field .mat-mdc-form-field-hint-wrapper { |
|||
color: rgba(255, 255, 255, 0.8); |
|||
} |
|||
} |
|||
|
|||
button.provider, button.navigation { |
|||
text-align: start; |
|||
font-weight: 400; |
|||
color: rgba(255, 255, 255, 0.8); |
|||
&:not([disabled][disabled]) { |
|||
border-color: rgba(255, 255, 255, .8); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,300 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 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, ElementRef, OnDestroy, OnInit, signal, ViewChild } from '@angular/core'; |
|||
import { AuthService } from '@core/auth/auth.service'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
|||
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; |
|||
import { |
|||
AccountTwoFaSettings, |
|||
BackupCodeTwoFactorAuthAccountConfig, |
|||
TotpTwoFactorAuthAccountConfig, |
|||
TwoFactorAuthAccountConfig, |
|||
twoFactorAuthProvidersEnterCodeCardTranslate, |
|||
twoFactorAuthProvidersLoginData, |
|||
twoFactorAuthProvidersSuccessCardTranslate, |
|||
TwoFactorAuthProviderType |
|||
} from '@shared/models/two-factor-auth.models'; |
|||
import { phoneNumberPattern } from '@shared/models/settings.models'; |
|||
import { deepClone, isDefinedAndNotNull, unwrapModule } from '@core/utils'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
|||
import printTemplate from '@home/pages/security/authentication-dialog/backup-code-print-template.raw'; |
|||
import { ImportExportService } from '@shared/import-export/import-export.service'; |
|||
import { mergeMap, tap } from 'rxjs/operators'; |
|||
|
|||
enum ForceTwoFAState { |
|||
SETUP = 'setup', |
|||
AUTHENTICATOR_APP = 'authenticatorApp', |
|||
SMS = 'sms', |
|||
EMAIL = 'email', |
|||
BACKUP_CODE = 'backupCode', |
|||
} |
|||
|
|||
enum ProvidersState { |
|||
INPUT = 'INPUT', |
|||
ENTER_CODE = 'ENTER_CODE', |
|||
SUCCESS = 'SUCCESS', |
|||
} |
|||
|
|||
enum BackupCodeState { |
|||
CODE = 'CODE', |
|||
SUCCESS = 'SUCCESS', |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-force-two-factor-auth-login', |
|||
templateUrl: './force-two-factor-auth-login.component.html', |
|||
styleUrls: ['./force-two-factor-auth-login.component.scss'] |
|||
}) |
|||
export class ForceTwoFactorAuthLoginComponent extends PageComponent implements OnInit, OnDestroy { |
|||
|
|||
TwoFactorAuthProviderType = TwoFactorAuthProviderType; |
|||
providersData = twoFactorAuthProvidersLoginData; |
|||
allowProviders: TwoFactorAuthProviderType[] = []; |
|||
config: AccountTwoFaSettings; |
|||
|
|||
twoFactorAuthProvidersEnterCodeCardTranslate = twoFactorAuthProvidersEnterCodeCardTranslate; |
|||
twoFactorAuthProvidersSuccessCardTranslate = twoFactorAuthProvidersSuccessCardTranslate; |
|||
|
|||
ForceTwoFAState = ForceTwoFAState; |
|||
ProvidersState = ProvidersState; |
|||
BackupCodeState = BackupCodeState |
|||
|
|||
state = signal<ForceTwoFAState>(ForceTwoFAState.SETUP); |
|||
appState = signal<ProvidersState>(ProvidersState.INPUT); |
|||
smsState = signal<ProvidersState>(ProvidersState.INPUT); |
|||
emailState = signal<ProvidersState>(ProvidersState.INPUT); |
|||
backupCodeState = signal<BackupCodeState>(BackupCodeState.CODE); |
|||
|
|||
totpAuthURL: string; |
|||
totpAuthURLSecret: string; |
|||
backupCode: BackupCodeTwoFactorAuthAccountConfig; |
|||
|
|||
configForm: UntypedFormGroup; |
|||
smsConfigForm: UntypedFormGroup; |
|||
emailConfigForm: UntypedFormGroup; |
|||
|
|||
private providersInfo: TwoFactorAuthProviderType[]; |
|||
private authAccountConfig: TwoFactorAuthAccountConfig; |
|||
private useByDefault: boolean = true; |
|||
|
|||
@ViewChild('canvas', {static: false}) canvasRef: ElementRef<HTMLCanvasElement>; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private authService: AuthService, |
|||
private twoFaService: TwoFactorAuthenticationService, |
|||
private importExportService: ImportExportService, |
|||
public dialog: MatDialog, |
|||
public dialogService: DialogService, |
|||
private fb: UntypedFormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.providersInfo = this.authService.forceTwoFactorAuthProviders; |
|||
this.allowedProviders(); |
|||
this.configForm = this.fb.group({ |
|||
verificationCode: ['', [ |
|||
Validators.required, |
|||
Validators.minLength(6), |
|||
Validators.maxLength(6), |
|||
Validators.pattern(/^\d*$/) |
|||
]] |
|||
}); |
|||
|
|||
this.smsConfigForm = this.fb.group({ |
|||
phone: ['', [Validators.required, Validators.pattern(phoneNumberPattern)]] |
|||
}); |
|||
|
|||
this.emailConfigForm = this.fb.group({ |
|||
email: [getCurrentAuthUser(this.store).sub, [Validators.required, Validators.email]] |
|||
}); |
|||
|
|||
this.twoFaService.getAccountTwoFaSettings().subscribe(accountConfig => { |
|||
if (accountConfig) { |
|||
this.config = accountConfig; |
|||
this.useByDefault = false; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
goBackByType(type: TwoFactorAuthProviderType) { |
|||
switch (type) { |
|||
case TwoFactorAuthProviderType.TOTP: |
|||
this.appState.set(ProvidersState.INPUT); |
|||
this.updateQRCode(); |
|||
break; |
|||
case TwoFactorAuthProviderType.SMS: |
|||
this.smsState.set(ProvidersState.INPUT); |
|||
break; |
|||
case TwoFactorAuthProviderType.EMAIL: |
|||
this.emailState.set(ProvidersState.INPUT); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
get isAnyProviderAvailable() { |
|||
return this.config?.configs ? Object.keys(this.config?.configs)?.length < this.allowProviders?.length : true; |
|||
} |
|||
|
|||
private allowedProviders() { |
|||
if (isDefinedAndNotNull(this.config)) { |
|||
this.allowProviders = this.providersInfo; |
|||
} else { |
|||
this.allowProviders = this.providersInfo.filter(provider => provider !== TwoFactorAuthProviderType.BACKUP_CODE); |
|||
} |
|||
} |
|||
|
|||
updateState(type: TwoFactorAuthProviderType) { |
|||
switch (type) { |
|||
case TwoFactorAuthProviderType.TOTP: |
|||
this.state.set(ForceTwoFAState.AUTHENTICATOR_APP); |
|||
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 = this.useByDefault; |
|||
this.useByDefault = false; |
|||
this.updateQRCode(); |
|||
}); |
|||
break; |
|||
case TwoFactorAuthProviderType.SMS: |
|||
this.state.set(ForceTwoFAState.SMS); |
|||
break; |
|||
case TwoFactorAuthProviderType.EMAIL: |
|||
this.state.set(ForceTwoFAState.EMAIL); |
|||
break; |
|||
case TwoFactorAuthProviderType.BACKUP_CODE: |
|||
this.state.set(ForceTwoFAState.BACKUP_CODE); |
|||
this.twoFaService.generateTwoFaAccountConfig(TwoFactorAuthProviderType.BACKUP_CODE).pipe( |
|||
tap((data: BackupCodeTwoFactorAuthAccountConfig) => this.backupCode = data), |
|||
mergeMap(data => this.twoFaService.verifyAndSaveTwoFaAccountConfig(data, null, {ignoreLoading: true})) |
|||
).subscribe((config) => { |
|||
this.config = config; |
|||
}); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
sendSmsCode() { |
|||
if (this.smsConfigForm.valid) { |
|||
this.authAccountConfig = { |
|||
providerType: TwoFactorAuthProviderType.SMS, |
|||
useByDefault: this.useByDefault, |
|||
phoneNumber: this.smsConfigForm.get('phone').value as string |
|||
}; |
|||
this.useByDefault = false; |
|||
this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => this.smsState.set(ProvidersState.ENTER_CODE)); |
|||
} |
|||
} |
|||
|
|||
sendEmailCode() { |
|||
if (this.emailConfigForm.valid) { |
|||
this.authAccountConfig = { |
|||
providerType: TwoFactorAuthProviderType.EMAIL, |
|||
useByDefault: this.useByDefault, |
|||
email: this.emailConfigForm.get('email').value as string |
|||
}; |
|||
this.useByDefault = false; |
|||
this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => this.emailState.set(ProvidersState.ENTER_CODE)); |
|||
} |
|||
} |
|||
|
|||
tryAnotherWay(type: TwoFactorAuthProviderType) { |
|||
this.state.set(ForceTwoFAState.SETUP); |
|||
this.configForm.reset(); |
|||
switch (type) { |
|||
case TwoFactorAuthProviderType.TOTP: |
|||
this.appState.set(ProvidersState.INPUT); |
|||
break; |
|||
case TwoFactorAuthProviderType.SMS: |
|||
this.smsState.set(ProvidersState.INPUT); |
|||
this.smsConfigForm.reset(); |
|||
break; |
|||
case TwoFactorAuthProviderType.EMAIL: |
|||
this.emailState.set(ProvidersState.INPUT) |
|||
this.emailConfigForm.get('email').reset(getCurrentAuthUser(this.store).sub); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
saveConfig(type: TwoFactorAuthProviderType) { |
|||
if (this.configForm.valid) { |
|||
this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig, |
|||
this.configForm.get('verificationCode').value).subscribe((config) => { |
|||
switch (type) { |
|||
case TwoFactorAuthProviderType.TOTP: |
|||
this.appState.set(ProvidersState.SUCCESS); |
|||
break; |
|||
case TwoFactorAuthProviderType.SMS: |
|||
this.smsState.set(ProvidersState.SUCCESS); |
|||
break; |
|||
case TwoFactorAuthProviderType.EMAIL: |
|||
this.emailState.set(ProvidersState.SUCCESS); |
|||
break; |
|||
} |
|||
this.config = config; |
|||
this.authAccountConfig = null; |
|||
this.allowedProviders(); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private updateQRCode() { |
|||
import('qrcode').then((QRCode) => { |
|||
unwrapModule(QRCode).toCanvas(this.canvasRef.nativeElement, this.totpAuthURL); |
|||
this.canvasRef.nativeElement.style.width = 'auto'; |
|||
this.canvasRef.nativeElement.style.height = 'auto'; |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
super.ngOnDestroy(); |
|||
} |
|||
|
|||
cancelLogin() { |
|||
this.authService.logout(); |
|||
} |
|||
|
|||
downloadFile() { |
|||
this.importExportService.exportText(this.backupCode.codes, 'backup-codes'); |
|||
} |
|||
|
|||
printCode() { |
|||
const codeTemplate = deepClone(this.backupCode.codes) |
|||
.map(code => `<div class="code-row"><input type="checkbox"><span class="code">${code}</span></div>`).join(''); |
|||
const printPage = printTemplate.replace('${codesBlock}', codeTemplate); |
|||
const newWindow = window.open('', 'Print backup code'); |
|||
|
|||
newWindow.document.open(); |
|||
newWindow.document.write(printPage); |
|||
|
|||
setTimeout(() => { |
|||
newWindow.print(); |
|||
|
|||
newWindow.document.close(); |
|||
|
|||
setTimeout(() => { |
|||
newWindow.close(); |
|||
}, 10); |
|||
}, 0); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue