mirror of
https://github.com/bitwarden/browser.git
synced 2025-03-10 13:09:37 +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">
|
<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">
|
|
||||||
{{ "recoverAccount" | i18n }}
|
|
||||||
<small class="text-muted" *ngIf="name">{{ name }}</small>
|
|
||||||
</h1>
|
|
||||||
<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 }}
|
>{{ "resetPasswordLoggedOutWarning" | i18n: loggedOutWarningName }}
|
||||||
</app-callout>
|
</bit-callout>
|
||||||
<auth-password-callout
|
<auth-password-callout
|
||||||
[policy]="enforcedPolicyOptions"
|
[policy]="enforcedPolicyOptions"
|
||||||
message="resetPasswordMasterPasswordPolicyInEffect"
|
message="resetPasswordMasterPasswordPolicyInEffect"
|
||||||
*ngIf="enforcedPolicyOptions"
|
*ngIf="enforcedPolicyOptions"
|
||||||
>
|
>
|
||||||
</auth-password-callout>
|
</auth-password-callout>
|
||||||
<div class="row">
|
<bit-form-field>
|
||||||
<div class="col form-group">
|
<bit-label>
|
||||||
<div class="d-flex">
|
{{ "newPassword" | i18n }}
|
||||||
<label for="newPassword">{{ "newPassword" | i18n }}</label>
|
</bit-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
|
<input
|
||||||
id="newPassword"
|
id="newPassword"
|
||||||
class="form-control text-monospace"
|
bitInput
|
||||||
appAutofocus
|
[type]="showPassword ? 'text' : 'password'"
|
||||||
type="{{ showPassword ? 'text' : 'password' }}"
|
|
||||||
name="NewPassword"
|
name="NewPassword"
|
||||||
[(ngModel)]="newPassword"
|
formControlName="newPassword"
|
||||||
required
|
required
|
||||||
appInputVerbatim
|
appInputVerbatim
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
/>
|
/>
|
||||||
<div class="input-group-append">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-secondary"
|
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 }}"
|
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||||
(click)="togglePassword()"
|
(click)="togglePassword()"
|
||||||
>
|
></button>
|
||||||
<i
|
|
||||||
class="bwi bwi-lg"
|
|
||||||
aria-hidden="true"
|
|
||||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-secondary"
|
bitSuffix
|
||||||
|
bitIconButton="bwi-clone"
|
||||||
appA11yTitle="{{ 'copyPassword' | i18n }}"
|
appA11yTitle="{{ 'copyPassword' | i18n }}"
|
||||||
(click)="copy(newPassword)"
|
(click)="copy()"
|
||||||
>
|
></button>
|
||||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
</bit-form-field>
|
||||||
</button>
|
<tools-password-strength
|
||||||
</div>
|
[password]="formGroup.value.newPassword"
|
||||||
</div>
|
[email]="data.email"
|
||||||
<app-password-strength
|
|
||||||
[password]="newPassword"
|
|
||||||
[email]="email"
|
|
||||||
[showText]="true"
|
[showText]="true"
|
||||||
(passwordStrengthResult)="getStrengthResult($event)"
|
(passwordStrengthScore)="getStrengthScore($event)"
|
||||||
>
|
>
|
||||||
</app-password-strength>
|
</tools-password-strength>
|
||||||
</div>
|
</ng-container>
|
||||||
</div>
|
<ng-container bitDialogFooter>
|
||||||
</div>
|
<button bitButton buttonType="primary" bitFormButton type="submit">
|
||||||
<div class="modal-footer">
|
{{ "save" | i18n }}
|
||||||
<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>
|
||||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||||
{{ "cancel" | i18n }}
|
{{ "cancel" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</ng-container>
|
||||||
|
</bit-dialog>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
@ -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);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user