mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-18 15:47:57 +01: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:
parent
7fc987d806
commit
35ff7d49b3
@ -1,100 +1,67 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="resetPasswordTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="resetPasswordTitle">
|
||||
{{ "recoverAccount" | i18n }}
|
||||
<small class="text-muted" *ngIf="name">{{ name }}</small>
|
||||
</h1>
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog [title]="'recoverAccount' | i18n" [subtitle]="data.name">
|
||||
<ng-container bitDialogContent>
|
||||
<bit-callout type="warning"
|
||||
>{{ "resetPasswordLoggedOutWarning" | i18n: loggedOutWarningName }}
|
||||
</bit-callout>
|
||||
<auth-password-callout
|
||||
[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
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<app-callout type="warning"
|
||||
>{{ "resetPasswordLoggedOutWarning" | i18n: loggedOutWarningName }}
|
||||
</app-callout>
|
||||
<auth-password-callout
|
||||
[policy]="enforcedPolicyOptions"
|
||||
message="resetPasswordMasterPasswordPolicyInEffect"
|
||||
*ngIf="enforcedPolicyOptions"
|
||||
>
|
||||
</auth-password-callout>
|
||||
<div class="row">
|
||||
<div class="col form-group">
|
||||
<div class="d-flex">
|
||||
<label for="newPassword">{{ "newPassword" | i18n }}</label>
|
||||
<div class="ml-auto d-flex">
|
||||
<a
|
||||
href="#"
|
||||
class="d-block mr-2 bwi-icon-above-input"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'generatePassword' | i18n }}"
|
||||
(click)="generatePassword()"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-fw bwi-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group mb-1">
|
||||
<input
|
||||
id="newPassword"
|
||||
class="form-control text-monospace"
|
||||
appAutofocus
|
||||
type="{{ showPassword ? 'text' : 'password' }}"
|
||||
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>
|
||||
bitIconButton="bwi-generate"
|
||||
bitSuffix
|
||||
[appA11yTitle]="'generatePassword' | i18n"
|
||||
(click)="generatePassword()"
|
||||
></button>
|
||||
<button
|
||||
type="button"
|
||||
bitSuffix
|
||||
[bitIconButton]="showPassword ? 'bwi-eye-slash' : 'bwi-eye'"
|
||||
buttonType="secondary"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="togglePassword()"
|
||||
></button>
|
||||
<button
|
||||
type="button"
|
||||
bitSuffix
|
||||
bitIconButton="bwi-clone"
|
||||
appA11yTitle="{{ 'copyPassword' | i18n }}"
|
||||
(click)="copy()"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<tools-password-strength
|
||||
[password]="formGroup.value.newPassword"
|
||||
[email]="data.email"
|
||||
[showText]="true"
|
||||
(passwordStrengthScore)="getStrengthScore($event)"
|
||||
>
|
||||
</tools-password-strength>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary" bitFormButton type="submit">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
@ -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<any>;
|
||||
passwordStrengthScore: number;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
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<ResetPasswordDialogResult>,
|
||||
) {}
|
||||
|
||||
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<ResetPasswordDialogData>) => {
|
||||
return dialogService.open<ResetPasswordDialogResult>(ResetPasswordComponent, input);
|
||||
};
|
||||
}
|
||||
|
@ -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<OrganizationUserView> {
|
||||
protected statusType = OrganizationUserStatusType;
|
||||
@ -663,24 +666,19 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user