web/sdk: embed authentik settings into anything

the web part

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens Langhammer
2024-09-01 00:11:02 +02:00
parent 5f261bed96
commit accf25a626
13 changed files with 112 additions and 14 deletions

View File

@ -13,6 +13,7 @@
<link rel="shortcut icon" href="{{ brand.branding_favicon }}"> <link rel="shortcut icon" href="{{ brand.branding_favicon }}">
{% block head_before %} {% block head_before %}
{% endblock %} {% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject> <link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
{% versioned_script "dist/poly-%v.js" %} {% versioned_script "dist/poly-%v.js" %}

View File

@ -41,6 +41,7 @@ const definitions = {
const otherFiles = [ const otherFiles = [
["node_modules/@patternfly/patternfly/patternfly.min.css", "."], ["node_modules/@patternfly/patternfly/patternfly.min.css", "."],
["node_modules/@patternfly/patternfly/patternfly-base.css", "."],
["node_modules/@patternfly/patternfly/assets/**", ".", "node_modules/@patternfly/patternfly/"], ["node_modules/@patternfly/patternfly/assets/**", ".", "node_modules/@patternfly/patternfly/"],
["src/custom.css", "."], ["src/custom.css", "."],
["src/common/styles/**", "."], ["src/common/styles/**", "."],
@ -79,6 +80,11 @@ const interfaces = [
["polyfill/poly.ts", "."], ["polyfill/poly.ts", "."],
]; ];
const extraTargets = [
["sdk/index.ts", "sdk", { entryNames: "[dir]/[name]" }],
["sdk/user-settings.ts", "sdk/user-settings", { entryNames: "[dir]/[name]" }],
];
const baseArgs = { const baseArgs = {
bundle: true, bundle: true,
write: true, write: true,
@ -101,7 +107,11 @@ function getVersion() {
return version; 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); const DIST = path.join(__dirname, "./dist", dest);
console.log(`[${new Date(Date.now()).toISOString()}] Starting build for target ${source}`); console.log(`[${new Date(Date.now()).toISOString()}] Starting build for target ${source}`);
@ -112,6 +122,7 @@ async function buildOneSource(source, dest) {
entryPoints: [`./src/${source}`], entryPoints: [`./src/${source}`],
entryNames: `[dir]/[name]-${getVersion()}`, entryNames: `[dir]/[name]-${getVersion()}`,
outdir: DIST, outdir: DIST,
...options,
}); });
const end = Date.now(); const end = Date.now();
console.log( console.log(
@ -124,8 +135,10 @@ async function buildOneSource(source, dest) {
} }
} }
async function buildAuthentik(interfaces) { async function buildTargets(targets) {
await Promise.allSettled(interfaces.map(([source, dest]) => buildOneSource(source, dest))); await Promise.allSettled(
targets.map(([source, dest, options]) => buildSingleTarget(source, dest, options)),
);
} }
let timeoutId = null; let timeoutId = null;
@ -135,7 +148,7 @@ function debouncedBuild() {
} }
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
console.clear(); console.clear();
buildAuthentik(interfaces); buildTargets(getAllTargets());
}, 250); }, 250);
} }
@ -143,7 +156,7 @@ if (process.argv.length > 2 && (process.argv[2] === "-h" || process.argv[2] ===
console.log(`Build the authentikUI console.log(`Build the authentikUI
options: 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 -p, --proxy: Build only the polyfills and the loading application
-h, --help: This help message -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")) { } else if (process.argv.length > 2 && (process.argv[2] === "-p" || process.argv[2] === "--proxy")) {
// There's no watch-for-proxy, sorry. // There's no watch-for-proxy, sorry.
await buildAuthentik( await buildTargets(
interfaces.filter(([_, dest]) => ["standalone/loading", "."].includes(dest)), interfaces.filter(([_, dest]) => ["standalone/loading", "."].includes(dest)),
); );
process.exit(0); process.exit(0);
} else { } else {
// And the fallback: just build it. // And the fallback: just build it.
await buildAuthentik(interfaces); await buildTargets(interfaces);
} }

View File

@ -7,6 +7,9 @@ export function renderSourceIcon(name: string, iconUrl: string | undefined | nul
const url = iconUrl.replaceAll("fa://", ""); const url = iconUrl.replaceAll("fa://", "");
return html`<i class="fas ${url}" title="${name}"></i>`; return html`<i class="fas ${url}" title="${name}"></i>`;
} }
if (window.authentik_sdk?.base) {
return html`<img src="${window.authentik_sdk?.base}${iconUrl}" alt="${name}" />`;
}
return html`<img src="${iconUrl}" alt="${name}" />`; return html`<img src="${iconUrl}" alt="${name}" />`;
} }
return icon; return icon;

View File

