Compare commits
7 Commits
trustchain
...
5165-passw
Author | SHA1 | Date |
---|---|---|
Ken Sternberg | 465820b002 | |
Ken Sternberg | a75c9434d9 | |
Ken Sternberg | 4ea9b69ab5 | |
Ken Sternberg | c48eee0ebf | |
Ken Sternberg | 0d94373f10 | |
Ken Sternberg | 1c85dc512f | |
Ken Sternberg | a71778651f |
|
@ -3,6 +3,15 @@
|
||||||
This is the default UI for the authentik server. The documentation is going to be a little sparse
|
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.
|
for awhile, but at least let's get started.
|
||||||
|
|
||||||
|
# Standards
|
||||||
|
|
||||||
|
- Be flexible in what you accept as input, be precise in what you produce as output.
|
||||||
|
- Mis-use is always a crash. A component that takes the ID of an HTMLInputElement as an argument
|
||||||
|
should throw an exception if the element is anything but an HTMLInputElement ("anything" includes
|
||||||
|
non-existent, null, undefined, etc.).
|
||||||
|
- Single Responsibility is ideal, but not always practical. To the best of your obility, every
|
||||||
|
object in the system should do one thing and do it well.
|
||||||
|
|
||||||
# Comments
|
# Comments
|
||||||
|
|
||||||
**NOTE:** The comments in this section are for specific changes to this repository that cannot be
|
**NOTE:** The comments in this section are for specific changes to this repository that cannot be
|
||||||
|
@ -21,3 +30,7 @@ settings in JSON files, which do not support comments.
|
||||||
- `compilerOptions.plugins.ts-lit-plugin.rules.no-incompatible-type-binding: "warn"`: lit-analyzer
|
- `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
|
does not support generics well when parsing a subtype of `HTMLElement`. As a result, this threw
|
||||||
too many errors to be supportable.
|
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`.
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -16,6 +16,7 @@
|
||||||
"watch": "run-s build-locales rollup:watch",
|
"watch": "run-s build-locales rollup:watch",
|
||||||
"lint": "eslint . --max-warnings 0 --fix",
|
"lint": "eslint . --max-warnings 0 --fix",
|
||||||
"lit-analyse": "lit-analyzer src",
|
"lit-analyse": "lit-analyzer src",
|
||||||
|
"precommit": "run-s build-locales lint tsc prettier",
|
||||||
"prettier-check": "prettier --check .",
|
"prettier-check": "prettier --check .",
|
||||||
"prettier": "prettier --write .",
|
"prettier": "prettier --write .",
|
||||||
"tsc:execute": "tsc --noEmit -p .",
|
"tsc:execute": "tsc --noEmit -p .",
|
||||||
|
@ -52,7 +53,8 @@
|
||||||
"rapidoc": "^9.3.4",
|
"rapidoc": "^9.3.4",
|
||||||
"style-mod": "^4.0.3",
|
"style-mod": "^4.0.3",
|
||||||
"webcomponent-qr-code": "^1.1.1",
|
"webcomponent-qr-code": "^1.1.1",
|
||||||
"yaml": "^2.3.1"
|
"yaml": "^2.3.1",
|
||||||
|
"zxcvbn": "^4.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.22.7",
|
"@babel/core": "^7.22.7",
|
||||||
|
@ -80,6 +82,7 @@
|
||||||
"@types/chart.js": "^2.9.37",
|
"@types/chart.js": "^2.9.37",
|
||||||
"@types/codemirror": "5.60.8",
|
"@types/codemirror": "5.60.8",
|
||||||
"@types/grecaptcha": "^3.0.4",
|
"@types/grecaptcha": "^3.0.4",
|
||||||
|
"@types/zxcvbn": "^4.4.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||||
"@typescript-eslint/parser": "^5.61.0",
|
"@typescript-eslint/parser": "^5.61.0",
|
||||||
"babel-plugin-macros": "^3.1.0",
|
"babel-plugin-macros": "^3.1.0",
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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",
|
||||||
|
};
|
|
@ -0,0 +1,272 @@
|
||||||
|
import { LOCALES } from "@goauthentik/common/ui/locale";
|
||||||
|
import { rootInterface } from "@goauthentik/elements/Base";
|
||||||
|
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,5 +1,3 @@
|
||||||
import { LOCALES } from "@goauthentik/common/ui/locale";
|
|
||||||
import { rootInterface } from "@goauthentik/elements/Base";
|
|
||||||
import "@goauthentik/elements/Divider";
|
import "@goauthentik/elements/Divider";
|
||||||
import "@goauthentik/elements/EmptyState";
|
import "@goauthentik/elements/EmptyState";
|
||||||
import "@goauthentik/elements/forms/FormElement";
|
import "@goauthentik/elements/forms/FormElement";
|
||||||
|
@ -8,7 +6,6 @@ import { BaseStage } from "@goauthentik/flow/stages/base";
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
import { customElement } from "lit/decorators.js";
|
import { customElement } from "lit/decorators.js";
|
||||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
|
||||||
|
|
||||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
|
@ -20,13 +17,20 @@ import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CapabilitiesEnum,
|
|
||||||
PromptChallenge,
|
PromptChallenge,
|
||||||
PromptChallengeResponseRequest,
|
PromptChallengeResponseRequest,
|
||||||
PromptTypeEnum,
|
PromptTypeEnum,
|
||||||
StagePrompt,
|
StagePrompt,
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
|
import { renderCheckbox } from "./FieldRenderers";
|
||||||
|
import {
|
||||||
|
renderContinue,
|
||||||
|
renderPromptHelpText,
|
||||||
|
renderPromptInner,
|
||||||
|
shouldRenderInWrapper,
|
||||||
|
} from "./helpers";
|
||||||
|
|
||||||
@customElement("ak-stage-prompt")
|
@customElement("ak-stage-prompt")
|
||||||
export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeResponseRequest> {
|
export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeResponseRequest> {
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
|
@ -49,234 +53,35 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPromptInner(prompt: StagePrompt): TemplateResult {
|
/* TODO: Legacy: None of these refer to the `this` field. Static fields are a code smell. */
|
||||||
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 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}">
|
renderPromptInner(prompt: StagePrompt) {
|
||||||
<option value="" ?selected=${prompt.initialValue === ""}>
|
return renderPromptInner(prompt);
|
||||||
${msg("Auto-detect (based on your browser)")}
|
|
||||||
</option>
|
|
||||||
${options}
|
|
||||||
</select>`;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return html`<p>invalid type '${prompt.type}'</p>`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
renderPromptHelpText(prompt: StagePrompt) {
|
||||||
renderPromptHelpText(prompt: StagePrompt): TemplateResult {
|
return renderPromptHelpText(prompt);
|
||||||
if (prompt.subText === "") {
|
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
return html`<p class="pf-c-form__helper-text">${unsafeHTML(prompt.subText)}</p>`;
|
|
||||||
}
|
}
|
||||||
|
shouldRenderInWrapper(prompt: StagePrompt) {
|
||||||
shouldRenderInWrapper(prompt: StagePrompt): boolean {
|
return shouldRenderInWrapper(prompt);
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderField(prompt: StagePrompt): TemplateResult {
|
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) {
|
if (prompt.type === PromptTypeEnum.Checkbox) {
|
||||||
return html`<div class="pf-c-check">
|
return renderCheckbox(prompt);
|
||||||
<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>`;
|
|
||||||
}
|
}
|
||||||
if (this.shouldRenderInWrapper(prompt)) {
|
|
||||||
|
if (shouldRenderInWrapper(prompt)) {
|
||||||
return html`<ak-form-element
|
return html`<ak-form-element
|
||||||
label="${prompt.label}"
|
label="${prompt.label}"
|
||||||
?required="${prompt.required}"
|
?required="${prompt.required}"
|
||||||
class="pf-c-form__group"
|
class="pf-c-form__group"
|
||||||
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}
|
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}
|
||||||
>
|
>
|
||||||
${this.renderPromptInner(prompt)} ${this.renderPromptHelpText(prompt)}
|
${renderPromptInner(prompt)} ${renderPromptHelpText(prompt)}
|
||||||
</ak-form-element>`;
|
</ak-form-element>`;
|
||||||
}
|
}
|
||||||
return html` ${this.renderPromptInner(prompt)} ${this.renderPromptHelpText(prompt)}`;
|
return html` ${renderPromptInner(prompt)} ${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>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
|
@ -284,6 +89,7 @@ ${prompt.initialValue}</textarea
|
||||||
return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}>
|
return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}>
|
||||||
</ak-empty-state>`;
|
</ak-empty-state>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`<header class="pf-c-login__main-header">
|
return html`<header class="pf-c-login__main-header">
|
||||||
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
|
||||||
</header>
|
</header>
|
||||||
|
@ -302,7 +108,7 @@ ${prompt.initialValue}</textarea
|
||||||
this.challenge?.responseErrors?.non_field_errors || [],
|
this.challenge?.responseErrors?.non_field_errors || [],
|
||||||
)
|
)
|
||||||
: html``}
|
: html``}
|
||||||
${this.renderContinue()}
|
${renderContinue()}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<footer class="pf-c-login__main-footer">
|
<footer class="pf-c-login__main-footer">
|
||||||
|
|
|
@ -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