 Marc 'risson' Schmitt
					Marc 'risson' Schmitt
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							cf160f800d
						
					
				
				
					commit
					337956672f
				
			
							
								
								
									
										52
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										52
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -25,6 +25,7 @@ | |||||||
|                 "@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", | ||||||
| @ -65,7 +66,6 @@ | |||||||
|                 "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,11 +2281,47 @@ | |||||||
|                 "@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", | ||||||
| @ -3521,7 +3557,6 @@ | |||||||
|             "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" | ||||||
|             } |             } | ||||||
| @ -10688,7 +10723,6 @@ | |||||||
|             "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" | ||||||
|             } |             } | ||||||
| @ -11309,7 +11343,6 @@ | |||||||
|             "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" | ||||||
| @ -13787,8 +13820,7 @@ | |||||||
|         "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", | ||||||
| @ -18224,7 +18256,6 @@ | |||||||
|             "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", | ||||||
| @ -22342,7 +22373,6 @@ | |||||||
|             "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" | ||||||
|             } |             } | ||||||
| @ -22694,12 +22724,6 @@ | |||||||
|                 "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,6 +13,7 @@ | |||||||
|         "@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", | ||||||
| @ -53,7 +54,6 @@ | |||||||
|         "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,17 +4,13 @@ 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"; | ||||||
| @ -25,32 +21,25 @@ 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, nothing } from "lit"; | import { CSSResult, TemplateResult, css, html } 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 { LicenseSummaryStatusEnum, SessionUser, UiThemeEnum } from "@goauthentik/api"; | import { SessionUser, UiThemeEnum } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| import { | import "./AdminSidebar"; | ||||||
|     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 WithLicenseSummary(AuthenticatedInterface) { | export class AdminInterface extends AuthenticatedInterface { | ||||||
|     //#region Properties |  | ||||||
|  |  | ||||||
|     @property({ type: Boolean }) |     @property({ type: Boolean }) | ||||||
|     notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); |     notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); | ||||||
|  |  | ||||||
| @ -65,29 +54,12 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) { | |||||||
|     @query("ak-about-modal") |     @query("ak-about-modal") | ||||||
|     aboutModal?: AboutModal; |     aboutModal?: AboutModal; | ||||||
|  |  | ||||||
|     @property({ type: Boolean, reflect: true }) |  | ||||||
|     public sidebarOpen: boolean; |  | ||||||
|  |  | ||||||
|     #toggleSidebar = () => { |  | ||||||
|         this.sidebarOpen = !this.sidebarOpen; |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     #sidebarMatcher: MediaQueryList; |  | ||||||
|     #sidebarListener = (event: MediaQueryListEvent) => { |  | ||||||
|         this.sidebarOpen = event.matches; |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     //#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, | ||||||
| @ -95,30 +67,23 @@ export class AdminInterface extends WithLicenseSummary(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; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 :host([theme="dark"]) { |  | ||||||
|                 /* Global page background colour */ |                 /* Global page background colour */ | ||||||
|                     .pf-c-page { |                 :host([theme="dark"]) .pf-c-page { | ||||||
|                     --pf-c-page--BackgroundColor: var(--ak-dark-background); |                     --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); | ||||||
|                 } |                 } | ||||||
| @ -126,23 +91,10 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) { | |||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     //#endregion |  | ||||||
|  |  | ||||||
|     //#region Lifecycle |  | ||||||
|  |  | ||||||
|     constructor() { |     constructor() { | ||||||
|         super(); |         super(); | ||||||
|         this.ws = new WebsocketClient(); |         this.ws = new WebsocketClient(); | ||||||
|  |  | ||||||
|         this.#sidebarMatcher = window.matchMedia("(min-width: 1200px)"); |  | ||||||
|         this.sidebarOpen = this.#sidebarMatcher.matches; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public connectedCallback() { |  | ||||||
|         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; | ||||||
|             updateURLParams({ |             updateURLParams({ | ||||||
| @ -156,14 +108,6 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) { | |||||||
|                 apiDrawerOpen: this.apiDrawerOpen, |                 apiDrawerOpen: this.apiDrawerOpen, | ||||||
|             }); |             }); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         this.#sidebarMatcher.addEventListener("change", this.#sidebarListener); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public disconnectedCallback(): void { |  | ||||||
|         super.disconnectedCallback(); |  | ||||||
|         window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.#toggleSidebar); |  | ||||||
|         this.#sidebarMatcher.removeEventListener("change", this.#sidebarListener); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async firstUpdated(): Promise<void> { |     async firstUpdated(): Promise<void> { | ||||||
| @ -174,7 +118,6 @@ export class AdminInterface extends WithLicenseSummary(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/"); | ||||||
|         } |         } | ||||||
| @ -182,14 +125,10 @@ export class AdminInterface extends WithLicenseSummary(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, | ||||||
| @ -197,18 +136,11 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) { | |||||||
|  |  | ||||||
|         return html` <ak-locale-context> |         return html` <ak-locale-context> | ||||||
|             <div class="pf-c-page"> |             <div class="pf-c-page"> | ||||||
|                 <ak-page-navbar> |  | ||||||
|                     <ak-version-banner></ak-version-banner> |  | ||||||
|                 <ak-enterprise-status interface="admin"></ak-enterprise-status> |                 <ak-enterprise-status interface="admin"></ak-enterprise-status> | ||||||
|                 </ak-page-navbar> |                 <ak-version-banner></ak-version-banner> | ||||||
|  |                 <ak-admin-sidebar | ||||||
|                 <ak-sidebar class="${classMap(sidebarClasses)}"> |                     class="pf-c-page__sidebar ${classMap(sidebarClasses)}" | ||||||
|                     ${renderSidebarItems(AdminSidebarEntries)} |                 ></ak-admin-sidebar> | ||||||
|                     ${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,77 +1,132 @@ | |||||||
|  | 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 { repeat } from "lit/directives/repeat.js"; | import { customElement, property, state } from "lit/decorators.js"; | ||||||
|  | import { map } from "lit/directives/map.js"; | ||||||
|  |  | ||||||
| // The second attribute type is of string[] to help with the 'activeWhen' control, which was | import { UiThemeEnum } from "@goauthentik/api"; | ||||||
| // commonplace and singular enough to merit its own handler. | import type { SessionUser, UserSelf } from "@goauthentik/api"; | ||||||
| type SidebarEntry = [ |  | ||||||
|  | @customElement("ak-admin-sidebar") | ||||||
|  | export class AkAdminSidebar extends WithCapabilitiesConfig(WithVersion(AKElement)) { | ||||||
|  |     @property({ type: Boolean, reflect: true }) | ||||||
|  |     open = true; | ||||||
|  |  | ||||||
|  |     @state() | ||||||
|  |     impersonation: UserSelf["username"] | null = null; | ||||||
|  |  | ||||||
|  |     constructor() { | ||||||
|  |         super(); | ||||||
|  |         me().then((user: SessionUser) => { | ||||||
|  |             this.impersonation = user.original ? user.user.username : null; | ||||||
|  |         }); | ||||||
|  |         this.toggleOpen = this.toggleOpen.bind(this); | ||||||
|  |         this.checkWidth = this.checkWidth.bind(this); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // This has to be a bound method so the event listener can be removed on disconnection as | ||||||
|  |     // needed. | ||||||
|  |     toggleOpen() { | ||||||
|  |         this.open = !this.open; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     checkWidth() { | ||||||
|  |         // This works just fine, but it assumes that the `--ak-sidebar--minimum-auto-width` is in | ||||||
|  |         // REMs. If that changes, this code will have to be adjusted as well. | ||||||
|  |         const minWidth = | ||||||
|  |             parseFloat(getRootStyle("--ak-sidebar--minimum-auto-width")) * | ||||||
|  |             parseFloat(getRootStyle("font-size")); | ||||||
|  |         this.open = window.innerWidth >= minWidth; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     connectedCallback() { | ||||||
|  |         super.connectedCallback(); | ||||||
|  |         window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen); | ||||||
|  |         window.addEventListener("resize", this.checkWidth); | ||||||
|  |         // After connecting to the DOM, we can now perform this check to see if the sidebar should | ||||||
|  |         // be open by default. | ||||||
|  |         this.checkWidth(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // The symmetry (☟, ☝) here is critical in that you want to start adding these handlers after | ||||||
|  |     // connection, and removing them before disconnection. | ||||||
|  |  | ||||||
|  |     disconnectedCallback() { | ||||||
|  |         window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen); | ||||||
|  |         window.removeEventListener("resize", this.checkWidth); | ||||||
|  |         super.disconnectedCallback(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     render() { | ||||||
|  |         return html` | ||||||
|  |             <ak-sidebar | ||||||
|  |                 class="pf-c-page__sidebar ${this.open ? "pf-m-expanded" : "pf-m-collapsed"} ${this | ||||||
|  |                     .activeTheme === UiThemeEnum.Light | ||||||
|  |                     ? "pf-m-light" | ||||||
|  |                     : ""}" | ||||||
|  |             > | ||||||
|  |                 ${this.renderSidebarItems()} | ||||||
|  |             </ak-sidebar> | ||||||
|  |         `; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     updated() { | ||||||
|  |         // This is permissible as`:host.classList` is not one of the properties Lit uses as a | ||||||
|  |         // scheduling trigger. This sort of shenanigans can trigger an loop, in that it will trigger | ||||||
|  |         // a browser reflow, which may trigger some other styling the application is monitoring, | ||||||
|  |         // triggering a re-render which triggers a browser reflow, ad infinitum. But we've been | ||||||
|  |         // living with that since jQuery, and it's both well-known and fortunately rare. | ||||||
|  |  | ||||||
|  |         // eslint-disable-next-line wc/no-self-class | ||||||
|  |         this.classList.remove("pf-m-expanded", "pf-m-collapsed"); | ||||||
|  |         // eslint-disable-next-line wc/no-self-class | ||||||
|  |         this.classList.add(this.open ? "pf-m-expanded" : "pf-m-collapsed"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     renderSidebarItems(): TemplateResult { | ||||||
|  |         // The second attribute type is of string[] to help with the 'activeWhen' control, which was | ||||||
|  |         // commonplace and singular enough to merit its own handler. | ||||||
|  |         type SidebarEntry = [ | ||||||
|             path: string | null, |             path: string | null, | ||||||
|             label: string, |             label: string, | ||||||
|             attributes?: Record<string, any> | string[] | null, // eslint-disable-line |             attributes?: Record<string, any> | string[] | null, // eslint-disable-line | ||||||
|             children?: SidebarEntry[], |             children?: SidebarEntry[], | ||||||
| ]; |         ]; | ||||||
|  |  | ||||||
| /** |         // prettier-ignore | ||||||
|  * Recursively renders a sidebar entry. |         const sidebarContent: SidebarEntry[] = [ | ||||||
|  */ |  | ||||||
| export function renderSidebarItem([ |  | ||||||
|     path, |  | ||||||
|     label, |  | ||||||
|     attributes, |  | ||||||
|     children, |  | ||||||
| ]: SidebarEntry): TemplateResult { |  | ||||||
|     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} |  | ||||||
|         ${children ? renderSidebarItems(children) : nothing} |  | ||||||
|     </ak-sidebar-item>`; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Recursively renders a collection of sidebar entries. |  | ||||||
|  */ |  | ||||||
| export function renderSidebarItems(entries: readonly SidebarEntry[]) { |  | ||||||
|     return repeat(entries, ([path, label]) => path || label, renderSidebarItem); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // prettier-ignore |  | ||||||
| export const AdminSidebarEntries: readonly SidebarEntry[] = [ |  | ||||||
|             [null, msg("Dashboards"), { "?expanded": true }, [ |             [null, msg("Dashboards"), { "?expanded": true }, [ | ||||||
|                 ["/administration/overview", msg("Overview")], |                 ["/administration/overview", msg("Overview")], | ||||||
|                 ["/administration/dashboard/users", msg("User Statistics")], |                 ["/administration/dashboard/users", msg("User Statistics")], | ||||||
|         ["/administration/system-tasks", msg("System Tasks")]] |                 ["/administration/system-tasks", msg("System Tasks")]]], | ||||||
|     ], |  | ||||||
|             [null, msg("Applications"), null, [ |             [null, msg("Applications"), null, [ | ||||||
|                 ["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]], |                 ["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]], | ||||||
|                 ["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]], |                 ["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]], | ||||||
|         ["/outpost/outposts", msg("Outposts")]] |                 ["/outpost/outposts", msg("Outposts")]]], | ||||||
|     ], |  | ||||||
|             [null, msg("Events"), null, [ |             [null, msg("Events"), null, [ | ||||||
|                 ["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]], |                 ["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]], | ||||||
|                 ["/events/rules", msg("Notification Rules")], |                 ["/events/rules", msg("Notification Rules")], | ||||||
|         ["/events/transports", msg("Notification Transports")]] |                 ["/events/transports", msg("Notification Transports")]]], | ||||||
|     ], |  | ||||||
|             [null, msg("Customization"), null, [ |             [null, msg("Customization"), null, [ | ||||||
|                 ["/policy/policies", msg("Policies")], |                 ["/policy/policies", msg("Policies")], | ||||||
|                 ["/core/property-mappings", msg("Property Mappings")], |                 ["/core/property-mappings", msg("Property Mappings")], | ||||||
|                 ["/blueprints/instances", msg("Blueprints")], |                 ["/blueprints/instances", msg("Blueprints")], | ||||||
|         ["/policy/reputation", msg("Reputation scores")]] |                 ["/policy/reputation", msg("Reputation scores")]]], | ||||||
|     ], |  | ||||||
|             [null, msg("Flows and Stages"), null, [ |             [null, msg("Flows and Stages"), null, [ | ||||||
|                 ["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]], |                 ["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]], | ||||||
|                 ["/flow/stages", msg("Stages")], |                 ["/flow/stages", msg("Stages")], | ||||||
|         ["/flow/stages/prompts", msg("Prompts")]] |                 ["/flow/stages/prompts", msg("Prompts")]]], | ||||||
|     ], |  | ||||||
|             [null, msg("Directory"), null, [ |             [null, msg("Directory"), null, [ | ||||||
|                 ["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]], |                 ["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]], | ||||||
|                 ["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]], |                 ["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]], | ||||||
| @ -79,19 +134,53 @@ export const AdminSidebarEntries: readonly SidebarEntry[] = [ | |||||||
|                 ["/identity/initial-permissions", msg("Initial Permissions"), [`^/identity/initial-permissions/(?<id>${ID_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/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]], | ||||||
|                 ["/core/tokens", msg("Tokens and App passwords")], |                 ["/core/tokens", msg("Tokens and App passwords")], | ||||||
|         ["/flow/stages/invitations", msg("Invitations")]] |                 ["/flow/stages/invitations", msg("Invitations")]]], | ||||||
|     ], |  | ||||||
|             [null, msg("System"), null, [ |             [null, msg("System"), null, [ | ||||||
|                 ["/core/brands", msg("Brands")], |                 ["/core/brands", msg("Brands")], | ||||||
|                 ["/crypto/certificates", msg("Certificates")], |                 ["/crypto/certificates", msg("Certificates")], | ||||||
|                 ["/outpost/integrations", msg("Outpost Integrations")], |                 ["/outpost/integrations", msg("Outpost Integrations")], | ||||||
|         ["/admin/settings", msg("Settings")]] |                 ["/admin/settings", msg("Settings")]]], | ||||||
|     ], |         ]; | ||||||
| ]; |  | ||||||
|  |  | ||||||
| // prettier-ignore |         // Typescript requires the type here to correctly type the recursive path | ||||||
| export const AdminSidebarEnterpriseEntries: readonly SidebarEntry[] = [ |         type SidebarRenderer = (_: SidebarEntry) => TemplateResult; | ||||||
|     [null, msg("Enterprise"), null, [ |  | ||||||
|         ["/enterprise/licenses", msg("Licenses"), null] |         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 { | ||||||
|  |         "ak-admin-sidebar": AkAdminSidebar; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
| @ -94,13 +94,10 @@ export class AdminOverviewPage extends AdminOverviewBase { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
|         const username = this.user?.user.name || this.user?.user.username; |         const name = this.user?.user.name ?? this.user?.user.username; | ||||||
|  |  | ||||||
|         return html` <ak-page-header |         return html`<ak-page-header description=${msg("General system status")} ?hasIcon=${false}> | ||||||
|                 header=${msg(str`Welcome, ${username || ""}.`)} |                 <span slot="header"> ${msg(str`Welcome, ${name || ""}.`)} </span> | ||||||
|                 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"> | ||||||
|  | |||||||
| @ -83,10 +83,13 @@ export class AdminSettingsPage extends AKElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     render() { |     render() { | ||||||
|         if (!this.settings) return nothing; |         if (!this.settings) { | ||||||
|  |             return nothing; | ||||||
|  |         } | ||||||
|         return html` |         return html` | ||||||
|             <ak-page-header icon="fa fa-cog" header="${msg("System settings")}"> </ak-page-header> |             <ak-page-header icon="fa fa-cog" header="" description=""> | ||||||
|  |                 <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,110 +1,26 @@ | |||||||
| 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"; | import { render } from "@lit-labs/ssr"; | ||||||
|  | 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> { | ||||||
|  * Render untrusted HTML to a string without escaping it. |     return await collectResult(render(input)); | ||||||
|  * | } | ||||||
|  * @returns {string} The rendered HTML string. |  | ||||||
|  */ | export function purify(input: TemplateResult): TemplateResult { | ||||||
| export function renderStaticHTMLUnsafe(untrustedHTML: unknown): string { |     return html`${until( | ||||||
|     const container = document.createElement("html"); |         (async () => { | ||||||
|     render(untrustedHTML, container); |             const rendered = await renderStatic(input); | ||||||
|  |             const purified = DOMPurify.sanitize(rendered); | ||||||
|     const result = container.innerHTML; |             return html`${unsafeHTML(purified)}`; | ||||||
|  |         })(), | ||||||
|     return result; |     )}`; | ||||||
| } | } | ||||||
|  | |||||||
| @ -17,13 +17,6 @@ | |||||||
|  |  | ||||||
|     /* 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) { | ||||||
|  | |||||||
| @ -1,220 +0,0 @@ | |||||||
| /** |  | ||||||
|  * @file Stylesheet utilities. |  | ||||||
|  */ |  | ||||||
| import { CSSResult, CSSResultOrNative, ReactiveElement, css } from "lit"; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Elements containing adoptable stylesheets. |  | ||||||
|  */ |  | ||||||
| export type StyleSheetParent = Pick<DocumentOrShadowRoot, "adoptedStyleSheets">; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Type-predicate to determine if a given object has adoptable stylesheets. |  | ||||||
|  */ |  | ||||||
| export function isAdoptableStyleSheetParent(input: unknown): input is StyleSheetParent { |  | ||||||
|     // Sanity check - Does the input have the right shape? |  | ||||||
|  |  | ||||||
|     if (!input || typeof input !== "object") return false; |  | ||||||
|  |  | ||||||
|     if (!("adoptedStyleSheets" in input) || !input.adoptedStyleSheets) return false; |  | ||||||
|  |  | ||||||
|     if (typeof input.adoptedStyleSheets !== "object") return false; |  | ||||||
|  |  | ||||||
|     // We avoid `Array.isArray` because the adopted stylesheets property |  | ||||||
|     // is defined as a proxied array. |  | ||||||
|     // All we care about is that it's shaped like an array. |  | ||||||
|     if (!("length" in input.adoptedStyleSheets)) return false; |  | ||||||
|  |  | ||||||
|     if (typeof input.adoptedStyleSheets.length !== "number") return false; |  | ||||||
|  |  | ||||||
|     // Finally is the array mutable? |  | ||||||
|     return "push" in input.adoptedStyleSheets; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Assert that the given input can adopt stylesheets. |  | ||||||
|  */ |  | ||||||
| export function assertAdoptableStyleSheetParent<T>( |  | ||||||
|     input: T, |  | ||||||
| ): asserts input is T & StyleSheetParent { |  | ||||||
|     if (isAdoptableStyleSheetParent(input)) return; |  | ||||||
|  |  | ||||||
|     console.debug("Given input missing `adoptedStyleSheets`", input); |  | ||||||
|  |  | ||||||
|     throw new TypeError("Assertion failed: `adoptedStyleSheets` missing in given input"); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function resolveStyleSheetParent<T extends HTMLElement | DocumentFragment | Document>( |  | ||||||
|     renderRoot: T, |  | ||||||
| ) { |  | ||||||
|     const styleRoot = "ShadyDOM" in window ? document : renderRoot; |  | ||||||
|  |  | ||||||
|     assertAdoptableStyleSheetParent(styleRoot); |  | ||||||
|  |  | ||||||
|     return styleRoot; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export type StyleSheetInit = string | CSSResult | CSSStyleSheet; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Given a source of CSS, create a `CSSStyleSheet`. |  | ||||||
|  * |  | ||||||
|  * @throw {@linkcode TypeError} if the input cannot be converted to a `CSSStyleSheet` |  | ||||||
|  * |  | ||||||
|  * @remarks |  | ||||||
|  * |  | ||||||
|  * Storybook's `build` does not currently have a coherent way of importing |  | ||||||
|  * CSS-as-text into CSSStyleSheet. |  | ||||||
|  * |  | ||||||
|  * It works well when Storybook is running in `dev`, but in `build` it fails. |  | ||||||
|  * Storied components will have to map their textual CSS imports. |  | ||||||
|  */ |  | ||||||
| export function createStyleSheet(input: string): CSSResult { |  | ||||||
|     const inputTemplate = [input] as unknown as TemplateStringsArray; |  | ||||||
|  |  | ||||||
|     const result = css(inputTemplate, []); |  | ||||||
|  |  | ||||||
|     return result; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Given a source of CSS, create a `CSSStyleSheet`. |  | ||||||
|  * |  | ||||||
|  * @see {@linkcode createStyleSheet} |  | ||||||
|  */ |  | ||||||
| export function normalizeCSSSource(css: string): CSSStyleSheet; |  | ||||||
| export function normalizeCSSSource(styleSheet: CSSStyleSheet): CSSStyleSheet; |  | ||||||
| export function normalizeCSSSource(cssResult: CSSResult): CSSResult; |  | ||||||
| export function normalizeCSSSource(input: StyleSheetInit): CSSResultOrNative; |  | ||||||
| export function normalizeCSSSource(input: StyleSheetInit): CSSResultOrNative { |  | ||||||
|     if (typeof input === "string") return createStyleSheet(input); |  | ||||||
|  |  | ||||||
|     return input; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function createStyleSheetUnsafe(input: StyleSheetInit): CSSStyleSheet { |  | ||||||
|     const result = normalizeCSSSource(input); |  | ||||||
|     if (result instanceof CSSStyleSheet) return result; |  | ||||||
|  |  | ||||||
|     if (!result.styleSheet) { |  | ||||||
|         console.debug( |  | ||||||
|             "authentik/common/stylesheets: CSSResult missing styleSheet, returning empty", |  | ||||||
|             { result, input }, |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         throw new TypeError("Expected a CSSStyleSheet"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return result.styleSheet; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Append stylesheet(s) to the given roots. |  | ||||||
|  */ |  | ||||||
| export function appendStyleSheet( |  | ||||||
|     insertions: CSSStyleSheet | Iterable<CSSStyleSheet>, |  | ||||||
|     ...styleParents: StyleSheetParent[] |  | ||||||
| ): void { |  | ||||||
|     insertions = Array.isArray(insertions) ? insertions : [insertions]; |  | ||||||
|  |  | ||||||
|     for (const nextStyleSheet of insertions) { |  | ||||||
|         for (const styleParent of styleParents) { |  | ||||||
|             if (styleParent.adoptedStyleSheets.includes(nextStyleSheet)) return; |  | ||||||
|  |  | ||||||
|             styleParent.adoptedStyleSheets = [...styleParent.adoptedStyleSheets, nextStyleSheet]; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Remove a stylesheet from the given roots, matching by referential equality. |  | ||||||
|  */ |  | ||||||
| export function removeStyleSheet( |  | ||||||
|     currentStyleSheet: CSSStyleSheet, |  | ||||||
|     ...styleParents: StyleSheetParent[] |  | ||||||
| ): void { |  | ||||||
|     for (const styleParent of styleParents) { |  | ||||||
|         const nextAdoptedStyleSheets = styleParent.adoptedStyleSheets.filter( |  | ||||||
|             (styleSheet) => styleSheet !== currentStyleSheet, |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         if (nextAdoptedStyleSheets.length === styleParent.adoptedStyleSheets.length) return; |  | ||||||
|  |  | ||||||
|         styleParent.adoptedStyleSheets = nextAdoptedStyleSheets; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Serialize a stylesheet to a string. |  | ||||||
|  * |  | ||||||
|  * This is useful for debugging or inspecting the contents of a stylesheet. |  | ||||||
|  */ |  | ||||||
| export function serializeStyleSheet(stylesheet: CSSStyleSheet): string { |  | ||||||
|     return Array.from(stylesheet.cssRules || [], (rule) => rule.cssText || "").join("\n"); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Inspect the adopted stylesheets of a given style parent, serializing them to strings. |  | ||||||
|  */ |  | ||||||
| export function inspectStyleSheets(styleParent: StyleSheetParent): string[] { |  | ||||||
|     return styleParent.adoptedStyleSheets.map((styleSheet) => serializeStyleSheet(styleSheet)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| interface InspectedStyleSheetEntry { |  | ||||||
|     tagName: string; |  | ||||||
|     element: ReactiveElement; |  | ||||||
|     styles: string[]; |  | ||||||
|     children?: InspectedStyleSheetEntry[]; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Recursively inspect the adopted stylesheets of a given style parent, serializing them to strings. |  | ||||||
|  */ |  | ||||||
| export function inspectStyleSheetTree(element: ReactiveElement): InspectedStyleSheetEntry { |  | ||||||
|     const styleParent = resolveStyleSheetParent(element.renderRoot); |  | ||||||
|     const styles = inspectStyleSheets(styleParent); |  | ||||||
|     const tagName = element.tagName.toLowerCase(); |  | ||||||
|  |  | ||||||
|     const treewalker = document.createTreeWalker(element.renderRoot, NodeFilter.SHOW_ELEMENT, { |  | ||||||
|         acceptNode(node) { |  | ||||||
|             if (node instanceof ReactiveElement) { |  | ||||||
|                 return NodeFilter.FILTER_ACCEPT; |  | ||||||
|             } |  | ||||||
|             return NodeFilter.FILTER_SKIP; |  | ||||||
|         }, |  | ||||||
|     }); |  | ||||||
|     const children: InspectedStyleSheetEntry[] = []; |  | ||||||
|     let currentNode: Node | null = treewalker.nextNode(); |  | ||||||
|     while (currentNode) { |  | ||||||
|         const childElement = currentNode as ReactiveElement; |  | ||||||
|  |  | ||||||
|         if (!isAdoptableStyleSheetParent(childElement.renderRoot)) { |  | ||||||
|             currentNode = treewalker.nextNode(); |  | ||||||
|             continue; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const childStyles = inspectStyleSheets(childElement.renderRoot); |  | ||||||
|  |  | ||||||
|         children.push({ |  | ||||||
|             tagName: childElement.tagName.toLowerCase(), |  | ||||||
|             element: childElement, |  | ||||||
|             styles: childStyles, |  | ||||||
|         }); |  | ||||||
|         currentNode = treewalker.nextNode(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return { |  | ||||||
|         tagName, |  | ||||||
|         element, |  | ||||||
|         styles, |  | ||||||
|         children, |  | ||||||
|     }; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| if (process.env.NODE_ENV === "development") { |  | ||||||
|     Object.assign(window, { |  | ||||||
|         inspectStyleSheetTree, |  | ||||||
|         serializeStyleSheet, |  | ||||||
|         inspectStyleSheets, |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
| @ -1,200 +0,0 @@ | |||||||
| /** |  | ||||||
|  * @file Theme utilities. |  | ||||||
|  */ |  | ||||||
| import { UIConfig } from "@goauthentik/common/ui/config"; |  | ||||||
|  |  | ||||||
| import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api"; |  | ||||||
|  |  | ||||||
| //#region Scheme Types |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Valid CSS color scheme values. |  | ||||||
|  * |  | ||||||
|  * @link {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme | MDN} |  | ||||||
|  * |  | ||||||
|  * @category CSS |  | ||||||
|  */ |  | ||||||
| export type CSSColorSchemeValue = "dark" | "light" | "auto"; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * A CSS color scheme value that can be preferred by the user, i.e. not `"auto"`. |  | ||||||
|  * |  | ||||||
|  * @category CSS |  | ||||||
|  */ |  | ||||||
| export type ResolvedCSSColorSchemeValue = Exclude<CSSColorSchemeValue, "auto">; |  | ||||||
|  |  | ||||||
| //#endregion |  | ||||||
|  |  | ||||||
| //#region UI Theme Types |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * A UI color scheme value that can be preferred by the user. |  | ||||||
|  * |  | ||||||
|  * i.e. not an lack of preference or unknown value. |  | ||||||
|  * |  | ||||||
|  * @category CSS |  | ||||||
|  */ |  | ||||||
| export type ResolvedUITheme = typeof UiThemeEnum.Light | typeof UiThemeEnum.Dark; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * A mapping of theme values to their respective inversion. |  | ||||||
|  * |  | ||||||
|  * @category CSS |  | ||||||
|  */ |  | ||||||
| export const UIThemeInversion = { |  | ||||||
|     dark: "light", |  | ||||||
|     light: "dark", |  | ||||||
| } as const satisfies Record<ResolvedUITheme, ResolvedUITheme>; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Either a valid CSS color scheme value, or a theme preference. |  | ||||||
|  */ |  | ||||||
| export type UIThemeHint = CSSColorSchemeValue | UiThemeEnum; |  | ||||||
|  |  | ||||||
| //#endregion |  | ||||||
|  |  | ||||||
| //#region Scheme Functions |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Creates an event target for the given color scheme. |  | ||||||
|  * |  | ||||||
|  * @param colorScheme The color scheme to target. |  | ||||||
|  * @returns A {@linkcode MediaQueryList} that can be used to listen for changes to the color scheme. |  | ||||||
|  * |  | ||||||
|  * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList | MDN} |  | ||||||
|  * |  | ||||||
|  * @category CSS |  | ||||||
|  */ |  | ||||||
| export function createColorSchemeTarget(colorScheme: ResolvedCSSColorSchemeValue): MediaQueryList { |  | ||||||
|     return window.matchMedia(`(prefers-color-scheme: ${colorScheme})`); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Formats the given input into a valid CSS color scheme value. |  | ||||||
|  * |  | ||||||
|  * If the input is not provided, it defaults to "auto". |  | ||||||
|  * |  | ||||||
|  * @category CSS |  | ||||||
|  */ |  | ||||||
| export function formatColorScheme(theme: ResolvedUITheme): ResolvedCSSColorSchemeValue; |  | ||||||
| export function formatColorScheme( |  | ||||||
|     colorScheme: ResolvedCSSColorSchemeValue, |  | ||||||
| ): ResolvedCSSColorSchemeValue; |  | ||||||
| export function formatColorScheme(hint?: UIThemeHint): CSSColorSchemeValue; |  | ||||||
| export function formatColorScheme(hint?: UIThemeHint): CSSColorSchemeValue { |  | ||||||
|     if (!hint) return "auto"; |  | ||||||
|  |  | ||||||
|     switch (hint) { |  | ||||||
|         case "dark": |  | ||||||
|         case UiThemeEnum.Dark: |  | ||||||
|             return "dark"; |  | ||||||
|         case "light": |  | ||||||
|         case UiThemeEnum.Light: |  | ||||||
|             return "light"; |  | ||||||
|         case "auto": |  | ||||||
|         case UiThemeEnum.Automatic: |  | ||||||
|             return "auto"; |  | ||||||
|         default: |  | ||||||
|             console.warn(`Unknown color scheme hint: ${hint}. Defaulting to "auto".`); |  | ||||||
|             return "auto"; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| //#endregion |  | ||||||
|  |  | ||||||
| //#region Theme Functions |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Resolve the current UI theme based on the user's preference or the provided color scheme. |  | ||||||
|  * |  | ||||||
|  * @param hint The color scheme hint to use. |  | ||||||
|  * |  | ||||||
|  * @category CSS |  | ||||||
|  */ |  | ||||||
| export function resolveUITheme( |  | ||||||
|     hint?: UIThemeHint, |  | ||||||
|     defaultUITheme: ResolvedUITheme = UiThemeEnum.Light, |  | ||||||
| ): ResolvedUITheme { |  | ||||||
|     const colorScheme = formatColorScheme(hint); |  | ||||||
|  |  | ||||||
|     if (colorScheme !== "auto") return colorScheme; |  | ||||||
|  |  | ||||||
|     // Given that we don't know the user's preference, |  | ||||||
|     // we can determine the theme based on whether the default theme is |  | ||||||
|     // currently being overridden. |  | ||||||
|  |  | ||||||
|     const colorSchemeInversion = formatColorScheme(UIThemeInversion[defaultUITheme]); |  | ||||||
|  |  | ||||||
|     const mediaQueryList = createColorSchemeTarget(colorSchemeInversion); |  | ||||||
|  |  | ||||||
|     return mediaQueryList.matches ? colorSchemeInversion : defaultUITheme; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Effect listener invoked when the color scheme changes. |  | ||||||
|  */ |  | ||||||
| export type UIThemeListener = (currentUITheme: ResolvedUITheme) => void; |  | ||||||
| /** |  | ||||||
|  * Create an effect that runs |  | ||||||
|  * |  | ||||||
|  * @returns A cleanup function that removes the effect. |  | ||||||
|  */ |  | ||||||
| export function createUIThemeEffect( |  | ||||||
|     effect: UIThemeListener, |  | ||||||
|     listenerOptions?: AddEventListenerOptions, |  | ||||||
| ): () => void { |  | ||||||
|     const colorSchemeTarget = resolveUITheme(); |  | ||||||
|     const invertedColorSchemeTarget = UIThemeInversion[colorSchemeTarget]; |  | ||||||
|  |  | ||||||
|     let previousUITheme: ResolvedUITheme | undefined; |  | ||||||
|  |  | ||||||
|     // First, wrap the effect to ensure we can abort it. |  | ||||||
|     const changeListener = (event: MediaQueryListEvent) => { |  | ||||||
|         if (listenerOptions?.signal?.aborted) return; |  | ||||||
|  |  | ||||||
|         const currentUITheme = event.matches ? colorSchemeTarget : invertedColorSchemeTarget; |  | ||||||
|  |  | ||||||
|         if (previousUITheme === currentUITheme) return; |  | ||||||
|  |  | ||||||
|         previousUITheme = currentUITheme; |  | ||||||
|  |  | ||||||
|         effect(currentUITheme); |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     const mediaQueryList = createColorSchemeTarget(colorSchemeTarget); |  | ||||||
|  |  | ||||||
|     // Trigger the effect immediately. |  | ||||||
|     effect(colorSchemeTarget); |  | ||||||
|  |  | ||||||
|     // Listen for changes to the color scheme... |  | ||||||
|     mediaQueryList.addEventListener("change", changeListener, listenerOptions); |  | ||||||
|  |  | ||||||
|     // Finally, allow the caller to remove the effect. |  | ||||||
|     const cleanup = () => { |  | ||||||
|         mediaQueryList.removeEventListener("change", changeListener); |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     return cleanup; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| //#endregion |  | ||||||
|  |  | ||||||
| //#region Theme Element |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * An element that can be themed. |  | ||||||
|  */ |  | ||||||
| export interface ThemedElement extends HTMLElement { |  | ||||||
|     brand?: CurrentBrand; |  | ||||||
|     uiConfig?: UIConfig; |  | ||||||
|     config?: Config; |  | ||||||
|     activeTheme: ResolvedUITheme; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function rootInterface<T extends ThemedElement = ThemedElement>(): T | null { |  | ||||||
|     const element = document.body.querySelector<T>("[data-ak-interface-root]"); |  | ||||||
|  |  | ||||||
|     return element; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| //#endregion |  | ||||||
| @ -95,7 +95,7 @@ export class NavigationButtons extends AKElement { | |||||||
|             ); |             ); | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-xl"> |         return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg"> | ||||||
|             <button class="pf-c-button pf-m-plain" type="button" @click=${onClick}> |             <button class="pf-c-button pf-m-plain" type="button" @click=${onClick}> | ||||||
|                 <pf-tooltip position="top" content=${msg("Open API drawer")}> |                 <pf-tooltip position="top" content=${msg("Open API drawer")}> | ||||||
|                     <i class="fas fa-code" aria-hidden="true"></i> |                     <i class="fas fa-code" aria-hidden="true"></i> | ||||||
| @ -116,7 +116,7 @@ export class NavigationButtons extends AKElement { | |||||||
|             ); |             ); | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-xl"> |         return html`<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg"> | ||||||
|             <button |             <button | ||||||
|                 class="pf-c-button pf-m-plain" |                 class="pf-c-button pf-m-plain" | ||||||
|                 type="button" |                 type="button" | ||||||
| @ -156,7 +156,9 @@ export class NavigationButtons extends AKElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderImpersonation() { |     renderImpersonation() { | ||||||
|         if (!this.me?.original) return nothing; |         if (!this.me?.original) { | ||||||
|  |             return nothing; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         const onClick = async () => { |         const onClick = async () => { | ||||||
|             await new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve(); |             await new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve(); | ||||||
| @ -173,14 +175,6 @@ export class NavigationButtons extends AKElement { | |||||||
|             </div>`; |             </div>`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderAvatar() { |  | ||||||
|         return html`<img |  | ||||||
|             class="pf-c-page__header-tools-item pf-c-avatar pf-m-hidden pf-m-visible-on-xl" |  | ||||||
|             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,13 +206,17 @@ export class NavigationButtons extends AKElement { | |||||||
|             </div> |             </div> | ||||||
|             ${this.renderImpersonation()} |             ${this.renderImpersonation()} | ||||||
|             ${this.userDisplayName != "" |             ${this.userDisplayName != "" | ||||||
|                 ? html`<div class="pf-c-page__header-tools-group pf-m-hidden"> |                 ? html`<div class="pf-c-page__header-tools-group"> | ||||||
|                       <div class="pf-c-page__header-tools-item pf-m-visible-on-2xl"> |                       <div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-md"> | ||||||
|                           ${this.userDisplayName} |                           ${this.userDisplayName} | ||||||
|                       </div> |                       </div> | ||||||
|                   </div>` |                   </div>` | ||||||
|                 : nothing} |                 : nothing} | ||||||
|             ${this.renderAvatar()} |             <img | ||||||
|  |                 class="pf-c-avatar" | ||||||
|  |                 src=${ifDefined(this.me?.user.avatar)} | ||||||
|  |                 alt="${msg("Avatar image")}" | ||||||
|  |             /> | ||||||
|         </div>`; |         </div>`; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,125 +1,165 @@ | |||||||
|  | import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants"; | ||||||
| import { globalAK } from "@goauthentik/common/global"; | import { globalAK } from "@goauthentik/common/global"; | ||||||
| import { | import { UIConfig } from "@goauthentik/common/ui/config"; | ||||||
|     StyleSheetInit, | import { adaptCSS } from "@goauthentik/common/utils"; | ||||||
|     StyleSheetParent, | import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet"; | ||||||
|     appendStyleSheet, |  | ||||||
|     createStyleSheetUnsafe, |  | ||||||
|     removeStyleSheet, |  | ||||||
|     resolveStyleSheetParent, |  | ||||||
| } from "@goauthentik/common/stylesheets"; |  | ||||||
| import { ResolvedUITheme, createUIThemeEffect, resolveUITheme } from "@goauthentik/common/theme"; |  | ||||||
| import { type ThemedElement } from "@goauthentik/common/theme"; |  | ||||||
|  |  | ||||||
| import { localized } from "@lit/localize"; | import { localized } from "@lit/localize"; | ||||||
| import { CSSResultGroup, CSSResultOrNative, LitElement } from "lit"; | import { LitElement, ReactiveElement } from "lit"; | ||||||
| import { property } from "lit/decorators.js"; |  | ||||||
|  |  | ||||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||||
| import OneDark from "@goauthentik/common/styles/one-dark.css"; | import OneDark from "@goauthentik/common/styles/one-dark.css"; | ||||||
| import ThemeDark from "@goauthentik/common/styles/theme-dark.css"; | import ThemeDark from "@goauthentik/common/styles/theme-dark.css"; | ||||||
|  |  | ||||||
| import { CurrentBrand, UiThemeEnum } from "@goauthentik/api"; | import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| // Re-export the theme helpers | type AkInterface = HTMLElement & { | ||||||
| export { rootInterface } from "@goauthentik/common/theme"; |     getTheme: () => Promise<UiThemeEnum>; | ||||||
|  |     brand?: CurrentBrand; | ||||||
|  |     uiConfig?: UIConfig; | ||||||
|  |     config?: Config; | ||||||
|  |     get activeTheme(): UiThemeEnum | undefined; | ||||||
|  | }; | ||||||
|  |  | ||||||
| export interface AKElementInit { | export const rootInterface = <T extends AkInterface>(): T | undefined => | ||||||
|     brand?: Partial<CurrentBrand>; |     (document.body.querySelector("[data-ak-interface-root]") as T) ?? undefined; | ||||||
|     styleParents?: StyleSheetParent[]; |  | ||||||
| } | export const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)"; | ||||||
|  |  | ||||||
|  | // Ensure themes are converted to a static instance of CSS Stylesheet, otherwise the | ||||||
|  | // when changing themes we might not remove the correct css stylesheet instance. | ||||||
|  | const _darkTheme = ensureCSSStyleSheet(ThemeDark); | ||||||
|  |  | ||||||
| @localized() | @localized() | ||||||
| export class AKElement extends LitElement implements ThemedElement { | export class AKElement extends LitElement { | ||||||
|     /** |     _mediaMatcher?: MediaQueryList; | ||||||
|      * The resolved theme of the current element. |     _mediaMatcherHandler?: (ev?: MediaQueryListEvent) => void; | ||||||
|      * |     _activeTheme?: UiThemeEnum; | ||||||
|      * @remarks |  | ||||||
|      * |  | ||||||
|      * Unlike the browser's current color scheme, this is a value that can be |  | ||||||
|      * resolved to a specific theme, i.e. dark or light. |  | ||||||
|      */ |  | ||||||
|     @property({ |  | ||||||
|         attribute: "theme", |  | ||||||
|         type: String, |  | ||||||
|         reflect: true, |  | ||||||
|     }) |  | ||||||
|     public activeTheme: ResolvedUITheme; |  | ||||||
|  |  | ||||||
|     protected static readonly DarkColorSchemeStyleSheet = createStyleSheetUnsafe(ThemeDark); |     get activeTheme(): UiThemeEnum | undefined { | ||||||
|  |         return this._activeTheme; | ||||||
|     protected static finalizeStyles(styles?: CSSResultGroup): CSSResultOrNative[] { |  | ||||||
|         // Ensure all style sheets being passed are really style sheets. |  | ||||||
|         const baseStyles: StyleSheetInit[] = [AKGlobal, OneDark]; |  | ||||||
|  |  | ||||||
|         if (!styles) return baseStyles.map(createStyleSheetUnsafe); |  | ||||||
|  |  | ||||||
|         if (Array.isArray(styles)) { |  | ||||||
|             return [ |  | ||||||
|                 //--- |  | ||||||
|                 ...(styles as unknown as CSSResultOrNative[]), |  | ||||||
|                 ...baseStyles, |  | ||||||
|             ].flatMap(createStyleSheetUnsafe); |  | ||||||
|         } |  | ||||||
|         return [styles, ...baseStyles].map(createStyleSheetUnsafe); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     constructor(init?: AKElementInit) { |     constructor() { | ||||||
|         super(); |         super(); | ||||||
|  |  | ||||||
|         const config = globalAK(); |  | ||||||
|         const { brand = config.brand, styleParents = [] } = init || {}; |  | ||||||
|  |  | ||||||
|         this.activeTheme = resolveUITheme(brand?.uiTheme); |  | ||||||
|         this.#styleParents = styleParents; |  | ||||||
|  |  | ||||||
|         this.#customCSSStyleSheet = brand?.brandingCustomCss |  | ||||||
|             ? createStyleSheetUnsafe(brand.brandingCustomCss) |  | ||||||
|             : null; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     #styleParents: StyleSheetParent[] = []; |     setInitialStyles(root: DocumentOrShadowRoot) { | ||||||
|     #customCSSStyleSheet: CSSStyleSheet | null; |         const styleRoot: DocumentOrShadowRoot = ( | ||||||
|  |             "ShadyDOM" in window ? document : root | ||||||
|     #themeAbortController: AbortController | null = null; |         ) as DocumentOrShadowRoot; | ||||||
|  |         styleRoot.adoptedStyleSheets = adaptCSS([ | ||||||
|     public disconnectedCallback(): void { |  | ||||||
|         super.disconnectedCallback(); |  | ||||||
|         this.#themeAbortController?.abort(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     protected createRenderRoot(): HTMLElement | DocumentFragment { |  | ||||||
|         const renderRoot = super.createRenderRoot(); |  | ||||||
|  |  | ||||||
|         const styleRoot = resolveStyleSheetParent(renderRoot); |  | ||||||
|         const styleParents = Array.from( |  | ||||||
|             new Set<StyleSheetParent>([styleRoot, ...this.#styleParents]), |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         if (this.#customCSSStyleSheet) { |  | ||||||
|             console.debug(`authentik/element[${this.tagName.toLowerCase()}]: Adding custom CSS`); |  | ||||||
|  |  | ||||||
|             styleRoot.adoptedStyleSheets = [ |  | ||||||
|             ...styleRoot.adoptedStyleSheets, |             ...styleRoot.adoptedStyleSheets, | ||||||
|                 this.#customCSSStyleSheet, |             ensureCSSStyleSheet(AKGlobal), | ||||||
|  |             ensureCSSStyleSheet(OneDark), | ||||||
|  |         ]); | ||||||
|  |         this._initTheme(styleRoot); | ||||||
|  |         this._initCustomCSS(styleRoot); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected createRenderRoot() { | ||||||
|  |         this.fixElementStyles(); | ||||||
|  |         const root = super.createRenderRoot(); | ||||||
|  |         this.setInitialStyles(root as unknown as DocumentOrShadowRoot); | ||||||
|  |         return root; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getTheme(): Promise<UiThemeEnum> { | ||||||
|  |         return rootInterface()?.getTheme() || UiThemeEnum.Automatic; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fixElementStyles() { | ||||||
|  |         // Ensure all style sheets being passed are really style sheets. | ||||||
|  |         (this.constructor as typeof ReactiveElement).elementStyles = ( | ||||||
|  |             this.constructor as typeof ReactiveElement | ||||||
|  |         ).elementStyles.map(ensureCSSStyleSheet); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async _initTheme(root: DocumentOrShadowRoot): Promise<void> { | ||||||
|  |         // Early activate theme based on media query to prevent light flash | ||||||
|  |         // when dark is preferred | ||||||
|  |         this._applyTheme(root, globalAK().brand.uiTheme); | ||||||
|  |         this._applyTheme(root, await this.getTheme()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async _initCustomCSS(root: DocumentOrShadowRoot): Promise<void> { | ||||||
|  |         const brand = globalAK().brand; | ||||||
|  |         if (!brand) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         const sheet = await new CSSStyleSheet().replace(brand.brandingCustomCss); | ||||||
|  |         root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     _applyTheme(root: DocumentOrShadowRoot, theme?: UiThemeEnum): void { | ||||||
|  |         if (!theme) { | ||||||
|  |             theme = UiThemeEnum.Automatic; | ||||||
|  |         } | ||||||
|  |         if (theme === UiThemeEnum.Automatic) { | ||||||
|  |             // Create a media matcher to automatically switch the theme depending on | ||||||
|  |             // prefers-color-scheme | ||||||
|  |             if (!this._mediaMatcher) { | ||||||
|  |                 this._mediaMatcher = window.matchMedia(QUERY_MEDIA_COLOR_LIGHT); | ||||||
|  |                 this._mediaMatcherHandler = (ev?: MediaQueryListEvent) => { | ||||||
|  |                     const theme = | ||||||
|  |                         ev?.matches || this._mediaMatcher?.matches | ||||||
|  |                             ? UiThemeEnum.Light | ||||||
|  |                             : UiThemeEnum.Dark; | ||||||
|  |                     this._activateTheme(theme, root); | ||||||
|  |                 }; | ||||||
|  |                 this._mediaMatcherHandler(undefined); | ||||||
|  |                 this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler); | ||||||
|  |             } | ||||||
|  |             return; | ||||||
|  |         } else if (this._mediaMatcher && this._mediaMatcherHandler) { | ||||||
|  |             // Theme isn't automatic and we have a matcher configured, remove the matcher | ||||||
|  |             // to prevent changes | ||||||
|  |             this._mediaMatcher.removeEventListener("change", this._mediaMatcherHandler); | ||||||
|  |             this._mediaMatcher = undefined; | ||||||
|  |         } | ||||||
|  |         this._activateTheme(theme, root); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     static themeToStylesheet(theme?: UiThemeEnum): CSSStyleSheet | undefined { | ||||||
|  |         if (theme === UiThemeEnum.Dark) { | ||||||
|  |             return _darkTheme; | ||||||
|  |         } | ||||||
|  |         return undefined; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Directly activate a given theme, accepts multiple document/ShadowDOMs to apply the stylesheet | ||||||
|  |      * to. The stylesheets are applied to each DOM in order. Does nothing if the given theme is already active. | ||||||
|  |      */ | ||||||
|  |     _activateTheme(theme: UiThemeEnum, ...roots: DocumentOrShadowRoot[]) { | ||||||
|  |         if (theme === this._activeTheme) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         // Make sure we only get to this callback once we've picked a concise theme choice | ||||||
|  |         this.dispatchEvent( | ||||||
|  |             new CustomEvent(EVENT_THEME_CHANGE, { | ||||||
|  |                 bubbles: true, | ||||||
|  |                 composed: true, | ||||||
|  |                 detail: theme, | ||||||
|  |             }), | ||||||
|  |         ); | ||||||
|  |         this.setAttribute("theme", theme); | ||||||
|  |         const stylesheet = AKElement.themeToStylesheet(theme); | ||||||
|  |         const oldStylesheet = AKElement.themeToStylesheet(this._activeTheme); | ||||||
|  |         roots.forEach((root) => { | ||||||
|  |             if (stylesheet) { | ||||||
|  |                 root.adoptedStyleSheets = [ | ||||||
|  |                     ...root.adoptedStyleSheets, | ||||||
|  |                     ensureCSSStyleSheet(stylesheet), | ||||||
|                 ]; |                 ]; | ||||||
|             } |             } | ||||||
|  |             if (oldStylesheet) { | ||||||
|         this.#themeAbortController = new AbortController(); |                 root.adoptedStyleSheets = root.adoptedStyleSheets.filter( | ||||||
|  |                     (v) => v !== oldStylesheet, | ||||||
|         createUIThemeEffect( |  | ||||||
|             (currentUITheme) => { |  | ||||||
|                 if (currentUITheme === UiThemeEnum.Dark) { |  | ||||||
|                     appendStyleSheet(AKElement.DarkColorSchemeStyleSheet, ...styleParents); |  | ||||||
|                 } else { |  | ||||||
|                     removeStyleSheet(AKElement.DarkColorSchemeStyleSheet, ...styleParents); |  | ||||||
|                 } |  | ||||||
|                 this.activeTheme = currentUITheme; |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|                 signal: this.#themeAbortController.signal, |  | ||||||
|             }, |  | ||||||
|                 ); |                 ); | ||||||
|  |             } | ||||||
|         return renderRoot; |         }); | ||||||
|  |         this._activeTheme = theme; | ||||||
|  |         this.requestUpdate(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,6 +1,5 @@ | |||||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||||
| import { EVENT_REFRESH } from "@goauthentik/common/constants"; | import { EVENT_REFRESH } from "@goauthentik/common/constants"; | ||||||
| import { ThemedElement } from "@goauthentik/common/theme"; |  | ||||||
| import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts"; | import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts"; | ||||||
| import type { ReactiveElementHost } from "@goauthentik/elements/types.js"; | import type { ReactiveElementHost } from "@goauthentik/elements/types.js"; | ||||||
|  |  | ||||||
| @ -10,12 +9,14 @@ import type { ReactiveController } from "lit"; | |||||||
| import type { CurrentBrand } from "@goauthentik/api"; | import type { CurrentBrand } from "@goauthentik/api"; | ||||||
| import { CoreApi } from "@goauthentik/api"; | import { CoreApi } from "@goauthentik/api"; | ||||||
|  |  | ||||||
|  | import type { AkInterface } from "./Interface"; | ||||||
|  |  | ||||||
| export class BrandContextController implements ReactiveController { | export class BrandContextController implements ReactiveController { | ||||||
|     host!: ReactiveElementHost<ThemedElement>; |     host!: ReactiveElementHost<AkInterface>; | ||||||
|  |  | ||||||
|     context!: ContextProvider<{ __context__: CurrentBrand | undefined }>; |     context!: ContextProvider<{ __context__: CurrentBrand | undefined }>; | ||||||
|  |  | ||||||
|     constructor(host: ReactiveElementHost<ThemedElement>) { |     constructor(host: ReactiveElementHost<AkInterface>) { | ||||||
|         this.host = host; |         this.host = host; | ||||||
|         this.context = new ContextProvider(this.host, { |         this.context = new ContextProvider(this.host, { | ||||||
|             context: authentikBrandContext, |             context: authentikBrandContext, | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||||
| import { EVENT_REFRESH } from "@goauthentik/common/constants"; | import { EVENT_REFRESH } from "@goauthentik/common/constants"; | ||||||
| import { globalAK } from "@goauthentik/common/global"; | import { globalAK } from "@goauthentik/common/global"; | ||||||
| import { ThemedElement } from "@goauthentik/common/theme"; |  | ||||||
| import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts"; | import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts"; | ||||||
| import type { ReactiveElementHost } from "@goauthentik/elements/types.js"; | import type { ReactiveElementHost } from "@goauthentik/elements/types.js"; | ||||||
|  |  | ||||||
| @ -11,12 +10,14 @@ import type { ReactiveController } from "lit"; | |||||||
| import type { Config } from "@goauthentik/api"; | import type { Config } from "@goauthentik/api"; | ||||||
| import { RootApi } from "@goauthentik/api"; | import { RootApi } from "@goauthentik/api"; | ||||||
|  |  | ||||||
|  | import type { AkInterface } from "./Interface"; | ||||||
|  |  | ||||||
| export class ConfigContextController implements ReactiveController { | export class ConfigContextController implements ReactiveController { | ||||||
|     host!: ReactiveElementHost<ThemedElement>; |     host!: ReactiveElementHost<AkInterface>; | ||||||
|  |  | ||||||
|     context!: ContextProvider<{ __context__: Config | undefined }>; |     context!: ContextProvider<{ __context__: Config | undefined }>; | ||||||
|  |  | ||||||
|     constructor(host: ReactiveElementHost<ThemedElement>) { |     constructor(host: ReactiveElementHost<AkInterface>) { | ||||||
|         this.host = host; |         this.host = host; | ||||||
|         this.context = new ContextProvider(this.host, { |         this.context = new ContextProvider(this.host, { | ||||||
|             context: authentikConfigContext, |             context: authentikConfigContext, | ||||||
|  | |||||||
| @ -1,85 +1,107 @@ | |||||||
| import { | import { UIConfig, uiConfig } from "@goauthentik/common/ui/config"; | ||||||
|     appendStyleSheet, |  | ||||||
|     createStyleSheetUnsafe, |  | ||||||
|     resolveStyleSheetParent, |  | ||||||
| } from "@goauthentik/common/stylesheets"; |  | ||||||
| import { ThemedElement } from "@goauthentik/common/theme"; |  | ||||||
| import { UIConfig } from "@goauthentik/common/ui/config"; |  | ||||||
| import { AKElement, AKElementInit } from "@goauthentik/elements/Base"; |  | ||||||
| import { VersionContextController } from "@goauthentik/elements/Interface/VersionContextController"; | import { VersionContextController } from "@goauthentik/elements/Interface/VersionContextController"; | ||||||
| import { ModalOrchestrationController } from "@goauthentik/elements/controllers/ModalOrchestrationController.js"; | import { ModalOrchestrationController } from "@goauthentik/elements/controllers/ModalOrchestrationController.js"; | ||||||
|  | import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet"; | ||||||
|  |  | ||||||
| import { state } from "lit/decorators.js"; | import { state } from "lit/decorators.js"; | ||||||
|  |  | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
| import type { Config, CurrentBrand, LicenseSummary, Version } from "@goauthentik/api"; | import type { Config, CurrentBrand, LicenseSummary, Version } from "@goauthentik/api"; | ||||||
|  | import { UiThemeEnum } from "@goauthentik/api"; | ||||||
|  |  | ||||||
|  | import { AKElement, rootInterface } from "../Base"; | ||||||
| import { BrandContextController } from "./BrandContextController"; | import { BrandContextController } from "./BrandContextController"; | ||||||
| import { ConfigContextController } from "./ConfigContextController"; | import { ConfigContextController } from "./ConfigContextController"; | ||||||
| import { EnterpriseContextController } from "./EnterpriseContextController"; | import { EnterpriseContextController } from "./EnterpriseContextController"; | ||||||
|  |  | ||||||
|  | export type AkInterface = HTMLElement & { | ||||||
|  |     getTheme: () => Promise<UiThemeEnum>; | ||||||
|  |     brand?: CurrentBrand; | ||||||
|  |     uiConfig?: UIConfig; | ||||||
|  |     config?: Config; | ||||||
|  | }; | ||||||
|  |  | ||||||
| const brandContext = Symbol("brandContext"); | const brandContext = Symbol("brandContext"); | ||||||
| const configContext = Symbol("configContext"); | const configContext = Symbol("configContext"); | ||||||
| const modalController = Symbol("modalController"); | const modalController = Symbol("modalController"); | ||||||
| const versionContext = Symbol("versionContext"); | const versionContext = Symbol("versionContext"); | ||||||
|  |  | ||||||
| export abstract class Interface extends AKElement implements ThemedElement { | export class Interface extends AKElement implements AkInterface { | ||||||
|     protected static readonly PFBaseStyleSheet = createStyleSheetUnsafe(PFBase); |     [brandContext]!: BrandContextController; | ||||||
|  |  | ||||||
|     [brandContext]: BrandContextController; |     [configContext]!: ConfigContextController; | ||||||
|  |  | ||||||
|     [configContext]: ConfigContextController; |     [modalController]!: ModalOrchestrationController; | ||||||
|  |  | ||||||
|     [modalController]: ModalOrchestrationController; |  | ||||||
|  |  | ||||||
|     @state() |     @state() | ||||||
|     public config?: Config; |     uiConfig?: UIConfig; | ||||||
|  |  | ||||||
|     @state() |     @state() | ||||||
|     public brand?: CurrentBrand; |     config?: Config; | ||||||
|  |  | ||||||
|     constructor({ styleParents = [], ...init }: AKElementInit = {}) { |     @state() | ||||||
|         const styleParent = resolveStyleSheetParent(document); |     brand?: CurrentBrand; | ||||||
|  |  | ||||||
|         super({ |     constructor() { | ||||||
|             ...init, |         super(); | ||||||
|             styleParents: [styleParent, ...styleParents], |         document.adoptedStyleSheets = [...document.adoptedStyleSheets, ensureCSSStyleSheet(PFBase)]; | ||||||
|         }); |         this._initContexts(); | ||||||
|  |         this.dataset.akInterfaceRoot = "true"; | ||||||
|         this.dataset.akInterfaceRoot = this.tagName.toLowerCase(); |     } | ||||||
|  |  | ||||||
|         appendStyleSheet(Interface.PFBaseStyleSheet, styleParent); |  | ||||||
|  |  | ||||||
|  |     _initContexts() { | ||||||
|         this[brandContext] = new BrandContextController(this); |         this[brandContext] = new BrandContextController(this); | ||||||
|         this[configContext] = new ConfigContextController(this); |         this[configContext] = new ConfigContextController(this); | ||||||
|         this[modalController] = new ModalOrchestrationController(this); |         this[modalController] = new ModalOrchestrationController(this); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     _activateTheme(theme: UiThemeEnum, ...roots: DocumentOrShadowRoot[]): void { | ||||||
|  |         if (theme === this._activeTheme) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         console.debug( | ||||||
|  |             `authentik/interface[${rootInterface()?.tagName.toLowerCase()}]: Enabling theme ${theme}`, | ||||||
|  |         ); | ||||||
|  |         // Special case for root interfaces, as they need to modify the global document CSS too | ||||||
|  |         // Instead of calling ._activateTheme() twice, we insert the root document in the call | ||||||
|  |         // since multiple calls to ._activateTheme() would not do anything after the first call | ||||||
|  |         // as the theme is already enabled. | ||||||
|  |         roots.unshift(document as unknown as DocumentOrShadowRoot); | ||||||
|  |         super._activateTheme(theme, ...roots); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getTheme(): Promise<UiThemeEnum> { | ||||||
|  |         if (!this.uiConfig) { | ||||||
|  |             this.uiConfig = await uiConfig(); | ||||||
|  |         } | ||||||
|  |         return this.uiConfig.theme?.base || UiThemeEnum.Automatic; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface AkAuthenticatedInterface extends ThemedElement { | export type AkAuthenticatedInterface = AkInterface & { | ||||||
|     licenseSummary?: LicenseSummary; |     licenseSummary?: LicenseSummary; | ||||||
|     version?: Version; |     version?: Version; | ||||||
| } | }; | ||||||
|  |  | ||||||
| const enterpriseContext = Symbol("enterpriseContext"); | const enterpriseContext = Symbol("enterpriseContext"); | ||||||
|  |  | ||||||
| export class AuthenticatedInterface extends Interface implements AkAuthenticatedInterface { | export class AuthenticatedInterface extends Interface { | ||||||
|     [enterpriseContext]!: EnterpriseContextController; |     [enterpriseContext]!: EnterpriseContextController; | ||||||
|     [versionContext]!: VersionContextController; |     [versionContext]!: VersionContextController; | ||||||
|  |  | ||||||
|     @state() |     @state() | ||||||
|     public uiConfig?: UIConfig; |     licenseSummary?: LicenseSummary; | ||||||
|  |  | ||||||
|     @state() |     @state() | ||||||
|     public licenseSummary?: LicenseSummary; |     version?: Version; | ||||||
|  |  | ||||||
|     @state() |     constructor() { | ||||||
|     public version?: Version; |         super(); | ||||||
|  |     } | ||||||
|     constructor(init?: AKElementInit) { |  | ||||||
|         super(init); |  | ||||||
|  |  | ||||||
|  |     _initContexts(): void { | ||||||
|  |         super._initContexts(); | ||||||
|         this[enterpriseContext] = new EnterpriseContextController(this); |         this[enterpriseContext] = new EnterpriseContextController(this); | ||||||
|         this[versionContext] = new VersionContextController(this); |         this[versionContext] = new VersionContextController(this); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -5,23 +5,20 @@ 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, getConfigForUser } from "@goauthentik/common/ui/config"; | import { UIConfig, UserDisplay, uiConfig } 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, LitElement, TemplateResult, css, html, nothing } from "lit"; | import { CSSResult, 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"; | ||||||
| @ -29,52 +26,34 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; | |||||||
|  |  | ||||||
| import { SessionUser } from "@goauthentik/api"; | import { SessionUser } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| //#region Page Navbar | @customElement("ak-page-header") | ||||||
|  | export class PageHeader extends WithBrandConfig(AKElement) { | ||||||
| export interface PageNavbarDetails { |     @property() | ||||||
|     header?: string; |  | ||||||
|     description?: string; |  | ||||||
|     icon?: string; |     icon?: string; | ||||||
|     iconImage?: boolean; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |     @property({ type: Boolean }) | ||||||
|  * A global navbar component at the top of the page. |     iconImage = false; | ||||||
|  * |  | ||||||
|  * 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 |  | ||||||
|  |  | ||||||
|     private static elementRef: AKPageNavbar | null = null; |     @property() | ||||||
|  |     header = ""; | ||||||
|  |  | ||||||
|     static readonly setNavbarDetails = (detail: Partial<PageNavbarDetails>): void => { |     @property() | ||||||
|         const { elementRef } = AKPageNavbar; |     description?: string; | ||||||
|         if (!elementRef) { |  | ||||||
|             console.debug( |  | ||||||
|                 `ak-page-header: Could not find ak-page-navbar, skipping event dispatch.`, |  | ||||||
|             ); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const { header, description, icon, iconImage } = detail; |     @property({ type: Boolean }) | ||||||
|  |     hasIcon = true; | ||||||
|  |  | ||||||
|         elementRef.header = header; |     @state() | ||||||
|         elementRef.description = description; |     me?: SessionUser; | ||||||
|         elementRef.icon = icon; |  | ||||||
|         elementRef.iconImage = iconImage || false; |     @state() | ||||||
|         elementRef.hasIcon = !!icon; |     uiConfig!: UIConfig; | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return [ |         return [ | ||||||
|             PFBase, |             PFBase, | ||||||
|             PFButton, |             PFButton, | ||||||
|             PFPage, |             PFPage, | ||||||
|             PFDrawer, |  | ||||||
|  |  | ||||||
|             PFNotificationBadge, |             PFNotificationBadge, | ||||||
|             PFContent, |             PFContent, | ||||||
|             PFAvatar, |             PFAvatar, | ||||||
| @ -84,313 +63,127 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb | |||||||
|                     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: 6rem; |                     min-height: 114px; | ||||||
|  |                     max-height: 114px; | ||||||
|                     display: grid; |                     background-color: var(--pf-c-page--BackgroundColor); | ||||||
|                     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; | ||||||
|                 } |                 } | ||||||
|  |                 .pf-c-page__main-section { | ||||||
|                 .items { |                     flex-grow: 1; | ||||||
|                     display: block; |                     flex-shrink: 1; | ||||||
|  |  | ||||||
|                     &.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); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 .brand { |  | ||||||
|                     grid-area: brand; |  | ||||||
|                     background-color: var(--ak-brand-background-color); |  | ||||||
|                     height: 100%; |  | ||||||
|                     width: var(--pf-c-page__sidebar--Width); |  | ||||||
|                     align-items: center; |  | ||||||
|                     padding-inline: var(--pf-global--spacer--sm); |  | ||||||
|  |  | ||||||
|                     display: flex; |                     display: flex; | ||||||
|  |                     flex-direction: column; | ||||||
|                     justify-content: center; |                     justify-content: center; | ||||||
|  |  | ||||||
|                     &.pf-m-collapsed { |  | ||||||
|                         display: none; |  | ||||||
|                 } |                 } | ||||||
|  |                 img.pf-icon { | ||||||
|                     @media (max-width: 1199px) { |                     max-height: 24px; | ||||||
|                         display: none; |  | ||||||
|                 } |                 } | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 .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: 1.5rem; |                     font-size: 24px; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 .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; | ||||||
|  |                 } | ||||||
|             `, |             `, | ||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     //#endregion |     constructor() { | ||||||
|  |         super(); | ||||||
|  |         window.addEventListener(EVENT_WS_MESSAGE, () => { | ||||||
|  |             this.firstUpdated(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     //#region Properties |     async firstUpdated() { | ||||||
|  |         this.me = await me(); | ||||||
|  |         this.uiConfig = await uiConfig(); | ||||||
|  |         this.uiConfig.navbar.userDisplay = UserDisplay.none; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @property({ type: String }) |     setTitle(header?: 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) { |         if (header !== undefined && header !== "") { | ||||||
|             title = `${header} - ${title}`; |             title = `${header} - ${title}`; | ||||||
|         } |         } | ||||||
|         document.title = title; |         document.title = title; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     #toggleSidebar() { |     willUpdate() { | ||||||
|         this.open = !this.open; |         // Always update title, even if there's no header value set, | ||||||
|  |         // as in that case we still need to return to the generic title | ||||||
|  |         this.setTitle(this.header); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     renderIcon() { | ||||||
|  |         if (this.icon) { | ||||||
|  |             if (this.iconImage && !this.icon.startsWith("fa://")) { | ||||||
|  |                 return html`<img class="pf-icon" src="${this.icon}" alt="page icon" />`; | ||||||
|  |             } | ||||||
|  |             const icon = this.icon.replaceAll("fa://", "fa "); | ||||||
|  |             return html`<i class=${icon}></i>`; | ||||||
|  |         } | ||||||
|  |         return nothing; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     render(): TemplateResult { | ||||||
|  |         return html`<div class="bar"> | ||||||
|  |             <button | ||||||
|  |                 class="sidebar-trigger pf-c-button pf-m-plain" | ||||||
|  |                 @click=${() => { | ||||||
|                     this.dispatchEvent( |                     this.dispatchEvent( | ||||||
|                         new CustomEvent(EVENT_SIDEBAR_TOGGLE, { |                         new CustomEvent(EVENT_SIDEBAR_TOGGLE, { | ||||||
|                             bubbles: true, |                             bubbles: true, | ||||||
|                             composed: true, |                             composed: true, | ||||||
|                         }), |                         }), | ||||||
|                     ); |                     ); | ||||||
|     } |                 }} | ||||||
|  |  | ||||||
|     //#endregion |  | ||||||
|  |  | ||||||
|     //#region Lifecycle |  | ||||||
|  |  | ||||||
|     public connectedCallback(): void { |  | ||||||
|         super.connectedCallback(); |  | ||||||
|         AKPageNavbar.elementRef = this; |  | ||||||
|  |  | ||||||
|         window.addEventListener(EVENT_WS_MESSAGE, () => { |  | ||||||
|             this.firstUpdated(); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public disconnectedCallback(): void { |  | ||||||
|         super.disconnectedCallback(); |  | ||||||
|         AKPageNavbar.elementRef = null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public async firstUpdated() { |  | ||||||
|         this.session = await me(); |  | ||||||
|         this.uiConfig = getConfigForUser(this.session.user); |  | ||||||
|         this.uiConfig.navbar.userDisplay = UserDisplay.none; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     willUpdate() { |  | ||||||
|         // Always update title, even if there's no header value set, |  | ||||||
|         // as in that case we still need to return to the generic title |  | ||||||
|         this.#setTitle(this.header); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     //#endregion |  | ||||||
|  |  | ||||||
|     //#region Render |  | ||||||
|  |  | ||||||
|     renderIcon() { |  | ||||||
|         if (this.icon) { |  | ||||||
|             if (this.iconImage && !this.icon.startsWith("fa://")) { |  | ||||||
|                 return html`<img class="accent-icon pf-icon" src="${this.icon}" alt="page icon" />`; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             const icon = this.icon.replaceAll("fa://", "fa "); |  | ||||||
|  |  | ||||||
|             return html`<i class="accent-icon ${icon}"></i>`; |  | ||||||
|         } |  | ||||||
|         return nothing; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     render(): TemplateResult { |  | ||||||
|         return html`<navbar aria-label="Main" class="navbar"> |  | ||||||
|                 <aside class="brand ${this.open ? "" : "pf-m-collapsed"}"> |  | ||||||
|                     <a href="#/"> |  | ||||||
|                         <div class="logo"> |  | ||||||
|                             <img |  | ||||||
|                                 src=${themeImage( |  | ||||||
|                                     this.brand?.brandingLogo ?? DefaultBrand.brandingLogo, |  | ||||||
|                                 )} |  | ||||||
|                                 alt="${msg("authentik Logo")}" |  | ||||||
|                                 loading="lazy" |  | ||||||
|                             /> |  | ||||||
|                         </div> |  | ||||||
|                     </a> |  | ||||||
|                 </aside> |  | ||||||
|                 <button |  | ||||||
|                     class="sidebar-trigger pf-c-button pf-m-plain" |  | ||||||
|                     @click=${this.#toggleSidebar} |  | ||||||
|                     aria-label=${msg("Toggle sidebar")} |  | ||||||
|                     aria-expanded=${this.open ? "true" : "false"} |  | ||||||
|             > |             > | ||||||
|                 <i class="fas fa-bars"></i> |                 <i class="fas fa-bars"></i> | ||||||
|             </button> |             </button> | ||||||
|  |             <section class="pf-c-page__main-section pf-m-light"> | ||||||
|                 <section |                 <div class="pf-c-content"> | ||||||
|                     class="items primary pf-c-content ${this.description ? "block-sibling" : ""}" |                     <h1> | ||||||
|                 > |  | ||||||
|                     <h1 class="page-title"> |  | ||||||
|                         ${this.hasIcon |                         ${this.hasIcon | ||||||
|                             ? html`<slot name="icon">${this.renderIcon()}</slot>` |                             ? html`<slot name="icon">${this.renderIcon()}</slot> ` | ||||||
|                             : nothing} |                             : nothing} | ||||||
|                         ${this.header} |                         <slot name="header">${this.header}</slot> | ||||||
|                     </h1> |                     </h1> | ||||||
|  |                     ${this.description ? html`<p>${this.description}</p>` : html``} | ||||||
|  |                 </div> | ||||||
|             </section> |             </section> | ||||||
|                 ${this.description |             <div class="pf-c-page__header-tools"> | ||||||
|                     ? html`<section class="items page-description pf-c-content"> |  | ||||||
|                           <p>${this.description}</p> |  | ||||||
|                       </section>` |  | ||||||
|                     : nothing} |  | ||||||
|  |  | ||||||
|                 <section class="items secondary"> |  | ||||||
|                 <div class="pf-c-page__header-tools-group"> |                 <div class="pf-c-page__header-tools-group"> | ||||||
|                         <ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.session}> |                     <ak-nav-buttons .uiConfig=${this.uiConfig} .me=${this.me}> | ||||||
|                         <a |                         <a | ||||||
|                             class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md" |                             class="pf-c-button pf-m-secondary pf-m-small pf-u-display-none pf-u-display-block-on-md" | ||||||
|                             href="${globalAK().api.base}if/user/" |                             href="${globalAK().api.base}if/user/" | ||||||
| @ -400,76 +193,13 @@ export class AKPageNavbar extends WithBrandConfig(AKElement) implements PageNavb | |||||||
|                         </a> |                         </a> | ||||||
|                     </ak-nav-buttons> |                     </ak-nav-buttons> | ||||||
|                 </div> |                 </div> | ||||||
|                 </section> |             </div> | ||||||
|             </navbar> |         </div>`; | ||||||
|             <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": AKPageHeader; |         "ak-page-header": PageHeader; | ||||||
|         "ak-page-navbar": AKPageNavbar; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -22,7 +22,6 @@ export class Sidebar extends AKElement { | |||||||
|             css` |             css` | ||||||
|                 :host { |                 :host { | ||||||
|                     z-index: 100; |                     z-index: 100; | ||||||
|                     --pf-c-page__sidebar--Transition: 0 !important; |  | ||||||
|                 } |                 } | ||||||
|                 .pf-c-nav__link.pf-m-current::after, |                 .pf-c-nav__link.pf-m-current::after, | ||||||
|                 .pf-c-nav__link.pf-m-current:hover::after, |                 .pf-c-nav__link.pf-m-current:hover::after, | ||||||
| @ -36,7 +35,10 @@ 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; | ||||||
| @ -68,6 +70,7 @@ 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,3 +1,17 @@ | |||||||
|  | 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 | ||||||
| @ -14,3 +28,79 @@ 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; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,20 +1,19 @@ | |||||||
| import { |  | ||||||
|     appendStyleSheet, |  | ||||||
|     assertAdoptableStyleSheetParent, |  | ||||||
| } from "@goauthentik/common/stylesheets.js"; |  | ||||||
|  |  | ||||||
| import { TemplateResult, render as litRender } from "lit"; | import { TemplateResult, render as litRender } from "lit"; | ||||||
|  |  | ||||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
|  | import { ensureCSSStyleSheet } from "../utils/ensureCSSStyleSheet.js"; | ||||||
|  |  | ||||||
| // A special version of render that ensures our style sheets will always be available | // A special version of render that ensures our style sheets will always be available | ||||||
| // to all elements under test.  Ensures they look right during testing, and that any | // to all elements under test.  Ensures they look right during testing, and that any | ||||||
| // CSS-based checks for visibility will return correct values. | // CSS-based checks for visibility will return correct values. | ||||||
|  |  | ||||||
| export const render = (body: TemplateResult) => { | export const render = (body: TemplateResult) => { | ||||||
|     assertAdoptableStyleSheetParent(document); |     document.adoptedStyleSheets = [ | ||||||
|  |         ...document.adoptedStyleSheets, | ||||||
|     appendStyleSheet([PFBase, AKGlobal], document); |         ensureCSSStyleSheet(PFBase), | ||||||
|  |         ensureCSSStyleSheet(AKGlobal), | ||||||
|  |     ]; | ||||||
|     return litRender(body, document.body); |     return litRender(body, document.body); | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -1,14 +1,9 @@ | |||||||
|  | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
|  |  | ||||||
| import { type LitElement, type ReactiveControllerHost, type TemplateResult, nothing } from "lit"; | import { type LitElement, type ReactiveControllerHost, type TemplateResult, nothing } from "lit"; | ||||||
| import "lit"; | import "lit"; | ||||||
|  |  | ||||||
| /** | export type ReactiveElementHost<T = AKElement> = Partial<ReactiveControllerHost> & T; | ||||||
|  * A custom element which may be used as a host for a ReactiveController. |  | ||||||
|  * |  | ||||||
|  * @remarks |  | ||||||
|  * |  | ||||||
|  * This type is derived from an internal type in Lit. |  | ||||||
|  */ |  | ||||||
| export type ReactiveElementHost<T> = Partial<ReactiveControllerHost & T> & HTMLElement; |  | ||||||
|  |  | ||||||
| export type AbstractLitElementConstructor = abstract new (...args: never[]) => LitElement; | export type AbstractLitElementConstructor = abstract new (...args: never[]) => LitElement; | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										35
									
								
								web/src/elements/utils/ensureCSSStyleSheet.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								web/src/elements/utils/ensureCSSStyleSheet.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | |||||||
|  | import { CSSResult, unsafeCSS } from "lit"; | ||||||
|  |  | ||||||
|  | const supportsAdoptingStyleSheets: boolean = | ||||||
|  |     window.ShadowRoot && | ||||||
|  |     (window.ShadyCSS === undefined || window.ShadyCSS.nativeShadow) && | ||||||
|  |     "adoptedStyleSheets" in Document.prototype && | ||||||
|  |     "replace" in CSSStyleSheet.prototype; | ||||||
|  |  | ||||||
|  | function stringToStylesheet(css: string) { | ||||||
|  |     if (supportsAdoptingStyleSheets) { | ||||||
|  |         const sheet = unsafeCSS(css).styleSheet; | ||||||
|  |         if (sheet === undefined) { | ||||||
|  |             throw new Error( | ||||||
|  |                 `CSS processing error: undefined stylesheet from string.  Source: ${css}`, | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |         return sheet; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const sheet = new CSSStyleSheet(); | ||||||
|  |     sheet.replaceSync(css); | ||||||
|  |     return sheet; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function cssResultToStylesheet(css: CSSResult) { | ||||||
|  |     const sheet = css.styleSheet; | ||||||
|  |     return sheet ? sheet : stringToStylesheet(css.toString()); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const ensureCSSStyleSheet = (css: string | CSSStyleSheet | CSSResult): CSSStyleSheet => | ||||||
|  |     css instanceof CSSResult | ||||||
|  |         ? cssResultToStylesheet(css) | ||||||
|  |         : typeof css === "string" | ||||||
|  |           ? stringToStylesheet(css) | ||||||
|  |           : css; | ||||||
| @ -1,55 +0,0 @@ | |||||||
| /** |  | ||||||
|  * @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,8 +1,13 @@ | |||||||
| import { resolveUITheme } from "@goauthentik/common/theme"; | import { QUERY_MEDIA_COLOR_LIGHT, rootInterface } from "@goauthentik/elements/Base"; | ||||||
| import { rootInterface } from "@goauthentik/elements/Base"; |  | ||||||
|  | import { UiThemeEnum } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| export function themeImage(rawPath: string) { | export function themeImage(rawPath: string) { | ||||||
|     const enabledTheme = rootInterface()?.activeTheme || resolveUITheme(); |     let enabledTheme = rootInterface()?.activeTheme; | ||||||
|  |     if (!enabledTheme || enabledTheme === UiThemeEnum.Automatic) { | ||||||
|  |         enabledTheme = window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches | ||||||
|  |             ? UiThemeEnum.Light | ||||||
|  |             : UiThemeEnum.Dark; | ||||||
|  |     } | ||||||
|     return rawPath.replaceAll("%(theme)s", enabledTheme); |     return rawPath.replaceAll("%(theme)s", enabledTheme); | ||||||
| } | } | ||||||
|  | |||||||
| @ -46,6 +46,7 @@ import { | |||||||
|     FlowsApi, |     FlowsApi, | ||||||
|     ResponseError, |     ResponseError, | ||||||
|     ShellChallenge, |     ShellChallenge, | ||||||
|  |     UiThemeEnum, | ||||||
| } from "@goauthentik/api"; | } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| @customElement("ak-flow-executor") | @customElement("ak-flow-executor") | ||||||
| @ -199,6 +200,10 @@ export class FlowExecutor extends Interface implements StageHost { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async getTheme(): Promise<UiThemeEnum> { | ||||||
|  |         return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     async submit( |     async submit( | ||||||
|         payload?: FlowChallengeResponseRequest, |         payload?: FlowChallengeResponseRequest, | ||||||
|         options?: SubmitOptions, |         options?: SubmitOptions, | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { BrandedHTMLPolicy, sanitizeHTML } from "@goauthentik/common/purify"; | import { purify } 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,6 +21,8 @@ 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() { | ||||||
| @ -31,21 +33,13 @@ export class BrandLinks extends AKElement { | |||||||
|     links: FooterLink[] = []; |     links: FooterLink[] = []; | ||||||
|  |  | ||||||
|     render() { |     render() { | ||||||
|         const links = [...(this.links ?? [])]; |         const links = [...(this.links ?? []), poweredBy]; | ||||||
|  |  | ||||||
|         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) => | ||||||
|                 const children = sanitizeHTML(BrandedHTMLPolicy, link.name); |                 link.href | ||||||
|  |                     ? purify(html`<li><a href="${link.href}">${link.name}</a></li>`) | ||||||
|                 if (link.href) { |                     : html`<li><span>${link.name}</span></li>`, | ||||||
|                     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,16 +1,15 @@ | |||||||
| /// <reference types="@hcaptcha/types"/> | ///<reference types="@hcaptcha/types"/> | ||||||
| /// <reference types="turnstile-types"/> | import { renderStatic } from "@goauthentik/common/purify"; | ||||||
| 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"; | ||||||
| @ -57,14 +56,18 @@ 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} | const iframeTemplate = (captchaElement: TemplateResult, challengeUrl: string) => | ||||||
|  |     html`<!doctype html> | ||||||
|  |         <head> | ||||||
|  |             <html> | ||||||
|  |                 <body style="display:flex;flex-direction:row;justify-content:center;"> | ||||||
|  |                     ${captchaElement} | ||||||
|                     <script> |                     <script> | ||||||
|                         new ResizeObserver((entries) => { |                         new ResizeObserver((entries) => { | ||||||
|                             const height = |                             const height = | ||||||
|                                 document.body.offsetHeight + |                                 document.body.offsetHeight + | ||||||
|                                 parseFloat(getComputedStyle(document.body).fontSize) * 2; |                                 parseFloat(getComputedStyle(document.body).fontSize) * 2; | ||||||
|  |  | ||||||
|                             window.parent.postMessage({ |                             window.parent.postMessage({ | ||||||
|                                 message: "resize", |                                 message: "resize", | ||||||
|                                 source: "goauthentik.io", |                                 source: "goauthentik.io", | ||||||
| @ -73,20 +76,20 @@ function iframeTemplate(children: TemplateResult, challengeURL: string): Templat | |||||||
|                             }); |                             }); | ||||||
|                         }).observe(document.querySelector(".ak-captcha-container")); |                         }).observe(document.querySelector(".ak-captcha-container")); | ||||||
|                     </script> |                     </script> | ||||||
|  |                     <script src=${challengeUrl}></script> | ||||||
|         <script src=${challengeURL}></script> |  | ||||||
|  |  | ||||||
|                     <script> |                     <script> | ||||||
|                         function callback(token) { |                         function callback(token) { | ||||||
|                             window.parent.postMessage({ |                             window.parent.postMessage({ | ||||||
|                                 message: "captcha", |                                 message: "captcha", | ||||||
|                                 source: "goauthentik.io", |                                 source: "goauthentik.io", | ||||||
|                                 context: "flow-executor", |                                 context: "flow-executor", | ||||||
|                     token, |                                 token: token, | ||||||
|                             }); |                             }); | ||||||
|                         } |                         } | ||||||
|         </script>`; |                     </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> { | ||||||
| @ -302,25 +305,11 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async renderFrame(captchaElement: TemplateResult) { |     async renderFrame(captchaElement: TemplateResult) { | ||||||
|         const { contentDocument } = this.captchaFrame || {}; |         this.captchaFrame.contentWindow?.document.open(); | ||||||
|  |         this.captchaFrame.contentWindow?.document.write( | ||||||
|         if (!contentDocument) { |             await renderStatic(iframeTemplate(captchaElement, this.challenge.jsUrl)), | ||||||
|             console.debug( |  | ||||||
|                 "authentik/stages/captcha: unable to render captcha frame, no contentDocument", |  | ||||||
|         ); |         ); | ||||||
|  |         this.captchaFrame.contentWindow?.document.close(); | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         contentDocument.open(); |  | ||||||
|  |  | ||||||
|         contentDocument.write( |  | ||||||
|             createIFrameHTMLWrapper( |  | ||||||
|                 renderStaticHTMLUnsafe(iframeTemplate(captchaElement, this.challenge.jsUrl)), |  | ||||||
|             ), |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         contentDocument.close(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderBody() { |     renderBody() { | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ import "rapidoc"; | |||||||
|  |  | ||||||
| import { CSRFHeaderName } from "@goauthentik/common/api/config"; | import { CSRFHeaderName } from "@goauthentik/common/api/config"; | ||||||
| import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants"; | import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants"; | ||||||
|  | import { globalAK } from "@goauthentik/common/global"; | ||||||
| import { first, getCookie } from "@goauthentik/common/utils"; | import { first, getCookie } from "@goauthentik/common/utils"; | ||||||
| import { Interface } from "@goauthentik/elements/Interface"; | import { Interface } from "@goauthentik/elements/Interface"; | ||||||
| import "@goauthentik/elements/ak-locale-context"; | import "@goauthentik/elements/ak-locale-context"; | ||||||
| @ -61,6 +62,10 @@ export class APIBrowser extends Interface { | |||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async getTheme(): Promise<UiThemeEnum> { | ||||||
|  |         return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
|         return html` |         return html` | ||||||
|             <ak-locale-context> |             <ak-locale-context> | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | import { globalAK } from "@goauthentik/common/global"; | ||||||
| import { Interface } from "@goauthentik/elements/Interface"; | import { Interface } from "@goauthentik/elements/Interface"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| @ -9,6 +10,8 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css"; | |||||||
| import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css"; | import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css"; | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
|  | import { UiThemeEnum } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| @customElement("ak-loading") | @customElement("ak-loading") | ||||||
| export class Loading extends Interface { | export class Loading extends Interface { | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
| @ -25,7 +28,7 @@ export class Loading extends Interface { | |||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     registerContexts(): void { |     _initContexts(): void { | ||||||
|         // Stub function to avoid making API requests for things we don't need. The `Interface` base class loads |         // Stub function to avoid making API requests for things we don't need. The `Interface` base class loads | ||||||
|         // a bunch of data that is used globally by various things, however this is an interface that is shown |         // a bunch of data that is used globally by various things, however this is an interface that is shown | ||||||
|         // very briefly and we don't need any of that data. |         // very briefly and we don't need any of that data. | ||||||
| @ -35,6 +38,10 @@ export class Loading extends Interface { | |||||||
|         // Stub function to avoid fetching custom CSS. |         // Stub function to avoid fetching custom CSS. | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async getTheme(): Promise<UiThemeEnum> { | ||||||
|  |         return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
|         return html` <section |         return html` <section | ||||||
|             class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl" |             class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl" | ||||||
|  | |||||||
| @ -1,9 +1,18 @@ | |||||||
| import { FlowExecutor } from "@goauthentik/flow/FlowExecutor"; | import { FlowExecutor } from "@goauthentik/flow/FlowExecutor"; | ||||||
|  |  | ||||||
| import { customElement } from "lit/decorators.js"; | import { customElement, property } from "lit/decorators.js"; | ||||||
|  |  | ||||||
|  | import { UiThemeEnum } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| @customElement("ak-storybook-interface-flow") | @customElement("ak-storybook-interface-flow") | ||||||
| export class StoryFlowInterface extends FlowExecutor {} | export class StoryFlowInterface extends FlowExecutor { | ||||||
|  |     @property() | ||||||
|  |     storyTheme: UiThemeEnum = UiThemeEnum.Dark; | ||||||
|  |  | ||||||
|  |     async getTheme(): Promise<UiThemeEnum> { | ||||||
|  |         return this.storyTheme; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|     interface HTMLElementTagNameMap { |     interface HTMLElementTagNameMap { | ||||||
|  | |||||||
| @ -1,9 +1,18 @@ | |||||||
| import { Interface } from "@goauthentik/elements/Interface"; | import { Interface } from "@goauthentik/elements/Interface"; | ||||||
|  |  | ||||||
| import { customElement } from "lit/decorators.js"; | import { customElement, property } from "lit/decorators.js"; | ||||||
|  |  | ||||||
|  | import { UiThemeEnum } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| @customElement("ak-storybook-interface") | @customElement("ak-storybook-interface") | ||||||
| export class StoryInterface extends Interface {} | export class StoryInterface extends Interface { | ||||||
|  |     @property() | ||||||
|  |     storyTheme: UiThemeEnum = UiThemeEnum.Dark; | ||||||
|  |  | ||||||
|  |     async getTheme(): Promise<UiThemeEnum> { | ||||||
|  |         return this.storyTheme; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|     interface HTMLElementTagNameMap { |     interface HTMLElementTagNameMap { | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ import { | |||||||
| } from "@goauthentik/common/constants"; | } from "@goauthentik/common/constants"; | ||||||
| import { globalAK } from "@goauthentik/common/global"; | import { globalAK } from "@goauthentik/common/global"; | ||||||
| import { configureSentry } from "@goauthentik/common/sentry"; | import { configureSentry } from "@goauthentik/common/sentry"; | ||||||
| import { UIConfig, getConfigForUser } from "@goauthentik/common/ui/config"; | import { UIConfig } from "@goauthentik/common/ui/config"; | ||||||
| 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 "@goauthentik/components/ak-nav-buttons"; | import "@goauthentik/components/ak-nav-buttons"; | ||||||
| @ -292,7 +292,6 @@ export class UserInterface extends AuthenticatedInterface { | |||||||
|  |  | ||||||
|     async connectedCallback() { |     async connectedCallback() { | ||||||
|         super.connectedCallback(); |         super.connectedCallback(); | ||||||
|  |  | ||||||
|         window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, this.toggleNotificationDrawer); |         window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, this.toggleNotificationDrawer); | ||||||
|         window.addEventListener(EVENT_API_DRAWER_TOGGLE, this.toggleApiDrawer); |         window.addEventListener(EVENT_API_DRAWER_TOGGLE, this.toggleApiDrawer); | ||||||
|         window.addEventListener(EVENT_WS_MESSAGE, this.fetchConfigurationDetails); |         window.addEventListener(EVENT_WS_MESSAGE, this.fetchConfigurationDetails); | ||||||
| @ -302,7 +301,6 @@ export class UserInterface extends AuthenticatedInterface { | |||||||
|         window.removeEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, this.toggleNotificationDrawer); |         window.removeEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, this.toggleNotificationDrawer); | ||||||
|         window.removeEventListener(EVENT_API_DRAWER_TOGGLE, this.toggleApiDrawer); |         window.removeEventListener(EVENT_API_DRAWER_TOGGLE, this.toggleApiDrawer); | ||||||
|         window.removeEventListener(EVENT_WS_MESSAGE, this.fetchConfigurationDetails); |         window.removeEventListener(EVENT_WS_MESSAGE, this.fetchConfigurationDetails); | ||||||
|  |  | ||||||
|         super.disconnectedCallback(); |         super.disconnectedCallback(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @ -321,10 +319,8 @@ export class UserInterface extends AuthenticatedInterface { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     fetchConfigurationDetails() { |     fetchConfigurationDetails() { | ||||||
|         me().then((session: SessionUser) => { |         me().then((me: SessionUser) => { | ||||||
|             this.me = session; |             this.me = me; | ||||||
|             this.uiConfig = getConfigForUser(session.user); |  | ||||||
|  |  | ||||||
|             new EventsApi(DEFAULT_CONFIG) |             new EventsApi(DEFAULT_CONFIG) | ||||||
|                 .eventsNotificationsList({ |                 .eventsNotificationsList({ | ||||||
|                     seen: false, |                     seen: false, | ||||||
| @ -338,16 +334,12 @@ export class UserInterface extends AuthenticatedInterface { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     render() { |     get isFullyConfigured() { | ||||||
|         if (!this.me) { |         return Boolean(this.uiConfig && this.me); | ||||||
|             console.debug(`authentik/user/UserInterface: waiting for user session to be available`); |  | ||||||
|  |  | ||||||
|             return nothing; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|         if (!this.uiConfig) { |     render() { | ||||||
|             console.debug(`authentik/user/UserInterface: waiting for UI config to be available`); |         if (!this.isFullyConfigured) { | ||||||
|  |  | ||||||
|             return nothing; |             return nothing; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user