1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 13:53:34 +00:00

[SG-414] Refactor password strength component (#3186)

* moved password strength to libs

* refactored password strength component

* made changes on desktop and browser to reuse component

* resolved suggestions from PR review

* shared module restructure

* shared module restructure
This commit is contained in:
Gbubemi Smith
2022-08-01 23:26:50 +01:00
committed by GitHub
parent 8820a42ec9
commit 257fb0c0af
27 changed files with 259 additions and 425 deletions

View File

@@ -25,11 +25,8 @@
<div class="row-main"> <div class="row-main">
<label for="masterPassword"> <label for="masterPassword">
{{ "masterPass" | i18n }} {{ "masterPass" | i18n }}
<strong <strong class="sub-label text-{{ color }}" *ngIf="text">
class="sub-label text-{{ masterPasswordScoreColor }}" {{ text }}
*ngIf="masterPasswordScoreText"
>
{{ masterPasswordScoreText }}
</strong> </strong>
</label> </label>
<input <input
@@ -38,7 +35,6 @@
class="monospaced" class="monospaced"
formControlName="masterPassword" formControlName="masterPassword"
appInputVerbatim appInputVerbatim
(input)="updatePasswordStrength()"
/> />
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
@@ -58,17 +54,14 @@
</button> </button>
</div> </div>
</div> </div>
<div class="progress"> <app-password-strength
<div [password]="formGroup.get('masterPassword')?.value"
class="progress-bar bg-{{ masterPasswordScoreColor }}" [email]="formGroup.get('email')?.value"
role="progressbar" [name]="formGroup.get('name')?.value"
aria-valuenow="0" (passwordStrengthResult)="getStrengthResult($event)"
aria-valuemin="0" (passwordScoreColor)="getPasswordScoreText($event)"
aria-valuemax="100" >
[ngStyle]="{ width: masterPasswordScoreWidth + '%' }" </app-password-strength>
attr.aria-valuenow="{{ masterPasswordScoreWidth }}"
></div>
</div>
</div> </div>
</div> </div>
<div class="box-footer"> <div class="box-footer">

View File

@@ -19,6 +19,9 @@ import { StateService } from "@bitwarden/common/abstractions/state.service";
templateUrl: "register.component.html", templateUrl: "register.component.html",
}) })
export class RegisterComponent extends BaseRegisterComponent { export class RegisterComponent extends BaseRegisterComponent {
color: string;
text: string;
constructor( constructor(
formValidationErrorService: FormValidationErrorsService, formValidationErrorService: FormValidationErrorsService,
formBuilder: UntypedFormBuilder, formBuilder: UntypedFormBuilder,

View File

@@ -42,10 +42,10 @@
<label for="masterPassword" <label for="masterPassword"
>{{ "masterPass" | i18n }} >{{ "masterPass" | i18n }}
<strong <strong
class="sub-label text-{{ masterPasswordScoreColor }}" class="sub-label text-{{ passwordStrengthComponent?.masterPasswordScoreColor }}"
*ngIf="masterPasswordScoreText" *ngIf="passwordStrengthComponent?.masterPasswordScoreText"
> >
{{ masterPasswordScoreText }} {{ passwordStrengthComponent?.masterPasswordScoreText }}
</strong> </strong>
</label> </label>
<input <input
@@ -55,7 +55,6 @@
class="monospaced" class="monospaced"
[(ngModel)]="masterPassword" [(ngModel)]="masterPassword"
required required
(input)="updatePasswordStrength()"
appInputVerbatim appInputVerbatim
/> />
</div> </div>
@@ -76,17 +75,14 @@
</button> </button>
</div> </div>
</div> </div>
<div class="progress">
<div <app-password-strength
class="progress-bar bg-{{ masterPasswordScoreColor }}" [password]="masterPassword"
role="progressbar" [email]="email"
aria-valuenow="0" (passwordStrengthResult)="getStrengthResult($event)"
aria-valuemin="0" (passwordScoreColor)="getPasswordScoreText($event)"
aria-valuemax="100" >
[ngStyle]="{ width: masterPasswordScoreWidth + '%' }" </app-password-strength>
attr.aria-valuenow="{{ masterPasswordScoreWidth }}"
></div>
</div>
</div> </div>
</div> </div>
<div class="box-footer"> <div class="box-footer">

View File

@@ -44,34 +44,4 @@ export class SetPasswordComponent extends BaseSetPasswordComponent {
stateService stateService
); );
} }
get masterPasswordScoreWidth() {
return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
}
get masterPasswordScoreColor() {
switch (this.masterPasswordScore) {
case 4:
return "success";
case 3:
return "primary";
case 2:
return "warning";
default:
return "danger";
}
}
get masterPasswordScoreText() {
switch (this.masterPasswordScore) {
case 4:
return this.i18nService.t("strong");
case 3:
return this.i18nService.t("good");
case 2:
return this.i18nService.t("weak");
default:
return this.masterPasswordScore != null ? this.i18nService.t("weak") : null;
}
}
} }

