Compare commits

...

11 Commits

Author SHA1 Message Date
Ken Sternberg
62a407ebbc Intermediate commit. 2026-03-13 10:02:18 -07:00
Ken Sternberg
e966159692 The great disarrangement has begun. 2026-03-12 15:57:39 -07:00
Ken Sternberg
b5eab028ee Accidentally deleted this with a 'git reset'; preserving now. 2026-03-12 09:23:41 -07:00
Ken Sternberg
25d7ba59fd Merge branch 'main' into web/style/flow-css-barrel-file
* main:
  core: bump black from 26.3.0 to 26.3.1 (#20848)
  core: bump aws-cdk-lib from 2.242.0 to 2.243.0 (#20849)
  core: bump goauthentik/fips-python from `46b26b8` to `f9f8a26` in /lifecycle/container (#20851)
  core: bump library/node from 25.8.0-trixie to 25.8.1-trixie in /website (#20854)
  ci: bump actions/download-artifact from 8.0.0 to 8.0.1 (#20850)
2026-03-12 09:18:30 -07:00
Ken Sternberg
f7096a2c84 Merge branch 'main' into web/style/flow-css-barrel-file
* main:
  web/flow/stages: permit the form handler to look in the light or shadowDOM for controls (#20832)
  web/style/flow: flow css barrel file (#20833)
  web/flow: provide labels for the stage import-and-invoke table (#20834)
  core: bump goauthentik/fips-python from `3636935` to `46b26b8` in /lifecycle/container (#20842)
  core: bump library/nginx from `0236ee0` to `d0913a1` in /website (#20843)
  core, web: update translations (#20835)
  core: bump goauthentik/fips-debian from `0975985` to `2517845` in /lifecycle/container (#20841)
  core: bump twilio from 9.10.2 to 9.10.3 (#20838)
  core: bump aws-cdk-lib from 2.241.0 to 2.242.0 (#20840)
  ci: bump astral-sh/setup-uv from 7.3.1 to 7.4.0 in /.github/actions/setup (#20844)
2026-03-11 13:07:37 -07:00
Ken Sternberg
4273b15320 Missed a spot. 2026-03-10 13:59:43 -07:00
Ken Sternberg
27a550b18d Merge branch 'main' into web/style/allow-alternative-stylesheets
* main:
  providers/SCIM: Add discover support (#20658)
2026-03-10 13:48:37 -07:00
Ken Sternberg
a09cf62bac Merge branch 'main' into web/style/allow-alternative-stylesheets
* main:
  core: allow interfaces to specify alternative stylesheets (#20774)
  website/docs: update agent docs (#20782)
  core, web: update translations (#20809)
  lifecycle/aws: bump aws-cdk from 2.1109.0 to 2.1110.0 in /lifecycle/aws (#20810)
  core: bump axllent/mailpit from v1.29.2 to v1.29.3 in /tests/e2e (#20811)
  core: bump cachetools from 7.0.4 to 7.0.5 (#20812)
  core: bump goauthentik/fips-python from `b481db2` to `3636935` in /lifecycle/container (#20814)
  core: bump goauthentik/fips-debian from `6c9197b` to `0975985` in /lifecycle/container (#20815)
  web: bump the storybook group across 1 directory with 5 updates (#20816)
  web: bump cspell from 9.6.4 to 9.7.0 (#20817)
  web: bump @formatjs/intl-listformat from 8.2.1 to 8.2.2 in /web (#20818)
  web: bump mermaid from 11.12.3 to 11.13.0 in /web (#20819)
  web: bump @types/node from 25.3.5 to 25.4.0 in /web (#20820)
  endpoints/connectors/agent: cleanup leftover (#20808)
  endpoints: prevent selection of incompatible connector (#20806)
  website/docs: Add steps to set up group devices (#20735)
  web/rbac: disambiguate duplicate permission names in initial permissions (#20786)
2026-03-10 10:28:08 -07:00
Ken Sternberg
cf2ab7f701 Isolated flows to have their own CSS barrel file. 2026-03-09 14:48:38 -07:00
Ken Sternberg
cc4ce19ccc Merge branch 'main' into web/style/allow-alternative-stylesheets
* main: (23 commits)
  web: CodeSpell -> CSpell migration (#20188)
  core: bump goauthentik.io/api/v3 to 3.2026.5.0-rc1-1773052201 (#20801)
  core: bump github.com/go-openapi/runtime from 0.29.2 to 0.29.3 (#20787)
  core: bump golang.org/x/sync from 0.19.0 to 0.20.0 (#20788)
  web: bump the storybook group across 1 directory with 5 updates (#20794)
  core: bump golang.org/x/oauth2 from 0.35.0 to 0.36.0 (#20789)
  core: bump goauthentik/selenium from 145.0-ak-0.40.3 to 145.0-ak-0.40.5 in /tests/e2e (#20790)
  core: bump black from 26.1.0 to 26.3.0 (#20791)
  core: bump cachetools from 7.0.3 to 7.0.4 (#20792)
  core: bump goauthentik/fips-python from `38c4dd2` to `b481db2` in /lifecycle/container (#20796)
  web: bump @rollup/plugin-commonjs from 29.0.1 to 29.0.2 in /web in the rollup group across 1 directory (#20795)
  core: bump astral-sh/uv from 0.10.8 to 0.10.9 in /lifecycle/container (#20797)
  core: bump goauthentik/fips-debian from `4966b90` to `6c9197b` in /lifecycle/container (#20798)
  web: bump @types/node from 25.3.3 to 25.3.5 in /web (#20799)
  web: bump knip from 5.85.0 to 5.86.0 in /web (#20800)
  enterprise/endpoints/connectors: add google_chrome (#19129)
  providers/oauth2: decode percent-encoded basic auth (#20779)
  web: bump immutable from 5.1.4 to 5.1.5 in /web (#20720)
  web: bump the storybook group across 1 directory with 5 updates (#20731)
  web: bump @rollup/plugin-commonjs from 29.0.0 to 29.0.1 in /web in the rollup group across 1 directory (#20732)
  ...
2026-03-09 10:50:56 -07:00
Ken Sternberg
bd304e76c8 web/core/templates: make it possible for interfaces to designate alternative stylesheets
## What

Moves the stylesheet invocation in `theme.html` to `skeleton.html`, give it a block and a block name so that pages using `skeleton.html` can override or extend it as needed.

## Why

The biggest wall we’re hitting right now is the lack of flexibility at the very top of the CSS. We simply use the same CSS file for *too much*, when really we should be thinking in terms of leaner, more targeted top-level CSS for some things, and more rich and expressive CSS when it’s necessary.

The style sheet was being loaded unconditionally in `theme.html`; it’s not in a conditional statement or overridable where it was; `skeleton` just loads it blindly. This change lets `theme.html` be what it is meant to be, an isolated container for the JavaScript logic for discerning the color mode, while enabling CSS developers to elide the stylesheet, provide alternative stylesheets, or (using `{{ block.super}}`) amend or extend the default stylesheet.
2026-03-06 14:15:11 -08:00
6 changed files with 355 additions and 97 deletions

View File

@@ -35,43 +35,14 @@
{% block head %} {% block head %}
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
<style data-id="flow-css">
:root {
--ak-global--background-image: url("{{ flow_background_url }}");
}
</style>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<ak-skip-to-content></ak-skip-to-content> <ak-skip-to-content></ak-skip-to-content>
<ak-message-container></ak-message-container> <ak-message-container></ak-message-container>
<ak-flow
<div class="pf-c-page__drawer"> slug="{{ flow.slug }}"
<div class="pf-c-drawer pf-m-collapsed" id="flow-drawer"> layout={{ flow.layout|default:'stacked' }}"
<div class="pf-c-drawer__main"> style='--ak-global--background-image: url("{{ flow_background_url }}")'
<div class="pf-c-drawer__content"> ></ak-flow>
<div class="pf-c-drawer__body">
<ak-flow-executor
slug="{{ flow.slug }}"
class="pf-c-login"
data-layout="{{ flow.layout|default:'stacked' }}"
loading
>
{% include "base/placeholder.html" %}
<ak-brand-links name="flow-links" slot="footer"></ak-brand-links>
</ak-flow-executor>
</div>
</div>
<ak-flow-inspector
id="flow-inspector"
data-registration="lazy"
class="pf-c-drawer__panel pf-m-width-33"
slug="{{ flow.slug }}"
></ak-flow-inspector>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,103 @@
import { html, nothing, render, TemplateResult } from "lit";
import { AsyncDirective, DirectiveResult } from "lit/async-directive.js";
import { ChildPart, directive, PartInfo, PartType } from "lit/directive.js";
import { RootPart } from "lit/html.js";
export interface LightChildOptions {
// Optional alternative target for any `@event`-style handlers passed into the template. NOTE:
// this only works if the handlers do not already have a `this` bound to them, so only ordinary
// functions and methods will respond to this parameter; arrow function class fields and bound
// functions will use the `this` to which they were bound.
host?: Element;
slotName?: string;
}
class LightChildDirective extends AsyncDirective {
#slotName: string | null = null;
#slot: HTMLSlotElement | null = null;
#host: Element | null = null;
#rootPart: RootPart | null = null;
#sentinel: Comment | null = null;
constructor(partInfo: PartInfo) {
super(partInfo);
if (partInfo.type !== PartType.CHILD) {
throw new Error("The `light()` directive can only be use in child position");
}
}
render(_template?: TemplateResult | DirectiveResult, options?: LightChildOptions) {
this.#slotName ??= options?.slotName ?? `lc-${Math.random().toString(36).slice(2, 8)}`;
return html`<slot name="${this.#slotName}"></slot>`;
}
update(
part: ChildPart,
[template, options = {}]: [TemplateResult | DirectiveResult, LightChildOptions],
) {
this.#slotName ??= options?.slotName ?? `lc-${Math.random().toString(36).slice(2, 8)}`;
// This places a comment in the LightDom that belongs to this directive. Comments are not
// part of the DOM tree for the purposes of CSS, so it will be possible to style this child
// directly without a wrapper.
if (!this.#sentinel) {
const rootNode = part.parentNode.getRootNode();
this.#host ??= (part.options?.host ||
(rootNode instanceof ShadowRoot ? rootNode.host : null)) as Element | null;
if (!this.#host) {
throw new Error(
"light() must be used inside a shadow root or a valid options.host",
);
}
this.#sentinel = document.createComment("");
this.#host.appendChild(this.#sentinel);
}
if (!this.#sentinel.parentNode) {
throw new Error("Could not assign sentinel to element.");
}
const renderOptions = Object.fromEntries(
Object.entries(options).filter(([key]) => ["host"].includes(key)),
);
this.#rootPart = render(template, this.#sentinel.parentNode as HTMLElement, {
renderBefore: this.#sentinel,
...renderOptions,
});
const rendered = this.#sentinel.previousSibling;
if (rendered instanceof Element) {
rendered.slot = this.#slotName;
}
return (this.#slot ??= Object.assign(document.createElement("slot"), {
name: this.#slotName,
}));
}
disconnected() {
if (this.#sentinel?.parentNode && this.#host?.isConnected) {
// The content being rendered this way, with the `render()` *function*, has its own Lit
// VDOM comment nodes in the HTML unrelated to the `host` context. Rendering `nothing`
// here ensures that any children of the lightDOM component receive clean-up signals and
// correctly disconnect (including listeners, etc.) from the current display as well.
// This is what lets us receive other DirectiveResults as template content.
render(nothing, this.#sentinel.parentNode as HTMLElement, {
renderBefore: this.#sentinel,
});
}
this.#rootPart?.setConnected(false);
}
reconnected() {
this.#rootPart?.setConnected(true);
}
}
export const light = directive(LightChildDirective);

84
web/src/flow/Flow.ts Normal file
View File

@@ -0,0 +1,84 @@
import "#flow/FlowExecutor";
import "#flow/inspector/FlowInspector";
import "#flow/components/ak-brand-footer";
import { light } from "#elements/directives/light";
import { Interface } from "#elements/Interface";
import AKPlaceholder from "#styles/authentik/base/placeholder.css";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css";
@customElement("ak-flow")
export class Flow extends Interface {
static readonly styles = [PFDrawer, PFLogin, PFSpinner, AKPlaceholder];
@property()
public slug?: string;
@property()
public layout?: string = "stacked";
protected renderPlacehonder() {
return html`<div class="ak-c-placeholder" id="ak-placeholder" slot="placeholder">
<span class="pf-c-spinner" role="progressbar" aria-valuetext=${msg("Loading...")}>
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</div>`;
}
render() {
const { slug, layout } = this;
const footerClasses = classMap({
"pf-c-login_footer": true,
"pf-m-dark": this.layout === FlowLayoutEnum.Stacked,
});
return html`
<div class="pf-c-page__drawer">
<div class="pf-c-drawer pf-m-collapsed" id="flow-drawer">
<div class="pf-c-drawer__main">
<div class="pf-c-drawer__content">
<div class="pf-c-drawer__body">
${light(
html`<ak-flow-executor
slug="${slug}"
class="pf-c-login"
data-layout="${layout}"
loading
>
${this.renderPlaceholder()}
</ak-flow-executor>`
)}
<footer
aria-label=${msg("Site footer")}
name="site-footer"
part="footer"
class=${footerClasses}
>
<ak-brand-links name="flow-links"></ak-brand-links>
</footer>
</div>
</div>
<ak-flow-inspector
id="flow-inspector"
data-registration="lazy"
class="pf-c-drawer__panel pf-m-width-33"
slug=${slug}
></ak-flow-inspector>
</div>
</div>
</div>
`;
}
}

View File

@@ -15,8 +15,8 @@ import { applyBackgroundImageProperty } from "#common/theme";
import { AKSessionAuthenticatedEvent } from "#common/ws/events"; import { AKSessionAuthenticatedEvent } from "#common/ws/events";
import { WebsocketClient } from "#common/ws/WebSocketClient"; import { WebsocketClient } from "#common/ws/WebSocketClient";
import { AKElement } from "#elements/Base";
import { listen } from "#elements/decorators/listen"; import { listen } from "#elements/decorators/listen";
import { Interface } from "#elements/Interface";
import { showAPIErrorMessage } from "#elements/messages/MessageContainer"; import { showAPIErrorMessage } from "#elements/messages/MessageContainer";
import { WithBrandConfig } from "#elements/mixins/branding"; import { WithBrandConfig } from "#elements/mixins/branding";
import { LitPropertyRecord, SlottedTemplateResult } from "#elements/types"; import { LitPropertyRecord, SlottedTemplateResult } from "#elements/types";
@@ -78,7 +78,7 @@ import PFTitle from "@patternfly/patternfly/components/Title/title.css";
* @part locale-select-select - The select element of the locale select component. * @part locale-select-select - The select element of the locale select component.
*/ */
@customElement("ak-flow-executor") @customElement("ak-flow-executor")
export class FlowExecutor extends WithBrandConfig(Interface) implements StageHost { export class FlowExecutor extends WithBrandConfig(AKElement) implements StageHost {
public static readonly DefaultLayout: FlowLayoutEnum = public static readonly DefaultLayout: FlowLayoutEnum =
globalAK()?.flow?.layout || FlowLayoutEnum.Stacked; globalAK()?.flow?.layout || FlowLayoutEnum.Stacked;
@@ -98,7 +98,11 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
//#region Properties //#region Properties
@property({ type: String, attribute: "slug", useDefault: true }) @property({
type: String,
attribute: "slug",
useDefault: true,
})
public flowSlug: string = window.location.pathname.split("/")[3]; public flowSlug: string = window.location.pathname.split("/")[3];
@property({ attribute: false }) @property({ attribute: false })
@@ -107,7 +111,12 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
@property({ type: Boolean }) @property({ type: Boolean })
public loading = false; public loading = false;
@property({ type: String, attribute: "data-layout", useDefault: true, reflect: true }) @property({
type: String,
attribute: "data-layout",
useDefault: true,
reflect: true,
})
public layout: FlowLayoutEnum = FlowExecutor.DefaultLayout; public layout: FlowLayoutEnum = FlowExecutor.DefaultLayout;
//#endregion //#endregion
@@ -195,7 +204,9 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
? this.closest<HTMLDivElement>(".docs-story") ? this.closest<HTMLDivElement>(".docs-story")
: this.ownerDocument.body; : this.ownerDocument.body;
applyBackgroundImageProperty(background, { target }); applyBackgroundImageProperty(background, {
target,
});
} }
//#region Listeners //#region Listeners
@@ -283,7 +294,7 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
public submit = async ( public submit = async (
payload?: FlowChallengeResponseRequest, payload?: FlowChallengeResponseRequest,
options?: SubmitOptions, options?: SubmitOptions
): Promise<boolean> => { ): Promise<boolean> => {
if (!payload) throw new Error("No payload provided"); if (!payload) throw new Error("No payload provided");
if (!this.challenge) throw new Error("No challenge provided"); if (!this.challenge) throw new Error("No challenge provided");
@@ -336,7 +347,7 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
} }
return this.renderChallengeError( return this.renderChallengeError(
`No stage found for component: ${challenge.component}`, `No stage found for component: ${challenge.component}`
); );
} }
@@ -364,11 +375,16 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
const props = spread( const props = spread(
match(variant) match(variant)
.with("challenge", () => challengeProps) .with("challenge", () => challengeProps)
.with("standard", () => ({ ...challengeProps, ...litParts })) .with("standard", () => ({
.exhaustive(), ...challengeProps,
...litParts,
}))
.exhaustive()
); );
return staticHTML`<${unsafeStatic(tag)} ${props}></${unsafeStatic(tag)}>`; return html`<div part="challange">
${light(staticHTML`<${unsafeStatic(tag)} ${props}></${unsafeStatic(tag)}>`)}
</div>`;
} }
protected renderChallengeError(error: unknown): SlottedTemplateResult { protected renderChallengeError(error: unknown): SlottedTemplateResult {
@@ -394,59 +410,10 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
return html`<slot name="placeholder"></slot>`; return html`<slot name="placeholder"></slot>`;
} }
protected renderFrameBackground(): SlottedTemplateResult {
return guard([this.layout, this.challenge], () => {
if (
this.layout !== FlowLayoutEnum.SidebarLeftFrameBackground &&
this.layout !== FlowLayoutEnum.SidebarRightFrameBackground
) {
return nothing;
}
const src = this.challenge?.flowInfo?.background;
if (!src) return nothing;
return html`
<div class="ak-c-login__content" part="content">
<iframe
class="ak-c-login__content-iframe"
part="content-iframe"
name="flow-content-frame"
src=${src}
></iframe>
</div>
`;
});
}
protected renderFooter(): SlottedTemplateResult {
return guard([this.layout], () => {
return html`<footer
aria-label=${msg("Site footer")}
name="site-footer"
part="footer"
class="pf-c-login__footer ${this.layout === FlowLayoutEnum.Stacked
? "pf-m-dark"
: ""}"
>
<slot name="footer"></slot>
</footer>`;
});
}
protected override render(): SlottedTemplateResult { protected override render(): SlottedTemplateResult {
const { challenge, loading } = this; const { challenge, loading } = this;
return html`<ak-locale-select return html`
part="locale-select"
exportparts="label:locale-select-label,select:locale-select-select"
class="pf-m-dark"
></ak-locale-select>
<header class="pf-c-login__header">
<ak-flow-inspector-button></ak-flow-inspector-button>
</header>
<main <main
data-layout=${this.layout} data-layout=${this.layout}
class="pf-c-login__main" class="pf-c-login__main"
@@ -469,13 +436,14 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
: this.renderLoading(); : this.renderLoading();
})} })}
</main> </main>
${this.renderFooter()}`; `;
} }
//#endregion //#endregion
} }
declare global { declare global {
g;
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ak-flow-executor": FlowExecutor; "ak-flow-executor": FlowExecutor;
} }

View File

@@ -0,0 +1,130 @@
import type { Interface } from "#elements/Interface";
type MessageHost = ReactiveControllerHost & Interface;
export class FlowMessageController implements ReactiveController {
// eslint-disable-next-line no-useless-constructor
constructor(private host: RememberMeHost) {
/* no op */
}
hostConnected() {
try {
const sessionId = localStorage.getItem("authentik-remember-me-session");
if (!!this.localSession && sessionId === this.localSession) {
this.username = undefined;
localStorage?.removeItem("authentik-remember-me-user");
}
localStorage?.setItem("authentik-remember-me-session", this.localSession);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (_e: any) {
this.username = undefined;
}
}
get localSession() {
return (getCookie("authentik_csrf") ?? "").substring(0, 8);
}
get usernameField() {
return this.host.renderRoot.querySelector(
'input[name="uidField"]'
) as HTMLInputElement | null;
}
get rememberMeToggle() {
return this.host.renderRoot.querySelector(
"#authentik-remember-me"
) as HTMLInputElement | null;
}
get isValidChallenge() {
return !(
this.host.challenge?.responseErrors &&
this.host.challenge.responseErrors.non_field_errors &&
this.host.challenge.responseErrors.non_field_errors.find(
(cre) => cre.code === "invalid"
)
);
}
get submitButton() {
return this.host.renderRoot.querySelector('button[type="submit"]') as HTMLButtonElement;
}
get isEnabled() {
return this.host.challenge?.enableRememberMe && typeof localStorage !== "undefined";
}
get canAutoSubmit() {
return (
!!this.host.challenge &&
!!this.username &&
!!this.usernameField?.value &&
!this.host.challenge.passwordFields &&
!this.host.challenge.passwordlessUrl
);
}
// Before the page is updated, try to extract the username from localstorage.
hostUpdate() {
if (!this.isEnabled) {
return;
}
try {
this.username = localStorage.getItem("authentik-remember-me-user") || undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (_e: any) {
this.username = undefined;
}
}
// After the page is updated, if everything is ready to go, do the autosubmit.
hostUpdated() {
if (this.isEnabled && this.canAutoSubmit) {
this.submitButton?.click();
}
}
trackRememberMe() {
if (!this.usernameField || this.usernameField.value === undefined) {
return;
}
this.username = this.usernameField.value;
localStorage?.setItem("authentik-remember-me-user", this.username);
}
// When active, save current details and record every keystroke to the username.
// When inactive, clear all fields and remove keystroke recorder.
toggleRememberMe() {
if (!this.rememberMeToggle || !this.rememberMeToggle.checked) {
localStorage?.removeItem("authentik-remember-me-user");
localStorage?.removeItem("authentik-remember-me-session");
this.username = undefined;
this.usernameField?.removeEventListener("keyup", this.trackRememberMe);
return;
}
if (!this.usernameField) {
return;
}
localStorage?.setItem("authentik-remember-me-user", this.usernameField.value);
localStorage?.setItem("authentik-remember-me-session", this.localSession);
this.usernameField.addEventListener("keyup", this.trackRememberMe);
}
render() {
return this.isEnabled
? html` <label class="pf-c-switch remember-me-switch">
<input
class="pf-c-switch__input"
id="authentik-remember-me"
@click=${this.toggleRememberMe}
type="checkbox"
?checked=${!!this.username}
/>
<span class="pf-c-form__label">${msg("Remember me on this device")}</span>
</label>`
: nothing;
}
}

View File

@@ -1,5 +1,5 @@
import "#elements/messages/MessageContainer"; import "#elements/messages/MessageContainer";
import "#flow/FlowExecutor"; import "#flow/Flow";
// Statically import some stages to speed up load speed // Statically import some stages to speed up load speed
import "#flow/stages/access_denied/AccessDeniedStage"; import "#flow/stages/access_denied/AccessDeniedStage";
// Import webauthn-related stages to prevent issues on safari // Import webauthn-related stages to prevent issues on safari
@@ -15,5 +15,7 @@ import "#flow/stages/password/PasswordStage";
// end of stage import // end of stage import
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
await import("@goauthentik/esbuild-plugin-live-reload/client"); await import(
"@goauthentik/esbuild-plugin-live-reload/client"
);
} }