1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 13:23:34 +00:00

refactor(account-recovery): [PM-18721][PM-21272] Integrate InputPasswordComponent in AccountRecoveryDialogComponent (#14662)

Integrates the `InputPasswordComponent` within the new `AccountRecoveryDialogComponent`.

Feature flag: `PM16117_ChangeExistingPasswordRefactor`
This commit is contained in:
rr-bw
2025-06-25 07:29:22 -07:00
committed by GitHub
parent 1b441e8a0f
commit 1df54c71be
9 changed files with 229 additions and 14 deletions

View File

@@ -86,7 +86,7 @@ export abstract class BaseMembersComponent<UserView extends UserViewTypes> {
protected i18nService: I18nService,
protected keyService: KeyService,
protected validationService: ValidationService,
private logService: LogService,
protected logService: LogService,
protected userNamePipe: UserNamePipe,
protected dialogService: DialogService,
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,

View File

@@ -0,0 +1,21 @@
<bit-dialog [title]="'recoverAccount' | i18n" [subtitle]="dialogData.name">
<ng-container bitDialogContent>
<bit-callout type="warning"
>{{ "resetPasswordLoggedOutWarning" | i18n: loggedOutWarningName }}
</bit-callout>
<auth-input-password
[flow]="inputPasswordFlow"
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions$ | async"
></auth-input-password>
</ng-container>
<ng-container bitDialogFooter>
<button type="button" bitButton buttonType="primary" [bitAction]="handlePrimaryButtonClick">
{{ "save" | i18n }}
</button>
<button type="button" bitButton buttonType="secondary" bitDialogClose>
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>

View File

@@ -0,0 +1,146 @@
import { CommonModule } from "@angular/common";
import { Component, Inject, ViewChild } from "@angular/core";
import { switchMap } from "rxjs";
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 { OrganizationId } from "@bitwarden/common/types/guid";
import {
AsyncActionsModule,
ButtonModule,
CalloutModule,
DIALOG_DATA,
DialogConfig,
DialogModule,
DialogRef,
DialogService,
ToastService,
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { OrganizationUserResetPasswordService } from "../../services/organization-user-reset-password/organization-user-reset-password.service";
/**
* Encapsulates a few key data inputs needed to initiate an account recovery
* process for the organization user in question.
*/
export type AccountRecoveryDialogData = {
/**
* The organization user's full name
*/
name: string;
/**
* The organization user's email address
*/
email: string;
/**
* The `organizationUserId` for the user
*/
organizationUserId: string;
/**
* The organization's `organizationId`
*/
organizationId: OrganizationId;
};
export const AccountRecoveryDialogResultType = {
Ok: "ok",
} as const;
export type AccountRecoveryDialogResultType =
(typeof AccountRecoveryDialogResultType)[keyof typeof AccountRecoveryDialogResultType];
/**
* Used in a dialog for initiating the account recovery process against a
* given organization user. An admin will access this form when they want to
* reset a user's password and log them out of sessions.
*/
@Component({
standalone: true,
selector: "app-account-recovery-dialog",
templateUrl: "account-recovery-dialog.component.html",
imports: [
AsyncActionsModule,
ButtonModule,
CalloutModule,
CommonModule,
DialogModule,
I18nPipe,
InputPasswordComponent,
],
})
export class AccountRecoveryDialogComponent {
@ViewChild(InputPasswordComponent)
inputPasswordComponent: InputPasswordComponent | undefined = undefined;
masterPasswordPolicyOptions$ = this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
);
inputPasswordFlow = InputPasswordFlow.ChangePasswordDelegation;
get loggedOutWarningName() {
return this.dialogData.name != null ? this.dialogData.name : this.i18nService.t("thisUser");
}
constructor(
@Inject(DIALOG_DATA) protected dialogData: AccountRecoveryDialogData,
private accountService: AccountService,
private dialogRef: DialogRef<AccountRecoveryDialogResultType>,
private i18nService: I18nService,
private policyService: PolicyService,
private resetPasswordService: OrganizationUserResetPasswordService,
private toastService: ToastService,
) {}
handlePrimaryButtonClick = async () => {
if (!this.inputPasswordComponent) {
throw new Error("InputPasswordComponent is not initialized");
}
const passwordInputResult = await this.inputPasswordComponent.submit();
if (!passwordInputResult) {
return;
}
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"),
});
this.dialogRef.close(AccountRecoveryDialogResultType.Ok);
};
/**
* Strongly typed helper to open an `AccountRecoveryDialogComponent`
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param dialogConfig Configuration for the dialog
*/
static open = (
dialogService: DialogService,
dialogConfig: DialogConfig<
AccountRecoveryDialogData,
DialogRef<AccountRecoveryDialogResultType, unknown>
>,
) => {
return dialogService.open<AccountRecoveryDialogResultType, AccountRecoveryDialogData>(
AccountRecoveryDialogComponent,
dialogConfig,
);
};
}

View File

@@ -0,0 +1 @@
export * from "./account-recovery-dialog.component";

