Compare commits
	
		
			9 Commits
		
	
	
		
			providers/
			...
			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`. | ||||
|  | ||||
							
								
								
									
										6667
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6667
									
								
								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(); | ||||
|     } | ||||
|  | ||||
|     async runCallAction() { | ||||
|         await this.callAction(); | ||||
|         return initialState; | ||||
|     } | ||||
|  | ||||
|     onClick() { | ||||
|         if (this.actionTask.status !== TaskStatus.INITIAL) { | ||||
|             return; | ||||
|         } | ||||
|         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>`; | ||||
|             } | ||||
|             default: | ||||
|                 return html`<p>invalid type '${prompt.type}'</p>`; | ||||
|         } | ||||
|     renderPromptInner(prompt: StagePrompt) { | ||||
|         return renderPromptInner(prompt); | ||||
|     } | ||||
|  | ||||
|     renderPromptHelpText(prompt: StagePrompt): TemplateResult { | ||||
|         if (prompt.subText === "") { | ||||
|             return html``; | ||||
|         } | ||||
|         return html`<p class="pf-c-form__helper-text">${unsafeHTML(prompt.subText)}</p>`; | ||||
|     renderPromptHelpText(prompt: StagePrompt) { | ||||
|         return renderPromptHelpText(prompt); | ||||
|     } | ||||
|  | ||||
|     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
	