diff --git a/apps/web/src/app/admin-console/organizations/members/components/account-recovery/account-recovery-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/account-recovery/account-recovery-dialog.component.html index 368817a45b3..fa16ace4a0f 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/account-recovery/account-recovery-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/account-recovery/account-recovery-dialog.component.html @@ -7,8 +7,6 @@ @@ -17,8 +15,8 @@ type="button" bitButton buttonType="primary" - [disabled]="submitting$ | async" - (click)="handlePrimaryButtonClick()" + bitButton + [bitAction]="handlePrimaryButtonClick" > {{ "save" | i18n }} diff --git a/apps/web/src/app/admin-console/organizations/members/components/account-recovery/account-recovery-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/account-recovery/account-recovery-dialog.component.ts index 42cd257e1a2..4ff478857b2 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/account-recovery/account-recovery-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/account-recovery/account-recovery-dialog.component.ts @@ -1,19 +1,15 @@ import { CommonModule } from "@angular/common"; import { Component, Inject, ViewChild } from "@angular/core"; -import { BehaviorSubject, combineLatest, map, switchMap } from "rxjs"; +import { switchMap } from "rxjs"; -import { - InputPasswordComponent, - InputPasswordFlow, - PasswordInputResult, -} from "@bitwarden/auth/angular"; +import { InputPasswordComponent, InputPasswordFlow } from "@bitwarden/auth/angular"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { + AsyncActionsModule, ButtonModule, CalloutModule, DIALOG_DATA, @@ -76,22 +72,14 @@ export type AccountRecoveryDialogResultType = DialogModule, I18nPipe, InputPasswordComponent, + ButtonModule, + AsyncActionsModule, ], }) export class AccountRecoveryDialogComponent { @ViewChild(InputPasswordComponent) inputPasswordComponent: InputPasswordComponent | undefined = undefined; - private parentSubmittingBehaviorSubject = new BehaviorSubject(false); - parentSubmitting$ = this.parentSubmittingBehaviorSubject.asObservable(); - - private childSubmittingBehaviorSubject = new BehaviorSubject(false); - childSubmitting$ = this.childSubmittingBehaviorSubject.asObservable(); - - submitting$ = combineLatest([this.parentSubmitting$, this.childSubmitting$]).pipe( - map(([parentIsSubmitting, childIsSubmitting]) => parentIsSubmitting || childIsSubmitting), - ); - masterPasswordPolicyOptions$ = this.accountService.activeAccount$.pipe( getUserId, switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)), @@ -108,7 +96,6 @@ export class AccountRecoveryDialogComponent { private accountService: AccountService, private dialogRef: DialogRef, private i18nService: I18nService, - private logService: LogService, private policyService: PolicyService, private resetPasswordService: OrganizationUserResetPasswordService, private toastService: ToastService, @@ -119,37 +106,26 @@ export class AccountRecoveryDialogComponent { throw new Error("InputPasswordComponent is not initialized"); } - await this.inputPasswordComponent.submit(); - }; - - async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) { - this.parentSubmittingBehaviorSubject.next(true); - - try { - await this.resetPasswordService.resetMasterPassword( - passwordInputResult.newPassword, - this.dialogData.email, - this.dialogData.organizationUserId, - this.dialogData.organizationId, - ); - - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("resetPasswordSuccess"), - }); - } catch (e) { - this.logService.error(e); - } finally { - this.parentSubmittingBehaviorSubject.next(false); + const passwordInputResult = await this.inputPasswordComponent.submit(); + if (!passwordInputResult) { + return; } - this.dialogRef.close(AccountRecoveryDialogResultType.Ok); - } + await this.resetPasswordService.resetMasterPassword( + passwordInputResult.newPassword, + this.dialogData.email, + this.dialogData.organizationUserId, + this.dialogData.organizationId, + ); - protected handleIsSubmittingChange(isSubmitting: boolean) { - this.childSubmittingBehaviorSubject.next(isSubmitting); - } + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("resetPasswordSuccess"), + }); + + this.dialogRef.close(AccountRecoveryDialogResultType.Ok); + }; /** * Strongly typed helper to open an `AccountRecoveryDialogComponent` diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts index bd3a015055b..2ae5590ad3e 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -13,7 +13,6 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { HashPurpose } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { UserId } from "@bitwarden/common/types/guid"; @@ -121,9 +120,7 @@ export class InputPasswordComponent implements OnInit { | PasswordStrengthV2Component | undefined = undefined; - @Output() onPasswordFormSubmit = new EventEmitter(); @Output() onSecondaryButtonClick = new EventEmitter(); - @Output() isSubmitting = new EventEmitter(); @Input({ required: true }) flow!: InputPasswordFlow; @@ -192,7 +189,6 @@ export class InputPasswordComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private policyService: PolicyService, private toastService: ToastService, - private validationService: ValidationService, ) {} ngOnInit(): void { @@ -262,138 +258,129 @@ export class InputPasswordComponent implements OnInit { } } - submit = async () => { - try { - this.isSubmitting.emit(true); + submit = async (): Promise => { + this.verifyFlow(); - this.verifyFlow(); + this.formGroup.markAllAsTouched(); - this.formGroup.markAllAsTouched(); + if (this.formGroup.invalid) { + this.showErrorSummary = true; + return; + } - if (this.formGroup.invalid) { - this.showErrorSummary = true; - return; + const currentPassword = this.formGroup.controls.currentPassword?.value ?? ""; + const newPassword = this.formGroup.controls.newPassword.value; + const newPasswordHint = this.formGroup.controls.newPasswordHint?.value ?? ""; + const checkForBreaches = this.formGroup.controls.checkForBreaches?.value ?? true; + + if (this.flow === InputPasswordFlow.ChangePasswordDelegation) { + return this.handleChangePasswordDelegationFlow(newPassword); + } + + if (!this.email) { + throw new Error("Email is required to create master key."); + } + + // 1. Determine kdfConfig + if (this.flow === InputPasswordFlow.SetInitialPasswordAccountRegistration) { + this.kdfConfig = DEFAULT_KDF_CONFIG; + } else { + if (!this.userId) { + throw new Error("userId not passed down"); } + this.kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(this.userId)); + } - const currentPassword = this.formGroup.controls.currentPassword?.value ?? ""; - const newPassword = this.formGroup.controls.newPassword.value; - const newPasswordHint = this.formGroup.controls.newPasswordHint?.value ?? ""; - const checkForBreaches = this.formGroup.controls.checkForBreaches?.value ?? true; + if (this.kdfConfig == null) { + throw new Error("KdfConfig is required to create master key."); + } - if (this.flow === InputPasswordFlow.ChangePasswordDelegation) { - await this.handleChangePasswordDelegationFlow(newPassword); - return; - } - - if (!this.email) { - throw new Error("Email is required to create master key."); - } - - // 1. Determine kdfConfig - if (this.flow === InputPasswordFlow.SetInitialPasswordAccountRegistration) { - this.kdfConfig = DEFAULT_KDF_CONFIG; - } else { - if (!this.userId) { - throw new Error("userId not passed down"); - } - this.kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(this.userId)); - } - - if (this.kdfConfig == null) { - throw new Error("KdfConfig is required to create master key."); - } - - // 2. Verify current password is correct (if necessary) - if ( - this.flow === InputPasswordFlow.ChangePassword || - this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation - ) { - const currentPasswordVerified = await this.verifyCurrentPassword( - currentPassword, - this.kdfConfig, - ); - if (!currentPasswordVerified) { - return; - } - } - - // 3. Verify new password - const newPasswordVerified = await this.verifyNewPassword( - newPassword, - this.passwordStrengthScore, - checkForBreaches, + // 2. Verify current password is correct (if necessary) + if ( + this.flow === InputPasswordFlow.ChangePassword || + this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation + ) { + const currentPasswordVerified = await this.verifyCurrentPassword( + currentPassword, + this.kdfConfig, ); - if (!newPasswordVerified) { + if (!currentPasswordVerified) { return; } + } - // 4. Create cryptographic keys and build a PasswordInputResult object - const newMasterKey = await this.keyService.makeMasterKey( - newPassword, + // 3. Verify new password + const newPasswordVerified = await this.verifyNewPassword( + newPassword, + this.passwordStrengthScore, + checkForBreaches, + ); + if (!newPasswordVerified) { + return; + } + + // 4. Create cryptographic keys and build a PasswordInputResult object + const newMasterKey = await this.keyService.makeMasterKey( + newPassword, + this.email, + this.kdfConfig, + ); + + const newServerMasterKeyHash = await this.keyService.hashMasterKey( + newPassword, + newMasterKey, + HashPurpose.ServerAuthorization, + ); + + const newLocalMasterKeyHash = await this.keyService.hashMasterKey( + newPassword, + newMasterKey, + HashPurpose.LocalAuthorization, + ); + + const passwordInputResult: PasswordInputResult = { + newPassword, + newMasterKey, + newServerMasterKeyHash, + newLocalMasterKeyHash, + newPasswordHint, + kdfConfig: this.kdfConfig, + }; + + if ( + this.flow === InputPasswordFlow.ChangePassword || + this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation + ) { + const currentMasterKey = await this.keyService.makeMasterKey( + currentPassword, this.email, this.kdfConfig, ); - const newServerMasterKeyHash = await this.keyService.hashMasterKey( - newPassword, - newMasterKey, + const currentServerMasterKeyHash = await this.keyService.hashMasterKey( + currentPassword, + currentMasterKey, HashPurpose.ServerAuthorization, ); - const newLocalMasterKeyHash = await this.keyService.hashMasterKey( - newPassword, - newMasterKey, + const currentLocalMasterKeyHash = await this.keyService.hashMasterKey( + currentPassword, + currentMasterKey, HashPurpose.LocalAuthorization, ); - const passwordInputResult: PasswordInputResult = { - newPassword, - newMasterKey, - newServerMasterKeyHash, - newLocalMasterKeyHash, - newPasswordHint, - kdfConfig: this.kdfConfig, - }; - - if ( - this.flow === InputPasswordFlow.ChangePassword || - this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation - ) { - const currentMasterKey = await this.keyService.makeMasterKey( - currentPassword, - this.email, - this.kdfConfig, - ); - - const currentServerMasterKeyHash = await this.keyService.hashMasterKey( - currentPassword, - currentMasterKey, - HashPurpose.ServerAuthorization, - ); - - const currentLocalMasterKeyHash = await this.keyService.hashMasterKey( - currentPassword, - currentMasterKey, - HashPurpose.LocalAuthorization, - ); - - passwordInputResult.currentPassword = currentPassword; - passwordInputResult.currentMasterKey = currentMasterKey; - passwordInputResult.currentServerMasterKeyHash = currentServerMasterKeyHash; - passwordInputResult.currentLocalMasterKeyHash = currentLocalMasterKeyHash; - } - - if (this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) { - passwordInputResult.rotateUserKey = this.formGroup.controls.rotateUserKey?.value; - } - - // 5. Emit cryptographic keys and other password related properties - this.onPasswordFormSubmit.emit(passwordInputResult); - } catch (e) { - this.validationService.showError(e); - } finally { - this.isSubmitting.emit(false); + passwordInputResult.currentPassword = currentPassword; + passwordInputResult.currentMasterKey = currentMasterKey; + passwordInputResult.currentServerMasterKeyHash = currentServerMasterKeyHash; + passwordInputResult.currentLocalMasterKeyHash = currentLocalMasterKeyHash; } + + if (this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) { + passwordInputResult.rotateUserKey = this.formGroup.controls.rotateUserKey?.value; + } + + // 5. Emit cryptographic keys and other password related properties + return passwordInputResult; }; /** @@ -442,7 +429,9 @@ export class InputPasswordComponent implements OnInit { } } - private async handleChangePasswordDelegationFlow(newPassword: string) { + private async handleChangePasswordDelegationFlow( + newPassword: string, + ): Promise { const newPasswordVerified = await this.verifyNewPassword( newPassword, this.passwordStrengthScore, @@ -452,11 +441,9 @@ export class InputPasswordComponent implements OnInit { return; } - const passwordInputResult: PasswordInputResult = { + return { newPassword, }; - - this.onPasswordFormSubmit.emit(passwordInputResult); } /**