mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +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:
@@ -86,7 +86,7 @@ export abstract class BaseMembersComponent<UserView extends UserViewTypes> {
|
|||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
protected keyService: KeyService,
|
protected keyService: KeyService,
|
||||||
protected validationService: ValidationService,
|
protected validationService: ValidationService,
|
||||||
private logService: LogService,
|
protected logService: LogService,
|
||||||
protected userNamePipe: UserNamePipe,
|
protected userNamePipe: UserNamePipe,
|
||||||
protected dialogService: DialogService,
|
protected dialogService: DialogService,
|
||||||
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,
|
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./account-recovery-dialog.component";
|
||||||
@@ -13,6 +13,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
import {
|
import {
|
||||||
DIALOG_DATA,
|
DIALOG_DATA,
|
||||||
DialogConfig,
|
DialogConfig,
|
||||||
@@ -47,7 +48,7 @@ export type ResetPasswordDialogData = {
|
|||||||
/**
|
/**
|
||||||
* The organization's `organizationId`
|
* The organization's `organizationId`
|
||||||
*/
|
*/
|
||||||
organizationId: string;
|
organizationId: OrganizationId;
|
||||||
};
|
};
|
||||||
|
|
||||||
// FIXME: update to use a const object instead of a typescript enum
|
// FIXME: update to use a const object instead of a typescript enum
|
||||||
@@ -56,16 +57,18 @@ export enum ResetPasswordDialogResult {
|
|||||||
Ok = "ok",
|
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({
|
@Component({
|
||||||
selector: "app-reset-password",
|
selector: "app-reset-password",
|
||||||
templateUrl: "reset-password.component.html",
|
templateUrl: "reset-password.component.html",
|
||||||
standalone: false,
|
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 {
|
export class ResetPasswordComponent implements OnInit, OnDestroy {
|
||||||
formGroup = this.formBuilder.group({
|
formGroup = this.formBuilder.group({
|
||||||
newPassword: ["", Validators.required],
|
newPassword: ["", Validators.required],
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components";
|
import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
@@ -66,6 +67,10 @@ import { GroupApiService } from "../core";
|
|||||||
import { OrganizationUserView } from "../core/views/organization-user.view";
|
import { OrganizationUserView } from "../core/views/organization-user.view";
|
||||||
import { openEntityEventsDialog } from "../manage/entity-events.component";
|
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 { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.component";
|
||||||
import { BulkDeleteDialogComponent } from "./components/bulk/bulk-delete-dialog.component";
|
import { BulkDeleteDialogComponent } from "./components/bulk/bulk-delete-dialog.component";
|
||||||
import { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-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) {
|
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, {
|
const dialogRef = ResetPasswordComponent.open(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
name: this.userNamePipe.transform(user),
|
name: this.userNamePipe.transform(user),
|
||||||
email: user != null ? user.email : null,
|
email: user != null ? user.email : null,
|
||||||
organizationId: this.organization.id,
|
organizationId: this.organization.id as OrganizationId,
|
||||||
id: user != null ? user.id : null,
|
id: user != null ? user.id : null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
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 { UserKey } from "@bitwarden/common/types/key";
|
||||||
import {
|
import {
|
||||||
Argon2KdfConfig,
|
Argon2KdfConfig,
|
||||||
@@ -96,7 +96,7 @@ export class OrganizationUserResetPasswordService
|
|||||||
newMasterPassword: string,
|
newMasterPassword: string,
|
||||||
email: string,
|
email: string,
|
||||||
orgUserId: string,
|
orgUserId: string,
|
||||||
orgId: string,
|
orgId: OrganizationId,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const response = await this.organizationUserApiService.getOrganizationUserResetPasswordDetails(
|
const response = await this.organizationUserApiService.getOrganizationUserResetPasswordDetails(
|
||||||
orgId,
|
orgId,
|
||||||
|
|||||||
@@ -2225,6 +2225,9 @@
|
|||||||
"disable": {
|
"disable": {
|
||||||
"message": "Turn off"
|
"message": "Turn off"
|
||||||
},
|
},
|
||||||
|
"orgUserDetailsNotFound": {
|
||||||
|
"message": "Member details not found."
|
||||||
|
},
|
||||||
"revokeAccess": {
|
"revokeAccess": {
|
||||||
"message": "Revoke access"
|
"message": "Revoke access"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ export class InputPasswordComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
submit = async () => {
|
submit = async (): Promise<PasswordInputResult | undefined> => {
|
||||||
try {
|
try {
|
||||||
this.isSubmitting.emit(true);
|
this.isSubmitting.emit(true);
|
||||||
|
|
||||||
@@ -280,8 +280,7 @@ export class InputPasswordComponent implements OnInit {
|
|||||||
const checkForBreaches = this.formGroup.controls.checkForBreaches?.value ?? true;
|
const checkForBreaches = this.formGroup.controls.checkForBreaches?.value ?? true;
|
||||||
|
|
||||||
if (this.flow === InputPasswordFlow.ChangePasswordDelegation) {
|
if (this.flow === InputPasswordFlow.ChangePasswordDelegation) {
|
||||||
await this.handleChangePasswordDelegationFlow(newPassword);
|
return await this.handleChangePasswordDelegationFlow(newPassword);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.email) {
|
if (!this.email) {
|
||||||
@@ -388,6 +387,7 @@ export class InputPasswordComponent implements OnInit {
|
|||||||
|
|
||||||
// 5. Emit cryptographic keys and other password related properties
|
// 5. Emit cryptographic keys and other password related properties
|
||||||
this.onPasswordFormSubmit.emit(passwordInputResult);
|
this.onPasswordFormSubmit.emit(passwordInputResult);
|
||||||
|
return passwordInputResult;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.validationService.showError(e);
|
this.validationService.showError(e);
|
||||||
} finally {
|
} 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(
|
const newPasswordVerified = await this.verifyNewPassword(
|
||||||
newPassword,
|
newPassword,
|
||||||
this.passwordStrengthScore,
|
this.passwordStrengthScore,
|
||||||
@@ -456,6 +458,7 @@ export class InputPasswordComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.onPasswordFormSubmit.emit(passwordInputResult);
|
this.onPasswordFormSubmit.emit(passwordInputResult);
|
||||||
|
return passwordInputResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user