Enforce strong passwords in UI (#1266)
This commit is contained in:
parent
834fa72baf
commit
630c00c5b0
@ -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",
|
||||
|
||||
@ -1,15 +1,21 @@
|
||||
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, PASSWORD_MAXLENGTH, PASSWORD_MIN_SCORE } =
|
||||
PasswordService;
|
||||
|
||||
@localized()
|
||||
class RequestVerify extends LitElement {
|
||||
@ -98,6 +104,9 @@ export class AccountSettings extends LiteElement {
|
||||
@state()
|
||||
private isChangingPassword = false;
|
||||
|
||||
@state()
|
||||
private pwStrengthResults: null | ZxcvbnResult = null;
|
||||
|
||||
@queryAsync('sl-input[name="password"]')
|
||||
private passwordInput?: Promise<SlInput | null>;
|
||||
|
||||
@ -110,6 +119,10 @@ export class AccountSettings extends LiteElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
PasswordService.setOptions();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.userInfo) return;
|
||||
return html`
|
||||
@ -222,16 +235,26 @@ export class AccountSettings extends LiteElement {
|
||||
password-toggle
|
||||
minlength="8"
|
||||
required
|
||||
@input=${this.onPasswordInput}
|
||||
></sl-input>
|
||||
|
||||
${when(this.pwStrengthResults, this.renderPasswordStrength)}
|
||||
</div>
|
||||
<footer
|
||||
class="flex items-center justify-end border-t px-4 py-3"
|
||||
>
|
||||
<p class="mr-auto text-gray-500">
|
||||
${msg(
|
||||
str`Choose a strong password between ${PASSWORD_MINLENGTH}-${PASSWORD_MAXLENGTH} characters.`
|
||||
)}
|
||||
</p>
|
||||
<sl-button
|
||||
type="submit"
|
||||
size="small"
|
||||
variant="primary"
|
||||
?loading=${this.sectionSubmitting === "password"}
|
||||
?disabled=${!this.pwStrengthResults ||
|
||||
this.pwStrengthResults.score < PASSWORD_MIN_SCORE}
|
||||
>${msg("Save")}</sl-button
|
||||
>
|
||||
</footer>
|
||||
@ -255,6 +278,32 @@ export class AccountSettings extends LiteElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderPasswordStrength = () => html`
|
||||
<div class="mt-4">
|
||||
<btrix-pw-strength-alert
|
||||
.result=${this.pwStrengthResults}
|
||||
min=${PASSWORD_MIN_SCORE}
|
||||
>
|
||||
</btrix-pw-strength-alert>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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;
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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`
|
||||
<div class="sl-label">
|
||||
|
||||
160
frontend/src/components/pw-strength-alert.ts
Normal file
160
frontend/src/components/pw-strength-alert.ts
Normal file
@ -0,0 +1,160 @@
|
||||
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
|
||||
* <btrix-pw-strength-alert .result=${this.zxcvbnResult}></btrix-pw-strength-alert>
|
||||
* ```
|
||||
*/
|
||||
@localized()
|
||||
export class PasswordStrengthAlert extends LitElement {
|
||||
@property({ type: String })
|
||||
result?: ZxcvbnResult;
|
||||
|
||||
/** Minimum acceptable score */
|
||||
@property({ type: String })
|
||||
min = 1;
|
||||
|
||||
/** Optimal score */
|
||||
@property({ type: String })
|
||||
optimal = 4;
|
||||
|
||||
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("Very weak 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;
|
||||
}
|
||||
if (score < this.min) {
|
||||
scoreProps.label = msg("Please choose a stronger password");
|
||||
}
|
||||
return html`
|
||||
<sl-alert variant=${scoreProps.variant as any} open>
|
||||
<div class="score">
|
||||
<sl-icon class="icon" name=${scoreProps.icon}></sl-icon>
|
||||
<p class="label">${scoreProps.label}</p>
|
||||
</div>
|
||||
|
||||
<div class="feedback">
|
||||
${when(
|
||||
feedback.warning,
|
||||
() => html` <p class="text">${feedback.warning}</p> `
|
||||
)}
|
||||
${when(feedback.suggestions.length, () =>
|
||||
feedback.suggestions.length === 1
|
||||
? html`<p class="text">
|
||||
${msg("Suggestion:")} ${feedback.suggestions[0]}
|
||||
</p>`
|
||||
: html`<p class="text">${msg("Suggestions:")}</p>
|
||||
<ul>
|
||||
${feedback.suggestions.map(
|
||||
(text) => html`<li>${text}</li>`
|
||||
)}
|
||||
</ul>`
|
||||
)}
|
||||
${when(
|
||||
score >= this.min && score < this.optimal,
|
||||
() => html`
|
||||
<p class="text">
|
||||
${msg(
|
||||
"Tip: To generate very strong passwords, consider using a password manager."
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</sl-alert>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,17 @@
|
||||
import { state, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.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 type { ZxcvbnResult } from "@zxcvbn-ts/core";
|
||||
|
||||
import LiteElement, { html } from "../utils/LiteElement";
|
||||
import AuthService from "../utils/AuthService";
|
||||
import PasswordService from "../utils/PasswordService";
|
||||
import type { Input as BtrixInput } from "./input/input";
|
||||
|
||||
const { PASSWORD_MINLENGTH, PASSWORD_MAXLENGTH, PASSWORD_MIN_SCORE } =
|
||||
PasswordService;
|
||||
|
||||
/**
|
||||
* @event submit
|
||||
@ -32,6 +40,13 @@ export class SignUpForm extends LiteElement {
|
||||
@state()
|
||||
private isSubmitting: boolean = false;
|
||||
|
||||
@state()
|
||||
private pwStrengthResults: null | ZxcvbnResult = null;
|
||||
|
||||
protected firstUpdated() {
|
||||
PasswordService.setOptions();
|
||||
}
|
||||
|
||||
render() {
|
||||
let serverError;
|
||||
|
||||
@ -74,20 +89,23 @@ export class SignUpForm extends LiteElement {
|
||||
</btrix-input>
|
||||
`}
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<div class="mb-5 list-">
|
||||
<btrix-input
|
||||
id="name"
|
||||
name="name"
|
||||
label=${msg("Your name")}
|
||||
placeholder=${msg("Lisa Simpson", {
|
||||
desc: "Example user's name",
|
||||
desc: "Example user’s name",
|
||||
})}
|
||||
autocomplete="nickname"
|
||||
minlength="2"
|
||||
required
|
||||
>
|
||||
</btrix-input>
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
${msg("Your name will be visible to organization collaborators.")}
|
||||
<p class="mt-2 text-gray-500">
|
||||
${msg(
|
||||
"Your full name, nickname, or another name that org collaborators can see."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
@ -96,15 +114,19 @@ export class SignUpForm extends LiteElement {
|
||||
name="password"
|
||||
type="password"
|
||||
label="${msg("Password")}"
|
||||
minlength="8"
|
||||
minlength=${PASSWORD_MINLENGTH}
|
||||
autocomplete="new-password"
|
||||
passwordToggle
|
||||
required
|
||||
@input=${this.onPasswordInput}
|
||||
>
|
||||
</btrix-input>
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
${msg("Choose a strong password between 8-64 characters.")}
|
||||
<p class="mt-2 text-gray-500">
|
||||
${msg(
|
||||
str`Choose a strong password between ${PASSWORD_MINLENGTH}–${PASSWORD_MAXLENGTH} characters.`
|
||||
)}
|
||||
</p>
|
||||
${when(this.pwStrengthResults, this.renderPasswordStrength)}
|
||||
</div>
|
||||
|
||||
${serverError}
|
||||
@ -113,6 +135,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")}</sl-button
|
||||
>
|
||||
@ -120,8 +144,33 @@ export class SignUpForm extends LiteElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderPasswordStrength = () => html`
|
||||
<div class="my-3">
|
||||
<btrix-pw-strength-alert
|
||||
.result=${this.pwStrengthResults}
|
||||
min=${PASSWORD_MIN_SCORE}
|
||||
>
|
||||
</btrix-pw-strength-alert>
|
||||
</div>
|
||||
`;
|
||||
|
||||
private onPasswordInput = debounce(150)(async (e: InputEvent) => {
|
||||
const { value } = e.target as BtrixInput;
|
||||
if (!value || value.length < 4) {
|
||||
this.pwStrengthResults = null;
|
||||
return;
|
||||
}
|
||||
const userInputs: string[] = [];
|
||||
if (this.email) {
|
||||
userInputs.push(this.email);
|
||||
}
|
||||
this.pwStrengthResults = await PasswordService.checkStrength(
|
||||
value,
|
||||
userInputs
|
||||
);
|
||||
}) as any;
|
||||
|
||||
private async onSubmit(event: SubmitEvent) {
|
||||
const form = event.target as HTMLFormElement;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.dispatchEvent(new CustomEvent("submit"));
|
||||
@ -129,7 +178,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;
|
||||
|
||||
@ -56,26 +56,29 @@ export class Join extends LiteElement {
|
||||
return html`
|
||||
<article class="w-full p-5 flex flex-col md:flex-row justify-center">
|
||||
<div class="max-w-sm md:mt-12 md:mr-12">
|
||||
<div class="mb-3 text-sm text-gray-400">
|
||||
${msg("Invited by ")}
|
||||
${this.inviteInfo.inviterName ||
|
||||
this.inviteInfo.inviterEmail ||
|
||||
placeholder}
|
||||
<div class="mb-3 text-gray-500">
|
||||
${msg(
|
||||
str`Invited by ${
|
||||
this.inviteInfo.inviterName ||
|
||||
this.inviteInfo.inviterEmail ||
|
||||
placeholder
|
||||
}`
|
||||
)}
|
||||
</div>
|
||||
<p class="text-xl md:text-2xl font-semibold mb-5">
|
||||
${msg(
|
||||
html`You've been invited to join
|
||||
html`You’ve been invited to join
|
||||
<span class="text-primary break-words"
|
||||
>${hasInviteInfo
|
||||
? this.inviteInfo.orgName || msg("Browsertrix Cloud")
|
||||
: placeholder}</span
|
||||
>`
|
||||
>.`
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<main
|
||||
class="max-w-md md:bg-white md:shadow-xl md:rounded-lg md:px-12 md:py-12"
|
||||
class="max-w-md md:bg-white md:border md:shadow-lg md:rounded-lg md:p-10"
|
||||
>
|
||||
<btrix-sign-up-form
|
||||
email=${this.email!}
|
||||
|
||||
@ -216,10 +216,10 @@ export class LogInPage extends LiteElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<article class="w-full max-w-sm grid gap-5">
|
||||
<article class="w-full max-w-md grid gap-5">
|
||||
${successMessage}
|
||||
|
||||
<main class="md:bg-white md:shadow-xl md:rounded-lg p-12">
|
||||
<main class="md:bg-white md:border md:shadow-lg md:rounded-lg p-10">
|
||||
<div>${form}</div>
|
||||
</main>
|
||||
<footer class="text-center">${link}</footer>
|
||||
@ -284,7 +284,7 @@ export class LogInPage extends LiteElement {
|
||||
?loading=${this.formState.value === "signingIn"}
|
||||
?disabled=${this.formState.value === "backendInitializing"}
|
||||
type="submit"
|
||||
>${msg("Log in")}</sl-button
|
||||
>${msg("Log In")}</sl-button
|
||||
>
|
||||
${this.formState.value === "backendInitializing"
|
||||
? html` <div class="mt-3">
|
||||
|
||||
@ -1,20 +1,35 @@
|
||||
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, PASSWORD_MAXLENGTH, PASSWORD_MIN_SCORE } =
|
||||
PasswordService;
|
||||
|
||||
@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 +44,29 @@ export class ResetPassword extends LiteElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="w-full max-w-sm grid gap-5">
|
||||
<div class="md:bg-white md:shadow-xl md:rounded-lg md:px-12 md:py-12">
|
||||
<div class="w-full max-w-md grid gap-5">
|
||||
<div class="md:bg-white md:border md:shadow-lg md:rounded-lg md:p-10">
|
||||
<form @submit=${this.onSubmit} aria-describedby="formError">
|
||||
<div class="mb-5">
|
||||
<btrix-input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
label="${msg("New password")}"
|
||||
label="${msg("Enter new password")}"
|
||||
help-text=${msg("Must be between 8-64 characters")}
|
||||
minlength="8"
|
||||
autocomplete="new-password"
|
||||
passwordToggle
|
||||
required
|
||||
@input=${this.onPasswordInput}
|
||||
>
|
||||
</btrix-input>
|
||||
<p class="mt-2 text-gray-500">
|
||||
${msg(
|
||||
str`Choose a strong password between ${PASSWORD_MINLENGTH}-${PASSWORD_MAXLENGTH} characters.`
|
||||
)}
|
||||
</p>
|
||||
${when(this.pwStrengthResults, this.renderPasswordStrength)}
|
||||
</div>
|
||||
|
||||
${formError}
|
||||
@ -53,8 +75,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")}</sl-button
|
||||
>${msg("Change Password")}</sl-button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
@ -71,6 +95,25 @@ export class ResetPassword extends LiteElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderPasswordStrength = () => html`
|
||||
<div class="my-3">
|
||||
<btrix-pw-strength-alert
|
||||
.result=${this.pwStrengthResults}
|
||||
min=${PASSWORD_MIN_SCORE}
|
||||
>
|
||||
</btrix-pw-strength-alert>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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;
|
||||
|
||||
@ -15,8 +15,8 @@ export class SignUp extends LiteElement {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<article class="w-full max-w-sm grid gap-5">
|
||||
<main class="md:bg-white md:shadow-xl md:rounded-lg p-12">
|
||||
<article class="w-full max-w-md grid gap-5">
|
||||
<main class="md:bg-white md:border md:shadow-lg md:rounded-lg p-10">
|
||||
${this.isSignedUpWithoutAuth
|
||||
? html`
|
||||
<div
|
||||
|
||||
62
frontend/src/utils/PasswordService.ts
Normal file
62
frontend/src/utils/PasswordService.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { zxcvbn, zxcvbnOptions, OptionsType } from "@zxcvbn-ts/core";
|
||||
|
||||
const loadOptions = async (): Promise<OptionsType> => {
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
if (opts) {
|
||||
zxcvbnOptions.setOptions({
|
||||
...PasswordService.options,
|
||||
...opts,
|
||||
});
|
||||
} else {
|
||||
zxcvbnOptions.setOptions(PasswordService.options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
userInputs?: string[]
|
||||
) {
|
||||
return zxcvbn(password, userInputs);
|
||||
}
|
||||
}
|
||||
@ -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==
|
||||
|
||||
Loading…
Reference in New Issue
Block a user