Compare commits

...

2 Commits

Author SHA1 Message Date
Teffen Ellis
d019488bec web: Flesh out JSX components. 2025-07-31 17:48:30 +02:00
Teffen Ellis
6694529317 web: Fix issues surrounding label a11y. Flesh out functional components. 2025-07-31 01:28:41 +02:00
29 changed files with 479 additions and 226 deletions

23
web/package-lock.json generated
View File

@@ -27,6 +27,7 @@
"@goauthentik/core": "^1.0.0",
"@goauthentik/esbuild-plugin-live-reload": "^1.1.0",
"@goauthentik/eslint-config": "^1.0.5",
"@goauthentik/lit-jsx": "^1.0.0",
"@goauthentik/prettier-config": "^3.1.0",
"@goauthentik/tsconfig": "^1.0.4",
"@hcaptcha/types": "^1.0.4",
@@ -1548,6 +1549,10 @@
}
}
},
"node_modules/@goauthentik/lit-jsx": {
"resolved": "packages/lit-jsx",
"link": true
},
"node_modules/@goauthentik/prettier-config": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@goauthentik/prettier-config/-/prettier-config-3.1.0.tgz",
@@ -26972,6 +26977,24 @@
"esbuild": "^0.25.5"
}
},
"packages/lit-jsx": {
"name": "@goauthentik/lit-jsx",
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"@goauthentik/tsconfig": "^1.0.4",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"lit": "^3.3.1",
"typescript": "^5.8.3"
},
"engines": {
"node": ">=24"
},
"peerDependencies": {
"lit": "^3.3.1"
}
},
"packages/monorepo": {
"name": "@goauthentik/monorepo",
"version": "1.0.0",

View File

@@ -98,6 +98,7 @@
"@goauthentik/core": "^1.0.0",
"@goauthentik/esbuild-plugin-live-reload": "^1.1.0",
"@goauthentik/eslint-config": "^1.0.5",
"@goauthentik/lit-jsx": "^1.0.0",
"@goauthentik/prettier-config": "^3.1.0",
"@goauthentik/tsconfig": "^1.0.4",
"@hcaptcha/types": "^1.0.4",

31
web/packages/lit-jsx/jsx-runtime.d.ts vendored Normal file
View File

@@ -0,0 +1,31 @@
import { JSX } from "react";
import { nothing, TemplateResult } from "lit";
export = LitJSX;
export as namespace LitJSX;
declare namespace LitJSX {
export type CustomElementConstructor<T extends HTMLElement = HTMLElement> = new (
...args: any[]
) => T;
export type CustomElementComponent<P = any> = (props: P) => ElementType;
export type ElementType<
P = any,
Tag extends keyof React.JSX.IntrinsicElements = keyof React.JSX.IntrinsicElements,
> =
| { [K in Tag]: P extends React.JSX.IntrinsicElements[K] ? K : never }[Tag]
| CustomElementConstructor<P>;
export interface Fragment {
children: LitJSX.ElementType | LitJSX.ElementType[];
}
export type LitNode = JSX.Element | ElementType | string | TemplateResult | typeof nothing;
export type FC<P = any> = (props: P) => LitNode | LitNode[];
export { JSX };
}

View File

@@ -0,0 +1,47 @@
import { createElement } from "./utils.js";
/**
* @param {unknown} elementLike
* @returns {elementLike is LitJSX.CustomElementConstructor}
*/
function isCustomElementConstructor(elementLike) {
return typeof elementLike === "function" && elementLike.prototype instanceof HTMLElement;
}
/**
*
* @param {LitJSX.ElementType | LitJSX.CustomElementComponent} elementLike
* @param {unknown} props
*/
export function jsx(elementLike, props) {
console.log({ elementLike, props });
if (typeof elementLike === "function") {
if (isCustomElementConstructor(elementLike)) {
const tagName = customElements.getName(elementLike);
if (!tagName) {
throw new Error(`Custom element ${elementLike.name} is not registered`);
}
// Render the custom web component as any other html element.
return createElement(tagName, props);
}
// @ts-ignore
return elementLike(props);
}
// @ts-ignore
return createElement(elementLike, props);
}
export { jsx as jsxs };
/**
*
* @param {LitJSX.Fragment} fragment
* @returns {LitJSX.ElementType[]}
*/
export function Fragment(fragment) {
return Array.isArray(fragment.children) ? fragment.children : [fragment.children];
}

View File

@@ -0,0 +1,32 @@
{
"name": "@goauthentik/lit-jsx",
"version": "1.0.0",
"description": "JSX Runtime for Lit",
"license": "MIT",
"main": "tsconfig.json",
"type": "module",
"exports": {
"./package.json": "./package.json",
".": {
"import": "./index.js",
"types": "./index.d.ts"
},
"./jsx-runtime": "./jsx-runtime.js"
},
"devDependencies": {
"@goauthentik/tsconfig": "^1.0.4",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"lit": "^3.3.1",
"typescript": "^5.8.3"
},
"peerDependencies": {
"lit": "^3.3.1"
},
"engines": {
"node": ">=24"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,9 @@
{
"extends": "@goauthentik/tsconfig",
"compilerOptions": {
"baseUrl": ".",
"checkJs": true,
"emitDeclarationOnly": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"]
}
}

View File

@@ -0,0 +1,63 @@
import { spread } from "@open-wc/lit-helpers";
import { ifDefined } from "lit/directives/if-defined.js";
import { ref } from "lit/directives/ref.js";
import { html, unsafeStatic } from "lit/static-html.js";
/**
*
* @param {string} tagName
* @param {Record<PropertyKey, unknown>} props
* @returns
*/
export function parseProps(tagName, props) {
/**
* @type {Record<PropertyKey, unknown>}
*/
const spreadable = {};
const ElementConstructor = customElements.get(tagName);
for (const [propName, value] of Object.entries(props)) {
if (propName === "htmlFor") {
spreadable.for = value;
continue;
}
if (propName.startsWith("on")) {
const eventName = propName.slice(2).toLowerCase();
spreadable[`@${eventName}`] = value;
} else if (typeof value === "boolean") {
spreadable[`?${propName}`] = value;
} else if (ElementConstructor) {
spreadable[`.${propName}`] = value;
} else {
spreadable[`${propName}`] = value;
}
}
console.log(spreadable);
return spreadable;
}
/**
*
* @param {string} tagName
* @param {*} props
* @returns
*/
export function createElement(
tagName,
{ className, children, ref: refProp, style = {}, ...props },
) {
const tag = unsafeStatic(tagName);
const result = html`
<${tag} class="${ifDefined(className)}"
${ref(refProp)}
${spread(parseProps(tagName, props))}>
${children}
</${tag}>
`;
return result;
}

View File

@@ -0,0 +1,22 @@
import { ErrorDetail } from "@goauthentik/api";
import { nothing } from "lit";
export interface AKFormErrorsProps {
errors?: ErrorDetail[];
}
export const AKFormErrors: LitJSX.FC<AKFormErrorsProps> = ({ errors } = {}) => {
if (!errors?.length) return nothing;
return errors.map((error) => {
return (
<p className="pf-c-form__helper-text pf-m-error">
<span className="pf-c-form__helper-text-icon">
<i className="fas fa-exclamation-circle" aria-hidden="true"></i>
</span>
{error.string}
</p>
);
});
};

View File

@@ -0,0 +1,30 @@
import { FC } from "@goauthentik/lit-jsx/jsx-runtime";
import type { LabelHTMLAttributes } from "react";
import { nothing } from "lit";
export interface FormLabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
required?: boolean;
children?: string;
}
export const AKLabel: FC<FormLabelProps> = ({
required,
htmlFor,
children,
...labelAttributes
} = {}) => {
if (!children) return nothing;
return (
<label className="pf-c-form__label" htmlFor={htmlFor} {...labelAttributes}>
<span className="pf-c-form__label-text">{children}</span>
{required ? (
<span className="pf-c-form__label-required" aria-hidden="true">
*
</span>
) : null}
</label>
);
};

View File

@@ -21,6 +21,7 @@ import diffGrammar from "highlight.js/lib/languages/diff";
import confGrammar from "highlight.js/lib/languages/ini";
import nginxGrammar from "highlight.js/lib/languages/nginx";
import { common } from "lowlight";
import React from "react";
import { createRoot, Root } from "react-dom/client";
import * as runtime from "react/jsx-runtime";
import rehypeHighlight, { Options as HighlightOptions } from "rehype-highlight";
@@ -229,15 +230,17 @@ export class AKMDX extends AKElement {
const { frontmatter = {} } = mdxExports;
this.#reactRoot.render(
<MDXModuleContext.Provider value={mdxModule}>
<Content
frontmatter={frontmatter}
components={{
React.createElement(
MDXModuleContext.Provider,
{ value: mdxModule },
React.createElement(Content, {
frontmatter,
components: {
wrapper: MDXWrapper,
a: MDXAnchor,
}}
/>
</MDXModuleContext.Provider>,
},
}),
),
);
}
}

