diff --git a/apps/web/src/app/admin-console/common/base-members.component.ts b/apps/web/src/app/admin-console/common/base-members.component.ts index 488af7ee518..624615edd6a 100644 --- a/apps/web/src/app/admin-console/common/base-members.component.ts +++ b/apps/web/src/app/admin-console/common/base-members.component.ts @@ -86,7 +86,7 @@ export abstract class BaseMembersComponent { protected i18nService: I18nService, protected keyService: KeyService, protected validationService: ValidationService, - private logService: LogService, + protected logService: LogService, protected userNamePipe: UserNamePipe, protected dialogService: DialogService, protected organizationManagementPreferencesService: OrganizationManagementPreferencesService, 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 new file mode 100644 index 00000000000..7fa063364e3 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/account-recovery/account-recovery-dialog.component.html @@ -0,0 +1,21 @@ + + + {{ "resetPasswordLoggedOutWarning" | i18n: loggedOutWarningName }} + + + + + + + + + + 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 new file mode 100644 index 00000000000..3240b8d707a --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/account-recovery/account-recovery-dialog.component.ts @@ -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, + 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 + >, + ) => { + return dialogService.open( + AccountRecoveryDialogComponent, + dialogConfig, + ); + }; +} diff --git a/apps/web/src/app/admin-console/organizations/members/components/account-recovery/index.ts b/apps/web/src/app/admin-console/organizations/members/components/account-recovery/index.ts new file mode 100644 index 00000000000..e15fb7b40ef --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/account-recovery/index.ts @@ -0,0 +1 @@ +export * from "./account-recovery-dialog.component"; diff --git a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts index 80f0745f6d5..961d5482d8a 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts @@ -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], diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 49c57f5e5a6..94f268cde21 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -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 } 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, }, }); diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts index ecf4d26eb52..d54e12c0ee7 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts @@ -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 { const response = await this.organizationUserApiService.getOrganizationUserResetPasswordDetails( orgId, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 43c3cec090e..79197f1eb06 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -2225,6 +2225,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, 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 79157cae901..2d469e89fcd 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -261,7 +261,7 @@ export class InputPasswordComponent implements OnInit { } } - submit = async () => { + submit = async (): Promise => { 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 { const newPasswordVerified = await this.verifyNewPassword( newPassword, this.passwordStrengthScore, @@ -456,6 +458,7 @@ export class InputPasswordComponent implements OnInit { }; this.onPasswordFormSubmit.emit(passwordInputResult); + return passwordInputResult; } /**