Compare commits
	
		
			4 Commits
		
	
	
		
			providers/
			...
			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", |                 "@formatjs/intl-listformat": "^7.5.7", | ||||||
|                 "@fortawesome/fontawesome-free": "^6.6.0", |                 "@fortawesome/fontawesome-free": "^6.6.0", | ||||||
|                 "@goauthentik/api": "^2025.2.4-1745325566", |                 "@goauthentik/api": "^2025.2.4-1745325566", | ||||||
|                 "@lit-labs/ssr": "^3.2.2", |  | ||||||
|                 "@lit/context": "^1.1.2", |                 "@lit/context": "^1.1.2", | ||||||
|                 "@lit/localize": "^0.12.2", |                 "@lit/localize": "^0.12.2", | ||||||
|                 "@lit/reactive-element": "^2.0.4", |                 "@lit/reactive-element": "^2.0.4", | ||||||
| @ -66,6 +65,7 @@ | |||||||
|                 "remark-gfm": "^4.0.1", |                 "remark-gfm": "^4.0.1", | ||||||
|                 "remark-mdx-frontmatter": "^5.0.0", |                 "remark-mdx-frontmatter": "^5.0.0", | ||||||
|                 "style-mod": "^4.1.2", |                 "style-mod": "^4.1.2", | ||||||
|  |                 "trusted-types": "^2.0.0", | ||||||
|                 "ts-pattern": "^5.4.0", |                 "ts-pattern": "^5.4.0", | ||||||
|                 "unist-util-visit": "^5.0.0", |                 "unist-util-visit": "^5.0.0", | ||||||
|                 "webcomponent-qr-code": "^1.2.0", |                 "webcomponent-qr-code": "^1.2.0", | ||||||
| @ -2281,47 +2281,11 @@ | |||||||
|                 "@lezer/lr": "^1.0.0" |                 "@lezer/lr": "^1.0.0" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         "node_modules/@lit-labs/ssr": { |  | ||||||
|             "version": "3.3.1", |  | ||||||
|             "resolved": "https://registry.npmjs.org/@lit-labs/ssr/-/ssr-3.3.1.tgz", |  | ||||||
|             "integrity": "sha512-JlF1PempxvzrGEpRFrF+Ki0MHzR3HA51SK8Zv0cFpW9p0bPW4k0FeCwrElCu371UEpXF7RcaE2wgYaE1az0XKg==", |  | ||||||
|             "dependencies": { |  | ||||||
|                 "@lit-labs/ssr-client": "^1.1.7", |  | ||||||
|                 "@lit-labs/ssr-dom-shim": "^1.3.0", |  | ||||||
|                 "@lit/reactive-element": "^2.0.4", |  | ||||||
|                 "@parse5/tools": "^0.3.0", |  | ||||||
|                 "@types/node": "^16.0.0", |  | ||||||
|                 "enhanced-resolve": "^5.10.0", |  | ||||||
|                 "lit": "^3.1.2", |  | ||||||
|                 "lit-element": "^4.0.4", |  | ||||||
|                 "lit-html": "^3.1.2", |  | ||||||
|                 "node-fetch": "^3.2.8", |  | ||||||
|                 "parse5": "^7.1.1" |  | ||||||
|             }, |  | ||||||
|             "engines": { |  | ||||||
|                 "node": ">=13.9.0" |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         "node_modules/@lit-labs/ssr-client": { |  | ||||||
|             "version": "1.1.7", |  | ||||||
|             "resolved": "https://registry.npmjs.org/@lit-labs/ssr-client/-/ssr-client-1.1.7.tgz", |  | ||||||
|             "integrity": "sha512-VvqhY/iif3FHrlhkzEPsuX/7h/NqnfxLwVf0p8ghNIlKegRyRqgeaJevZ57s/u/LiFyKgqksRP5n+LmNvpxN+A==", |  | ||||||
|             "dependencies": { |  | ||||||
|                 "@lit/reactive-element": "^2.0.4", |  | ||||||
|                 "lit": "^3.1.2", |  | ||||||
|                 "lit-html": "^3.1.2" |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         "node_modules/@lit-labs/ssr-dom-shim": { |         "node_modules/@lit-labs/ssr-dom-shim": { | ||||||
|             "version": "1.3.0", |             "version": "1.3.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz", |             "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz", | ||||||
|             "integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==" |             "integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==" | ||||||
|         }, |         }, | ||||||
|         "node_modules/@lit-labs/ssr/node_modules/@types/node": { |  | ||||||
|             "version": "16.18.126", |  | ||||||
|             "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", |  | ||||||
|             "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==" |  | ||||||
|         }, |  | ||||||
|         "node_modules/@lit/context": { |         "node_modules/@lit/context": { | ||||||
|             "version": "1.1.5", |             "version": "1.1.5", | ||||||
|             "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.5.tgz", |             "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.5.tgz", | ||||||
| @ -3557,6 +3521,7 @@ | |||||||
|             "version": "0.3.0", |             "version": "0.3.0", | ||||||
|             "resolved": "https://registry.npmjs.org/@parse5/tools/-/tools-0.3.0.tgz", |             "resolved": "https://registry.npmjs.org/@parse5/tools/-/tools-0.3.0.tgz", | ||||||
|             "integrity": "sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==", |             "integrity": "sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==", | ||||||
|  |             "dev": true, | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "parse5": "^7.0.0" |                 "parse5": "^7.0.0" | ||||||
|             } |             } | ||||||
| @ -10723,6 +10688,7 @@ | |||||||
|             "version": "4.0.1", |             "version": "4.0.1", | ||||||
|             "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", |             "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", | ||||||
|             "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", |             "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", | ||||||
|  |             "dev": true, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">= 12" |                 "node": ">= 12" | ||||||
|             } |             } | ||||||
| @ -11343,6 +11309,7 @@ | |||||||
|             "version": "5.18.1", |             "version": "5.18.1", | ||||||
|             "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", |             "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", | ||||||
|             "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", |             "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", | ||||||
|  |             "dev": true, | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "graceful-fs": "^4.2.4", |                 "graceful-fs": "^4.2.4", | ||||||
|                 "tapable": "^2.2.0" |                 "tapable": "^2.2.0" | ||||||
| @ -13820,7 +13787,8 @@ | |||||||
|         "node_modules/graceful-fs": { |         "node_modules/graceful-fs": { | ||||||
|             "version": "4.2.11", |             "version": "4.2.11", | ||||||
|             "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", |             "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", | ||||||
|             "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" |             "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", | ||||||
|  |             "dev": true | ||||||
|         }, |         }, | ||||||
|         "node_modules/grapheme-splitter": { |         "node_modules/grapheme-splitter": { | ||||||
|             "version": "1.0.4", |             "version": "1.0.4", | ||||||
| @ -18256,6 +18224,7 @@ | |||||||
|             "version": "3.3.2", |             "version": "3.3.2", | ||||||
|             "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", |             "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", | ||||||
|             "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", |             "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", | ||||||
|  |             "dev": true, | ||||||
|             "dependencies": { |             "dependencies": { | ||||||
|                 "data-uri-to-buffer": "^4.0.0", |                 "data-uri-to-buffer": "^4.0.0", | ||||||
|                 "fetch-blob": "^3.1.4", |                 "fetch-blob": "^3.1.4", | ||||||
| @ -22373,6 +22342,7 @@ | |||||||
|             "version": "2.2.1", |             "version": "2.2.1", | ||||||
|             "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", |             "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", | ||||||
|             "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", |             "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", | ||||||
|  |             "dev": true, | ||||||
|             "engines": { |             "engines": { | ||||||
|                 "node": ">=6" |                 "node": ">=6" | ||||||
|             } |             } | ||||||
| @ -22724,6 +22694,12 @@ | |||||||
|                 "url": "https://github.com/sponsors/wooorm" |                 "url": "https://github.com/sponsors/wooorm" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/trusted-types": { | ||||||
|  |             "version": "2.0.0", | ||||||
|  |             "resolved": "https://registry.npmjs.org/trusted-types/-/trusted-types-2.0.0.tgz", | ||||||
|  |             "integrity": "sha512-Eam+AUp6lg04YjmYkuLNhEJX+6ByocrKTpY/TtfRK/gV6OmxeN0OwkIasor28SUJ606snArpPLGtPMGbqdaaUA==", | ||||||
|  |             "license": "W3C-20150513" | ||||||
|  |         }, | ||||||
|         "node_modules/ts-api-utils": { |         "node_modules/ts-api-utils": { | ||||||
|             "version": "2.1.0", |             "version": "2.1.0", | ||||||
|             "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", |             "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", | ||||||
|  | |||||||
| @ -13,7 +13,6 @@ | |||||||
|         "@formatjs/intl-listformat": "^7.5.7", |         "@formatjs/intl-listformat": "^7.5.7", | ||||||
|         "@fortawesome/fontawesome-free": "^6.6.0", |         "@fortawesome/fontawesome-free": "^6.6.0", | ||||||
|         "@goauthentik/api": "^2025.2.4-1745325566", |         "@goauthentik/api": "^2025.2.4-1745325566", | ||||||
|         "@lit-labs/ssr": "^3.2.2", |  | ||||||
|         "@lit/context": "^1.1.2", |         "@lit/context": "^1.1.2", | ||||||
|         "@lit/localize": "^0.12.2", |         "@lit/localize": "^0.12.2", | ||||||
|         "@lit/reactive-element": "^2.0.4", |         "@lit/reactive-element": "^2.0.4", | ||||||
| @ -54,6 +53,7 @@ | |||||||
|         "remark-gfm": "^4.0.1", |         "remark-gfm": "^4.0.1", | ||||||
|         "remark-mdx-frontmatter": "^5.0.0", |         "remark-mdx-frontmatter": "^5.0.0", | ||||||
|         "style-mod": "^4.1.2", |         "style-mod": "^4.1.2", | ||||||
|  |         "trusted-types": "^2.0.0", | ||||||
|         "ts-pattern": "^5.4.0", |         "ts-pattern": "^5.4.0", | ||||||
|         "unist-util-visit": "^5.0.0", |         "unist-util-visit": "^5.0.0", | ||||||
|         "webcomponent-qr-code": "^1.2.0", |         "webcomponent-qr-code": "^1.2.0", | ||||||
|  | |||||||
| @ -4,13 +4,17 @@ import { ROUTES } from "@goauthentik/admin/Routes"; | |||||||
| import { | import { | ||||||
|     EVENT_API_DRAWER_TOGGLE, |     EVENT_API_DRAWER_TOGGLE, | ||||||
|     EVENT_NOTIFICATION_DRAWER_TOGGLE, |     EVENT_NOTIFICATION_DRAWER_TOGGLE, | ||||||
|  |     EVENT_SIDEBAR_TOGGLE, | ||||||
| } from "@goauthentik/common/constants"; | } from "@goauthentik/common/constants"; | ||||||
| import { configureSentry } from "@goauthentik/common/sentry"; | import { configureSentry } from "@goauthentik/common/sentry"; | ||||||
| import { me } from "@goauthentik/common/users"; | import { me } from "@goauthentik/common/users"; | ||||||
| import { WebsocketClient } from "@goauthentik/common/ws"; | import { WebsocketClient } from "@goauthentik/common/ws"; | ||||||
| import { AuthenticatedInterface } from "@goauthentik/elements/Interface"; | import { AuthenticatedInterface } from "@goauthentik/elements/Interface"; | ||||||
|  | import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js"; | ||||||
| import "@goauthentik/elements/ak-locale-context"; | import "@goauthentik/elements/ak-locale-context"; | ||||||
| import "@goauthentik/elements/banner/EnterpriseStatusBanner"; | import "@goauthentik/elements/banner/EnterpriseStatusBanner"; | ||||||
|  | import "@goauthentik/elements/banner/EnterpriseStatusBanner"; | ||||||
|  | import "@goauthentik/elements/banner/VersionBanner"; | ||||||
| import "@goauthentik/elements/banner/VersionBanner"; | import "@goauthentik/elements/banner/VersionBanner"; | ||||||
| import "@goauthentik/elements/messages/MessageContainer"; | import "@goauthentik/elements/messages/MessageContainer"; | ||||||
| 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/Sidebar"; | ||||||
| import "@goauthentik/elements/sidebar/SidebarItem"; | import "@goauthentik/elements/sidebar/SidebarItem"; | ||||||
|  |  | ||||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | import { CSSResult, TemplateResult, css, html, nothing } from "lit"; | ||||||
| import { customElement, property, query, state } from "lit/decorators.js"; | import { customElement, property, query, state } from "lit/decorators.js"; | ||||||
| import { classMap } from "lit/directives/class-map.js"; | import { classMap } from "lit/directives/class-map.js"; | ||||||
|  |  | ||||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||||
| import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css"; | import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css"; | ||||||
|  | import PFNav from "@patternfly/patternfly/components/Nav/nav.css"; | ||||||
| import PFPage from "@patternfly/patternfly/components/Page/page.css"; | import PFPage from "@patternfly/patternfly/components/Page/page.css"; | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
| import { SessionUser, UiThemeEnum } from "@goauthentik/api"; | import { LicenseSummaryStatusEnum, SessionUser, UiThemeEnum } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| import "./AdminSidebar"; | import { | ||||||
|  |     AdminSidebarEnterpriseEntries, | ||||||
|  |     AdminSidebarEntries, | ||||||
|  |     renderSidebarItems, | ||||||
|  | } from "./AdminSidebar.js"; | ||||||
|  |  | ||||||
| if (process.env.NODE_ENV === "development") { | if (process.env.NODE_ENV === "development") { | ||||||
|     await import("@goauthentik/esbuild-plugin-live-reload/client"); |     await import("@goauthentik/esbuild-plugin-live-reload/client"); | ||||||
| } | } | ||||||
|  |  | ||||||
| @customElement("ak-interface-admin") | @customElement("ak-interface-admin") | ||||||
| export class AdminInterface extends AuthenticatedInterface { | export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) { | ||||||
|  |     //#region Properties | ||||||
|  |  | ||||||
|     @property({ type: Boolean }) |     @property({ type: Boolean }) | ||||||
|     notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); |     notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); | ||||||
|  |  | ||||||
| @ -54,12 +65,24 @@ export class AdminInterface extends AuthenticatedInterface { | |||||||
|     @query("ak-about-modal") |     @query("ak-about-modal") | ||||||
|     aboutModal?: AboutModal; |     aboutModal?: AboutModal; | ||||||
|  |  | ||||||
|  |     @property({ type: Boolean, reflect: true }) | ||||||
|  |     public sidebarOpen = true; | ||||||
|  |  | ||||||
|  |     #toggleSidebar = () => { | ||||||
|  |         this.sidebarOpen = !this.sidebarOpen; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     //#endregion | ||||||
|  |  | ||||||
|  |     //#region Styles | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return [ |         return [ | ||||||
|             PFBase, |             PFBase, | ||||||
|             PFPage, |             PFPage, | ||||||
|             PFButton, |             PFButton, | ||||||
|             PFDrawer, |             PFDrawer, | ||||||
|  |             PFNav, | ||||||
|             css` |             css` | ||||||
|                 .pf-c-page__main, |                 .pf-c-page__main, | ||||||
|                 .pf-c-drawer__content, |                 .pf-c-drawer__content, | ||||||
| @ -67,23 +90,30 @@ export class AdminInterface extends AuthenticatedInterface { | |||||||
|                     z-index: auto !important; |                     z-index: auto !important; | ||||||
|                     background-color: transparent; |                     background-color: transparent; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 .display-none { |                 .display-none { | ||||||
|                     display: none; |                     display: none; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 .pf-c-page { |                 .pf-c-page { | ||||||
|                     background-color: var(--pf-c-page--BackgroundColor) !important; |                     background-color: var(--pf-c-page--BackgroundColor) !important; | ||||||
|                 } |                 } | ||||||
|                 /* Global page background colour */ |  | ||||||
|                 :host([theme="dark"]) .pf-c-page { |                 :host([theme="dark"]) { | ||||||
|                     --pf-c-page--BackgroundColor: var(--ak-dark-background); |                     /* 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; |                     grid-area: header; | ||||||
|                 } |                 } | ||||||
|                 ak-admin-sidebar { |  | ||||||
|  |                 .ak-sidebar { | ||||||
|                     grid-area: nav; |                     grid-area: nav; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 .pf-c-drawer__panel { |                 .pf-c-drawer__panel { | ||||||
|                     z-index: var(--pf-global--ZIndex--xl); |                     z-index: var(--pf-global--ZIndex--xl); | ||||||
|                 } |                 } | ||||||
| @ -91,9 +121,19 @@ export class AdminInterface extends AuthenticatedInterface { | |||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     //#endregion | ||||||
|  |  | ||||||
|  |     //#region Lifecycle | ||||||
|  |  | ||||||
|     constructor() { |     constructor() { | ||||||
|         super(); |         super(); | ||||||
|         this.ws = new WebsocketClient(); |         this.ws = new WebsocketClient(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public connectedCallback(): void { | ||||||
|  |         super.connectedCallback(); | ||||||
|  |  | ||||||
|  |         window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar); | ||||||
|  |  | ||||||
|         window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => { |         window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => { | ||||||
|             this.notificationDrawerOpen = !this.notificationDrawerOpen; |             this.notificationDrawerOpen = !this.notificationDrawerOpen; | ||||||
| @ -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> { |     async firstUpdated(): Promise<void> { | ||||||
|         configureSentry(true); |         configureSentry(true); | ||||||
|         this.user = await me(); |         this.user = await me(); | ||||||
| @ -118,6 +163,7 @@ export class AdminInterface extends AuthenticatedInterface { | |||||||
|             this.user.user.isSuperuser || |             this.user.user.isSuperuser || | ||||||
|             // TODO: somehow add `access_admin_interface` to the API schema |             // TODO: somehow add `access_admin_interface` to the API schema | ||||||
|             this.user.user.systemPermissions.includes("access_admin_interface"); |             this.user.user.systemPermissions.includes("access_admin_interface"); | ||||||
|  |  | ||||||
|         if (!canAccessAdmin && this.user.user.pk > 0) { |         if (!canAccessAdmin && this.user.user.pk > 0) { | ||||||
|             window.location.assign("/if/user/"); |             window.location.assign("/if/user/"); | ||||||
|         } |         } | ||||||
| @ -125,10 +171,14 @@ export class AdminInterface extends AuthenticatedInterface { | |||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
|         const sidebarClasses = { |         const sidebarClasses = { | ||||||
|  |             "pf-c-page__sidebar": true, | ||||||
|             "pf-m-light": this.activeTheme === UiThemeEnum.Light, |             "pf-m-light": this.activeTheme === UiThemeEnum.Light, | ||||||
|  |             "pf-m-expanded": this.sidebarOpen, | ||||||
|  |             "pf-m-collapsed": !this.sidebarOpen, | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         const drawerOpen = this.notificationDrawerOpen || this.apiDrawerOpen; |         const drawerOpen = this.notificationDrawerOpen || this.apiDrawerOpen; | ||||||
|  |  | ||||||
|         const drawerClasses = { |         const drawerClasses = { | ||||||
|             "pf-m-expanded": drawerOpen, |             "pf-m-expanded": drawerOpen, | ||||||
|             "pf-m-collapsed": !drawerOpen, |             "pf-m-collapsed": !drawerOpen, | ||||||
| @ -136,11 +186,18 @@ export class AdminInterface extends AuthenticatedInterface { | |||||||
|  |  | ||||||
|         return html` <ak-locale-context> |         return html` <ak-locale-context> | ||||||
|             <div class="pf-c-page"> |             <div class="pf-c-page"> | ||||||
|                 <ak-enterprise-status interface="admin"></ak-enterprise-status> |                 <ak-page-navbar> | ||||||
|                 <ak-version-banner></ak-version-banner> |                     <ak-version-banner></ak-version-banner> | ||||||
|                 <ak-admin-sidebar |                     <ak-enterprise-status interface="admin"></ak-enterprise-status> | ||||||
|                     class="pf-c-page__sidebar ${classMap(sidebarClasses)}" |                 </ak-page-navbar> | ||||||
|                 ></ak-admin-sidebar> |  | ||||||
|  |                 <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-page__drawer"> | ||||||
|                     <div class="pf-c-drawer ${classMap(drawerClasses)}"> |                     <div class="pf-c-drawer ${classMap(drawerClasses)}"> | ||||||
|                         <div class="pf-c-drawer__main"> |                         <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 { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route"; | ||||||
| import { getRootStyle } from "@goauthentik/elements/utils/getRootStyle"; |  | ||||||
| import { spread } from "@open-wc/lit-helpers"; | import { spread } from "@open-wc/lit-helpers"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { TemplateResult, html, nothing } from "lit"; | import { TemplateResult, html, nothing } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators.js"; | import { repeat } from "lit/directives/repeat.js"; | ||||||
| import { map } from "lit/directives/map.js"; |  | ||||||
|  |  | ||||||
| import { UiThemeEnum } from "@goauthentik/api"; | // The second attribute type is of string[] to help with the 'activeWhen' control, which was | ||||||
| import type { SessionUser, UserSelf } from "@goauthentik/api"; | // commonplace and singular enough to merit its own handler. | ||||||
|  | type SidebarEntry = [ | ||||||
|  |     path: string | null, | ||||||
|  |     label: string, | ||||||
|  |     attributes?: Record<string, any> | string[] | null, // eslint-disable-line | ||||||
|  |     children?: SidebarEntry[], | ||||||
|  | ]; | ||||||
|  |  | ||||||
| @customElement("ak-admin-sidebar") | /** | ||||||
| export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement)) { |  * Recursively renders a sidebar entry. | ||||||
|     @property({ type: Boolean, reflect: true }) |  */ | ||||||
|     open = true; | export function renderSidebarItem([ | ||||||
|  |     path, | ||||||
|  |     label, | ||||||
|  |     attributes, | ||||||
|  |     children, | ||||||
|  | ]: SidebarEntry): TemplateResult { | ||||||
|  |     const properties = Array.isArray(attributes) | ||||||
|  |         ? { ".activeWhen": attributes } | ||||||
|  |         : (attributes ?? {}); | ||||||
|  |  | ||||||
|     @state() |     if (path) { | ||||||
|     impersonation: UserSelf["username"] | null = null; |         properties.path = path; | ||||||
|  |  | ||||||
|     constructor() { |  | ||||||
|         super(); |  | ||||||
|         me().then((user: SessionUser) => { |  | ||||||
|             this.impersonation = user.original ? user.user.username : null; |  | ||||||
|         }); |  | ||||||
|         this.toggleOpen = this.toggleOpen.bind(this); |  | ||||||
|         this.checkWidth = this.checkWidth.bind(this); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // This has to be a bound method so the event listener can be removed on disconnection as |     return html`<ak-sidebar-item ${spread(properties)}> | ||||||
|     // needed. |         ${label ? html`<span slot="label">${label}</span>` : nothing} | ||||||
|     toggleOpen() { |         ${children ? renderSidebarItems(children) : nothing} | ||||||
|         this.open = !this.open; |     </ak-sidebar-item>`; | ||||||
|     } |  | ||||||
|  |  | ||||||
|     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; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | /** | ||||||
|     interface HTMLElementTagNameMap { |  * Recursively renders a collection of sidebar entries. | ||||||
|         "ak-admin-sidebar": AkAdminSidebar; |  */ | ||||||
|     } | export function renderSidebarItems(entries: readonly SidebarEntry[]) { | ||||||
|  |     console.debug("authentik/sidebar: Rendering sidebar items", entries); | ||||||
|  |     return repeat(entries, ([path, label]) => path || label, renderSidebarItem); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // prettier-ignore | ||||||
|  | export const AdminSidebarEntries: readonly SidebarEntry[] = [ | ||||||
|  |     [null, msg("Dashboards"), { "?expanded": true }, [ | ||||||
|  |         ["/administration/overview", msg("Overview")], | ||||||
|  |         ["/administration/dashboard/users", msg("User Statistics")], | ||||||
|  |         ["/administration/system-tasks", msg("System Tasks")]] | ||||||
|  |     ], | ||||||
|  |     [null, msg("Applications"), null, [ | ||||||
|  |         ["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]], | ||||||
|  |         ["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]], | ||||||
|  |         ["/outpost/outposts", msg("Outposts")]] | ||||||
|  |     ], | ||||||
|  |     [null, msg("Events"), null, [ | ||||||
|  |         ["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]], | ||||||
|  |         ["/events/rules", msg("Notification Rules")], | ||||||
|  |         ["/events/transports", msg("Notification Transports")]] | ||||||
|  |     ], | ||||||
|  |     [null, msg("Customization"), null, [ | ||||||
|  |         ["/policy/policies", msg("Policies")], | ||||||
|  |         ["/core/property-mappings", msg("Property Mappings")], | ||||||
|  |         ["/blueprints/instances", msg("Blueprints")], | ||||||
|  |         ["/policy/reputation", msg("Reputation scores")]] | ||||||
|  |     ], | ||||||
|  |     [null, msg("Flows and Stages"), null, [ | ||||||
|  |         ["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]], | ||||||
|  |         ["/flow/stages", msg("Stages")], | ||||||
|  |         ["/flow/stages/prompts", msg("Prompts")]] | ||||||
|  |     ], | ||||||
|  |     [null, msg("Directory"), null, [ | ||||||
|  |         ["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]], | ||||||
|  |         ["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]], | ||||||
|  |         ["/identity/roles", msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]], | ||||||
|  |         ["/identity/initial-permissions", msg("Initial Permissions"), [`^/identity/initial-permissions/(?<id>${ID_REGEX})$`]], | ||||||
|  |         ["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]], | ||||||
|  |         ["/core/tokens", msg("Tokens and App passwords")], | ||||||
|  |         ["/flow/stages/invitations", msg("Invitations")]] | ||||||
|  |     ], | ||||||
|  |     [null, msg("System"), null, [ | ||||||
|  |         ["/core/brands", msg("Brands")], | ||||||
|  |         ["/crypto/certificates", msg("Certificates")], | ||||||
|  |         ["/outpost/integrations", msg("Outpost Integrations")], | ||||||
|  |         ["/admin/settings", msg("Settings")]] | ||||||
|  |     ], | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | // prettier-ignore | ||||||
|  | export const AdminSidebarEnterpriseEntries: readonly SidebarEntry[] = [ | ||||||
|  |     [null, msg("Enterprise"), null, [ | ||||||
|  |         ["/enterprise/licenses", msg("Licenses"), null] | ||||||
|  |     ], | ||||||
|  | ]] | ||||||
|  | |||||||
| @ -58,9 +58,6 @@ export class AdminOverviewPage extends AdminOverviewBase { | |||||||
|             PFContent, |             PFContent, | ||||||
|             PFDivider, |             PFDivider, | ||||||
|             css` |             css` | ||||||
|                 .pf-l-grid__item { |  | ||||||
|                     height: 100%; |  | ||||||
|                 } |  | ||||||
|                 .pf-l-grid__item.big-graph-container { |                 .pf-l-grid__item.big-graph-container { | ||||||
|                     height: 35em; |                     height: 35em; | ||||||
|                 } |                 } | ||||||
| @ -74,6 +71,10 @@ export class AdminOverviewPage extends AdminOverviewBase { | |||||||
|                     line-height: normal; |                     line-height: normal; | ||||||
|                     font-size: var(--pf-global--icon--FontSize--sm); |                     font-size: var(--pf-global--icon--FontSize--sm); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |                 .chart-item { | ||||||
|  |                     aspect-ratio: 2 / 1; | ||||||
|  |                 } | ||||||
|             `, |             `, | ||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
| @ -94,22 +95,31 @@ export class AdminOverviewPage extends AdminOverviewBase { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     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}> |         return html` <ak-page-header | ||||||
|                 <span slot="header"> ${msg(str`Welcome, ${name || ""}.`)} </span> |                 header=${msg(str`Welcome, ${username || ""}.`)} | ||||||
|  |                 description=${msg("General system status")} | ||||||
|  |                 ?hasIcon=${false} | ||||||
|  |             > | ||||||
|             </ak-page-header> |             </ak-page-header> | ||||||
|             <section class="pf-c-page__main-section"> |             <section class="pf-c-page__main-section"> | ||||||
|                 <div class="pf-l-grid pf-m-gutter"> |                 <div class="pf-l-grid pf-m-gutter"> | ||||||
|                     <!-- row 1 --> |                     <div class="pf-l-grid__item pf-m-12-col pf-m-2-row pf-m-9-col-on-xl"> | ||||||
|                     <div |                         <ak-recent-events pageSize="6"></ak-recent-events> | ||||||
|                         class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl pf-l-grid pf-m-gutter" |                     </div> | ||||||
|                     > |                     <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-sm pf-m-3-col-on-xl"> | ||||||
|                         <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl"> |                         <ak-quick-actions-card .actions=${this.quickActions}> | ||||||
|                             <ak-quick-actions-card .actions=${this.quickActions}> |                         </ak-quick-actions-card> | ||||||
|                             </ak-quick-actions-card> |                     </div> | ||||||
|                         </div> |  | ||||||
|                         <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl"> |                     <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-sm pf-m-3-col-on-xl"> | ||||||
|  |                         <ak-admin-status-version> </ak-admin-status-version> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="pf-l-grid pf-l-grid__item pf-m-12-col pf-m-gutter"> | ||||||
|  |                         ${this.renderSecondaryRow()} | ||||||
|  |  | ||||||
|  |                         <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-md chart-item"> | ||||||
|                             <ak-aggregate-card |                             <ak-aggregate-card | ||||||
|                                 icon="pf-icon pf-icon-zone" |                                 icon="pf-icon pf-icon-zone" | ||||||
|                                 header=${msg("Outpost status")} |                                 header=${msg("Outpost status")} | ||||||
| @ -118,24 +128,13 @@ export class AdminOverviewPage extends AdminOverviewBase { | |||||||
|                                 <ak-admin-status-chart-outpost></ak-admin-status-chart-outpost> |                                 <ak-admin-status-chart-outpost></ak-admin-status-chart-outpost> | ||||||
|                             </ak-aggregate-card> |                             </ak-aggregate-card> | ||||||
|                         </div> |                         </div> | ||||||
|                         <div |                         <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-md chart-item"> | ||||||
|                             class="pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-4-col-on-2xl" |  | ||||||
|                         > |  | ||||||
|                             <ak-aggregate-card icon="fa fa-sync-alt" header=${msg("Sync status")}> |                             <ak-aggregate-card icon="fa fa-sync-alt" header=${msg("Sync status")}> | ||||||
|                                 <ak-admin-status-chart-sync></ak-admin-status-chart-sync> |                                 <ak-admin-status-chart-sync></ak-admin-status-chart-sync> | ||||||
|                             </ak-aggregate-card> |                             </ak-aggregate-card> | ||||||
|                         </div> |                         </div> | ||||||
|                         <div class="pf-l-grid__item pf-m-12-col"> |  | ||||||
|                             <hr class="pf-c-divider" /> |  | ||||||
|                         </div> |  | ||||||
|                         ${this.renderCards()} |  | ||||||
|                     </div> |  | ||||||
|                     <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl"> |  | ||||||
|                         <ak-recent-events pageSize="6"></ak-recent-events> |  | ||||||
|                     </div> |  | ||||||
|                     <div class="pf-l-grid__item pf-m-12-col"> |  | ||||||
|                         <hr class="pf-c-divider" /> |  | ||||||
|                     </div> |                     </div> | ||||||
|  |  | ||||||
|                     <!-- row 3 --> |                     <!-- row 3 --> | ||||||
|                     <div |                     <div | ||||||
|                         class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-8-col-on-2xl big-graph-container" |                         class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-8-col-on-2xl big-graph-container" | ||||||
| @ -163,32 +162,34 @@ export class AdminOverviewPage extends AdminOverviewBase { | |||||||
|             </section>`; |             </section>`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderCards() { |     renderSecondaryRow() { | ||||||
|         const isEnterprise = this.hasEnterpriseLicense; |         const isEnterprise = this.hasEnterpriseLicense; | ||||||
|  |         const colSpan = isEnterprise ? 4 : 6; | ||||||
|  |  | ||||||
|         const classes = { |         const classes = { | ||||||
|             "card-container": true, |             "card-container": true, | ||||||
|             "pf-l-grid__item": true, |             "pf-l-grid__item": true, | ||||||
|             "pf-m-6-col": true, |             [`pf-m-12-col`]: true, | ||||||
|             "pf-m-4-col-on-md": !isEnterprise, |             [`pf-m-${colSpan}-col-on-md`]: true, | ||||||
|             "pf-m-4-col-on-xl": !isEnterprise, |  | ||||||
|             "pf-m-3-col-on-md": isEnterprise, |  | ||||||
|             "pf-m-3-col-on-xl": isEnterprise, |  | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         return html`<div class=${classMap(classes)}> |         return html` | ||||||
|  |             <div class=${classMap(classes)}> | ||||||
|                 <ak-admin-status-system> </ak-admin-status-system> |                 <ak-admin-status-system> </ak-admin-status-system> | ||||||
|             </div> |             </div> | ||||||
|             <div class=${classMap(classes)}> |  | ||||||
|                 <ak-admin-status-version> </ak-admin-status-version> |  | ||||||
|             </div> |  | ||||||
|             <div class=${classMap(classes)}> |             <div class=${classMap(classes)}> | ||||||
|                 <ak-admin-status-card-workers> </ak-admin-status-card-workers> |                 <ak-admin-status-card-workers> </ak-admin-status-card-workers> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|             ${isEnterprise |             ${isEnterprise | ||||||
|                 ? html` <div class=${classMap(classes)}> |                 ? html` | ||||||
|                       <ak-admin-fips-status-system> </ak-admin-fips-status-system> |                       <div class=${classMap(classes)}> | ||||||
|                   </div>` |                           <ak-admin-fips-status-system> </ak-admin-fips-status-system> | ||||||
|                 : nothing} `; |                       </div> | ||||||
|  |                   ` | ||||||
|  |                 : nothing} | ||||||
|  |         `; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderActions() { |     renderActions() { | ||||||
|  | |||||||
| @ -83,13 +83,10 @@ export class AdminSettingsPage extends AKElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     render() { |     render() { | ||||||
|         if (!this.settings) { |         if (!this.settings) return nothing; | ||||||
|             return nothing; |  | ||||||
|         } |  | ||||||
|         return html` |         return html` | ||||||
|             <ak-page-header icon="fa fa-cog" header="" description=""> |             <ak-page-header icon="fa fa-cog" header="${msg("System settings")}"> </ak-page-header> | ||||||
|                 <span slot="header"> ${msg("System settings")} </span> |  | ||||||
|             </ak-page-header> |  | ||||||
|             <section class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"> |             <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"> | ||||||
|                     <div class="pf-c-card__body"> |                     <div class="pf-c-card__body"> | ||||||
|  | |||||||
| @ -1,26 +1,110 @@ | |||||||
| import type { Config as DOMPurifyConfig } from "dompurify"; | import type { Config as DOMPurifyConfig } from "dompurify"; | ||||||
| import DOMPurify from "dompurify"; | import DOMPurify from "dompurify"; | ||||||
|  | import { trustedTypes } from "trusted-types"; | ||||||
|  |  | ||||||
| import { render } from "@lit-labs/ssr"; | import { render } from "lit"; | ||||||
| import { collectResult } from "@lit-labs/ssr/lib/render-result.js"; |  | ||||||
| import { TemplateResult, html } from "lit"; |  | ||||||
| import { unsafeHTML } from "lit/directives/unsafe-html.js"; | import { unsafeHTML } from "lit/directives/unsafe-html.js"; | ||||||
| import { until } from "lit/directives/until.js"; |  | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Trusted types policy that escapes HTML content in place. | ||||||
|  |  * | ||||||
|  |  * @see {@linkcode SanitizedTrustPolicy} to strip HTML content. | ||||||
|  |  * | ||||||
|  |  * @returns {TrustedHTML} All HTML content, escaped. | ||||||
|  |  */ | ||||||
|  | export const EscapeTrustPolicy = trustedTypes.createPolicy("authentik-escape", { | ||||||
|  |     createHTML: (untrustedHTML: string) => { | ||||||
|  |         return DOMPurify.sanitize(untrustedHTML, { | ||||||
|  |             RETURN_TRUSTED_TYPE: false, | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Trusted types policy, stripping all HTML content. | ||||||
|  |  * | ||||||
|  |  * @returns {TrustedHTML} Text content only, all HTML tags stripped. | ||||||
|  |  */ | ||||||
|  | export const SanitizedTrustPolicy = trustedTypes.createPolicy("authentik-sanitize", { | ||||||
|  |     createHTML: (untrustedHTML: string) => { | ||||||
|  |         return DOMPurify.sanitize(untrustedHTML, { | ||||||
|  |             RETURN_TRUSTED_TYPE: false, | ||||||
|  |             ALLOWED_TAGS: ["#text"], | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Trusted types policy, allowing a minimal set of _safe_ HTML tags supplied by | ||||||
|  |  * a trusted source, such as the brand API. | ||||||
|  |  */ | ||||||
|  | export const BrandedHTMLPolicy = trustedTypes.createPolicy("authentik-restrict", { | ||||||
|  |     createHTML: (untrustedHTML: string) => { | ||||||
|  |         return DOMPurify.sanitize(untrustedHTML, { | ||||||
|  |             RETURN_TRUSTED_TYPE: false, | ||||||
|  |             FORBID_TAGS: [ | ||||||
|  |                 "script", | ||||||
|  |                 "style", | ||||||
|  |                 "iframe", | ||||||
|  |                 "link", | ||||||
|  |                 "object", | ||||||
|  |                 "embed", | ||||||
|  |                 "applet", | ||||||
|  |                 "meta", | ||||||
|  |                 "base", | ||||||
|  |                 "form", | ||||||
|  |                 "input", | ||||||
|  |                 "textarea", | ||||||
|  |                 "select", | ||||||
|  |                 "button", | ||||||
|  |             ], | ||||||
|  |             FORBID_ATTR: [ | ||||||
|  |                 "onerror", | ||||||
|  |                 "onclick", | ||||||
|  |                 "onload", | ||||||
|  |                 "onmouseover", | ||||||
|  |                 "onmouseout", | ||||||
|  |                 "onmouseup", | ||||||
|  |                 "onmousedown", | ||||||
|  |                 "onfocus", | ||||||
|  |                 "onblur", | ||||||
|  |                 "onsubmit", | ||||||
|  |             ], | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export type AuthentikTrustPolicy = | ||||||
|  |     | typeof EscapeTrustPolicy | ||||||
|  |     | typeof SanitizedTrustPolicy | ||||||
|  |     | typeof BrandedHTMLPolicy; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Sanitize an untrusted HTML string using a trusted types policy. | ||||||
|  |  */ | ||||||
|  | export function sanitizeHTML(trustPolicy: AuthentikTrustPolicy, untrustedHTML: string) { | ||||||
|  |     return unsafeHTML(trustPolicy.createHTML(untrustedHTML).toString()); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * DOMPurify configuration for strict sanitization. | ||||||
|  |  * | ||||||
|  |  * This configuration only allows text nodes and disallows all HTML tags. | ||||||
|  |  */ | ||||||
| export const DOM_PURIFY_STRICT = { | export const DOM_PURIFY_STRICT = { | ||||||
|     ALLOWED_TAGS: ["#text"], |     ALLOWED_TAGS: ["#text"], | ||||||
| } as const satisfies DOMPurifyConfig; | } as const satisfies DOMPurifyConfig; | ||||||
|  |  | ||||||
| export async function renderStatic(input: TemplateResult): Promise<string> { | /** | ||||||
|     return await collectResult(render(input)); |  * Render untrusted HTML to a string without escaping it. | ||||||
| } |  * | ||||||
|  |  * @returns {string} The rendered HTML string. | ||||||
|  |  */ | ||||||
|  | export function renderStaticHTMLUnsafe(untrustedHTML: unknown): string { | ||||||
|  |     const container = document.createElement("html"); | ||||||
|  |     render(untrustedHTML, container); | ||||||
|  |  | ||||||
| export function purify(input: TemplateResult): TemplateResult { |     const result = container.innerHTML; | ||||||
|     return html`${until( |  | ||||||
|         (async () => { |     return result; | ||||||
|             const rendered = await renderStatic(input); |  | ||||||
|             const purified = DOMPurify.sanitize(rendered); |  | ||||||
|             return html`${unsafeHTML(purified)}`; |  | ||||||
|         })(), |  | ||||||
|     )}`; |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -17,6 +17,13 @@ | |||||||
|  |  | ||||||
|     /* Minimum width after which the sidebar becomes automatic */ |     /* Minimum width after which the sidebar becomes automatic */ | ||||||
|     --ak-sidebar--minimum-auto-width: 80rem; |     --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) { | @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 { |                 :host([theme="light"]) .pf-c-page__header-tools-group .pf-c-button { | ||||||
|                     color: var(--ak-global--Color--100) !important; |                     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() { |     renderImpersonation() { | ||||||
|         if (!this.me?.original) { |         if (!this.me?.original) return nothing; | ||||||
|             return nothing; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const onClick = async () => { |         const onClick = async () => { | ||||||
|             await new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve(); |             await new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve(); | ||||||
| @ -175,6 +179,14 @@ export class NavigationButtons extends AKElement { | |||||||
|             </div>`; |             </div>`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     renderAvatar() { | ||||||
|  |         return html`<img | ||||||
|  |             class="pf-c-avatar" | ||||||
|  |             src=${ifDefined(this.me?.user.avatar)} | ||||||
|  |             alt="${msg("Avatar image")}" | ||||||
|  |         />`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     get userDisplayName() { |     get userDisplayName() { | ||||||
|         return match<UserDisplay | undefined, string | undefined>(this.uiConfig?.navbar.userDisplay) |         return match<UserDisplay | undefined, string | undefined>(this.uiConfig?.navbar.userDisplay) | ||||||
|             .with(UserDisplay.username, () => this.me?.user.username) |             .with(UserDisplay.username, () => this.me?.user.username) | ||||||
| @ -212,11 +224,7 @@ export class NavigationButtons extends AKElement { | |||||||
|                       </div> |                       </div> | ||||||
|                   </div>` |                   </div>` | ||||||
|                 : nothing} |                 : nothing} | ||||||
|             <img |             ${this.renderAvatar()} | ||||||
|                 class="pf-c-avatar" |  | ||||||
|                 src=${ifDefined(this.me?.user.avatar)} |  | ||||||
|                 alt="${msg("Avatar image")}" |  | ||||||
|             /> |  | ||||||
|         </div>`; |         </div>`; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -5,20 +5,23 @@ import { | |||||||
| } from "@goauthentik/common/constants"; | } from "@goauthentik/common/constants"; | ||||||
| import { globalAK } from "@goauthentik/common/global"; | import { globalAK } from "@goauthentik/common/global"; | ||||||
| import { currentInterface } from "@goauthentik/common/sentry"; | import { currentInterface } from "@goauthentik/common/sentry"; | ||||||
| import { UIConfig, UserDisplay, uiConfig } from "@goauthentik/common/ui/config"; | import { UIConfig, UserDisplay, getConfigForUser } from "@goauthentik/common/ui/config"; | ||||||
| import { me } from "@goauthentik/common/users"; | import { me } from "@goauthentik/common/users"; | ||||||
| import "@goauthentik/components/ak-nav-buttons"; | import "@goauthentik/components/ak-nav-buttons"; | ||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
| import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; | 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 "@patternfly/elements/pf-tooltip/pf-tooltip.js"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | 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 { customElement, property, state } from "lit/decorators.js"; | ||||||
|  |  | ||||||
| import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css"; | import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css"; | ||||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||||
| import PFContent from "@patternfly/patternfly/components/Content/content.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 PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css"; | ||||||
| import PFNotificationBadge from "@patternfly/patternfly/components/NotificationBadge/notification-badge.css"; | import PFNotificationBadge from "@patternfly/patternfly/components/NotificationBadge/notification-badge.css"; | ||||||
| import PFPage from "@patternfly/patternfly/components/Page/page.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"; | import { SessionUser } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| @customElement("ak-page-header") | //#region Page Navbar | ||||||
| export class PageHeader extends WithBrandConfig(AKElement) { |  | ||||||
|     @property() |  | ||||||
|     icon?: string; |  | ||||||
|  |  | ||||||
|     @property({ type: Boolean }) | export interface PageNavbarDetails { | ||||||
|     iconImage = false; |     header?: string; | ||||||
|  |  | ||||||
|     @property() |  | ||||||
|     header = ""; |  | ||||||
|  |  | ||||||
|     @property() |  | ||||||
|     description?: 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() |     private static elementRef: AKPageNavbar | null = null; | ||||||
|     me?: SessionUser; |  | ||||||
|  |  | ||||||
|     @state() |     static readonly setNavbarDetails = (detail: Partial<PageNavbarDetails>): void => { | ||||||
|     uiConfig!: UIConfig; |         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[] { |     static get styles(): CSSResult[] { | ||||||
|         return [ |         return [ | ||||||
|             PFBase, |             PFBase, | ||||||
|             PFButton, |             PFButton, | ||||||
|             PFPage, |             PFPage, | ||||||
|  |             PFDrawer, | ||||||
|  |  | ||||||
|             PFNotificationBadge, |             PFNotificationBadge, | ||||||
|             PFContent, |             PFContent, | ||||||
|             PFAvatar, |             PFAvatar, | ||||||
| @ -63,143 +84,392 @@ export class PageHeader extends WithBrandConfig(AKElement) { | |||||||
|                     position: sticky; |                     position: sticky; | ||||||
|                     top: 0; |                     top: 0; | ||||||
|                     z-index: var(--pf-global--ZIndex--lg); |                     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: var(--pf-global--BorderWidth--sm); | ||||||
|                     border-bottom-style: solid; |                     border-bottom-style: solid; | ||||||
|                     border-bottom-color: var(--pf-global--BorderColor--100); |                     border-bottom-color: var(--pf-global--BorderColor--100); | ||||||
|  |                     background-color: var(--pf-c-page--BackgroundColor); | ||||||
|  |  | ||||||
|                     display: flex; |                     display: flex; | ||||||
|                     flex-direction: row; |                     flex-direction: row; | ||||||
|                     min-height: 114px; |                     min-height: 6rem; | ||||||
|                     max-height: 114px; |  | ||||||
|                     background-color: var(--pf-c-page--BackgroundColor); |                     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; |                 .brand { | ||||||
|                     flex-shrink: 1; |                     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; |                     display: flex; | ||||||
|                     flex-direction: column; |  | ||||||
|                     justify-content: center; |                     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, |                 .sidebar-trigger, | ||||||
|                 .notification-trigger { |                 .notification-trigger { | ||||||
|                     font-size: 24px; |                     font-size: 1.5rem; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 .notification-trigger.has-notifications { |                 .notification-trigger.has-notifications { | ||||||
|                     color: var(--pf-global--active-color--100); |                     color: var(--pf-global--active-color--100); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |                 .page-title { | ||||||
|  |                     display: flex; | ||||||
|  |                     gap: var(--pf-global--spacer--xs); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                 h1 { |                 h1 { | ||||||
|                     display: flex; |                     display: flex; | ||||||
|                     flex-direction: row; |                     flex-direction: row; | ||||||
|                     align-items: center !important; |                     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() { |     //#endregion | ||||||
|         super(); |  | ||||||
|         window.addEventListener(EVENT_WS_MESSAGE, () => { |  | ||||||
|             this.firstUpdated(); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async firstUpdated() { |     //#region Properties | ||||||
|         this.me = await me(); |  | ||||||
|         this.uiConfig = await uiConfig(); |  | ||||||
|         this.uiConfig.navbar.userDisplay = UserDisplay.none; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     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(); |         const currentIf = currentInterface(); | ||||||
|         let title = this.brand?.brandingTitle || TITLE_DEFAULT; |         let title = this.brand?.brandingTitle || TITLE_DEFAULT; | ||||||
|  |  | ||||||
|         if (currentIf === "admin") { |         if (currentIf === "admin") { | ||||||
|             title = `${msg("Admin")} - ${title}`; |             title = `${msg("Admin")} - ${title}`; | ||||||
|         } |         } | ||||||
|         // Prepend the header to the title |         // Prepend the header to the title | ||||||
|         if (header !== undefined && header !== "") { |         if (header) { | ||||||
|             title = `${header} - ${title}`; |             title = `${header} - ${title}`; | ||||||
|         } |         } | ||||||
|         document.title = 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() { |     willUpdate() { | ||||||
|         // Always update title, even if there's no header value set, |         // Always update title, even if there's no header value set, | ||||||
|         // as in that case we still need to return to the generic title |         // as in that case we still need to return to the generic title | ||||||
|         this.setTitle(this.header); |         this.#setTitle(this.header); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     //#endregion | ||||||
|  |  | ||||||
|  |     //#region Render | ||||||
|  |  | ||||||
|     renderIcon() { |     renderIcon() { | ||||||
|         if (this.icon) { |         if (this.icon) { | ||||||
|             if (this.iconImage && !this.icon.startsWith("fa://")) { |             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 "); |             const icon = this.icon.replaceAll("fa://", "fa "); | ||||||
|             return html`<i class=${icon}></i>`; |  | ||||||
|  |             return html`<i class="accent-icon ${icon}"></i>`; | ||||||
|         } |         } | ||||||
|         return nothing; |         return nothing; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
|         return html`<div class="bar"> |         return html`<navbar aria-label="Main" class="navbar"> | ||||||
|             <button |                 <aside class="brand ${this.open ? "" : "pf-m-collapsed"}"> | ||||||
|                 class="sidebar-trigger pf-c-button pf-m-plain" |                     <a href="#/"> | ||||||
|                 @click=${() => { |                         <div class="logo"> | ||||||
|                     this.dispatchEvent( |                             <img | ||||||
|                         new CustomEvent(EVENT_SIDEBAR_TOGGLE, { |                                 src=${themeImage( | ||||||
|                             bubbles: true, |                                     this.brand?.brandingLogo ?? DefaultBrand.brandingLogo, | ||||||
|                             composed: true, |                                 )} | ||||||
|                         }), |                                 alt="${msg("authentik Logo")}" | ||||||
|                     ); |                                 loading="lazy" | ||||||
|                 }} |                             /> | ||||||
|             > |                         </div> | ||||||
|                 <i class="fas fa-bars"></i> |                     </a> | ||||||
|             </button> |                 </aside> | ||||||
|             <section class="pf-c-page__main-section pf-m-light"> |                 <button | ||||||
|                 <div class="pf-c-content"> |                     class="sidebar-trigger pf-c-button pf-m-plain" | ||||||
|                     <h1> |                     @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 |                         ${this.hasIcon | ||||||
|                             ? html`<slot name="icon">${this.renderIcon()}</slot> ` |                             ? html`<slot name="icon">${this.renderIcon()}</slot>` | ||||||
|                             : nothing} |                             : nothing} | ||||||
|                         <slot name="header">${this.header}</slot> |                         ${this.header} | ||||||
|                     </h1> |                     </h1> | ||||||
|                     ${this.description ? html`<p>${this.description}</p>` : html``} |                 </section> | ||||||
|                 </div> |                 ${this.description | ||||||
|             </section> |                     ? html`<section class="items page-description pf-c-content"> | ||||||
|             <div class="pf-c-page__header-tools"> |                           <p>${this.description}</p> | ||||||
|                 <div class="pf-c-page__header-tools-group"> |                       </section>` | ||||||
|                     <ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.me}> |                     : nothing} | ||||||
|                         <a |  | ||||||
|                             class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md" |                 <section class="items secondary"> | ||||||
|                             href="${globalAK().api.base}if/user/" |                     <div class="pf-c-page__header-tools-group"> | ||||||
|                             slot="extra" |                         <ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.session}> | ||||||
|                         > |                             <a | ||||||
|                             ${msg("User interface")} |                                 class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md" | ||||||
|                         </a> |                                 href="${globalAK().api.base}if/user/" | ||||||
|                     </ak-nav-buttons> |                                 slot="extra" | ||||||
|                 </div> |                             > | ||||||
|             </div> |                                 ${msg("User interface")} | ||||||
|         </div>`; |                             </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 { | declare global { | ||||||
|     interface HTMLElementTagNameMap { |     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 { |                 .center-value { | ||||||
|                     font-size: var(--pf-global--icon--FontSize--lg); |                     font-size: var(--pf-global--icon--FontSize--lg); | ||||||
|                     text-align: center; |                     text-align: center; | ||||||
|  |                     place-content: center; | ||||||
|                 } |                 } | ||||||
|                 .subtext { |                 .subtext { | ||||||
|                     margin-top: var(--pf-global--spacer--sm); |                     margin-top: var(--pf-global--spacer--sm); | ||||||
|  | |||||||
| @ -35,10 +35,7 @@ export class Sidebar extends AKElement { | |||||||
|                 .pf-c-nav__section + .pf-c-nav__section { |                 .pf-c-nav__section + .pf-c-nav__section { | ||||||
|                     --pf-c-nav__section--section--MarginTop: var(--pf-global--spacer--sm); |                     --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 { |                 nav { | ||||||
|                     display: flex; |                     display: flex; | ||||||
|                     flex-direction: column; |                     flex-direction: column; | ||||||
| @ -70,7 +67,6 @@ export class Sidebar extends AKElement { | |||||||
|             class="pf-c-nav ${this.activeTheme === UiThemeEnum.Light ? "pf-m-light" : ""}" |             class="pf-c-nav ${this.activeTheme === UiThemeEnum.Light ? "pf-m-light" : ""}" | ||||||
|             aria-label=${msg("Global")} |             aria-label=${msg("Global")} | ||||||
|         > |         > | ||||||
|             <ak-sidebar-brand></ak-sidebar-brand> |  | ||||||
|             <ul class="pf-c-nav__list"> |             <ul class="pf-c-nav__list"> | ||||||
|                 <slot></slot> |                 <slot></slot> | ||||||
|             </ul> |             </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"; | import { CurrentBrand, UiThemeEnum } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| // If the viewport is wider than MIN_WIDTH, the sidebar | // If the viewport is wider than MIN_WIDTH, the sidebar | ||||||
| @ -28,79 +14,3 @@ export const DefaultBrand: CurrentBrand = { | |||||||
|     matchedDomain: "", |     matchedDomain: "", | ||||||
|     defaultLocale: "", |     defaultLocale: "", | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @customElement("ak-sidebar-brand") |  | ||||||
| export class SidebarBrand extends WithBrandConfig(AKElement) { |  | ||||||
|     static get styles(): CSSResult[] { |  | ||||||
|         return [ |  | ||||||
|             PFBase, |  | ||||||
|             PFGlobal, |  | ||||||
|             PFPage, |  | ||||||
|             PFButton, |  | ||||||
|             css` |  | ||||||
|                 :host { |  | ||||||
|                     display: flex; |  | ||||||
|                     flex-direction: row; |  | ||||||
|                     align-items: center; |  | ||||||
|                     height: 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 { AKElement } from "@goauthentik/elements/Base.js"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| @ -21,8 +21,6 @@ const styles = css` | |||||||
|     } |     } | ||||||
| `; | `; | ||||||
|  |  | ||||||
| const poweredBy: FooterLink = { name: msg("Powered by authentik"), href: null }; |  | ||||||
|  |  | ||||||
| @customElement("ak-brand-links") | @customElement("ak-brand-links") | ||||||
| export class BrandLinks extends AKElement { | export class BrandLinks extends AKElement { | ||||||
|     static get styles() { |     static get styles() { | ||||||
| @ -33,13 +31,21 @@ export class BrandLinks extends AKElement { | |||||||
|     links: FooterLink[] = []; |     links: FooterLink[] = []; | ||||||
|  |  | ||||||
|     render() { |     render() { | ||||||
|         const links = [...(this.links ?? []), poweredBy]; |         const links = [...(this.links ?? [])]; | ||||||
|  |  | ||||||
|         return html` <ul class="pf-c-list pf-m-inline"> |         return html` <ul class="pf-c-list pf-m-inline"> | ||||||
|             ${map(links, (link) => |             ${map(links, (link) => { | ||||||
|                 link.href |                 const children = sanitizeHTML(BrandedHTMLPolicy, link.name); | ||||||
|                     ? purify(html`<li><a href="${link.href}">${link.name}</a></li>`) |  | ||||||
|                     : html`<li><span>${link.name}</span></li>`, |                 if (link.href) { | ||||||
|             )} |                     return html`<li><a href="${link.href}">${children}</a></li>`; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 return html`<li> | ||||||
|  |                     <span> ${children} </span> | ||||||
|  |                 </li>`; | ||||||
|  |             })} | ||||||
|  |             <li><span>${msg("Powered by authentik")}</span></li> | ||||||
|         </ul>`; |         </ul>`; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,15 +1,16 @@ | |||||||
| ///<reference types="@hcaptcha/types"/> | /// <reference types="@hcaptcha/types"/> | ||||||
| import { renderStatic } from "@goauthentik/common/purify"; | /// <reference types="turnstile-types"/> | ||||||
|  | import { renderStaticHTMLUnsafe } from "@goauthentik/common/purify"; | ||||||
| import "@goauthentik/elements/EmptyState"; | import "@goauthentik/elements/EmptyState"; | ||||||
| import { akEmptyState } from "@goauthentik/elements/EmptyState"; | import { akEmptyState } from "@goauthentik/elements/EmptyState"; | ||||||
| import { bound } from "@goauthentik/elements/decorators/bound"; | import { bound } from "@goauthentik/elements/decorators/bound"; | ||||||
| import "@goauthentik/elements/forms/FormElement"; | import "@goauthentik/elements/forms/FormElement"; | ||||||
|  | import { createIFrameHTMLWrapper } from "@goauthentik/elements/utils/iframe"; | ||||||
| import { ListenerController } from "@goauthentik/elements/utils/listenerController.js"; | import { ListenerController } from "@goauthentik/elements/utils/listenerController.js"; | ||||||
| import { randomId } from "@goauthentik/elements/utils/randomId"; | import { randomId } from "@goauthentik/elements/utils/randomId"; | ||||||
| import "@goauthentik/flow/FormStatic"; | import "@goauthentik/flow/FormStatic"; | ||||||
| import { BaseStage } from "@goauthentik/flow/stages/base"; | import { BaseStage } from "@goauthentik/flow/stages/base"; | ||||||
| import { P, match } from "ts-pattern"; | import { P, match } from "ts-pattern"; | ||||||
| import type * as _ from "turnstile-types"; |  | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit"; | import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit"; | ||||||
| @ -56,40 +57,36 @@ type CaptchaHandler = { | |||||||
| // a resize. Because the Captcha is itself in an iframe, the reported height is often off by some | // a resize. Because the Captcha is itself in an iframe, the reported height is often off by some | ||||||
| // margin, so adding 2rem of height to our container adds padding and prevents scroll bars or hidden | // margin, so adding 2rem of height to our container adds padding and prevents scroll bars or hidden | ||||||
| // rendering. | // rendering. | ||||||
|  | function iframeTemplate(children: TemplateResult, challengeURL: string): TemplateResult { | ||||||
|  |     return html` ${children} | ||||||
|  |         <script> | ||||||
|  |             new ResizeObserver((entries) => { | ||||||
|  |                 const height = | ||||||
|  |                     document.body.offsetHeight + | ||||||
|  |                     parseFloat(getComputedStyle(document.body).fontSize) * 2; | ||||||
|  |  | ||||||
| const iframeTemplate = (captchaElement: TemplateResult, challengeUrl: string) => |                 window.parent.postMessage({ | ||||||
|     html`<!doctype html> |                     message: "resize", | ||||||
|         <head> |                     source: "goauthentik.io", | ||||||
|             <html> |                     context: "flow-executor", | ||||||
|                 <body style="display:flex;flex-direction:row;justify-content:center;"> |                     size: { height }, | ||||||
|                     ${captchaElement} |                 }); | ||||||
|                     <script> |             }).observe(document.querySelector(".ak-captcha-container")); | ||||||
|                         new ResizeObserver((entries) => { |         </script> | ||||||
|                             const height = |  | ||||||
|                                 document.body.offsetHeight + |         <script src=${challengeURL}></script> | ||||||
|                                 parseFloat(getComputedStyle(document.body).fontSize) * 2; |  | ||||||
|                             window.parent.postMessage({ |         <script> | ||||||
|                                 message: "resize", |             function callback(token) { | ||||||
|                                 source: "goauthentik.io", |                 window.parent.postMessage({ | ||||||
|                                 context: "flow-executor", |                     message: "captcha", | ||||||
|                                 size: { height }, |                     source: "goauthentik.io", | ||||||
|                             }); |                     context: "flow-executor", | ||||||
|                         }).observe(document.querySelector(".ak-captcha-container")); |                     token, | ||||||
|                     </script> |                 }); | ||||||
|                     <script src=${challengeUrl}></script> |             } | ||||||
|                     <script> |         </script>`; | ||||||
|                         function callback(token) { | } | ||||||
|                             window.parent.postMessage({ |  | ||||||
|                                 message: "captcha", |  | ||||||
|                                 source: "goauthentik.io", |  | ||||||
|                                 context: "flow-executor", |  | ||||||
|                                 token: token, |  | ||||||
|                             }); |  | ||||||
|                         } |  | ||||||
|                     </script> |  | ||||||
|                 </body> |  | ||||||
|             </html> |  | ||||||
|         </head>`; |  | ||||||
|  |  | ||||||
| @customElement("ak-stage-captcha") | @customElement("ak-stage-captcha") | ||||||
| export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> { | export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> { | ||||||
| @ -305,11 +302,25 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async renderFrame(captchaElement: TemplateResult) { |     async renderFrame(captchaElement: TemplateResult) { | ||||||
|         this.captchaFrame.contentWindow?.document.open(); |         const { contentDocument } = this.captchaFrame || {}; | ||||||
|         this.captchaFrame.contentWindow?.document.write( |  | ||||||
|             await renderStatic(iframeTemplate(captchaElement, this.challenge.jsUrl)), |         if (!contentDocument) { | ||||||
|  |             console.debug( | ||||||
|  |                 "authentik/stages/captcha: unable to render captcha frame, no contentDocument", | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         contentDocument.open(); | ||||||
|  |  | ||||||
|  |         contentDocument.write( | ||||||
|  |             createIFrameHTMLWrapper( | ||||||
|  |                 renderStaticHTMLUnsafe(iframeTemplate(captchaElement, this.challenge.jsUrl)), | ||||||
|  |             ), | ||||||
|         ); |         ); | ||||||
|         this.captchaFrame.contentWindow?.document.close(); |  | ||||||
|  |         contentDocument.close(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderBody() { |     renderBody() { | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	