diff --git a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.html b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.html index 1d39bcd0e9..b5f50841e1 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.html @@ -1,100 +1,67 @@ - + bitIconButton="bwi-generate" + bitSuffix + [appA11yTitle]="'generatePassword' | i18n" + (click)="generatePassword()" + > + + + + + + + + + + + + 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 ce605a6f5a..46d0c55094 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 @@ -1,16 +1,9 @@ -import { - Component, - EventEmitter, - Input, - OnDestroy, - OnInit, - Output, - ViewChild, -} from "@angular/core"; +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; import { Subject, takeUntil } from "rxjs"; -import zxcvbn from "zxcvbn"; -import { PasswordStrengthComponent } from "@bitwarden/angular/tools/password-strength/password-strength.component"; +import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -22,27 +15,60 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac 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 ResetPasswordDialogData = { + /** + * The organization user's full name + */ + name: string; + + /** + * The organization user's email address + */ + email: string; + + /** + * The `organizationUserId` for the user + */ + id: string; + + /** + * The organization's `organizationId` + */ + organizationId: string; +}; + +export enum ResetPasswordDialogResult { + Ok = "ok", +} + @Component({ selector: "app-reset-password", templateUrl: "reset-password.component.html", }) +/** + * 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 { - @Input() name: string; - @Input() email: string; - @Input() id: string; - @Input() organizationId: string; - @Output() passwordReset = new EventEmitter(); - @ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent; + formGroup = this.formBuilder.group({ + newPassword: ["", Validators.required], + }); + + @ViewChild(PasswordStrengthV2Component) passwordStrengthComponent: PasswordStrengthV2Component; enforcedPolicyOptions: MasterPasswordPolicyOptions; - newPassword: string = null; showPassword = false; - passwordStrengthResult: zxcvbn.ZXCVBNResult; - formPromise: Promise; + passwordStrengthScore: number; private destroy$ = new Subject(); constructor( + @Inject(DIALOG_DATA) protected data: ResetPasswordDialogData, private resetPasswordService: OrganizationUserResetPasswordService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, @@ -51,6 +77,8 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { private logService: LogService, private dialogService: DialogService, private toastService: ToastService, + private formBuilder: FormBuilder, + private dialogRef: DialogRef, ) {} async ngOnInit() { @@ -69,13 +97,15 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { } get loggedOutWarningName() { - return this.name != null ? this.name : this.i18nService.t("thisUser"); + return this.data.name != null ? this.data.name : this.i18nService.t("thisUser"); } async generatePassword() { const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {}; - this.newPassword = await this.passwordGenerationService.generatePassword(options); - this.passwordStrengthComponent.updatePasswordStrength(this.newPassword); + this.formGroup.patchValue({ + newPassword: await this.passwordGenerationService.generatePassword(options), + }); + this.passwordStrengthComponent.updatePasswordStrength(this.formGroup.value.newPassword); } togglePassword() { @@ -83,7 +113,8 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { document.getElementById("newPassword").focus(); } - copy(value: string) { + copy() { + const value = this.formGroup.value.newPassword; if (value == null) { return; } @@ -96,9 +127,9 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { }); } - async submit() { + submit = async () => { // Validation - if (this.newPassword == null || this.newPassword === "") { + if (this.formGroup.value.newPassword == null || this.formGroup.value.newPassword === "") { this.toastService.showToast({ variant: "error", title: this.i18nService.t("errorOccurred"), @@ -107,7 +138,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { return false; } - if (this.newPassword.length < Utils.minimumPasswordLength) { + if (this.formGroup.value.newPassword.length < Utils.minimumPasswordLength) { this.toastService.showToast({ variant: "error", title: this.i18nService.t("errorOccurred"), @@ -119,8 +150,8 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { if ( this.enforcedPolicyOptions != null && !this.policyService.evaluateMasterPassword( - this.passwordStrengthResult.score, - this.newPassword, + this.passwordStrengthScore, + this.formGroup.value.newPassword, this.enforcedPolicyOptions, ) ) { @@ -132,7 +163,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { return; } - if (this.passwordStrengthResult.score < 3) { + if (this.passwordStrengthScore < 3) { const result = await this.dialogService.openSimpleDialog({ title: { key: "weakMasterPassword" }, content: { key: "weakMasterPasswordDesc" }, @@ -145,26 +176,29 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { } try { - this.formPromise = this.resetPasswordService.resetMasterPassword( - this.newPassword, - this.email, - this.id, - this.organizationId, + await this.resetPasswordService.resetMasterPassword( + this.formGroup.value.newPassword, + this.data.email, + this.data.id, + this.data.organizationId, ); - await this.formPromise; this.toastService.showToast({ variant: "success", title: null, message: this.i18nService.t("resetPasswordSuccess"), }); - this.passwordReset.emit(); } catch (e) { this.logService.error(e); } - this.formPromise = null; + + this.dialogRef.close(ResetPasswordDialogResult.Ok); + }; + + getStrengthScore(result: number) { + this.passwordStrengthScore = result; } - getStrengthResult(result: zxcvbn.ZXCVBNResult) { - this.passwordStrengthResult = result; - } + static open = (dialogService: DialogService, input: DialogConfig) => { + return dialogService.open(ResetPasswordComponent, input); + }; } 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 698c260632..3cc73c84a9 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 @@ -70,7 +70,10 @@ import { MemberDialogTab, openUserAddEditDialog, } from "./components/member-dialog"; -import { ResetPasswordComponent } from "./components/reset-password.component"; +import { + ResetPasswordComponent, + ResetPasswordDialogResult, +} from "./components/reset-password.component"; class MembersTableDataSource extends PeopleTableDataSource { protected statusType = OrganizationUserStatusType; @@ -663,24 +666,19 @@ export class MembersComponent extends BaseMembersComponent } async resetPassword(user: OrganizationUserView) { - const [modal] = await this.modalService.openViewRef( - ResetPasswordComponent, - this.resetPasswordModalRef, - (comp) => { - comp.name = this.userNamePipe.transform(user); - comp.email = user != null ? user.email : null; - comp.organizationId = this.organization.id; - comp.id = user != null ? user.id : null; - - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - comp.passwordReset.subscribe(() => { - modal.close(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.load(); - }); + const dialogRef = ResetPasswordComponent.open(this.dialogService, { + data: { + name: this.userNamePipe.transform(user), + email: user != null ? user.email : null, + organizationId: this.organization.id, + id: user != null ? user.id : null, }, - ); + }); + + const result = await lastValueFrom(dialogRef.closed); + if (result === ResetPasswordDialogResult.Ok) { + await this.load(); + } } protected async removeUserConfirmationDialog(user: OrganizationUserView) { diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts index 6e8870a675..d849b1f1f3 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts @@ -1,6 +1,7 @@ import { ScrollingModule } from "@angular/cdk/scrolling"; import { NgModule } from "@angular/core"; +import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component"; import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; import { LooseComponentsModule } from "../../../shared"; @@ -24,6 +25,7 @@ import { MembersComponent } from "./members.component"; UserDialogModule, PasswordCalloutComponent, ScrollingModule, + PasswordStrengthV2Component, ], declarations: [ BulkConfirmComponent,