View File

@@ -49,15 +49,15 @@ export const MDXAnchor = ({
});
};
return (
<a
href={href}
onClick={interceptHeadingLinks}
rel="noopener noreferrer"
target="_blank"
{...props}
>
{children}
</a>
return React.createElement(
"a",
{
href,
onClick: interceptHeadingLinks,
rel: "noopener noreferrer",
target: "_blank",
...props,
},
children,
);
};

View File

@@ -13,8 +13,8 @@ export const MDXWrapper = ({ children, frontmatter }: MDXWrapperProps) => {
const nextChildren = React.Children.toArray(children);
if (title) {
nextChildren.unshift(<h1 key="header-title">{title}</h1>);
nextChildren.unshift(React.createElement("h1", { key: "header-title" }, title));
}
return <div className="pf-c-content">{nextChildren}</div>;
return React.createElement("div", { className: "pf-c-content" }, nextChildren);
};

View File

@@ -1,70 +0,0 @@
import { AKElement } from "#elements/Base";
import { ErrorDetail } from "@goauthentik/api";
import { CSSResult, html, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
/**
* This is used in two places outside of Flow, and in both cases is used primarily to
* display content, not take input. It displays the TOTP QR code, and the static
* recovery tokens. But it's used a lot in Flow.
*/
@customElement("ak-form-element")
export class FormElement extends AKElement {
static styles: CSSResult[] = [PFBase, PFForm, PFFormControl];
@property()
label?: string;
@property({ type: Boolean })
required = false;
@property({ attribute: false })
set errors(value: ErrorDetail[] | undefined) {
this._errors = value;
const hasError = (value || []).length > 0;
this.querySelectorAll("input").forEach((input) => {
input.setAttribute("aria-invalid", hasError.toString());
});
this.requestUpdate();
}
_errors?: ErrorDetail[];
updated(): void {
this.querySelectorAll<HTMLInputElement>("input[autofocus]").forEach((input) => {
input.focus();
});
}
render(): TemplateResult {
return html`<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text">${this.label}</span>
${this.required
? html`<span class="pf-c-form__label-required" aria-hidden="true">*</span>`
: html``}
</label>
<slot></slot>
${(this._errors || []).map((error) => {
return html`<p class="pf-c-form__helper-text pf-m-error">
<span class="pf-c-form__helper-text-icon">
<i class="fas fa-exclamation-circle" aria-hidden="true"></i> </span
>${error.string}
</p>`;
})}
</div>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-form-element": FormElement;
}
}

View File

@@ -1,9 +1,9 @@
import "#elements/forms/FormElement";
import { AKElement } from "#elements/Base";
import { bound } from "#elements/decorators/bound";
import { isActiveElement } from "#elements/utils/focus";
import { AKLabel } from "#components/ak-label";
import { msg } from "@lit/localize";
import { html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
@@ -12,6 +12,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
import { createRef, ref, Ref } from "lit/directives/ref.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@@ -40,7 +41,7 @@ const Visibility = {
@customElement("ak-flow-input-password")
export class InputPassword extends AKElement {
static styles = [PFBase, PFInputGroup, PFFormControl, PFButton];
static styles = [PFBase, PFForm, PFInputGroup, PFFormControl, PFButton];
//#region Properties
@@ -50,7 +51,7 @@ export class InputPassword extends AKElement {
* @attr
*/
@property({ type: String, attribute: "input-id" })
inputId = "ak-stage-password-input";
public inputID = "ak-stage-password-input";
/**
* The name of the input field.
@@ -58,7 +59,7 @@ export class InputPassword extends AKElement {
* @attr
*/
@property({ type: String })
name = "password";
public name = "password";
/**
* The label for the input field.
@@ -66,7 +67,7 @@ export class InputPassword extends AKElement {
* @attr
*/
@property({ type: String })
label = msg("Password");
public label = msg("Password");
/**
* The placeholder text for the input field.
@@ -74,7 +75,7 @@ export class InputPassword extends AKElement {
* @attr
*/
@property({ type: String })
placeholder = msg("Please enter your password");
public placeholder = msg("Please enter your password");
/**
* The initial value of the input field.
@@ -82,20 +83,20 @@ export class InputPassword extends AKElement {
* @attr
*/
@property({ type: String, attribute: "prefill" })
initialValue = "";
public initialValue = "";
/**
* The errors for the input field.
*/
@property({ type: Object })
errors: Record<string, string> = {};
public errors: Record<string, string> = {};
/**
* Forwarded to the input tag's aria-invalid attribute, if set
* @attr
*/
@property({ type: String })
invalid?: string;
public invalid?: string;
/**
* Whether to allow the user to toggle the visibility of the password.
@@ -103,7 +104,7 @@ export class InputPassword extends AKElement {
* @attr
*/
@property({ type: Boolean, attribute: "allow-show-password" })
allowShowPassword = false;
public allowShowPassword = false;
/**
* Whether the password is currently visible.
@@ -111,7 +112,7 @@ export class InputPassword extends AKElement {
* @attr
*/
@property({ type: Boolean, attribute: "password-visible" })
passwordVisible = false;
public passwordVisible = false;
/**
* Automatically grab focus after rendering.
@@ -119,15 +120,15 @@ export class InputPassword extends AKElement {
* @attr
*/
@property({ type: Boolean, attribute: "grab-focus" })
grabFocus = false;
public grabFocus = false;
//#endregion
//#region Refs
inputRef: Ref<HTMLInputElement> = createRef();
public inputRef: Ref<HTMLInputElement> = createRef();
toggleVisibilityRef: Ref<HTMLButtonElement> = createRef();
public toggleVisibilityRef: Ref<HTMLButtonElement> = createRef();
//#endregion
@@ -137,7 +138,7 @@ export class InputPassword extends AKElement {
* Whether the caps lock key is enabled.
*/
@state()
capsLock = false;
public capsLock = false;
//#endregion
@@ -314,37 +315,33 @@ export class InputPassword extends AKElement {
}
render() {
return html` <ak-form-element
label="${this.label}"
required
class="pf-c-form__group"
.errors=${this.errors}
>
<div class="pf-c-form__group-control">
<div class="pf-c-input-group">
<input
type=${this.passwordVisible ? "text" : "password"}
id=${this.inputId}
name=${this.name}
placeholder=${this.placeholder}
autocomplete="current-password"
class="${classMap({
"pf-c-form-control": true,
"pf-m-icon": true,
"pf-m-caps-lock": this.capsLock,
})}"
required
aria-invalid=${ifDefined(this.invalid)}
value=${this.initialValue}
${ref(this.inputRef)}
/>
return html` ${AKLabel({ required: true, htmlFor: this.inputID, children: this.label })}
<div class="pf-c-form__group">
<div class="pf-c-form__group-control">
<div class="pf-c-input-group">
<input
type=${this.passwordVisible ? "text" : "password"}
id=${this.inputID}
name=${this.name}
placeholder=${this.placeholder}
autocomplete="current-password"
class="${classMap({
"pf-c-form-control": true,
"pf-m-icon": true,
"pf-m-caps-lock": this.capsLock,
})}"
required
aria-invalid=${ifDefined(this.invalid)}
value=${this.initialValue}
${ref(this.inputRef)}
/>
${this.renderVisibilityToggle()}
${this.renderVisibilityToggle()}
</div>
${this.renderHelperText()}
</div>
${this.renderHelperText()}
</div>
</ak-form-element>`;
</div>`;
}
//#endregion

View File

@@ -1,7 +1,9 @@
import "#elements/forms/FormElement";
import "#flow/FormStatic";
import "#flow/components/ak-flow-card";
import { AKFormErrors } from "#components/ak-field-errors";
import { AKLabel } from "#components/ak-label";
import { BaseStage } from "#flow/stages/base";
import {
@@ -16,6 +18,7 @@ import { customElement } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@@ -25,15 +28,28 @@ export class OAuth2DeviceCode extends BaseStage<
OAuthDeviceCodeChallenge,
OAuthDeviceCodeChallengeResponseRequest
> {
static styles: CSSResult[] = [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton];
static styles: CSSResult[] = [
PFBase,
PFLogin,
PFForm,
PFFormControl,
PFTitle,
PFButton,
PFInputGroup,
];
render(): TemplateResult {
return html`<ak-flow-card .challenge=${this.challenge}>
<form
class="pf-c-form"
@submit=${this.submitForm}
>
<form class="pf-c-form" @submit=${this.submitForm}>
<div class="pf-c-form__group">
${AKLabel({
required: true,
htmlFor: "device-code-input",
children: msg("Device Code"),
})}
<input
id="device-code-input"
type="text"
name="code"
inputmode="numeric"
@@ -45,7 +61,8 @@ export class OAuth2DeviceCode extends BaseStage<
value=""
required
/>
</ak-form-element>
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
</div>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">

