[PM-5165][PM-8645] Migrate password strength component (#9912)
* Create standalone password-strength-v2 component * Add deprecation notice to old component * PM-8645: Use new password-strength component on export * Remove unneccessary variable * Remove setPasswordScoreText method * Rename passwordStrengthResult to passwordStrengthScore and assign proper type * Add missing types * Document component Inputs/Outputs * Add unit tests --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
parent
f446c3b454
commit
33de685b40
|
@ -0,0 +1,7 @@
|
|||
<bit-progress
|
||||
[size]="size"
|
||||
[text]="text"
|
||||
[bgColor]="color"
|
||||
[showText]="showText"
|
||||
[barWidth]="scoreWidth"
|
||||
></bit-progress>
|
|
@ -0,0 +1,80 @@
|
|||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
|
||||
import {
|
||||
PasswordColorText,
|
||||
PasswordStrengthScore,
|
||||
PasswordStrengthV2Component,
|
||||
} from "./password-strength-v2.component";
|
||||
|
||||
describe("PasswordStrengthV2Component", () => {
|
||||
let component: PasswordStrengthV2Component;
|
||||
let fixture: ComponentFixture<PasswordStrengthV2Component>;
|
||||
|
||||
const mockPasswordStrengthService = mock<PasswordStrengthServiceAbstraction>();
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: PasswordStrengthServiceAbstraction, useValue: mockPasswordStrengthService },
|
||||
],
|
||||
});
|
||||
fixture = TestBed.createComponent(PasswordStrengthV2Component);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create the component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should update password strength when password changes", () => {
|
||||
const password = "testPassword";
|
||||
jest.spyOn(component, "updatePasswordStrength");
|
||||
component.password = password;
|
||||
expect(component.updatePasswordStrength).toHaveBeenCalledWith(password);
|
||||
expect(mockPasswordStrengthService.getPasswordStrength).toHaveBeenCalledWith(
|
||||
password,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("should emit password strength result when password changes", () => {
|
||||
const password = "testPassword";
|
||||
jest.spyOn(component.passwordStrengthScore, "emit");
|
||||
component.password = password;
|
||||
expect(component.passwordStrengthScore.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should emit password score text and color when ngOnChanges executes", () => {
|
||||
jest.spyOn(component.passwordScoreTextWithColor, "emit");
|
||||
jest.useFakeTimers();
|
||||
component.ngOnChanges();
|
||||
jest.runAllTimers();
|
||||
expect(component.passwordScoreTextWithColor.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const table = [
|
||||
[4, { color: "success", text: "strong" }],
|
||||
[3, { color: "primary", text: "good" }],
|
||||
[2, { color: "warning", text: "weak" }],
|
||||
[1, { color: "danger", text: "weak" }],
|
||||
[null, { color: "danger", text: null }],
|
||||
];
|
||||
|
||||
test.each(table)(
|
||||
"should passwordScore be %d then emit passwordScoreTextWithColor = %s",
|
||||
(score: PasswordStrengthScore, expected: PasswordColorText) => {
|
||||
jest.useFakeTimers();
|
||||
jest.spyOn(component.passwordScoreTextWithColor, "emit");
|
||||
component.passwordScore = score;
|
||||
component.ngOnChanges();
|
||||
jest.runAllTimers();
|
||||
expect(component.passwordScoreTextWithColor.emit).toHaveBeenCalledWith(expected);
|
||||
},
|
||||
);
|
||||
});
|
|
@ -0,0 +1,119 @@
|
|||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, OnChanges, Output } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { ProgressModule } from "@bitwarden/components";
|
||||
|
||||
export interface PasswordColorText {
|
||||
color: BackgroundTypes;
|
||||
text: string;
|
||||
}
|
||||
export type PasswordStrengthScore = 0 | 1 | 2 | 3 | 4;
|
||||
|
||||
type SizeTypes = "small" | "default" | "large";
|
||||
type BackgroundTypes = "danger" | "primary" | "success" | "warning";
|
||||
|
||||
@Component({
|
||||
selector: "tools-password-strength",
|
||||
templateUrl: "password-strength-v2.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, JslibModule, ProgressModule],
|
||||
})
|
||||
export class PasswordStrengthV2Component implements OnChanges {
|
||||
/**
|
||||
* The size (height) of the password strength component.
|
||||
* Possible values are "default", "small" and "large".
|
||||
*/
|
||||
@Input() size: SizeTypes = "default";
|
||||
/**
|
||||
* Determines whether to show the password strength score text on the progress bar or not.
|
||||
*/
|
||||
@Input() showText = false;
|
||||
/**
|
||||
* Optional email address which can be used as input for the password strength calculation
|
||||
*/
|
||||
@Input() email: string;
|
||||
/**
|
||||
* Optional name which can be used as input for the password strength calculation
|
||||
*/
|
||||
@Input() name: string;
|
||||
/**
|
||||
* Sets the password value and updates the password strength.
|
||||
*
|
||||
* @param value - password provided by the hosting component
|
||||
*/
|
||||
@Input() set password(value: string) {
|
||||
this.updatePasswordStrength(value);
|
||||
}
|
||||
/**
|
||||
* Emits the password strength score.
|
||||
*
|
||||
* @remarks
|
||||
* The password strength score represents the strength of a password.
|
||||
* It is emitted as an event when the password strength changes.
|
||||
*/
|
||||
@Output() passwordStrengthScore = new EventEmitter<PasswordStrengthScore>();
|
||||
|
||||
/**
|
||||
* Emits an event with the password score text and color.
|
||||
*/
|
||||
@Output() passwordScoreTextWithColor = new EventEmitter<PasswordColorText>();
|
||||
|
||||
passwordScore: PasswordStrengthScore;
|
||||
scoreWidth = 0;
|
||||
color: BackgroundTypes = "danger";
|
||||
text: string;
|
||||
|
||||
private passwordStrengthTimeout: number | NodeJS.Timeout;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
) {}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.passwordStrengthTimeout = setTimeout(() => {
|
||||
this.scoreWidth = this.passwordScore == null ? 0 : (this.passwordScore + 1) * 20;
|
||||
|
||||
switch (this.passwordScore) {
|
||||
case 4:
|
||||
this.color = "success";
|
||||
this.text = this.i18nService.t("strong");
|
||||
break;
|
||||
case 3:
|
||||
this.color = "primary";
|
||||
this.text = this.i18nService.t("good");
|
||||
break;
|
||||
case 2:
|
||||
this.color = "warning";
|
||||
this.text = this.i18nService.t("weak");
|
||||
break;
|
||||
default:
|
||||
this.color = "danger";
|
||||
this.text = this.passwordScore != null ? this.i18nService.t("weak") : null;
|
||||
break;
|
||||
}
|
||||
|
||||
this.passwordScoreTextWithColor.emit({
|
||||
color: this.color,
|
||||
text: this.text,
|
||||
} as PasswordColorText);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
updatePasswordStrength(password: string) {
|
||||
if (this.passwordStrengthTimeout != null) {
|
||||
clearTimeout(this.passwordStrengthTimeout);
|
||||
}
|
||||
|
||||
const strengthResult = this.passwordStrengthService.getPasswordStrength(
|
||||
password,
|
||||
this.email,
|
||||
this.name?.trim().toLowerCase().split(" "),
|
||||
);
|
||||
this.passwordScore = strengthResult == null ? null : strengthResult.score;
|
||||
this.passwordStrengthScore.emit(this.passwordScore);
|
||||
}
|
||||
}
|
|
@ -8,6 +8,9 @@ export interface PasswordColorText {
|
|||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated July 2024: Use new PasswordStrengthV2Component instead
|
||||
*/
|
||||
@Component({
|
||||
selector: "app-password-strength",
|
||||
templateUrl: "password-strength.component.html",
|
||||
|
|
|
@ -76,7 +76,8 @@
|
|||
></button>
|
||||
<bit-hint>{{ "exportPasswordDescription" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<app-password-strength [password]="filePassword" [showText]="true"> </app-password-strength>
|
||||
<tools-password-strength [password]="filePassword" [showText]="true">
|
||||
</tools-password-strength>
|
||||
</div>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "confirmFilePassword" | i18n }}</bit-label>
|
||||
|
|
|
@ -12,7 +12,7 @@ import { ReactiveFormsModule, UntypedFormBuilder, Validators } from "@angular/fo
|
|||
import { map, merge, Observable, startWith, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { PasswordStrengthComponent } from "@bitwarden/angular/tools/password-strength/password-strength.component";
|
||||
import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component";
|
||||
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
|
@ -58,6 +58,7 @@ import { ExportScopeCalloutComponent } from "./export-scope-callout.component";
|
|||
RadioButtonModule,
|
||||
ExportScopeCalloutComponent,
|
||||
UserVerificationDialogComponent,
|
||||
PasswordStrengthV2Component,
|
||||
],
|
||||
})
|
||||
export class ExportComponent implements OnInit, OnDestroy {
|
||||
|
@ -110,7 +111,7 @@ export class ExportComponent implements OnInit, OnDestroy {
|
|||
@Output()
|
||||
onSuccessfulExport = new EventEmitter<string>();
|
||||
|
||||
@ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent;
|
||||
@ViewChild(PasswordStrengthV2Component) passwordStrengthComponent: PasswordStrengthV2Component;
|
||||
|
||||
encryptedExportType = EncryptedExportType;
|
||||
protected showFilePassword: boolean;
|
||||
|
|
Loading…
Reference in New Issue