mirror of
https://github.com/goauthentik/authentik
synced 2026-04-25 17:15:26 +02:00
web: Flesh out module driven tag names.
This commit is contained in:
@@ -11,7 +11,7 @@ import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { AKFormErrors, ErrorProp } from "#components/ak-field-errors";
|
||||
|
||||
import { StageHost } from "#flow/stages/base";
|
||||
import type { StageHost } from "#flow/types";
|
||||
|
||||
import { Prompt, PromptChallenge, PromptTypeEnum, StagesApi } from "@goauthentik/api";
|
||||
|
||||
|
||||
41
web/src/common/modules/types.ts
Normal file
41
web/src/common/modules/types.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @file Type utilities for ESM modules.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A type representing a resolved ES module with a default export of type `DefaultExport`.
|
||||
*
|
||||
* ```ts
|
||||
* const mod: ResolvedESModule<MyType> = await import('./my-module.js');
|
||||
* const myValue: MyType = mod.default;
|
||||
* ```
|
||||
*/
|
||||
export interface ResolvedDefaultESModule<DefaultExport> {
|
||||
default: DefaultExport;
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback that returns a promise resolving to a module of type `T`.
|
||||
*/
|
||||
export type ImportCallback<T extends object> = () => Promise<T>;
|
||||
|
||||
/**
|
||||
* A callback that returns a promise resolving to a module with a default export of type `T`.
|
||||
*/
|
||||
export type DefaultImportCallback<T = unknown> = ImportCallback<ResolvedDefaultESModule<T>>;
|
||||
|
||||
/**
|
||||
* Asserts that the given module has a default export and is an object.
|
||||
*
|
||||
* @param mod The module to check.
|
||||
* @throws {TypeError} If the module is not an object or does not have a default export.
|
||||
*/
|
||||
export function assertDefaultExport<T>(mod: unknown): asserts mod is ResolvedDefaultESModule<T> {
|
||||
if (!mod || typeof mod !== "object") {
|
||||
throw new TypeError("Module is not an object");
|
||||
}
|
||||
|
||||
if (!Object.hasOwn(mod, "default")) {
|
||||
throw new TypeError("Module does not have a default export");
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import "#flow/components/ak-brand-footer";
|
||||
import "#flow/components/ak-flow-card";
|
||||
|
||||
import Styles from "./FlowExecutor.css" with { type: "bundled-text" };
|
||||
import { stages } from "./FlowExecutorSelections";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
|
||||
@@ -25,7 +24,9 @@ import { exportParts } from "#elements/utils/attributes";
|
||||
import { ThemedImage } from "#elements/utils/images";
|
||||
|
||||
import { AKFlowAdvanceEvent, AKFlowInspectorChangeEvent } from "#flow/events";
|
||||
import { BaseStage, StageHost, SubmitOptions } from "#flow/stages/base";
|
||||
import { readStageModuleTag, StageMappings } from "#flow/FlowExecutorSelections";
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
import type { FlowChallengeComponentName, StageHost, SubmitOptions } from "#flow/types";
|
||||
|
||||
import { ConsoleLogger } from "#logger/browser";
|
||||
|
||||
@@ -354,21 +355,22 @@ export class FlowExecutor
|
||||
//#region Render Challenge
|
||||
|
||||
protected async renderChallenge(
|
||||
component: ChallengeTypes["component"],
|
||||
component: FlowChallengeComponentName,
|
||||
): Promise<TemplateResult> {
|
||||
const { challenge, inspectorOpen } = this;
|
||||
|
||||
const stage = stages.get(component);
|
||||
const stage = StageMappings.get(component);
|
||||
|
||||
// The special cases!
|
||||
if (!stage) {
|
||||
if (component === "xak-flow-shell") {
|
||||
return html`${unsafeHTML((challenge as ShellChallenge).body)}`;
|
||||
}
|
||||
|
||||
return html`Invalid native challenge element`;
|
||||
}
|
||||
|
||||
const challengeProps: LitPropertyRecord<BaseStage<NonNullable<typeof challenge>, unknown>> =
|
||||
const challengeProps: LitPropertyRecord<BaseStage<NonNullable<typeof challenge>, object>> =
|
||||
{ ".challenge": challenge!, ".host": this };
|
||||
|
||||
const litParts = {
|
||||
@@ -376,13 +378,10 @@ export class FlowExecutor
|
||||
exportparts: exportParts(["additional-actions", "footer-band"], "challenge"),
|
||||
};
|
||||
|
||||
const { tag, variant, importfn } = stage;
|
||||
if (importfn) {
|
||||
await importfn();
|
||||
}
|
||||
const tag = await readStageModuleTag(stage);
|
||||
|
||||
const props = spread(
|
||||
match(variant)
|
||||
match(stage.variant)
|
||||
.with("challenge", () => challengeProps)
|
||||
.with("standard", () => ({ ...challengeProps, ...litParts }))
|
||||
.with("inspect", () => ({ ...challengeProps, "?promptUser": inspectorOpen }))
|
||||
|
||||
@@ -5,111 +5,148 @@ import "#flow/stages/FlowErrorStage";
|
||||
import "#flow/stages/FlowFrameStage";
|
||||
import "#flow/stages/RedirectStage";
|
||||
|
||||
import { isMatching, match, P } from "ts-pattern";
|
||||
import type { UnwrapSet } from "#common/sets";
|
||||
|
||||
export const propVariants = ["standard", "challenge", "inspect"] as const;
|
||||
type PropVariant = (typeof propVariants)[number];
|
||||
import type { FlowChallengeComponentName, StageModuleCallback } from "#flow/types";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type ComponentImport = () => Promise<any>;
|
||||
export const propVariants = new Set(["standard", "challenge", "inspect"] as const);
|
||||
type PropVariant = UnwrapSet<typeof propVariants>;
|
||||
|
||||
// Any of these are valid variations, so you don't have to specify that this stage takes the
|
||||
// "standard" set of props, or if the server-side stage token and the client-side component tag are
|
||||
// the same you only need to specify one. Although the PropVariants and TagName are both strings on
|
||||
// the wire, custom element tags always contain a dash and the PropVariants don't, making them
|
||||
// easy to distinguish.
|
||||
//
|
||||
type BaseStage =
|
||||
| [token: string, tag: string, variant: PropVariant]
|
||||
| [token: string, variant: PropVariant]
|
||||
| [token: string, tag: string]
|
||||
| [token: string];
|
||||
|
||||
type RawStage =
|
||||
| BaseStage
|
||||
| [token: string, tag: string, variant: PropVariant, import: ComponentImport]
|
||||
| [token: string, variant: PropVariant, import: ComponentImport]
|
||||
| [token: string, tag: string, import: ComponentImport]
|
||||
| [token: string, import: ComponentImport];
|
||||
|
||||
type Stage = {
|
||||
tag: string;
|
||||
variant: PropVariant;
|
||||
importfn?: ComponentImport;
|
||||
};
|
||||
/**
|
||||
* @remarks
|
||||
* Any of these are valid variations, so you don't have to specify that this stage takes the
|
||||
* "standard" set of props, or if the server-side stage token and the client-side component tag are
|
||||
* the same you only need to specify one. Although the PropVariants and TagName are both strings on
|
||||
* the wire, custom element tags always contain a dash and the PropVariants don't, making them
|
||||
* easy to distinguish.
|
||||
*/
|
||||
type StageEntry =
|
||||
| StageModuleCallback
|
||||
| [PropVariant?, tagName?: string]
|
||||
| [PropVariant, StageModuleCallback]
|
||||
| null;
|
||||
|
||||
// Tables are allowed to go wide, just for readability. Sorting them alphabetically helps, too.
|
||||
//
|
||||
// prettier-ignore
|
||||
const rawStages: RawStage[] = [
|
||||
["ak-provider-iframe-logout", async () => await import("#flow/providers/IFrameLogoutStage")],
|
||||
["ak-provider-oauth2-device-code", "ak-flow-provider-oauth2-code", async () => await import("#flow/providers/oauth2/DeviceCode")],
|
||||
["ak-provider-oauth2-device-code-finish", "ak-flow-provider-oauth2-code-finish", async () => await import("#flow/providers/oauth2/DeviceCodeFinish")],
|
||||
["ak-provider-saml-native-logout", async () => await import("#flow/providers/saml/NativeLogoutStage")],
|
||||
["ak-source-oauth-apple", "ak-flow-source-oauth-apple"],
|
||||
["ak-source-plex", "ak-flow-source-plex"],
|
||||
["ak-source-telegram", "ak-flow-source-telegram"],
|
||||
["ak-stage-access-denied", async () => await import("#flow/stages/access_denied/AccessDeniedStage")],
|
||||
["ak-stage-authenticator-duo", async () => await import("#flow/stages/authenticator_duo/AuthenticatorDuoStage")],
|
||||
["ak-stage-authenticator-email", async () => await import("#flow/stages/authenticator_email/AuthenticatorEmailStage")],
|
||||
["ak-stage-authenticator-sms", async () => await import("#flow/stages/authenticator_sms/AuthenticatorSMSStage")],
|
||||
["ak-stage-authenticator-static", async () => await import("#flow/stages/authenticator_static/AuthenticatorStaticStage")],
|
||||
["ak-stage-authenticator-totp", async () => await import("#flow/stages/authenticator_totp/AuthenticatorTOTPStage")],
|
||||
["ak-stage-authenticator-validate", async () => await import("#flow/stages/authenticator_validate/AuthenticatorValidateStage")],
|
||||
["ak-stage-authenticator-webauthn"],
|
||||
["ak-stage-autosubmit", async () => await import("#flow/stages/autosubmit/AutosubmitStage")],
|
||||
["ak-stage-captcha", async () => await import("#flow/stages/captcha/CaptchaStage")],
|
||||
["ak-stage-consent", async () => await import("#flow/stages/consent/ConsentStage")],
|
||||
["ak-stage-dummy", async () => await import("#flow/stages/dummy/DummyStage")],
|
||||
["ak-stage-email", async () => await import("#flow/stages/email/EmailStage")],
|
||||
["ak-stage-endpoint-agent", "challenge", async () => await import("#flow/stages/endpoint/agent/EndpointAgentStage")],
|
||||
["ak-stage-flow-error"],
|
||||
["ak-stage-identification", async () => await import("#flow/stages/identification/IdentificationStage")],
|
||||
["ak-stage-password", async () => await import("#flow/stages/password/PasswordStage")],
|
||||
["ak-stage-prompt", async () => await import("#flow/stages/prompt/PromptStage")],
|
||||
["ak-stage-session-end", async () => await import("#flow/providers/SessionEnd")],
|
||||
["ak-stage-user-login", "challenge", async () => await import("#flow/stages/user_login/UserLoginStage")],
|
||||
["xak-flow-redirect", "ak-stage-redirect", "inspect"],
|
||||
["xak-flow-frame", "challenge"],
|
||||
];
|
||||
const StageModuleRecord: Record<FlowChallengeComponentName, StageEntry> = {
|
||||
"ak-provider-iframe-logout": () => import("#flow/providers/IFrameLogoutStage"),
|
||||
"ak-provider-oauth2-device-code-finish": () => import("#flow/providers/oauth2/DeviceCodeFinish"),
|
||||
"ak-provider-oauth2-device-code": () => import("#flow/providers/oauth2/DeviceCode"),
|
||||
"ak-provider-saml-native-logout": () => import("#flow/providers/saml/NativeLogoutStage"),
|
||||
|
||||
const isImport = isMatching(P.when((x): x is ComponentImport => typeof x === "function"));
|
||||
"ak-stage-access-denied": () => import("#flow/stages/access_denied/AccessDeniedStage"),
|
||||
"ak-stage-authenticator-duo": () => import("#flow/stages/authenticator_duo/AuthenticatorDuoStage"),
|
||||
"ak-stage-authenticator-email": () => import("#flow/stages/authenticator_email/AuthenticatorEmailStage"),
|
||||
"ak-stage-authenticator-sms": () => import("#flow/stages/authenticator_sms/AuthenticatorSMSStage"),
|
||||
"ak-stage-authenticator-static": () => import("#flow/stages/authenticator_static/AuthenticatorStaticStage"),
|
||||
"ak-stage-authenticator-totp": () => import("#flow/stages/authenticator_totp/AuthenticatorTOTPStage"),
|
||||
"ak-stage-authenticator-validate": () => import("#flow/stages/authenticator_validate/AuthenticatorValidateStage"),
|
||||
"ak-stage-autosubmit": () => import("#flow/stages/autosubmit/AutosubmitStage"),
|
||||
"ak-stage-captcha": () => import("#flow/stages/captcha/CaptchaStage"),
|
||||
"ak-stage-consent": () => import("#flow/stages/consent/ConsentStage"),
|
||||
"ak-stage-dummy": () => import("#flow/stages/dummy/DummyStage"),
|
||||
"ak-stage-email": () => import("#flow/stages/email/EmailStage"),
|
||||
"ak-stage-identification": () => import("#flow/stages/identification/IdentificationStage"),
|
||||
"ak-stage-password": () => import("#flow/stages/password/PasswordStage"),
|
||||
"ak-stage-prompt": () => import("#flow/stages/prompt/PromptStage"),
|
||||
"ak-stage-session-end": () => import("#flow/providers/SessionEnd"),
|
||||
|
||||
const PVariant = P.when(
|
||||
(x): x is PropVariant => typeof x === "string" && propVariants.includes(x as PropVariant),
|
||||
"ak-stage-endpoint-agent": ["challenge", () => import("#flow/stages/endpoint/agent/EndpointAgentStage")],
|
||||
"ak-stage-user-login": ["challenge", () => import("#flow/stages/user_login/UserLoginStage")],
|
||||
|
||||
"ak-source-oauth-apple": ["standard", "ak-flow-source-oauth-apple"],
|
||||
"ak-stage-authenticator-webauthn": [],
|
||||
"ak-stage-flow-error": [],
|
||||
|
||||
"ak-source-plex": ["standard", "ak-flow-source-plex"],
|
||||
"ak-source-telegram": ["standard", "ak-flow-source-telegram"],
|
||||
|
||||
"xak-flow-frame": ["challenge"],
|
||||
"xak-flow-redirect": ["inspect", "ak-stage-redirect"],
|
||||
"xak-flow-shell": null,
|
||||
}
|
||||
|
||||
type StageMapping =
|
||||
| {
|
||||
variant: PropVariant;
|
||||
tag: string;
|
||||
importCallback: null;
|
||||
}
|
||||
| {
|
||||
variant: PropVariant;
|
||||
tag: null;
|
||||
importCallback: StageModuleCallback;
|
||||
};
|
||||
|
||||
/**
|
||||
* A mapping of server-side stage tokens to client-side custom element tags, along with the variant of props they consume and an optional import callback for lazy-loading.
|
||||
*
|
||||
* @remarks
|
||||
* This is the actual table of stages consumed by the FlowExecutor.
|
||||
* It is generated from the more concise `StageModuleRecord` above, which is easier to read and maintain.
|
||||
* The `StageModuleRecord` allows for specifying just the token, or the token and variant,
|
||||
* or the token and tag, or all three, and it can also include an import callback if the stage should be lazy-loaded.
|
||||
* The code below normalizes all of these possibilities into a consistent format that the FlowExecutor can use.
|
||||
*/
|
||||
export const StageMappings: ReadonlyMap<FlowChallengeComponentName, StageMapping | null> = new Map(
|
||||
Object.entries(StageModuleRecord).map(
|
||||
(foo): [FlowChallengeComponentName, StageMapping | null] => {
|
||||
const [token, entry] = foo as [FlowChallengeComponentName, StageEntry | null];
|
||||
|
||||
if (entry === null) {
|
||||
return [token, null];
|
||||
}
|
||||
|
||||
if (typeof entry === "function") {
|
||||
return [token, { tag: null, variant: "standard", importCallback: entry }];
|
||||
}
|
||||
|
||||
const [variant = "standard", maybeTagOrImport = token] = entry;
|
||||
|
||||
if (typeof maybeTagOrImport === "function") {
|
||||
return [token, { tag: null, variant, importCallback: maybeTagOrImport }];
|
||||
}
|
||||
|
||||
return [token, { tag: maybeTagOrImport, variant, importCallback: null }];
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Don't have to type-check what you get from the tap.
|
||||
const STANDARD = propVariants[0];
|
||||
/**
|
||||
* A cache for storing the resolved custom element tag names for stage mappings that require lazy-loading.
|
||||
*/
|
||||
const StageMappingTagNameCache = new WeakMap<StageMapping, string>();
|
||||
|
||||
type InStage = [token: string, tag: string, variant: PropVariant];
|
||||
/**
|
||||
* Given a stage mapping, returns the custom element tag name for that stage, loading the module if necessary.
|
||||
*
|
||||
* @param stageMapping The mapping for the stage, which may include a direct tag name or an import callback for lazy-loading.
|
||||
* @returns The custom element tag name for the stage.
|
||||
* @throws {TypeError} If the module fails to load or does not define a custom element.
|
||||
*/
|
||||
export async function readStageModuleTag(stageMapping: StageMapping): Promise<string> {
|
||||
if (stageMapping.importCallback === null) {
|
||||
return stageMapping.tag;
|
||||
}
|
||||
|
||||
export const stages: Map<string, Stage> = new Map(
|
||||
rawStages.map((rawstage: RawStage) => {
|
||||
// The RawStages table above is clear and lacks the repetition of the original, but it does
|
||||
// mean that doing all the pattern matching to normalize this to the format consumed by the
|
||||
// FlowExecutor looks a little hairy. Repetition, defaults, and optional values can be
|
||||
// ignored, making the table small and elegant, and this runs exactly once at start-time, so
|
||||
// its run-time cost is well worth the savings. The use of `.exhaustive()` at the bottom
|
||||
// guarantees that every variant specified in the `RawStage` type is handled.
|
||||
//
|
||||
// P.optional(PImport) does not work with tuples, but since it *is* optional and it *is* the
|
||||
// last thing when it's present, we lop it off for the purpose of checking the rest, so the
|
||||
// actual comparison table is for all the other variants, then put it back when we assemble
|
||||
// the map. This eliminates half the variant checks.
|
||||
//
|
||||
const last = rawstage.at(-1);
|
||||
const importfn = isImport(last) ? last : undefined;
|
||||
const rest: BaseStage = (importfn ? rawstage.slice(0, -1) : rawstage) as BaseStage;
|
||||
if (StageMappingTagNameCache.has(stageMapping)) {
|
||||
return StageMappingTagNameCache.get(stageMapping)!;
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
const [token, tag, variant] = match<BaseStage, InStage>(rest)
|
||||
.with([P.string, P.string, PVariant], ([token, tag, variant]) => [token, tag, variant])
|
||||
.with([P.string, PVariant], ([token, variant]) => [token, token, variant])
|
||||
.with([P.string, P.string], ([token, tag]) => [token, tag, STANDARD])
|
||||
.with([P.string], ([token])=> [token, token, STANDARD])
|
||||
.exhaustive();
|
||||
const module = await stageMapping.importCallback();
|
||||
const StageConstructor = module.default;
|
||||
const tag = window.customElements.getName(StageConstructor);
|
||||
|
||||
return [token, { tag, variant, importfn }];
|
||||
}),
|
||||
);
|
||||
if (!tag) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.trace(
|
||||
`Failed to load stage module: no custom element found in module`,
|
||||
stageMapping,
|
||||
);
|
||||
throw new TypeError("Failed to load stage module: no custom element found");
|
||||
}
|
||||
|
||||
StageMappingTagNameCache.set(stageMapping, tag);
|
||||
|
||||
return tag;
|
||||
}
|
||||
|
||||
@@ -3,19 +3,7 @@ import { LitFC } from "#elements/types";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
import { isDefaultAvatar } from "#elements/utils/images";
|
||||
|
||||
import {
|
||||
AccessDeniedChallenge,
|
||||
AuthenticatorDuoChallenge,
|
||||
AuthenticatorEmailChallenge,
|
||||
AuthenticatorStaticChallenge,
|
||||
AuthenticatorTOTPChallenge,
|
||||
AuthenticatorWebAuthnChallenge,
|
||||
CaptchaChallenge,
|
||||
ConsentChallenge,
|
||||
PasswordChallenge,
|
||||
SessionEndChallenge,
|
||||
UserLoginChallenge,
|
||||
} from "@goauthentik/api";
|
||||
import { StageChallengeLike } from "#flow/types";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { css, CSSResult, html, nothing } from "lit";
|
||||
@@ -110,26 +98,8 @@ export class AKFormStatic extends AKElement {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type FormStaticChallenge =
|
||||
| SessionEndChallenge
|
||||
| AccessDeniedChallenge
|
||||
| AuthenticatorDuoChallenge
|
||||
| AuthenticatorEmailChallenge
|
||||
| AuthenticatorStaticChallenge
|
||||
| AuthenticatorTOTPChallenge
|
||||
| AuthenticatorWebAuthnChallenge
|
||||
| CaptchaChallenge
|
||||
| ConsentChallenge
|
||||
| PasswordChallenge
|
||||
| UserLoginChallenge;
|
||||
|
||||
export interface FlowUserDetailsProps {
|
||||
challenge?: Partial<
|
||||
Pick<FormStaticChallenge, "pendingUserAvatar" | "pendingUser" | "flowInfo">
|
||||
> | null;
|
||||
challenge?: StageChallengeLike | null;
|
||||
}
|
||||
|
||||
export const FlowUserDetails: LitFC<FlowUserDetailsProps> = ({ challenge }) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import Styles from "./ak-flow-card.css";
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { FlowChallengeLike } from "#flow/components/types";
|
||||
import { FormStaticChallenge } from "#flow/types";
|
||||
|
||||
import { CSSResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
@@ -27,7 +27,7 @@ export class FlowCard extends AKElement {
|
||||
role = "presentation";
|
||||
|
||||
@property({ type: Object })
|
||||
challenge?: Pick<FlowChallengeLike, "flowInfo">;
|
||||
challenge?: Pick<FormStaticChallenge, "flowInfo">;
|
||||
|
||||
@property({ type: Boolean })
|
||||
loading = false;
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { ChallengeTypes } from "@goauthentik/api";
|
||||
|
||||
/**
|
||||
* Type utility to exclude the `component` property.
|
||||
*/
|
||||
export type ExcludeComponent<T> = T extends { component: string } ? Omit<T, "component"> : T;
|
||||
|
||||
/**
|
||||
* A {@link ChallengeTypes} without the `component` property.
|
||||
*/
|
||||
export type FlowChallengeLike = ExcludeComponent<ChallengeTypes>;
|
||||
@@ -106,7 +106,7 @@ export class IFrameLogoutStage extends BaseStage<
|
||||
`,
|
||||
];
|
||||
|
||||
public override firstUpdated(changedProperties: PropertyValues): void {
|
||||
public override firstUpdated(changedProperties: PropertyValues<this>): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
|
||||
// Initialize status tracking
|
||||
@@ -263,6 +263,8 @@ export class IFrameLogoutStage extends BaseStage<
|
||||
}
|
||||
}
|
||||
|
||||
export default IFrameLogoutStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-provider-iframe-logout": IFrameLogoutStage;
|
||||
|
||||
@@ -69,6 +69,8 @@ export class SessionEnd extends BaseStage<SessionEndChallenge, unknown> {
|
||||
}
|
||||
}
|
||||
|
||||
export default SessionEnd;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-session-end": SessionEnd;
|
||||
|
||||
@@ -68,6 +68,8 @@ export class OAuth2DeviceCode extends BaseStage<
|
||||
}
|
||||
}
|
||||
|
||||
export default OAuth2DeviceCode;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-flow-provider-oauth2-code": OAuth2DeviceCode;
|
||||
|
||||
@@ -25,6 +25,8 @@ export class DeviceCodeFinish extends BaseStage<
|
||||
}
|
||||
}
|
||||
|
||||
export default DeviceCodeFinish;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-flow-provider-oauth2-code-finish": DeviceCodeFinish;
|
||||
|
||||
@@ -31,7 +31,7 @@ export class NativeLogoutStage extends BaseStage<
|
||||
|
||||
public static styles: CSSResult[] = [PFLogin, PFForm, PFButton, PFFormControl, PFTitle];
|
||||
|
||||
public override firstUpdated(changedProperties: PropertyValues): void {
|
||||
public override firstUpdated(changedProperties: PropertyValues<this>): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
|
||||
if (!this.challenge) {
|
||||
@@ -121,6 +121,8 @@ export class NativeLogoutStage extends BaseStage<
|
||||
}
|
||||
}
|
||||
|
||||
export default NativeLogoutStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-provider-saml-native-logout": NativeLogoutStage;
|
||||
|
||||
@@ -74,6 +74,8 @@ export class AppleLoginInit extends BaseStage<AppleLoginChallenge, AppleChalleng
|
||||
}
|
||||
}
|
||||
|
||||
export default AppleLoginInit;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-flow-source-oauth-apple": AppleLoginInit;
|
||||
|
||||
@@ -87,6 +87,8 @@ export class PlexLoginInit extends BaseStage<
|
||||
}
|
||||
}
|
||||
|
||||
export default PlexLoginInit;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-flow-source-plex": PlexLoginInit;
|
||||
|
||||
@@ -72,6 +72,8 @@ export class TelegramLogin extends BaseStage<
|
||||
}
|
||||
}
|
||||
|
||||
export default TelegramLogin;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-flow-source-telegram": TelegramLogin;
|
||||
|
||||
@@ -68,6 +68,8 @@ export class FlowErrorStage extends BaseStage<FlowErrorChallenge, FlowChallengeR
|
||||
}
|
||||
}
|
||||
|
||||
export default FlowErrorStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-flow-error": FlowErrorStage;
|
||||
|
||||
@@ -41,6 +41,8 @@ export class FlowFrameStage extends BaseStage<FrameChallenge, FrameChallengeResp
|
||||
}
|
||||
}
|
||||
|
||||
export default FlowFrameStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"xak-flow-frame": FlowFrameStage;
|
||||
|
||||
@@ -118,6 +118,8 @@ export class RedirectStage extends BaseStage<RedirectChallenge, FlowChallengeRes
|
||||
}
|
||||
}
|
||||
|
||||
export default RedirectStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-redirect": RedirectStage;
|
||||
|
||||
@@ -55,6 +55,8 @@ export class AccessDeniedStage extends BaseStage<
|
||||
}
|
||||
}
|
||||
|
||||
export default AccessDeniedStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-access-denied": AccessDeniedStage;
|
||||
|
||||
@@ -103,6 +103,8 @@ export class AuthenticatorDuoStage extends BaseStage<
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthenticatorDuoStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-authenticator-duo": AuthenticatorDuoStage;
|
||||
|
||||
@@ -142,6 +142,8 @@ export class AuthenticatorEmailStage extends BaseStage<
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthenticatorEmailStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-authenticator-email": AuthenticatorEmailStage;
|
||||
|
||||
@@ -129,6 +129,8 @@ export class AuthenticatorSMSStage extends BaseStage<
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthenticatorSMSStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-authenticator-sms": AuthenticatorSMSStage;
|
||||
|
||||
@@ -81,6 +81,8 @@ export class AuthenticatorStaticStage extends BaseStage<
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthenticatorStaticStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-authenticator-static": AuthenticatorStaticStage;
|
||||
|
||||
@@ -190,6 +190,8 @@ export class AuthenticatorTOTPStage extends BaseStage<
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthenticatorTOTPStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-authenticator-totp": AuthenticatorTOTPStage;
|
||||
|
||||
@@ -7,8 +7,9 @@ import Styles from "./AuthenticatorValidateStage.css";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { BaseStage, StageHost, SubmitOptions } from "#flow/stages/base";
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
import { PasswordManagerPrefill } from "#flow/stages/identification/IdentificationStage";
|
||||
import type { StageHost, SubmitOptions } from "#flow/types";
|
||||
|
||||
import {
|
||||
AuthenticatorValidationChallenge,
|
||||
@@ -313,6 +314,8 @@ export class AuthenticatorValidateStage
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthenticatorValidateStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-authenticator-validate": AuthenticatorValidateStage;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BaseStage, FlowInfoChallenge, PendingUserChallenge } from "#flow/stages/base";
|
||||
import { BaseStage } from "#flow/stages/base";
|
||||
import { StageChallengeLike } from "#flow/types";
|
||||
|
||||
import { DeviceChallenge } from "@goauthentik/api";
|
||||
|
||||
@@ -13,10 +14,7 @@ import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-gro
|
||||
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
|
||||
export class BaseDeviceStage<
|
||||
Tin extends FlowInfoChallenge & PendingUserChallenge,
|
||||
Tout,
|
||||
> extends BaseStage<Tin, Tout> {
|
||||
export class BaseDeviceStage<Tin extends StageChallengeLike, Tout> extends BaseStage<Tin, Tout> {
|
||||
@property({ attribute: false })
|
||||
deviceChallenge?: DeviceChallenge;
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ export class AutosubmitStage extends BaseStage<
|
||||
}
|
||||
}
|
||||
|
||||
export default AutosubmitStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-autosubmit": AutosubmitStage;
|
||||
|
||||
@@ -8,28 +8,13 @@ import { WithLocale } from "#elements/mixins/locale";
|
||||
import { FocusTarget } from "#elements/utils/focus";
|
||||
|
||||
import { FlowUserDetails } from "#flow/FormStatic";
|
||||
import { IBaseStage, StageChallengeLike, StageHost } from "#flow/types";
|
||||
|
||||
import { ConsoleLogger } from "#logger/browser";
|
||||
|
||||
import { ContextualFlowInfo, CurrentBrand, ErrorDetail } from "@goauthentik/api";
|
||||
|
||||
import { html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { html, nothing, PropertyValues } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
export interface SubmitOptions {
|
||||
invisible: boolean;
|
||||
}
|
||||
|
||||
export interface StageHost {
|
||||
challenge?: unknown;
|
||||
flowSlug?: string;
|
||||
loading: boolean;
|
||||
reset?: () => void;
|
||||
submit(payload: unknown, options?: SubmitOptions): Promise<boolean>;
|
||||
|
||||
readonly brand?: CurrentBrand;
|
||||
}
|
||||
|
||||
export function readFileAsync(file: Blob) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@@ -41,22 +26,7 @@ export function readFileAsync(file: Blob) {
|
||||
});
|
||||
}
|
||||
|
||||
// Challenge which contains flow info
|
||||
export interface FlowInfoChallenge {
|
||||
flowInfo?: ContextualFlowInfo;
|
||||
}
|
||||
|
||||
// Challenge which has a pending user
|
||||
export interface PendingUserChallenge {
|
||||
pendingUser?: string;
|
||||
pendingUserAvatar?: string;
|
||||
}
|
||||
|
||||
export interface ResponseErrorsChallenge {
|
||||
responseErrors?: {
|
||||
[key: string]: ErrorDetail[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for all flow stages.
|
||||
@@ -65,12 +35,12 @@ export interface ResponseErrorsChallenge {
|
||||
* @prop {StageHost} host The host managing this stage.
|
||||
* @prop {Tin} challenge The challenge provided to this stage.
|
||||
*/
|
||||
export abstract class BaseStage<
|
||||
Tin extends FlowInfoChallenge & PendingUserChallenge & ResponseErrorsChallenge,
|
||||
Tout,
|
||||
> extends WithLocale(AKElement) {
|
||||
export abstract class BaseStage<Tin extends StageChallengeLike, Tout = unknown>
|
||||
extends WithLocale(AKElement)
|
||||
implements IBaseStage<Tin, Tout>
|
||||
{
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
...LitElement.shadowRootOptions,
|
||||
...AKElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -559,6 +559,8 @@ export class CaptchaStage
|
||||
}
|
||||
}
|
||||
|
||||
export default CaptchaStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-captcha": CaptchaStage;
|
||||
|
||||
@@ -141,6 +141,8 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
|
||||
}
|
||||
}
|
||||
|
||||
export default ConsentStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-consent": ConsentStage;
|
||||
|
||||
@@ -38,6 +38,8 @@ export class DummyStage extends BaseStage<DummyChallenge, DummyChallengeResponse
|
||||
}
|
||||
}
|
||||
|
||||
export default DummyStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-dummy": DummyStage;
|
||||
|
||||
@@ -40,6 +40,8 @@ export class EmailStage extends BaseStage<EmailChallenge, EmailChallengeResponse
|
||||
}
|
||||
}
|
||||
|
||||
export default EmailStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-email": EmailStage;
|
||||
|
||||
@@ -85,6 +85,8 @@ export class EndpointAgentStage extends BaseStage<
|
||||
}
|
||||
}
|
||||
|
||||
export default EndpointAgentStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-endpoint-agent": EndpointAgentStage;
|
||||
|
||||
@@ -548,6 +548,8 @@ export class IdentificationStage extends BaseStage<
|
||||
//#endregion
|
||||
}
|
||||
|
||||
export default IdentificationStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-identification": IdentificationStage;
|
||||
|
||||
@@ -82,6 +82,8 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
|
||||
}
|
||||
}
|
||||
|
||||
export default PasswordStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-password": PasswordStage;
|
||||
|
||||
@@ -42,6 +42,8 @@ import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
export class PromptStage extends WithCapabilitiesConfig(
|
||||
BaseStage<PromptChallenge, PromptChallengeResponseRequest>,
|
||||
) {
|
||||
static shadowRootOptions: ShadowRootInit = BaseStage.shadowRootOptions;
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
PFLogin,
|
||||
PFAlert,
|
||||
@@ -307,6 +309,8 @@ ${prompt.initialValue}</textarea
|
||||
}
|
||||
}
|
||||
|
||||
export default PromptStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-prompt": PromptStage;
|
||||
|
||||
@@ -64,6 +64,8 @@ export class PasswordStage extends BaseStage<
|
||||
}
|
||||
}
|
||||
|
||||
export default PasswordStage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-user-login": PasswordStage;
|
||||
|
||||
93
web/src/flow/types.ts
Normal file
93
web/src/flow/types.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @file Types related to flow stages.
|
||||
*/
|
||||
|
||||
import { DefaultImportCallback } from "#common/modules/types";
|
||||
|
||||
import type {
|
||||
AccessDeniedChallenge,
|
||||
AuthenticatorDuoChallenge,
|
||||
AuthenticatorEmailChallenge,
|
||||
AuthenticatorStaticChallenge,
|
||||
AuthenticatorTOTPChallenge,
|
||||
AuthenticatorWebAuthnChallenge,
|
||||
CaptchaChallenge,
|
||||
ChallengeTypes,
|
||||
ConsentChallenge,
|
||||
CurrentBrand,
|
||||
PasswordChallenge,
|
||||
SessionEndChallenge,
|
||||
UserLoginChallenge,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { ReactiveControllerHost } from "lit";
|
||||
|
||||
/**
|
||||
* Type utility to exclude the `component` property.
|
||||
*/
|
||||
export type ExcludeComponent<T> = T extends { component: string } ? Omit<T, "component"> : T;
|
||||
|
||||
/**
|
||||
* A {@link ChallengeTypes} without the `component` property.
|
||||
*/
|
||||
export type FlowChallengeLike = ExcludeComponent<ChallengeTypes>;
|
||||
|
||||
export type FlowChallengeComponentName = ChallengeTypes["component"];
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type FormStaticChallenge =
|
||||
| SessionEndChallenge
|
||||
| AccessDeniedChallenge
|
||||
| AuthenticatorDuoChallenge
|
||||
| AuthenticatorEmailChallenge
|
||||
| AuthenticatorStaticChallenge
|
||||
| AuthenticatorTOTPChallenge
|
||||
| AuthenticatorWebAuthnChallenge
|
||||
| CaptchaChallenge
|
||||
| ConsentChallenge
|
||||
| PasswordChallenge
|
||||
| UserLoginChallenge;
|
||||
|
||||
export type StageChallengeLike = Partial<
|
||||
Pick<FormStaticChallenge, "pendingUserAvatar" | "pendingUser" | "flowInfo" | "responseErrors">
|
||||
>;
|
||||
|
||||
export interface SubmitOptions {
|
||||
invisible: boolean;
|
||||
}
|
||||
|
||||
export interface StageHost {
|
||||
challenge?: unknown;
|
||||
flowSlug?: string;
|
||||
loading: boolean;
|
||||
reset?: () => void;
|
||||
submit(payload: unknown, options?: SubmitOptions): Promise<boolean>;
|
||||
|
||||
readonly brand?: CurrentBrand;
|
||||
}
|
||||
|
||||
export interface IBaseStage<Tin extends StageChallengeLike, Tout = never>
|
||||
extends HTMLElement, ReactiveControllerHost {
|
||||
host?: StageHost;
|
||||
challenge: Tin | null;
|
||||
submitForm: (event?: SubmitEvent, defaults?: Tout) => Promise<boolean>;
|
||||
reset?(): void;
|
||||
}
|
||||
|
||||
export type BaseStageConstructor<
|
||||
Tin extends StageChallengeLike = StageChallengeLike,
|
||||
Tout = never,
|
||||
> = new () => IBaseStage<Tin, Tout>;
|
||||
|
||||
export type StageModuleConstructor<
|
||||
Tin extends StageChallengeLike = StageChallengeLike,
|
||||
Tout = never,
|
||||
> = DefaultImportCallback<BaseStageConstructor<Tin, Tout>>;
|
||||
|
||||
/**
|
||||
* A type representing an ES module that exports a stage constructor as its default export.
|
||||
* This is used for dynamic imports of stages.
|
||||
*/
|
||||
export type StageModuleCallback = DefaultImportCallback<BaseStageConstructor>;
|
||||
@@ -5,7 +5,7 @@ import { DeepPartial } from "#common/types";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
|
||||
import { FlowChallengeLike } from "#flow/components/types";
|
||||
import { FlowChallengeLike } from "#flow/types";
|
||||
|
||||
import { ChallengeTypes, ContextualFlowInfoLayoutEnum, UiThemeEnum } from "@goauthentik/api";
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { WithBrandConfig } from "#elements/mixins/branding";
|
||||
import { WithSession } from "#elements/mixins/session";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { StageHost } from "#flow/stages/base";
|
||||
import type { StageHost } from "#flow/types";
|
||||
|
||||
import {
|
||||
ChallengeTypes,
|
||||
|
||||
Reference in New Issue
Block a user