View File

@@ -1,4 +1,3 @@
import "#elements/forms/FormElement";
import "#flow/FormStatic";
import "#flow/components/ak-flow-card";

View File

@@ -1,7 +1,9 @@
import "#elements/forms/FormElement";
import "#flow/FormStatic";
import "#flow/components/ak-flow-card";
import { AKFormErrors } from "#components/ak-field-errors";
import { AKLabel } from "#components/ak-label";
import { BaseStage } from "#flow/stages/base";
import {
@@ -18,6 +20,7 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@@ -33,6 +36,7 @@ export class AuthenticatorEmailStage extends BaseStage<
PFLogin,
PFForm,
PFFormControl,
PFInputGroup,
PFTitle,
PFButton,
];
@@ -51,13 +55,14 @@ export class AuthenticatorEmailStage extends BaseStage<
>
</div>
</ak-form-static>
<ak-form-element
label="${msg("Configure your email")}"
required
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {}).email}
>
<div class="pf-c-form__group">
${AKLabel({
required: true,
htmlFor: "email-input",
children: msg("Configure your email"),
})}
<input
id="email-input"
type="email"
name="email"
placeholder="${msg("Please enter your email address.")}"
@@ -66,7 +71,8 @@ export class AuthenticatorEmailStage extends BaseStage<
class="pf-c-form-control"
required
/>
</ak-form-element>
${AKFormErrors({ errors: this.challenge.responseErrors?.email })}
</div>
${this.renderNonFieldErrors()}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
@@ -93,13 +99,10 @@ export class AuthenticatorEmailStage extends BaseStage<
A verification token has been sent to your configured email address
${ifDefined(this.challenge.email)}
<form class="pf-c-form" @submit=${this.submitForm}>
<ak-form-element
label="${msg("Code")}"
required
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {}).code}
>
<div class="pf-c-form__group">
${AKLabel({ required: true, htmlFor: "code-input", children: msg("Code") })}
<input
id="code-input"
type="text"
name="code"
inputmode="numeric"
@@ -110,7 +113,8 @@ export class AuthenticatorEmailStage extends BaseStage<
class="pf-c-form-control"
required
/>
</ak-form-element>
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
</div>
${this.renderNonFieldErrors()}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">

