import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EVENT_FLOW_ADVANCE, EVENT_FLOW_INSPECTOR_TOGGLE, TITLE_DEFAULT, } from "@goauthentik/common/constants"; import { globalAK } from "@goauthentik/common/global"; import { configureSentry } from "@goauthentik/common/sentry"; import { first } from "@goauthentik/common/utils"; import { WebsocketClient } from "@goauthentik/common/ws"; import { Interface } from "@goauthentik/elements/Interface"; import "@goauthentik/elements/LoadingOverlay"; import "@goauthentik/elements/ak-locale-context"; import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand"; import { themeImage } from "@goauthentik/elements/utils/images"; import "@goauthentik/flow/components/ak-brand-footer"; import "@goauthentik/flow/sources/apple/AppleLoginInit"; import "@goauthentik/flow/sources/plex/PlexLoginInit"; import "@goauthentik/flow/stages/FlowErrorStage"; import "@goauthentik/flow/stages/FlowFrameStage"; import "@goauthentik/flow/stages/RedirectStage"; import { StageHost, SubmitOptions } from "@goauthentik/flow/stages/base"; import { msg } from "@lit/localize"; import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; import { until } from "lit/directives/until.js"; import PFBackgroundImage from "@patternfly/patternfly/components/BackgroundImage/background-image.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css"; import PFList from "@patternfly/patternfly/components/List/list.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"; import { CapabilitiesEnum, ChallengeTypes, ContextualFlowInfo, FetchError, FlowChallengeResponseRequest, FlowErrorChallenge, FlowLayoutEnum, FlowsApi, ResponseError, ShellChallenge, UiThemeEnum, } from "@goauthentik/api"; @customElement("ak-flow-executor") export class FlowExecutor extends Interface implements StageHost { @property() flowSlug: string = window.location.pathname.split("/")[3]; private _challenge?: ChallengeTypes; @property({ attribute: false }) set challenge(value: ChallengeTypes | undefined) { this._challenge = value; if (value?.flowInfo?.title) { document.title = `${value.flowInfo?.title} - ${this.brand?.brandingTitle}`; } else { document.title = this.brand?.brandingTitle || TITLE_DEFAULT; } this.requestUpdate(); } get challenge(): ChallengeTypes | undefined { return this._challenge; } @property({ type: Boolean }) loading = false; @state() inspectorOpen = false; @state() inspectorAvailable = false; @state() flowInfo?: ContextualFlowInfo; ws: WebsocketClient; static get styles(): CSSResult[] { return [PFBase, PFLogin, PFDrawer, PFButton, PFTitle, PFList, PFBackgroundImage].concat(css` :host { --pf-c-login__main-body--PaddingBottom: var(--pf-global--spacer--2xl); } .pf-c-background-image::before { --pf-c-background-image--BackgroundImage: var(--ak-flow-background); --pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background); --pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background); --pf-c-background-image--BackgroundImage--sm-2x: var(--ak-flow-background); --pf-c-background-image--BackgroundImage--lg: var(--ak-flow-background); } .ak-hidden { display: none; } :host { position: relative; } .pf-c-drawer__content { background-color: transparent; } /* layouts */ @media (min-height: 60rem) { .pf-c-login.stacked .pf-c-login__main { margin-top: 13rem; } } .pf-c-login__container.content-right { grid-template-areas: "header main" "footer main" ". main"; } .pf-c-login.sidebar_left { justify-content: flex-start; padding-top: 0; padding-bottom: 0; } .pf-c-login.sidebar_left .ak-login-container, .pf-c-login.sidebar_right .ak-login-container { height: 100vh; background-color: var(--pf-c-login__main--BackgroundColor); padding-left: var(--pf-global--spacer--lg); padding-right: var(--pf-global--spacer--lg); } .pf-c-login.sidebar_left .pf-c-list, .pf-c-login.sidebar_right .pf-c-list { color: #000; } .pf-c-login.sidebar_right { justify-content: flex-end; padding-top: 0; padding-bottom: 0; } :host([theme="dark"]) .pf-c-login.sidebar_left .ak-login-container, :host([theme="dark"]) .pf-c-login.sidebar_right .ak-login-container { background-color: var(--ak-dark-background); } :host([theme="dark"]) .pf-c-login.sidebar_left .pf-c-list, :host([theme="dark"]) .pf-c-login.sidebar_right .pf-c-list { color: var(--ak-dark-foreground); } .pf-c-brand { padding-top: calc( var(--pf-c-login__main-footer-links--PaddingTop) + var(--pf-c-login__main-footer-links--PaddingBottom) + var(--pf-c-login__main-body--PaddingBottom) ); max-height: 9rem; } .ak-brand { display: flex; justify-content: center; } .ak-brand img { padding: 0 2rem; max-height: inherit; } .inspector-toggle { position: absolute; top: 1rem; right: 1rem; z-index: 100; } `); } constructor() { super(); this.ws = new WebsocketClient(); const inspector = new URL(window.location.toString()).searchParams.get("inspector"); if (inspector === "" || inspector === "open") { this.inspectorOpen = true; this.inspectorAvailable = true; } else if (inspector === "available") { this.inspectorAvailable = true; } this.addEventListener(EVENT_FLOW_INSPECTOR_TOGGLE, () => { this.inspectorOpen = !this.inspectorOpen; }); window.addEventListener("message", (event) => { const msg: { source?: string; context?: string; message: string; } = event.data; if (msg.source !== "goauthentik.io" || msg.context !== "flow-executor") { return; } if (msg.message === "submit") { this.submit({} as FlowChallengeResponseRequest); } }); } async getTheme(): Promise { return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic; } async submit( payload?: FlowChallengeResponseRequest, options?: SubmitOptions, ): Promise { if (!payload) return Promise.reject(); if (!this.challenge) return Promise.reject(); // @ts-expect-error payload.component = this.challenge.component; if (!options?.invisible) { this.loading = true; } try { const challenge = await new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({ flowSlug: this.flowSlug, query: window.location.search.substring(1), flowChallengeResponseRequest: payload, }); if (this.inspectorOpen) { window.dispatchEvent( new CustomEvent(EVENT_FLOW_ADVANCE, { bubbles: true, composed: true, }), ); } this.challenge = challenge; if (this.challenge.flowInfo) { this.flowInfo = this.challenge.flowInfo; } return !this.challenge.responseErrors; } catch (exc: unknown) { this.errorMessage(exc as Error | ResponseError | FetchError); return false; } finally { this.loading = false; } } async firstUpdated(): Promise { configureSentry(); if (this.config?.capabilities.includes(CapabilitiesEnum.CanDebug)) { this.inspectorAvailable = true; } this.loading = true; try { const challenge = await new FlowsApi(DEFAULT_CONFIG).flowsExecutorGet({ flowSlug: this.flowSlug, query: window.location.search.substring(1), }); if (this.inspectorOpen) { window.dispatchEvent( new CustomEvent(EVENT_FLOW_ADVANCE, { bubbles: true, composed: true, }), ); } this.challenge = challenge; if (this.challenge.flowInfo) { this.flowInfo = this.challenge.flowInfo; } } catch (exc: unknown) { // Catch JSON or Update errors this.errorMessage(exc as Error | ResponseError | FetchError); } finally { this.loading = false; } } async errorMessage(error: Error | ResponseError | FetchError): Promise { let body = ""; if (error instanceof FetchError) { body = msg("Request failed. Please try again later."); } else if (error instanceof ResponseError) { body = await error.response.text(); } else if (error instanceof Error) { body = error.message; } const challenge: FlowErrorChallenge = { component: "ak-stage-flow-error", error: body, requestId: "", }; this.challenge = challenge as ChallengeTypes; } setShadowStyles(value: ContextualFlowInfo) { if (!value) { return; } this.shadowRoot ?.querySelectorAll(".pf-c-background-image") .forEach((bg) => { bg.style.setProperty("--ak-flow-background", `url('${value?.background}')`); }); } // DOM post-processing has to happen after the render. updated(changedProperties: PropertyValues) { if (changedProperties.has("flowInfo") && this.flowInfo !== undefined) { this.setShadowStyles(this.flowInfo); } } async renderChallenge(): Promise { if (!this.challenge) { return html` `; } switch (this.challenge?.component) { case "ak-stage-access-denied": await import("@goauthentik/flow/stages/access_denied/AccessDeniedStage"); return html``; case "ak-stage-identification": await import("@goauthentik/flow/stages/identification/IdentificationStage"); return html``; case "ak-stage-password": await import("@goauthentik/flow/stages/password/PasswordStage"); return html``; case "ak-stage-captcha": await import("@goauthentik/flow/stages/captcha/CaptchaStage"); return html``; case "ak-stage-consent": await import("@goauthentik/flow/stages/consent/ConsentStage"); return html``; case "ak-stage-dummy": await import("@goauthentik/flow/stages/dummy/DummyStage"); return html``; case "ak-stage-email": await import("@goauthentik/flow/stages/email/EmailStage"); return html``; case "ak-stage-autosubmit": await import("@goauthentik/flow/stages/autosubmit/AutosubmitStage"); return html``; case "ak-stage-prompt": await import("@goauthentik/flow/stages/prompt/PromptStage"); return html``; case "ak-stage-authenticator-totp": await import("@goauthentik/flow/stages/authenticator_totp/AuthenticatorTOTPStage"); return html``; case "ak-stage-authenticator-duo": await import("@goauthentik/flow/stages/authenticator_duo/AuthenticatorDuoStage"); return html``; case "ak-stage-authenticator-static": await import( "@goauthentik/flow/stages/authenticator_static/AuthenticatorStaticStage" ); return html``; case "ak-stage-authenticator-webauthn": return html``; case "ak-stage-authenticator-email": await import( "@goauthentik/flow/stages/authenticator_email/AuthenticatorEmailStage" ); return html``; case "ak-stage-authenticator-sms": await import("@goauthentik/flow/stages/authenticator_sms/AuthenticatorSMSStage"); return html``; case "ak-stage-authenticator-validate": await import( "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage" ); return html``; case "ak-stage-user-login": await import("@goauthentik/flow/stages/user_login/UserLoginStage"); return html``; // Sources case "ak-source-plex": return html``; case "ak-source-oauth-apple": return html``; // Providers case "ak-provider-oauth2-device-code": await import("@goauthentik/flow/providers/oauth2/DeviceCode"); return html``; case "ak-provider-oauth2-device-code-finish": await import("@goauthentik/flow/providers/oauth2/DeviceCodeFinish"); return html``; case "ak-stage-session-end": await import("@goauthentik/flow/providers/SessionEnd"); return html``; // Internal stages case "ak-stage-flow-error": return html``; case "xak-flow-redirect": return html` `; case "xak-flow-shell": return html`${unsafeHTML((this.challenge as ShellChallenge).body)}`; case "xak-flow-frame": return html``; default: return html`Invalid native challenge element`; } } async renderInspector() { if (!this.inspectorOpen) { return nothing; } await import("@goauthentik/flow/FlowInspector"); return html``; } getLayout(): string { const prefilledFlow = globalAK()?.flow?.layout || FlowLayoutEnum.Stacked; if (this.challenge) { return this.challenge?.flowInfo?.layout || prefilledFlow; } return prefilledFlow; } getLayoutClass(): string { const layout = this.getLayout(); switch (layout) { case FlowLayoutEnum.ContentLeft: return "pf-c-login__container"; case FlowLayoutEnum.ContentRight: return "pf-c-login__container content-right"; case FlowLayoutEnum.Stacked: default: return "ak-login-container"; } } render(): TemplateResult { return html`
${(this.inspectorAvailable ?? !this.inspectorOpen) ? html`` : nothing} ${until(this.renderInspector())}
`; } } declare global { interface HTMLElementTagNameMap { "ak-flow-executor": FlowExecutor; } }