web: Fix issues surrounding availability of controllers during init.

This commit is contained in:
Teffen Ellis
2025-05-21 15:48:52 +02:00
parent 5fed8ca575
commit 95ee88c946
39 changed files with 579 additions and 482 deletions

View File

@ -1,9 +1,8 @@
import { WithLicenseSummary } from "#elements/Interface/licenseSummaryProvider";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { DefaultBrand } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/EmptyState"; import "@goauthentik/elements/EmptyState";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import { ModalButton } from "@goauthentik/elements/buttons/ModalButton"; import { ModalButton } from "@goauthentik/elements/buttons/ModalButton";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
@ -57,7 +56,8 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
} }
renderModal() { renderModal() {
let product = globalAK().brand.brandingTitle || DefaultBrand.brandingTitle; let product = this.brandingTitle;
if (this.licenseSummary.status !== LicenseSummaryStatusEnum.Unlicensed) { if (this.licenseSummary.status !== LicenseSummaryStatusEnum.Unlicensed) {
product += ` ${msg("Enterprise")}`; product += ` ${msg("Enterprise")}`;
} }
@ -73,7 +73,7 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
<div class="pf-c-about-modal-box__brand"> <div class="pf-c-about-modal-box__brand">
<img <img
class="pf-c-about-modal-box__brand-image" class="pf-c-about-modal-box__brand-image"
src=${this.brand?.brandingFavicon ?? DefaultBrand.brandingFavicon} src=${this.brand.brandingFavicon}
alt="${msg("authentik Logo")}" alt="${msg("authentik Logo")}"
/> />
</div> </div>

View File

@ -1,9 +1,13 @@
import "#admin/AdminInterface/AboutModal";
import type { AboutModal } from "#admin/AdminInterface/AboutModal";
import { ROUTES } from "#admin/Routes";
import { EVENT_API_DRAWER_TOGGLE, EVENT_NOTIFICATION_DRAWER_TOGGLE } from "#common/constants"; import { EVENT_API_DRAWER_TOGGLE, EVENT_NOTIFICATION_DRAWER_TOGGLE } from "#common/constants";
import { configureSentry } from "#common/sentry/index"; import { configureSentry } from "#common/sentry/index";
import { me } from "#common/users"; import { me } from "#common/users";
import { WebsocketClient } from "#common/ws"; import { WebsocketClient } from "#common/ws";
import { AuthenticatedInterface } from "#elements/Interface/Interface"; import { SidebarToggleEventDetail } from "#components/ak-page-header";
import { WithLicenseSummary } from "#elements/Interface/licenseSummaryProvider"; import { AuthenticatedInterface } from "#elements/AuthenticatedInterface";
import { WithCapabilitiesConfig } from "#elements/Interface/capabilitiesProvider";
import "#elements/ak-locale-context/ak-locale-context"; import "#elements/ak-locale-context/ak-locale-context";
import "#elements/banner/EnterpriseStatusBanner"; import "#elements/banner/EnterpriseStatusBanner";
import "#elements/banner/EnterpriseStatusBanner"; import "#elements/banner/EnterpriseStatusBanner";
@ -17,10 +21,6 @@ import { getURLParam, updateURLParams } from "#elements/router/RouteMatch";
import "#elements/router/RouterOutlet"; import "#elements/router/RouterOutlet";
import "#elements/sidebar/Sidebar"; import "#elements/sidebar/Sidebar";
import "#elements/sidebar/SidebarItem"; import "#elements/sidebar/SidebarItem";
import "@goauthentik/admin/AdminInterface/AboutModal";
import type { AboutModal } from "@goauthentik/admin/AdminInterface/AboutModal";
import { ROUTES } from "@goauthentik/admin/Routes";
import { SidebarToggleEventDetail } from "@goauthentik/components/ak-page-header.js";
import { CSSResult, TemplateResult, css, html, nothing } from "lit"; import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, eventOptions, property, query } from "lit/decorators.js"; import { customElement, eventOptions, property, query } from "lit/decorators.js";
@ -45,7 +45,7 @@ if (process.env.NODE_ENV === "development") {
} }
@customElement("ak-interface-admin") @customElement("ak-interface-admin")
export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) { export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterface) {
//#region Properties //#region Properties
@property({ type: Boolean }) @property({ type: Boolean })
@ -202,7 +202,7 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
<ak-sidebar class="${classMap(sidebarClasses)}"> <ak-sidebar class="${classMap(sidebarClasses)}">
${renderSidebarItems(AdminSidebarEntries)} ${renderSidebarItems(AdminSidebarEntries)}
${this.config?.capabilities.includes(CapabilitiesEnum.IsEnterprise) ${this.can(CapabilitiesEnum.IsEnterprise)
? renderSidebarItems(AdminSidebarEnterpriseEntries) ? renderSidebarItems(AdminSidebarEnterpriseEntries)
: nothing} : nothing}
</ak-sidebar> </ak-sidebar>

View File

@ -1,3 +1,4 @@
import { CapabilitiesEnum, WithCapabilitiesConfig } from "#elements/Interface/capabilitiesProvider";
import "@goauthentik/admin/applications/ProviderSelectModal"; import "@goauthentik/admin/applications/ProviderSelectModal";
import { iconHelperText } from "@goauthentik/admin/helperText"; import { iconHelperText } from "@goauthentik/admin/helperText";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
@ -6,18 +7,14 @@ import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input"; import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input"; import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input"; import "@goauthentik/components/ak-textarea-input";
import "@goauthentik/elements/Alert.js"; import "@goauthentik/elements/Alert";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/ProxyForm"; import "@goauthentik/elements/forms/ProxyForm";
import "@goauthentik/elements/forms/Radio"; import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect"; import "@goauthentik/elements/forms/SearchSelect/ak-search-select";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";

View File

@ -1,10 +1,10 @@
import "@goauthentik/admin/applications/ApplicationForm"; import "@goauthentik/admin/applications/ApplicationForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import MDApplication from "@goauthentik/docs/add-secure-apps/applications/index.md"; import MDApplication from "@goauthentik/docs/add-secure-apps/applications/index.md";
import "@goauthentik/elements/AppIcon.js"; import "@goauthentik/elements/AppIcon";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import "@goauthentik/elements/ak-mdx"; import "@goauthentik/elements/ak-mdx/ak-mdx";
import "@goauthentik/elements/buttons/SpinnerButton"; import "@goauthentik/elements/buttons/SpinnerButton/ak-spinner-button";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
import { getURLParam } from "@goauthentik/elements/router/RouteMatch"; import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
@ -22,7 +22,7 @@ import PFCard from "@patternfly/patternfly/components/Card/card.css";
import { Application, CoreApi, PoliciesApi } from "@goauthentik/api"; import { Application, CoreApi, PoliciesApi } from "@goauthentik/api";
import "./ApplicationWizardHint"; import "./ApplicationWizardHint.js";
export const applicationListStyle = css` export const applicationListStyle = css`
/* Fix alignment issues with images in tables */ /* Fix alignment issues with images in tables */
@ -50,7 +50,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
} }
pageDescription(): string { pageDescription(): string {
return msg( return msg(
str`External applications that use ${this.brand?.brandingTitle ?? "authentik"} as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.`, str`External applications that use ${this.brandingTitle} as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.`,
); );
} }
pageIcon(): string { pageIcon(): string {

View File

@ -295,7 +295,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
${msg("Set password")} ${msg("Set password")}
</button> </button>
</ak-forms-modal> </ak-forms-modal>
${this.brand?.flowRecovery ${this.brand.flowRecovery
? html` ? html`
<ak-action-button <ak-action-button
class="pf-m-secondary" class="pf-m-secondary"

View File

@ -11,10 +11,10 @@ import { parseAPIResponseError } from "@goauthentik/common/errors/network";
import { userTypeToLabel } from "@goauthentik/common/labels"; import { userTypeToLabel } from "@goauthentik/common/labels";
import { MessageLevel } from "@goauthentik/common/messages"; import { MessageLevel } from "@goauthentik/common/messages";
import { formatElapsedTime } from "@goauthentik/common/temporal"; import { formatElapsedTime } from "@goauthentik/common/temporal";
import { rootInterface } from "@goauthentik/common/theme";
import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config"; import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import "@goauthentik/components/ak-status-label"; import "@goauthentik/components/ak-status-label";
import { rootInterface } from "@goauthentik/elements/Base";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { import {
CapabilitiesEnum, CapabilitiesEnum,

View File

@ -18,7 +18,6 @@ export const CURRENT_CLASS = "pf-m-current";
//#region Application //#region Application
export const TITLE_DEFAULT = "authentik";
/** /**
* The delimiter used to parse the URL for the current route. * The delimiter used to parse the URL for the current route.
* *

View File

@ -26,8 +26,12 @@ export const HTTPStatusCodeTransformer: Record<number, HTTPErrorJSONTransformer>
[HTTPStatusCode.Forbidden]: GenericErrorFromJSON, [HTTPStatusCode.Forbidden]: GenericErrorFromJSON,
} as const; } as const;
//#endregion
//#region Type Predicates
/** /**
* Type guard to check if a response contains a JSON body. * Type predicate to check if a response contains a JSON body.
* *
* This is useful to guard against parsing errors when attempting to read the response body. * This is useful to guard against parsing errors when attempting to read the response body.
*/ */
@ -35,6 +39,24 @@ export function isJSONResponse(response: Response): boolean {
return Boolean(response.headers.get("content-type")?.includes("application/json")); return Boolean(response.headers.get("content-type")?.includes("application/json"));
} }
/**
* An error originating from an aborted request.
*
* @see {@linkcode isAbortError} to check if an error originates from an aborted request.
*/
export interface AbortErrorLike extends DOMException {
name: "AbortError";
}
/**
* Type predicate to check if an error originates from an aborted request.
*
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort | MDN}
*/
export function isAbortError(error: unknown): error is AbortErrorLike {
return error instanceof DOMException && error.name === "AbortError";
}
//#endregion //#endregion
//#region API //#region API

View File

@ -1,14 +1,17 @@
/** /**
* @file Theme utilities. * @file Theme utilities.
*/ */
import { type StyleRoot, createStyleSheetUnsafe, setAdoptedStyleSheets } from "#common/stylesheets"; import {
import { UIConfig } from "#common/ui/config"; type StyleRoot,
createStyleSheetUnsafe,
setAdoptedStyleSheets,
} from "@goauthentik/common/stylesheets";
import AKBase from "#common/styles/authentik.css"; import AKBase from "#common/styles/authentik.css";
import AKBaseDark from "#common/styles/theme-dark.css"; import AKBaseDark from "#common/styles/theme-dark.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api"; import { UiThemeEnum } from "@goauthentik/api";
//#region Stylesheet Exports //#region Stylesheet Exports
@ -259,6 +262,8 @@ export function applyUITheme(
export function applyDocumentTheme(hint: CSSColorSchemeValue | UIThemeHint = "auto"): void { export function applyDocumentTheme(hint: CSSColorSchemeValue | UIThemeHint = "auto"): void {
const preferredColorScheme = formatColorScheme(hint); const preferredColorScheme = formatColorScheme(hint);
if (document.documentElement.dataset.theme === preferredColorScheme) return;
const applyStyleSheets: UIThemeListener = (currentUITheme) => { const applyStyleSheets: UIThemeListener = (currentUITheme) => {
console.debug(`authentik/theme (document): switching to ${currentUITheme} theme`); console.debug(`authentik/theme (document): switching to ${currentUITheme} theme`);
@ -285,36 +290,20 @@ export function applyDocumentTheme(hint: CSSColorSchemeValue | UIThemeHint = "au
applyStyleSheets(preferredColorScheme); applyStyleSheets(preferredColorScheme);
} }
/**
* An element that can be themed.
*/
export interface ThemedElement extends HTMLElement {
/**
* The brand information for the current theme.
*/
readonly brand?: CurrentBrand;
/**
* The UI configuration for the current theme,
* typically injected through a Lit Mixin.
*
* @see {@linkcode UIConfig} for details.
*/
readonly uiConfig?: UIConfig;
/**
* An authentik configuration initially provided by the server.
*/
readonly config?: Config;
activeTheme: ResolvedUITheme;
}
/** /**
* Returns the root interface element of the page. * Returns the root interface element of the page.
* *
* @todo Can this be handled with a Lit Mixin? * @todo Can this be handled with a Lit Mixin?
*/ */
export function rootInterface<T extends ThemedElement = ThemedElement>(): T | null { export function rootInterface<T extends HTMLElement = HTMLElement>(): T {
const element = document.body.querySelector<T>("[data-ak-interface-root]"); const element = document.body.querySelector<T>("[data-ak-interface-root]");
if (!element) {
throw new Error(
`Could not find root interface element. Was this element added before the parent interface element?`,
);
}
return element; return element;
} }

View File

@ -1,7 +1,6 @@
import { EVENT_WS_MESSAGE, TITLE_DEFAULT } from "#common/constants"; import { EVENT_WS_MESSAGE } from "#common/constants";
import { globalAK } from "#common/global"; import { globalAK } from "#common/global";
import { UIConfig, UserDisplay, getConfigForUser } from "#common/ui/config"; import { UIConfig, UserDisplay, getConfigForUser } from "#common/ui/config";
import { DefaultBrand } from "#common/ui/config";
import { me } from "#common/users"; import { me } from "#common/users";
import "#components/ak-nav-buttons"; import "#components/ak-nav-buttons";
import type { PageHeaderInit, SidebarToggleEventDetail } from "#components/ak-page-header"; import type { PageHeaderInit, SidebarToggleEventDetail } from "#components/ak-page-header";
@ -290,7 +289,7 @@ export class AKPageNavbar
//#region Private Methods //#region Private Methods
#setTitle(header?: string) { #setTitle(header?: string) {
let title = this.brand?.brandingTitle || TITLE_DEFAULT; let title = this.brandingTitle;
if (isAdminRoute()) { if (isAdminRoute()) {
title = `${msg("Admin")} - ${title}`; title = `${msg("Admin")} - ${title}`;
@ -368,9 +367,7 @@ export class AKPageNavbar
<a href="#/"> <a href="#/">
<div class="logo"> <div class="logo">
<img <img
src=${themeImage( src=${themeImage(this.brandingLogo)}
this.brand?.brandingLogo ?? DefaultBrand.brandingLogo,
)}
alt="${msg("authentik Logo")}" alt="${msg("authentik Logo")}"
loading="lazy" loading="lazy"
/> />

View File

@ -0,0 +1,12 @@
import { LicenseContextController } from "#elements/Interface/EnterpriseContextController";
import { VersionContextController } from "#elements/Interface/VersionContextController";
import { Interface } from "@goauthentik/elements/Interface";
export class AuthenticatedInterface extends Interface {
constructor() {
super();
this.addController(new LicenseContextController(this));
this.addController(new VersionContextController(this));
}
}

View File

@ -1,17 +0,0 @@
import { createContext } from "@lit/context";
import type { Config, CurrentBrand, LicenseSummary, SessionUser, Version } from "@goauthentik/api";
export const authentikConfigContext = createContext<Config>(Symbol("authentik-config-context"));
export const authentikUserContext = createContext<SessionUser>(Symbol("authentik-user-context"));
export const authentikEnterpriseContext = createContext<LicenseSummary>(
Symbol("authentik-enterprise-context"),
);
export const authentikBrandContext = createContext<CurrentBrand>(Symbol("authentik-brand-context"));
export const authentikVersionContext = createContext<Version>(Symbol("authentik-version-context"));
export default authentikConfigContext;

View File

@ -1,19 +1,14 @@
import { globalAK } from "@goauthentik/common/global.js"; import { globalAK } from "#common/global";
import { import { StyleRoot, createCSSResult, createStyleSheetUnsafe } from "#common/stylesheets";
StyleRoot,
createCSSResult,
createStyleSheetUnsafe,
} from "@goauthentik/common/stylesheets.js";
import { import {
$AKBase, $AKBase,
CSSColorSchemeValue, CSSColorSchemeValue,
ResolvedUITheme, ResolvedUITheme,
ThemedElement,
applyUITheme, applyUITheme,
createUIThemeEffect, createUIThemeEffect,
formatColorScheme, formatColorScheme,
resolveUITheme, resolveUITheme,
} from "@goauthentik/common/theme.js"; } from "#common/theme";
import { localized } from "@lit/localize"; import { localized } from "@lit/localize";
import { CSSResult, CSSResultGroup, CSSResultOrNative, LitElement } from "lit"; import { CSSResult, CSSResultGroup, CSSResultOrNative, LitElement } from "lit";
@ -21,11 +16,8 @@ import { property } from "lit/decorators.js";
import { UiThemeEnum } from "@goauthentik/api"; import { UiThemeEnum } from "@goauthentik/api";
// Re-export the theme helpers
export { rootInterface } from "@goauthentik/common/theme";
@localized() @localized()
export class AKElement extends LitElement implements ThemedElement { export class AKElement extends LitElement {
//#region Static Properties //#region Static Properties
public static styles?: Array<CSSResult | CSSModule>; public static styles?: Array<CSSResult | CSSModule>;

View File

@ -0,0 +1,33 @@
import { BrandingContextController } from "#elements/Interface/BrandContextController";
import { ConfigContextController } from "#elements/Interface/ConfigContextController";
import { WithAuthentikConfig } from "#elements/Interface/authentikConfigProvider";
import { globalAK } from "@goauthentik/common/global";
import { applyDocumentTheme } from "@goauthentik/common/theme";
import { AKElement } from "@goauthentik/elements/Base";
import { ModalOrchestrationController } from "@goauthentik/elements/controllers/ModalOrchestrationController";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
/**
* The base interface element for the application.
*/
export abstract class Interface extends WithAuthentikConfig(AKElement) {
static styles = [PFBase];
constructor() {
super();
const { config, brand } = globalAK();
applyDocumentTheme(brand.uiTheme);
this.addController(new ConfigContextController(this, config));
this.addController(new BrandingContextController(this, brand));
this.addController(new ModalOrchestrationController());
}
public connectedCallback(): void {
super.connectedCallback();
this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
}
}

View File

@ -1,50 +1,74 @@
import { BrandingContext, BrandingMixin } from "#elements/Interface/brandProvider";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { ThemedElement } from "@goauthentik/common/theme"; import { isAbortError } from "@goauthentik/common/errors/network";
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts"; import type { ReactiveElementHost } from "@goauthentik/elements/types";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
import { ContextProvider } from "@lit/context"; import { Context, ContextProvider } from "@lit/context";
import type { ReactiveController } from "lit"; import type { ReactiveController } from "lit";
import type { CurrentBrand } from "@goauthentik/api"; import type { CurrentBrand } from "@goauthentik/api";
import { CoreApi } from "@goauthentik/api"; import { CoreApi } from "@goauthentik/api";
export class BrandContextController implements ReactiveController { export class BrandingContextController implements ReactiveController {
host!: ReactiveElementHost<ThemedElement>; #log = console.debug.bind(console, `authentik/controller/branding`);
#abortController: null | AbortController = null;
context!: ContextProvider<{ __context__: CurrentBrand | undefined }>; #host: ReactiveElementHost<BrandingMixin>;
#context: ContextProvider<Context<unknown, CurrentBrand>>;
constructor(host: ReactiveElementHost<ThemedElement>) { constructor(host: ReactiveElementHost<BrandingMixin>, initialValue: CurrentBrand) {
this.host = host; this.#host = host;
this.context = new ContextProvider(this.host, { this.#context = new ContextProvider(this.#host, {
context: authentikBrandContext, context: BrandingContext,
initialValue: undefined, initialValue,
});
this.fetch = this.fetch.bind(this);
this.fetch();
}
fetch() {
new CoreApi(DEFAULT_CONFIG).coreBrandsCurrentRetrieve().then((brand) => {
this.context.setValue(brand);
this.host.brand = brand;
}); });
} }
hostConnected() { #fetch = () => {
window.addEventListener(EVENT_REFRESH, this.fetch); this.#log("Fetching configuration...");
this.#abortController?.abort();
this.#abortController = new AbortController();
return new CoreApi(DEFAULT_CONFIG)
.coreBrandsCurrentRetrieve({
signal: this.#abortController.signal,
})
.then((brand) => {
this.#context.setValue(brand);
this.#host.brand = brand;
})
.catch((error: unknown) => {
if (isAbortError(error)) {
this.#log("Aborted fetching brand");
return;
} }
hostDisconnected() { throw error;
window.removeEventListener(EVENT_REFRESH, this.fetch); })
.finally(() => {
this.#abortController = null;
});
};
public hostConnected() {
window.addEventListener(EVENT_REFRESH, this.#fetch);
this.#fetch();
} }
hostUpdate() { public hostDisconnected() {
window.removeEventListener(EVENT_REFRESH, this.#fetch);
this.#abortController?.abort();
}
public hostUpdate() {
// If the Interface changes its brand information for some reason, // If the Interface changes its brand information for some reason,
// we should notify all users of the context of that change. doesn't // we should notify all users of the context of that change. doesn't
if (this.host.brand !== this.context.value) { if (this.#host.brand && this.#host.brand !== this.#context.value) {
this.context.setValue(this.host.brand); this.#context.setValue(this.#host.brand);
} }
} }
} }

View File

@ -1,55 +1,79 @@
import { AKConfigMixin, AuthentikConfigContext } from "#elements/Interface/authentikConfigProvider";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { isAbortError } from "@goauthentik/common/errors/network";
import { ThemedElement } from "@goauthentik/common/theme"; import type { ReactiveElementHost } from "@goauthentik/elements/types";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
import { ContextProvider } from "@lit/context"; import { Context, ContextProvider } from "@lit/context";
import type { ReactiveController } from "lit"; import type { ReactiveController } from "lit";
import type { Config } from "@goauthentik/api"; import { Config, RootApi } from "@goauthentik/api";
import { RootApi } from "@goauthentik/api";
/**
* A controller that provides the application configuration to the element.
*/
export class ConfigContextController implements ReactiveController { export class ConfigContextController implements ReactiveController {
host!: ReactiveElementHost<ThemedElement>; #log = console.debug.bind(console, `authentik/controller/config`);
#abortController: null | AbortController = null;
context!: ContextProvider<{ __context__: Config | undefined }>; #host: ReactiveElementHost<AKConfigMixin>;
#context: ContextProvider<Context<unknown, Config>>;
constructor(host: ReactiveElementHost<ThemedElement>) { constructor(host: ReactiveElementHost<AKConfigMixin>, initialValue: Config) {
this.host = host; this.#host = host;
this.context = new ContextProvider(this.host, {
context: authentikConfigContext, this.#context = new ContextProvider(this.#host, {
initialValue: undefined, context: AuthentikConfigContext,
initialValue,
}); });
// Pre-hydrate from template-embedded config
this.context.setValue(globalAK().config); this.#host.authentikConfig = initialValue;
this.host.config = globalAK().config;
this.fetch = this.fetch.bind(this);
this.fetch();
} }
fetch() { #fetch = () => {
new RootApi(DEFAULT_CONFIG).rootConfigRetrieve().then((config) => { this.#log("Fetching configuration...");
this.context.setValue(config);
this.host.config = config; this.#abortController?.abort();
this.#abortController = new AbortController();
return new RootApi(DEFAULT_CONFIG)
.rootConfigRetrieve({
signal: this.#abortController.signal,
})
.then((authentikConfig) => {
this.#context.setValue(authentikConfig);
this.#host.authentikConfig = authentikConfig;
})
.catch((error: unknown) => {
if (isAbortError(error)) {
this.#log("Aborted fetching configuration");
return;
}
throw error;
})
.finally(() => {
this.#abortController = null;
}); });
};
public hostConnected() {
window.addEventListener(EVENT_REFRESH, this.#fetch);
this.#fetch();
} }
hostConnected() { public hostDisconnected() {
window.addEventListener(EVENT_REFRESH, this.fetch); window.removeEventListener(EVENT_REFRESH, this.#fetch);
this.#abortController?.abort();
} }
hostDisconnected() { public hostUpdate() {
window.removeEventListener(EVENT_REFRESH, this.fetch);
}
hostUpdate() {
// If the Interface changes its config information, we should notify all // If the Interface changes its config information, we should notify all
// users of the context of that change, without creating an infinite // users of the context of that change, without creating an infinite
// loop of resets. // loop of resets.
if (this.host.config !== this.context.value) { if (this.#host.authentikConfig && this.#host.authentikConfig !== this.#context.value) {
this.context.setValue(this.host.config); this.#context.setValue(this.#host.authentikConfig);
} }
} }
} }

View File

@ -1,52 +1,77 @@
import { LicenseContext, LicenseMixin } from "#elements/Interface/licenseSummaryProvider";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/common/constants"; import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/common/constants";
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts"; import { isAbortError } from "@goauthentik/common/errors/network";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js"; import type { ReactiveElementHost } from "@goauthentik/elements/types";
import { ContextProvider } from "@lit/context"; import { Context, ContextProvider } from "@lit/context";
import type { ReactiveController } from "lit"; import type { ReactiveController } from "lit";
import type { LicenseSummary } from "@goauthentik/api"; import { EnterpriseApi, LicenseSummary } from "@goauthentik/api";
import { EnterpriseApi } from "@goauthentik/api";
import type { AkAuthenticatedInterface } from "./Interface"; export class LicenseContextController implements ReactiveController {
#log = console.debug.bind(console, `authentik/controller/license`);
#abortController: null | AbortController = null;
export class EnterpriseContextController implements ReactiveController { #host: ReactiveElementHost<LicenseMixin>;
host!: ReactiveElementHost<AkAuthenticatedInterface>; #context: ContextProvider<Context<unknown, LicenseSummary>>;
context!: ContextProvider<{ __context__: LicenseSummary | undefined }>; constructor(host: ReactiveElementHost<LicenseMixin>, initialValue?: LicenseSummary) {
this.#host = host;
constructor(host: ReactiveElementHost<AkAuthenticatedInterface>) { this.#context = new ContextProvider(this.#host, {
this.host = host; context: LicenseContext,
this.context = new ContextProvider(this.host, { initialValue: initialValue,
context: authentikEnterpriseContext,
initialValue: undefined,
});
this.fetch = this.fetch.bind(this);
this.fetch();
}
fetch() {
new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve().then((enterprise) => {
this.context.setValue(enterprise);
this.host.licenseSummary = enterprise;
}); });
} }
hostConnected() { #fetch = () => {
window.addEventListener(EVENT_REFRESH_ENTERPRISE, this.fetch); this.#log("Fetching license summary...");
this.#abortController?.abort();
this.#abortController = new AbortController();
return new EnterpriseApi(DEFAULT_CONFIG)
.enterpriseLicenseSummaryRetrieve(
{},
{
signal: this.#abortController.signal,
},
)
.then((enterprise) => {
this.#context.setValue(enterprise);
this.#host.licenseSummary = enterprise;
})
.catch((error: unknown) => {
if (isAbortError(error)) {
this.#log("Aborted fetching license summary");
return;
} }
hostDisconnected() { throw error;
window.removeEventListener(EVENT_REFRESH_ENTERPRISE, this.fetch); })
.finally(() => {
this.#abortController = null;
});
};
public hostConnected() {
window.addEventListener(EVENT_REFRESH_ENTERPRISE, this.#fetch);
this.#fetch();
} }
hostUpdate() { public hostDisconnected() {
window.removeEventListener(EVENT_REFRESH_ENTERPRISE, this.#fetch);
this.#abortController?.abort();
}
public hostUpdate() {
// If the Interface changes its config information, we should notify all // If the Interface changes its config information, we should notify all
// users of the context of that change, without creating an infinite // users of the context of that change, without creating an infinite
// loop of resets. // loop of resets.
if (this.host.licenseSummary !== this.context.value) { if (this.#host.licenseSummary && this.#host.licenseSummary !== this.#context.value) {
this.context.setValue(this.host.licenseSummary); this.#context.setValue(this.#host.licenseSummary);
} }
} }
} }

View File

@ -1,85 +0,0 @@
import { globalAK } from "@goauthentik/common/global.js";
import { ThemedElement, applyDocumentTheme } from "@goauthentik/common/theme.js";
import { UIConfig } from "@goauthentik/common/ui/config.js";
import { AKElement } from "@goauthentik/elements/Base.js";
import { VersionContextController } from "@goauthentik/elements/Interface/VersionContextController.js";
import { ModalOrchestrationController } from "@goauthentik/elements/controllers/ModalOrchestrationController.js";
import { state } from "lit/decorators.js";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import {
type Config,
type CurrentBrand,
type LicenseSummary,
type Version,
} from "@goauthentik/api";
import { BrandContextController } from "./BrandContextController.js";
import { ConfigContextController } from "./ConfigContextController.js";
import { EnterpriseContextController } from "./EnterpriseContextController.js";
const configContext = Symbol("configContext");
const modalController = Symbol("modalController");
const versionContext = Symbol("versionContext");
export abstract class LightInterface extends AKElement implements ThemedElement {
constructor() {
super();
this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
if (!document.documentElement.dataset.theme) {
applyDocumentTheme(globalAK().brand.uiTheme);
}
}
}
export abstract class Interface extends LightInterface implements ThemedElement {
static styles = [PFBase];
protected [configContext]: ConfigContextController;
protected [modalController]: ModalOrchestrationController;
@state()
public config?: Config;
@state()
public brand?: CurrentBrand;
constructor() {
super();
this.addController(new BrandContextController(this));
this[configContext] = new ConfigContextController(this);
this[modalController] = new ModalOrchestrationController(this);
}
}
export interface AkAuthenticatedInterface extends ThemedElement {
licenseSummary?: LicenseSummary;
version?: Version;
}
const enterpriseContext = Symbol("enterpriseContext");
export class AuthenticatedInterface extends Interface implements AkAuthenticatedInterface {
[enterpriseContext]!: EnterpriseContextController;
[versionContext]!: VersionContextController;
@state()
public uiConfig?: UIConfig;
@state()
public licenseSummary?: LicenseSummary;
@state()
public version?: Version;
constructor() {
super();
this[enterpriseContext] = new EnterpriseContextController(this);
this[versionContext] = new VersionContextController(this);
}
}

View File

@ -1,51 +1,74 @@
import { VersionContext, VersionMixin } from "#elements/Interface/versionProvider";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { authentikVersionContext } from "@goauthentik/elements/AuthentikContexts"; import { isAbortError } from "@goauthentik/common/errors/network";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js"; import type { ReactiveElementHost } from "@goauthentik/elements/types";
import { ContextProvider } from "@lit/context"; import { Context, ContextProvider } from "@lit/context";
import type { ReactiveController } from "lit"; import type { ReactiveController } from "lit";
import type { Version } from "@goauthentik/api"; import type { Version } from "@goauthentik/api";
import { AdminApi } from "@goauthentik/api"; import { AdminApi } from "@goauthentik/api";
import type { AkAuthenticatedInterface } from "./Interface";
export class VersionContextController implements ReactiveController { export class VersionContextController implements ReactiveController {
host!: ReactiveElementHost<AkAuthenticatedInterface>; #log = console.debug.bind(console, `authentik/controller/version`);
#abortController: null | AbortController = null;
context!: ContextProvider<{ __context__: Version | undefined }>; #host: ReactiveElementHost<VersionMixin>;
#context: ContextProvider<Context<unknown, Version>>;
constructor(host: ReactiveElementHost<AkAuthenticatedInterface>) { constructor(host: ReactiveElementHost<VersionMixin>, initialValue?: Version) {
this.host = host; this.#host = host;
this.context = new ContextProvider(this.host, { this.#context = new ContextProvider(this.#host, {
context: authentikVersionContext, context: VersionContext,
initialValue: undefined, initialValue,
});
this.fetch = this.fetch.bind(this);
this.fetch();
}
fetch() {
new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve().then((version) => {
this.context.setValue(version);
this.host.version = version;
}); });
} }
hostConnected() { #fetch = () => {
window.addEventListener(EVENT_REFRESH, this.fetch); this.#log("Fetching latest version...");
this.#abortController?.abort();
this.#abortController = new AbortController();
return new AdminApi(DEFAULT_CONFIG)
.adminVersionRetrieve({
signal: this.#abortController.signal,
})
.then((version) => {
this.#context.setValue(version);
this.#host.version = version;
})
.catch((error: unknown) => {
if (isAbortError(error)) {
this.#log("Aborted fetching license summary");
return;
} }
hostDisconnected() { throw error;
window.removeEventListener(EVENT_REFRESH, this.fetch); })
.finally(() => {
this.#abortController = null;
});
};
public hostConnected() {
window.addEventListener(EVENT_REFRESH, this.#fetch);
this.#fetch();
} }
hostUpdate() { public hostDisconnected() {
window.removeEventListener(EVENT_REFRESH, this.#fetch);
this.#abortController?.abort();
}
public hostUpdate() {
// If the Interface changes its version information for some reason, // If the Interface changes its version information for some reason,
// we should notify all users of the context of that change. doesn't // we should notify all users of the context of that change. doesn't
if (this.host.version !== this.context.value) { if (this.#host.version && this.#host.version !== this.#context.value) {
this.context.setValue(this.host.version); this.#context.setValue(this.#host.version);
} }
} }
} }

View File

@ -1,12 +1,23 @@
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import { createMixin } from "@goauthentik/elements/types"; import { createMixin } from "@goauthentik/elements/types";
import { consume } from "@lit/context"; import { consume, createContext } from "@lit/context";
import type { Config } from "@goauthentik/api"; import type { Config } from "@goauthentik/api";
/**
* The Lit context for the application configuration.
*
* @category Context
* @see {@linkcode AKConfigMixin}
* @see {@linkcode WithAuthentikConfig}
*/
export const AuthentikConfigContext = createContext<Config>(Symbol("authentik-config-context"));
/** /**
* A consumer that provides the application configuration to the element. * A consumer that provides the application configuration to the element.
*
* @category Mixin
* @see {@linkcode WithAuthentikConfig}
*/ */
export interface AKConfigMixin { export interface AKConfigMixin {
/** /**
@ -33,7 +44,7 @@ export const WithAuthentikConfig = createMixin<AKConfigMixin>(
}) => { }) => {
abstract class AKConfigProvider extends SuperClass implements AKConfigMixin { abstract class AKConfigProvider extends SuperClass implements AKConfigMixin {
@consume({ @consume({
context: authentikConfigContext, context: AuthentikConfigContext,
subscribe, subscribe,
}) })
public readonly authentikConfig!: Readonly<Config>; public readonly authentikConfig!: Readonly<Config>;

View File

@ -1,19 +1,34 @@
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts"; import { DefaultBrand } from "@goauthentik/common/ui/config";
import { createMixin } from "@goauthentik/elements/types"; import { createMixin } from "@goauthentik/elements/types";
import { consume } from "@lit/context"; import { consume, createContext } from "@lit/context";
import { state } from "lit/decorators.js"; import { state } from "lit/decorators.js";
import type { CurrentBrand } from "@goauthentik/api"; import type { CurrentBrand } from "@goauthentik/api";
/** /**
* A mixin that provides the current brand to the element. * The Lit context for application branding.
*
* @category Context
* @see {@linkcode BrandingMixin}
* @see {@linkcode WithBrandConfig}
*/ */
export interface StyleBrandMixin { export const BrandingContext = createContext<CurrentBrand>(Symbol("authentik-branding-context"));
/**
* A mixin that provides the current brand to the element.
*
* @see {@linkcode WithBrandConfig}
*/
export interface BrandingMixin {
/** /**
* The current style branding configuration. * The current style branding configuration.
*/ */
brand: CurrentBrand; readonly brand: Readonly<CurrentBrand>;
readonly brandingTitle: string;
readonly brandingLogo: string;
readonly brandingFavicon: string;
} }
/** /**
@ -23,7 +38,7 @@ export interface StyleBrandMixin {
* *
* @see {@link https://lit.dev/docs/composition/mixins/#mixins-in-typescript | Lit Mixins} * @see {@link https://lit.dev/docs/composition/mixins/#mixins-in-typescript | Lit Mixins}
*/ */
export const WithBrandConfig = createMixin<StyleBrandMixin>( export const WithBrandConfig = createMixin<BrandingMixin>(
({ ({
/** /**
* The superclass constructor to extend. * The superclass constructor to extend.
@ -34,15 +49,27 @@ export const WithBrandConfig = createMixin<StyleBrandMixin>(
*/ */
subscribe = true, subscribe = true,
}) => { }) => {
abstract class StyleBrandProvider extends SuperClass implements StyleBrandMixin { abstract class BrandingProvider extends SuperClass implements BrandingMixin {
@consume({ @consume({
context: authentikBrandContext, context: BrandingContext,
subscribe, subscribe,
}) })
@state() @state()
public brand!: CurrentBrand; public brand!: CurrentBrand;
public get brandingTitle(): string {
return this.brand.brandingTitle || DefaultBrand.brandingTitle;
} }
return StyleBrandProvider; public get brandingLogo(): string {
return this.brand.brandingLogo || DefaultBrand.brandingLogo;
}
public get brandingFavicon(): string {
return this.brand.brandingFavicon || DefaultBrand.brandingFavicon;
}
}
return BrandingProvider;
}, },
); );

View File

@ -1,10 +1,7 @@
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts"; import { AKConfigMixin } from "#elements/Interface/authentikConfigProvider";
import { createMixin } from "@goauthentik/elements/types"; import { createMixin } from "@goauthentik/elements/types";
import { consume } from "@lit/context";
import { CapabilitiesEnum } from "@goauthentik/api"; import { CapabilitiesEnum } from "@goauthentik/api";
import { Config } from "@goauthentik/api";
/** /**
* A consumer that provides the capability methods to the element. * A consumer that provides the capability methods to the element.
@ -22,13 +19,6 @@ export interface CapabilitiesMixin {
): boolean; ): boolean;
} }
/**
* Lexically-scoped symbol for the capabilities configuration.
*
* @internal
*/
const kCapabilitiesConfig: unique symbol = Symbol("capabilitiesConfig");
/** /**
* A mixin that provides the capability methods to the element. * A mixin that provides the capability methods to the element.
* *
@ -37,14 +27,14 @@ const kCapabilitiesConfig: unique symbol = Symbol("capabilitiesConfig");
* After importing, simply mixin this function: * After importing, simply mixin this function:
* *
* ```ts * ```ts
* export class AkMyNiftyNewFeature extends withCapabilitiesContext(AKElement) { * export class AkMyNiftyNewFeature extends WithCapabilitiesConfig(AKElement) {
* } * }
* ``` * ```
* *
* And then if you need to check on a capability: * And then if you need to check on a capability:
* *
* ```ts * ```ts
* if (this.can(CapabilitiesEnum.IsEnterprise) { ... } * if (this.can(CapabilitiesEnum.IsEnterprise)) { ... }
* ``` * ```
* *
* *
@ -53,21 +43,15 @@ const kCapabilitiesConfig: unique symbol = Symbol("capabilitiesConfig");
* @category Mixin * @category Mixin
* *
*/ */
export const WithCapabilitiesConfig = createMixin<CapabilitiesMixin>( export const WithCapabilitiesConfig = createMixin<CapabilitiesMixin, AKConfigMixin>(
({ SuperClass, subscribe = true }) => { ({ SuperClass }) => {
abstract class CapabilitiesProvider extends SuperClass implements CapabilitiesMixin { abstract class CapabilitiesProvider extends SuperClass implements CapabilitiesMixin {
@consume({
context: authentikConfigContext,
subscribe,
})
private readonly [kCapabilitiesConfig]: Config | undefined;
public can(capability: CapabilitiesEnum) { public can(capability: CapabilitiesEnum) {
const config = this[kCapabilitiesConfig]; const config = this.authentikConfig;
if (!config) { if (!config) {
throw new Error( throw new Error(
"ConfigContext: Attempted to access site configuration before initialization.", `ConfigContext: Attempted to check capability "${capability}" before initialization. Does the element have the AuthentikConfigMixin applied?`,
); );
} }

View File

@ -1,4 +0,0 @@
import { AuthenticatedInterface, Interface, LightInterface } from "./Interface";
export { Interface, AuthenticatedInterface, LightInterface };
export default Interface;

View File

@ -1,10 +1,11 @@
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts";
import { createMixin } from "@goauthentik/elements/types"; import { createMixin } from "@goauthentik/elements/types";
import { consume } from "@lit/context"; import { consume, createContext } from "@lit/context";
import { type LicenseSummary, LicenseSummaryStatusEnum } from "@goauthentik/api"; import { type LicenseSummary, LicenseSummaryStatusEnum } from "@goauthentik/api";
export const LicenseContext = createContext<LicenseSummary>(Symbol("authentik-license-context"));
/** /**
* A consumer that provides license information to the element. * A consumer that provides license information to the element.
*/ */
@ -26,7 +27,7 @@ export interface LicenseMixin {
export const WithLicenseSummary = createMixin<LicenseMixin>(({ SuperClass, subscribe = true }) => { export const WithLicenseSummary = createMixin<LicenseMixin>(({ SuperClass, subscribe = true }) => {
abstract class LicenseProvider extends SuperClass implements LicenseMixin { abstract class LicenseProvider extends SuperClass implements LicenseMixin {
@consume({ @consume({
context: authentikEnterpriseContext, context: LicenseContext,
subscribe, subscribe,
}) })
public readonly licenseSummary!: LicenseSummary; public readonly licenseSummary!: LicenseSummary;

View File

@ -1,35 +1,61 @@
import { authentikVersionContext } from "@goauthentik/elements/AuthentikContexts"; import { createMixin } from "@goauthentik/elements/types";
import { consume } from "@lit/context"; import { consume, createContext } from "@lit/context";
import { Constructor } from "@lit/reactive-element/decorators/base.js"; import { state } from "lit/decorators.js";
import type { LitElement } from "lit";
import type { Version } from "@goauthentik/api"; import type { Version } from "@goauthentik/api";
/** /**
* A consumer that provides version information to the element. * The Lit context for application branding.
*
* @category Context
* @see {@linkcode VersionMixin}
* @see {@linkcode WithVersion}
*/ */
export declare class VersionConsumer {
export const VersionContext = createContext<Version>(Symbol("authentik-version-context"));
/**
* A mixin that provides the current version to the element.
*
* @see {@linkcode WithVersion}
*/
export interface VersionMixin {
/** /**
* The current version of the application. * The current version of the application.
*
* @format semver
*/ */
public readonly version: Version; readonly version: Version;
} }
export function WithVersion<T extends Constructor<LitElement>>( /**
* A mixin that provides the current authentik version to the element.
*
* @category Mixin
*
* @see {@link https://lit.dev/docs/composition/mixins/#mixins-in-typescript | Lit Mixins}
*/
export const WithVersion = createMixin<VersionMixin>(
({
/** /**
* The superclass constructor to extend. * The superclass constructor to extend.
*/ */
SuperClass: T, SuperClass,
/** /**
* Whether or not to subscribe to the context. * Whether or not to subscribe to the context.
*/ */
subscribe = true, subscribe = true,
) { }) => {
class VersionProvider extends SuperClass { abstract class VersionProvider extends SuperClass implements VersionMixin {
@consume({ context: authentikVersionContext, subscribe }) @consume({
context: VersionContext,
subscribe,
})
@state()
public version!: Version; public version!: Version;
} }
return VersionProvider as Constructor<VersionConsumer> & T; return VersionProvider;
} },
);

View File

@ -70,7 +70,7 @@ export class LocaleContext extends WithBrandConfig(AKElement) {
} }
updateLocale(requestedLocale: string | undefined = undefined) { updateLocale(requestedLocale: string | undefined = undefined) {
const localeRequest = autoDetectLanguage(requestedLocale, this.brand?.defaultLocale); const localeRequest = autoDetectLanguage(requestedLocale, this.brand.defaultLocale);
const locale = getBestMatchLocale(localeRequest); const locale = getBestMatchLocale(localeRequest);
if (!locale) { if (!locale) {
console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`); console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`);

View File

@ -1,10 +1,8 @@
import { bound } from "@goauthentik/elements/decorators/bound.js"; import { LitElement, ReactiveController } from "lit";
import { LitElement, ReactiveController, ReactiveControllerHost } from "lit"; interface ModalElement extends LitElement {
closeModal(): void | boolean;
type ReactiveElementHost = Partial<ReactiveControllerHost> & LitElement; }
type ModalElement = LitElement & { closeModal(): void | boolean };
export class ModalShowEvent extends Event { export class ModalShowEvent extends Event {
modal: ModalElement; modal: ModalElement;
@ -50,75 +48,70 @@ const modalIsLive = (modal: ModalElement) => modal.isConnected && modal.checkVis
*/ */
export class ModalOrchestrationController implements ReactiveController { export class ModalOrchestrationController implements ReactiveController {
host!: ReactiveElementHost; #knownModals: ModalElement[] = [];
knownModals: ModalElement[] = []; public hostConnected() {
constructor(host: ReactiveElementHost) {
this.host = host;
host.addController(this);
}
hostConnected() {
window.addEventListener("keyup", this.handleKeyup); window.addEventListener("keyup", this.handleKeyup);
window.addEventListener("ak-modal-show", this.addModal); window.addEventListener("ak-modal-show", this.#addModal);
window.addEventListener("ak-modal-hide", this.closeModal); window.addEventListener("ak-modal-hide", this.closeModal);
} }
hostDisconnected() { public hostDisconnected() {
window.removeEventListener("keyup", this.handleKeyup); window.removeEventListener("keyup", this.handleKeyup);
window.removeEventListener("ak-modal-show", this.addModal); window.removeEventListener("ak-modal-show", this.#addModal);
window.removeEventListener("ak-modal-hide", this.closeModal); window.removeEventListener("ak-modal-hide", this.closeModal);
} }
@bound #addModal = (e: ModalShowEvent) => {
addModal(e: ModalShowEvent) { this.#knownModals = [...this.#knownModals, e.modal];
this.knownModals = [...this.knownModals, e.modal]; };
}
scheduleCleanup(modal: ModalElement) { #cleanupFrameID = -1;
setTimeout(() => {
this.knownModals = this.knownModals.filter((m) => modalIsLive(m) && modal !== m);
}, 0);
}
@bound #scheduleCleanup = (modal: ModalElement) => {
closeModal(e: ModalHideEvent) { cancelAnimationFrame(this.#cleanupFrameID);
this.#cleanupFrameID = requestAnimationFrame(() => {
this.#knownModals = this.#knownModals.filter((m) => modalIsLive(m) && modal !== m);
});
};
closeModal = (e: ModalHideEvent) => {
const modal = e.modal; const modal = e.modal;
if (!modalIsLive(modal)) {
return;
}
if (modal.closeModal() !== false) {
this.scheduleCleanup(modal);
}
}
removeTopmostModal() { if (!modalIsLive(modal)) return;
const knownModals = [...this.knownModals];
if (modal.closeModal() !== false) {
this.#scheduleCleanup(modal);
}
};
#removeTopmostModal = () => {
const knownModals = [...this.#knownModals];
// Pop off modals until you find the first live one, schedule it to be closed, and make that // Pop off modals until you find the first live one, schedule it to be closed, and make that
// cleaned list the current state. Since this is our *only* state object, this has the // cleaned list the current state. Since this is our *only* state object, this has the
// effect of creating a new "knownModals" collection with some semantics. // effect of creating a new "knownModals" collection with some semantics.
while (true) { while (true) {
const modal = knownModals.pop(); const modal = knownModals.pop();
if (!modal) {
break; if (!modal) break;
}
if (!modalIsLive(modal)) { if (!modalIsLive(modal)) continue;
continue;
}
if (modal.closeModal() !== false) { if (modal.closeModal() !== false) {
this.scheduleCleanup(modal); this.#scheduleCleanup(modal);
}
break;
}
this.knownModals = knownModals;
} }
@bound break;
handleKeyup(e: KeyboardEvent) { }
this.#knownModals = knownModals;
};
handleKeyup = ({ key }: KeyboardEvent) => {
// The latter handles Firefox 37 and earlier. // The latter handles Firefox 37 and earlier.
if (e.key === "Escape" || e.key === "Esc") { if (key === "Escape" || key === "Esc") {
this.removeTopmostModal(); this.#removeTopmostModal();
}
} }
};
} }

View File

@ -1,7 +1,8 @@
import type { AdminInterface } from "@goauthentik/admin/AdminInterface/index.entrypoint.js"; import type { AdminInterface } from "@goauthentik/admin/AdminInterface/index.entrypoint";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { rootInterface } from "@goauthentik/common/theme";
import { DefaultBrand } from "@goauthentik/common/ui/config"; import { DefaultBrand } from "@goauthentik/common/ui/config";
import { AKElement, rootInterface } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider"; import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import { WithVersion } from "@goauthentik/elements/Interface/versionProvider"; import { WithVersion } from "@goauthentik/elements/Interface/versionProvider";

View File

@ -15,10 +15,13 @@ export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
*/ */
export type ReactiveElementHost<T> = Partial<ReactiveControllerHost & Writeable<T>> & HTMLElement; export type ReactiveElementHost<T> = Partial<ReactiveControllerHost & Writeable<T>> & HTMLElement;
export type AbstractLitElementConstructor = abstract new (...args: never[]) => LitElement; export type AbstractLitElementConstructor<T = unknown> = abstract new (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...args: any[]
) => LitElement & T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export type LitElementConstructor = new (...args: any[]) => LitElement; export type LitElementConstructor<T = unknown> = new (...args: any[]) => LitElement & T;
/** /**
* A constructor that has been extended with a mixin. * A constructor that has been extended with a mixin.
@ -37,11 +40,11 @@ export type ConstructorWithMixin<SuperClass, Mixin> =
/** /**
* The init object passed to the `createMixin` callback. * The init object passed to the `createMixin` callback.
*/ */
export interface CreateMixinInit<T extends LitElementConstructor = LitElementConstructor> { export interface CreateMixinInit<C = unknown> {
/** /**
* The superclass constructor to extend. * The superclass constructor to extend.
*/ */
SuperClass: T; SuperClass: LitElementConstructor<C>;
/** /**
* Whether or not to subscribe to the context. * Whether or not to subscribe to the context.
* *
@ -58,7 +61,9 @@ export interface CreateMixinInit<T extends LitElementConstructor = LitElementCon
* @param mixinCallback The callback that will be called to create the mixin. * @param mixinCallback The callback that will be called to create the mixin.
* @template Mixin The mixin class to union with the superclass. * @template Mixin The mixin class to union with the superclass.
*/ */
export function createMixin<Mixin>(mixinCallback: (init: CreateMixinInit) => unknown) { export function createMixin<Mixin, C = unknown>(
mixinCallback: (init: CreateMixinInit<C>) => unknown,
) {
return <T extends LitElementConstructor | AbstractLitElementConstructor>( return <T extends LitElementConstructor | AbstractLitElementConstructor>(
/** /**
* The superclass constructor to extend. * The superclass constructor to extend.
@ -73,7 +78,7 @@ export function createMixin<Mixin>(mixinCallback: (init: CreateMixinInit) => unk
subscribe?: boolean, subscribe?: boolean,
) => { ) => {
const MixinClass = mixinCallback({ const MixinClass = mixinCallback({
SuperClass: SuperClass as LitElementConstructor, SuperClass: SuperClass as LitElementConstructor<C>,
subscribe, subscribe,
}); });

View File

@ -1,8 +1,8 @@
import { resolveUITheme } from "@goauthentik/common/theme"; import { resolveUITheme, rootInterface } from "#common/theme";
import { rootInterface } from "@goauthentik/elements/Base"; import type { AKElement } from "#elements/Base";
export function themeImage(rawPath: string) { export function themeImage(rawPath: string) {
const enabledTheme = rootInterface()?.activeTheme || resolveUITheme(); const enabledTheme = rootInterface<AKElement>()?.activeTheme || resolveUITheme();
return rawPath.replaceAll("%(theme)s", enabledTheme); return rawPath.replaceAll("%(theme)s", enabledTheme);
} }

View File

@ -1,16 +1,14 @@
import { WithCapabilitiesConfig } from "#elements/Interface/capabilitiesProvider";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { import { EVENT_FLOW_ADVANCE, EVENT_FLOW_INSPECTOR_TOGGLE } from "@goauthentik/common/constants";
EVENT_FLOW_ADVANCE,
EVENT_FLOW_INSPECTOR_TOGGLE,
TITLE_DEFAULT,
} from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { configureSentry } from "@goauthentik/common/sentry"; import { configureSentry } from "@goauthentik/common/sentry";
import { DefaultBrand } from "@goauthentik/common/ui/config"; import { DefaultBrand } from "@goauthentik/common/ui/config";
import { WebsocketClient } from "@goauthentik/common/ws"; import { WebsocketClient } from "@goauthentik/common/ws";
import { Interface } from "@goauthentik/elements/Interface"; import { Interface } from "@goauthentik/elements/Interface";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import "@goauthentik/elements/LoadingOverlay"; import "@goauthentik/elements/LoadingOverlay";
import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/ak-locale-context/ak-locale-context";
import { themeImage } from "@goauthentik/elements/utils/images"; import { themeImage } from "@goauthentik/elements/utils/images";
import "@goauthentik/flow/components/ak-brand-footer"; import "@goauthentik/flow/components/ak-brand-footer";
import "@goauthentik/flow/sources/apple/AppleLoginInit"; import "@goauthentik/flow/sources/apple/AppleLoginInit";
@ -48,7 +46,10 @@ import {
} from "@goauthentik/api"; } from "@goauthentik/api";
@customElement("ak-flow-executor") @customElement("ak-flow-executor")
export class FlowExecutor extends Interface implements StageHost { export class FlowExecutor
extends WithCapabilitiesConfig(WithBrandConfig(Interface))
implements StageHost
{
@property() @property()
flowSlug: string = window.location.pathname.split("/")[3]; flowSlug: string = window.location.pathname.split("/")[3];
@ -58,9 +59,9 @@ export class FlowExecutor extends Interface implements StageHost {
set challenge(value: ChallengeTypes | undefined) { set challenge(value: ChallengeTypes | undefined) {
this._challenge = value; this._challenge = value;
if (value?.flowInfo?.title) { if (value?.flowInfo?.title) {
document.title = `${value.flowInfo?.title} - ${this.brand?.brandingTitle}`; document.title = `${value.flowInfo?.title} - ${this.brandingTitle}`;
} else { } else {
document.title = this.brand?.brandingTitle || TITLE_DEFAULT; document.title = this.brandingTitle;
} }
this.requestUpdate(); this.requestUpdate();
} }
@ -238,7 +239,7 @@ export class FlowExecutor extends Interface implements StageHost {
} }
async firstUpdated(): Promise<void> { async firstUpdated(): Promise<void> {
if (this.config?.capabilities.includes(CapabilitiesEnum.CanDebug)) { if (this.can(CapabilitiesEnum.CanDebug)) {
this.inspectorAvailable = true; this.inspectorAvailable = true;
} }
this.loading = true; this.loading = true;
@ -520,7 +521,7 @@ export class FlowExecutor extends Interface implements StageHost {
> >
<img <img
src="${themeImage( src="${themeImage(
this.brand?.brandingLogo ?? this.brand.brandingLogo ??
globalAK()?.brand.brandingLogo ?? globalAK()?.brand.brandingLogo ??
DefaultBrand.brandingLogo, DefaultBrand.brandingLogo,
)}" )}"
@ -531,7 +532,7 @@ export class FlowExecutor extends Interface implements StageHost {
</div> </div>
<ak-brand-links <ak-brand-links
class="pf-c-login__footer" class="pf-c-login__footer"
.links=${this.brand?.uiFooterLinks ?? []} .links=${this.brand.uiFooterLinks ?? []}
></ak-brand-links> ></ak-brand-links>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
import { TITLE_DEFAULT } from "@goauthentik/common/constants";
import { Interface } from "@goauthentik/elements/Interface"; import { Interface } from "@goauthentik/elements/Interface";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import "@goauthentik/elements/LoadingOverlay"; import "@goauthentik/elements/LoadingOverlay";
import Guacamole from "guacamole-common-js"; import Guacamole from "guacamole-common-js";
@ -43,7 +43,7 @@ const RECONNECT_ATTEMPTS_INITIAL = 5;
const RECONNECT_ATTEMPTS = 5; const RECONNECT_ATTEMPTS = 5;
@customElement("ak-rac") @customElement("ak-rac")
export class RacInterface extends Interface { export class RacInterface extends WithBrandConfig(Interface) {
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
PFBase, PFBase,
@ -231,10 +231,12 @@ export class RacInterface extends Interface {
} }
updateTitle(): void { updateTitle(): void {
let title = this.brand?.brandingTitle || TITLE_DEFAULT; let title = this.brand.brandingTitle;
if (this.endpointName) { if (this.endpointName) {
title = `${this.endpointName} - ${title}`; title = `${this.endpointName} - ${title}`;
} }
document.title = `${title}`; document.title = `${title}`;
} }

View File

@ -5,8 +5,7 @@ import "@goauthentik/elements/ak-locale-context/index.js";
import { CSRFHeaderName } from "@goauthentik/common/api/middleware.js"; import { CSRFHeaderName } from "@goauthentik/common/api/middleware.js";
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants.js"; import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants.js";
import { getCookie } from "@goauthentik/common/utils.js"; import { getCookie } from "@goauthentik/common/utils.js";
import { Interface } from "@goauthentik/elements/Interface/Interface.js"; import { Interface } from "@goauthentik/elements/Interface.js";
import { DefaultBrand } from "@goauthentik/common/ui/config.js";
import { themeImage } from "@goauthentik/elements/utils/images.js"; import { themeImage } from "@goauthentik/elements/utils/images.js";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
@ -15,9 +14,10 @@ import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { UiThemeEnum } from "@goauthentik/api"; import { UiThemeEnum } from "@goauthentik/api";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
@customElement("ak-api-browser") @customElement("ak-api-browser")
export class APIBrowser extends Interface { export class APIBrowser extends WithBrandConfig(Interface) {
@property() @property()
schemaPath?: string; schemaPath?: string;
@ -102,9 +102,7 @@ export class APIBrowser extends Interface {
<img <img
alt="${msg("authentik Logo")}" alt="${msg("authentik Logo")}"
class="logo" class="logo"
src="${themeImage( src="${themeImage(this.brandingLogo)}"
this.brand?.brandingLogo ?? DefaultBrand.brandingLogo,
)}"
/> />
</div> </div>
</rapi-doc> </rapi-doc>

View File

@ -1,4 +1,6 @@
import { LightInterface } from "@goauthentik/elements/Interface"; import { globalAK } from "@goauthentik/common/global";
import { applyDocumentTheme } from "@goauthentik/common/theme";
import { AKElement } from "@goauthentik/elements/Base";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { TemplateResult, css, html } from "lit"; import { TemplateResult, css, html } from "lit";
@ -10,7 +12,7 @@ import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-loading") @customElement("ak-loading")
export class Loading extends LightInterface { export class Loading extends AKElement {
static styles = [ static styles = [
PFBase, PFBase,
PFPage, PFPage,
@ -23,6 +25,16 @@ export class Loading extends LightInterface {
`, `,
]; ];
constructor() {
super();
applyDocumentTheme(globalAK().brand.uiTheme);
}
public connectedCallback(): void {
this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
}
render(): TemplateResult { render(): TemplateResult {
return html` <section return html` <section
class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl" class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl"

View File

@ -1,8 +1,9 @@
import { PFSize } from "@goauthentik/common/enums.js"; import { PFSize } from "@goauthentik/common/enums.js";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { rootInterface } from "@goauthentik/common/theme";
import { truncateWords } from "@goauthentik/common/utils"; import { truncateWords } from "@goauthentik/common/utils";
import "@goauthentik/elements/AppIcon"; import "@goauthentik/elements/AppIcon";
import { AKElement, rootInterface } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/Expand"; import "@goauthentik/elements/Expand";
import "@goauthentik/user/LibraryApplication/RACLaunchEndpointModal"; import "@goauthentik/user/LibraryApplication/RACLaunchEndpointModal";
import type { RACLaunchEndpointModal } from "@goauthentik/user/LibraryApplication/RACLaunchEndpointModal"; import type { RACLaunchEndpointModal } from "@goauthentik/user/LibraryApplication/RACLaunchEndpointModal";
@ -71,14 +72,14 @@ export class LibraryApplication extends AKElement {
} }
renderExpansion(application: Application) { renderExpansion(application: Application) {
const me = rootInterface<UserInterface>()?.me; const { me, uiConfig } = rootInterface<UserInterface>();
return html`<ak-expand textOpen=${msg("Fewer details")} textClosed=${msg("More details")}> return html`<ak-expand textOpen=${msg("Fewer details")} textClosed=${msg("More details")}>
<div class="pf-c-content"> <div class="pf-c-content">
<small>${application.metaPublisher}</small> <small>${application.metaPublisher}</small>
</div> </div>
${truncateWords(application.metaDescription || "", 10)} ${truncateWords(application.metaDescription || "", 10)}
${rootInterface()?.uiConfig?.enabledFeatures.applicationEdit && me?.user.isSuperuser ${uiConfig?.enabledFeatures.applicationEdit && me?.user.isSuperuser
? html` ? html`
<a <a
class="pf-c-button pf-m-control pf-m-small pf-m-block" class="pf-c-button pf-m-control pf-m-small pf-m-block"
@ -148,9 +149,10 @@ export class LibraryApplication extends AKElement {
return html`<ak-spinner></ak-spinner>`; return html`<ak-spinner></ak-spinner>`;
} }
const me = rootInterface<UserInterface>()?.me; const { me, uiConfig } = rootInterface<UserInterface>();
const expandable = const expandable =
(rootInterface()?.uiConfig?.enabledFeatures.applicationEdit && me?.user.isSuperuser) || (uiConfig?.enabledFeatures.applicationEdit && me?.user.isSuperuser) ||
this.application.metaPublisher !== "" || this.application.metaPublisher !== "" ||
this.application.metaDescription !== ""; this.application.metaDescription !== "";

View File

@ -1,7 +1,9 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { rootInterface } from "@goauthentik/common/theme";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { AKElement, rootInterface } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/EmptyState"; import "@goauthentik/elements/EmptyState";
import type { UserInterface } from "@goauthentik/user/index.entrypoint";
import { localized, msg } from "@lit/localize"; import { localized, msg } from "@lit/localize";
import { html } from "lit"; import { html } from "lit";
@ -47,7 +49,8 @@ export class LibraryPage extends AKElement {
constructor() { constructor() {
super(); super();
const uiConfig = rootInterface()?.uiConfig; const { uiConfig } = rootInterface<UserInterface>();
if (!uiConfig) { if (!uiConfig) {
throw new Error("Could not retrieve uiConfig. Reason: unknown. Check logs."); throw new Error("Could not retrieve uiConfig. Reason: unknown. Check logs.");
} }

View File

@ -11,11 +11,12 @@ import { DefaultBrand } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { WebsocketClient } from "@goauthentik/common/ws"; import { WebsocketClient } from "@goauthentik/common/ws";
import "@goauthentik/components/ak-nav-buttons"; import "@goauthentik/components/ak-nav-buttons";
import { AuthenticatedInterface } from "@goauthentik/elements/AuthenticatedInterface";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { AuthenticatedInterface } from "@goauthentik/elements/Interface"; import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/ak-locale-context/ak-locale-context";
import "@goauthentik/elements/banner/EnterpriseStatusBanner"; import "@goauthentik/elements/banner/EnterpriseStatusBanner";
import "@goauthentik/elements/buttons/ActionButton"; import "@goauthentik/elements/buttons/ActionButton/ak-action-button";
import "@goauthentik/elements/messages/MessageContainer"; import "@goauthentik/elements/messages/MessageContainer";
import "@goauthentik/elements/notifications/APIDrawer"; import "@goauthentik/elements/notifications/APIDrawer";
import "@goauthentik/elements/notifications/NotificationDrawer"; import "@goauthentik/elements/notifications/NotificationDrawer";
@ -41,7 +42,7 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css"; import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
import { CurrentBrand, EventsApi, SessionUser } from "@goauthentik/api"; import { EventsApi, SessionUser } from "@goauthentik/api";
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");
@ -117,9 +118,8 @@ const customStyles = css`
@customElement("ak-interface-user-presentation") @customElement("ak-interface-user-presentation")
// @ts-ignore // @ts-ignore
class UserInterfacePresentation extends AKElement { class UserInterfacePresentation extends WithBrandConfig(AKElement) {
static get styles() { static styles = [
return [
PFBase, PFBase,
PFDisplay, PFDisplay,
PFBrand, PFBrand,
@ -131,7 +131,6 @@ class UserInterfacePresentation extends AKElement {
PFNotificationBadge, PFNotificationBadge,
customStyles, customStyles,
]; ];
}
@property({ type: Object }) @property({ type: Object })
uiConfig!: UIConfig; uiConfig!: UIConfig;
@ -148,9 +147,6 @@ class UserInterfacePresentation extends AKElement {
@property({ type: Number }) @property({ type: Number })
notificationsCount = 0; notificationsCount = 0;
@property({ type: Object })
brand!: CurrentBrand;
get canAccessAdmin() { get canAccessAdmin() {
return ( return (
this.me.user.isSuperuser || this.me.user.isSuperuser ||
@ -207,7 +203,7 @@ class UserInterfacePresentation extends AKElement {
<img <img
class="pf-c-brand" class="pf-c-brand"
src="${themeImage(this.brand.brandingLogo)}" src="${themeImage(this.brand.brandingLogo)}"
alt="${this.brand.brandingTitle}" alt="${this.brandingTitle}"
/> />
</a> </a>
</div> </div>
@ -265,7 +261,7 @@ class UserInterfacePresentation extends AKElement {
// //
// //
@customElement("ak-interface-user") @customElement("ak-interface-user")
export class UserInterface extends AuthenticatedInterface { export class UserInterface extends WithBrandConfig(AuthenticatedInterface) {
@property({ type: Boolean }) @property({ type: Boolean })
notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
@ -278,7 +274,10 @@ export class UserInterface extends AuthenticatedInterface {
notificationsCount = 0; notificationsCount = 0;
@state() @state()
me?: SessionUser; me: SessionUser | null = null;
@state()
uiConfig: UIConfig | null = null;
constructor() { constructor() {
configureSentry(true); configureSentry(true);

View File

@ -1,6 +1,7 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { AKElement, rootInterface } from "@goauthentik/elements/Base"; import { rootInterface } from "@goauthentik/common/theme";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/Tabs"; import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/user/SessionList"; import "@goauthentik/elements/user/SessionList";
import "@goauthentik/elements/user/UserConsentList"; import "@goauthentik/elements/user/UserConsentList";

View File

@ -92,10 +92,10 @@ export class UserSettingsFlowExecutor
updated(changedProperties: PropertyValues<this>): void { updated(changedProperties: PropertyValues<this>): void {
if (changedProperties.has("brand") && this.brand) { if (changedProperties.has("brand") && this.brand) {
this.flowSlug = this.brand?.flowUserSettings; this.flowSlug = this.brand.flowUserSettings;
if (!this.flowSlug) {
return; if (!this.flowSlug) return;
}
this.nextChallenge(); this.nextChallenge();
} }
} }