View File

@@ -1,7 +1,9 @@
import "#elements/forms/FormElement";
import "#flow/FormStatic";
import "#flow/components/ak-flow-card";
import { AKFormErrors } from "#components/ak-field-errors";
import { AKLabel } from "#components/ak-label";
import { BaseStage } from "#flow/stages/base";
import {
@@ -18,6 +20,7 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@@ -33,15 +36,18 @@ export class AuthenticatorSMSStage extends BaseStage<
PFLogin,
PFForm,
PFFormControl,
PFInputGroup,
PFTitle,
PFButton,
];
renderPhoneNumber(): TemplateResult {
return html`<ak-flow-card .challenge=${this.challenge}>
<form
class="pf-c-form"
@submit=${this.submitForm}
<form class="pf-c-form" @submit=${this.submitForm}>
<ak-form-static
class="pf-c-form__group"
userAvatar=${this.challenge.pendingUserAvatar}
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
@@ -49,12 +55,13 @@ export class AuthenticatorSMSStage extends BaseStage<
>
</div>
</ak-form-static>
<ak-form-element
label="${msg("Phone number")}"
required
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {}).phone_number}
>
<div class="pf-c-form__group">
${AKLabel({
required: true,
htmlFor: "phone-number-input",
children: msg("Phone number"),
})}
<input
type="tel"
name="phoneNumber"
@@ -64,7 +71,8 @@ export class AuthenticatorSMSStage extends BaseStage<
class="pf-c-form-control"
required
/>
</ak-form-element>
${AKFormErrors({ errors: this.challenge.responseErrors?.phone_number })}
</div>
${this.renderNonFieldErrors()}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
@@ -89,13 +97,10 @@ export class AuthenticatorSMSStage extends BaseStage<
>
</div>
</ak-form-static>
<ak-form-element
label="${msg("Code")}"
required
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {}).code}
>
<div class="pf-c-form__group">
${AKLabel({ required: true, htmlFor: "sms-code-input", children: msg("Code") })}
<input
id="sms-code-input"
type="text"
name="code"
inputmode="numeric"
@@ -106,7 +111,8 @@ export class AuthenticatorSMSStage extends BaseStage<
class="pf-c-form-control"
required
/>
</ak-form-element>
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
</div>
${this.renderNonFieldErrors()}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">

View File

@@ -1,4 +1,3 @@
import "#elements/forms/FormElement";
import "#flow/FormStatic";
import "#flow/components/ak-flow-card";
@@ -64,13 +63,13 @@ export class AuthenticatorStaticStage extends BaseStage<
>
</div>
</ak-form-static>
<ak-form-element label="" class="pf-c-form__group">
<div class="pf-c-form__group">
<ul>
${this.challenge.codes.map((token) => {
return html`<li class="pf-m-monospace">${token}</li>`;
})}
</ul>
</ak-form-element>
</div>
<p>${msg("Make sure to keep these tokens in a safe place.")}</p>
<div class="pf-c-form__group pf-m-action">

View File

@@ -1,4 +1,3 @@
import "#elements/forms/FormElement";
import "#flow/FormStatic";
import "#flow/components/ak-flow-card";
import "webcomponent-qr-code";
@@ -7,6 +6,9 @@ import { MessageLevel } from "#common/messages";
import { showMessage } from "#elements/messages/MessageContainer";
import { AKFormErrors } from "#components/ak-field-errors";
import { AKLabel } from "#components/ak-label";
import { BaseStage } from "#flow/stages/base";
import {
@@ -22,6 +24,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@@ -36,6 +39,7 @@ export class AuthenticatorTOTPStage extends BaseStage<
PFLogin,
PFForm,
PFFormControl,
PFInputGroup,
PFTitle,
PFButton,
css`
@@ -62,7 +66,8 @@ export class AuthenticatorTOTPStage extends BaseStage<
</div>
</ak-form-static>
<input type="hidden" name="otp_uri" value=${this.challenge.configUrl} />
<ak-form-element>
<div class="pf-c-form__group">
<div class="qr-container">
<qr-code data="${this.challenge.configUrl}"></qr-code>
<button
@@ -92,20 +97,20 @@ export class AuthenticatorTOTPStage extends BaseStage<
${msg("Copy")}
</button>
</div>
</ak-form-element>
</div>
<p>
${msg(
"Please scan the QR code above using the Microsoft Authenticator, Google Authenticator, or other authenticator apps on your device, and enter the code the device displays below to finish setting up the MFA device.",
)}
</p>
<ak-form-element
label="${msg("Code")}"
required
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {}).code}
>
<!-- @ts-ignore -->
<div class="pf-c-form__group">
${AKLabel({
required: true,
htmlFor: "totp-code-input",
children: msg("Code"),
})}
<input
id="totp-code-input"
type="text"
name="code"
inputmode="numeric"
@@ -117,7 +122,8 @@ export class AuthenticatorTOTPStage extends BaseStage<
spellcheck="false"
required
/>
</ak-form-element>
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
</div>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">

