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`
`;
+ }
return html`
`;
}
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/*"]
}