Compare commits
	
		
			9 Commits
		
	
	
		
			web/parts/
			...
			5165-passw
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5b9bb12822 | |||
| 8f09955d58 | |||
| 465820b002 | |||
| a75c9434d9 | |||
| 4ea9b69ab5 | |||
| c48eee0ebf | |||
| 0d94373f10 | |||
| 1c85dc512f | |||
| a71778651f | 
@ -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.
 | 
			
		||||
 | 
			
		||||
# The Theory of the authentik UI
 | 
			
		||||
 | 
			
		||||
In Peter Naur's 1985 essay [Programming as Theory
 | 
			
		||||
@ -107,3 +116,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`.
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										6665
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6665
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -15,7 +15,7 @@
 | 
			
		||||
        "build-proxy": "run-s build-locales esbuild:build-proxy",
 | 
			
		||||
        "watch": "run-s build-locales esbuild:watch",
 | 
			
		||||
        "lint": "cross-env NODE_OPTIONS='--max_old_space_size=65536' eslint . --max-warnings 0 --fix",
 | 
			
		||||
        "lint:precommit": "cross-env NODE_OPTIONS='--max_old_space_size=65536' node scripts/eslint-precommit.mjs",
 | 
			
		||||
        "lint:precommit": "bun scripts/eslint-precommit.mjs",
 | 
			
		||||
        "lint:spelling": "node scripts/check-spelling.mjs",
 | 
			
		||||
        "lit-analyse": "lit-analyzer src",
 | 
			
		||||
        "precommit": "npm-run-all --parallel tsc lit-analyse lint:spelling --sequential lint:precommit prettier",
 | 
			
		||||
@ -65,7 +65,8 @@
 | 
			
		||||
        "style-mod": "^4.1.2",
 | 
			
		||||
        "ts-pattern": "^5.1.2",
 | 
			
		||||
        "webcomponent-qr-code": "^1.2.0",
 | 
			
		||||
        "yaml": "^2.4.5"
 | 
			
		||||
        "yaml": "^2.4.5",
 | 
			
		||||
        "zxcvbn": "^4.4.2"
 | 
			
		||||
    },
 | 
			
		||||
    "devDependencies": {
 | 
			
		||||
        "@babel/core": "^7.24.7",
 | 
			
		||||
@ -94,6 +95,7 @@
 | 
			
		||||
        "@types/grecaptcha": "^3.0.9",
 | 
			
		||||
        "@types/guacamole-common-js": "1.5.2",
 | 
			
		||||
        "@types/showdown": "^2.0.6",
 | 
			
		||||
        "@types/zxcvbn": "^4.4.4",
 | 
			
		||||
        "@typescript-eslint/eslint-plugin": "^7.5.0",
 | 
			
		||||
        "@typescript-eslint/parser": "^7.5.0",
 | 
			
		||||
        "babel-plugin-macros": "^3.1.0",
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ import { PFSize } from "@goauthentik/common/enums.js";
 | 
			
		||||
import { AKElement } from "@goauthentik/elements/Base";
 | 
			
		||||
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
 | 
			
		||||
 | 
			
		||||
import { Task, TaskStatus } from "@lit/task";
 | 
			
		||||
import { Task, TaskStatus, initialState } from "@lit/task";
 | 
			
		||||
import { css, html } from "lit";
 | 
			
		||||
import { property } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
@ -67,7 +67,7 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
 | 
			
		||||
        this.onError = this.onError.bind(this);
 | 
			
		||||
        this.onClick = this.onClick.bind(this);
 | 
			
		||||
        this.actionTask = new Task(this, {
 | 
			
		||||
            task: () => this.callAction(),
 | 
			
		||||
            task: () => this.runCallAction(),
 | 
			
		||||
            args: () => [],
 | 
			
		||||
            autoRun: false,
 | 
			
		||||
            onComplete: (r: unknown) => this.onSuccess(r),
 | 
			
		||||
@ -77,7 +77,6 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
 | 
			
		||||
 | 
			
		||||
    onComplete() {
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            this.actionTask.status = TaskStatus.INITIAL;
 | 
			
		||||
            this.dispatchCustomEvent(`${this.eventPrefix}-reset`);
 | 
			
		||||
            this.requestUpdate();
 | 
			
		||||
        }, SPINNER_TIMEOUT);
 | 
			
		||||
@ -97,10 +96,12 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
 | 
			
		||||
        this.onComplete();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onClick() {
 | 
			
		||||
        if (this.actionTask.status !== TaskStatus.INITIAL) {
 | 
			
		||||
            return;
 | 
			
		||||
    async runCallAction() {
 | 
			
		||||
        await this.callAction();
 | 
			
		||||
        return initialState;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onClick() {
 | 
			
		||||
        this.dispatchCustomEvent(`${this.eventPrefix}-click`);
 | 
			
		||||
        this.actionTask.run();
 | 
			
		||||
    }
 | 
			
		||||
@ -113,7 +114,7 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
 | 
			
		||||
        return [
 | 
			
		||||
            ...this.classList,
 | 
			
		||||
            StatusMap.get(this.actionTask.status),
 | 
			
		||||
            this.actionTask.status === TaskStatus.INITIAL ? "" : "working",
 | 
			
		||||
            this.actionTask.status === TaskStatus.PENDING ? "working" : "",
 | 
			
		||||
        ]
 | 
			
		||||
            .join(" ")
 | 
			
		||||
            .trim();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								web/src/elements/password-match-indicator/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								web/src/elements/password-match-indicator/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
import PasswordMatchIndicator from "./password-match-indicator.js";
 | 
			
		||||
 | 
			
		||||
export { PasswordMatchIndicator };
 | 
			
		||||
 | 
			
		||||
export default PasswordMatchIndicator;
 | 
			
		||||
@ -0,0 +1,19 @@
 | 
			
		||||
import { html } from "lit";
 | 
			
		||||
 | 
			
		||||
import ".";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    title: "Elements/Password Match Indicator",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Primary = () =>
 | 
			
		||||
    html`<div style="background: #fff; padding: 4em">
 | 
			
		||||
        <p>Type some text: <input id="primary-example" style="color:#000" /></p>
 | 
			
		||||
        <p style="margin-top:0.5em">
 | 
			
		||||
            Type some other text: <input id="primary-example_repeat" style="color:#000" />
 | 
			
		||||
            <ak-password-match-indicator
 | 
			
		||||
                first="#primary-example"
 | 
			
		||||
                second="#primary-example_repeat"
 | 
			
		||||
            ></ak-password-match-indicator>
 | 
			
		||||
        </p>
 | 
			
		||||
    </div>`;
 | 
			
		||||
@ -0,0 +1,94 @@
 | 
			
		||||
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";
 | 
			
		||||
 | 
			
		||||
const ELEMENT = "ak-password-match-indicator";
 | 
			
		||||
 | 
			
		||||
@customElement(ELEMENT)
 | 
			
		||||
export class PasswordMatchIndicator extends AKElement {
 | 
			
		||||
    static styles = [
 | 
			
		||||
        PFBase,
 | 
			
		||||
        css`
 | 
			
		||||
            :host {
 | 
			
		||||
                display: grid;
 | 
			
		||||
                place-items: center center;
 | 
			
		||||
            }
 | 
			
		||||
        `,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A valid selector for the first input element to observe. Attaching this to anything other
 | 
			
		||||
     * than an HTMLInputElement will throw an exception.
 | 
			
		||||
     */
 | 
			
		||||
    @property({ attribute: true })
 | 
			
		||||
    first = "";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A valid selector for the second input element to observe. Attaching this to anything other
 | 
			
		||||
     * than an HTMLInputElement will throw an exception.
 | 
			
		||||
     */
 | 
			
		||||
    @property({ attribute: true })
 | 
			
		||||
    second = "";
 | 
			
		||||
 | 
			
		||||
    firstElement?: HTMLInputElement;
 | 
			
		||||
 | 
			
		||||
    secondElement?: HTMLInputElement;
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    match = false;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        this.checkPasswordMatch = this.checkPasswordMatch.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
        this.firstInput.addEventListener("keyup", this.checkPasswordMatch);
 | 
			
		||||
        this.secondInput.addEventListener("keyup", this.checkPasswordMatch);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    disconnectedCallback() {
 | 
			
		||||
        this.secondInput.removeEventListener("keyup", this.checkPasswordMatch);
 | 
			
		||||
        this.firstInput.removeEventListener("keyup", this.checkPasswordMatch);
 | 
			
		||||
        super.disconnectedCallback();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    checkPasswordMatch() {
 | 
			
		||||
        this.match =
 | 
			
		||||
            this.firstInput.value.length > 0 &&
 | 
			
		||||
            this.secondInput.value.length > 0 &&
 | 
			
		||||
            this.firstInput.value === this.secondInput.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get firstInput() {
 | 
			
		||||
        if (this.firstElement) {
 | 
			
		||||
            return this.firstElement;
 | 
			
		||||
        }
 | 
			
		||||
        return (this.firstElement = findInput(this.getRootNode() as Element, ELEMENT, this.first));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get secondInput() {
 | 
			
		||||
        if (this.secondElement) {
 | 
			
		||||
            return this.secondElement;
 | 
			
		||||
        }
 | 
			
		||||
        return (this.secondElement = findInput(
 | 
			
		||||
            this.getRootNode() as Element,
 | 
			
		||||
            ELEMENT,
 | 
			
		||||
            this.second,
 | 
			
		||||
        ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return this.match
 | 
			
		||||
            ? html`<i class="pf-icon pf-icon-ok pf-m-success"></i>`
 | 
			
		||||
            : html`<i class="pf-icon pf-icon-warning-triangle pf-m-warning"></i>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default PasswordMatchIndicator;
 | 
			
		||||
							
								
								
									
										18
									
								
								web/src/elements/password-strength-indicator/findInput.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								web/src/elements/password-strength-indicator/findInput.ts
									
									
									
									
									
										Normal file
									
								
							@ -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 <input> tag, found ${input.localName}`,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    return input;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default findInput;
 | 
			
		||||
							
								
								
									
										5
									
								
								web/src/elements/password-strength-indicator/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								web/src/elements/password-strength-indicator/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
import PasswordStrengthIndicator from "./password-strength-indicator.js";
 | 
			
		||||
 | 
			
		||||
export { PasswordStrengthIndicator };
 | 
			
		||||
 | 
			
		||||
export default PasswordStrengthIndicator;
 | 
			
		||||
@ -0,0 +1,13 @@
 | 
			
		||||
import { html } from "lit";
 | 
			
		||||
 | 
			
		||||
import ".";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    title: "Elements/Password Strength Indicator",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Primary = () =>
 | 
			
		||||
    html`<div style="background: #fff; padding: 4em">
 | 
			
		||||
        <p>Type some text: <input id="primary-example" style="color:#000" /></p>
 | 
			
		||||
        <ak-password-strength-indicator src="#primary-example"></ak-password-strength-indicator>
 | 
			
		||||
    </div>`;
 | 
			
		||||
@ -0,0 +1,91 @@
 | 
			
		||||
import { AKElement } from "@goauthentik/elements/Base";
 | 
			
		||||
import zxcvbn from "zxcvbn";
 | 
			
		||||
 | 
			
		||||
import { css, html } from "lit";
 | 
			
		||||
import { styleMap } from "lit-html/directives/style-map.js";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import findInput from "./findInput";
 | 
			
		||||
 | 
			
		||||
const styles = css`
 | 
			
		||||
    .password-meter-wrap {
 | 
			
		||||
        margin-top: 5px;
 | 
			
		||||
        height: 0.5em;
 | 
			
		||||
        background-color: #ddd;
 | 
			
		||||
        border-radius: 0.25em;
 | 
			
		||||
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .password-meter-bar {
 | 
			
		||||
        width: 0;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
        transition: width 400ms ease-in;
 | 
			
		||||
    }
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const LEVELS = [
 | 
			
		||||
    ["20%", "#dd0000"],
 | 
			
		||||
    ["40%", "#ff5500"],
 | 
			
		||||
    ["60%", "#ffff00"],
 | 
			
		||||
    ["80%", "#a1a841"],
 | 
			
		||||
    ["100%", "#339933"],
 | 
			
		||||
].map(([width, backgroundColor]) => ({ width, backgroundColor }));
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A simple display of the password strength.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const ELEMENT = "ak-password-strength-indicator";
 | 
			
		||||
 | 
			
		||||
@customElement(ELEMENT)
 | 
			
		||||
export class PasswordStrengthIndicator extends AKElement {
 | 
			
		||||
    static styles = styles;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The input element to observe. Attaching this to anything other than an HTMLInputElement will
 | 
			
		||||
     * throw an exception.
 | 
			
		||||
     */
 | 
			
		||||
    @property({ attribute: true })
 | 
			
		||||
    src = "";
 | 
			
		||||
 | 
			
		||||
    sourceInput?: HTMLInputElement;
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    strength = LEVELS[0];
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        this.checkPasswordStrength = this.checkPasswordStrength.bind(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
        this.input.addEventListener("keyup", this.checkPasswordStrength);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    disconnectedCallback() {
 | 
			
		||||
        this.input.removeEventListener("keyup", this.checkPasswordStrength);
 | 
			
		||||
        super.disconnectedCallback();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    checkPasswordStrength() {
 | 
			
		||||
        const { score } = zxcvbn(this.input.value);
 | 
			
		||||
        this.strength = LEVELS[score];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get input(): HTMLInputElement {
 | 
			
		||||
        if (this.sourceInput) {
 | 
			
		||||
            return this.sourceInput;
 | 
			
		||||
        }
 | 
			
		||||
        return (this.sourceInput = findInput(this.getRootNode() as Element, ELEMENT, this.src));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return html` <div class="password-meter-wrap">
 | 
			
		||||
            <div class="password-meter-bar" style=${styleMap(this.strength)}></div>
 | 
			
		||||
        </div>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default PasswordStrengthIndicator;
 | 
			
		||||
							
								
								
									
										108
									
								
								web/src/flow/stages/prompt/FieldRenderers.stories.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								web/src/flow/stages/prompt/FieldRenderers.stories.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,108 @@
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
 | 
			
		||||
import "@patternfly/patternfly/components/Alert/alert.css";
 | 
			
		||||
import "@patternfly/patternfly/components/Button/button.css";
 | 
			
		||||
import "@patternfly/patternfly/components/Check/check.css";
 | 
			
		||||
import "@patternfly/patternfly/components/Form/form.css";
 | 
			
		||||
import "@patternfly/patternfly/components/FormControl/form-control.css";
 | 
			
		||||
import "@patternfly/patternfly/components/Login/login.css";
 | 
			
		||||
import "@patternfly/patternfly/components/Title/title.css";
 | 
			
		||||
import "@patternfly/patternfly/patternfly-base.css";
 | 
			
		||||
 | 
			
		||||
import { PromptTypeEnum } from "@goauthentik/api";
 | 
			
		||||
import type { StagePrompt } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
import promptRenderers from "./FieldRenderers";
 | 
			
		||||
import { renderContinue, renderPromptHelpText, renderPromptInner } from "./helpers";
 | 
			
		||||
 | 
			
		||||
// Storybook stories are meant to show not just that the objects work, but to document good
 | 
			
		||||
// practices around using them.  Because of their uniform signature, the renderers can easily
 | 
			
		||||
// be encapsulated into containers that show them at their most functional, even without
 | 
			
		||||
// building Shadow DOMs with which to do it.  This is 100% Light DOM work, and they still
 | 
			
		||||
// work well.
 | 
			
		||||
 | 
			
		||||
const baseRenderer = (prompt: TemplateResult) =>
 | 
			
		||||
    html`<div style="background: #fff; padding: 4em; max-width: 24em;">
 | 
			
		||||
        <style>
 | 
			
		||||
            input,
 | 
			
		||||
            textarea,
 | 
			
		||||
            select,
 | 
			
		||||
            button,
 | 
			
		||||
            .pf-c-form__helper-text:not(.pf-m-error),
 | 
			
		||||
            input + label.pf-c-check__label {
 | 
			
		||||
                color: #000;
 | 
			
		||||
            }
 | 
			
		||||
            input[readonly],
 | 
			
		||||
            textarea[readonly] {
 | 
			
		||||
                color: #fff;
 | 
			
		||||
            }
 | 
			
		||||
        </style>
 | 
			
		||||
        ${prompt}
 | 
			
		||||
    </div>`;
 | 
			
		||||
 | 
			
		||||
function renderer(kind: PromptTypeEnum, prompt: Partial<StagePrompt>) {
 | 
			
		||||
    const renderer = promptRenderers.get(kind);
 | 
			
		||||
    if (!renderer) {
 | 
			
		||||
        throw new Error(`A renderer of type ${kind} does not exist.`);
 | 
			
		||||
    }
 | 
			
		||||
    return baseRenderer(html`${renderer(prompt as StagePrompt)}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const textPrompt = {
 | 
			
		||||
    fieldKey: "test_text_field",
 | 
			
		||||
    placeholder: "This is the placeholder",
 | 
			
		||||
    required: false,
 | 
			
		||||
    initialValue: "initial value",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Text = () => renderer(PromptTypeEnum.Text, textPrompt);
 | 
			
		||||
export const TextArea = () => renderer(PromptTypeEnum.TextArea, textPrompt);
 | 
			
		||||
export const TextReadOnly = () => renderer(PromptTypeEnum.TextReadOnly, textPrompt);
 | 
			
		||||
export const TextAreaReadOnly = () => renderer(PromptTypeEnum.TextAreaReadOnly, textPrompt);
 | 
			
		||||
export const Username = () => renderer(PromptTypeEnum.Username, textPrompt);
 | 
			
		||||
export const Password = () => renderer(PromptTypeEnum.Password, textPrompt);
 | 
			
		||||
 | 
			
		||||
const emailPrompt = { ...textPrompt, initialValue: "example@example.fun" };
 | 
			
		||||
export const Email = () => renderer(PromptTypeEnum.Email, emailPrompt);
 | 
			
		||||
 | 
			
		||||
const numberPrompt = { ...textPrompt, initialValue: "10" };
 | 
			
		||||
export const Number = () => renderer(PromptTypeEnum.Number, numberPrompt);
 | 
			
		||||
 | 
			
		||||
const datePrompt = { ...textPrompt, initialValue: "2018-06-12T19:30" };
 | 
			
		||||
export const Date = () => renderer(PromptTypeEnum.Date, datePrompt);
 | 
			
		||||
export const DateTime = () => renderer(PromptTypeEnum.DateTime, datePrompt);
 | 
			
		||||
 | 
			
		||||
const separatorPrompt = { placeholder: "😊" };
 | 
			
		||||
export const Separator = () => renderer(PromptTypeEnum.Separator, separatorPrompt);
 | 
			
		||||
 | 
			
		||||
const staticPrompt = { initialValue: "😊" };
 | 
			
		||||
export const Static = () => renderer(PromptTypeEnum.Static, staticPrompt);
 | 
			
		||||
 | 
			
		||||
const choicePrompt = {
 | 
			
		||||
    fieldKey: "test_text_field",
 | 
			
		||||
    placeholder: "This is the placeholder",
 | 
			
		||||
    required: false,
 | 
			
		||||
    initialValue: "first",
 | 
			
		||||
    choices: ["first", "second", "third"],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Dropdown = () => renderer(PromptTypeEnum.Dropdown, choicePrompt);
 | 
			
		||||
export const RadioButtonGroup = () => renderer(PromptTypeEnum.RadioButtonGroup, choicePrompt);
 | 
			
		||||
 | 
			
		||||
const checkPrompt = { ...textPrompt, label: "Favorite Subtext?", subText: "(Xena & Gabrielle)" };
 | 
			
		||||
export const Checkbox = () => renderer(PromptTypeEnum.Checkbox, checkPrompt);
 | 
			
		||||
 | 
			
		||||
const localePrompt = { ...textPrompt, initialValue: "en" };
 | 
			
		||||
export const Locale = () => renderer(PromptTypeEnum.AkLocale, localePrompt);
 | 
			
		||||
 | 
			
		||||
export const PromptFailure = () =>
 | 
			
		||||
    baseRenderer(renderPromptInner({ type: null } as unknown as StagePrompt));
 | 
			
		||||
 | 
			
		||||
export const HelpText = () =>
 | 
			
		||||
    baseRenderer(renderPromptHelpText({ subText: "There is no subtext here." } as StagePrompt));
 | 
			
		||||
 | 
			
		||||
export const Continue = () => baseRenderer(renderContinue());
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    title: "Flow Components/Field Renderers",
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										271
									
								
								web/src/flow/stages/prompt/FieldRenderers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										271
									
								
								web/src/flow/stages/prompt/FieldRenderers.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,271 @@
 | 
			
		||||
import { rootInterface } from "@goauthentik/elements/Base";
 | 
			
		||||
import { LOCALES } from "@goauthentik/elements/ak-locale-context/helpers";
 | 
			
		||||
import "@goauthentik/elements/password-match-indicator";
 | 
			
		||||
import "@goauthentik/elements/password-strength-indicator";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
 | 
			
		||||
 | 
			
		||||
import { CapabilitiesEnum, PromptTypeEnum, StagePrompt } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
export function password(prompt: StagePrompt) {
 | 
			
		||||
    return html`<input
 | 
			
		||||
            type="password"
 | 
			
		||||
            name="${prompt.fieldKey}"
 | 
			
		||||
            placeholder="${prompt.placeholder}"
 | 
			
		||||
            autocomplete="new-password"
 | 
			
		||||
            class="pf-c-form-control"
 | 
			
		||||
            ?required=${prompt.required}
 | 
			
		||||
        /><ak-password-strength-indicator
 | 
			
		||||
            src='input[name="${prompt.fieldKey}"]'
 | 
			
		||||
        ></ak-password-strength-indicator>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const REPEAT = /_repeat/;
 | 
			
		||||
 | 
			
		||||
export function repeatPassword(prompt: StagePrompt) {
 | 
			
		||||
    const first = `input[name="${prompt.fieldKey}"]`;
 | 
			
		||||
    const second = `input[name="${prompt.fieldKey.replace(REPEAT, "")}"]`;
 | 
			
		||||
 | 
			
		||||
    return html` <div style="display:flex; flex-direction:row; gap: 0.5em; align-content: center">
 | 
			
		||||
        <input
 | 
			
		||||
            style="flex:1 0"
 | 
			
		||||
            type="password"
 | 
			
		||||
            name="${prompt.fieldKey}"
 | 
			
		||||
            placeholder="${prompt.placeholder}"
 | 
			
		||||
            autocomplete="new-password"
 | 
			
		||||
            class="pf-c-form-control"
 | 
			
		||||
            ?required=${prompt.required}
 | 
			
		||||
        /><ak-password-match-indicator
 | 
			
		||||
            first="${first}"
 | 
			
		||||
            second="${second}"
 | 
			
		||||
        ></ak-password-match-indicator>
 | 
			
		||||
    </div>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderPassword(prompt: StagePrompt) {
 | 
			
		||||
    return REPEAT.test(prompt.fieldKey) ? repeatPassword(prompt) : password(prompt);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderText(prompt: StagePrompt) {
 | 
			
		||||
    return html`<input
 | 
			
		||||
        type="text"
 | 
			
		||||
        name="${prompt.fieldKey}"
 | 
			
		||||
        placeholder="${prompt.placeholder}"
 | 
			
		||||
        autocomplete="off"
 | 
			
		||||
        class="pf-c-form-control"
 | 
			
		||||
        ?required=${prompt.required}
 | 
			
		||||
        value="${prompt.initialValue}"
 | 
			
		||||
    />`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderTextArea(prompt: StagePrompt) {
 | 
			
		||||
    return html`<textarea
 | 
			
		||||
        name="${prompt.fieldKey}"
 | 
			
		||||
        placeholder="${prompt.placeholder}"
 | 
			
		||||
        autocomplete="off"
 | 
			
		||||
        class="pf-c-form-control"
 | 
			
		||||
        ?required=${prompt.required}
 | 
			
		||||
    >
 | 
			
		||||
${prompt.initialValue}</textarea
 | 
			
		||||
    >`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderTextReadOnly(prompt: StagePrompt) {
 | 
			
		||||
    return html`<input
 | 
			
		||||
        type="text"
 | 
			
		||||
        name="${prompt.fieldKey}"
 | 
			
		||||
        placeholder="${prompt.placeholder}"
 | 
			
		||||
        class="pf-c-form-control"
 | 
			
		||||
        ?readonly=${true}
 | 
			
		||||
        value="${prompt.initialValue}"
 | 
			
		||||
    />`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderTextAreaReadOnly(prompt: StagePrompt) {
 | 
			
		||||
    return html`<textarea
 | 
			
		||||
        name="${prompt.fieldKey}"
 | 
			
		||||
        placeholder="${prompt.placeholder}"
 | 
			
		||||
        class="pf-c-form-control"
 | 
			
		||||
        readonly
 | 
			
		||||
    >
 | 
			
		||||
${prompt.initialValue}</textarea
 | 
			
		||||
    >`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderUsername(prompt: StagePrompt) {
 | 
			
		||||
    return html`<input
 | 
			
		||||
        type="text"
 | 
			
		||||
        name="${prompt.fieldKey}"
 | 
			
		||||
        placeholder="${prompt.placeholder}"
 | 
			
		||||
        autocomplete="username"
 | 
			
		||||
        class="pf-c-form-control"
 | 
			
		||||
        ?required=${prompt.required}
 | 
			
		||||
        value="${prompt.initialValue}"
 | 
			
		||||
    />`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderEmail(prompt: StagePrompt) {
 | 
			
		||||
    return html`<input
 | 
			
		||||
        type="email"
 | 
			
		||||
        name="${prompt.fieldKey}"
 | 
			
		||||
        placeholder="${prompt.placeholder}"
 | 
			
		||||
        class="pf-c-form-control"
 | 
			
		||||
        ?required=${prompt.required}
 | 
			
		||||
        value="${prompt.initialValue}"
 | 
			
		||||
    />`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderNumber(prompt: StagePrompt) {
 | 
			
		||||
    return html`<input
 | 
			
		||||
        type="number"
 | 
			
		||||
        name="${prompt.fieldKey}"
 | 
			
		||||
        placeholder="${prompt.placeholder}"
 | 
			
		||||
        class="pf-c-form-control"
 | 
			
		||||
        ?required=${prompt.required}
 | 
			
		||||
        value="${prompt.initialValue}"
 | 
			
		||||
    />`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderDate(prompt: StagePrompt) {
 | 
			
		||||
    return html`<input
 | 
			
		||||
        type="date"
 | 
			
		||||
        name="${prompt.fieldKey}"
 | 
			
		||||
        placeholder="${prompt.placeholder}"
 | 
			
		||||
        class="pf-c-form-control"
 | 
			
		||||
        ?required=${prompt.required}
 | 
			
		||||
        value="${prompt.initialValue}"
 | 
			
		||||
    />`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderDateTime(prompt: StagePrompt) {
 | 
			
		||||
    return html`<input
 | 
			
		||||
        type="datetime"
 | 
			
		||||
        name="${prompt.fieldKey}"
 | 
			
		||||
        placeholder="${prompt.placeholder}"
 | 
			
		||||
        class="pf-c-form-control"
 | 
			
		||||
        ?required=${prompt.required}
 | 
			
		||||
        value="${prompt.initialValue}"
 | 
			
		||||
    />`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderFile(prompt: StagePrompt) {
 | 
			
		||||
    return html`<input
 | 
			
		||||
        type="file"
 | 
			
		||||
        name="${prompt.fieldKey}"
 | 
			
		||||
        placeholder="${prompt.placeholder}"
 | 
			
		||||
        class="pf-c-form-control"
 | 
			
		||||
        ?required=${prompt.required}
 | 
			
		||||
        value="${prompt.initialValue}"
 | 
			
		||||
    />`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderSeparator(prompt: StagePrompt) {
 | 
			
		||||
    return html`<ak-divider>${prompt.placeholder}</ak-divider>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderHidden(prompt: StagePrompt) {
 | 
			
		||||
    return html`<input
 | 
			
		||||
        type="hidden"
 | 
			
		||||
        name="${prompt.fieldKey}"
 | 
			
		||||
        value="${prompt.initialValue}"
 | 
			
		||||
        class="pf-c-form-control"
 | 
			
		||||
        ?required=${prompt.required}
 | 
			
		||||
    />`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderStatic(prompt: StagePrompt) {
 | 
			
		||||
    return html`<p>${unsafeHTML(prompt.initialValue)}</p>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderDropdown(prompt: StagePrompt) {
 | 
			
		||||
    return html`<select class="pf-c-form-control" name="${prompt.fieldKey}">
 | 
			
		||||
        ${prompt.choices?.map((choice) => {
 | 
			
		||||
            return html`<option value="${choice}" ?selected=${prompt.initialValue === choice}>
 | 
			
		||||
                ${choice}
 | 
			
		||||
            </option>`;
 | 
			
		||||
        })}
 | 
			
		||||
    </select>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderRadioButtonGroup(prompt: StagePrompt) {
 | 
			
		||||
    return html`${(prompt.choices || []).map((choice) => {
 | 
			
		||||
        const id = `${prompt.fieldKey}-${choice}`;
 | 
			
		||||
        return html`<div class="pf-c-check">
 | 
			
		||||
            <input
 | 
			
		||||
                type="radio"
 | 
			
		||||
                class="pf-c-check__input"
 | 
			
		||||
                name="${prompt.fieldKey}"
 | 
			
		||||
                id="${id}"
 | 
			
		||||
                ?checked="${prompt.initialValue === choice}"
 | 
			
		||||
                ?required="${prompt.required}"
 | 
			
		||||
                value="${choice}"
 | 
			
		||||
            />
 | 
			
		||||
            <label class="pf-c-check__label" for=${id}>${choice}</label>
 | 
			
		||||
        </div> `;
 | 
			
		||||
    })}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderCheckbox(prompt: StagePrompt) {
 | 
			
		||||
    return html`<div class="pf-c-check">
 | 
			
		||||
        <input
 | 
			
		||||
            type="checkbox"
 | 
			
		||||
            class="pf-c-check__input"
 | 
			
		||||
            id="${prompt.fieldKey}"
 | 
			
		||||
            name="${prompt.fieldKey}"
 | 
			
		||||
            ?checked=${prompt.initialValue !== ""}
 | 
			
		||||
            ?required=${prompt.required}
 | 
			
		||||
        />
 | 
			
		||||
        <label class="pf-c-check__label" for="${prompt.fieldKey}">${prompt.label}</label>
 | 
			
		||||
        ${prompt.required
 | 
			
		||||
            ? html`<p class="pf-c-form__helper-text">${msg("Required.")}</p>`
 | 
			
		||||
            : html``}
 | 
			
		||||
        <p class="pf-c-form__helper-text">${unsafeHTML(prompt.subText)}</p>
 | 
			
		||||
    </div>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderAkLocale(prompt: StagePrompt) {
 | 
			
		||||
    // TODO: External reference.
 | 
			
		||||
    const inDebug = rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanDebug);
 | 
			
		||||
    const locales = inDebug ? LOCALES : LOCALES.filter((locale) => locale.code !== "debug");
 | 
			
		||||
 | 
			
		||||
    const options = locales.map(
 | 
			
		||||
        (locale) =>
 | 
			
		||||
            html`<option value=${locale.code} ?selected=${locale.code === prompt.initialValue}>
 | 
			
		||||
                ${locale.code.toUpperCase()} - ${locale.label()}
 | 
			
		||||
            </option> `,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return html`<select class="pf-c-form-control" name="${prompt.fieldKey}">
 | 
			
		||||
        <option value="" ?selected=${prompt.initialValue === ""}>
 | 
			
		||||
            ${msg("Auto-detect (based on your browser)")}
 | 
			
		||||
        </option>
 | 
			
		||||
        ${options}
 | 
			
		||||
    </select>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Renderer = (prompt: StagePrompt) => TemplateResult;
 | 
			
		||||
 | 
			
		||||
export const promptRenderers = new Map<PromptTypeEnum, Renderer>([
 | 
			
		||||
    [PromptTypeEnum.Text, renderText],
 | 
			
		||||
    [PromptTypeEnum.TextArea, renderTextArea],
 | 
			
		||||
    [PromptTypeEnum.TextReadOnly, renderTextReadOnly],
 | 
			
		||||
    [PromptTypeEnum.TextAreaReadOnly, renderTextAreaReadOnly],
 | 
			
		||||
    [PromptTypeEnum.Username, renderUsername],
 | 
			
		||||
    [PromptTypeEnum.Email, renderEmail],
 | 
			
		||||
    [PromptTypeEnum.Password, renderPassword],
 | 
			
		||||
    [PromptTypeEnum.Number, renderNumber],
 | 
			
		||||
    [PromptTypeEnum.Date, renderDate],
 | 
			
		||||
    [PromptTypeEnum.DateTime, renderDateTime],
 | 
			
		||||
    [PromptTypeEnum.File, renderFile],
 | 
			
		||||
    [PromptTypeEnum.Separator, renderSeparator],
 | 
			
		||||
    [PromptTypeEnum.Hidden, renderHidden],
 | 
			
		||||
    [PromptTypeEnum.Static, renderStatic],
 | 
			
		||||
    [PromptTypeEnum.Dropdown, renderDropdown],
 | 
			
		||||
    [PromptTypeEnum.RadioButtonGroup, renderRadioButtonGroup],
 | 
			
		||||
    [PromptTypeEnum.Checkbox, renderCheckbox],
 | 
			
		||||
    [PromptTypeEnum.AkLocale, renderAkLocale],
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
export default promptRenderers;
 | 
			
		||||
@ -1,17 +1,12 @@
 | 
			
		||||
import "@goauthentik/elements/Divider";
 | 
			
		||||
import "@goauthentik/elements/EmptyState";
 | 
			
		||||
import {
 | 
			
		||||
    CapabilitiesEnum,
 | 
			
		||||
    WithCapabilitiesConfig,
 | 
			
		||||
} from "@goauthentik/elements/Interface/capabilitiesProvider";
 | 
			
		||||
import { LOCALES } from "@goauthentik/elements/ak-locale-context/definitions";
 | 
			
		||||
import { WithCapabilitiesConfig } from "@goauthentik/elements/Interface/capabilitiesProvider";
 | 
			
		||||
import "@goauthentik/elements/forms/FormElement";
 | 
			
		||||
import { BaseStage } from "@goauthentik/flow/stages/base";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { CSSResult, TemplateResult, css, html } from "lit";
 | 
			
		||||
import { customElement } from "lit/decorators.js";
 | 
			
		||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
 | 
			
		||||
 | 
			
		||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
 | 
			
		||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
 | 
			
		||||
@ -29,6 +24,14 @@ import {
 | 
			
		||||
    StagePrompt,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
import { renderCheckbox } from "./FieldRenderers";
 | 
			
		||||
import {
 | 
			
		||||
    renderContinue,
 | 
			
		||||
    renderPromptHelpText,
 | 
			
		||||
    renderPromptInner,
 | 
			
		||||
    shouldRenderInWrapper,
 | 
			
		||||
} from "./helpers";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-prompt")
 | 
			
		||||
export class PromptStage extends WithCapabilitiesConfig(
 | 
			
		||||
    BaseStage<PromptChallenge, PromptChallengeResponseRequest>,
 | 
			
		||||
@ -53,232 +56,35 @@ export class PromptStage extends WithCapabilitiesConfig(
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderPromptInner(prompt: StagePrompt): TemplateResult {
 | 
			
		||||
        switch (prompt.type) {
 | 
			
		||||
            case PromptTypeEnum.Text:
 | 
			
		||||
                return html`<input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    name="${prompt.fieldKey}"
 | 
			
		||||
                    placeholder="${prompt.placeholder}"
 | 
			
		||||
                    autocomplete="off"
 | 
			
		||||
                    class="pf-c-form-control"
 | 
			
		||||
                    ?required=${prompt.required}
 | 
			
		||||
                    value="${prompt.initialValue}"
 | 
			
		||||
                />`;
 | 
			
		||||
            case PromptTypeEnum.TextArea:
 | 
			
		||||
                return html`<textarea
 | 
			
		||||
                    name="${prompt.fieldKey}"
 | 
			
		||||
                    placeholder="${prompt.placeholder}"
 | 
			
		||||
                    autocomplete="off"
 | 
			
		||||
                    class="pf-c-form-control"
 | 
			
		||||
                    ?required=${prompt.required}
 | 
			
		||||
                >
 | 
			
		||||
${prompt.initialValue}</textarea
 | 
			
		||||
                >`;
 | 
			
		||||
            case PromptTypeEnum.TextReadOnly:
 | 
			
		||||
                return html`<input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    name="${prompt.fieldKey}"
 | 
			
		||||
                    placeholder="${prompt.placeholder}"
 | 
			
		||||
                    class="pf-c-form-control"
 | 
			
		||||
                    ?readonly=${true}
 | 
			
		||||
                    value="${prompt.initialValue}"
 | 
			
		||||
                />`;
 | 
			
		||||
            case PromptTypeEnum.TextAreaReadOnly:
 | 
			
		||||
                return html`<textarea
 | 
			
		||||
                    name="${prompt.fieldKey}"
 | 
			
		||||
                    placeholder="${prompt.placeholder}"
 | 
			
		||||
                    class="pf-c-form-control"
 | 
			
		||||
                    readonly
 | 
			
		||||
                >
 | 
			
		||||
${prompt.initialValue}</textarea
 | 
			
		||||
                >`;
 | 
			
		||||
            case PromptTypeEnum.Username:
 | 
			
		||||
                return html`<input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    name="${prompt.fieldKey}"
 | 
			
		||||
                    placeholder="${prompt.placeholder}"
 | 
			
		||||
                    autocomplete="username"
 | 
			
		||||
                    class="pf-c-form-control"
 | 
			
		||||
                    ?required=${prompt.required}
 | 
			
		||||
                    value="${prompt.initialValue}"
 | 
			
		||||
                />`;
 | 
			
		||||
            case PromptTypeEnum.Email:
 | 
			
		||||
                return html`<input
 | 
			
		||||
                    type="email"
 | 
			
		||||
                    name="${prompt.fieldKey}"
 | 
			
		||||
                    placeholder="${prompt.placeholder}"
 | 
			
		||||
                    class="pf-c-form-control"
 | 
			
		||||
                    ?required=${prompt.required}
 | 
			
		||||
                    value="${prompt.initialValue}"
 | 
			
		||||
                />`;
 | 
			
		||||
            case PromptTypeEnum.Password:
 | 
			
		||||
                return html`<input
 | 
			
		||||
                    type="password"
 | 
			
		||||
                    name="${prompt.fieldKey}"
 | 
			
		||||
                    placeholder="${prompt.placeholder}"
 | 
			
		||||
                    autocomplete="new-password"
 | 
			
		||||
                    class="pf-c-form-control"
 | 
			
		||||
                    ?required=${prompt.required}
 | 
			
		||||
                />`;
 | 
			
		||||
            case PromptTypeEnum.Number:
 | 
			
		||||
                return html`<input
 | 
			
		||||
                    type="number"
 | 
			
		||||
                    name="${prompt.fieldKey}"
 | 
			
		||||
                    placeholder="${prompt.placeholder}"
 | 
			
		||||
                    class="pf-c-form-control"
 | 
			
		||||
                    ?required=${prompt.required}
 | 
			
		||||
                    value="${prompt.initialValue}"
 | 
			
		||||
                />`;
 | 
			
		||||
            case PromptTypeEnum.Date:
 | 
			
		||||
                return html`<input
 | 
			
		||||
                    type="date"
 | 
			
		||||
                    name="${prompt.fieldKey}"
 | 
			
		||||
                    placeholder="${prompt.placeholder}"
 | 
			
		||||
                    class="pf-c-form-control"
 | 
			
		||||
                    ?required=${prompt.required}
 | 
			
		||||
                    value="${prompt.initialValue}"
 | 
			
		||||
                />`;
 | 
			
		||||
            case PromptTypeEnum.DateTime:
 | 
			
		||||
                return html`<input
 | 
			
		||||
                    type="datetime"
 | 
			
		||||
                    name="${prompt.fieldKey}"
 | 
			
		||||
                    placeholder="${prompt.placeholder}"
 | 
			
		||||
                    class="pf-c-form-control"
 | 
			
		||||
                    ?required=${prompt.required}
 | 
			
		||||
                    value="${prompt.initialValue}"
 | 
			
		||||
                />`;
 | 
			
		||||
            case PromptTypeEnum.File:
 | 
			
		||||
                return html`<input
 | 
			
		||||
                    type="file"
 | 
			
		||||
                    name="${prompt.fieldKey}"
 | 
			
		||||
                    placeholder="${prompt.placeholder}"
 | 
			
		||||
                    class="pf-c-form-control"
 | 
			
		||||
                    ?required=${prompt.required}
 | 
			
		||||
                    value="${prompt.initialValue}"
 | 
			
		||||
                />`;
 | 
			
		||||
            case PromptTypeEnum.Separator:
 | 
			
		||||
                return html`<ak-divider>${prompt.placeholder}</ak-divider>`;
 | 
			
		||||
            case PromptTypeEnum.Hidden:
 | 
			
		||||
                return html`<input
 | 
			
		||||
                    type="hidden"
 | 
			
		||||
                    name="${prompt.fieldKey}"
 | 
			
		||||
                    value="${prompt.initialValue}"
 | 
			
		||||
                    class="pf-c-form-control"
 | 
			
		||||
                    ?required=${prompt.required}
 | 
			
		||||
                />`;
 | 
			
		||||
            case PromptTypeEnum.Static:
 | 
			
		||||
                return html`<p>${unsafeHTML(prompt.initialValue)}</p>`;
 | 
			
		||||
            case PromptTypeEnum.Dropdown:
 | 
			
		||||
                return html`<select class="pf-c-form-control" name="${prompt.fieldKey}">
 | 
			
		||||
                    ${prompt.choices?.map((choice) => {
 | 
			
		||||
                        return html`<option
 | 
			
		||||
                            value="${choice}"
 | 
			
		||||
                            ?selected=${prompt.initialValue === choice}
 | 
			
		||||
                        >
 | 
			
		||||
                            ${choice}
 | 
			
		||||
                        </option>`;
 | 
			
		||||
                    })}
 | 
			
		||||
                </select>`;
 | 
			
		||||
            case PromptTypeEnum.RadioButtonGroup:
 | 
			
		||||
                return html`${(prompt.choices || []).map((choice) => {
 | 
			
		||||
                    const id = `${prompt.fieldKey}-${choice}`;
 | 
			
		||||
                    return html`<div class="pf-c-check">
 | 
			
		||||
                        <input
 | 
			
		||||
                            type="radio"
 | 
			
		||||
                            class="pf-c-check__input"
 | 
			
		||||
                            name="${prompt.fieldKey}"
 | 
			
		||||
                            id="${id}"
 | 
			
		||||
                            ?checked="${prompt.initialValue === choice}"
 | 
			
		||||
                            ?required="${prompt.required}"
 | 
			
		||||
                            value="${choice}"
 | 
			
		||||
                        />
 | 
			
		||||
                        <label class="pf-c-check__label" for=${id}>${choice}</label>
 | 
			
		||||
                    </div> `;
 | 
			
		||||
                })}`;
 | 
			
		||||
            case PromptTypeEnum.AkLocale: {
 | 
			
		||||
                const locales = this.can(CapabilitiesEnum.CanDebug)
 | 
			
		||||
                    ? LOCALES
 | 
			
		||||
                    : LOCALES.filter((locale) => locale.code !== "debug");
 | 
			
		||||
                const options = locales.map(
 | 
			
		||||
                    (locale) =>
 | 
			
		||||
                        html`<option
 | 
			
		||||
                            value=${locale.code}
 | 
			
		||||
                            ?selected=${locale.code === prompt.initialValue}
 | 
			
		||||
                        >
 | 
			
		||||
                            ${locale.code.toUpperCase()} - ${locale.label()}
 | 
			
		||||
                        </option> `,
 | 
			
		||||
                );
 | 
			
		||||
    /* TODO: Legacy: None of these refer to the `this` field. Static fields are a code smell. */
 | 
			
		||||
 | 
			
		||||
                return html`<select class="pf-c-form-control" name="${prompt.fieldKey}">
 | 
			
		||||
                    <option value="" ?selected=${prompt.initialValue === ""}>
 | 
			
		||||
                        ${msg("Auto-detect (based on your browser)")}
 | 
			
		||||
                    </option>
 | 
			
		||||
                    ${options}
 | 
			
		||||
                </select>`;
 | 
			
		||||
    renderPromptInner(prompt: StagePrompt) {
 | 
			
		||||
        return renderPromptInner(prompt);
 | 
			
		||||
    }
 | 
			
		||||
            default:
 | 
			
		||||
                return html`<p>invalid type '${prompt.type}'</p>`;
 | 
			
		||||
    renderPromptHelpText(prompt: StagePrompt) {
 | 
			
		||||
        return renderPromptHelpText(prompt);
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderPromptHelpText(prompt: StagePrompt): TemplateResult {
 | 
			
		||||
        if (prompt.subText === "") {
 | 
			
		||||
            return html``;
 | 
			
		||||
        }
 | 
			
		||||
        return html`<p class="pf-c-form__helper-text">${unsafeHTML(prompt.subText)}</p>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    shouldRenderInWrapper(prompt: StagePrompt): boolean {
 | 
			
		||||
        // Special types that aren't rendered in a wrapper
 | 
			
		||||
        if (
 | 
			
		||||
            prompt.type === PromptTypeEnum.Static ||
 | 
			
		||||
            prompt.type === PromptTypeEnum.Hidden ||
 | 
			
		||||
            prompt.type === PromptTypeEnum.Separator
 | 
			
		||||
        ) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    shouldRenderInWrapper(prompt: StagePrompt) {
 | 
			
		||||
        return shouldRenderInWrapper(prompt);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderField(prompt: StagePrompt): TemplateResult {
 | 
			
		||||
        // Checkbox is rendered differently
 | 
			
		||||
        // Checkbox has a slightly different layout, so it must be intercepted early.
 | 
			
		||||
        if (prompt.type === PromptTypeEnum.Checkbox) {
 | 
			
		||||
            return html`<div class="pf-c-check">
 | 
			
		||||
                <input
 | 
			
		||||
                    type="checkbox"
 | 
			
		||||
                    class="pf-c-check__input"
 | 
			
		||||
                    id="${prompt.fieldKey}"
 | 
			
		||||
                    name="${prompt.fieldKey}"
 | 
			
		||||
                    ?checked=${prompt.initialValue !== ""}
 | 
			
		||||
                    ?required=${prompt.required}
 | 
			
		||||
                />
 | 
			
		||||
                <label class="pf-c-check__label" for="${prompt.fieldKey}">${prompt.label}</label>
 | 
			
		||||
                ${prompt.required
 | 
			
		||||
                    ? html`<p class="pf-c-form__helper-text">${msg("Required.")}</p>`
 | 
			
		||||
                    : html``}
 | 
			
		||||
                <p class="pf-c-form__helper-text">${unsafeHTML(prompt.subText)}</p>
 | 
			
		||||
            </div>`;
 | 
			
		||||
            return renderCheckbox(prompt);
 | 
			
		||||
        }
 | 
			
		||||
        if (this.shouldRenderInWrapper(prompt)) {
 | 
			
		||||
 | 
			
		||||
        if (shouldRenderInWrapper(prompt)) {
 | 
			
		||||
            return html`<ak-form-element
 | 
			
		||||
                label="${prompt.label}"
 | 
			
		||||
                ?required="${prompt.required}"
 | 
			
		||||
                class="pf-c-form__group"
 | 
			
		||||
                .errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}
 | 
			
		||||
            >
 | 
			
		||||
                ${this.renderPromptInner(prompt)} ${this.renderPromptHelpText(prompt)}
 | 
			
		||||
                ${renderPromptInner(prompt)} ${renderPromptHelpText(prompt)}
 | 
			
		||||
            </ak-form-element>`;
 | 
			
		||||
        }
 | 
			
		||||
        return html` ${this.renderPromptInner(prompt)} ${this.renderPromptHelpText(prompt)}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderContinue(): TemplateResult {
 | 
			
		||||
        return html` <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
            <button type="submit" class="pf-c-button pf-m-primary pf-m-block">
 | 
			
		||||
                ${msg("Continue")}
 | 
			
		||||
            </button>
 | 
			
		||||
        </div>`;
 | 
			
		||||
        return html` ${renderPromptInner(prompt)} ${renderPromptHelpText(prompt)}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
@ -286,6 +92,7 @@ ${prompt.initialValue}</textarea
 | 
			
		||||
            return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}>
 | 
			
		||||
            </ak-empty-state>`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return html`<header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
 | 
			
		||||
            </header>
 | 
			
		||||
@ -304,7 +111,7 @@ ${prompt.initialValue}</textarea
 | 
			
		||||
                              this.challenge?.responseErrors?.non_field_errors || [],
 | 
			
		||||
                          )
 | 
			
		||||
                        : html``}
 | 
			
		||||
                    ${this.renderContinue()}
 | 
			
		||||
                    ${renderContinue()}
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
            <footer class="pf-c-login__main-footer">
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										37
									
								
								web/src/flow/stages/prompt/helpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								web/src/flow/stages/prompt/helpers.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { html } from "lit";
 | 
			
		||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
 | 
			
		||||
 | 
			
		||||
import { PromptTypeEnum, StagePrompt } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
import promptRenderers from "./FieldRenderers";
 | 
			
		||||
 | 
			
		||||
export function renderPromptInner(prompt: StagePrompt) {
 | 
			
		||||
    const renderer = promptRenderers.get(prompt.type);
 | 
			
		||||
    if (!renderer) {
 | 
			
		||||
        return html`<p>invalid type '${JSON.stringify(prompt.type, null, 2)}'</p>`;
 | 
			
		||||
    }
 | 
			
		||||
    return renderer(prompt);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderPromptHelpText(prompt: StagePrompt) {
 | 
			
		||||
    if (prompt.subText === "") {
 | 
			
		||||
        return html``;
 | 
			
		||||
    }
 | 
			
		||||
    return html`<p class="pf-c-form__helper-text">${unsafeHTML(prompt.subText)}</p>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function shouldRenderInWrapper(prompt: StagePrompt) {
 | 
			
		||||
    // Special types that aren't rendered in a wrapper
 | 
			
		||||
    const specialTypes = [PromptTypeEnum.Static, PromptTypeEnum.Hidden, PromptTypeEnum.Separator];
 | 
			
		||||
    const special = specialTypes.find((s) => s === prompt.type);
 | 
			
		||||
    return !special;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderContinue() {
 | 
			
		||||
    return html` <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
        <button type="submit" class="pf-c-button pf-m-primary pf-m-block">
 | 
			
		||||
            ${msg("Continue")}
 | 
			
		||||
        </button>
 | 
			
		||||
    </div>`;
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user