Skip to content

Commit

Permalink
[PM-5085] Create InputPasswordComponent (#9630)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rr-bw authored Jun 17, 2024
1 parent 7561590 commit 2a0e21b
Show file tree
Hide file tree
Showing 8 changed files with 534 additions and 3 deletions.
13 changes: 13 additions & 0 deletions apps/browser/src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@
"masterPassHintDesc": {
"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": {
"message": "Re-type master password"
},
Expand Down
13 changes: 13 additions & 0 deletions apps/desktop/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,19 @@
"masterPassHint": {
"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": {
"message": "Settings"
},
Expand Down
13 changes: 13 additions & 0 deletions apps/web/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,19 @@
"masterPassHintLabel": {
"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": {
"message": "Settings"
},
Expand Down
116 changes: 113 additions & 3 deletions libs/angular/src/auth/validators/inputs-field-match.validator.ts
Original file line number Diff line number Diff line change
@@ -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";

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 {
return (control: AbstractControl) => {
if (control.parent && control.parent.controls) {
Expand Down Expand Up @@ -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) {
return (formGroup: UntypedFormGroup) => {
const fieldCtrl = formGroup.controls[field];
Expand All @@ -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;
}
};
}
}
1 change: 1 addition & 0 deletions libs/auth/src/angular/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from "./icons";
export * from "./anon-layout/anon-layout.component";
export * from "./anon-layout/anon-layout-wrapper.component";
export * from "./fingerprint-dialog/fingerprint-dialog.component";
export * from "./input-password/input-password.component";
export * from "./password-callout/password-callout.component";

// user verification
Expand Down
73 changes: 73 additions & 0 deletions libs/auth/src/angular/input-password/input-password.component.html
Original file line number Diff line number Diff line change
@@ -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>
Loading

0 comments on commit 2a0e21b

Please sign in to comment.