diff --git a/web/README.md b/web/README.md
index 24cc5c622..9e95b2272 100644
--- a/web/README.md
+++ b/web/README.md
@@ -3,6 +3,15 @@
This is the default UI for the authentik server. The documentation is going to be a little sparse
for awhile, but at least let's get started.
+# Standards
+
+- Be flexible in what you accept as input, be precise in what you produce as output.
+- Mis-use is always a crash. A component that takes the ID of an HTMLInputElement as an argument
+ should throw an exception if the element is anything but an HTMLInputElement ("anything" includes
+ non-existent, null, undefined, etc.).
+- Single Responsibility is ideal, but not always practical. To the best of your obility, every
+ object in the system should do one thing and do it well.
+
# Comments
**NOTE:** The comments in this section are for specific changes to this repository that cannot be
@@ -21,3 +30,7 @@ settings in JSON files, which do not support comments.
- `compilerOptions.plugins.ts-lit-plugin.rules.no-incompatible-type-binding: "warn"`: lit-analyzer
does not support generics well when parsing a subtype of `HTMLElement`. As a result, this threw
too many errors to be supportable.
+- `package.json`
+ - `prettier` should always be the last thing run in any pre-commit pass. The `precommit` script
+ does this, but if you don't use `precommit`, make sure `prettier` is the _last_ thing you do
+ before a `git commit`.
diff --git a/web/package-lock.json b/web/package-lock.json
index a033c5a5c..4c004870d 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -35,7 +35,8 @@
"mermaid": "^10.2.2",
"rapidoc": "^9.3.4",
"webcomponent-qr-code": "^1.1.1",
- "yaml": "^2.3.1"
+ "yaml": "^2.3.1",
+ "zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/core": "^7.22.1",
@@ -64,6 +65,7 @@
"@types/chart.js": "^2.9.37",
"@types/codemirror": "5.60.8",
"@types/grecaptcha": "^3.0.4",
+ "@types/zxcvbn": "^4.4.1",
"@typescript-eslint/eslint-plugin": "^5.59.9",
"@typescript-eslint/parser": "^5.59.9",
"babel-plugin-macros": "^3.1.0",
@@ -6507,6 +6509,12 @@
"integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==",
"dev": true
},
+ "node_modules/@types/zxcvbn": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/@types/zxcvbn/-/zxcvbn-4.4.1.tgz",
+ "integrity": "sha512-3NoqvZC2W5gAC5DZbTpCeJ251vGQmgcWIHQJGq2J240HY6ErQ9aWKkwfoKJlHLx+A83WPNTZ9+3cd2ILxbvr1w==",
+ "dev": true
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.59.9",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.9.tgz",
@@ -18034,6 +18042,11 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zxcvbn": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
+ "integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ=="
}
}
}
diff --git a/web/package.json b/web/package.json
index dd127c296..48797c24f 100644
--- a/web/package.json
+++ b/web/package.json
@@ -16,6 +16,7 @@
"watch": "run-s build-locales rollup:watch",
"lint": "eslint . --max-warnings 0 --fix",
"lit-analyse": "lit-analyzer src",
+ "precommit": "run-s build-locales lint tsc prettier",
"prettier-check": "prettier --check .",
"prettier": "prettier --write .",
"tsc:execute": "tsc --noEmit -p .",
@@ -51,7 +52,8 @@
"mermaid": "^10.2.2",
"rapidoc": "^9.3.4",
"webcomponent-qr-code": "^1.1.1",
- "yaml": "^2.3.1"
+ "yaml": "^2.3.1",
+ "zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/core": "^7.22.1",
@@ -80,6 +82,7 @@
"@types/chart.js": "^2.9.37",
"@types/codemirror": "5.60.8",
"@types/grecaptcha": "^3.0.4",
+ "@types/zxcvbn": "^4.4.1",
"@typescript-eslint/eslint-plugin": "^5.59.9",
"@typescript-eslint/parser": "^5.59.9",
"babel-plugin-macros": "^3.1.0",
diff --git a/web/src/elements/password-match-indicator/index.ts b/web/src/elements/password-match-indicator/index.ts
new file mode 100644
index 000000000..4116e1985
--- /dev/null
+++ b/web/src/elements/password-match-indicator/index.ts
@@ -0,0 +1,5 @@
+import PasswordMatchIndicator from "./password-match-indicator.js";
+
+export { PasswordMatchIndicator };
+
+export default PasswordMatchIndicator;
diff --git a/web/src/elements/password-match-indicator/password-match-indicator.stories.ts b/web/src/elements/password-match-indicator/password-match-indicator.stories.ts
new file mode 100644
index 000000000..eb6e85ea8
--- /dev/null
+++ b/web/src/elements/password-match-indicator/password-match-indicator.stories.ts
@@ -0,0 +1,14 @@
+import { html } from "lit";
+
+import ".";
+
+export default {
+ title: "Elements/Password Match Indicator",
+};
+
+export const Primary = () =>
+ html`
+
Type some text:
+
Type some other text:
+
+
`;
diff --git a/web/src/elements/password-match-indicator/password-match-indicator.ts b/web/src/elements/password-match-indicator/password-match-indicator.ts
new file mode 100644
index 000000000..745926faa
--- /dev/null
+++ b/web/src/elements/password-match-indicator/password-match-indicator.ts
@@ -0,0 +1,91 @@
+import { AKElement } from "@goauthentik/elements/Base";
+
+import { css, html } from "lit";
+import { customElement, property, state } from "lit/decorators.js";
+
+import PFBase from "@patternfly/patternfly/patternfly-base.css";
+
+import findInput from "../password-strength-indicator/findInput.js";
+
+/**
+ * A simple display showing if the passwords match. This element is extremely fragile and
+ * role-specific, depending as it does on the token string '_repeat' inside the selector.
+ */
+
+const ELEMENT = "ak-password-match-indicator";
+
+@customElement(ELEMENT)
+export class PasswordMatchIndicator extends AKElement {
+ static styles = [
+ PFBase,
+ css`
+ :host {
+ display: grid;
+ place-items: center center;
+ }
+ `,
+ ];
+
+ /**
+ * The input element to observe. Attaching this to anything other than an HTMLInputElement will
+ * throw an exception.
+ */
+ @property({ attribute: true })
+ src = "";
+
+ sourceInput?: HTMLInputElement;
+ otherInput?: HTMLInputElement;
+
+ @state()
+ match = false;
+
+ constructor() {
+ super();
+ this.checkPasswordMatch = this.checkPasswordMatch.bind(this);
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.input.addEventListener("keyup", this.checkPasswordMatch);
+ this.other.addEventListener("keyup", this.checkPasswordMatch);
+ }
+
+ disconnectedCallback() {
+ this.other.removeEventListener("keyup", this.checkPasswordMatch);
+ this.input.removeEventListener("keyup", this.checkPasswordMatch);
+ super.disconnectedCallback();
+ }
+
+ checkPasswordMatch() {
+ this.match =
+ this.input.value.length > 0 &&
+ this.other.value.length > 0 &&
+ this.input.value === this.other.value;
+ }
+
+ get input() {
+ if (this.sourceInput) {
+ return this.sourceInput;
+ }
+ return (this.sourceInput = findInput(this.getRootNode() as Element, ELEMENT, this.src));
+ }
+
+ get other() {
+ if (this.otherInput) {
+ return this.otherInput;
+ }
+ return (this.otherInput = findInput(
+ this.getRootNode() as Element,
+ ELEMENT,
+ this.src.replace(/_repeat/, ""),
+ ));
+ }
+
+ render() {
+ return this.match
+ ? html``
+ : html``;
+ }
+}
+
+export default PasswordMatchIndicator;
diff --git a/web/src/elements/password-strength-indicator/findInput.ts b/web/src/elements/password-strength-indicator/findInput.ts
new file mode 100644
index 000000000..7a42d8699
--- /dev/null
+++ b/web/src/elements/password-strength-indicator/findInput.ts
@@ -0,0 +1,18 @@
+export function findInput(root: Element, tag: string, src: string) {
+ const inputs = Array.from(root.querySelectorAll(src));
+ if (inputs.length === 0) {
+ throw new Error(`${tag}: no element found for 'src' ${src}`);
+ }
+ if (inputs.length > 1) {
+ throw new Error(`${tag}: more than one element found for 'src' ${src}`);
+ }
+ const input = inputs[0];
+ if (!(input instanceof HTMLInputElement)) {
+ throw new Error(
+ `${tag}: the 'src' element must be an tag, found ${input.localName}`,
+ );
+ }
+ return input;
+}
+
+export default findInput;
diff --git a/web/src/elements/password-strength-indicator/index.ts b/web/src/elements/password-strength-indicator/index.ts
new file mode 100644
index 000000000..1b1bcba7b
--- /dev/null
+++ b/web/src/elements/password-strength-indicator/index.ts
@@ -0,0 +1,5 @@
+import PasswordStrengthIndicator from "./password-strength-indicator.js";
+
+export { PasswordStrengthIndicator };
+
+export default PasswordStrengthIndicator;
diff --git a/web/src/elements/password-strength-indicator/password-strength-indicator.stories.ts b/web/src/elements/password-strength-indicator/password-strength-indicator.stories.ts
new file mode 100644
index 000000000..787e66da3
--- /dev/null
+++ b/web/src/elements/password-strength-indicator/password-strength-indicator.stories.ts
@@ -0,0 +1,13 @@
+import { html } from "lit";
+
+import ".";
+
+export default {
+ title: "Elements/Password Strength Indicator",
+};
+
+export const Primary = () =>
+ html`