View File

@@ -1,6 +1,8 @@
import "#elements/forms/FormElement";
import "#flow/components/ak-flow-card";
import { AKFormErrors } from "#components/ak-field-errors";
import { AKLabel } from "#components/ak-label";
import { BaseDeviceStage } from "#flow/stages/authenticator_validate/base";
import { PasswordManagerPrefill } from "#flow/stages/identification/IdentificationStage";
@@ -74,16 +76,17 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
<i class="fa ${this.deviceIcon()}" aria-hidden="true"></i>
<p>${this.deviceMessage()}</p>
</div>
<ak-form-element
label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
? msg("Static token")
: msg("Authentication code")}"
required
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {}).code}
>
<!-- @ts-ignore -->
<div class="pf-c-form__group">
${AKLabel({
required: true,
htmlFor: "validation-code-input",
children:
this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
? msg("Static token")
: msg("Authentication code"),
})}
<input
id="validation-code-input"
type="text"
name="code"
inputmode="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
@@ -99,7 +102,8 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
value="${PasswordManagerPrefill.totp || ""}"
required
/>
</ak-form-element>
${AKFormErrors({ errors: this.challenge.responseErrors?.code })}
</div>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">

View File

@@ -1,5 +1,4 @@
import "#elements/EmptyState";
import "#elements/forms/FormElement";
import { BaseDeviceStage } from "#flow/stages/authenticator_validate/base";

