[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:
Daniel James Smith 2024-07-10 16:59:17 +02:00 committed by GitHub
parent f446c3b454
commit 33de685b40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 214 additions and 3 deletions

View File

@ -0,0 +1,7 @@
<bit-progress
[size]="size"
[text]="text"
[bgColor]="color"
[showText]="showText"
[barWidth]="scoreWidth"
></bit-progress>

View File

@ -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);
},
);
});

View File

@ -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);
}
}

View File

@ -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",

View File

@ -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>

View File

@ -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;