View File

@@ -13,6 +13,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { OrganizationId } from "@bitwarden/common/types/guid";
import {
DIALOG_DATA,
DialogConfig,
@@ -47,7 +48,7 @@ export type ResetPasswordDialogData = {
/**
* The organization's `organizationId`
*/
organizationId: string;
organizationId: OrganizationId;
};
// FIXME: update to use a const object instead of a typescript enum
@@ -56,16 +57,18 @@ export enum ResetPasswordDialogResult {
Ok = "ok",
}
/**
* Used in a dialog for initiating the account recovery process against a
* given organization user. An admin will access this form when they want to
* reset a user's password and log them out of sessions.
*
* @deprecated Use the `AccountRecoveryDialogComponent` instead.
*/
@Component({
selector: "app-reset-password",
templateUrl: "reset-password.component.html",
standalone: false,
})
/**
* Used in a dialog for initiating the account recovery process against a
* given organization user. An admin will access this form when they want to
* reset a user's password and log them out of sessions.
*/
export class ResetPasswordComponent implements OnInit, OnDestroy {
formGroup = this.formBuilder.group({
newPassword: ["", Validators.required],

View File

@@ -52,6 +52,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
@@ -66,6 +67,10 @@ import { GroupApiService } from "../core";
import { OrganizationUserView } from "../core/views/organization-user.view";
import { openEntityEventsDialog } from "../manage/entity-events.component";
import {
AccountRecoveryDialogComponent,
AccountRecoveryDialogResultType,
} from "./components/account-recovery/account-recovery-dialog.component";
import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.component";
import { BulkDeleteDialogComponent } from "./components/bulk/bulk-delete-dialog.component";
import { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-dialog.component";
@@ -749,11 +754,44 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
}
async resetPassword(user: OrganizationUserView) {
const changePasswordRefactorFlag = await this.configService.getFeatureFlag(
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
);
if (changePasswordRefactorFlag) {
if (!user || !user.email || !user.id) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("orgUserDetailsNotFound"),
});
this.logService.error("Org user details not found when attempting account recovery");
return;
}
const dialogRef = AccountRecoveryDialogComponent.open(this.dialogService, {
data: {
name: this.userNamePipe.transform(user),
email: user.email,
organizationId: this.organization.id as OrganizationId,
organizationUserId: user.id,
},
});
const result = await lastValueFrom(dialogRef.closed);
if (result === AccountRecoveryDialogResultType.Ok) {
await this.load();
}
return;
}
const dialogRef = ResetPasswordComponent.open(this.dialogService, {
data: {
name: this.userNamePipe.transform(user),
email: user != null ? user.email : null,
organizationId: this.organization.id,
organizationId: this.organization.id as OrganizationId,
id: user != null ? user.id : null,
},
});

View File

@@ -14,7 +14,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { UserId } from "@bitwarden/common/types/guid";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
import {
Argon2KdfConfig,
@@ -96,7 +96,7 @@ export class OrganizationUserResetPasswordService
newMasterPassword: string,
email: string,
orgUserId: string,
orgId: string,
orgId: OrganizationId,
): Promise<void> {
const response = await this.organizationUserApiService.getOrganizationUserResetPasswordDetails(
orgId,

View File

@@ -2225,6 +2225,9 @@
"disable": {
"message": "Turn off"
},
"orgUserDetailsNotFound": {
"message": "Member details not found."
},
"revokeAccess": {
"message": "Revoke access"
},

View File

@@ -261,7 +261,7 @@ export class InputPasswordComponent implements OnInit {
}
}
submit = async () => {
submit = async (): Promise<PasswordInputResult | undefined> => {
try {
this.isSubmitting.emit(true);
@@ -280,8 +280,7 @@ export class InputPasswordComponent implements OnInit {
const checkForBreaches = this.formGroup.controls.checkForBreaches?.value ?? true;
if (this.flow === InputPasswordFlow.ChangePasswordDelegation) {
await this.handleChangePasswordDelegationFlow(newPassword);
return;
return await this.handleChangePasswordDelegationFlow(newPassword);
}
if (!this.email) {
@@ -388,6 +387,7 @@ export class InputPasswordComponent implements OnInit {
// 5. Emit cryptographic keys and other password related properties
this.onPasswordFormSubmit.emit(passwordInputResult);
return passwordInputResult;
} catch (e) {
this.validationService.showError(e);
} finally {
@@ -441,7 +441,9 @@ export class InputPasswordComponent implements OnInit {
}
}
private async handleChangePasswordDelegationFlow(newPassword: string) {
private async handleChangePasswordDelegationFlow(
newPassword: string,
): Promise<PasswordInputResult | undefined> {
const newPasswordVerified = await this.verifyNewPassword(
newPassword,
this.passwordStrengthScore,
@@ -456,6 +458,7 @@ export class InputPasswordComponent implements OnInit {
};
this.onPasswordFormSubmit.emit(passwordInputResult);
return passwordInputResult;
}
/**