View File

@@ -30,11 +30,8 @@
<div class="row-main"> <div class="row-main">
<label for="masterPassword"> <label for="masterPassword">
{{ "masterPass" | i18n }} {{ "masterPass" | i18n }}
<strong <strong class="sub-label text-{{ color }}" *ngIf="text">
class="sub-label text-{{ masterPasswordScoreStyle.Color }}" {{ text }}
*ngIf="masterPasswordScoreStyle.Text"
>
{{ masterPasswordScoreStyle.Text }}
</strong> </strong>
</label> </label>
<input <input
@@ -45,7 +42,6 @@
[(ngModel)]="masterPassword" [(ngModel)]="masterPassword"
required required
appInputVerbatim appInputVerbatim
(input)="updatePasswordStrength()"
/> />
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
@@ -65,17 +61,13 @@
</button> </button>
</div> </div>
</div> </div>
<div class="progress"> <app-password-strength
<div [password]="masterPassword"
class="progress-bar bg-{{ masterPasswordScoreStyle.Color }}" [email]="email"
role="progressbar" (passwordStrengthResult)="getStrengthResult($event)"
aria-valuenow="0" (passwordScoreColor)="getPasswordScoreText($event)"
aria-valuemin="0" >
aria-valuemax="100" </app-password-strength>
[ngStyle]="{ width: masterPasswordScoreStyle.Width + '%' }"
attr.aria-valuenow="{{ masterPasswordScoreStyle.Width }}"
></div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -12,47 +12,11 @@ import { PolicyService } from "@bitwarden/common/abstractions/policy.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { SyncService } from "@bitwarden/common/abstractions/sync.service"; import { SyncService } from "@bitwarden/common/abstractions/sync.service";
interface MasterPasswordScore {
Color: string;
Text: string;
Width: number;
}
@Component({ @Component({
selector: "app-update-temp-password", selector: "app-update-temp-password",
templateUrl: "update-temp-password.component.html", templateUrl: "update-temp-password.component.html",
}) })
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent { export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {
get masterPasswordScoreStyle(): MasterPasswordScore {
const scoreWidth = this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
switch (this.masterPasswordScore) {
case 4:
return {
Color: "bg-success",
Text: "strong",
Width: scoreWidth,
};
case 3:
return {
Color: "bg-primary",
Text: "good",
Width: scoreWidth,
};
case 2:
return {
Color: "bg-warning",
Text: "weak",
Width: scoreWidth,
};
default:
return {
Color: "bg-danger",
Text: "weak",
Width: scoreWidth,
};
}
}
constructor( constructor(
i18nService: I18nService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService,

View File

@@ -18,11 +18,8 @@
<div class="row-main"> <div class="row-main">
<label for="masterPassword"> <label for="masterPassword">
{{ "masterPass" | i18n }} {{ "masterPass" | i18n }}
<strong <strong class="sub-label text-{{ color }}" *ngIf="text">
class="sub-label text-{{ masterPasswordScoreColor }}" {{ text }}
*ngIf="masterPasswordScoreText"
>
{{ masterPasswordScoreText }}
</strong> </strong>
</label> </label>
<input <input
@@ -30,7 +27,6 @@
type="{{ showPassword ? 'text' : 'password' }}" type="{{ showPassword ? 'text' : 'password' }}"
class="monospaced" class="monospaced"
formControlName="masterPassword" formControlName="masterPassword"
(input)="updatePasswordStrength()"
appInputVerbatim appInputVerbatim
/> />
</div> </div>
@@ -51,17 +47,14 @@
</button> </button>
</div> </div>
</div> </div>
<div class="progress"> <app-password-strength
<div [password]="formGroup.get('masterPassword')?.value"
class="progress-bar bg-{{ masterPasswordScoreColor }}" [email]="formGroup.get('email')?.value"
role="progressbar" [name]="formGroup.get('name')?.value"
aria-valuenow="0" (passwordStrengthResult)="getStrengthResult($event)"
aria-valuemin="0" (passwordScoreColor)="getPasswordScoreText($event)"
aria-valuemax="100" >
[ngStyle]="{ width: masterPasswordScoreWidth + '%' }" </app-password-strength>
attr.aria-valuenow="{{ masterPasswordScoreWidth }}"
></div>
</div>
</div> </div>
</div> </div>
<div class="box-footer"> <div class="box-footer">

View File

@@ -37,11 +37,8 @@
<div class="row-main"> <div class="row-main">
<label for="masterPassword" <label for="masterPassword"
>{{ "masterPass" | i18n }} >{{ "masterPass" | i18n }}
<strong <strong class="sub-label text-{{ color }}" *ngIf="text">
class="sub-label text-{{ masterPasswordScoreColor }}" {{ text }}
*ngIf="masterPasswordScoreText"
>
{{ masterPasswordScoreText }}
</strong> </strong>
</label> </label>
<input <input
@@ -51,7 +48,6 @@
class="monospaced" class="monospaced"
[(ngModel)]="masterPassword" [(ngModel)]="masterPassword"
required required
(input)="updatePasswordStrength()"
appInputVerbatim appInputVerbatim
/> />
</div> </div>
@@ -72,17 +68,13 @@
</button> </button>
</div> </div>
</div> </div>
<div class="progress"> <app-password-strength
<div [password]="masterPassword"
class="progress-bar bg-{{ masterPasswordScoreColor }}" [email]="email"
role="progressbar" (passwordStrengthResult)="getStrengthResult($event)"
aria-valuenow="0" (passwordScoreColor)="getPasswordScoreText($event)"
aria-valuemin="0" >
aria-valuemax="100" </app-password-strength>
[ngStyle]="{ width: masterPasswordScoreWidth + '%' }"
attr.aria-valuenow="{{ masterPasswordScoreWidth }}"
></div>
</div>
</div> </div>
</div> </div>
<div class="box-footer"> <div class="box-footer">

View File

@@ -50,36 +50,6 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
); );
} }
get masterPasswordScoreWidth() {
return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
}
get masterPasswordScoreColor() {
switch (this.masterPasswordScore) {
case 4:
return "success";
case 3:
return "primary";
case 2:
return "warning";
default:
return "danger";
}
}
get masterPasswordScoreText() {
switch (this.masterPasswordScore) {
case 4:
return this.i18nService.t("strong");
case 3:
return this.i18nService.t("good");
case 2:
return this.i18nService.t("weak");
default:
return this.masterPasswordScore != null ? this.i18nService.t("weak") : null;
}
}
async ngOnInit() { async ngOnInit() {
await super.ngOnInit(); await super.ngOnInit();
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {

View File

@@ -16,11 +16,8 @@
<div class="row-main"> <div class="row-main">
<label for="masterPassword"> <label for="masterPassword">
{{ "masterPass" | i18n }} {{ "masterPass" | i18n }}
<strong <strong class="sub-label text-{{ color }}" *ngIf="text">
class="sub-label text-{{ masterPasswordScoreStyle.Color }}" {{ text }}
*ngIf="masterPasswordScoreStyle.Text"
>
{{ masterPasswordScoreStyle.Text }}
</strong> </strong>
</label> </label>
<input <input
@@ -31,7 +28,6 @@
[(ngModel)]="masterPassword" [(ngModel)]="masterPassword"
required required
[appAutofocus]="masterPassword === ''" [appAutofocus]="masterPassword === ''"
(input)="updatePasswordStrength()"
appInputVerbatim appInputVerbatim
/> />
</div> </div>
@@ -52,17 +48,13 @@
</button> </button>
</div> </div>
</div> </div>
<div class="progress"> <app-password-strength
<div [password]="masterPassword"
class="progress-bar bg-{{ masterPasswordScoreStyle.Color }}" [email]="email"
role="progressbar" (passwordStrengthResult)="getStrengthResult($event)"
aria-valuenow="0" (passwordScoreColor)="getPasswordScoreText($event)"
aria-valuemin="0" >
aria-valuemax="100" </app-password-strength>
[ngStyle]="{ width: masterPasswordScoreStyle.Width + '%' }"
attr.aria-valuenow="{{ masterPasswordScoreStyle.Width }}"
></div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -12,46 +12,11 @@ import { PolicyService } from "@bitwarden/common/abstractions/policy.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { SyncService } from "@bitwarden/common/abstractions/sync.service"; import { SyncService } from "@bitwarden/common/abstractions/sync.service";
interface MasterPasswordScore {
Color: string;
Text: string;
Width: number;
}
@Component({ @Component({
selector: "app-update-temp-password", selector: "app-update-temp-password",
templateUrl: "update-temp-password.component.html", templateUrl: "update-temp-password.component.html",
}) })
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent { export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {
get masterPasswordScoreStyle(): MasterPasswordScore {
const scoreWidth = this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
switch (this.masterPasswordScore) {
case 4:
return {
Color: "bg-success",
Text: "strong",
Width: scoreWidth,
};
case 3:
return {
Color: "bg-primary",
Text: "good",
Width: scoreWidth,
};
case 2:
return {
Color: "bg-warning",
Text: "weak",
Width: scoreWidth,
};
default:
return {
Color: "bg-danger",
Text: "weak",
Width: scoreWidth,
};
}
}
constructor( constructor(
i18nService: I18nService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService,

View File

@@ -32,11 +32,15 @@
name="MasterPasswordHash" name="MasterPasswordHash"
class="text-monospace form-control mb-1" class="text-monospace form-control mb-1"
[(ngModel)]="masterPassword" [(ngModel)]="masterPassword"
(input)="updatePasswordStrength()"
required required
appInputVerbatim appInputVerbatim
/> />
<app-password-strength [score]="masterPasswordScore" [showText]="true"> <app-password-strength
[password]="masterPassword"
[email]="email"
[showText]="true"
(passwordStrengthResult)="getStrengthResult($event)"
>
</app-password-strength> </app-password-strength>
</div> </div>
<div> <div>

View File

@@ -44,13 +44,13 @@
name="NewMasterPasswordHash" name="NewMasterPasswordHash"
class="form-control mb-1" class="form-control mb-1"
[(ngModel)]="masterPassword" [(ngModel)]="masterPassword"
(input)="updatePasswordStrength()"
required required
appInputVerbatim appInputVerbatim
autocomplete="new-password" autocomplete="new-password"
/> />
<app-password-strength <app-password-strength
[score]="masterPasswordScore" [password]="masterPassword"
[email]="email"
[showText]="true" [showText]="true"
></app-password-strength> ></app-password-strength>
</div> </div>

View File

@@ -21,11 +21,14 @@
name="MasterPasswordHash" name="MasterPasswordHash"
class="text-monospace form-control mb-1" class="text-monospace form-control mb-1"
[(ngModel)]="masterPassword" [(ngModel)]="masterPassword"
(input)="updatePasswordStrength()"
required required
appInputVerbatim appInputVerbatim
/> />
<app-password-strength [score]="masterPasswordScore" [showText]="true"> <app-password-strength
[password]="masterPassword"
[email]="email"
[showText]="true"
>
</app-password-strength> </app-password-strength>
</div> </div>
<div> <div>

View File

@@ -1,40 +0,0 @@
import { Component, Input, OnChanges } from "@angular/core";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
@Component({
selector: "app-password-strength",
templateUrl: "password-strength.component.html",
})
export class PasswordStrengthComponent implements OnChanges {
@Input() score?: number;
@Input() showText = false;
scoreWidth = 0;
color = "bg-danger";
text: string;
constructor(private i18nService: I18nService) {}
ngOnChanges(): void {
this.scoreWidth = this.score == null ? 0 : (this.score + 1) * 20;
switch (this.score) {
case 4:
this.color = "bg-success";
this.text = this.i18nService.t("strong");
break;
case 3:
this.color = "bg-primary";
this.text = this.i18nService.t("good");
break;
case 2:
this.color = "bg-warning";
this.text = this.i18nService.t("weak");
break;
default:
this.color = "bg-danger";
this.text = this.score != null ? this.i18nService.t("weak") : null;
break;
}
}
}

View File

@@ -34,7 +34,6 @@
<input <input
id="register-form_input_master-password" id="register-form_input_master-password"
bitInput bitInput
(input)="updatePasswordStrength()"
type="{{ showPassword ? 'text' : 'password' }}" type="{{ showPassword ? 'text' : 'password' }}"
formControlName="masterPassword" formControlName="masterPassword"
/> />
@@ -50,7 +49,13 @@
{{ "masterPassImportant" | i18n }} {{ "masterPassImportant" | i18n }}
</bit-hint> </bit-hint>
</bit-form-field> </bit-form-field>
<app-password-strength [score]="masterPasswordScore" [showText]="true"> <app-password-strength
[password]="formGroup.get('masterPassword')?.value"
[email]="formGroup.get('email')?.value"
[name]="formGroup.get('name')?.value"
[showText]="true"
(passwordStrengthResult)="getStrengthResult($event)"
>
</app-password-strength> </app-password-strength>
</div> </div>

View File

@@ -69,7 +69,7 @@ export class RegisterFormComponent extends BaseRegisterComponent {
if ( if (
this.enforcedPolicyOptions != null && this.enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword( !this.policyService.evaluateMasterPassword(
this.masterPasswordScore, this.passwordStrengthResult.score,
this.formGroup.get("masterPassword")?.value, this.formGroup.get("masterPassword")?.value,
this.enforcedPolicyOptions this.enforcedPolicyOptions
) )

View File

@@ -66,7 +66,6 @@ import {
MenuModule, MenuModule,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { PasswordStrengthComponent } from "../components/password-strength.component";
import { PaymentComponent } from "../settings/payment.component"; import { PaymentComponent } from "../settings/payment.component";
import { TaxInfoComponent } from "../settings/tax-info.component"; import { TaxInfoComponent } from "../settings/tax-info.component";
@@ -122,7 +121,7 @@ registerLocaleData(localeZhCn, "zh-CN");
registerLocaleData(localeZhTw, "zh-TW"); registerLocaleData(localeZhTw, "zh-TW");
@NgModule({ @NgModule({
declarations: [PasswordStrengthComponent, PaymentComponent, TaxInfoComponent], declarations: [PaymentComponent, TaxInfoComponent],
imports: [ imports: [
CommonModule, CommonModule,
DragDropModule, DragDropModule,
@@ -158,7 +157,6 @@ registerLocaleData(localeZhTw, "zh-TW");
MenuModule, MenuModule,
FormFieldModule, FormFieldModule,
SubmitButtonModule, SubmitButtonModule,
PasswordStrengthComponent,
PaymentComponent, PaymentComponent,
TaxInfoComponent, TaxInfoComponent,
], ],

View File

@@ -53,7 +53,6 @@
required required
appInputVerbatim appInputVerbatim
autocomplete="new-password" autocomplete="new-password"
(input)="updatePasswordStrength()"
/> />
<div class="input-group-append"> <div class="input-group-append">
<button <button
@@ -78,7 +77,7 @@
</button> </button>
</div> </div>
</div> </div>
<app-password-strength [score]="masterPasswordScore" [showText]="true"> <app-password-strength [password]="newPassword" [email]="email" [showText]="true">
</app-password-strength> </app-password-strength>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,6 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from "@angular/core";
import { PasswordStrengthComponent } from "@bitwarden/angular/shared/components/password-strength/password-strength.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
@@ -22,13 +23,13 @@ export class ResetPasswordComponent implements OnInit {
@Input() id: string; @Input() id: string;
@Input() organizationId: string; @Input() organizationId: string;
@Output() onPasswordReset = new EventEmitter(); @Output() onPasswordReset = new EventEmitter();
@ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent;
enforcedPolicyOptions: MasterPasswordPolicyOptions; enforcedPolicyOptions: MasterPasswordPolicyOptions;
newPassword: string = null; newPassword: string = null;
showPassword = false; showPassword = false;
masterPasswordScore: number; masterPasswordScore: number;
formPromise: Promise<any>; formPromise: Promise<any>;
private newPasswordStrengthTimeout: any;
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
@@ -52,7 +53,7 @@ export class ResetPasswordComponent implements OnInit {
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.newPassword = await this.passwordGenerationService.generatePassword(options);
this.updatePasswordStrength(); this.passwordStrengthComponent.updatePasswordStrength(this.newPassword);
} }
togglePassword() { togglePassword() {
@@ -183,35 +184,4 @@ export class ResetPasswordComponent implements OnInit {
this.logService.error(e); this.logService.error(e);
} }
} }
updatePasswordStrength() {
if (this.newPasswordStrengthTimeout != null) {
clearTimeout(this.newPasswordStrengthTimeout);
}
this.newPasswordStrengthTimeout = setTimeout(() => {
const strengthResult = this.passwordGenerationService.passwordStrength(
this.newPassword,
this.getPasswordStrengthUserInput()
);
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
}, 300);
}
private getPasswordStrengthUserInput() {
let userInput: string[] = [];
const atPosition = this.email.indexOf("@");
if (atPosition > -1) {
userInput = userInput.concat(
this.email
.substr(0, atPosition)
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/)
);
}
if (this.name != null && this.name !== "") {
userInput = userInput.concat(this.name.trim().toLowerCase().split(" "));
}
return userInput;
}
} }

View File

@@ -37,15 +37,17 @@
name="NewMasterPasswordHash" name="NewMasterPasswordHash"
class="form-control mb-1" class="form-control mb-1"
[(ngModel)]="masterPassword" [(ngModel)]="masterPassword"
(input)="updatePasswordStrength()"
required required
appInputVerbatim appInputVerbatim
autocomplete="new-password" autocomplete="new-password"
/> />
<app-password-strength <app-password-strength
[score]="masterPasswordScore" [password]="masterPassword"
[email]="email"
[showText]="true" [showText]="true"
></app-password-strength> (passwordStrengthResult)="getStrengthResult($event)"
>
</app-password-strength>
</div> </div>
</div> </div>
<div class="col-6"> <div class="col-6">

View File

@@ -39,12 +39,16 @@
name="NewMasterPasswordHash" name="NewMasterPasswordHash"
class="form-control mb-1" class="form-control mb-1"
[(ngModel)]="masterPassword" [(ngModel)]="masterPassword"
(input)="updatePasswordStrength()"
required required
appInputVerbatim appInputVerbatim
autocomplete="new-password" autocomplete="new-password"
/> />
<app-password-strength [score]="masterPasswordScore" [showText]="true"> <app-password-strength
[password]="masterPassword"
[email]="email"
[showText]="true"
(passwordStrengthResult)="getStrengthResult($event)"
>
</app-password-strength> </app-password-strength>
</div> </div>
</div> </div>

View File

@@ -12,20 +12,22 @@ import { EncString } from "@bitwarden/common/models/domain/encString";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/models/domain/masterPasswordPolicyOptions"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/models/domain/masterPasswordPolicyOptions";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey"; import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
import { PasswordColorText } from "../shared/components/password-strength/password-strength.component";
@Directive() @Directive()
export class ChangePasswordComponent implements OnInit { export class ChangePasswordComponent implements OnInit {
masterPassword: string; masterPassword: string;
masterPasswordRetype: string; masterPasswordRetype: string;
formPromise: Promise<any>; formPromise: Promise<any>;
masterPasswordScore: number;
enforcedPolicyOptions: MasterPasswordPolicyOptions; enforcedPolicyOptions: MasterPasswordPolicyOptions;
passwordStrengthResult: any;
color: string;
text: string;
protected email: string; protected email: string;
protected kdf: KdfType; protected kdf: KdfType;
protected kdfIterations: number; protected kdfIterations: number;
private masterPasswordStrengthTimeout: any;
constructor( constructor(
protected i18nService: I18nService, protected i18nService: I18nService,
protected cryptoService: CryptoService, protected cryptoService: CryptoService,
@@ -116,10 +118,7 @@ export class ChangePasswordComponent implements OnInit {
return false; return false;
} }
const strengthResult = this.passwordGenerationService.passwordStrength( const strengthResult = this.passwordStrengthResult;
this.masterPassword,
this.getPasswordStrengthUserInput()
);
if ( if (
this.enforcedPolicyOptions != null && this.enforcedPolicyOptions != null &&
@@ -153,19 +152,6 @@ export class ChangePasswordComponent implements OnInit {
return true; return true;
} }
updatePasswordStrength() {
if (this.masterPasswordStrengthTimeout != null) {
clearTimeout(this.masterPasswordStrengthTimeout);
}
this.masterPasswordStrengthTimeout = setTimeout(() => {
const strengthResult = this.passwordGenerationService.passwordStrength(
this.masterPassword,
this.getPasswordStrengthUserInput()
);
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
}, 300);
}
async logOut() { async logOut() {
const confirmed = await this.platformUtilsService.showDialog( const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("logOutConfirmation"), this.i18nService.t("logOutConfirmation"),
@@ -178,18 +164,12 @@ export class ChangePasswordComponent implements OnInit {
} }
} }
private getPasswordStrengthUserInput() { getStrengthResult(result: any) {
let userInput: string[] = []; this.passwordStrengthResult = result;
const atPosition = this.email.indexOf("@"); }
if (atPosition > -1) {
userInput = userInput.concat( getPasswordScoreText(event: PasswordColorText) {
this.email this.color = event.color;
.substr(0, atPosition) this.text = event.text;
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/)
);
}
return userInput;
} }
} }

View File

@@ -22,6 +22,8 @@ import { KeysRequest } from "@bitwarden/common/models/request/keysRequest";
import { ReferenceEventRequest } from "@bitwarden/common/models/request/referenceEventRequest"; import { ReferenceEventRequest } from "@bitwarden/common/models/request/referenceEventRequest";
import { RegisterRequest } from "@bitwarden/common/models/request/registerRequest"; import { RegisterRequest } from "@bitwarden/common/models/request/registerRequest";
import { PasswordColorText } from "../shared/components/password-strength/password-strength.component";
import { CaptchaProtectedComponent } from "./captchaProtected.component"; import { CaptchaProtectedComponent } from "./captchaProtected.component";
@Directive() @Directive()
@@ -31,10 +33,12 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
showPassword = false; showPassword = false;
formPromise: Promise<any>; formPromise: Promise<any>;
masterPasswordScore: number;
referenceData: ReferenceEventRequest; referenceData: ReferenceEventRequest;
showTerms = true; showTerms = true;
showErrorSummary = false; showErrorSummary = false;
passwordStrengthResult: any;
color: string;
text: string;
formGroup = this.formBuilder.group( formGroup = this.formBuilder.group(
{ {
@@ -63,7 +67,6 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
); );
protected successRoute = "login"; protected successRoute = "login";
private masterPasswordStrengthTimeout: any;
constructor( constructor(
protected formValidationErrorService: FormValidationErrorsService, protected formValidationErrorService: FormValidationErrorsService,
@@ -87,36 +90,6 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
this.setupCaptcha(); this.setupCaptcha();
} }
get masterPasswordScoreWidth() {
return this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
}
get masterPasswordScoreColor() {
switch (this.masterPasswordScore) {
case 4:
return "success";
case 3:
return "primary";
case 2:
return "warning";
default:
return "danger";
}
}
get masterPasswordScoreText() {
switch (this.masterPasswordScore) {
case 4:
return this.i18nService.t("strong");
case 3:
return this.i18nService.t("good");
case 2:
return this.i18nService.t("weak");
default:
return this.masterPasswordScore != null ? this.i18nService.t("weak") : null;
}
}
async submit(showToast = true) { async submit(showToast = true) {
let email = this.formGroup.get("email")?.value; let email = this.formGroup.get("email")?.value;
let name = this.formGroup.get("name")?.value; let name = this.formGroup.get("name")?.value;
@@ -147,11 +120,7 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
return; return;
} }
const strengthResult = this.passwordGenerationService.passwordStrength( if (this.passwordStrengthResult != null && this.passwordStrengthResult.score < 3) {
masterPassword,
this.getPasswordStrengthUserInput()
);
if (strengthResult != null && strengthResult.score < 3) {
const result = await this.platformUtilsService.showDialog( const result = await this.platformUtilsService.showDialog(
this.i18nService.t("weakMasterPasswordDesc"), this.i18nService.t("weakMasterPasswordDesc"),
this.i18nService.t("weakMasterPassword"), this.i18nService.t("weakMasterPassword"),
@@ -235,39 +204,13 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
this.showPassword = !this.showPassword; this.showPassword = !this.showPassword;
} }
updatePasswordStrength() { getStrengthResult(result: any) {
const masterPassword = this.formGroup.get("masterPassword")?.value; this.passwordStrengthResult = result;
if (this.masterPasswordStrengthTimeout != null) {
clearTimeout(this.masterPasswordStrengthTimeout);
}
this.masterPasswordStrengthTimeout = setTimeout(() => {
const strengthResult = this.passwordGenerationService.passwordStrength(
masterPassword,
this.getPasswordStrengthUserInput()
);
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
}, 300);
} }
private getPasswordStrengthUserInput() { getPasswordScoreText(event: PasswordColorText) {
let userInput: string[] = []; this.color = event.color;
const email = this.formGroup.get("email")?.value; this.text = event.text;
const name = this.formGroup.get("name").value;
const atPosition = email.indexOf("@");
if (atPosition > -1) {
userInput = userInput.concat(
email
.substr(0, atPosition)
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/)
);
}
if (name != null && name !== "") {
userInput = userInput.concat(name.trim().toLowerCase().split(" "));
}
return userInput;
} }
private getErrorToastMessage() { private getErrorToastMessage() {

View File

@@ -29,6 +29,7 @@ import { I18nPipe } from "./pipes/i18n.pipe";
import { SearchCiphersPipe } from "./pipes/search-ciphers.pipe"; import { SearchCiphersPipe } from "./pipes/search-ciphers.pipe";
import { SearchPipe } from "./pipes/search.pipe"; import { SearchPipe } from "./pipes/search.pipe";
import { UserNamePipe } from "./pipes/user-name.pipe"; import { UserNamePipe } from "./pipes/user-name.pipe";
import { PasswordStrengthComponent } from "./shared/components/password-strength/password-strength.component";
@NgModule({ @NgModule({
imports: [ imports: [
@@ -68,6 +69,7 @@ import { UserNamePipe } from "./pipes/user-name.pipe";
StopPropDirective, StopPropDirective,
TrueFalseValueDirective, TrueFalseValueDirective,
UserNamePipe, UserNamePipe,
PasswordStrengthComponent,
], ],
exports: [ exports: [
A11yInvalidDirective, A11yInvalidDirective,
@@ -97,6 +99,7 @@ import { UserNamePipe } from "./pipes/user-name.pipe";
StopPropDirective, StopPropDirective,
TrueFalseValueDirective, TrueFalseValueDirective,
UserNamePipe, UserNamePipe,
PasswordStrengthComponent,
], ],
providers: [CreditCardNumberPipe, DatePipe, I18nPipe, SearchPipe, UserNamePipe], providers: [CreditCardNumberPipe, DatePipe, I18nPipe, SearchPipe, UserNamePipe],
}) })

View File

@@ -0,0 +1,133 @@
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from "@angular/core";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
export interface PasswordColorText {
color: string;
text: string;
}
@Component({
selector: "app-password-strength",
templateUrl: "password-strength.component.html",
})
export class PasswordStrengthComponent implements OnChanges {
@Input() showText = false;
@Input() email: string;
@Input() password: string;
@Input() name: string;
@Output() passwordStrengthResult = new EventEmitter<any>();
@Output() passwordScoreColor = new EventEmitter<PasswordColorText>();
masterPasswordScore: number;
scoreWidth = 0;
color = "bg-danger";
text: string;
private masterPasswordStrengthTimeout: any;
//used by desktop and browser to display strength text color
get masterPasswordScoreColor() {
switch (this.masterPasswordScore) {
case 4:
return "success";
case 3:
return "primary";
case 2:
return "warning";
default:
return "danger";
}
}
//used by desktop and browser to display strength text
get masterPasswordScoreText() {
switch (this.masterPasswordScore) {
case 4:
return this.i18nService.t("strong");
case 3:
return this.i18nService.t("good");
case 2:
return this.i18nService.t("weak");
default:
return this.masterPasswordScore != null ? this.i18nService.t("weak") : null;
}
}
constructor(
private i18nService: I18nService,
private passwordGenerationService: PasswordGenerationService
) {}
ngOnChanges(changes: SimpleChanges): void {
this.masterPasswordStrengthTimeout = setTimeout(() => {
this.updatePasswordStrength(changes.password?.currentValue);
this.scoreWidth = this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
switch (this.masterPasswordScore) {
case 4:
this.color = "bg-success";
this.text = this.i18nService.t("strong");
break;
case 3:
this.color = "bg-primary";
this.text = this.i18nService.t("good");
break;
case 2:
this.color = "bg-warning";
this.text = this.i18nService.t("weak");
break;
default:
this.color = "bg-danger";
this.text = this.masterPasswordScore != null ? this.i18nService.t("weak") : null;
break;
}
this.setPasswordScoreText(this.color, this.text);
}, 100);
}
updatePasswordStrength(password: string) {
const masterPassword = password;
if (this.masterPasswordStrengthTimeout != null) {
clearTimeout(this.masterPasswordStrengthTimeout);
}
const strengthResult = this.passwordGenerationService.passwordStrength(
masterPassword,
this.getPasswordStrengthUserInput()
);
this.passwordStrengthResult.emit(strengthResult);
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
}
getPasswordStrengthUserInput() {
let userInput: string[] = [];
const email = this.email;
const name = this.name;
const atPosition = email.indexOf("@");
if (atPosition > -1) {
userInput = userInput.concat(
email
.substr(0, atPosition)
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/)
);
}
if (name != null && name !== "") {
userInput = userInput.concat(name.trim().toLowerCase().split(" "));
}
return userInput;
}
setPasswordScoreText(color: string, text: string) {
color = color.slice(3);
this.passwordScoreColor.emit({ color: color, text: text });
}
}