@ -2,6 +2,7 @@ import {
CSRFMiddleware, CSRFMiddleware,
EventMiddleware, EventMiddleware,
LoggingMiddleware, LoggingMiddleware,
SDKMiddleware,
} from "@goauthentik/common/api/middleware"; } from "@goauthentik/common/api/middleware";
import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants"; import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
@ -67,8 +68,18 @@ export function getMetaContent(key: string): string {
return metaEl.content; 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({ export const DEFAULT_CONFIG = new Configuration({
basePath: (process.env.AK_API_BASE_PATH || window.location.origin) + "/api/v3", basePath: `${apiBase()}/api/v3`,
headers: { headers: {
"sentry-trace": getMetaContent("sentry-trace"), "sentry-trace": getMetaContent("sentry-trace"),
}, },
@ -76,6 +87,7 @@ export const DEFAULT_CONFIG = new Configuration({
new CSRFMiddleware(), new CSRFMiddleware(),
new EventMiddleware(), new EventMiddleware(),
new LoggingMiddleware(globalAK().brand), new LoggingMiddleware(globalAK().brand),
new SDKMiddleware(),
], ],
}); });

View File

@ -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<FetchParams | void> {
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 { export class EventMiddleware implements Middleware {
post?(context: ResponseContext): Promise<Response | void> { post?(context: ResponseContext): Promise<Response | void> {
const request: RequestInfo = { const request: RequestInfo = {

View File

@ -1,4 +1,10 @@
import { Config, ConfigFromJSON, CurrentBrand, CurrentBrandFromJSON } from "@goauthentik/api"; import {
Config,
ConfigFromJSON,
CurrentBrand,
CurrentBrandFromJSON,
UiThemeEnum,
} from "@goauthentik/api";
export interface GlobalAuthentik { export interface GlobalAuthentik {
_converted?: boolean; _converted?: boolean;
@ -30,7 +36,9 @@ export function globalAK(): GlobalAuthentik {
capabilities: [], capabilities: [],
}), }),
brand: CurrentBrandFromJSON({ brand: CurrentBrandFromJSON({
matched_domain: window.location.host,
ui_footer_links: [], ui_footer_links: [],
ui_theme: window.authentik_sdk?.forceTheme ?? UiThemeEnum.Automatic,
}), }),
versionFamily: "", versionFamily: "",
versionSubdomain: "", versionSubdomain: "",
@ -40,6 +48,10 @@ export function globalAK(): GlobalAuthentik {
return ak; return ak;
} }
export function isEmbedded() {
return !!window.authentik_sdk;
}
export function docLink(path: string): string { export function docLink(path: string): string {
const ak = globalAK(); const ak = globalAK();
// Default case or beta build which should always point to latest // Default case or beta build which should always point to latest

View File

@ -1,10 +1,11 @@
import { isEmbedded } from "@goauthentik/common/global";
import { UIConfig, uiConfig } from "@goauthentik/common/ui/config"; import { UIConfig, uiConfig } from "@goauthentik/common/ui/config";
import { ModalOrchestrationController } from "@goauthentik/elements/controllers/ModalOrchestrationController.js"; import { ModalOrchestrationController } from "@goauthentik/elements/controllers/ModalOrchestrationController.js";
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet"; import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
import { state } from "lit/decorators.js"; 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 type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api";
import { UiThemeEnum } from "@goauthentik/api"; import { UiThemeEnum } from "@goauthentik/api";
@ -43,7 +44,10 @@ export class Interface extends AKElement implements AkInterface {
constructor() { constructor() {
super(); super();
document.adoptedStyleSheets = [...document.adoptedStyleSheets, ensureCSSStyleSheet(PFBase)]; document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
ensureCSSStyleSheet(PFVariables),
];
this[brandContext] = new BrandContextController(this); this[brandContext] = new BrandContextController(this);
this[configContext] = new ConfigContextController(this); this[configContext] = new ConfigContextController(this);
this[modalController] = new ModalOrchestrationController(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 // 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 // since multiple calls to ._activateTheme() would not do anything after the first call
// as the theme is already enabled. // 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); super._activateTheme(theme, ...roots);
} }

8
web/src/global.d.ts vendored
View File

@ -12,3 +12,11 @@ declare namespace Intl {
public format: (items: string[]) => string; public format: (items: string[]) => string;
} }
} }
declare interface Window {
authentik_sdk?: {
base: string;
token?: string;
forceTheme?: string;
};
}

19
web/src/sdk/common.ts Normal file
View File

@ -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`<slot></slot>`;
}
async getTheme(): Promise<UiThemeEnum> {
return UiThemeEnumFromJSON(window.authentik_sdk?.forceTheme) || UiThemeEnum.Automatic;
}
}

1
web/src/sdk/index.ts Normal file
View File

@ -0,0 +1 @@
import "@goauthentik/sdk/user-settings";

View File

@ -0,0 +1,3 @@
import "@goauthentik/elements/messages/MessageContainer";
import "@goauthentik/sdk/common";
import "@goauthentik/user/user-settings/UserSettingsPage";

View File

@ -21,6 +21,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { import {
ChallengeTypes, ChallengeTypes,
CoreApi,
FlowChallengeResponseRequest, FlowChallengeResponseRequest,
FlowErrorChallenge, FlowErrorChallenge,
FlowsApi, FlowsApi,
@ -82,8 +83,11 @@ export class UserSettingsFlowExecutor
}); });
} }
firstUpdated(): void { async firstUpdated(): Promise<void> {
this.flowSlug = this.brand?.flowUserSettings; if (!this.brand) {
this.brand = await new CoreApi(DEFAULT_CONFIG).coreBrandsCurrentRetrieve();
}
this.flowSlug = this.brand.flowUserSettings;
if (!this.flowSlug) { if (!this.flowSlug) {
return; return;
} }

View File

@ -10,6 +10,7 @@
"@goauthentik/flow/*": ["./src/flow/*"], "@goauthentik/flow/*": ["./src/flow/*"],
"@goauthentik/locales/*": ["./src/locales/*"], "@goauthentik/locales/*": ["./src/locales/*"],
"@goauthentik/polyfill/*": ["./src/polyfill/*"], "@goauthentik/polyfill/*": ["./src/polyfill/*"],
"@goauthentik/sdk/*": ["./src/sdk/*"],
"@goauthentik/standalone/*": ["./src/standalone/*"], "@goauthentik/standalone/*": ["./src/standalone/*"],
"@goauthentik/user/*": ["./src/user/*"] "@goauthentik/user/*": ["./src/user/*"]
} }