Compare commits

..

4 Commits

12 changed files with 424 additions and 414 deletions

52
web/package-lock.json generated
View File

@ -25,7 +25,6 @@
"@formatjs/intl-listformat": "^7.5.7", "@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.2.4-1745325566", "@goauthentik/api": "^2025.2.4-1745325566",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2", "@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4", "@lit/reactive-element": "^2.0.4",
@ -66,6 +65,7 @@
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"remark-mdx-frontmatter": "^5.0.0", "remark-mdx-frontmatter": "^5.0.0",
"style-mod": "^4.1.2", "style-mod": "^4.1.2",
"trusted-types": "^2.0.0",
"ts-pattern": "^5.4.0", "ts-pattern": "^5.4.0",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"webcomponent-qr-code": "^1.2.0", "webcomponent-qr-code": "^1.2.0",
@ -2281,47 +2281,11 @@
"@lezer/lr": "^1.0.0" "@lezer/lr": "^1.0.0"
} }
}, },
"node_modules/@lit-labs/ssr": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr/-/ssr-3.3.1.tgz",
"integrity": "sha512-JlF1PempxvzrGEpRFrF+Ki0MHzR3HA51SK8Zv0cFpW9p0bPW4k0FeCwrElCu371UEpXF7RcaE2wgYaE1az0XKg==",
"dependencies": {
"@lit-labs/ssr-client": "^1.1.7",
"@lit-labs/ssr-dom-shim": "^1.3.0",
"@lit/reactive-element": "^2.0.4",
"@parse5/tools": "^0.3.0",
"@types/node": "^16.0.0",
"enhanced-resolve": "^5.10.0",
"lit": "^3.1.2",
"lit-element": "^4.0.4",
"lit-html": "^3.1.2",
"node-fetch": "^3.2.8",
"parse5": "^7.1.1"
},
"engines": {
"node": ">=13.9.0"
}
},
"node_modules/@lit-labs/ssr-client": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-client/-/ssr-client-1.1.7.tgz",
"integrity": "sha512-VvqhY/iif3FHrlhkzEPsuX/7h/NqnfxLwVf0p8ghNIlKegRyRqgeaJevZ57s/u/LiFyKgqksRP5n+LmNvpxN+A==",
"dependencies": {
"@lit/reactive-element": "^2.0.4",
"lit": "^3.1.2",
"lit-html": "^3.1.2"
}
},
"node_modules/@lit-labs/ssr-dom-shim": { "node_modules/@lit-labs/ssr-dom-shim": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz",
"integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==" "integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ=="
}, },
"node_modules/@lit-labs/ssr/node_modules/@types/node": {
"version": "16.18.126",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw=="
},
"node_modules/@lit/context": { "node_modules/@lit/context": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.5.tgz", "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.5.tgz",
@ -3557,6 +3521,7 @@
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/@parse5/tools/-/tools-0.3.0.tgz", "resolved": "https://registry.npmjs.org/@parse5/tools/-/tools-0.3.0.tgz",
"integrity": "sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==", "integrity": "sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==",
"dev": true,
"dependencies": { "dependencies": {
"parse5": "^7.0.0" "parse5": "^7.0.0"
} }
@ -10723,6 +10688,7 @@
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"dev": true,
"engines": { "engines": {
"node": ">= 12" "node": ">= 12"
} }
@ -11343,6 +11309,7 @@
"version": "5.18.1", "version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
"dev": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.2.4", "graceful-fs": "^4.2.4",
"tapable": "^2.2.0" "tapable": "^2.2.0"
@ -13820,7 +13787,8 @@
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
}, },
"node_modules/grapheme-splitter": { "node_modules/grapheme-splitter": {
"version": "1.0.4", "version": "1.0.4",
@ -18256,6 +18224,7 @@
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"dev": true,
"dependencies": { "dependencies": {
"data-uri-to-buffer": "^4.0.0", "data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4", "fetch-blob": "^3.1.4",
@ -22373,6 +22342,7 @@
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"dev": true,
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
@ -22724,6 +22694,12 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/trusted-types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/trusted-types/-/trusted-types-2.0.0.tgz",
"integrity": "sha512-Eam+AUp6lg04YjmYkuLNhEJX+6ByocrKTpY/TtfRK/gV6OmxeN0OwkIasor28SUJ606snArpPLGtPMGbqdaaUA==",
"license": "W3C-20150513"
},
"node_modules/ts-api-utils": { "node_modules/ts-api-utils": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",

View File

@ -13,7 +13,6 @@
"@formatjs/intl-listformat": "^7.5.7", "@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.2.4-1745325566", "@goauthentik/api": "^2025.2.4-1745325566",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2", "@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4", "@lit/reactive-element": "^2.0.4",
@ -54,6 +53,7 @@
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"remark-mdx-frontmatter": "^5.0.0", "remark-mdx-frontmatter": "^5.0.0",
"style-mod": "^4.1.2", "style-mod": "^4.1.2",
"trusted-types": "^2.0.0",
"ts-pattern": "^5.4.0", "ts-pattern": "^5.4.0",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"webcomponent-qr-code": "^1.2.0", "webcomponent-qr-code": "^1.2.0",

View File

@ -10,6 +10,7 @@ import { configureSentry } from "@goauthentik/common/sentry";
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 { AuthenticatedInterface } from "@goauthentik/elements/Interface"; import { AuthenticatedInterface } from "@goauthentik/elements/Interface";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js";
import "@goauthentik/elements/ak-locale-context"; import "@goauthentik/elements/ak-locale-context";
import "@goauthentik/elements/banner/EnterpriseStatusBanner"; import "@goauthentik/elements/banner/EnterpriseStatusBanner";
import "@goauthentik/elements/banner/EnterpriseStatusBanner"; import "@goauthentik/elements/banner/EnterpriseStatusBanner";
@ -24,25 +25,30 @@ import "@goauthentik/elements/router/RouterOutlet";
import "@goauthentik/elements/sidebar/Sidebar"; import "@goauthentik/elements/sidebar/Sidebar";
import "@goauthentik/elements/sidebar/SidebarItem"; import "@goauthentik/elements/sidebar/SidebarItem";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators.js"; import { customElement, property, query, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js"; import { classMap } from "lit/directives/class-map.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css"; import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
import PFNav from "@patternfly/patternfly/components/Nav/nav.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css"; 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 { SessionUser, UiThemeEnum } from "@goauthentik/api"; import { LicenseSummaryStatusEnum, SessionUser, UiThemeEnum } from "@goauthentik/api";
import "./AdminSidebar"; import {
AdminSidebarEnterpriseEntries,
AdminSidebarEntries,
renderSidebarItems,
} from "./AdminSidebar.js";
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");
} }
@customElement("ak-interface-admin") @customElement("ak-interface-admin")
export class AdminInterface extends AuthenticatedInterface { export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
//#region Properties //#region Properties
@property({ type: Boolean }) @property({ type: Boolean })
@ -59,11 +65,11 @@ export class AdminInterface extends AuthenticatedInterface {
@query("ak-about-modal") @query("ak-about-modal")
aboutModal?: AboutModal; aboutModal?: AboutModal;
@state() @property({ type: Boolean, reflect: true })
sidebarVisible = false; public sidebarOpen = true;
#toggleSidebar = () => { #toggleSidebar = () => {
this.sidebarVisible = !this.sidebarVisible; this.sidebarOpen = !this.sidebarOpen;
}; };
//#endregion //#endregion
@ -76,6 +82,7 @@ export class AdminInterface extends AuthenticatedInterface {
PFPage, PFPage,
PFButton, PFButton,
PFDrawer, PFDrawer,
PFNav,
css` css`
.pf-c-page__main, .pf-c-page__main,
.pf-c-drawer__content, .pf-c-drawer__content,
@ -103,7 +110,7 @@ export class AdminInterface extends AuthenticatedInterface {
grid-area: header; grid-area: header;
} }
ak-admin-sidebar { .ak-sidebar {
grid-area: nav; grid-area: nav;
} }
@ -121,6 +128,12 @@ export class AdminInterface extends AuthenticatedInterface {
constructor() { constructor() {
super(); super();
this.ws = new WebsocketClient(); this.ws = new WebsocketClient();
}
public connectedCallback(): void {
super.connectedCallback();
window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar);
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => { window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => {
this.notificationDrawerOpen = !this.notificationDrawerOpen; this.notificationDrawerOpen = !this.notificationDrawerOpen;
@ -137,6 +150,11 @@ export class AdminInterface extends AuthenticatedInterface {
}); });
} }
public disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar);
}
async firstUpdated(): Promise<void> { async firstUpdated(): Promise<void> {
configureSentry(true); configureSentry(true);
this.user = await me(); this.user = await me();
@ -145,27 +163,18 @@ export class AdminInterface extends AuthenticatedInterface {
this.user.user.isSuperuser || this.user.user.isSuperuser ||
// TODO: somehow add `access_admin_interface` to the API schema // TODO: somehow add `access_admin_interface` to the API schema
this.user.user.systemPermissions.includes("access_admin_interface"); this.user.user.systemPermissions.includes("access_admin_interface");
if (!canAccessAdmin && this.user.user.pk > 0) { if (!canAccessAdmin && this.user.user.pk > 0) {
window.location.assign("/if/user/"); window.location.assign("/if/user/");
} }
} }
async connectedCallback(): Promise<void> {
super.connectedCallback();
window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar);
}
disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar);
}
render(): TemplateResult { render(): TemplateResult {
const sidebarClasses = { const sidebarClasses = {
"pf-c-page__sidebar": true,
"pf-m-light": this.activeTheme === UiThemeEnum.Light, "pf-m-light": this.activeTheme === UiThemeEnum.Light,
"pf-m-expanded": !this.sidebarVisible, "pf-m-expanded": this.sidebarOpen,
"pf-m-collapsed": this.sidebarVisible, "pf-m-collapsed": !this.sidebarOpen,
}; };
const drawerOpen = this.notificationDrawerOpen || this.apiDrawerOpen; const drawerOpen = this.notificationDrawerOpen || this.apiDrawerOpen;
@ -182,10 +191,12 @@ export class AdminInterface extends AuthenticatedInterface {
<ak-enterprise-status interface="admin"></ak-enterprise-status> <ak-enterprise-status interface="admin"></ak-enterprise-status>
</ak-page-navbar> </ak-page-navbar>
<ak-admin-sidebar <ak-sidebar class="${classMap(sidebarClasses)}">
class="pf-c-page__sidebar ${renderSidebarItems(AdminSidebarEntries)}
${classMap(sidebarClasses)}" ${this.licenseSummary?.status !== LicenseSummaryStatusEnum.Unlicensed
></ak-admin-sidebar> ? renderSidebarItems(AdminSidebarEnterpriseEntries)
: nothing}
</ak-sidebar>
<div class="pf-c-page__drawer"> <div class="pf-c-page__drawer">
<div class="pf-c-drawer ${classMap(drawerClasses)}"> <div class="pf-c-drawer ${classMap(drawerClasses)}">

View File

@ -1,165 +1,98 @@
import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import { WithVersion } from "@goauthentik/elements/Interface/versionProvider";
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route"; import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
import { getRootStyle } from "@goauthentik/elements/utils/getRootStyle";
import { spread } from "@open-wc/lit-helpers"; import { spread } from "@open-wc/lit-helpers";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { TemplateResult, html, nothing } from "lit"; import { TemplateResult, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { repeat } from "lit/directives/repeat.js";
import { map } from "lit/directives/map.js";
import { UiThemeEnum } from "@goauthentik/api"; // The second attribute type is of string[] to help with the 'activeWhen' control, which was
import type { SessionUser, UserSelf } from "@goauthentik/api"; // commonplace and singular enough to merit its own handler.
type SidebarEntry = [
path: string | null,
label: string,
attributes?: Record<string, any> | string[] | null, // eslint-disable-line
children?: SidebarEntry[],
];
@customElement("ak-admin-sidebar") /**
export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement)) { * Recursively renders a sidebar entry.
@property({ type: Boolean, reflect: true }) */
open = true; export function renderSidebarItem([
path,
label,
attributes,
children,
]: SidebarEntry): TemplateResult {
const properties = Array.isArray(attributes)
? { ".activeWhen": attributes }
: (attributes ?? {});
@state() if (path) {
impersonation: UserSelf["username"] | null = null; properties.path = path;
constructor() {
super();
me().then((user: SessionUser) => {
this.impersonation = user.original ? user.user.username : null;
});
this.checkWidth = this.checkWidth.bind(this);
} }
checkWidth() { return html`<ak-sidebar-item ${spread(properties)}>
// This works just fine, but it assumes that the `--ak-sidebar--minimum-auto-width` is in ${label ? html`<span slot="label">${label}</span>` : nothing}
// REMs. If that changes, this code will have to be adjusted as well. ${children ? renderSidebarItems(children) : nothing}
const minWidth = </ak-sidebar-item>`;
parseFloat(getRootStyle("--ak-sidebar--minimum-auto-width")) *
parseFloat(getRootStyle("font-size"));
this.open = window.innerWidth >= minWidth;
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("resize", this.checkWidth);
// After connecting to the DOM, we can now perform this check to see if the sidebar should
// be open by default.
this.checkWidth();
}
// The symmetry (☟, ☝) here is critical in that you want to start adding these handlers after
// connection, and removing them before disconnection.
disconnectedCallback() {
window.removeEventListener("resize", this.checkWidth);
super.disconnectedCallback();
}
render() {
return html`
<ak-sidebar
class="pf-c-page__sidebar
${this.open ? "pf-m-expanded" : "pf-m-collapsed"} ${this.activeTheme ===
UiThemeEnum.Light
? "pf-m-light"
: ""}"
>
${this.renderSidebarItems()}
</ak-sidebar>
`;
}
renderSidebarItems(): TemplateResult {
// The second attribute type is of string[] to help with the 'activeWhen' control, which was
// commonplace and singular enough to merit its own handler.
type SidebarEntry = [
path: string | null,
label: string,
attributes?: Record<string, any> | string[] | null, // eslint-disable-line
children?: SidebarEntry[],
];
// prettier-ignore
const sidebarContent: SidebarEntry[] = [
[null, msg("Dashboards"), { "?expanded": true }, [
["/administration/overview", msg("Overview")],
["/administration/dashboard/users", msg("User Statistics")],
["/administration/system-tasks", msg("System Tasks")]]],
[null, msg("Applications"), null, [
["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]],
["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]],
["/outpost/outposts", msg("Outposts")]]],
[null, msg("Events"), null, [
["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]],
["/events/rules", msg("Notification Rules")],
["/events/transports", msg("Notification Transports")]]],
[null, msg("Customization"), null, [
["/policy/policies", msg("Policies")],
["/core/property-mappings", msg("Property Mappings")],
["/blueprints/instances", msg("Blueprints")],
["/policy/reputation", msg("Reputation scores")]]],
[null, msg("Flows and Stages"), null, [
["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]],
["/flow/stages", msg("Stages")],
["/flow/stages/prompts", msg("Prompts")]]],
[null, msg("Directory"), null, [
["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
["/identity/roles", msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]],
["/identity/initial-permissions", msg("Initial Permissions"), [`^/identity/initial-permissions/(?<id>${ID_REGEX})$`]],
["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]],
["/core/tokens", msg("Tokens and App passwords")],
["/flow/stages/invitations", msg("Invitations")]]],
[null, msg("System"), null, [
["/core/brands", msg("Brands")],
["/crypto/certificates", msg("Certificates")],
["/outpost/integrations", msg("Outpost Integrations")],
["/admin/settings", msg("Settings")]]],
];
// Typescript requires the type here to correctly type the recursive path
type SidebarRenderer = (_: SidebarEntry) => TemplateResult;
const renderOneSidebarItem: SidebarRenderer = ([path, label, attributes, children]) => {
const properties = Array.isArray(attributes)
? { ".activeWhen": attributes }
: (attributes ?? {});
if (path) {
properties.path = path;
}
return html`<ak-sidebar-item ${spread(properties)}>
${label ? html`<span slot="label">${label}</span>` : nothing}
${map(children, renderOneSidebarItem)}
</ak-sidebar-item>`;
};
// prettier-ignore
return html`
${map(sidebarContent, renderOneSidebarItem)}
${this.renderEnterpriseMenu()}
`;
}
renderEnterpriseMenu() {
return this.can(CapabilitiesEnum.IsEnterprise)
? html`
<ak-sidebar-item>
<span slot="label">${msg("Enterprise")}</span>
<ak-sidebar-item path="/enterprise/licenses">
<span slot="label">${msg("Licenses")}</span>
</ak-sidebar-item>
</ak-sidebar-item>
`
: nothing;
}
} }
declare global { /**
interface HTMLElementTagNameMap { * Recursively renders a collection of sidebar entries.
"ak-admin-sidebar": AkAdminSidebar; */
} export function renderSidebarItems(entries: readonly SidebarEntry[]) {
console.debug("authentik/sidebar: Rendering sidebar items", entries);
return repeat(entries, ([path, label]) => path || label, renderSidebarItem);
} }
// prettier-ignore
export const AdminSidebarEntries: readonly SidebarEntry[] = [
[null, msg("Dashboards"), { "?expanded": true }, [
["/administration/overview", msg("Overview")],
["/administration/dashboard/users", msg("User Statistics")],
["/administration/system-tasks", msg("System Tasks")]]
],
[null, msg("Applications"), null, [
["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]],
["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]],
["/outpost/outposts", msg("Outposts")]]
],
[null, msg("Events"), null, [
["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]],
["/events/rules", msg("Notification Rules")],
["/events/transports", msg("Notification Transports")]]
],
[null, msg("Customization"), null, [
["/policy/policies", msg("Policies")],
["/core/property-mappings", msg("Property Mappings")],
["/blueprints/instances", msg("Blueprints")],
["/policy/reputation", msg("Reputation scores")]]
],
[null, msg("Flows and Stages"), null, [
["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]],
["/flow/stages", msg("Stages")],
["/flow/stages/prompts", msg("Prompts")]]
],
[null, msg("Directory"), null, [
["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
["/identity/roles", msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]],
["/identity/initial-permissions", msg("Initial Permissions"), [`^/identity/initial-permissions/(?<id>${ID_REGEX})$`]],
["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]],
["/core/tokens", msg("Tokens and App passwords")],
["/flow/stages/invitations", msg("Invitations")]]
],
[null, msg("System"), null, [
["/core/brands", msg("Brands")],
["/crypto/certificates", msg("Certificates")],
["/outpost/integrations", msg("Outpost Integrations")],
["/admin/settings", msg("Settings")]]
],
];
// prettier-ignore
export const AdminSidebarEnterpriseEntries: readonly SidebarEntry[] = [
[null, msg("Enterprise"), null, [
["/enterprise/licenses", msg("Licenses"), null]
],
]]

View File

@ -58,9 +58,6 @@ export class AdminOverviewPage extends AdminOverviewBase {
PFContent, PFContent,
PFDivider, PFDivider,
css` css`
.pf-l-grid__item {
height: 100%;
}
.pf-l-grid__item.big-graph-container { .pf-l-grid__item.big-graph-container {
height: 35em; height: 35em;
} }
@ -74,6 +71,10 @@ export class AdminOverviewPage extends AdminOverviewBase {
line-height: normal; line-height: normal;
font-size: var(--pf-global--icon--FontSize--sm); font-size: var(--pf-global--icon--FontSize--sm);
} }
.chart-item {
aspect-ratio: 2 / 1;
}
`, `,
]; ];
} }
@ -104,15 +105,21 @@ export class AdminOverviewPage extends AdminOverviewBase {
</ak-page-header> </ak-page-header>
<section class="pf-c-page__main-section"> <section class="pf-c-page__main-section">
<div class="pf-l-grid pf-m-gutter"> <div class="pf-l-grid pf-m-gutter">
<!-- row 1 --> <div class="pf-l-grid__item pf-m-12-col pf-m-2-row pf-m-9-col-on-xl">
<div <ak-recent-events pageSize="6"></ak-recent-events>
class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl pf-l-grid pf-m-gutter" </div>
> <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-sm pf-m-3-col-on-xl">
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl"> <ak-quick-actions-card .actions=${this.quickActions}>
<ak-quick-actions-card .actions=${this.quickActions}> </ak-quick-actions-card>
</ak-quick-actions-card> </div>
</div>
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl"> <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-sm pf-m-3-col-on-xl">
<ak-admin-status-version> </ak-admin-status-version>
</div>
<div class="pf-l-grid pf-l-grid__item pf-m-12-col pf-m-gutter">
${this.renderSecondaryRow()}
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-md chart-item">
<ak-aggregate-card <ak-aggregate-card
icon="pf-icon pf-icon-zone" icon="pf-icon pf-icon-zone"
header=${msg("Outpost status")} header=${msg("Outpost status")}
@ -121,24 +128,13 @@ export class AdminOverviewPage extends AdminOverviewBase {
<ak-admin-status-chart-outpost></ak-admin-status-chart-outpost> <ak-admin-status-chart-outpost></ak-admin-status-chart-outpost>
</ak-aggregate-card> </ak-aggregate-card>
</div> </div>
<div <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-md chart-item">
class="pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-4-col-on-2xl"
>
<ak-aggregate-card icon="fa fa-sync-alt" header=${msg("Sync status")}> <ak-aggregate-card icon="fa fa-sync-alt" header=${msg("Sync status")}>
<ak-admin-status-chart-sync></ak-admin-status-chart-sync> <ak-admin-status-chart-sync></ak-admin-status-chart-sync>
</ak-aggregate-card> </ak-aggregate-card>
</div> </div>
<div class="pf-l-grid__item pf-m-12-col">
<hr class="pf-c-divider" />
</div>
${this.renderCards()}
</div>
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl">
<ak-recent-events pageSize="6"></ak-recent-events>
</div>
<div class="pf-l-grid__item pf-m-12-col">
<hr class="pf-c-divider" />
</div> </div>
<!-- row 3 --> <!-- row 3 -->
<div <div
class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-8-col-on-2xl big-graph-container" class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-8-col-on-2xl big-graph-container"
@ -166,32 +162,34 @@ export class AdminOverviewPage extends AdminOverviewBase {
</section>`; </section>`;
} }
renderCards() { renderSecondaryRow() {
const isEnterprise = this.hasEnterpriseLicense; const isEnterprise = this.hasEnterpriseLicense;
const colSpan = isEnterprise ? 4 : 6;
const classes = { const classes = {
"card-container": true, "card-container": true,
"pf-l-grid__item": true, "pf-l-grid__item": true,
"pf-m-6-col": true, [`pf-m-12-col`]: true,
"pf-m-4-col-on-md": !isEnterprise, [`pf-m-${colSpan}-col-on-md`]: true,
"pf-m-4-col-on-xl": !isEnterprise,
"pf-m-3-col-on-md": isEnterprise,
"pf-m-3-col-on-xl": isEnterprise,
}; };
return html`<div class=${classMap(classes)}> return html`
<div class=${classMap(classes)}>
<ak-admin-status-system> </ak-admin-status-system> <ak-admin-status-system> </ak-admin-status-system>
</div> </div>
<div class=${classMap(classes)}>
<ak-admin-status-version> </ak-admin-status-version>
</div>
<div class=${classMap(classes)}> <div class=${classMap(classes)}>
<ak-admin-status-card-workers> </ak-admin-status-card-workers> <ak-admin-status-card-workers> </ak-admin-status-card-workers>
</div> </div>
${isEnterprise ${isEnterprise
? html` <div class=${classMap(classes)}> ? html`
<ak-admin-fips-status-system> </ak-admin-fips-status-system> <div class=${classMap(classes)}>
</div>` <ak-admin-fips-status-system> </ak-admin-fips-status-system>
: nothing} `; </div>
`
: nothing}
`;
} }
renderActions() { renderActions() {

View File

@ -1,26 +1,110 @@
import type { Config as DOMPurifyConfig } from "dompurify"; import type { Config as DOMPurifyConfig } from "dompurify";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { trustedTypes } from "trusted-types";
import { render } from "@lit-labs/ssr"; import { render } from "lit";
import { collectResult } from "@lit-labs/ssr/lib/render-result.js";
import { TemplateResult, html } from "lit";
import { unsafeHTML } from "lit/directives/unsafe-html.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { until } from "lit/directives/until.js";
/**
* Trusted types policy that escapes HTML content in place.
*
* @see {@linkcode SanitizedTrustPolicy} to strip HTML content.
*
* @returns {TrustedHTML} All HTML content, escaped.
*/
export const EscapeTrustPolicy = trustedTypes.createPolicy("authentik-escape", {
createHTML: (untrustedHTML: string) => {
return DOMPurify.sanitize(untrustedHTML, {
RETURN_TRUSTED_TYPE: false,
});
},
});
/**
* Trusted types policy, stripping all HTML content.
*
* @returns {TrustedHTML} Text content only, all HTML tags stripped.
*/
export const SanitizedTrustPolicy = trustedTypes.createPolicy("authentik-sanitize", {
createHTML: (untrustedHTML: string) => {
return DOMPurify.sanitize(untrustedHTML, {
RETURN_TRUSTED_TYPE: false,
ALLOWED_TAGS: ["#text"],
});
},
});
/**
* Trusted types policy, allowing a minimal set of _safe_ HTML tags supplied by
* a trusted source, such as the brand API.
*/
export const BrandedHTMLPolicy = trustedTypes.createPolicy("authentik-restrict", {
createHTML: (untrustedHTML: string) => {
return DOMPurify.sanitize(untrustedHTML, {
RETURN_TRUSTED_TYPE: false,
FORBID_TAGS: [
"script",
"style",
"iframe",
"link",
"object",
"embed",
"applet",
"meta",
"base",
"form",
"input",
"textarea",
"select",
"button",
],
FORBID_ATTR: [
"onerror",
"onclick",
"onload",
"onmouseover",
"onmouseout",
"onmouseup",
"onmousedown",
"onfocus",
"onblur",
"onsubmit",
],
});
},
});
export type AuthentikTrustPolicy =
| typeof EscapeTrustPolicy
| typeof SanitizedTrustPolicy
| typeof BrandedHTMLPolicy;
/**
* Sanitize an untrusted HTML string using a trusted types policy.
*/
export function sanitizeHTML(trustPolicy: AuthentikTrustPolicy, untrustedHTML: string) {
return unsafeHTML(trustPolicy.createHTML(untrustedHTML).toString());
}
/**
* DOMPurify configuration for strict sanitization.
*
* This configuration only allows text nodes and disallows all HTML tags.
*/
export const DOM_PURIFY_STRICT = { export const DOM_PURIFY_STRICT = {
ALLOWED_TAGS: ["#text"], ALLOWED_TAGS: ["#text"],
} as const satisfies DOMPurifyConfig; } as const satisfies DOMPurifyConfig;
export async function renderStatic(input: TemplateResult): Promise<string> { /**
return await collectResult(render(input)); * Render untrusted HTML to a string without escaping it.
} *
* @returns {string} The rendered HTML string.
*/
export function renderStaticHTMLUnsafe(untrustedHTML: unknown): string {
const container = document.createElement("html");
render(untrustedHTML, container);
export function purify(input: TemplateResult): TemplateResult { const result = container.innerHTML;
return html`${until(
(async () => { return result;
const rendered = await renderStatic(input);
const purified = DOMPurify.sanitize(rendered);
return html`${unsafeHTML(purified)}`;
})(),
)}`;
} }

View File

@ -5,7 +5,7 @@ import {
} from "@goauthentik/common/constants"; } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { currentInterface } from "@goauthentik/common/sentry"; import { currentInterface } from "@goauthentik/common/sentry";
import { UIConfig, UserDisplay, uiConfig } from "@goauthentik/common/ui/config"; import { UIConfig, UserDisplay, getConfigForUser } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import "@goauthentik/components/ak-nav-buttons"; import "@goauthentik/components/ak-nav-buttons";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
@ -267,15 +267,28 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb
open = true; open = true;
@state() @state()
me?: SessionUser; session?: SessionUser;
@state() @state()
uiConfig!: UIConfig; uiConfig!: UIConfig;
//#endregion //#endregion
//#endregion //#region Private Methods
//#region Methods
#setTitle(header?: string) {
const currentIf = currentInterface();
let title = this.brand?.brandingTitle || TITLE_DEFAULT;
if (currentIf === "admin") {
title = `${msg("Admin")} - ${title}`;
}
// Prepend the header to the title
if (header) {
title = `${header} - ${title}`;
}
document.title = title;
}
#toggleSidebar() { #toggleSidebar() {
this.open = !this.open; this.open = !this.open;
@ -288,50 +301,38 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb
); );
} }
//#region Constructor //#endregion
//#region Lifecycle
public connectedCallback(): void {
super.connectedCallback();
AKPageNavbar.elementRef = this;
constructor() {
super();
window.addEventListener(EVENT_WS_MESSAGE, () => { window.addEventListener(EVENT_WS_MESSAGE, () => {
this.firstUpdated(); this.firstUpdated();
}); });
} }
connectedCallback(): void { public disconnectedCallback(): void {
super.connectedCallback();
AKPageNavbar.elementRef = this;
}
disconnectedCallback(): void {
super.disconnectedCallback(); super.disconnectedCallback();
AKPageNavbar.elementRef = null; AKPageNavbar.elementRef = null;
} }
public async firstUpdated() { public async firstUpdated() {
this.me = await me(); this.session = await me();
this.uiConfig = await uiConfig(); this.uiConfig = getConfigForUser(this.session.user);
this.uiConfig.navbar.userDisplay = UserDisplay.none; this.uiConfig.navbar.userDisplay = UserDisplay.none;
} }
#setTitle(header?: string) {
const currentIf = currentInterface();
let title = this.brand?.brandingTitle || TITLE_DEFAULT;
if (currentIf === "admin") {
title = `${msg("Admin")} - ${title}`;
}
// Prepend the header to the title
if (header !== undefined && header !== "") {
title = `${header} - ${title}`;
}
document.title = title;
}
willUpdate() { willUpdate() {
// Always update title, even if there's no header value set, // Always update title, even if there's no header value set,
// as in that case we still need to return to the generic title // as in that case we still need to return to the generic title
this.#setTitle(this.header); this.#setTitle(this.header);
} }
//#endregion
//#region Render //#region Render
renderIcon() { renderIcon() {
@ -389,7 +390,7 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb
<section class="items secondary"> <section class="items secondary">
<div class="pf-c-page__header-tools-group"> <div class="pf-c-page__header-tools-group">
<ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.me}> <ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.session}>
<a <a
class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md" class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md"
href="${globalAK().api.base}if/user/" href="${globalAK().api.base}if/user/"

View File

@ -80,6 +80,7 @@ export class AggregateCard extends AKElement implements IAggregateCard {
.center-value { .center-value {
font-size: var(--pf-global--icon--FontSize--lg); font-size: var(--pf-global--icon--FontSize--lg);
text-align: center; text-align: center;
place-content: center;
} }
.subtext { .subtext {
margin-top: var(--pf-global--spacer--sm); margin-top: var(--pf-global--spacer--sm);

View File

@ -1,16 +1,3 @@
import { AKElement } from "@goauthentik/elements/Base";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { themeImage } from "@goauthentik/elements/utils/images";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGlobal from "@patternfly/patternfly/patternfly-base.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { CurrentBrand, UiThemeEnum } from "@goauthentik/api"; import { CurrentBrand, UiThemeEnum } from "@goauthentik/api";
// If the viewport is wider than MIN_WIDTH, the sidebar // If the viewport is wider than MIN_WIDTH, the sidebar
@ -27,56 +14,3 @@ export const DefaultBrand: CurrentBrand = {
matchedDomain: "", matchedDomain: "",
defaultLocale: "", defaultLocale: "",
}; };
@customElement("ak-sidebar-brand")
export class SidebarBrand extends WithBrandConfig(AKElement) {
static get styles(): CSSResult[] {
return [
PFBase,
PFGlobal,
PFPage,
PFButton,
css`
:host {
display: flex;
flex-direction: row;
align-items: center;
height: var(--ak-navbar-height);
border-bottom: var(--pf-global--BorderWidth--sm);
border-bottom-style: solid;
border-bottom-color: var(--pf-global--BorderColor--100);
}
.pf-c-brand img {
padding: 0 0.5rem;
height: 42px;
}
`,
];
}
constructor() {
super();
window.addEventListener("resize", () => {
this.requestUpdate();
});
}
render(): TemplateResult {
return html` <a href="#/" class="pf-c-page__header-brand-link">
<div class="pf-c-brand ak-brand">
<img
src=${themeImage(this.brand?.brandingLogo ?? DefaultBrand.brandingLogo)}
alt="${msg("authentik Logo")}"
loading="lazy"
/>
</div>
</a>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-sidebar-brand": SidebarBrand;
}
}

View File

@ -0,0 +1,55 @@
/**
* @file IFrame Utilities
*/
interface IFrameLoadResult {
contentWindow: Window;
contentDocument: Document;
}
export function pluckIFrameContent(iframe: HTMLIFrameElement) {
const contentWindow = iframe.contentWindow;
const contentDocument = iframe.contentDocument;
if (!contentWindow) {
throw new Error("Iframe contentWindow is not accessible");
}
if (!contentDocument) {
throw new Error("Iframe contentDocument is not accessible");
}
return {
contentWindow,
contentDocument,
};
}
export function resolveIFrameContent(iframe: HTMLIFrameElement): Promise<IFrameLoadResult> {
if (iframe.contentDocument?.readyState === "complete") {
return Promise.resolve(pluckIFrameContent(iframe));
}
return new Promise((resolve) => {
iframe.addEventListener("load", () => resolve(pluckIFrameContent(iframe)), { once: true });
});
}
/**
* Creates a minimal HTML wrapper for an iframe.
*
* @deprecated Use the `contentDocument.body` directly instead.
*/
export function createIFrameHTMLWrapper(bodyContent: string): string {
const html = String.raw;
return html`<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body style="display:flex;flex-direction:row;justify-content:center;">
${bodyContent}
</body>
</html>`;
}

View File

@ -1,4 +1,4 @@
import { purify } from "@goauthentik/common/purify"; import { BrandedHTMLPolicy, sanitizeHTML } from "@goauthentik/common/purify";
import { AKElement } from "@goauthentik/elements/Base.js"; import { AKElement } from "@goauthentik/elements/Base.js";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
@ -21,8 +21,6 @@ const styles = css`
} }
`; `;
const poweredBy: FooterLink = { name: msg("Powered by authentik"), href: null };
@customElement("ak-brand-links") @customElement("ak-brand-links")
export class BrandLinks extends AKElement { export class BrandLinks extends AKElement {
static get styles() { static get styles() {
@ -33,13 +31,21 @@ export class BrandLinks extends AKElement {
links: FooterLink[] = []; links: FooterLink[] = [];
render() { render() {
const links = [...(this.links ?? []), poweredBy]; const links = [...(this.links ?? [])];
return html` <ul class="pf-c-list pf-m-inline"> return html` <ul class="pf-c-list pf-m-inline">
${map(links, (link) => ${map(links, (link) => {
link.href const children = sanitizeHTML(BrandedHTMLPolicy, link.name);
? purify(html`<li><a href="${link.href}">${link.name}</a></li>`)
: html`<li><span>${link.name}</span></li>`, if (link.href) {
)} return html`<li><a href="${link.href}">${children}</a></li>`;
}
return html`<li>
<span> ${children} </span>
</li>`;
})}
<li><span>${msg("Powered by authentik")}</span></li>
</ul>`; </ul>`;
} }
} }

View File

@ -1,15 +1,16 @@
///<reference types="@hcaptcha/types"/> /// <reference types="@hcaptcha/types"/>
import { renderStatic } from "@goauthentik/common/purify"; /// <reference types="turnstile-types"/>
import { renderStaticHTMLUnsafe } from "@goauthentik/common/purify";
import "@goauthentik/elements/EmptyState"; import "@goauthentik/elements/EmptyState";
import { akEmptyState } from "@goauthentik/elements/EmptyState"; import { akEmptyState } from "@goauthentik/elements/EmptyState";
import { bound } from "@goauthentik/elements/decorators/bound"; import { bound } from "@goauthentik/elements/decorators/bound";
import "@goauthentik/elements/forms/FormElement"; import "@goauthentik/elements/forms/FormElement";
import { createIFrameHTMLWrapper } from "@goauthentik/elements/utils/iframe";
import { ListenerController } from "@goauthentik/elements/utils/listenerController.js"; import { ListenerController } from "@goauthentik/elements/utils/listenerController.js";
import { randomId } from "@goauthentik/elements/utils/randomId"; import { randomId } from "@goauthentik/elements/utils/randomId";
import "@goauthentik/flow/FormStatic"; import "@goauthentik/flow/FormStatic";
import { BaseStage } from "@goauthentik/flow/stages/base"; import { BaseStage } from "@goauthentik/flow/stages/base";
import { P, match } from "ts-pattern"; import { P, match } from "ts-pattern";
import type * as _ from "turnstile-types";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit"; import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
@ -56,40 +57,36 @@ type CaptchaHandler = {
// a resize. Because the Captcha is itself in an iframe, the reported height is often off by some // a resize. Because the Captcha is itself in an iframe, the reported height is often off by some
// margin, so adding 2rem of height to our container adds padding and prevents scroll bars or hidden // margin, so adding 2rem of height to our container adds padding and prevents scroll bars or hidden
// rendering. // rendering.
function iframeTemplate(children: TemplateResult, challengeURL: string): TemplateResult {
return html` ${children}
<script>
new ResizeObserver((entries) => {
const height =
document.body.offsetHeight +
parseFloat(getComputedStyle(document.body).fontSize) * 2;
const iframeTemplate = (captchaElement: TemplateResult, challengeUrl: string) => window.parent.postMessage({
html`<!doctype html> message: "resize",
<head> source: "goauthentik.io",
<html> context: "flow-executor",
<body style="display:flex;flex-direction:row;justify-content:center;"> size: { height },
${captchaElement} });
<script> }).observe(document.querySelector(".ak-captcha-container"));
new ResizeObserver((entries) => { </script>
const height =
document.body.offsetHeight + <script src=${challengeURL}></script>
parseFloat(getComputedStyle(document.body).fontSize) * 2;
window.parent.postMessage({ <script>
message: "resize", function callback(token) {
source: "goauthentik.io", window.parent.postMessage({
context: "flow-executor", message: "captcha",
size: { height }, source: "goauthentik.io",
}); context: "flow-executor",
}).observe(document.querySelector(".ak-captcha-container")); token,
</script> });
<script src=${challengeUrl}></script> }
<script> </script>`;
function callback(token) { }
window.parent.postMessage({
message: "captcha",
source: "goauthentik.io",
context: "flow-executor",
token: token,
});
}
</script>
</body>
</html>
</head>`;
@customElement("ak-stage-captcha") @customElement("ak-stage-captcha")
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> { export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
@ -305,11 +302,25 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
} }
async renderFrame(captchaElement: TemplateResult) { async renderFrame(captchaElement: TemplateResult) {
this.captchaFrame.contentWindow?.document.open(); const { contentDocument } = this.captchaFrame || {};
this.captchaFrame.contentWindow?.document.write(
await renderStatic(iframeTemplate(captchaElement, this.challenge.jsUrl)), if (!contentDocument) {
console.debug(
"authentik/stages/captcha: unable to render captcha frame, no contentDocument",
);
return;
}
contentDocument.open();
contentDocument.write(
createIFrameHTMLWrapper(
renderStaticHTMLUnsafe(iframeTemplate(captchaElement, this.challenge.jsUrl)),
),
); );
this.captchaFrame.contentWindow?.document.close();
contentDocument.close();
} }
renderBody() { renderBody() {