1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-10-14 06:48:18 +02:00

Migrate ResetPasswordComponent off of bootstrap (#11390)

* Migrate `ResetPasswordComponent` off of bootstrap

* Remove redundant `[appApiAction]` directive usage

* Replace `app-callout` with `bit-callout`

* Remove `formPromise` from compononent. It is unused.

* Implement new password strength component
This commit is contained in:
Addison Beck 2024-10-09 12:30:08 -04:00 committed by GitHub
parent 7fc987d806
commit 35ff7d49b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 158 additions and 157 deletions

View File

@ -1,100 +1,67 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="resetPasswordTitle"> <form [formGroup]="formGroup" [bitSubmit]="submit">
<div class="modal-dialog" role="document"> <bit-dialog [title]="'recoverAccount' | i18n" [subtitle]="data.name">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise"> <ng-container bitDialogContent>
<div class="modal-header"> <bit-callout type="warning"
<h1 class="modal-title" id="resetPasswordTitle"> >{{ "resetPasswordLoggedOutWarning" | i18n: loggedOutWarningName }}
{{ "recoverAccount" | i18n }} </bit-callout>
<small class="text-muted" *ngIf="name">{{ name }}</small> <auth-password-callout
</h1> [policy]="enforcedPolicyOptions"
message="resetPasswordMasterPasswordPolicyInEffect"
*ngIf="enforcedPolicyOptions"
>
</auth-password-callout>
<bit-form-field>
<bit-label>
{{ "newPassword" | i18n }}
</bit-label>
<input
id="newPassword"
bitInput
[type]="showPassword ? 'text' : 'password'"
name="NewPassword"
formControlName="newPassword"
required
appInputVerbatim
autocomplete="new-password"
/>
<button <button
type="button" type="button"
class="close" bitIconButton="bwi-generate"
data-dismiss="modal" bitSuffix
appA11yTitle="{{ 'close' | i18n }}" [appA11yTitle]="'generatePassword' | i18n"
> (click)="generatePassword()"
<span aria-hidden="true">&times;</span> ></button>
</button> <button
</div> type="button"
<div class="modal-body"> bitSuffix
<app-callout type="warning" [bitIconButton]="showPassword ? 'bwi-eye-slash' : 'bwi-eye'"
>{{ "resetPasswordLoggedOutWarning" | i18n: loggedOutWarningName }} buttonType="secondary"
</app-callout> appA11yTitle="{{ 'toggleVisibility' | i18n }}"
<auth-password-callout (click)="togglePassword()"
[policy]="enforcedPolicyOptions" ></button>
message="resetPasswordMasterPasswordPolicyInEffect" <button
*ngIf="enforcedPolicyOptions" type="button"
> bitSuffix
</auth-password-callout> bitIconButton="bwi-clone"
<div class="row"> appA11yTitle="{{ 'copyPassword' | i18n }}"
<div class="col form-group"> (click)="copy()"
<div class="d-flex"> ></button>
<label for="newPassword">{{ "newPassword" | i18n }}</label> </bit-form-field>
<div class="ml-auto d-flex"> <tools-password-strength
<a [password]="formGroup.value.newPassword"
href="#" [email]="data.email"
class="d-block mr-2 bwi-icon-above-input" [showText]="true"
appStopClick (passwordStrengthScore)="getStrengthScore($event)"
appA11yTitle="{{ 'generatePassword' | i18n }}" >
(click)="generatePassword()" </tools-password-strength>
> </ng-container>
<i class="bwi bwi-lg bwi-fw bwi-refresh" aria-hidden="true"></i> <ng-container bitDialogFooter>
</a> <button bitButton buttonType="primary" bitFormButton type="submit">
</div> {{ "save" | i18n }}
</div> </button>
<div class="input-group mb-1"> <button bitButton buttonType="secondary" bitDialogClose type="button">
<input {{ "cancel" | i18n }}
id="newPassword" </button>
class="form-control text-monospace" </ng-container>
appAutofocus </bit-dialog>
type="{{ showPassword ? 'text' : 'password' }}" </form>
name="NewPassword"
[(ngModel)]="newPassword"
required
appInputVerbatim
autocomplete="new-password"
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy(newPassword)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<app-password-strength
[password]="newPassword"
[email]="email"
[showText]="true"
(passwordStrengthResult)="getStrengthResult($event)"
>
</app-password-strength>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@ -1,16 +1,9 @@
import { import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
Component, import { Component, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core";
EventEmitter, import { FormBuilder, Validators } from "@angular/forms";
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
} from "@angular/core";
import { Subject, takeUntil } from "rxjs"; 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 { 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 { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; 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"; 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({ @Component({
selector: "app-reset-password", selector: "app-reset-password",
templateUrl: "reset-password.component.html", 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 { export class ResetPasswordComponent implements OnInit, OnDestroy {
@Input() name: string; formGroup = this.formBuilder.group({
@Input() email: string; newPassword: ["", Validators.required],
@Input() id: string; });
@Input() organizationId: string;
@Output() passwordReset = new EventEmitter(); @ViewChild(PasswordStrengthV2Component) passwordStrengthComponent: PasswordStrengthV2Component;
@ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent;
enforcedPolicyOptions: MasterPasswordPolicyOptions; enforcedPolicyOptions: MasterPasswordPolicyOptions;
newPassword: string = null;
showPassword = false; showPassword = false;
passwordStrengthResult: zxcvbn.ZXCVBNResult; passwordStrengthScore: number;
formPromise: Promise<any>;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
constructor( constructor(
@Inject(DIALOG_DATA) protected data: ResetPasswordDialogData,
private resetPasswordService: OrganizationUserResetPasswordService, private resetPasswordService: OrganizationUserResetPasswordService,
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
@ -51,6 +77,8 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
private logService: LogService, private logService: LogService,
private dialogService: DialogService, private dialogService: DialogService,
private toastService: ToastService, private toastService: ToastService,
private formBuilder: FormBuilder,
private dialogRef: DialogRef<ResetPasswordDialogResult>,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@ -69,13 +97,15 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
} }
get loggedOutWarningName() { 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() { async generatePassword() {
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {}; const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
this.newPassword = await this.passwordGenerationService.generatePassword(options); this.formGroup.patchValue({
this.passwordStrengthComponent.updatePasswordStrength(this.newPassword); newPassword: await this.passwordGenerationService.generatePassword(options),
});
this.passwordStrengthComponent.updatePasswordStrength(this.formGroup.value.newPassword);
} }
togglePassword() { togglePassword() {
@ -83,7 +113,8 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
document.getElementById("newPassword").focus(); document.getElementById("newPassword").focus();
} }
copy(value: string) { copy() {
const value = this.formGroup.value.newPassword;
if (value == null) { if (value == null) {
return; return;
} }
@ -96,9 +127,9 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
}); });
} }
async submit() { submit = async () => {
// Validation // Validation
if (this.newPassword == null || this.newPassword === "") { if (this.formGroup.value.newPassword == null || this.formGroup.value.newPassword === "") {
this.toastService.showToast({ this.toastService.showToast({
variant: "error", variant: "error",
title: this.i18nService.t("errorOccurred"), title: this.i18nService.t("errorOccurred"),
@ -107,7 +138,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
return false; return false;
} }
if (this.newPassword.length < Utils.minimumPasswordLength) { if (this.formGroup.value.newPassword.length < Utils.minimumPasswordLength) {
this.toastService.showToast({ this.toastService.showToast({
variant: "error", variant: "error",
title: this.i18nService.t("errorOccurred"), title: this.i18nService.t("errorOccurred"),
@ -119,8 +150,8 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
if ( if (
this.enforcedPolicyOptions != null && this.enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword( !this.policyService.evaluateMasterPassword(
this.passwordStrengthResult.score, this.passwordStrengthScore,
this.newPassword, this.formGroup.value.newPassword,
this.enforcedPolicyOptions, this.enforcedPolicyOptions,
) )
) { ) {
@ -132,7 +163,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
return; return;
} }
if (this.passwordStrengthResult.score < 3) { if (this.passwordStrengthScore < 3) {
const result = await this.dialogService.openSimpleDialog({ const result = await this.dialogService.openSimpleDialog({
title: { key: "weakMasterPassword" }, title: { key: "weakMasterPassword" },
content: { key: "weakMasterPasswordDesc" }, content: { key: "weakMasterPasswordDesc" },
@ -145,26 +176,29 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
} }
try { try {
this.formPromise = this.resetPasswordService.resetMasterPassword( await this.resetPasswordService.resetMasterPassword(
this.newPassword, this.formGroup.value.newPassword,
this.email, this.data.email,
this.id, this.data.id,
this.organizationId, this.data.organizationId,
); );
await this.formPromise;
this.toastService.showToast({ this.toastService.showToast({
variant: "success", variant: "success",
title: null, title: null,
message: this.i18nService.t("resetPasswordSuccess"), message: this.i18nService.t("resetPasswordSuccess"),
}); });
this.passwordReset.emit();
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
} }
this.formPromise = null;
this.dialogRef.close(ResetPasswordDialogResult.Ok);
};
getStrengthScore(result: number) {
this.passwordStrengthScore = result;
} }
getStrengthResult(result: zxcvbn.ZXCVBNResult) { static open = (dialogService: DialogService, input: DialogConfig<ResetPasswordDialogData>) => {
this.passwordStrengthResult = result; return dialogService.open<ResetPasswordDialogResult>(ResetPasswordComponent, input);
} };
} }

View File

@ -70,7 +70,10 @@ import {
MemberDialogTab, MemberDialogTab,
openUserAddEditDialog, openUserAddEditDialog,
} from "./components/member-dialog"; } from "./components/member-dialog";
import { ResetPasswordComponent } from "./components/reset-password.component"; import {
ResetPasswordComponent,
ResetPasswordDialogResult,
} from "./components/reset-password.component";
class MembersTableDataSource extends PeopleTableDataSource<OrganizationUserView> { class MembersTableDataSource extends PeopleTableDataSource<OrganizationUserView> {
protected statusType = OrganizationUserStatusType; protected statusType = OrganizationUserStatusType;
@ -663,24 +666,19 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
} }
async resetPassword(user: OrganizationUserView) { async resetPassword(user: OrganizationUserView) {
const [modal] = await this.modalService.openViewRef( const dialogRef = ResetPasswordComponent.open(this.dialogService, {
ResetPasswordComponent, data: {
this.resetPasswordModalRef, name: this.userNamePipe.transform(user),
(comp) => { email: user != null ? user.email : null,
comp.name = this.userNamePipe.transform(user); organizationId: this.organization.id,
comp.email = user != null ? user.email : null; id: user != null ? user.id : 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 result = await lastValueFrom(dialogRef.closed);
if (result === ResetPasswordDialogResult.Ok) {
await this.load();
}
} }
protected async removeUserConfirmationDialog(user: OrganizationUserView) { protected async removeUserConfirmationDialog(user: OrganizationUserView) {

View File

@ -1,6 +1,7 @@
import { ScrollingModule } from "@angular/cdk/scrolling"; import { ScrollingModule } from "@angular/cdk/scrolling";
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component";
import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; import { PasswordCalloutComponent } from "@bitwarden/auth/angular";
import { LooseComponentsModule } from "../../../shared"; import { LooseComponentsModule } from "../../../shared";
@ -24,6 +25,7 @@ import { MembersComponent } from "./members.component";
UserDialogModule, UserDialogModule,
PasswordCalloutComponent, PasswordCalloutComponent,
ScrollingModule, ScrollingModule,
PasswordStrengthV2Component,
], ],
declarations: [ declarations: [
BulkConfirmComponent, BulkConfirmComponent,