From 9ffb732b7fd895a732c95905314d64beeb890db5 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Tue, 10 Oct 2023 17:53:00 -0700 Subject: [PATCH 01/16] add password service --- frontend/package.json | 3 ++ frontend/src/components/sign-up-form.ts | 19 ++++++++++- frontend/src/utils/PasswordService.ts | 45 +++++++++++++++++++++++++ frontend/yarn.lock | 19 ++++++++++- 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 frontend/src/utils/PasswordService.ts diff --git a/frontend/package.json b/frontend/package.json index 6f7213ccfb..9748d193f6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,9 @@ "@typescript-eslint/parser": "^5.4.0", "@wysimark/standalone": "2.2.15", "@xstate/fsm": "^1.6.2", + "@zxcvbn-ts/core": "^3.0.4", + "@zxcvbn-ts/language-common": "^3.0.4", + "@zxcvbn-ts/language-en": "^3.0.2", "autoprefixer": "^10.4.2", "axios": "^0.22.0", "broadcastchannel-polyfill": "^1.0.1", diff --git a/frontend/src/components/sign-up-form.ts b/frontend/src/components/sign-up-form.ts index 13e4ea6070..11706c752e 100644 --- a/frontend/src/components/sign-up-form.ts +++ b/frontend/src/components/sign-up-form.ts @@ -4,6 +4,9 @@ import { msg, localized } from "@lit/localize"; import LiteElement, { html } from "../utils/LiteElement"; import AuthService from "../utils/AuthService"; +import PasswordService from "../utils/PasswordService"; +import type { Input as BtrixInput } from "./input/input"; +import { PropertyValueMap } from "lit"; /** * @event submit @@ -32,6 +35,10 @@ export class SignUpForm extends LiteElement { @state() private isSubmitting: boolean = false; + protected firstUpdated() { + PasswordService.setOptions(); + } + render() { let serverError; @@ -100,10 +107,11 @@ export class SignUpForm extends LiteElement { autocomplete="new-password" passwordToggle required + @input=${this.onPasswordInput} >

- ${msg("Choose a strong password between 8-64 characters.")} + ${msg("Choose a strong password between 8 and 64 characters.")}

@@ -120,6 +128,15 @@ export class SignUpForm extends LiteElement { `; } + private async onPasswordInput(e: InputEvent) { + const input = e.target as BtrixInput; + const userInputs: string[] = []; + if (this.email) { + userInputs.push(this.email); + } + console.log(await PasswordService.checkStrength(input.value, userInputs)); + } + private async onSubmit(event: SubmitEvent) { const form = event.target as HTMLFormElement; event.preventDefault(); diff --git a/frontend/src/utils/PasswordService.ts b/frontend/src/utils/PasswordService.ts new file mode 100644 index 0000000000..fe3b2c2049 --- /dev/null +++ b/frontend/src/utils/PasswordService.ts @@ -0,0 +1,45 @@ +import { zxcvbn, zxcvbnOptions, OptionsType } from "@zxcvbn-ts/core"; + +const loadOptions = async (): Promise => { + const zxcvbnCommonPackage = await import( + /* webpackChunkName: "zxcvbnCommonPackage" */ "@zxcvbn-ts/language-common" + ); + const zxcvbnEnPackage = await import( + /* webpackChunkName: "zxcvbnEnPackage" */ "@zxcvbn-ts/language-en" + ); + + return { + dictionary: { + ...zxcvbnCommonPackage.dictionary, + ...zxcvbnEnPackage.dictionary, + }, + graphs: zxcvbnCommonPackage.adjacencyGraphs, + translations: zxcvbnEnPackage.translations, + }; +}; + +export default class PasswordService { + static options?: any; + + static async setOptions(opts?: OptionsType) { + if (!PasswordService.options) { + PasswordService.options = await loadOptions(); + } + if (opts) { + zxcvbnOptions.setOptions({ + ...PasswordService.options, + ...opts, + }); + } else { + zxcvbnOptions.setOptions(PasswordService.options); + } + } + + static async checkStrength( + password: string, + // User input to check, e.g. emails + userInputs?: string[] + ) { + return zxcvbn(password, userInputs); + } +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 8bc2e4779e..a9e9b1ad31 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1679,6 +1679,23 @@ resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== +"@zxcvbn-ts/core@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@zxcvbn-ts/core/-/core-3.0.4.tgz#c5bde72235eb6c273cec78b672bb47c0d7045cad" + integrity sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw== + dependencies: + fastest-levenshtein "1.0.16" + +"@zxcvbn-ts/language-common@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@zxcvbn-ts/language-common/-/language-common-3.0.4.tgz#fa1d2a42f8c8a589555859795da90d6b8027b7c4" + integrity sha512-viSNNnRYtc7ULXzxrQIVUNwHAPSXRtoIwy/Tq4XQQdIknBzw4vz36lQLF6mvhMlTIlpjoN/Z1GFu/fwiAlUSsw== + +"@zxcvbn-ts/language-en@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@zxcvbn-ts/language-en/-/language-en-3.0.2.tgz#162ada6b2b556444efd5a7700e70845cfde6d6ec" + integrity sha512-Zp+zL+I6Un2Bj0tRXNs6VUBq3Djt+hwTwUz4dkt2qgsQz47U0/XthZ4ULrT/RxjwJRl5LwiaKOOZeOtmixHnjg== + accepts@^1.3.5, accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -3401,7 +3418,7 @@ fast-levenshtein@^2.0.6: resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fastest-levenshtein@^1.0.12: +fastest-levenshtein@1.0.16, fastest-levenshtein@^1.0.12: version "1.0.16" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== From 2a4f466f8a10e3bed9ed6b67bfb374bb56be9eb8 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Tue, 10 Oct 2023 18:28:09 -0700 Subject: [PATCH 02/16] show password tips --- frontend/src/components/sign-up-form.ts | 59 ++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/sign-up-form.ts b/frontend/src/components/sign-up-form.ts index 11706c752e..81484f3168 100644 --- a/frontend/src/components/sign-up-form.ts +++ b/frontend/src/components/sign-up-form.ts @@ -1,6 +1,9 @@ import { state, property } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { msg, localized } from "@lit/localize"; +import debounce from "lodash/fp/debounce"; +import { when } from "lit/directives/when.js"; +import type { ZxcvbnResult } from "@zxcvbn-ts/core"; import LiteElement, { html } from "../utils/LiteElement"; import AuthService from "../utils/AuthService"; @@ -35,6 +38,9 @@ export class SignUpForm extends LiteElement { @state() private isSubmitting: boolean = false; + @state() + private pwStrengthResults: null | ZxcvbnResult = null; + protected firstUpdated() { PasswordService.setOptions(); } @@ -93,7 +99,7 @@ export class SignUpForm extends LiteElement { minlength="2" > -

+

${msg("Your name will be visible to organization collaborators.")}

@@ -110,9 +116,10 @@ export class SignUpForm extends LiteElement { @input=${this.onPasswordInput} > -

+

${msg("Choose a strong password between 8 and 64 characters.")}

+ ${when(this.pwStrengthResults, this.renderPasswordStrength)} ${serverError} @@ -128,14 +135,54 @@ export class SignUpForm extends LiteElement { `; } - private async onPasswordInput(e: InputEvent) { - const input = e.target as BtrixInput; + private renderPasswordStrength = () => { + if (!this.pwStrengthResults) return; + const { score, feedback } = this.pwStrengthResults; + console.log({ score }); + return html` +
+ ${when( + score < 3, + () => + html` +

+ ${msg("Please choose a stronger password.")} +

+ ` + )} + ${when( + feedback.warning, + () => + html`

${msg("Warning:")} ${feedback.warning}

` + )} + ${when(feedback.suggestions.length, () => + feedback.suggestions.length === 1 + ? html`

+ ${msg("Suggestion:")} ${feedback.suggestions[0]} +

` + : html`

${msg("Suggestions:")}

+
    + ${feedback.suggestions.map((text) => html`
  • ${text}
  • `)} +
` + )} +
+ `; + }; + + private onPasswordInput = debounce(100)(async (e: InputEvent) => { + const { value } = e.target as BtrixInput; + if (!value) { + this.pwStrengthResults = null; + } const userInputs: string[] = []; if (this.email) { userInputs.push(this.email); } - console.log(await PasswordService.checkStrength(input.value, userInputs)); - } + this.pwStrengthResults = await PasswordService.checkStrength( + value, + userInputs + ); + }) as any; private async onSubmit(event: SubmitEvent) { const form = event.target as HTMLFormElement; From 89c3952dfd17b0628523508b0c7f53433390e2fd Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 11 Oct 2023 11:23:03 -0700 Subject: [PATCH 03/16] show in alert --- frontend/src/components/sign-up-form.ts | 98 ++++++++++++++++++------- 1 file changed, 70 insertions(+), 28 deletions(-) diff --git a/frontend/src/components/sign-up-form.ts b/frontend/src/components/sign-up-form.ts index 81484f3168..3566796a40 100644 --- a/frontend/src/components/sign-up-form.ts +++ b/frontend/src/components/sign-up-form.ts @@ -9,7 +9,8 @@ import LiteElement, { html } from "../utils/LiteElement"; import AuthService from "../utils/AuthService"; import PasswordService from "../utils/PasswordService"; import type { Input as BtrixInput } from "./input/input"; -import { PropertyValueMap } from "lit"; + +const PASSWORD_MIN_SCORE = 3; /** * @event submit @@ -128,6 +129,8 @@ export class SignUpForm extends LiteElement { class="w-full" variant="primary" ?loading=${this.isSubmitting} + ?disabled=${!this.pwStrengthResults || + this.pwStrengthResults.score < PASSWORD_MIN_SCORE} type="submit" >${msg("Sign up")} @@ -138,34 +141,73 @@ export class SignUpForm extends LiteElement { private renderPasswordStrength = () => { if (!this.pwStrengthResults) return; const { score, feedback } = this.pwStrengthResults; - console.log({ score }); + let scoreProps = { + icon: "exclamation-triangle", + label: msg("Please choose a stronger password"), + className: "text-danger", + variant: "danger", + }; + switch (score) { + case 2: + scoreProps = { + icon: "exclamation-circle", + label: msg("Weak password"), + className: "text-warning", + variant: "warning", + }; + break; + case 3: + scoreProps = { + icon: "shield-check", + label: msg("Acceptably strong password"), + className: "text-primary", + variant: "primary", + }; + break; + case 4: + scoreProps = { + icon: "shield-fill-check", + label: msg("Very strong password"), + className: "text-success", + variant: "success", + }; + break; + default: + break; + } return html` -
- ${when( - score < 3, - () => - html` -

- ${msg("Please choose a stronger password.")} -

- ` - )} - ${when( - feedback.warning, - () => - html`

${msg("Warning:")} ${feedback.warning}

` - )} - ${when(feedback.suggestions.length, () => - feedback.suggestions.length === 1 - ? html`

- ${msg("Suggestion:")} ${feedback.suggestions[0]} -

` - : html`

${msg("Suggestions:")}

-
    - ${feedback.suggestions.map((text) => html`
  • ${text}
  • `)} -
` - )} -
+ +
+ +

${scoreProps.label}

+
+
+ ${when( + feedback.warning, + () => html`

${feedback.warning}

` + )} + ${when(feedback.suggestions.length, () => + feedback.suggestions.length === 1 + ? html`

+ ${msg("Suggestion:")} ${feedback.suggestions[0]} +

` + : html`

${msg("Suggestions:")}

+
    + ${feedback.suggestions.map( + (text) => html`
  • ${text}
  • ` + )} +
` + )} +
+
`; }; From a3d7fb20e4b6f3c161b732212ac74ab2383d4299 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 11 Oct 2023 11:23:14 -0700 Subject: [PATCH 04/16] add comments --- frontend/src/utils/PasswordService.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/PasswordService.ts b/frontend/src/utils/PasswordService.ts index fe3b2c2049..861a4d831b 100644 --- a/frontend/src/utils/PasswordService.ts +++ b/frontend/src/utils/PasswordService.ts @@ -18,9 +18,17 @@ const loadOptions = async (): Promise => { }; }; +/** + * Test and estimate password strength + */ export default class PasswordService { - static options?: any; + static options?: OptionsType; + /** + * Update zxcvbn options asynchronously + * @TODO Localize by loading different translations + * @param opts See https://zxcvbn-ts.github.io/zxcvbn/guide/options/ + */ static async setOptions(opts?: OptionsType) { if (!PasswordService.options) { PasswordService.options = await loadOptions(); @@ -35,6 +43,11 @@ export default class PasswordService { } } + /** + * @param password + * @param userInputs Array of personal data to check against + * @returns {ZxcvbnResult} See https://zxcvbn-ts.github.io/zxcvbn/guide/getting-started/#output + */ static async checkStrength( password: string, // User input to check, e.g. emails From 75a16fdac58b8df2880452aa75190f14f7da955d Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 11 Oct 2023 11:47:03 -0700 Subject: [PATCH 05/16] move alert to common component --- frontend/src/components/index.ts | 3 + frontend/src/components/pw-strength-alert.ts | 142 +++++++++++++++++++ frontend/src/components/sign-up-form.ts | 79 ++--------- 3 files changed, 154 insertions(+), 70 deletions(-) create mode 100644 frontend/src/components/pw-strength-alert.ts diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index bd629d854a..afa16078bb 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -150,6 +150,9 @@ import("./collections-add").then(({ CollectionsAdd }) => { import("./code").then(({ Code }) => { customElements.define("btrix-code", Code); }); +import("./pw-strength-alert").then(({ PasswordStrengthAlert }) => { + customElements.define("btrix-pw-strength-alert", PasswordStrengthAlert); +}); import("./search-combobox").then(({ SearchCombobox }) => { customElements.define("btrix-search-combobox", SearchCombobox); }); diff --git a/frontend/src/components/pw-strength-alert.ts b/frontend/src/components/pw-strength-alert.ts new file mode 100644 index 0000000000..277eb987e8 --- /dev/null +++ b/frontend/src/components/pw-strength-alert.ts @@ -0,0 +1,142 @@ +import { LitElement, html, css } from "lit"; +import { msg, localized } from "@lit/localize"; +import { property } from "lit/decorators.js"; +import { when } from "lit/directives/when.js"; +import type { ZxcvbnResult } from "@zxcvbn-ts/core"; + +/** + * Show results of password strength estimate + * + * Usage example: + * ```ts + * + * ``` + */ +@localized() +export class PasswordStrengthAlert extends LitElement { + @property({ type: String }) + result?: ZxcvbnResult; + + /** Minimum acceptable score */ + @property({ type: String }) + min = 0; + + static styles = css` + sl-alert::part(message) { + /* Decrease padding size: */ + --sl-spacing-large: var(--sl-spacing-small); + } + + sl-alert[variant="danger"] .icon { + color: var(--sl-color-danger-600); + } + + sl-alert[variant="warning"] .icon { + color: var(--sl-color-warning-600); + } + + sl-alert[variant="primary"] .icon { + color: var(--sl-color-primary-600); + } + + sl-alert[variant="success"] .icon { + color: var(--sl-color-success-600); + } + + p, + ul { + margin: 0; + padding: 0; + } + + ul { + list-style-position: inside; + } + + .score { + display: flex; + gap: var(--sl-spacing-x-small); + align-items: center; + } + + .icon { + font-size: var(--sl-font-size-large); + } + + .label { + color: var(--sl-color-neutral-900); + font-weight: var(--sl-font-weight-semibold); + } + + .feedback { + color: var(--sl-color-neutral-700); + margin-left: var(--sl-spacing-x-large); + } + + .text { + margin-top: var(--sl-spacing-small); + } + `; + + render() { + if (!this.result) return; + + const { score, feedback } = this.result; + let scoreProps = { + icon: "exclamation-triangle", + label: msg("Please choose a stronger password"), + variant: "danger", + }; + switch (score) { + case 2: + scoreProps = { + icon: "exclamation-circle", + label: msg("Weak password"), + variant: "warning", + }; + break; + case 3: + scoreProps = { + icon: "shield-check", + label: msg("Acceptably strong password"), + variant: "primary", + }; + break; + case 4: + scoreProps = { + icon: "shield-fill-check", + label: msg("Very strong password"), + variant: "success", + }; + break; + default: + break; + } + return html` + +
+ +

${scoreProps.label}

+
+ +
+ `; + } +} diff --git a/frontend/src/components/sign-up-form.ts b/frontend/src/components/sign-up-form.ts index 3566796a40..0e88e05692 100644 --- a/frontend/src/components/sign-up-form.ts +++ b/frontend/src/components/sign-up-form.ts @@ -61,7 +61,7 @@ export class SignUpForm extends LiteElement { return html`
-
+
${this.email ? html`
@@ -88,7 +88,7 @@ export class SignUpForm extends LiteElement { `}
-
+
{ - if (!this.pwStrengthResults) return; - const { score, feedback } = this.pwStrengthResults; - let scoreProps = { - icon: "exclamation-triangle", - label: msg("Please choose a stronger password"), - className: "text-danger", - variant: "danger", - }; - switch (score) { - case 2: - scoreProps = { - icon: "exclamation-circle", - label: msg("Weak password"), - className: "text-warning", - variant: "warning", - }; - break; - case 3: - scoreProps = { - icon: "shield-check", - label: msg("Acceptably strong password"), - className: "text-primary", - variant: "primary", - }; - break; - case 4: - scoreProps = { - icon: "shield-fill-check", - label: msg("Very strong password"), - className: "text-success", - variant: "success", - }; - break; - default: - break; - } return html` - -
- -

${scoreProps.label}

-
-
- ${when( - feedback.warning, - () => html`

${feedback.warning}

` - )} - ${when(feedback.suggestions.length, () => - feedback.suggestions.length === 1 - ? html`

- ${msg("Suggestion:")} ${feedback.suggestions[0]} -

` - : html`

${msg("Suggestions:")}

-
    - ${feedback.suggestions.map( - (text) => html`
  • ${text}
  • ` - )} -
` - )} -
-
+
+ + +
`; }; From 8d9ec8dfae53d2d15fe4ee7399cc32299fd11b46 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 11 Oct 2023 11:55:50 -0700 Subject: [PATCH 06/16] update styles --- frontend/src/components/pw-strength-alert.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/pw-strength-alert.ts b/frontend/src/components/pw-strength-alert.ts index 277eb987e8..5a0837612e 100644 --- a/frontend/src/components/pw-strength-alert.ts +++ b/frontend/src/components/pw-strength-alert.ts @@ -19,7 +19,7 @@ export class PasswordStrengthAlert extends LitElement { /** Minimum acceptable score */ @property({ type: String }) - min = 0; + min = 1; static styles = css` sl-alert::part(message) { @@ -84,7 +84,7 @@ export class PasswordStrengthAlert extends LitElement { const { score, feedback } = this.result; let scoreProps = { icon: "exclamation-triangle", - label: msg("Please choose a stronger password"), + label: msg("Very weak password"), variant: "danger", }; switch (score) { @@ -112,12 +112,16 @@ export class PasswordStrengthAlert extends LitElement { default: break; } + if (score < this.min) { + scoreProps.label = msg("Please choose a stronger password"); + } return html`

${scoreProps.label}

+ @@ -150,10 +154,11 @@ export class SignUpForm extends LiteElement { `; }; - private onPasswordInput = debounce(100)(async (e: InputEvent) => { + private onPasswordInput = debounce(150)(async (e: InputEvent) => { const { value } = e.target as BtrixInput; - if (!value) { + if (!value || value.length < PASSWORD_MINLENGTH) { this.pwStrengthResults = null; + return; } const userInputs: string[] = []; if (this.email) { From ae79f1be0ec383ad50a0236cdb783c3eb12c3eb1 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 11 Oct 2023 12:37:10 -0700 Subject: [PATCH 08/16] adjust styles --- frontend/src/components/pw-strength-alert.ts | 14 ++++++++++++++ frontend/src/components/sign-up-form.ts | 8 +++++--- frontend/src/pages/join.ts | 17 ++++++++++------- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/pw-strength-alert.ts b/frontend/src/components/pw-strength-alert.ts index 5a0837612e..f555366e1b 100644 --- a/frontend/src/components/pw-strength-alert.ts +++ b/frontend/src/components/pw-strength-alert.ts @@ -21,6 +21,10 @@ export class PasswordStrengthAlert extends LitElement { @property({ type: String }) min = 1; + /** Optimal score */ + @property({ type: String }) + optimal = 4; + static styles = css` sl-alert::part(message) { /* Decrease padding size: */ @@ -139,6 +143,16 @@ export class PasswordStrengthAlert extends LitElement { )} ` )} + ${when( + score >= this.min && score < this.optimal, + () => html` +

+ ${msg( + "Tip: To generate very strong passwords, install a built-in password manager." + )} +

+ ` + )}
`; diff --git a/frontend/src/components/sign-up-form.ts b/frontend/src/components/sign-up-form.ts index 3941c4fdf3..ad886c8e3c 100644 --- a/frontend/src/components/sign-up-form.ts +++ b/frontend/src/components/sign-up-form.ts @@ -63,7 +63,7 @@ export class SignUpForm extends LiteElement { return html` -
+
${this.email ? html`
@@ -96,14 +96,16 @@ export class SignUpForm extends LiteElement { name="name" label=${msg("Your name")} placeholder=${msg("Lisa Simpson", { - desc: "Example user's name", + desc: "Example user’s name", })} autocomplete="nickname" minlength="2" >

- ${msg("Your name will be visible to organization collaborators.")} + ${msg( + "Your full name, or another name that org collaborators will see." + )}

diff --git a/frontend/src/pages/join.ts b/frontend/src/pages/join.ts index 8ae6f71da4..82ccbdb804 100644 --- a/frontend/src/pages/join.ts +++ b/frontend/src/pages/join.ts @@ -56,11 +56,14 @@ export class Join extends LiteElement { return html`
-
- ${msg("Invited by ")} - ${this.inviteInfo.inviterName || - this.inviteInfo.inviterEmail || - placeholder} +
+ ${msg( + str`Invited by ${ + this.inviteInfo.inviterName || + this.inviteInfo.inviterEmail || + placeholder + }` + )}

${msg( @@ -69,13 +72,13 @@ export class Join extends LiteElement { >${hasInviteInfo ? this.inviteInfo.orgName || msg("Browsertrix Cloud") : placeholder}` + >.` )}

Date: Wed, 11 Oct 2023 12:41:14 -0700 Subject: [PATCH 09/16] shorter min length --- frontend/src/components/sign-up-form.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/sign-up-form.ts b/frontend/src/components/sign-up-form.ts index ad886c8e3c..126ead8ff7 100644 --- a/frontend/src/components/sign-up-form.ts +++ b/frontend/src/components/sign-up-form.ts @@ -158,7 +158,7 @@ export class SignUpForm extends LiteElement { private onPasswordInput = debounce(150)(async (e: InputEvent) => { const { value } = e.target as BtrixInput; - if (!value || value.length < PASSWORD_MINLENGTH) { + if (!value || value.length < 4) { this.pwStrengthResults = null; return; } From d7b67b88c063102a1d206934f4c0da4f5c73478c Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 11 Oct 2023 12:54:37 -0700 Subject: [PATCH 10/16] update sign up form --- frontend/src/components/input/input.ts | 3 +-- frontend/src/components/sign-up-form.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/input/input.ts b/frontend/src/components/input/input.ts index cc59b7ed3f..cc3343b102 100644 --- a/frontend/src/components/input/input.ts +++ b/frontend/src/components/input/input.ts @@ -23,7 +23,7 @@ export class Input extends LiteElement { label?: string; @property({ type: String }) - id: string = ""; + id: string = "customInput"; @property({ type: String }) name?: string; @@ -51,7 +51,6 @@ export class Input extends LiteElement { @state() isPasswordVisible: boolean = false; - render() { return html`
diff --git a/frontend/src/components/sign-up-form.ts b/frontend/src/components/sign-up-form.ts index 126ead8ff7..836c9a81dd 100644 --- a/frontend/src/components/sign-up-form.ts +++ b/frontend/src/components/sign-up-form.ts @@ -100,6 +100,7 @@ export class SignUpForm extends LiteElement { })} autocomplete="nickname" minlength="2" + required >

@@ -173,7 +174,6 @@ export class SignUpForm extends LiteElement { }) as any; private async onSubmit(event: SubmitEvent) { - const form = event.target as HTMLFormElement; event.preventDefault(); event.stopPropagation(); this.dispatchEvent(new CustomEvent("submit")); @@ -181,7 +181,7 @@ export class SignUpForm extends LiteElement { this.serverError = undefined; this.isSubmitting = true; - const formData = new FormData(form); + const formData = new FormData(event.target as HTMLFormElement); const email = formData.get("email") as string; const password = formData.get("password") as string; const name = formData.get("name") as string; From a72d6aed629dd3ac1e41adabc699d3c85377f69e Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 11 Oct 2023 13:26:24 -0700 Subject: [PATCH 11/16] add to reset form --- frontend/src/components/sign-up-form.ts | 24 +++++------ frontend/src/pages/join.ts | 4 +- frontend/src/pages/log-in.ts | 6 +-- frontend/src/pages/reset-password.ts | 54 ++++++++++++++++++++++--- frontend/src/pages/sign-up.ts | 4 +- 5 files changed, 67 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/sign-up-form.ts b/frontend/src/components/sign-up-form.ts index 836c9a81dd..98fda655d7 100644 --- a/frontend/src/components/sign-up-form.ts +++ b/frontend/src/components/sign-up-form.ts @@ -105,7 +105,7 @@ export class SignUpForm extends LiteElement {

${msg( - "Your full name, or another name that org collaborators will see." + "Your full name, nickname, or another name that org collaborators can see." )}

@@ -124,7 +124,7 @@ export class SignUpForm extends LiteElement {

${msg( - str`Choose a strong password between ${PASSWORD_MINLENGTH} and ${PASSWORD_MAXLENGTH} characters.` + str`Choose a strong password between ${PASSWORD_MINLENGTH}-${PASSWORD_MAXLENGTH} characters.` )}

${when(this.pwStrengthResults, this.renderPasswordStrength)} @@ -145,17 +145,15 @@ export class SignUpForm extends LiteElement { `; } - private renderPasswordStrength = () => { - return html` -
- - -
- `; - }; + private renderPasswordStrength = () => html` +
+ + +
+ `; private onPasswordInput = debounce(150)(async (e: InputEvent) => { const { value } = e.target as BtrixInput; diff --git a/frontend/src/pages/join.ts b/frontend/src/pages/join.ts index 82ccbdb804..b3b80ced72 100644 --- a/frontend/src/pages/join.ts +++ b/frontend/src/pages/join.ts @@ -67,7 +67,7 @@ export class Join extends LiteElement {

${msg( - html`You've been invited to join + html`You’ve been invited to join ${hasInviteInfo ? this.inviteInfo.orgName || msg("Browsertrix Cloud") @@ -78,7 +78,7 @@ export class Join extends LiteElement {

+
${successMessage} -
+
${form}
${link}
@@ -284,7 +284,7 @@ export class LogInPage extends LiteElement { ?loading=${this.formState.value === "signingIn"} ?disabled=${this.formState.value === "backendInitializing"} type="submit" - >${msg("Log in")}${msg("Log In")} ${this.formState.value === "backendInitializing" ? html`
diff --git a/frontend/src/pages/reset-password.ts b/frontend/src/pages/reset-password.ts index 875862ce55..6281c0f377 100644 --- a/frontend/src/pages/reset-password.ts +++ b/frontend/src/pages/reset-password.ts @@ -1,20 +1,36 @@ import { state, property } from "lit/decorators.js"; -import { msg, localized } from "@lit/localize"; +import { str, msg, localized } from "@lit/localize"; +import debounce from "lodash/fp/debounce"; +import { when } from "lit/directives/when.js"; +import type { ZxcvbnResult } from "@zxcvbn-ts/core"; import type { ViewState } from "../utils/APIRouter"; import LiteElement, { html } from "../utils/LiteElement"; +import PasswordService from "../utils/PasswordService"; +import type { Input as BtrixInput } from "../components/input/input"; + +const PASSWORD_MINLENGTH = 8; +const PASSWORD_MAXLENGTH = 64; +const PASSWORD_MIN_SCORE = 3; @localized() export class ResetPassword extends LiteElement { @property({ type: Object }) viewState!: ViewState; + @state() + private pwStrengthResults: null | ZxcvbnResult = null; + @state() private serverError?: string; @state() private isSubmitting: boolean = false; + protected firstUpdated() { + PasswordService.setOptions(); + } + render() { let formError; @@ -29,22 +45,29 @@ export class ResetPassword extends LiteElement { } return html` -
-
+
+
+

+ ${msg( + str`Choose a strong password between ${PASSWORD_MINLENGTH}-${PASSWORD_MAXLENGTH} characters.` + )} +

+ ${when(this.pwStrengthResults, this.renderPasswordStrength)}
${formError} @@ -53,8 +76,10 @@ export class ResetPassword extends LiteElement { class="w-full" variant="primary" ?loading=${this.isSubmitting} + ?disabled=${!this.pwStrengthResults || + this.pwStrengthResults.score < PASSWORD_MIN_SCORE} type="submit" - >${msg("Change password")}${msg("Change Password")}
@@ -71,6 +96,25 @@ export class ResetPassword extends LiteElement { `; } + private renderPasswordStrength = () => html` +
+ + +
+ `; + + private onPasswordInput = debounce(150)(async (e: InputEvent) => { + const { value } = e.target as BtrixInput; + if (!value || value.length < 4) { + this.pwStrengthResults = null; + return; + } + this.pwStrengthResults = await PasswordService.checkStrength(value); + }) as any; + async onSubmit(event: SubmitEvent) { event.preventDefault(); this.isSubmitting = true; diff --git a/frontend/src/pages/sign-up.ts b/frontend/src/pages/sign-up.ts index 3735d68419..51870d436c 100644 --- a/frontend/src/pages/sign-up.ts +++ b/frontend/src/pages/sign-up.ts @@ -15,8 +15,8 @@ export class SignUp extends LiteElement { render() { return html` -
-
+
+
${this.isSignedUpWithoutAuth ? html`
Date: Wed, 11 Oct 2023 13:59:32 -0700 Subject: [PATCH 12/16] enforce in account settings --- frontend/src/components/account-settings.ts | 49 ++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/account-settings.ts b/frontend/src/components/account-settings.ts index 460ff7074d..2bf2bdfbaf 100644 --- a/frontend/src/components/account-settings.ts +++ b/frontend/src/components/account-settings.ts @@ -1,15 +1,22 @@ import { LitElement } from "lit"; import { state, queryAsync, property } from "lit/decorators.js"; -import { msg, localized } from "@lit/localize"; +import { msg, str, localized } from "@lit/localize"; +import debounce from "lodash/fp/debounce"; import { when } from "lit/directives/when.js"; import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js"; import type { SlInput } from "@shoelace-style/shoelace"; +import type { ZxcvbnResult } from "@zxcvbn-ts/core"; import type { CurrentUser } from "../types/user"; import LiteElement, { html } from "../utils/LiteElement"; import { needLogin } from "../utils/auth"; import type { AuthState, Auth } from "../utils/AuthService"; import AuthService from "../utils/AuthService"; +import PasswordService from "../utils/PasswordService"; + +const PASSWORD_MINLENGTH = 8; +const PASSWORD_MAXLENGTH = 64; +const PASSWORD_MIN_SCORE = 3; @localized() class RequestVerify extends LitElement { @@ -98,6 +105,9 @@ export class AccountSettings extends LiteElement { @state() private isChangingPassword = false; + @state() + private pwStrengthResults: null | ZxcvbnResult = null; + @queryAsync('sl-input[name="password"]') private passwordInput?: Promise; @@ -110,6 +120,10 @@ export class AccountSettings extends LiteElement { } } + protected firstUpdated() { + PasswordService.setOptions(); + } + render() { if (!this.userInfo) return; return html` @@ -222,7 +236,14 @@ export class AccountSettings extends LiteElement { password-toggle minlength="8" required + @input=${this.onPasswordInput} > +

+ ${msg( + str`Choose a strong password between ${PASSWORD_MINLENGTH}-${PASSWORD_MAXLENGTH} characters.` + )} +

+ ${when(this.pwStrengthResults, this.renderPasswordStrength)}
html` +
+ + +
+ `; + + private onPasswordInput = debounce(150)(async (e: InputEvent) => { + const { value } = e.target as SlInput; + if (!value || value.length < 4) { + this.pwStrengthResults = null; + return; + } + const userInputs: string[] = []; + if (this.userInfo) { + userInputs.push(this.userInfo.name, this.userInfo.email); + } + this.pwStrengthResults = await PasswordService.checkStrength( + value, + userInputs + ); + }) as any; + private async onSubmitName(e: SubmitEvent) { if (!this.userInfo || !this.authState) return; const form = e.target as HTMLFormElement; From abfe77558e1ba3f019cc1da1370bf5a0b010a714 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 11 Oct 2023 14:01:22 -0700 Subject: [PATCH 13/16] move constants --- frontend/src/components/sign-up-form.ts | 5 ++--- frontend/src/pages/reset-password.ts | 5 ++--- frontend/src/utils/PasswordService.ts | 4 ++++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/sign-up-form.ts b/frontend/src/components/sign-up-form.ts index 98fda655d7..1fb78c7954 100644 --- a/frontend/src/components/sign-up-form.ts +++ b/frontend/src/components/sign-up-form.ts @@ -10,9 +10,8 @@ import AuthService from "../utils/AuthService"; import PasswordService from "../utils/PasswordService"; import type { Input as BtrixInput } from "./input/input"; -const PASSWORD_MINLENGTH = 8; -const PASSWORD_MAXLENGTH = 64; -const PASSWORD_MIN_SCORE = 3; +const { PASSWORD_MINLENGTH, PASSWORD_MAXLENGTH, PASSWORD_MIN_SCORE } = + PasswordService; /** * @event submit diff --git a/frontend/src/pages/reset-password.ts b/frontend/src/pages/reset-password.ts index 6281c0f377..be8d8793cd 100644 --- a/frontend/src/pages/reset-password.ts +++ b/frontend/src/pages/reset-password.ts @@ -9,9 +9,8 @@ import LiteElement, { html } from "../utils/LiteElement"; import PasswordService from "../utils/PasswordService"; import type { Input as BtrixInput } from "../components/input/input"; -const PASSWORD_MINLENGTH = 8; -const PASSWORD_MAXLENGTH = 64; -const PASSWORD_MIN_SCORE = 3; +const { PASSWORD_MINLENGTH, PASSWORD_MAXLENGTH, PASSWORD_MIN_SCORE } = + PasswordService; @localized() export class ResetPassword extends LiteElement { diff --git a/frontend/src/utils/PasswordService.ts b/frontend/src/utils/PasswordService.ts index 861a4d831b..6117541cd0 100644 --- a/frontend/src/utils/PasswordService.ts +++ b/frontend/src/utils/PasswordService.ts @@ -22,6 +22,10 @@ const loadOptions = async (): Promise => { * Test and estimate password strength */ export default class PasswordService { + static readonly PASSWORD_MINLENGTH = 8; + static readonly PASSWORD_MAXLENGTH = 64; + static readonly PASSWORD_MIN_SCORE = 3; + static options?: OptionsType; /** From 601d37e35c2d2508cd67f37a7593133fc66de348 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 11 Oct 2023 14:05:25 -0700 Subject: [PATCH 14/16] add to account settings --- frontend/src/components/account-settings.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/account-settings.ts b/frontend/src/components/account-settings.ts index 2bf2bdfbaf..76db2e3434 100644 --- a/frontend/src/components/account-settings.ts +++ b/frontend/src/components/account-settings.ts @@ -14,9 +14,8 @@ import type { AuthState, Auth } from "../utils/AuthService"; import AuthService from "../utils/AuthService"; import PasswordService from "../utils/PasswordService"; -const PASSWORD_MINLENGTH = 8; -const PASSWORD_MAXLENGTH = 64; -const PASSWORD_MIN_SCORE = 3; +const { PASSWORD_MINLENGTH, PASSWORD_MAXLENGTH, PASSWORD_MIN_SCORE } = + PasswordService; @localized() class RequestVerify extends LitElement { @@ -238,16 +237,17 @@ export class AccountSettings extends LiteElement { required @input=${this.onPasswordInput} > -

- ${msg( - str`Choose a strong password between ${PASSWORD_MINLENGTH}-${PASSWORD_MAXLENGTH} characters.` - )} -

+ ${when(this.pwStrengthResults, this.renderPasswordStrength)}
+

+ ${msg( + str`Choose a strong password between ${PASSWORD_MINLENGTH}-${PASSWORD_MAXLENGTH} characters.` + )} +

html` -
+
Date: Wed, 11 Oct 2023 14:07:38 -0700 Subject: [PATCH 15/16] disable save --- frontend/src/components/account-settings.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/components/account-settings.ts b/frontend/src/components/account-settings.ts index 76db2e3434..424866ffa8 100644 --- a/frontend/src/components/account-settings.ts +++ b/frontend/src/components/account-settings.ts @@ -253,6 +253,8 @@ export class AccountSettings extends LiteElement { size="small" variant="primary" ?loading=${this.sectionSubmitting === "password"} + ?disabled=${!this.pwStrengthResults || + this.pwStrengthResults.score < PASSWORD_MIN_SCORE} >${msg("Save")}
From ac4985137b72fa656f0614810661b1752c0534be Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 11 Oct 2023 19:17:53 -0700 Subject: [PATCH 16/16] Apply suggestions from code review Co-authored-by: Henry Wilkinson --- frontend/src/components/pw-strength-alert.ts | 2 +- frontend/src/components/sign-up-form.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/pw-strength-alert.ts b/frontend/src/components/pw-strength-alert.ts index f555366e1b..288d62bd9c 100644 --- a/frontend/src/components/pw-strength-alert.ts +++ b/frontend/src/components/pw-strength-alert.ts @@ -148,7 +148,7 @@ export class PasswordStrengthAlert extends LitElement { () => html`

${msg( - "Tip: To generate very strong passwords, install a built-in password manager." + "Tip: To generate very strong passwords, consider using a password manager." )}

` diff --git a/frontend/src/components/sign-up-form.ts b/frontend/src/components/sign-up-form.ts index 1fb78c7954..d54e4a2830 100644 --- a/frontend/src/components/sign-up-form.ts +++ b/frontend/src/components/sign-up-form.ts @@ -123,7 +123,7 @@ export class SignUpForm extends LiteElement {

${msg( - str`Choose a strong password between ${PASSWORD_MINLENGTH}-${PASSWORD_MAXLENGTH} characters.` + str`Choose a strong password between ${PASSWORD_MINLENGTH}–${PASSWORD_MAXLENGTH} characters.` )}

${when(this.pwStrengthResults, this.renderPasswordStrength)}