Compare commits
	
		
			4 Commits
		
	
	
		
			dependabot
			...
			safari-adm
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b19271bb03 | |||
| 88f112db87 | |||
| a1de44cd07 | |||
| 8869df4b1d | 
							
								
								
									
										52
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										52
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -25,7 +25,6 @@
 | 
			
		||||
                "@formatjs/intl-listformat": "^7.5.7",
 | 
			
		||||
                "@fortawesome/fontawesome-free": "^6.6.0",
 | 
			
		||||
                "@goauthentik/api": "^2025.2.4-1745325566",
 | 
			
		||||
                "@lit-labs/ssr": "^3.2.2",
 | 
			
		||||
                "@lit/context": "^1.1.2",
 | 
			
		||||
                "@lit/localize": "^0.12.2",
 | 
			
		||||
                "@lit/reactive-element": "^2.0.4",
 | 
			
		||||
@ -66,6 +65,7 @@
 | 
			
		||||
                "remark-gfm": "^4.0.1",
 | 
			
		||||
                "remark-mdx-frontmatter": "^5.0.0",
 | 
			
		||||
                "style-mod": "^4.1.2",
 | 
			
		||||
                "trusted-types": "^2.0.0",
 | 
			
		||||
                "ts-pattern": "^5.4.0",
 | 
			
		||||
                "unist-util-visit": "^5.0.0",
 | 
			
		||||
                "webcomponent-qr-code": "^1.2.0",
 | 
			
		||||
@ -2281,47 +2281,11 @@
 | 
			
		||||
                "@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": {
 | 
			
		||||
            "version": "1.3.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz",
 | 
			
		||||
            "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": {
 | 
			
		||||
            "version": "1.1.5",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.5.tgz",
 | 
			
		||||
@ -3557,6 +3521,7 @@
 | 
			
		||||
            "version": "0.3.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@parse5/tools/-/tools-0.3.0.tgz",
 | 
			
		||||
            "integrity": "sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "parse5": "^7.0.0"
 | 
			
		||||
            }
 | 
			
		||||
@ -10723,6 +10688,7 @@
 | 
			
		||||
            "version": "4.0.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
 | 
			
		||||
            "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "engines": {
 | 
			
		||||
                "node": ">= 12"
 | 
			
		||||
            }
 | 
			
		||||
@ -11343,6 +11309,7 @@
 | 
			
		||||
            "version": "5.18.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
 | 
			
		||||
            "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "graceful-fs": "^4.2.4",
 | 
			
		||||
                "tapable": "^2.2.0"
 | 
			
		||||
