diff --git a/authentik/core/templates/base/skeleton.html b/authentik/core/templates/base/skeleton.html index 86da52e949..371dd8d0e2 100644 --- a/authentik/core/templates/base/skeleton.html +++ b/authentik/core/templates/base/skeleton.html @@ -13,6 +13,7 @@ {% block head_before %} {% endblock %} + {% versioned_script "dist/poly-%v.js" %} diff --git a/web/build.mjs b/web/build.mjs index 1dbd2d3193..2885f8ab9f 100644 --- a/web/build.mjs +++ b/web/build.mjs @@ -41,6 +41,7 @@ const definitions = { const otherFiles = [ ["node_modules/@patternfly/patternfly/patternfly.min.css", "."], + ["node_modules/@patternfly/patternfly/patternfly-base.css", "."], ["node_modules/@patternfly/patternfly/assets/**", ".", "node_modules/@patternfly/patternfly/"], ["src/custom.css", "."], ["src/common/styles/**", "."], @@ -79,6 +80,11 @@ const interfaces = [ ["polyfill/poly.ts", "."], ]; +const extraTargets = [ + ["sdk/index.ts", "sdk", { entryNames: "[dir]/[name]" }], + ["sdk/user-settings.ts", "sdk/user-settings", { entryNames: "[dir]/[name]" }], +]; + const baseArgs = { bundle: true, write: true, @@ -101,7 +107,11 @@ function getVersion() { return version; } -async function buildOneSource(source, dest) { +function getAllTargets() { + return [...interfaces, ...extraTargets]; +} + +async function buildSingleTarget(source, dest, options) { const DIST = path.join(__dirname, "./dist", dest); console.log(`[${new Date(Date.now()).toISOString()}] Starting build for target ${source}`); @@ -112,6 +122,7 @@ async function buildOneSource(source, dest) { entryPoints: [`./src/${source}`], entryNames: `[dir]/[name]-${getVersion()}`, outdir: DIST, + ...options, }); const end = Date.now(); console.log( @@ -124,8 +135,10 @@ async function buildOneSource(source, dest) { } } -async function buildAuthentik(interfaces) { - await Promise.allSettled(interfaces.map(([source, dest]) => buildOneSource(source, dest))); +async function buildTargets(targets) { + await Promise.allSettled( + targets.map(([source, dest, options]) => buildSingleTarget(source, dest, options)), + ); } let timeoutId = null; @@ -135,7 +148,7 @@ function debouncedBuild() { } timeoutId = setTimeout(() => { console.clear(); - buildAuthentik(interfaces); + buildTargets(getAllTargets()); }, 250); } @@ -143,7 +156,7 @@ if (process.argv.length > 2 && (process.argv[2] === "-h" || process.argv[2] === console.log(`Build the authentikUI options: - -w, --watch: Build all ${interfaces.length} interfaces + -w, --watch: Build all ${getAllTargets().length} interfaces -p, --proxy: Build only the polyfills and the loading application -h, --help: This help message `); @@ -163,11 +176,11 @@ if (process.argv.length > 2 && (process.argv[2] === "-w" || process.argv[2] === }); } else if (process.argv.length > 2 && (process.argv[2] === "-p" || process.argv[2] === "--proxy")) { // There's no watch-for-proxy, sorry. - await buildAuthentik( + await buildTargets( interfaces.filter(([_, dest]) => ["standalone/loading", "."].includes(dest)), ); process.exit(0); } else { // And the fallback: just build it. - await buildAuthentik(interfaces); + await buildTargets(interfaces); } diff --git a/web/src/admin/sources/utils.ts b/web/src/admin/sources/utils.ts index 83ff0b967c..6ec8e57212 100644 --- a/web/src/admin/sources/utils.ts +++ b/web/src/admin/sources/utils.ts @@ -7,6 +7,9 @@ export function renderSourceIcon(name: string, iconUrl: string | undefined | nul const url = iconUrl.replaceAll("fa://", ""); return html``; } + if (window.authentik_sdk?.base) { + return html`${name}`; + } return html`${name}`; } return icon; diff --git a/web/src/common/api/config.ts b/web/src/common/api/config.ts index 52b2f8f55c..b93ade8d88 100644 --- a/web/src/common/api/config.ts +++ b/web/src/common/api/config.ts @@ -2,6 +2,7 @@ import { CSRFMiddleware, EventMiddleware, LoggingMiddleware, + SDKMiddleware, } from "@goauthentik/common/api/middleware"; import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants"; import { globalAK } from "@goauthentik/common/global"; @@ -67,8 +68,18 @@ export function getMetaContent(key: string): string { return metaEl.content; } +export function apiBase(): string { + if (process.env.AK_API_BASE_PATH) { + return process.env.AK_API_BASE_PATH; + } + if (window.authentik_sdk?.base) { + return window.authentik_sdk?.base; + } + return window.location.origin; +} + export const DEFAULT_CONFIG = new Configuration({ - basePath: (process.env.AK_API_BASE_PATH || window.location.origin) + "/api/v3", + basePath: `${apiBase()}/api/v3`, headers: { "sentry-trace": getMetaContent("sentry-trace"), }, @@ -76,6 +87,7 @@ export const DEFAULT_CONFIG = new Configuration({ new CSRFMiddleware(), new EventMiddleware(), new LoggingMiddleware(globalAK().brand), + new SDKMiddleware(), ], }); diff --git a/web/src/common/api/middleware.ts b/web/src/common/api/middleware.ts index 8aee822c7a..c68df79adc 100644 --- a/web/src/common/api/middleware.ts +++ b/web/src/common/api/middleware.ts @@ -44,6 +44,21 @@ export class CSRFMiddleware implements Middleware { } } +export class SDKMiddleware implements Middleware { + token?: string; + constructor() { + this.token = window.authentik_sdk?.token; + } + pre?(context: RequestContext): Promise { + if (this.token) { + context.init.credentials = "include"; + // @ts-ignore + context.init.headers["Authorization"] = `Bearer ${this.token}`; + } + return Promise.resolve(context); + } +} + export class EventMiddleware implements Middleware { post?(context: ResponseContext): Promise { const request: RequestInfo = { diff --git a/web/src/common/global.ts b/web/src/common/global.ts index 990303df0d..d779190e78 100644 --- a/web/src/common/global.ts +++ b/web/src/common/global.ts @@ -1,4 +1,10 @@ -import { Config, ConfigFromJSON, CurrentBrand, CurrentBrandFromJSON } from "@goauthentik/api"; +import { + Config, + ConfigFromJSON, + CurrentBrand, + CurrentBrandFromJSON, + UiThemeEnum, +} from "@goauthentik/api"; export interface GlobalAuthentik { _converted?: boolean; @@ -30,7 +36,9 @@ export function globalAK(): GlobalAuthentik { capabilities: [], }), brand: CurrentBrandFromJSON({ + matched_domain: window.location.host, ui_footer_links: [], + ui_theme: window.authentik_sdk?.forceTheme ?? UiThemeEnum.Automatic, }), versionFamily: "", versionSubdomain: "", @@ -40,6 +48,10 @@ export function globalAK(): GlobalAuthentik { return ak; } +export function isEmbedded() { + return !!window.authentik_sdk; +} + export function docLink(path: string): string { const ak = globalAK(); // Default case or beta build which should always point to latest diff --git a/web/src/elements/Interface/Interface.ts b/web/src/elements/Interface/Interface.ts index 29aa053d8f..28665af5b6 100644 --- a/web/src/elements/Interface/Interface.ts +++ b/web/src/elements/Interface/Interface.ts @@ -1,10 +1,11 @@ +import { isEmbedded } from "@goauthentik/common/global"; import { UIConfig, uiConfig } from "@goauthentik/common/ui/config"; import { ModalOrchestrationController } from "@goauthentik/elements/controllers/ModalOrchestrationController.js"; import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet"; import { state } from "lit/decorators.js"; -import PFBase from "@patternfly/patternfly/patternfly-base.css"; +import PFVariables from "@patternfly/patternfly/base/patternfly-variables.css"; import type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api"; import { UiThemeEnum } from "@goauthentik/api"; @@ -43,7 +44,10 @@ export class Interface extends AKElement implements AkInterface { constructor() { super(); - document.adoptedStyleSheets = [...document.adoptedStyleSheets, ensureCSSStyleSheet(PFBase)]; + document.adoptedStyleSheets = [ + ...document.adoptedStyleSheets, + ensureCSSStyleSheet(PFVariables), + ]; this[brandContext] = new BrandContextController(this); this[configContext] = new ConfigContextController(this); this[modalController] = new ModalOrchestrationController(this); @@ -61,7 +65,9 @@ export class Interface extends AKElement implements AkInterface { // Instead of calling ._activateTheme() twice, we insert the root document in the call // since multiple calls to ._activateTheme() would not do anything after the first call // as the theme is already enabled. - roots.unshift(document as unknown as DocumentOrShadowRoot); + if (!isEmbedded()) { + roots.unshift(document as unknown as DocumentOrShadowRoot); + } super._activateTheme(theme, ...roots); } diff --git a/web/src/global.d.ts b/web/src/global.d.ts index 059e868156..83efb9753d 100644 --- a/web/src/global.d.ts +++ b/web/src/global.d.ts @@ -12,3 +12,11 @@ declare namespace Intl { public format: (items: string[]) => string; } } + +declare interface Window { + authentik_sdk?: { + base: string; + token?: string; + forceTheme?: string; + }; +} diff --git a/web/src/sdk/common.ts b/web/src/sdk/common.ts new file mode 100644 index 0000000000..834c4ebe00 --- /dev/null +++ b/web/src/sdk/common.ts @@ -0,0 +1,19 @@ +import { Interface } from "@goauthentik/elements/Interface/Interface"; + +import { html } from "lit"; +import { customElement } from "lit/decorators.js"; + +import { UiThemeEnum, UiThemeEnumFromJSON } from "@goauthentik/api"; + +@customElement("ak-sdk-interface") +export class SDKInterface extends Interface { + constructor() { + super(); + } + render() { + return html``; + } + async getTheme(): Promise { + return UiThemeEnumFromJSON(window.authentik_sdk?.forceTheme) || UiThemeEnum.Automatic; + } +} diff --git a/web/src/sdk/index.ts b/web/src/sdk/index.ts new file mode 100644 index 0000000000..e3820a5f44 --- /dev/null +++ b/web/src/sdk/index.ts @@ -0,0 +1 @@ +import "@goauthentik/sdk/user-settings"; diff --git a/web/src/sdk/user-settings.ts b/web/src/sdk/user-settings.ts new file mode 100644 index 0000000000..69021de383 --- /dev/null +++ b/web/src/sdk/user-settings.ts @@ -0,0 +1,3 @@ +import "@goauthentik/elements/messages/MessageContainer"; +import "@goauthentik/sdk/common"; +import "@goauthentik/user/user-settings/UserSettingsPage"; diff --git a/web/src/user/user-settings/details/UserSettingsFlowExecutor.ts b/web/src/user/user-settings/details/UserSettingsFlowExecutor.ts index f362f3e2fe..8f6bcdf795 100644 --- a/web/src/user/user-settings/details/UserSettingsFlowExecutor.ts +++ b/web/src/user/user-settings/details/UserSettingsFlowExecutor.ts @@ -21,6 +21,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; import { ChallengeTypes, + CoreApi, FlowChallengeResponseRequest, FlowErrorChallenge, FlowsApi, @@ -82,8 +83,11 @@ export class UserSettingsFlowExecutor }); } - firstUpdated(): void { - this.flowSlug = this.brand?.flowUserSettings; + async firstUpdated(): Promise { + if (!this.brand) { + this.brand = await new CoreApi(DEFAULT_CONFIG).coreBrandsCurrentRetrieve(); + } + this.flowSlug = this.brand.flowUserSettings; if (!this.flowSlug) { return; } diff --git a/web/tsconfig.json b/web/tsconfig.json index 67960b2c46..6f62926057 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -10,6 +10,7 @@ "@goauthentik/flow/*": ["./src/flow/*"], "@goauthentik/locales/*": ["./src/locales/*"], "@goauthentik/polyfill/*": ["./src/polyfill/*"], + "@goauthentik/sdk/*": ["./src/sdk/*"], "@goauthentik/standalone/*": ["./src/standalone/*"], "@goauthentik/user/*": ["./src/user/*"] }