mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 06:43:35 +00:00
[PM-5085] Create InputPasswordComponent (#9630)
* setup for InputPasswordComponent and basic story * add all input fields * add translated error messages * update validation * add password-callout * update hint text * use PolicyService in component * setup SetPasswordComponent * remove div * add default button text * add mocks for InputPassword storybook * simplify ngOnInit * change param and use PolicyApiService * check for breaches and validate against policy * user toastService * use useValue for mocks * hash before emitting * validation cleanup and use PreloadedEnglishI18nModule * add ngOnDestroy * create validateFormInputsDoNotMatch fn * update validateFormInputsComparison and add deprecation jsdocs * rename validator fn * fix bugs in validation fn * cleanup and re-introduce services/logic * toggle password inputs together * update hint help text * remove SetPassword test * remove master key creation / hashing * add translations to browser/desktop * mock basic password-strength functionality * add check for controls * hash before emitting * type the EventEmitter * use DEFAULT_KDF_CONFIG * emit master key * clarify comment * update password mininum help text to match org policy requirement
This commit is contained in:
@@ -49,6 +49,19 @@
|
|||||||
"masterPassHintDesc": {
|
"masterPassHintDesc": {
|
||||||
"message": "A master password hint can help you remember your password if you forget it."
|
"message": "A master password hint can help you remember your password if you forget it."
|
||||||
},
|
},
|
||||||
|
"masterPassHintText": {
|
||||||
|
"message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.",
|
||||||
|
"placeholders": {
|
||||||
|
"current": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "0"
|
||||||
|
},
|
||||||
|
"maximum": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "50"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"reTypeMasterPass": {
|
"reTypeMasterPass": {
|
||||||
"message": "Re-type master password"
|
"message": "Re-type master password"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -526,6 +526,19 @@
|
|||||||
"masterPassHint": {
|
"masterPassHint": {
|
||||||
"message": "Master password hint (optional)"
|
"message": "Master password hint (optional)"
|
||||||
},
|
},
|
||||||
|
"masterPassHintText": {
|
||||||
|
"message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.",
|
||||||
|
"placeholders": {
|
||||||
|
"current": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "0"
|
||||||
|
},
|
||||||
|
"maximum": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "50"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"message": "Settings"
|
"message": "Settings"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -767,6 +767,19 @@
|
|||||||
"masterPassHintLabel": {
|
"masterPassHintLabel": {
|
||||||
"message": "Master password hint"
|
"message": "Master password hint"
|
||||||
},
|
},
|
||||||
|
"masterPassHintText": {
|
||||||
|
"message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.",
|
||||||
|
"placeholders": {
|
||||||
|
"current": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "0"
|
||||||
|
},
|
||||||
|
"maximum": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "50"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"message": "Settings"
|
"message": "Settings"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { AbstractControl, UntypedFormGroup, ValidatorFn } from "@angular/forms";
|
import { AbstractControl, UntypedFormGroup, ValidationErrors, ValidatorFn } from "@angular/forms";
|
||||||
|
|
||||||
import { FormGroupControls } from "../../platform/abstractions/form-validation-errors.service";
|
import { FormGroupControls } from "../../platform/abstractions/form-validation-errors.service";
|
||||||
|
|
||||||
export class InputsFieldMatch {
|
export class InputsFieldMatch {
|
||||||
//check to ensure two fields do not have the same value
|
/**
|
||||||
|
* Check to ensure two fields do not have the same value
|
||||||
|
*
|
||||||
|
* @deprecated Use compareInputs() instead
|
||||||
|
*/
|
||||||
static validateInputsDoesntMatch(matchTo: string, errorMessage: string): ValidatorFn {
|
static validateInputsDoesntMatch(matchTo: string, errorMessage: string): ValidatorFn {
|
||||||
return (control: AbstractControl) => {
|
return (control: AbstractControl) => {
|
||||||
if (control.parent && control.parent.controls) {
|
if (control.parent && control.parent.controls) {
|
||||||
@@ -37,7 +41,18 @@ export class InputsFieldMatch {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//checks the formGroup if two fields have the same value and validation is controlled from either field
|
/**
|
||||||
|
* Checks the formGroup if two fields have the same value and validation is controlled from either field
|
||||||
|
*
|
||||||
|
* @deprecated
|
||||||
|
* Use compareInputs() instead.
|
||||||
|
*
|
||||||
|
* For more info on deprecation
|
||||||
|
* - Do not use untyped `options` object in formBuilder.group() {@link https://angular.dev/api/forms/UntypedFormBuilder}
|
||||||
|
* - Use formBuilder.group() overload with AbstractControlOptions type instead {@link https://angular.dev/api/forms/AbstractControlOptions}
|
||||||
|
*
|
||||||
|
* Remove this method after deprecated instances are replaced
|
||||||
|
*/
|
||||||
static validateFormInputsMatch(field: string, fieldMatchTo: string, errorMessage: string) {
|
static validateFormInputsMatch(field: string, fieldMatchTo: string, errorMessage: string) {
|
||||||
return (formGroup: UntypedFormGroup) => {
|
return (formGroup: UntypedFormGroup) => {
|
||||||
const fieldCtrl = formGroup.controls[field];
|
const fieldCtrl = formGroup.controls[field];
|
||||||
@@ -54,4 +69,99 @@ export class InputsFieldMatch {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether two form controls do or do not have the same input value (except for empty string values).
|
||||||
|
*
|
||||||
|
* - Validation is controlled from either form control.
|
||||||
|
* - The error message is displayed under controlB by default, but can be set to controlA.
|
||||||
|
*
|
||||||
|
* @param validationGoal Whether you want to verify that the form control input values match or do not match
|
||||||
|
* @param controlNameA The name of the first form control to compare.
|
||||||
|
* @param controlNameB The name of the second form control to compare.
|
||||||
|
* @param errorMessage The error message to display if there is an error. This will probably
|
||||||
|
* be an i18n translated string.
|
||||||
|
* @param showErrorOn The control under which you want to display the error (default is controlB).
|
||||||
|
*/
|
||||||
|
static compareInputs(
|
||||||
|
validationGoal: "match" | "doNotMatch",
|
||||||
|
controlNameA: string,
|
||||||
|
controlNameB: string,
|
||||||
|
errorMessage: string,
|
||||||
|
showErrorOn: "controlA" | "controlB" = "controlB",
|
||||||
|
): ValidatorFn {
|
||||||
|
return (control: AbstractControl): ValidationErrors | null => {
|
||||||
|
const controlA = control.get(controlNameA);
|
||||||
|
const controlB = control.get(controlNameB);
|
||||||
|
|
||||||
|
if (!controlA || !controlB) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const controlThatShowsError = showErrorOn === "controlA" ? controlA : controlB;
|
||||||
|
|
||||||
|
// Don't compare empty strings
|
||||||
|
if (controlA.value === "" && controlB.value === "") {
|
||||||
|
return pass();
|
||||||
|
}
|
||||||
|
|
||||||
|
const controlValuesMatch = controlA.value === controlB.value;
|
||||||
|
|
||||||
|
if (validationGoal === "match") {
|
||||||
|
if (controlValuesMatch) {
|
||||||
|
return pass();
|
||||||
|
} else {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validationGoal === "doNotMatch") {
|
||||||
|
if (!controlValuesMatch) {
|
||||||
|
return pass();
|
||||||
|
} else {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // default return
|
||||||
|
|
||||||
|
function fail() {
|
||||||
|
controlThatShowsError.setErrors({
|
||||||
|
// Preserve any pre-existing errors
|
||||||
|
...controlThatShowsError.errors,
|
||||||
|
// Add new inputMatchError
|
||||||
|
inputMatchError: {
|
||||||
|
message: errorMessage,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputMatchError: {
|
||||||
|
message: errorMessage,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function pass(): null {
|
||||||
|
// Get the current errors object
|
||||||
|
const errorsObj = controlThatShowsError?.errors;
|
||||||
|
|
||||||
|
if (errorsObj != null) {
|
||||||
|
// Remove any inputMatchError if it exists, since that is the sole error we are targeting with this validator
|
||||||
|
if (errorsObj?.inputMatchError) {
|
||||||
|
delete errorsObj.inputMatchError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the errorsObj is now empty
|
||||||
|
const isEmptyObj = Object.keys(errorsObj).length === 0;
|
||||||
|
|
||||||
|
// If the errorsObj is empty, set errors to null, otherwise set the errors to an object of pre-existing errors (other than inputMatchError)
|
||||||
|
controlThatShowsError.setErrors(isEmptyObj ? null : errorsObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return null for this validator
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export * from "./icons";
|
|||||||
export * from "./anon-layout/anon-layout.component";
|
export * from "./anon-layout/anon-layout.component";
|
||||||
export * from "./anon-layout/anon-layout-wrapper.component";
|
export * from "./anon-layout/anon-layout-wrapper.component";
|
||||||
export * from "./fingerprint-dialog/fingerprint-dialog.component";
|
export * from "./fingerprint-dialog/fingerprint-dialog.component";
|
||||||
|
export * from "./input-password/input-password.component";
|
||||||
export * from "./password-callout/password-callout.component";
|
export * from "./password-callout/password-callout.component";
|
||||||
|
|
||||||
// user verification
|
// user verification
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||||
|
<auth-password-callout
|
||||||
|
*ngIf="masterPasswordPolicy"
|
||||||
|
[policy]="masterPasswordPolicy"
|
||||||
|
></auth-password-callout>
|
||||||
|
|
||||||
|
<div class="tw-mb-6">
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "masterPassword" | i18n }}</bit-label>
|
||||||
|
<input
|
||||||
|
id="input-password-form_password"
|
||||||
|
bitInput
|
||||||
|
type="password"
|
||||||
|
formControlName="password"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitIconButton
|
||||||
|
bitSuffix
|
||||||
|
bitPasswordInputToggle
|
||||||
|
[(toggled)]="showPassword"
|
||||||
|
></button>
|
||||||
|
<bit-hint>
|
||||||
|
<span class="tw-font-bold">{{ "important" | i18n }} </span>
|
||||||
|
{{ "masterPassImportant" | i18n }}
|
||||||
|
{{ minPasswordMsg }}.
|
||||||
|
</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
|
||||||
|
<app-password-strength
|
||||||
|
[password]="formGroup.controls.password.value"
|
||||||
|
[email]="email"
|
||||||
|
[showText]="true"
|
||||||
|
(passwordStrengthResult)="getPasswordStrengthResult($event)"
|
||||||
|
></app-password-strength>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "confirmMasterPassword" | i18n }}</bit-label>
|
||||||
|
<input
|
||||||
|
id="input-password-form_confirmed-password"
|
||||||
|
bitInput
|
||||||
|
type="password"
|
||||||
|
formControlName="confirmedPassword"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitIconButton
|
||||||
|
bitSuffix
|
||||||
|
bitPasswordInputToggle
|
||||||
|
[(toggled)]="showPassword"
|
||||||
|
></button>
|
||||||
|
</bit-form-field>
|
||||||
|
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "masterPassHintLabel" | i18n }}</bit-label>
|
||||||
|
<input bitInput formControlName="hint" />
|
||||||
|
<bit-hint>
|
||||||
|
{{ "masterPassHintText" | i18n: formGroup.value.hint.length : maxHintLength.toString() }}
|
||||||
|
</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
|
||||||
|
<bit-form-control>
|
||||||
|
<input type="checkbox" bitCheckbox formControlName="checkForBreaches" />
|
||||||
|
<bit-label>{{ "checkForBreaches" | i18n }}</bit-label>
|
||||||
|
</bit-form-control>
|
||||||
|
|
||||||
|
<button type="submit" bitButton bitFormButton buttonType="primary" [block]="true">
|
||||||
|
{{ buttonText || ("setMasterPassword" | i18n) }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<bit-error-summary *ngIf="showErrorSummary" [formGroup]="formGroup"></bit-error-summary>
|
||||||
|
</form>
|
||||||
192
libs/auth/src/angular/input-password/input-password.component.ts
Normal file
192
libs/auth/src/angular/input-password/input-password.component.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||||
|
import { ReactiveFormsModule, FormBuilder, Validators } from "@angular/forms";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||||
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.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 { PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { MasterKey } from "@bitwarden/common/types/key";
|
||||||
|
import {
|
||||||
|
AsyncActionsModule,
|
||||||
|
ButtonModule,
|
||||||
|
CheckboxModule,
|
||||||
|
DialogService,
|
||||||
|
FormFieldModule,
|
||||||
|
IconButtonModule,
|
||||||
|
InputModule,
|
||||||
|
ToastService,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { InputsFieldMatch } from "../../../../angular/src/auth/validators/inputs-field-match.validator";
|
||||||
|
import { SharedModule } from "../../../../components/src/shared";
|
||||||
|
import { PasswordCalloutComponent } from "../password-callout/password-callout.component";
|
||||||
|
|
||||||
|
export interface PasswordInputResult {
|
||||||
|
masterKey: MasterKey;
|
||||||
|
masterKeyHash: string;
|
||||||
|
kdfConfig: PBKDF2KdfConfig;
|
||||||
|
hint: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: "auth-input-password",
|
||||||
|
templateUrl: "./input-password.component.html",
|
||||||
|
imports: [
|
||||||
|
AsyncActionsModule,
|
||||||
|
ButtonModule,
|
||||||
|
CheckboxModule,
|
||||||
|
FormFieldModule,
|
||||||
|
IconButtonModule,
|
||||||
|
InputModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
SharedModule,
|
||||||
|
PasswordCalloutComponent,
|
||||||
|
JslibModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class InputPasswordComponent implements OnInit {
|
||||||
|
@Output() onPasswordFormSubmit = new EventEmitter<PasswordInputResult>();
|
||||||
|
|
||||||
|
@Input({ required: true }) email: string;
|
||||||
|
@Input() protected buttonText: string;
|
||||||
|
@Input() private orgId: string;
|
||||||
|
|
||||||
|
private minHintLength = 0;
|
||||||
|
protected maxHintLength = 50;
|
||||||
|
|
||||||
|
protected minPasswordLength = Utils.minimumPasswordLength;
|
||||||
|
protected minPasswordMsg = "";
|
||||||
|
protected masterPasswordPolicy: MasterPasswordPolicyOptions;
|
||||||
|
protected passwordStrengthResult: any;
|
||||||
|
protected showErrorSummary = false;
|
||||||
|
protected showPassword = false;
|
||||||
|
|
||||||
|
protected formGroup = this.formBuilder.group(
|
||||||
|
{
|
||||||
|
password: ["", [Validators.required, Validators.minLength(this.minPasswordLength)]],
|
||||||
|
confirmedPassword: ["", Validators.required],
|
||||||
|
hint: [
|
||||||
|
"", // must be string (not null) because we check length in validation
|
||||||
|
[Validators.minLength(this.minHintLength), Validators.maxLength(this.maxHintLength)],
|
||||||
|
],
|
||||||
|
checkForBreaches: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validators: [
|
||||||
|
InputsFieldMatch.compareInputs(
|
||||||
|
"match",
|
||||||
|
"password",
|
||||||
|
"confirmedPassword",
|
||||||
|
this.i18nService.t("masterPassDoesntMatch"),
|
||||||
|
),
|
||||||
|
InputsFieldMatch.compareInputs(
|
||||||
|
"doNotMatch",
|
||||||
|
"password",
|
||||||
|
"hint",
|
||||||
|
this.i18nService.t("hintEqualsPassword"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private auditService: AuditService,
|
||||||
|
private cryptoService: CryptoService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private policyService: PolicyService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private policyApiService: PolicyApiServiceAbstraction,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.masterPasswordPolicy = await this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(
|
||||||
|
this.orgId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.masterPasswordPolicy != null && this.masterPasswordPolicy.minLength > 0) {
|
||||||
|
this.minPasswordMsg = this.i18nService.t(
|
||||||
|
"characterMinimum",
|
||||||
|
this.masterPasswordPolicy.minLength,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.minPasswordMsg = this.i18nService.t("characterMinimum", this.minPasswordLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPasswordStrengthResult(result: any) {
|
||||||
|
this.passwordStrengthResult = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected submit = async () => {
|
||||||
|
this.formGroup.markAllAsTouched();
|
||||||
|
|
||||||
|
if (this.formGroup.invalid) {
|
||||||
|
this.showErrorSummary = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const password = this.formGroup.controls.password.value;
|
||||||
|
|
||||||
|
// Check if password is breached (if breached, user chooses to accept and continue or not)
|
||||||
|
const passwordIsBreached =
|
||||||
|
this.formGroup.controls.checkForBreaches.value &&
|
||||||
|
(await this.auditService.passwordLeaked(password));
|
||||||
|
|
||||||
|
if (passwordIsBreached) {
|
||||||
|
const userAcceptedDialog = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "exposedMasterPassword" },
|
||||||
|
content: { key: "exposedMasterPasswordDesc" },
|
||||||
|
type: "warning",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userAcceptedDialog) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if password meets org policy requirements
|
||||||
|
if (
|
||||||
|
this.masterPasswordPolicy != null &&
|
||||||
|
!this.policyService.evaluateMasterPassword(
|
||||||
|
this.passwordStrengthResult.score,
|
||||||
|
password,
|
||||||
|
this.masterPasswordPolicy,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: this.i18nService.t("errorOccurred"),
|
||||||
|
message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and hash new master key
|
||||||
|
const kdfConfig = DEFAULT_KDF_CONFIG;
|
||||||
|
|
||||||
|
const masterKey = await this.cryptoService.makeMasterKey(
|
||||||
|
password,
|
||||||
|
this.email.trim().toLowerCase(),
|
||||||
|
kdfConfig,
|
||||||
|
);
|
||||||
|
|
||||||
|
const masterKeyHash = await this.cryptoService.hashMasterKey(password, masterKey);
|
||||||
|
|
||||||
|
this.onPasswordFormSubmit.emit({
|
||||||
|
masterKey,
|
||||||
|
masterKeyHash,
|
||||||
|
kdfConfig,
|
||||||
|
hint: this.formGroup.controls.hint.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
116
libs/auth/src/angular/input-password/input-password.stories.ts
Normal file
116
libs/auth/src/angular/input-password/input-password.stories.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { importProvidersFrom } from "@angular/core";
|
||||||
|
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
|
import { action } from "@storybook/addon-actions";
|
||||||
|
import { Meta, StoryObj, applicationConfig } from "@storybook/angular";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
import { ZXCVBNResult } from "zxcvbn";
|
||||||
|
|
||||||
|
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||||
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.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 { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { PreloadedEnglishI18nModule } from "../../../../../apps/web/src/app/core/tests";
|
||||||
|
|
||||||
|
import { InputPasswordComponent } from "./input-password.component";
|
||||||
|
|
||||||
|
const mockMasterPasswordPolicyOptions = {
|
||||||
|
minComplexity: 4,
|
||||||
|
minLength: 14,
|
||||||
|
requireUpper: true,
|
||||||
|
requireLower: true,
|
||||||
|
requireNumbers: true,
|
||||||
|
requireSpecial: true,
|
||||||
|
} as MasterPasswordPolicyOptions;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Auth/Input Password",
|
||||||
|
component: InputPasswordComponent,
|
||||||
|
decorators: [
|
||||||
|
applicationConfig({
|
||||||
|
providers: [
|
||||||
|
importProvidersFrom(PreloadedEnglishI18nModule),
|
||||||
|
importProvidersFrom(BrowserAnimationsModule),
|
||||||
|
{
|
||||||
|
provide: AuditService,
|
||||||
|
useValue: {
|
||||||
|
passwordLeaked: () => Promise.resolve(1),
|
||||||
|
} as Partial<AuditService>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: CryptoService,
|
||||||
|
useValue: {
|
||||||
|
makeMasterKey: () => Promise.resolve("example-master-key"),
|
||||||
|
hashMasterKey: () => Promise.resolve("example-master-key-hash"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: DialogService,
|
||||||
|
useValue: {
|
||||||
|
openSimpleDialog: () => Promise.resolve(true),
|
||||||
|
} as Partial<DialogService>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PolicyApiServiceAbstraction,
|
||||||
|
useValue: {
|
||||||
|
getMasterPasswordPolicyOptsForOrgUser: () => mockMasterPasswordPolicyOptions,
|
||||||
|
} as Partial<PolicyService>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PolicyService,
|
||||||
|
useValue: {
|
||||||
|
masterPasswordPolicyOptions$: () => of(mockMasterPasswordPolicyOptions),
|
||||||
|
evaluateMasterPassword: (score) => {
|
||||||
|
if (score < 4) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
} as Partial<PolicyService>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PasswordStrengthServiceAbstraction,
|
||||||
|
useValue: {
|
||||||
|
getPasswordStrength: (password) => {
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
if (password.length === 0) {
|
||||||
|
score = null;
|
||||||
|
} else if (password.length <= 4) {
|
||||||
|
score = 1;
|
||||||
|
} else if (password.length <= 8) {
|
||||||
|
score = 2;
|
||||||
|
} else if (password.length <= 12) {
|
||||||
|
score = 3;
|
||||||
|
} else {
|
||||||
|
score = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { score } as ZXCVBNResult;
|
||||||
|
},
|
||||||
|
} as Partial<PasswordStrengthServiceAbstraction>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ToastService,
|
||||||
|
useValue: {
|
||||||
|
showToast: action("ToastService.showToast"),
|
||||||
|
} as Partial<ToastService>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<InputPasswordComponent>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<auth-input-password></auth-input-password>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user