@ -13820,7 +13787,8 @@
 | 
			
		||||
        "node_modules/graceful-fs": {
 | 
			
		||||
            "version": "4.2.11",
 | 
			
		||||
            "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": {
 | 
			
		||||
            "version": "1.0.4",
 | 
			
		||||
@ -18256,6 +18224,7 @@
 | 
			
		||||
            "version": "3.3.2",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
 | 
			
		||||
            "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "data-uri-to-buffer": "^4.0.0",
 | 
			
		||||
                "fetch-blob": "^3.1.4",
 | 
			
		||||
@ -22373,6 +22342,7 @@
 | 
			
		||||
            "version": "2.2.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
 | 
			
		||||
            "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "engines": {
 | 
			
		||||
                "node": ">=6"
 | 
			
		||||
            }
 | 
			
		||||
@ -22724,6 +22694,12 @@
 | 
			
		||||
                "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": {
 | 
			
		||||
            "version": "2.1.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,6 @@
 | 
			
		||||
        "@formatjs/intl-listformat": "^7.5.7",
 | 
			
		||||
        "@fortawesome/fontawesome-free": "^6.6.0",
 | 
			
		||||
        "@goauthentik/api": "^2025.2.4-1745325566",
 | 
			
		||||
        "@lit-labs/ssr": "^3.2.2",
 | 
			
		||||
        "@lit/context": "^1.1.2",
 | 
			
		||||
        "@lit/localize": "^0.12.2",
 | 
			
		||||
        "@lit/reactive-element": "^2.0.4",
 | 
			
		||||
@ -54,6 +53,7 @@
 | 
			
		||||
        "remark-gfm": "^4.0.1",
 | 
			
		||||
        "remark-mdx-frontmatter": "^5.0.0",
 | 
			
		||||
        "style-mod": "^4.1.2",
 | 
			
		||||
        "trusted-types": "^2.0.0",
 | 
			
		||||
        "ts-pattern": "^5.4.0",
 | 
			
		||||
        "unist-util-visit": "^5.0.0",
 | 
			
		||||
        "webcomponent-qr-code": "^1.2.0",
 | 
			
		||||
 | 
			
		||||
@ -4,13 +4,17 @@ import { ROUTES } from "@goauthentik/admin/Routes";
 | 
			
		||||
import {
 | 
			
		||||
    EVENT_API_DRAWER_TOGGLE,
 | 
			
		||||
    EVENT_NOTIFICATION_DRAWER_TOGGLE,
 | 
			
		||||
    EVENT_SIDEBAR_TOGGLE,
 | 
			
		||||
} from "@goauthentik/common/constants";
 | 
			
		||||
import { configureSentry } from "@goauthentik/common/sentry";
 | 
			
		||||
import { me } from "@goauthentik/common/users";
 | 
			
		||||
import { WebsocketClient } from "@goauthentik/common/ws";
 | 
			
		||||
import { AuthenticatedInterface } from "@goauthentik/elements/Interface";
 | 
			
		||||
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js";
 | 
			
		||||
import "@goauthentik/elements/ak-locale-context";
 | 
			
		||||
import "@goauthentik/elements/banner/EnterpriseStatusBanner";
 | 
			
		||||
import "@goauthentik/elements/banner/EnterpriseStatusBanner";
 | 
			
		||||
import "@goauthentik/elements/banner/VersionBanner";
 | 
			
		||||
import "@goauthentik/elements/banner/VersionBanner";
 | 
			
		||||
import "@goauthentik/elements/messages/MessageContainer";
 | 
			
		||||
import "@goauthentik/elements/messages/MessageContainer";
 | 
			
		||||
@ -21,25 +25,32 @@ import "@goauthentik/elements/router/RouterOutlet";
 | 
			
		||||
import "@goauthentik/elements/sidebar/Sidebar";
 | 
			
		||||
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 { classMap } from "lit/directives/class-map.js";
 | 
			
		||||
 | 
			
		||||
import PFButton from "@patternfly/patternfly/components/Button/button.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 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") {
 | 
			
		||||
    await import("@goauthentik/esbuild-plugin-live-reload/client");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@customElement("ak-interface-admin")
 | 
			
		||||
export class AdminInterface extends AuthenticatedInterface {
 | 
			
		||||
export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
 | 
			
		||||
    //#region Properties
 | 
			
		||||
 | 
			
		||||
    @property({ type: Boolean })
 | 
			
		||||
    notificationDrawerOpen = getURLParam("notificationDrawerOpen", false);
 | 
			
		||||
 | 
			
		||||
@ -54,12 +65,24 @@ export class AdminInterface extends AuthenticatedInterface {
 | 
			
		||||
    @query("ak-about-modal")
 | 
			
		||||
    aboutModal?: AboutModal;
 | 
			
		||||
 | 
			
		||||
    @property({ type: Boolean, reflect: true })
 | 
			
		||||
    public sidebarOpen = true;
 | 
			
		||||
 | 
			
		||||
    #toggleSidebar = () => {
 | 
			
		||||
        this.sidebarOpen = !this.sidebarOpen;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    //#endregion
 | 
			
		||||
 | 
			
		||||
    //#region Styles
 | 
			
		||||
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [
 | 
			
		||||
            PFBase,
 | 
			
		||||
            PFPage,
 | 
			
		||||
            PFButton,
 | 
			
		||||
            PFDrawer,
 | 
			
		||||
            PFNav,
 | 
			
		||||
            css`
 | 
			
		||||
                .pf-c-page__main,
 | 
			
		||||
                .pf-c-drawer__content,
 | 
			
		||||
@ -67,23 +90,30 @@ export class AdminInterface extends AuthenticatedInterface {
 | 
			
		||||
                    z-index: auto !important;
 | 
			
		||||
                    background-color: transparent;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .display-none {
 | 
			
		||||
                    display: none;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .pf-c-page {
 | 
			
		||||
                    background-color: var(--pf-c-page--BackgroundColor) !important;
 | 
			
		||||
                }
 | 
			
		||||
                /* Global page background colour */
 | 
			
		||||
                :host([theme="dark"]) .pf-c-page {
 | 
			
		||||
                    --pf-c-page--BackgroundColor: var(--ak-dark-background);
 | 
			
		||||
 | 
			
		||||
                :host([theme="dark"]) {
 | 
			
		||||
                    /* Global page background colour */
 | 
			
		||||
                    .pf-c-page {
 | 
			
		||||
                        --pf-c-page--BackgroundColor: var(--ak-dark-background);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                ak-enterprise-status,
 | 
			
		||||
                ak-version-banner {
 | 
			
		||||
 | 
			
		||||
                ak-page-navbar {
 | 
			
		||||
                    grid-area: header;
 | 
			
		||||
                }
 | 
			
		||||
                ak-admin-sidebar {
 | 
			
		||||
 | 
			
		||||
                .ak-sidebar {
 | 
			
		||||
                    grid-area: nav;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .pf-c-drawer__panel {
 | 
			
		||||
                    z-index: var(--pf-global--ZIndex--xl);
 | 
			
		||||
                }
 | 
			
		||||
@ -91,9 +121,19 @@ export class AdminInterface extends AuthenticatedInterface {
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //#endregion
 | 
			
		||||
 | 
			
		||||
    //#region Lifecycle
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        this.ws = new WebsocketClient();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public connectedCallback(): void {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
 | 
			
		||||
        window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar);
 | 
			
		||||
 | 
			
		||||
        window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => {
 | 
			
		||||
            this.notificationDrawerOpen = !this.notificationDrawerOpen;
 | 
			
		||||
@ -110,6 +150,11 @@ export class AdminInterface extends AuthenticatedInterface {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public disconnectedCallback(): void {
 | 
			
		||||
        super.disconnectedCallback();
 | 
			
		||||
        window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async firstUpdated(): Promise<void> {
 | 
			
		||||
        configureSentry(true);
 | 
			
		||||
        this.user = await me();
 | 
			
		||||
@ -118,6 +163,7 @@ export class AdminInterface extends AuthenticatedInterface {
 | 
			
		||||
            this.user.user.isSuperuser ||
 | 
			
		||||
            // TODO: somehow add `access_admin_interface` to the API schema
 | 
			
		||||
            this.user.user.systemPermissions.includes("access_admin_interface");
 | 
			
		||||
 | 
			
		||||
        if (!canAccessAdmin && this.user.user.pk > 0) {
 | 
			
		||||
            window.location.assign("/if/user/");
 | 
			
		||||
        }
 | 
			
		||||
@ -125,10 +171,14 @@ export class AdminInterface extends AuthenticatedInterface {
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        const sidebarClasses = {
 | 
			
		||||
            "pf-c-page__sidebar": true,
 | 
			
		||||
            "pf-m-light": this.activeTheme === UiThemeEnum.Light,
 | 
			
		||||
            "pf-m-expanded": this.sidebarOpen,
 | 
			
		||||
            "pf-m-collapsed": !this.sidebarOpen,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const drawerOpen = this.notificationDrawerOpen || this.apiDrawerOpen;
 | 
			
		||||
 | 
			
		||||
        const drawerClasses = {
 | 
			
		||||
            "pf-m-expanded": drawerOpen,
 | 
			
		||||
            "pf-m-collapsed": !drawerOpen,
 | 
			
		||||
@ -136,11 +186,18 @@ export class AdminInterface extends AuthenticatedInterface {
 | 
			
		||||
 | 
			
		||||
        return html` <ak-locale-context>
 | 
			
		||||
            <div class="pf-c-page">
 | 
			
		||||
                <ak-enterprise-status interface="admin"></ak-enterprise-status>
 | 
			
		||||
                <ak-version-banner></ak-version-banner>
 | 
			
		||||
                <ak-admin-sidebar
 | 
			
		||||
                    class="pf-c-page__sidebar ${classMap(sidebarClasses)}"
 | 
			
		||||
                ></ak-admin-sidebar>
 | 
			
		||||
                <ak-page-navbar>
 | 
			
		||||
                    <ak-version-banner></ak-version-banner>
 | 
			
		||||
                    <ak-enterprise-status interface="admin"></ak-enterprise-status>
 | 
			
		||||
                </ak-page-navbar>
 | 
			
		||||
 | 
			
		||||
                <ak-sidebar class="${classMap(sidebarClasses)}">
 | 
			
		||||
                    ${renderSidebarItems(AdminSidebarEntries)}
 | 
			
		||||
                    ${this.licenseSummary?.status !== LicenseSummaryStatusEnum.Unlicensed
 | 
			
		||||
                        ? renderSidebarItems(AdminSidebarEnterpriseEntries)
 | 
			
		||||
                        : nothing}
 | 
			
		||||
                </ak-sidebar>
 | 
			
		||||
 | 
			
		||||
                <div class="pf-c-page__drawer">
 | 
			
		||||
                    <div class="pf-c-drawer ${classMap(drawerClasses)}">
 | 
			
		||||
                        <div class="pf-c-drawer__main">
 | 
			
		||||
 | 
			
		||||
@ -1,186 +1,98 @@
 | 
			
		||||
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
 | 
			
		||||
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 { getRootStyle } from "@goauthentik/elements/utils/getRootStyle";
 | 
			
		||||
import { spread } from "@open-wc/lit-helpers";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { TemplateResult, html, nothing } from "lit";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators.js";
 | 
			
		||||
import { map } from "lit/directives/map.js";
 | 
			
		||||
import { repeat } from "lit/directives/repeat.js";
 | 
			
		||||
 | 
			
		||||
import { UiThemeEnum } from "@goauthentik/api";
 | 
			
		||||
import type { SessionUser, UserSelf } from "@goauthentik/api";
 | 
			
		||||
// 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[],
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@customElement("ak-admin-sidebar")
 | 
			
		||||
export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement)) {
 | 
			
		||||
    @property({ type: Boolean, reflect: true })
 | 
			
		||||
    open = true;
 | 
			
		||||
/**
 | 
			
		||||
 * Recursively renders a sidebar entry.
 | 
			
		||||
 */
 | 
			
		||||
export function renderSidebarItem([
 | 
			
		||||
    path,
 | 
			
		||||
    label,
 | 
			
		||||
    attributes,
 | 
			
		||||
    children,
 | 
			
		||||
]: SidebarEntry): TemplateResult {
 | 
			
		||||
    const properties = Array.isArray(attributes)
 | 
			
		||||
        ? { ".activeWhen": attributes }
 | 
			
		||||
        : (attributes ?? {});
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    impersonation: UserSelf["username"] | null = null;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        me().then((user: SessionUser) => {
 | 
			
		||||
            this.impersonation = user.original ? user.user.username : null;
 | 
			
		||||
        });
 | 
			
		||||
        this.toggleOpen = this.toggleOpen.bind(this);
 | 
			
		||||
        this.checkWidth = this.checkWidth.bind(this);
 | 
			
		||||
    if (path) {
 | 
			
		||||
        properties.path = path;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // This has to be a bound method so the event listener can be removed on disconnection as
 | 
			
		||||
    // needed.
 | 
			
		||||
    toggleOpen() {
 | 
			
		||||
        this.open = !this.open;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    checkWidth() {
 | 
			
		||||
        // This works just fine, but it assumes that the `--ak-sidebar--minimum-auto-width` is in
 | 
			
		||||
        // REMs. If that changes, this code will have to be adjusted as well.
 | 
			
		||||
        const minWidth =
 | 
			
		||||
            parseFloat(getRootStyle("--ak-sidebar--minimum-auto-width")) *
 | 
			
		||||
            parseFloat(getRootStyle("font-size"));
 | 
			
		||||
        this.open = window.innerWidth >= minWidth;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
        window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen);
 | 
			
		||||
        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(EVENT_SIDEBAR_TOGGLE, this.toggleOpen);
 | 
			
		||||
        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>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updated() {
 | 
			
		||||
        // This is permissible as`:host.classList` is not one of the properties Lit uses as a
 | 
			
		||||
        // scheduling trigger. This sort of shenanigans can trigger an loop, in that it will trigger
 | 
			
		||||
        // a browser reflow, which may trigger some other styling the application is monitoring,
 | 
			
		||||
        // triggering a re-render which triggers a browser reflow, ad infinitum. But we've been
 | 
			
		||||
        // living with that since jQuery, and it's both well-known and fortunately rare.
 | 
			
		||||
 | 
			
		||||
        // eslint-disable-next-line wc/no-self-class
 | 
			
		||||
        this.classList.remove("pf-m-expanded", "pf-m-collapsed");
 | 
			
		||||
        // eslint-disable-next-line wc/no-self-class
 | 
			
		||||
        this.classList.add(this.open ? "pf-m-expanded" : "pf-m-collapsed");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
    return html`<ak-sidebar-item ${spread(properties)}>
 | 
			
		||||
        ${label ? html`<span slot="label">${label}</span>` : nothing}
 | 
			
		||||
        ${children ? renderSidebarItems(children) : nothing}
 | 
			
		||||
    </ak-sidebar-item>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
    interface HTMLElementTagNameMap {
 | 
			
		||||
        "ak-admin-sidebar": AkAdminSidebar;
 | 
			
		||||
    }
 | 
			
		||||
/**
 | 
			
		||||
 * Recursively renders a collection of sidebar entries.
 | 
			
		||||
 */
 | 
			
		||||
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]
 | 
			
		||||
    ],
 | 
			
		||||
]]
 | 
			
		||||
 | 
			
		||||
@ -58,9 +58,6 @@ export class AdminOverviewPage extends AdminOverviewBase {
 | 
			
		||||
            PFContent,
 | 
			
		||||
            PFDivider,
 | 
			
		||||
            css`
 | 
			
		||||
                .pf-l-grid__item {
 | 
			
		||||
                    height: 100%;
 | 
			
		||||
                }
 | 
			
		||||
                .pf-l-grid__item.big-graph-container {
 | 
			
		||||
                    height: 35em;
 | 
			
		||||
                }
 | 
			
		||||
@ -74,6 +71,10 @@ export class AdminOverviewPage extends AdminOverviewBase {
 | 
			
		||||
                    line-height: normal;
 | 
			
		||||
                    font-size: var(--pf-global--icon--FontSize--sm);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .chart-item {
 | 
			
		||||
                    aspect-ratio: 2 / 1;
 | 
			
		||||
                }
 | 
			
		||||
            `,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
@ -94,22 +95,31 @@ export class AdminOverviewPage extends AdminOverviewBase {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        const name = this.user?.user.name ?? this.user?.user.username;
 | 
			
		||||
        const username = this.user?.user.name || this.user?.user.username;
 | 
			
		||||
 | 
			
		||||
        return html`<ak-page-header description=${msg("General system status")} ?hasIcon=${false}>
 | 
			
		||||
                <span slot="header"> ${msg(str`Welcome, ${name || ""}.`)} </span>
 | 
			
		||||
        return html` <ak-page-header
 | 
			
		||||
                header=${msg(str`Welcome, ${username || ""}.`)}
 | 
			
		||||
                description=${msg("General system status")}
 | 
			
		||||
                ?hasIcon=${false}
 | 
			
		||||
            >
 | 
			
		||||
            </ak-page-header>
 | 
			
		||||
            <section class="pf-c-page__main-section">
 | 
			
		||||
                <div class="pf-l-grid pf-m-gutter">
 | 
			
		||||
                    <!-- row 1 -->
 | 
			
		||||
                    <div
 | 
			
		||||
                        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 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>
 | 
			
		||||
                        </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-2-row pf-m-9-col-on-xl">
 | 
			
		||||
                        <ak-recent-events pageSize="6"></ak-recent-events>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-sm pf-m-3-col-on-xl">
 | 
			
		||||
                        <ak-quick-actions-card .actions=${this.quickActions}>
 | 
			
		||||
                        </ak-quick-actions-card>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <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
 | 
			
		||||
                                icon="pf-icon pf-icon-zone"
 | 
			
		||||
                                header=${msg("Outpost status")}
 | 
			
		||||
@ -118,24 +128,13 @@ export class AdminOverviewPage extends AdminOverviewBase {
 | 
			
		||||
                                <ak-admin-status-chart-outpost></ak-admin-status-chart-outpost>
 | 
			
		||||
                            </ak-aggregate-card>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div
 | 
			
		||||
                            class="pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-4-col-on-2xl"
 | 
			
		||||
                        >
 | 
			
		||||
                        <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-md chart-item">
 | 
			
		||||
                            <ak-aggregate-card icon="fa fa-sync-alt" header=${msg("Sync status")}>
 | 
			
		||||
                                <ak-admin-status-chart-sync></ak-admin-status-chart-sync>
 | 
			
		||||
                            </ak-aggregate-card>
 | 
			
		||||
                        </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>
 | 
			
		||||
 | 
			
		||||
                    <!-- row 3 -->
 | 
			
		||||
                    <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"
 | 
			
		||||
@ -163,32 +162,34 @@ export class AdminOverviewPage extends AdminOverviewBase {
 | 
			
		||||
            </section>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderCards() {
 | 
			
		||||
    renderSecondaryRow() {
 | 
			
		||||
        const isEnterprise = this.hasEnterpriseLicense;
 | 
			
		||||
        const colSpan = isEnterprise ? 4 : 6;
 | 
			
		||||
 | 
			
		||||
        const classes = {
 | 
			
		||||
            "card-container": true,
 | 
			
		||||
            "pf-l-grid__item": true,
 | 
			
		||||
            "pf-m-6-col": true,
 | 
			
		||||
            "pf-m-4-col-on-md": !isEnterprise,
 | 
			
		||||
            "pf-m-4-col-on-xl": !isEnterprise,
 | 
			
		||||
            "pf-m-3-col-on-md": isEnterprise,
 | 
			
		||||
            "pf-m-3-col-on-xl": isEnterprise,
 | 
			
		||||
            [`pf-m-12-col`]: true,
 | 
			
		||||
            [`pf-m-${colSpan}-col-on-md`]: true,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return html`<div class=${classMap(classes)}>
 | 
			
		||||
        return html`
 | 
			
		||||
            <div class=${classMap(classes)}>
 | 
			
		||||
                <ak-admin-status-system> </ak-admin-status-system>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class=${classMap(classes)}>
 | 
			
		||||
                <ak-admin-status-version> </ak-admin-status-version>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class=${classMap(classes)}>
 | 
			
		||||
                <ak-admin-status-card-workers> </ak-admin-status-card-workers>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            ${isEnterprise
 | 
			
		||||
                ? html` <div class=${classMap(classes)}>
 | 
			
		||||
                      <ak-admin-fips-status-system> </ak-admin-fips-status-system>
 | 
			
		||||
                  </div>`
 | 
			
		||||
                : nothing} `;
 | 
			
		||||
                ? html`
 | 
			
		||||
                      <div class=${classMap(classes)}>
 | 
			
		||||
                          <ak-admin-fips-status-system> </ak-admin-fips-status-system>
 | 
			
		||||
                      </div>
 | 
			
		||||
                  `
 | 
			
		||||
                : nothing}
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderActions() {
 | 
			
		||||
 | 
			
		||||
@ -83,13 +83,10 @@ export class AdminSettingsPage extends AKElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        if (!this.settings) {
 | 
			
		||||
            return nothing;
 | 
			
		||||
        }
 | 
			
		||||
        if (!this.settings) return nothing;
 | 
			
		||||
 | 
			
		||||
        return html`
 | 
			
		||||
            <ak-page-header icon="fa fa-cog" header="" description="">
 | 
			
		||||
                <span slot="header"> ${msg("System settings")} </span>
 | 
			
		||||
            </ak-page-header>
 | 
			
		||||
            <ak-page-header icon="fa fa-cog" header="${msg("System settings")}"> </ak-page-header>
 | 
			
		||||
            <section class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter">
 | 
			
		||||
                <div class="pf-c-card">
 | 
			
		||||
                    <div class="pf-c-card__body">
 | 
			
		||||
 | 
			
		||||
@ -1,26 +1,110 @@
 | 
			
		||||
import type { Config as DOMPurifyConfig } from "dompurify";
 | 
			
		||||
import DOMPurify from "dompurify";
 | 
			
		||||
import { trustedTypes } from "trusted-types";
 | 
			
		||||
 | 
			
		||||
import { render } from "@lit-labs/ssr";
 | 
			
		||||
import { collectResult } from "@lit-labs/ssr/lib/render-result.js";
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
import { render } from "lit";
 | 
			
		||||
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 = {
 | 
			
		||||
    ALLOWED_TAGS: ["#text"],
 | 
			
		||||
} 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 {
 | 
			
		||||
    return html`${until(
 | 
			
		||||
        (async () => {
 | 
			
		||||
            const rendered = await renderStatic(input);
 | 
			
		||||
            const purified = DOMPurify.sanitize(rendered);
 | 
			
		||||
            return html`${unsafeHTML(purified)}`;
 | 
			
		||||
        })(),
 | 
			
		||||
    )}`;
 | 
			
		||||
    const result = container.innerHTML;
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,13 @@
 | 
			
		||||
 | 
			
		||||
    /* Minimum width after which the sidebar becomes automatic */
 | 
			
		||||
    --ak-sidebar--minimum-auto-width: 80rem;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The height of the navbar and branded sidebar.
 | 
			
		||||
     * @todo This shouldn't be necessary. The sidebar can instead use a grid layout
 | 
			
		||||
     * ensuring they share the same height.
 | 
			
		||||
     */
 | 
			
		||||
    --ak-navbar--height: 7rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@supports selector(::-webkit-scrollbar) {
 | 
			
		||||
 | 
			
		||||
@ -67,6 +67,12 @@ export class NavigationButtons extends AKElement {
 | 
			
		||||
                :host([theme="light"]) .pf-c-page__header-tools-group .pf-c-button {
 | 
			
		||||
                    color: var(--ak-global--Color--100) !important;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @media (max-width: 768px) {
 | 
			
		||||
                    .pf-c-avatar {
 | 
			
		||||
                        display: none;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            `,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
@ -156,9 +162,7 @@ export class NavigationButtons extends AKElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderImpersonation() {
 | 
			
		||||
        if (!this.me?.original) {
 | 
			
		||||
            return nothing;
 | 
			
		||||
        }
 | 
			
		||||
        if (!this.me?.original) return nothing;
 | 
			
		||||
 | 
			
		||||
        const onClick = async () => {
 | 
			
		||||
            await new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve();
 | 
			
		||||
@ -175,6 +179,14 @@ export class NavigationButtons extends AKElement {
 | 
			
		||||
            </div>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderAvatar() {
 | 
			
		||||
        return html`<img
 | 
			
		||||
            class="pf-c-avatar"
 | 
			
		||||
            src=${ifDefined(this.me?.user.avatar)}
 | 
			
		||||
            alt="${msg("Avatar image")}"
 | 
			
		||||
        />`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get userDisplayName() {
 | 
			
		||||
        return match<UserDisplay | undefined, string | undefined>(this.uiConfig?.navbar.userDisplay)
 | 
			
		||||
            .with(UserDisplay.username, () => this.me?.user.username)
 | 
			
		||||
@ -212,11 +224,7 @@ export class NavigationButtons extends AKElement {
 | 
			
		||||
                      </div>
 | 
			
		||||
                  </div>`
 | 
			
		||||
                : nothing}
 | 
			
		||||
            <img
 | 
			
		||||
                class="pf-c-avatar"
 | 
			
		||||
                src=${ifDefined(this.me?.user.avatar)}
 | 
			
		||||
                alt="${msg("Avatar image")}"
 | 
			
		||||
            />
 | 
			
		||||
            ${this.renderAvatar()}
 | 
			
		||||
        </div>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -5,20 +5,23 @@ import {
 | 
			
		||||
} from "@goauthentik/common/constants";
 | 
			
		||||
import { globalAK } from "@goauthentik/common/global";
 | 
			
		||||
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 "@goauthentik/components/ak-nav-buttons";
 | 
			
		||||
import { AKElement } from "@goauthentik/elements/Base";
 | 
			
		||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
 | 
			
		||||
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
 | 
			
		||||
import { themeImage } from "@goauthentik/elements/utils/images";
 | 
			
		||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
 | 
			
		||||
import { CSSResult, LitElement, TemplateResult, css, html, nothing } from "lit";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
 | 
			
		||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
 | 
			
		||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
 | 
			
		||||
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
 | 
			
		||||
import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css";
 | 
			
		||||
import PFNotificationBadge from "@patternfly/patternfly/components/NotificationBadge/notification-badge.css";
 | 
			
		||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
 | 
			
		||||
@ -26,34 +29,52 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
 | 
			
		||||
 | 
			
		||||
import { SessionUser } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-page-header")
 | 
			
		||||
export class PageHeader extends WithBrandConfig(AKElement) {
 | 
			
		||||
    @property()
 | 
			
		||||
    icon?: string;
 | 
			
		||||
//#region Page Navbar
 | 
			
		||||
 | 
			
		||||
    @property({ type: Boolean })
 | 
			
		||||
    iconImage = false;
 | 
			
		||||
 | 
			
		||||
    @property()
 | 
			
		||||
    header = "";
 | 
			
		||||
 | 
			
		||||
    @property()
 | 
			
		||||
export interface PageNavbarDetails {
 | 
			
		||||
    header?: string;
 | 
			
		||||
    description?: string;
 | 
			
		||||
    icon?: string;
 | 
			
		||||
    iconImage?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    @property({ type: Boolean })
 | 
			
		||||
    hasIcon = true;
 | 
			
		||||
/**
 | 
			
		||||
 * A global navbar component at the top of the page.
 | 
			
		||||
 *
 | 
			
		||||
 * Internally, this component listens for the `ak-page-header` event, which is
 | 
			
		||||
 * dispatched by the `ak-page-header` component.
 | 
			
		||||
 */
 | 
			
		||||
@customElement("ak-page-navbar")
 | 
			
		||||
export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavbarDetails {
 | 
			
		||||
    //#region Static Properties
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    me?: SessionUser;
 | 
			
		||||
    private static elementRef: AKPageNavbar | null = null;
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    uiConfig!: UIConfig;
 | 
			
		||||
    static readonly setNavbarDetails = (detail: Partial<PageNavbarDetails>): void => {
 | 
			
		||||
        const { elementRef } = AKPageNavbar;
 | 
			
		||||
        if (!elementRef) {
 | 
			
		||||
            console.debug(
 | 
			
		||||
                `ak-page-header: Could not find ak-page-navbar, skipping event dispatch.`,
 | 
			
		||||
            );
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const { header, description, icon, iconImage } = detail;
 | 
			
		||||
 | 
			
		||||
        elementRef.header = header;
 | 
			
		||||
        elementRef.description = description;
 | 
			
		||||
        elementRef.icon = icon;
 | 
			
		||||
        elementRef.iconImage = iconImage || false;
 | 
			
		||||
        elementRef.hasIcon = !!icon;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [
 | 
			
		||||
            PFBase,
 | 
			
		||||
            PFButton,
 | 
			
		||||
            PFPage,
 | 
			
		||||
            PFDrawer,
 | 
			
		||||
 | 
			
		||||
            PFNotificationBadge,
 | 
			
		||||
            PFContent,
 | 
			
		||||
            PFAvatar,
 | 
			
		||||
@ -63,143 +84,392 @@ export class PageHeader extends WithBrandConfig(AKElement) {
 | 
			
		||||
                    position: sticky;
 | 
			
		||||
                    top: 0;
 | 
			
		||||
                    z-index: var(--pf-global--ZIndex--lg);
 | 
			
		||||
                    --pf-c-page__header-tools--MarginRight: 0;
 | 
			
		||||
                    --ak-brand-logo-height: var(--pf-global--FontSize--4xl, 2.25rem);
 | 
			
		||||
                    --ak-brand-background-color: var(
 | 
			
		||||
                        --pf-c-page__sidebar--m-light--BackgroundColor
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
                .bar {
 | 
			
		||||
 | 
			
		||||
                :host([theme="dark"]) {
 | 
			
		||||
                    --ak-brand-background-color: var(--pf-c-page__sidebar--BackgroundColor);
 | 
			
		||||
                    --pf-c-page__sidebar--BackgroundColor: var(--ak-dark-background-light);
 | 
			
		||||
                    color: var(--ak-dark-foreground);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                navbar {
 | 
			
		||||
                    border-bottom: var(--pf-global--BorderWidth--sm);
 | 
			
		||||
                    border-bottom-style: solid;
 | 
			
		||||
                    border-bottom-color: var(--pf-global--BorderColor--100);
 | 
			
		||||
                    background-color: var(--pf-c-page--BackgroundColor);
 | 
			
		||||
 | 
			
		||||
                    display: flex;
 | 
			
		||||
                    flex-direction: row;
 | 
			
		||||
                    min-height: 114px;
 | 
			
		||||
                    max-height: 114px;
 | 
			
		||||
                    background-color: var(--pf-c-page--BackgroundColor);
 | 
			
		||||
                    min-height: 6rem;
 | 
			
		||||
 | 
			
		||||
                    display: grid;
 | 
			
		||||
                    row-gap: var(--pf-global--spacer--sm);
 | 
			
		||||
                    column-gap: var(--pf-global--spacer--sm);
 | 
			
		||||
                    grid-template-columns: [brand] auto [toggle] auto [primary] 1fr [secondary] auto;
 | 
			
		||||
                    grid-template-rows: auto auto;
 | 
			
		||||
                    grid-template-areas:
 | 
			
		||||
                        "brand toggle primary secondary"
 | 
			
		||||
                        "brand toggle description secondary";
 | 
			
		||||
 | 
			
		||||
                    @media (max-width: 768px) {
 | 
			
		||||
                        row-gap: var(--pf-global--spacer--xs);
 | 
			
		||||
 | 
			
		||||
                        align-items: center;
 | 
			
		||||
                        grid-template-areas:
 | 
			
		||||
                            "toggle primary secondary"
 | 
			
		||||
                            "toggle description description";
 | 
			
		||||
                        justify-content: space-between;
 | 
			
		||||
                        width: 100%;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .pf-c-page__main-section.pf-m-light {
 | 
			
		||||
                    background-color: transparent;
 | 
			
		||||
 | 
			
		||||
                .items {
 | 
			
		||||
                    display: block;
 | 
			
		||||
 | 
			
		||||
                    &.primary {
 | 
			
		||||
                        grid-column: primary;
 | 
			
		||||
                        grid-row: primary / description;
 | 
			
		||||
 | 
			
		||||
                        align-content: center;
 | 
			
		||||
                        padding-block: var(--pf-global--spacer--md);
 | 
			
		||||
 | 
			
		||||
                        @media (min-width: 426px) {
 | 
			
		||||
                            &.block-sibling {
 | 
			
		||||
                                padding-block-end: 0;
 | 
			
		||||
                                grid-row: primary;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        @media (max-width: 768px) {
 | 
			
		||||
                            padding-block: var(--pf-global--spacer--sm);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        .accent-icon {
 | 
			
		||||
                            height: 1em;
 | 
			
		||||
                            width: 1em;
 | 
			
		||||
 | 
			
		||||
                            @media (max-width: 768px) {
 | 
			
		||||
                                display: none;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    &.page-description {
 | 
			
		||||
                        grid-area: description;
 | 
			
		||||
                        padding-block-end: var(--pf-global--spacer--md);
 | 
			
		||||
 | 
			
		||||
                        @media (max-width: 425px) {
 | 
			
		||||
                            display: none;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        @media (min-width: 769px) {
 | 
			
		||||
                            text-wrap: balance;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    &.secondary {
 | 
			
		||||
                        grid-area: secondary;
 | 
			
		||||
                        flex: 0 0 auto;
 | 
			
		||||
                        justify-self: end;
 | 
			
		||||
                        padding-block: var(--pf-global--spacer--sm);
 | 
			
		||||
                        padding-inline-end: var(--pf-global--spacer--sm);
 | 
			
		||||
 | 
			
		||||
                        @media (min-width: 769px) {
 | 
			
		||||
                            align-content: center;
 | 
			
		||||
                            padding-block: var(--pf-global--spacer--md);
 | 
			
		||||
                            padding-inline-end: var(--pf-global--spacer--xl);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .pf-c-page__main-section {
 | 
			
		||||
                    flex-grow: 1;
 | 
			
		||||
                    flex-shrink: 1;
 | 
			
		||||
 | 
			
		||||
                .brand {
 | 
			
		||||
                    grid-area: brand;
 | 
			
		||||
                    background-color: var(--ak-brand-background-color);
 | 
			
		||||
                    height: 100%;
 | 
			
		||||
                    width: var(--pf-c-page__sidebar--Width);
 | 
			
		||||
                    align-items: center;
 | 
			
		||||
                    padding-inline: var(--pf-global--spacer--sm);
 | 
			
		||||
 | 
			
		||||
                    display: flex;
 | 
			
		||||
                    flex-direction: column;
 | 
			
		||||
                    justify-content: center;
 | 
			
		||||
 | 
			
		||||
                    &.pf-m-collapsed {
 | 
			
		||||
                        display: none;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    @media (max-width: 1279px) {
 | 
			
		||||
                        display: none;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                img.pf-icon {
 | 
			
		||||
                    max-height: 24px;
 | 
			
		||||
 | 
			
		||||
                .sidebar-trigger {
 | 
			
		||||
                    grid-area: toggle;
 | 
			
		||||
                    height: 100%;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .logo {
 | 
			
		||||
                    flex: 0 0 auto;
 | 
			
		||||
                    height: var(--ak-brand-logo-height);
 | 
			
		||||
 | 
			
		||||
                    & img {
 | 
			
		||||
                        height: 100%;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .sidebar-trigger,
 | 
			
		||||
                .notification-trigger {
 | 
			
		||||
                    font-size: 24px;
 | 
			
		||||
                    font-size: 1.5rem;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .notification-trigger.has-notifications {
 | 
			
		||||
                    color: var(--pf-global--active-color--100);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .page-title {
 | 
			
		||||
                    display: flex;
 | 
			
		||||
                    gap: var(--pf-global--spacer--xs);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                h1 {
 | 
			
		||||
                    display: flex;
 | 
			
		||||
                    flex-direction: row;
 | 
			
		||||
                    align-items: center !important;
 | 
			
		||||
                }
 | 
			
		||||
                .pf-c-page__header-tools {
 | 
			
		||||
                    flex-shrink: 0;
 | 
			
		||||
                }
 | 
			
		||||
                .pf-c-page__header-tools-group {
 | 
			
		||||
                    height: 100%;
 | 
			
		||||
                }
 | 
			
		||||
                :host([theme="dark"]) .pf-c-page__header-tools {
 | 
			
		||||
                    color: var(--ak-dark-foreground) !important;
 | 
			
		||||
                }
 | 
			
		||||
            `,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        window.addEventListener(EVENT_WS_MESSAGE, () => {
 | 
			
		||||
            this.firstUpdated();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    //#endregion
 | 
			
		||||
 | 
			
		||||
    async firstUpdated() {
 | 
			
		||||
        this.me = await me();
 | 
			
		||||
        this.uiConfig = await uiConfig();
 | 
			
		||||
        this.uiConfig.navbar.userDisplay = UserDisplay.none;
 | 
			
		||||
    }
 | 
			
		||||
    //#region Properties
 | 
			
		||||
 | 
			
		||||
    setTitle(header?: string) {
 | 
			
		||||
    @property({ type: String })
 | 
			
		||||
    icon?: string;
 | 
			
		||||
 | 
			
		||||
    @property({ type: Boolean })
 | 
			
		||||
    iconImage = false;
 | 
			
		||||
 | 
			
		||||
    @property({ type: String })
 | 
			
		||||
    header?: string;
 | 
			
		||||
 | 
			
		||||
    @property({ type: String })
 | 
			
		||||
    description?: string;
 | 
			
		||||
 | 
			
		||||
    @property({ type: Boolean })
 | 
			
		||||
    hasIcon = true;
 | 
			
		||||
 | 
			
		||||
    @property({ type: Boolean })
 | 
			
		||||
    open = true;
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    session?: SessionUser;
 | 
			
		||||
 | 
			
		||||
    @state()
 | 
			
		||||
    uiConfig!: UIConfig;
 | 
			
		||||
 | 
			
		||||
    //#endregion
 | 
			
		||||
 | 
			
		||||
    //#region Private 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 !== undefined && header !== "") {
 | 
			
		||||
        if (header) {
 | 
			
		||||
            title = `${header} - ${title}`;
 | 
			
		||||
        }
 | 
			
		||||
        document.title = title;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #toggleSidebar() {
 | 
			
		||||
        this.open = !this.open;
 | 
			
		||||
 | 
			
		||||
        this.dispatchEvent(
 | 
			
		||||
            new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
 | 
			
		||||
                bubbles: true,
 | 
			
		||||
                composed: true,
 | 
			
		||||
            }),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //#endregion
 | 
			
		||||
 | 
			
		||||
    //#region Lifecycle
 | 
			
		||||
 | 
			
		||||
    public connectedCallback(): void {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
        AKPageNavbar.elementRef = this;
 | 
			
		||||
 | 
			
		||||
        window.addEventListener(EVENT_WS_MESSAGE, () => {
 | 
			
		||||
            this.firstUpdated();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public disconnectedCallback(): void {
 | 
			
		||||
        super.disconnectedCallback();
 | 
			
		||||
        AKPageNavbar.elementRef = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async firstUpdated() {
 | 
			
		||||
        this.session = await me();
 | 
			
		||||
        this.uiConfig = getConfigForUser(this.session.user);
 | 
			
		||||
        this.uiConfig.navbar.userDisplay = UserDisplay.none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    willUpdate() {
 | 
			
		||||
        // Always update title, even if there's no header value set,
 | 
			
		||||
        // as in that case we still need to return to the generic title
 | 
			
		||||
        this.setTitle(this.header);
 | 
			
		||||
        this.#setTitle(this.header);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //#endregion
 | 
			
		||||
 | 
			
		||||
    //#region Render
 | 
			
		||||
 | 
			
		||||
    renderIcon() {
 | 
			
		||||
        if (this.icon) {
 | 
			
		||||
            if (this.iconImage && !this.icon.startsWith("fa://")) {
 | 
			
		||||
                return html`<img class="pf-icon" src="${this.icon}" alt="page icon" />`;
 | 
			
		||||
                return html`<img class="accent-icon pf-icon" src="${this.icon}" alt="page icon" />`;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const icon = this.icon.replaceAll("fa://", "fa ");
 | 
			
		||||
            return html`<i class=${icon}></i>`;
 | 
			
		||||
 | 
			
		||||
            return html`<i class="accent-icon ${icon}"></i>`;
 | 
			
		||||
        }
 | 
			
		||||
        return nothing;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        return html`<div class="bar">
 | 
			
		||||
            <button
 | 
			
		||||
                class="sidebar-trigger pf-c-button pf-m-plain"
 | 
			
		||||
                @click=${() => {
 | 
			
		||||
                    this.dispatchEvent(
 | 
			
		||||
                        new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
 | 
			
		||||
                            bubbles: true,
 | 
			
		||||
                            composed: true,
 | 
			
		||||
                        }),
 | 
			
		||||
                    );
 | 
			
		||||
                }}
 | 
			
		||||
            >
 | 
			
		||||
                <i class="fas fa-bars"></i>
 | 
			
		||||
            </button>
 | 
			
		||||
            <section class="pf-c-page__main-section pf-m-light">
 | 
			
		||||
                <div class="pf-c-content">
 | 
			
		||||
                    <h1>
 | 
			
		||||
        return html`<navbar aria-label="Main" class="navbar">
 | 
			
		||||
                <aside class="brand ${this.open ? "" : "pf-m-collapsed"}">
 | 
			
		||||
                    <a href="#/">
 | 
			
		||||
                        <div class="logo">
 | 
			
		||||
                            <img
 | 
			
		||||
                                src=${themeImage(
 | 
			
		||||
                                    this.brand?.brandingLogo ?? DefaultBrand.brandingLogo,
 | 
			
		||||
                                )}
 | 
			
		||||
                                alt="${msg("authentik Logo")}"
 | 
			
		||||
                                loading="lazy"
 | 
			
		||||
                            />
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </a>
 | 
			
		||||
                </aside>
 | 
			
		||||
                <button
 | 
			
		||||
                    class="sidebar-trigger pf-c-button pf-m-plain"
 | 
			
		||||
                    @click=${this.#toggleSidebar}
 | 
			
		||||
                    aria-label=${msg("Toggle sidebar")}
 | 
			
		||||
                    aria-expanded=${this.open ? "true" : "false"}
 | 
			
		||||
                >
 | 
			
		||||
                    <i class="fas fa-bars"></i>
 | 
			
		||||
                </button>
 | 
			
		||||
 | 
			
		||||
                <section
 | 
			
		||||
                    class="items primary pf-c-content ${this.description ? "block-sibling" : ""}"
 | 
			
		||||
                >
 | 
			
		||||
                    <h1 class="page-title">
 | 
			
		||||
                        ${this.hasIcon
 | 
			
		||||
                            ? html`<slot name="icon">${this.renderIcon()}</slot> `
 | 
			
		||||
                            ? html`<slot name="icon">${this.renderIcon()}</slot>`
 | 
			
		||||
                            : nothing}
 | 
			
		||||
                        <slot name="header">${this.header}</slot>
 | 
			
		||||
                        ${this.header}
 | 
			
		||||
                    </h1>
 | 
			
		||||
                    ${this.description ? html`<p>${this.description}</p>` : html``}
 | 
			
		||||
                </div>
 | 
			
		||||
            </section>
 | 
			
		||||
            <div class="pf-c-page__header-tools">
 | 
			
		||||
                <div class="pf-c-page__header-tools-group">
 | 
			
		||||
                    <ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.me}>
 | 
			
		||||
                        <a
 | 
			
		||||
                            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/"
 | 
			
		||||
                            slot="extra"
 | 
			
		||||
                        >
 | 
			
		||||
                            ${msg("User interface")}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </ak-nav-buttons>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>`;
 | 
			
		||||
                </section>
 | 
			
		||||
                ${this.description
 | 
			
		||||
                    ? html`<section class="items page-description pf-c-content">
 | 
			
		||||
                          <p>${this.description}</p>
 | 
			
		||||
                      </section>`
 | 
			
		||||
                    : nothing}
 | 
			
		||||
 | 
			
		||||
                <section class="items secondary">
 | 
			
		||||
                    <div class="pf-c-page__header-tools-group">
 | 
			
		||||
                        <ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.session}>
 | 
			
		||||
                            <a
 | 
			
		||||
                                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/"
 | 
			
		||||
                                slot="extra"
 | 
			
		||||
                            >
 | 
			
		||||
                                ${msg("User interface")}
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </ak-nav-buttons>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </section>
 | 
			
		||||
            </navbar>
 | 
			
		||||
            <slot></slot>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //#endregion
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//#endregion
 | 
			
		||||
 | 
			
		||||
//#region Page Header
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A page header component, used to display the page title and description.
 | 
			
		||||
 *
 | 
			
		||||
 * Internally, this component dispatches the `ak-page-header` event, which is
 | 
			
		||||
 * listened to by the `ak-page-navbar` component.
 | 
			
		||||
 *
 | 
			
		||||
 * @singleton
 | 
			
		||||
 */
 | 
			
		||||
@customElement("ak-page-header")
 | 
			
		||||
export class AKPageHeader extends LitElement implements PageNavbarDetails {
 | 
			
		||||
    @property({ type: String })
 | 
			
		||||
    header?: string;
 | 
			
		||||
 | 
			
		||||
    @property({ type: String })
 | 
			
		||||
    description?: string;
 | 
			
		||||
 | 
			
		||||
    @property({ type: String })
 | 
			
		||||
    icon?: string;
 | 
			
		||||
 | 
			
		||||
    @property({ type: Boolean })
 | 
			
		||||
    iconImage = false;
 | 
			
		||||
 | 
			
		||||
    static get styles(): CSSResult[] {
 | 
			
		||||
        return [
 | 
			
		||||
            css`
 | 
			
		||||
                :host {
 | 
			
		||||
                    display: none;
 | 
			
		||||
                }
 | 
			
		||||
            `,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connectedCallback(): void {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
 | 
			
		||||
        AKPageNavbar.setNavbarDetails({
 | 
			
		||||
            header: this.header,
 | 
			
		||||
            description: this.description,
 | 
			
		||||
            icon: this.icon,
 | 
			
		||||
            iconImage: this.iconImage,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updated(): void {
 | 
			
		||||
        AKPageNavbar.setNavbarDetails({
 | 
			
		||||
            header: this.header,
 | 
			
		||||
            description: this.description,
 | 
			
		||||
            icon: this.icon,
 | 
			
		||||
            iconImage: this.iconImage,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//#endregion
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
    interface HTMLElementTagNameMap {
 | 
			
		||||
        "ak-page-header": PageHeader;
 | 
			
		||||
        "ak-page-header": AKPageHeader;
 | 
			
		||||
        "ak-page-navbar": AKPageNavbar;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -80,6 +80,7 @@ export class AggregateCard extends AKElement implements IAggregateCard {
 | 
			
		||||
                .center-value {
 | 
			
		||||
                    font-size: var(--pf-global--icon--FontSize--lg);
 | 
			
		||||
                    text-align: center;
 | 
			
		||||
                    place-content: center;
 | 
			
		||||
                }
 | 
			
		||||
                .subtext {
 | 
			
		||||
                    margin-top: var(--pf-global--spacer--sm);
 | 
			
		||||
 | 
			
		||||
@ -35,10 +35,7 @@ export class Sidebar extends AKElement {
 | 
			
		||||
                .pf-c-nav__section + .pf-c-nav__section {
 | 
			
		||||
                    --pf-c-nav__section--section--MarginTop: var(--pf-global--spacer--sm);
 | 
			
		||||
                }
 | 
			
		||||
                .pf-c-nav__list .sidebar-brand {
 | 
			
		||||
                    max-height: 82px;
 | 
			
		||||
                    margin-bottom: -0.5rem;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                nav {
 | 
			
		||||
                    display: flex;
 | 
			
		||||
                    flex-direction: column;
 | 
			
		||||
@ -70,7 +67,6 @@ export class Sidebar extends AKElement {
 | 
			
		||||
            class="pf-c-nav ${this.activeTheme === UiThemeEnum.Light ? "pf-m-light" : ""}"
 | 
			
		||||
            aria-label=${msg("Global")}
 | 
			
		||||
        >
 | 
			
		||||
            <ak-sidebar-brand></ak-sidebar-brand>
 | 
			
		||||
            <ul class="pf-c-nav__list">
 | 
			
		||||
                <slot></slot>
 | 
			
		||||
            </ul>
 | 
			
		||||
 | 
			
		||||
@ -1,17 +1,3 @@
 | 
			
		||||
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
 | 
			
		||||
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";
 | 
			
		||||
 | 
			
		||||
// If the viewport is wider than MIN_WIDTH, the sidebar
 | 
			
		||||
@ -28,79 +14,3 @@ export const DefaultBrand: CurrentBrand = {
 | 
			
		||||
    matchedDomain: "",
 | 
			
		||||
    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: 114px;
 | 
			
		||||
                    min-height: 114px;
 | 
			
		||||
                    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;
 | 
			
		||||
                }
 | 
			
		||||
                button.pf-c-button.sidebar-trigger {
 | 
			
		||||
                    background-color: transparent;
 | 
			
		||||
                    border-radius: 0px;
 | 
			
		||||
                    height: 100%;
 | 
			
		||||
                    color: var(--ak-dark-foreground);
 | 
			
		||||
                }
 | 
			
		||||
            `,
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        window.addEventListener("resize", () => {
 | 
			
		||||
            this.requestUpdate();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        return html` ${window.innerWidth <= MIN_WIDTH
 | 
			
		||||
                ? html`
 | 
			
		||||
                      <button
 | 
			
		||||
                          class="sidebar-trigger pf-c-button"
 | 
			
		||||
                          @click=${() => {
 | 
			
		||||
                              this.dispatchEvent(
 | 
			
		||||
                                  new CustomEvent(EVENT_SIDEBAR_TOGGLE, {
 | 
			
		||||
                                      bubbles: true,
 | 
			
		||||
                                      composed: true,
 | 
			
		||||
                                  }),
 | 
			
		||||
                              );
 | 
			
		||||
                          }}
 | 
			
		||||
                      >
 | 
			
		||||
                          <i class="fas fa-bars"></i>
 | 
			
		||||
                      </button>
 | 
			
		||||
                  `
 | 
			
		||||
                : 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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										55
									
								
								web/src/elements/utils/iframe.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								web/src/elements/utils/iframe.ts
									
									
									
									
									
										Normal 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>`;
 | 
			
		||||
}
 | 
			
		||||
@ -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 { 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")
 | 
			
		||||
export class BrandLinks extends AKElement {
 | 
			
		||||
    static get styles() {
 | 
			
		||||
@ -33,13 +31,21 @@ export class BrandLinks extends AKElement {
 | 
			
		||||
    links: FooterLink[] = [];
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const links = [...(this.links ?? []), poweredBy];
 | 
			
		||||
        const links = [...(this.links ?? [])];
 | 
			
		||||
 | 
			
		||||
        return html` <ul class="pf-c-list pf-m-inline">
 | 
			
		||||
            ${map(links, (link) =>
 | 
			
		||||
                link.href
 | 
			
		||||
                    ? purify(html`<li><a href="${link.href}">${link.name}</a></li>`)
 | 
			
		||||
                    : html`<li><span>${link.name}</span></li>`,
 | 
			
		||||
            )}
 | 
			
		||||
            ${map(links, (link) => {
 | 
			
		||||
                const children = sanitizeHTML(BrandedHTMLPolicy, link.name);
 | 
			
		||||
 | 
			
		||||
                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>`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,15 +1,16 @@
 | 
			
		||||
///<reference types="@hcaptcha/types"/>
 | 
			
		||||
import { renderStatic } from "@goauthentik/common/purify";
 | 
			
		||||
/// <reference types="@hcaptcha/types"/>
 | 
			
		||||
/// <reference types="turnstile-types"/>
 | 
			
		||||
import { renderStaticHTMLUnsafe } from "@goauthentik/common/purify";
 | 
			
		||||
import "@goauthentik/elements/EmptyState";
 | 
			
		||||
import { akEmptyState } from "@goauthentik/elements/EmptyState";
 | 
			
		||||
import { bound } from "@goauthentik/elements/decorators/bound";
 | 
			
		||||
import "@goauthentik/elements/forms/FormElement";
 | 
			
		||||
import { createIFrameHTMLWrapper } from "@goauthentik/elements/utils/iframe";
 | 
			
		||||
import { ListenerController } from "@goauthentik/elements/utils/listenerController.js";
 | 
			
		||||
import { randomId } from "@goauthentik/elements/utils/randomId";
 | 
			
		||||
import "@goauthentik/flow/FormStatic";
 | 
			
		||||
import { BaseStage } from "@goauthentik/flow/stages/base";
 | 
			
		||||
import { P, match } from "ts-pattern";
 | 
			
		||||
import type * as _ from "turnstile-types";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
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
 | 
			
		||||
// margin, so adding 2rem of height to our container adds padding and prevents scroll bars or hidden
 | 
			
		||||
// 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) =>
 | 
			
		||||
    html`<!doctype html>
 | 
			
		||||
        <head>
 | 
			
		||||
            <html>
 | 
			
		||||
                <body style="display:flex;flex-direction:row;justify-content:center;">
 | 
			
		||||
                    ${captchaElement}
 | 
			
		||||
                    <script>
 | 
			
		||||
                        new ResizeObserver((entries) => {
 | 
			
		||||
                            const height =
 | 
			
		||||
                                document.body.offsetHeight +
 | 
			
		||||
                                parseFloat(getComputedStyle(document.body).fontSize) * 2;
 | 
			
		||||
                            window.parent.postMessage({
 | 
			
		||||
                                message: "resize",
 | 
			
		||||
                                source: "goauthentik.io",
 | 
			
		||||
                                context: "flow-executor",
 | 
			
		||||
                                size: { height },
 | 
			
		||||
                            });
 | 
			
		||||
                        }).observe(document.querySelector(".ak-captcha-container"));
 | 
			
		||||
                    </script>
 | 
			
		||||
                    <script src=${challengeUrl}></script>
 | 
			
		||||
                    <script>
 | 
			
		||||
                        function callback(token) {
 | 
			
		||||
                            window.parent.postMessage({
 | 
			
		||||
                                message: "captcha",
 | 
			
		||||
                                source: "goauthentik.io",
 | 
			
		||||
                                context: "flow-executor",
 | 
			
		||||
                                token: token,
 | 
			
		||||
                            });
 | 
			
		||||
                        }
 | 
			
		||||
                    </script>
 | 
			
		||||
                </body>
 | 
			
		||||
            </html>
 | 
			
		||||
        </head>`;
 | 
			
		||||
                window.parent.postMessage({
 | 
			
		||||
                    message: "resize",
 | 
			
		||||
                    source: "goauthentik.io",
 | 
			
		||||
                    context: "flow-executor",
 | 
			
		||||
                    size: { height },
 | 
			
		||||
                });
 | 
			
		||||
            }).observe(document.querySelector(".ak-captcha-container"));
 | 
			
		||||
        </script>
 | 
			
		||||
 | 
			
		||||
        <script src=${challengeURL}></script>
 | 
			
		||||
 | 
			
		||||
        <script>
 | 
			
		||||
            function callback(token) {
 | 
			
		||||
                window.parent.postMessage({
 | 
			
		||||
                    message: "captcha",
 | 
			
		||||
                    source: "goauthentik.io",
 | 
			
		||||
                    context: "flow-executor",
 | 
			
		||||
                    token,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        </script>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@customElement("ak-stage-captcha")
 | 
			
		||||
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
 | 
			
		||||
@ -305,11 +302,25 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async renderFrame(captchaElement: TemplateResult) {
 | 
			
		||||
        this.captchaFrame.contentWindow?.document.open();
 | 
			
		||||
        this.captchaFrame.contentWindow?.document.write(
 | 
			
		||||
            await renderStatic(iframeTemplate(captchaElement, this.challenge.jsUrl)),
 | 
			
		||||
        const { contentDocument } = this.captchaFrame || {};
 | 
			
		||||
 | 
			
		||||
        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() {
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user