View File

@@ -10,6 +10,7 @@ import { property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@@ -29,6 +30,7 @@ export class BaseDeviceStage<
PFLogin,
PFForm,
PFFormControl,
PFInputGroup,
PFTitle,
PFButton,
css`

View File

@@ -1,4 +1,3 @@
import "#elements/forms/FormElement";
import "#flow/FormStatic";
import "#flow/components/ak-flow-card";

View File

@@ -1,10 +1,12 @@
import "#elements/Divider";
import "#elements/EmptyState";
import "#elements/forms/FormElement";
import "#flow/components/ak-flow-card";
import "#flow/components/ak-flow-password-input";
import "#flow/stages/captcha/CaptchaStage";
import { AKFormErrors } from "#components/ak-field-errors";
import { AKLabel } from "#components/ak-label";
import { renderSourceIcon } from "#admin/sources/utils";
import { BaseStage } from "#flow/stages/base";
@@ -20,7 +22,7 @@ import {
import { msg, str } from "@lit/localize";
import { css, CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators.js";
import { customElement, property, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
@@ -87,6 +89,14 @@ export class IdentificationStage extends BaseStage<
`,
];
/**
* The ID of the input field.
*
* @attr
*/
@property({ type: String, attribute: "input-id" })
public inputID = "ak-identifier-input";
#form?: HTMLFormElement;
#rememberMe = new AkRememberMeController(this);
@@ -301,6 +311,7 @@ export class IdentificationStage extends BaseStage<
[UserFieldsEnum.Upn]: msg("UPN"),
};
const label = OR_LIST_FORMATTERS.format(fields.map((f) => uiFields[f]));
return html`${this.challenge.flowDesignation === FlowDesignationEnum.Recovery
? html`
<p>
@@ -310,13 +321,10 @@ export class IdentificationStage extends BaseStage<
</p>
`
: nothing}
<ak-form-element
label=${label}
required
class="pf-c-form__group"
.errors=${(this.challenge.responseErrors || {}).uid_field}
>
<div class="pf-c-form__group">
${AKLabel({ required: true, htmlFor: this.inputID, children: label })}
<input
id=${this.inputID}
type=${type}
name="uidField"
placeholder=${label}
@@ -328,12 +336,13 @@ export class IdentificationStage extends BaseStage<
required
/>
${this.#rememberMe.render()}
</ak-form-element>
${AKFormErrors({ errors: this.challenge.responseErrors?.uid_field })}
</div>
${this.challenge.passwordFields
? html`
<ak-flow-input-password
label=${msg("Password")}
inputId="ak-stage-identification-password"
input-idd="ak-stage-identification-password"
required
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {}).password}

