diff --git a/ui-ngx/src/app/core/http/two-factor-authentication.service.ts b/ui-ngx/src/app/core/http/two-factor-authentication.service.ts index c2a7a5235d..2e3a7caeeb 100644 --- a/ui-ngx/src/app/core/http/two-factor-authentication.service.ts +++ b/ui-ngx/src/app/core/http/two-factor-authentication.service.ts @@ -24,6 +24,7 @@ import { TwoFactorAuthProviderType, TwoFactorAuthSettings } from '@shared/models/two-factor-auth.models'; +import { isDefinedAndNotNull } from '@core/utils'; @Injectable({ providedIn: 'root' @@ -66,10 +67,13 @@ export class TwoFactorAuthenticationService { return this.http.post(`/api/2fa/account/config/submit`, authConfig, defaultHttpOptionsFromConfig(config)); } - verifyAndSaveTwoFaAccountConfig(authConfig: TwoFactorAuthAccountConfig, verificationCode: number, + verifyAndSaveTwoFaAccountConfig(authConfig: TwoFactorAuthAccountConfig, verificationCode?: number, config?: RequestConfig): Observable { - return this.http.post(`/api/2fa/account/config?verificationCode=${verificationCode}`, - authConfig, defaultHttpOptionsFromConfig(config)); + let url = '/api/2fa/account/config'; + if (isDefinedAndNotNull(verificationCode)) { + url += `?verificationCode=${verificationCode}`; + } + return this.http.post(url, authConfig, defaultHttpOptionsFromConfig(config)); } deleteTwoFaAccountConfig(providerType: TwoFactorAuthProviderType, config?: RequestConfig): Observable { diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.html b/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.html index b1c241c2a0..ead10e727d 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.html +++ b/ui-ngx/src/app/modules/home/components/import-export/import-dialog-csv.component.html @@ -117,7 +117,7 @@ - {{ 'import.stepper-text.done' | translate }} + {{ 'action.done' | translate }}

{{ translate.instant('import.message.create-entities', {count: this.statistical.created}) }} diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts b/ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts index 3b3100f47a..e5426d0ce0 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts +++ b/ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts @@ -138,6 +138,11 @@ export interface FileType { extension: string; } +export const TEXT_TYPE: FileType = { + mimeType: 'text/plain', + extension: 'txt' +}; + export const JSON_TYPE: FileType = { mimeType: 'text/json', extension: 'json' diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts index d502aacc08..6e81442923 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts +++ b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts @@ -35,7 +35,7 @@ import { import { MatDialog } from '@angular/material/dialog'; import { ImportDialogComponent, ImportDialogData } from '@home/components/import-export/import-dialog.component'; import { forkJoin, Observable, of } from 'rxjs'; -import {catchError, map, mergeMap, switchMap, tap} from 'rxjs/operators'; +import { catchError, map, mergeMap, tap } from 'rxjs/operators'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { EntityService } from '@core/http/entity.service'; import { Widget, WidgetSize, WidgetType, WidgetTypeDetails } from '@shared/models/widget.models'; @@ -44,7 +44,16 @@ import { EntityAliasesDialogData } from '@home/components/alias/entity-aliases-dialog.component'; import { ItemBufferService, WidgetItem } from '@core/services/item-buffer.service'; -import { FileType, ImportWidgetResult, JSON_TYPE, WidgetsBundleItem, ZIP_TYPE, BulkImportRequest, BulkImportResult } from './import-export.models'; +import { + BulkImportRequest, + BulkImportResult, + FileType, + ImportWidgetResult, + JSON_TYPE, + TEXT_TYPE, + WidgetsBundleItem, + ZIP_TYPE +} from './import-export.models'; import { EntityType } from '@shared/models/entity-type.models'; import { UtilsService } from '@core/services/utils.service'; import { WidgetService } from '@core/http/widget.service'; @@ -554,6 +563,14 @@ export class ImportExportService { ); } + public exportText(data: string | Array, filename: string) { + let content = data; + if (Array.isArray(data)) { + content = data.join('\n'); + } + this.downloadFile(content, filename, TEXT_TYPE); + } + public exportJSZip(data: object, filename: string) { import('jszip').then((JSZip) => { const jsZip = new JSZip.default(); diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html index d50430cfe3..1efa83a2eb 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html @@ -41,7 +41,7 @@ (click)="toggleExtensionPanel($event, i, provider.get('enable').value)" formControlName="enable"> - {{ provider.value.providerType }} + {{ twoFactorAuthProvidersData.get(provider.value.providerType).name | translate }} @@ -94,6 +94,19 @@

+
+ + admin.2fa.number-of-codes + + + {{ "admin.2fa.number-of-codes-required" | translate }} + + + {{ "admin.2fa.number-of-codes-pattern" | translate }} + + +
@@ -142,7 +155,7 @@ - {{ 'admin.2fa.verification-code-check-rate-limit' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts index bc2d936aa6..c3f49a0c61 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; +import { Component, 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'; @@ -22,6 +22,7 @@ import { AppState } from '@core/core.state'; import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; import { + twoFactorAuthProvidersData, TwoFactorAuthProviderType, TwoFactorAuthSettings, TwoFactorAuthSettingsForm @@ -29,7 +30,6 @@ import { import { deepClone, isNotEmptyStr } from '@core/utils'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { MatStepper } from '@angular/material/stepper'; import { MatExpansionPanel } from '@angular/material/expansion'; @Component({ @@ -40,9 +40,11 @@ import { MatExpansionPanel } from '@angular/material/expansion'; export class TwoFactorAuthSettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy { private readonly destroy$ = new Subject(); + private readonly posIntValidation = [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]; twoFaFormGroup: FormGroup; twoFactorAuthProviderType = TwoFactorAuthProviderType; + twoFactorAuthProvidersData = twoFactorAuthProvidersData; @ViewChildren(MatExpansionPanel) expansionPanel: QueryList; @@ -123,8 +125,8 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI Validators.pattern(/^\d*$/) ]], verificationCodeCheckRateLimitEnable: [false], - verificationCodeCheckRateLimitNumber: ['3', [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]], - verificationCodeCheckRateLimitTime: ['900', [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]], + verificationCodeCheckRateLimitNumber: ['3', this.posIntValidation], + verificationCodeCheckRateLimitTime: ['900', this.posIntValidation], minVerificationCodeSendPeriod: ['30', [Validators.required, Validators.min(5), Validators.pattern(/^\d*$/)]], providers: this.fb.array([]) }); @@ -154,7 +156,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI providers: [] }); if (checkRateLimitNumber > 0) { - this.getByIndexPanel(3).open(); + this.getByIndexPanel(this.providersForm.length).open(); } Object.values(TwoFactorAuthProviderType).forEach((provider, index) => { const findIndex = allowProvidersConfig.indexOf(provider); @@ -182,18 +184,13 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI Validators.required, Validators.pattern(/\${code}/) ]]; - formControlConfig.verificationCodeLifetime = [{value: 120, disabled: true}, [ - Validators.required, - Validators.min(1), - Validators.pattern(/^\d*$/) - ]]; + formControlConfig.verificationCodeLifetime = [{value: 120, disabled: true}, this.posIntValidation]; break; case TwoFactorAuthProviderType.EMAIL: - formControlConfig.verificationCodeLifetime = [{value: 120, disabled: true}, [ - Validators.required, - Validators.min(1), - Validators.pattern(/^\d*$/) - ]]; + formControlConfig.verificationCodeLifetime = [{value: 120, disabled: true}, this.posIntValidation]; + break; + case TwoFactorAuthProviderType.BACKUP_CODE: + formControlConfig.codesQuantity = [{value: 10, disabled: true}, this.posIntValidation]; break; } const newProviders = this.fb.group(formControlConfig); diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.component.scss index 9b8b17b102..0b27d00983 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.component.scss @@ -30,6 +30,13 @@ &:not(:first-of-type) { margin: 0 0 8px; } + &.description { + margin: 0; + color: rgba(0, 0, 0, 0.54); + &:last-of-type { + margin-bottom: 24px; + } + } } .input-container { @@ -69,6 +76,23 @@ margin-bottom: 0; } + .backup-code { + max-width: 500px; + .container { + max-width: 500px; + margin: 40px 0 24px; + .code { + letter-spacing: 0.25px; + padding: 0 30px; + margin-bottom: 16px; + font-family: Roboto Mono, "Helvetica Neue", monospace; + &.even { + text-align: right; + } + } + } + } + & ::ng-deep { .mat-horizontal-stepper-header{ pointer-events: none !important; diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.map.ts b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.map.ts index 503fdd69a6..c5f319872e 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.map.ts +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/authentication-dialog.map.ts @@ -19,11 +19,15 @@ import { TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models import { TotpAuthDialogComponent } from './totp-auth-dialog.component'; import { SMSAuthDialogComponent } from './sms-auth-dialog.component'; import { EmailAuthDialogComponent } from './email-auth-dialog.component'; +import { + BackupCodeAuthDialogComponent +} from '@home/pages/security/authentication-dialog/backup-code-auth-dialog.component'; export const authenticationDialogMap = new Map>( [ [TwoFactorAuthProviderType.TOTP, TotpAuthDialogComponent], [TwoFactorAuthProviderType.SMS, SMSAuthDialogComponent], - [TwoFactorAuthProviderType.EMAIL, EmailAuthDialogComponent] + [TwoFactorAuthProviderType.EMAIL, EmailAuthDialogComponent], + [TwoFactorAuthProviderType.BACKUP_CODE, BackupCodeAuthDialogComponent] ] ); diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.html new file mode 100644 index 0000000000..61701c9358 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.html @@ -0,0 +1,46 @@ + + +

security.2fa.dialog.get-backup-code-title

+ + +
+ + +
+
+

security.2fa.dialog.backup-code-description

+
+
+ {{ code }} +
+
+

security.2fa.dialog.backup-code-warn

+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.ts new file mode 100644 index 0000000000..70c2888ac5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/backup-code-auth-dialog.component.ts @@ -0,0 +1,65 @@ +/// +/// Copyright © 2016-2022 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 } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; +import { MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder } from '@angular/forms'; +import { + AccountTwoFaSettings, + BackupCodeTwoFactorAuthAccountConfig, + TwoFactorAuthProviderType +} from '@shared/models/two-factor-auth.models'; +import { mergeMap, tap } from 'rxjs/operators'; +import { ImportExportService } from '@home/components/import-export/import-export.service'; + +@Component({ + selector: 'tb-backup-code-auth-dialog', + templateUrl: './backup-code-auth-dialog.component.html', + styleUrls: ['./authentication-dialog.component.scss'] +}) +export class BackupCodeAuthDialogComponent extends DialogComponent { + + private config: AccountTwoFaSettings; + backupCode: BackupCodeTwoFactorAuthAccountConfig; + + constructor(protected store: Store, + protected router: Router, + private twoFaService: TwoFactorAuthenticationService, + private importExportService: ImportExportService, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + 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; + }); + } + + closeDialog() { + this.dialogRef.close(this.config); + } + + downloadFile() { + this.importExportService.exportText(this.backupCode.codes, 'backup-codes'); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html index 39a46340d9..963374f49d 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html @@ -80,7 +80,7 @@ type="submit" color="primary" [disabled]="(isLoading$ | async) || emailVerificationForm.invalid"> - {{ 'security.2fa.dialog.activate' | translate }} + {{ 'action.activate' | translate }} @@ -94,8 +94,8 @@ diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts index 166ff799b7..385ea0e818 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts @@ -22,7 +22,11 @@ import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; -import { TwoFactorAuthAccountConfig, TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; +import { + AccountTwoFaSettings, + TwoFactorAuthAccountConfig, + TwoFactorAuthProviderType +} from '@shared/models/two-factor-auth.models'; import { MatStepper } from '@angular/material/stepper'; export interface EmailAuthDialogData { @@ -37,6 +41,7 @@ export interface EmailAuthDialogData { export class EmailAuthDialogComponent extends DialogComponent { private authAccountConfig: TwoFactorAuthAccountConfig; + private config: AccountTwoFaSettings; emailConfigForm: FormGroup; emailVerificationForm: FormGroup; @@ -84,9 +89,10 @@ export class EmailAuthDialogComponent extends DialogComponent { - this.stepper.next(); - }); + this.emailVerificationForm.get('verificationCode').value).subscribe((config) => { + this.config = config; + this.stepper.next(); + }); } else { this.showFormErrors(this.emailVerificationForm); } @@ -95,7 +101,7 @@ export class EmailAuthDialogComponent extends DialogComponent 1); + return this.dialogRef.close(this.config); } private showFormErrors(form: FormGroup) { diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.html index 49d8ee661f..50256ccc39 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.html @@ -82,7 +82,7 @@ type="submit" color="primary" [disabled]="(isLoading$ | async) || smsVerificationForm.invalid"> - {{ 'security.2fa.dialog.activate' | translate }} + {{ 'action.activate' | translate }} @@ -96,8 +96,8 @@ diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.ts index 030b67fd7a..7ab946b86f 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/sms-auth-dialog.component.ts @@ -22,7 +22,11 @@ import { Router } from '@angular/router'; import { MatDialogRef } from '@angular/material/dialog'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; -import { TwoFactorAuthAccountConfig, TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; +import { + AccountTwoFaSettings, + TwoFactorAuthAccountConfig, + TwoFactorAuthProviderType +} from '@shared/models/two-factor-auth.models'; import { phoneNumberPattern } from '@shared/models/settings.models'; import { MatStepper } from '@angular/material/stepper'; @@ -34,6 +38,7 @@ import { MatStepper } from '@angular/material/stepper'; export class SMSAuthDialogComponent extends DialogComponent { private authAccountConfig: TwoFactorAuthAccountConfig; + private config: AccountTwoFaSettings; phoneNumberPattern = phoneNumberPattern; @@ -82,9 +87,10 @@ export class SMSAuthDialogComponent extends DialogComponent { - this.stepper.next(); - }); + this.smsVerificationForm.get('verificationCode').value).subscribe((config) => { + this.config = config; + this.stepper.next(); + }); } else { this.showFormErrors(this.smsVerificationForm); } @@ -93,7 +99,7 @@ export class SMSAuthDialogComponent extends DialogComponent 1); + return this.dialogRef.close(this.config); } private showFormErrors(form: FormGroup) { diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.html index 1a788c8ba5..695d39860f 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.html @@ -84,8 +84,8 @@ diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts index 52aab77b57..8bda837df3 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/totp-auth-dialog.component.ts @@ -22,7 +22,11 @@ import { Router } from '@angular/router'; import { MatDialogRef } from '@angular/material/dialog'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; -import { TotpTwoFactorAuthAccountConfig, TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; +import { + AccountTwoFaSettings, + TotpTwoFactorAuthAccountConfig, + TwoFactorAuthProviderType +} from '@shared/models/two-factor-auth.models'; import { MatStepper } from '@angular/material/stepper'; @Component({ @@ -33,6 +37,7 @@ import { MatStepper } from '@angular/material/stepper'; export class TotpAuthDialogComponent extends DialogComponent { private authAccountConfig: TotpTwoFactorAuthAccountConfig; + private config: AccountTwoFaSettings; totpConfigForm: FormGroup; totpAuthURL: string; @@ -69,7 +74,8 @@ export class TotpAuthDialogComponent extends DialogComponent { + this.totpConfigForm.get('verificationCode').value).subscribe((config) => { + this.config = config; this.stepper.next(); }); } else { @@ -81,7 +87,7 @@ export class TotpAuthDialogComponent extends DialogComponent 1); + return this.dialogRef.close(this.config); } } diff --git a/ui-ngx/src/app/modules/home/pages/security/security.component.html b/ui-ngx/src/app/modules/home/pages/security/security.component.html index 6d5f57213f..8daa2f63ae 100644 --- a/ui-ngx/src/app/modules/home/pages/security/security.component.html +++ b/ui-ngx/src/app/modules/home/pages/security/security.component.html @@ -67,9 +67,15 @@ [checked]="useByDefault === provider" (click)="changeDefaultProvider($event, provider)" [disabled]="(isLoading$ | async) || activeSingleProvider" - *ngIf="twoFactorAuth.get(provider).value"> + *ngIf="twoFactorAuth.get(provider).value && provider !== twoFactorAuthProviderType.BACKUP_CODE"> security.2fa.main-2fa-method + diff --git a/ui-ngx/src/app/modules/home/pages/security/security.component.scss b/ui-ngx/src/app/modules/home/pages/security/security.component.scss index b95909c356..4688e46fae 100644 --- a/ui-ngx/src/app/modules/home/pages/security/security.component.scss +++ b/ui-ngx/src/app/modules/home/pages/security/security.component.scss @@ -29,13 +29,6 @@ @media #{$mat-gt-xl} { width: 45%; } - .title-container { - margin: 0; - } - .profile-email { - font-size: 16px; - font-weight: 400; - } .mat-subheader { line-height: 24px; color: rgba(0,0,0,0.54); @@ -52,22 +45,6 @@ opacity: 0.6; padding: 8px 0; } - .tb-home-dashboard { - tb-dashboard-autocomplete { - @media #{$mat-gt-sm} { - padding-right: 12px; - } - - @media #{$mat-lt-md} { - padding-bottom: 12px; - } - } - mat-checkbox { - @media #{$mat-gt-sm} { - margin-top: 16px; - } - } - } } .description { @@ -76,13 +53,6 @@ margin-right: 8px; } - .auth-title { - font-weight: 500; - line-height: 20px; - letter-spacing: 0.25px; - margin: 0; - } - .mat-divider-horizontal { left: 16px; right: 16px; @@ -111,5 +81,9 @@ .mat-checkbox { margin-top: 8px; } + + .mat-stroked-button { + margin-top: 8px; + } } } diff --git a/ui-ngx/src/app/modules/home/pages/security/security.component.ts b/ui-ngx/src/app/modules/home/pages/security/security.component.ts index 412bc305a3..e142dcbb1c 100644 --- a/ui-ngx/src/app/modules/home/pages/security/security.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/security.component.ts @@ -35,7 +35,8 @@ import { } from '@shared/models/two-factor-auth.models'; import { authenticationDialogMap } from '@home/pages/security/authentication-dialog/authentication-dialog.map'; import { takeUntil, tap } from 'rxjs/operators'; -import { Subject } from 'rxjs'; +import { Observable, of, Subject } from 'rxjs'; +import { isDefinedAndNotNull } from '@core/utils'; @Component({ selector: 'tb-security', @@ -45,11 +46,13 @@ import { Subject } from 'rxjs'; export class SecurityComponent extends PageComponent implements OnInit, OnDestroy { private readonly destroy$ = new Subject(); + private accountConfig: AccountTwoFaSettings; twoFactorAuth: FormGroup; user: User; allowTwoFactorProviders: TwoFactorAuthProviderType[] = []; providersData = twoFactorAuthProvidersData; + twoFactorAuthProviderType = TwoFactorAuthProviderType; useByDefault: TwoFactorAuthProviderType = null; activeSingleProvider = true; @@ -94,13 +97,19 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro this.twoFactorAuth = this.fb.group({ TOTP: [false], SMS: [false], - EMAIL: [false] + EMAIL: [false], + BACKUP_CODE: [{value: false, disabled: true}] }); this.twoFactorAuth.valueChanges.pipe( takeUntil(this.destroy$) ).subscribe((value: {TwoFactorAuthProviderType: boolean}) => { - const formActiveValue = Object.values(value).filter(item => item); + const formActiveValue = Object.keys(value).filter(item => value[item] && item !== TwoFactorAuthProviderType.BACKUP_CODE); this.activeSingleProvider = formActiveValue.length < 2; + if (formActiveValue.length) { + this.twoFactorAuth.get('BACKUP_CODE').enable({emitEvent: false}); + } else { + this.twoFactorAuth.get('BACKUP_CODE').disable({emitEvent: false}); + } }); } @@ -116,7 +125,8 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro } private processTwoFactorAuthConfig(setting: AccountTwoFaSettings) { - const configs = setting.configs; + this.accountConfig = setting; + const configs = this.accountConfig.configs; Object.values(TwoFactorAuthProviderType).forEach(provider => { if (configs[provider]) { this.twoFactorAuth.get(provider).setValue(true); @@ -171,20 +181,23 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro } }); } else { - const dialogData = provider === TwoFactorAuthProviderType.EMAIL ? {email: this.user.email} : {}; - this.dialog.open(authenticationDialogMap.get(provider), { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: dialogData - }).afterClosed().subscribe(res => { - if (res) { - this.twoFactorAuth.get(provider).setValue(res); - this.useByDefault = provider; - } - }); + this.createdNewAuthConfig(provider); } } + private createdNewAuthConfig(provider: TwoFactorAuthProviderType) { + const dialogData = provider === TwoFactorAuthProviderType.EMAIL ? {email: this.user.email} : {}; + this.dialog.open(authenticationDialogMap.get(provider), { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: dialogData + }).afterClosed().subscribe(res => { + if (isDefinedAndNotNull(res)) { + this.processTwoFactorAuthConfig(res); + } + }); + } + changeDefaultProvider(event: MouseEvent, provider: TwoFactorAuthProviderType) { event.stopPropagation(); event.preventDefault(); @@ -195,4 +208,27 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro .subscribe(data => this.processTwoFactorAuthConfig(data)); } } + + generateNewBackupCode() { + const codeLeft = this.accountConfig.configs[TwoFactorAuthProviderType.BACKUP_CODE].codesLeft; + let subscription: Observable; + if (codeLeft) { + subscription = this.dialogService.confirm( + 'Get new set of backup codes?', + `If you get new backup codes, ${codeLeft} remaining codes you have left will be unusable.`, + '', + 'Get new codes' + ); + } else { + subscription = of(true); + } + subscription.subscribe(res => { + if (res) { + this.twoFactorAuth.disable({emitEvent: false}); + this.twoFaService.deleteTwoFaAccountConfig(TwoFactorAuthProviderType.BACKUP_CODE) + .pipe(tap(() => this.twoFactorAuth.enable({emitEvent: false}))) + .subscribe(() => this.createdNewAuthConfig(TwoFactorAuthProviderType.BACKUP_CODE)); + } + }); + } } diff --git a/ui-ngx/src/app/modules/home/pages/security/security.module.ts b/ui-ngx/src/app/modules/home/pages/security/security.module.ts index 2df54747a7..5db12c2a68 100644 --- a/ui-ngx/src/app/modules/home/pages/security/security.module.ts +++ b/ui-ngx/src/app/modules/home/pages/security/security.module.ts @@ -22,13 +22,17 @@ import { SecurityRoutingModule } from './security-routing.module'; import { TotpAuthDialogComponent } from './authentication-dialog/totp-auth-dialog.component'; import { SMSAuthDialogComponent } from '@home/pages/security/authentication-dialog/sms-auth-dialog.component'; import { EmailAuthDialogComponent } from '@home/pages/security/authentication-dialog/email-auth-dialog.component'; +import { + BackupCodeAuthDialogComponent +} from '@home/pages/security/authentication-dialog/backup-code-auth-dialog.component'; @NgModule({ declarations: [ SecurityComponent, TotpAuthDialogComponent, SMSAuthDialogComponent, - EmailAuthDialogComponent + EmailAuthDialogComponent, + BackupCodeAuthDialogComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html index 78417564b3..cf9e3186ba 100644 --- a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html @@ -42,11 +42,11 @@ -