View File

@@ -1,4 +1,3 @@
import "#elements/forms/FormElement";
import "#flow/FormStatic";
import "#flow/components/ak-flow-card";
import "#flow/components/ak-flow-password-input";

View File

@@ -1,10 +1,12 @@
import "#elements/Divider";
import "#elements/forms/FormElement";
import "#flow/components/ak-flow-card";
import { LOCALES } from "#elements/ak-locale-context/definitions";
import { CapabilitiesEnum, WithCapabilitiesConfig } from "#elements/mixins/capabilities";
import { AKFormErrors } from "#components/ak-field-errors";
import { AKLabel } from "#components/ak-label";
import { BaseStage } from "#flow/stages/base";
import {
@@ -24,6 +26,7 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCheck from "@patternfly/patternfly/components/Check/check.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@@ -38,6 +41,7 @@ export class PromptStage extends WithCapabilitiesConfig(
PFAlert,
PFForm,
PFFormControl,
PFInputGroup,
PFTitle,
PFButton,
PFCheck,
@@ -55,6 +59,7 @@ export class PromptStage extends WithCapabilitiesConfig(
case PromptTypeEnum.Text:
return html`<input
type="text"
id="field-${prompt.fieldKey}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
autocomplete="off"
@@ -64,6 +69,7 @@ export class PromptStage extends WithCapabilitiesConfig(
/>`;
case PromptTypeEnum.TextArea:
return html`<textarea
id="field-${prompt.fieldKey}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
autocomplete="off"
@@ -75,6 +81,7 @@ ${prompt.initialValue}</textarea
case PromptTypeEnum.TextReadOnly:
return html`<input
type="text"
id="field-${prompt.fieldKey}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
@@ -83,6 +90,7 @@ ${prompt.initialValue}</textarea
/>`;
case PromptTypeEnum.TextAreaReadOnly:
return html`<textarea
id="field-${prompt.fieldKey}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
@@ -93,6 +101,7 @@ ${prompt.initialValue}</textarea
case PromptTypeEnum.Username:
return html`<input
type="text"
id="field-${prompt.fieldKey}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
autocomplete="username"
@@ -104,6 +113,7 @@ ${prompt.initialValue}</textarea
case PromptTypeEnum.Email:
return html`<input
type="email"
id="field-${prompt.fieldKey}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
@@ -113,6 +123,7 @@ ${prompt.initialValue}</textarea
case PromptTypeEnum.Password:
return html`<input
type="password"
id="field-${prompt.fieldKey}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
autocomplete="new-password"
@@ -122,6 +133,7 @@ ${prompt.initialValue}</textarea
case PromptTypeEnum.Number:
return html`<input
type="number"
id="field-${prompt.fieldKey}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
@@ -131,6 +143,7 @@ ${prompt.initialValue}</textarea
case PromptTypeEnum.Date:
return html`<input
type="date"
id="field-${prompt.fieldKey}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
@@ -140,6 +153,7 @@ ${prompt.initialValue}</textarea
case PromptTypeEnum.DateTime:
return html`<input
type="datetime"
id="field-${prompt.fieldKey}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
@@ -149,6 +163,7 @@ ${prompt.initialValue}</textarea
case PromptTypeEnum.File:
return html`<input
type="file"
id="field-${prompt.fieldKey}"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
@@ -160,6 +175,7 @@ ${prompt.initialValue}</textarea
case PromptTypeEnum.Hidden:
return html`<input
type="hidden"
id="field-${prompt.fieldKey}"
name="${prompt.fieldKey}"
value="${prompt.initialValue}"
class="pf-c-form-control"
@@ -168,7 +184,11 @@ ${prompt.initialValue}</textarea
case PromptTypeEnum.Static:
return html`<p>${unsafeHTML(prompt.initialValue)}</p>`;
case PromptTypeEnum.Dropdown:
return html`<select class="pf-c-form-control" name="${prompt.fieldKey}">
return html`<select
class="pf-c-form-control"
id="field-${prompt.fieldKey}"
name="${prompt.fieldKey}"
>
${prompt.choices?.map((choice) => {
return html`<option
value="${choice}"
@@ -256,14 +276,17 @@ ${prompt.initialValue}</textarea
</div>`;
}
if (this.shouldRenderInWrapper(prompt)) {
return html`<ak-form-element
label="${prompt.label}"
?required="${prompt.required}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}
>
const errors = (this.challenge?.responseErrors || {})[prompt.fieldKey];
return html`<div class="pf-c-form__group">
${AKLabel({
required: prompt.required,
htmlFor: `field-${prompt.fieldKey}`,
children: prompt.label,
})}
${this.renderPromptInner(prompt)} ${this.renderPromptHelpText(prompt)}
</ak-form-element>`;
${AKFormErrors({ errors })}
</div>`;
}
return html` ${this.renderPromptInner(prompt)} ${this.renderPromptHelpText(prompt)}`;
}
@@ -279,9 +302,7 @@ ${prompt.initialValue}</textarea
render(): TemplateResult {
return html`<ak-flow-card .challenge=${this.challenge}>
<form class="pf-c-form" @submit=${this.submitForm}>
${this.challenge.fields.map((prompt) => {
return this.renderField(prompt);
})}
${this.challenge.fields.map((prompt) => this.renderField(prompt))}
${this.renderNonFieldErrors()} ${this.renderContinue()}
</form>
</ak-flow-card>`;

View File

@@ -1,4 +1,3 @@
import "#elements/forms/FormElement";
import "#flow/FormStatic";
import "#flow/components/ak-flow-card";

View File

@@ -14,6 +14,8 @@
"module": "esnext",
"moduleResolution": "bundler",
"baseUrl": ".",
"jsx": "react-jsx",
"jsxImportSource": "@goauthentik/lit-jsx",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
// TODO: We should enable this when when we're ready to enforce it.
"noUncheckedIndexedAccess": false,