diff --git a/authentik/brands/utils.py b/authentik/brands/utils.py index db6309c7ff..e5c8380198 100644 --- a/authentik/brands/utils.py +++ b/authentik/brands/utils.py @@ -43,6 +43,6 @@ def context_processor(request: HttpRequest) -> dict[str, Any]: "brand": brand, "brand_css": brand_css, "footer_links": tenant.footer_links, - "html_meta": {**get_http_meta()}, + "html_meta": get_http_meta(), "version": get_full_version(), } diff --git a/authentik/core/templates/base/skeleton.html b/authentik/core/templates/base/skeleton.html index a9df91fdb0..fc5eda491c 100644 --- a/authentik/core/templates/base/skeleton.html +++ b/authentik/core/templates/base/skeleton.html @@ -10,6 +10,7 @@ {# Darkreader breaks the site regardless of theme as its not compatible with webcomponents, and we default to a dark theme based on preferred colour-scheme #} + {% block title %}{% trans title|default:brand.branding_title %}{% endblock %} diff --git a/internal/web/static.go b/internal/web/static.go index 25cdcaaed5..4e6bdd7455 100644 --- a/internal/web/static.go +++ b/internal/web/static.go @@ -1,11 +1,9 @@ package web import ( - "fmt" "net/http" "github.com/go-http-utils/etag" - "github.com/gorilla/mux" "goauthentik.io/internal/config" "goauthentik.io/internal/constants" @@ -42,27 +40,6 @@ func (ws *WebServer) configureStatic() { config.Get().Web.Path, )) - indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/flow/{flow_slug}/assets").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - - pathStripper( - distFs, - "if/flow/"+vars["flow_slug"], - config.Get().Web.Path, - ).ServeHTTP(rw, r) - }) - indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/admin/assets").Handler(http.StripPrefix(fmt.Sprintf("%sif/admin", config.Get().Web.Path), distFs)) - indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/user/assets").Handler(http.StripPrefix(fmt.Sprintf("%sif/user", config.Get().Web.Path), distFs)) - indexLessRouter.PathPrefix(config.Get().Web.Path).PathPrefix("/if/rac/{app_slug}/assets").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - - pathStripper( - distFs, - "if/rac/"+vars["app_slug"], - config.Get().Web.Path, - ).ServeHTTP(rw, r) - }) - // Media files, if backend is file if config.Get().Storage.Media.Backend == "file" { fsMedia := http.FileServer(http.Dir(config.Get().Storage.Media.File.Path)) diff --git a/web/bundler/css-assets-plugin/node.js b/web/bundler/css-assets-plugin/node.js new file mode 100644 index 0000000000..cafaa92a1e --- /dev/null +++ b/web/bundler/css-assets-plugin/node.js @@ -0,0 +1,30 @@ +/** + * @file CSS asset rewrite plugin for ESBuild. + * + * @import { Plugin } from "esbuild" + */ + +import * as fs from "node:fs/promises"; + +/** + * Rewrite `url()` calls in CSS files to point to the static directory. + * + * @returns {Plugin} + */ +export function cssAssetPlugin() { + return { + name: "css-text-loader", + setup: (build) => { + const URLPattern = /url\(\s*['"]?(?:[./]*)(assets\/[^)'"']*)['"]?\s*\)/g; + + build.onLoad({ filter: /\.css$/ }, async (args) => { + const contents = await fs.readFile(args.path, "utf8"); + + return { + loader: "text", + contents: contents.replaceAll(URLPattern, "url(./static/dist/$1)"), + }; + }); + }, + }; +} diff --git a/web/bundler/mdx-plugin/node.js b/web/bundler/mdx-plugin/node.js index fe33b8f63e..0a0c619a94 100644 --- a/web/bundler/mdx-plugin/node.js +++ b/web/bundler/mdx-plugin/node.js @@ -1,6 +1,3 @@ -import * as fs from "node:fs/promises"; -import * as path from "node:path"; - /** * @file MDX plugin for ESBuild. * @@ -13,6 +10,10 @@ import * as path from "node:path"; * PluginBuild * } from "esbuild" */ + +import * as fs from "node:fs/promises"; +import * as path from "node:path"; + import { MonoRepoRoot } from "@goauthentik/core/paths/node"; /** diff --git a/web/scripts/build-web.mjs b/web/scripts/build-web.mjs index 6cc153094f..a0c1130792 100644 --- a/web/scripts/build-web.mjs +++ b/web/scripts/build-web.mjs @@ -1,11 +1,13 @@ -import * as fs from "node:fs/promises"; -import * as path from "node:path"; - /** * @file ESBuild script for building the authentik web UI. * * @import { BuildOptions } from "esbuild"; */ + +import * as fs from "node:fs/promises"; +import * as path from "node:path"; + +import { cssAssetPlugin } from "#bundler/css-assets-plugin/node"; import { mdxPlugin } from "#bundler/mdx-plugin/node"; import { createBundleDefinitions } from "#bundler/utils/node"; import { DistDirectory, EntryPoint, PackageRoot } from "#paths/node"; @@ -40,11 +42,8 @@ const BASE_ESBUILD_OPTIONS = { legalComments: "external", splitting: true, treeShaking: true, - external: ["*.woff", "*.woff2"], + tsconfig: path.resolve(PackageRoot, "tsconfig.build.json"), - loader: { - ".css": "text", - }, plugins: [ copy({ assets: [ @@ -75,6 +74,7 @@ const BASE_ESBUILD_OPTIONS = { path: true, }, }), + cssAssetPlugin(), mdxPlugin({ root: MonoRepoRoot, }), diff --git a/web/src/common/styles/authentik.css b/web/src/common/styles/authentik.css index a1ef240095..61f734bd12 100644 --- a/web/src/common/styles/authentik.css +++ b/web/src/common/styles/authentik.css @@ -35,6 +35,31 @@ --ak-navbar--height: 7rem; } +/* #region Fonts */ + +body { + --pf-global--FontFamily--sans-serif: + RedHatVF, "RedHatText", "Overpass", overpass, helvetica, arial, sans-serif; + + --pf-global--FontFamily--heading--sans-serif: + RedHatDisplayVF, "RedHatDisplay", "Overpass", overpass, helvetica, arial, sans-serif; + + --pf-global--FontFamily--monospace: + RedHatMonoVF, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", + monospace; +} + +code, +pre { + /* + The variable weight is a bit too thin compared to the fixed weight. + So we'll use a slightly larger value here to compensate. + */ + font-weight: 500; +} + +/* #endregion */ + .pf-c-form__group { --pf-c-form--m-horizontal__group-label--md--GridColumnWidth: minmax(max-content, 9.375rem); column-gap: var(--pf-global--spacer--md); diff --git a/web/src/elements/router/RouteMatch.ts b/web/src/elements/router/RouteMatch.ts index 07c20d4a56..9a81bfc0a2 100644 --- a/web/src/elements/router/RouteMatch.ts +++ b/web/src/elements/router/RouteMatch.ts @@ -41,6 +41,14 @@ export class RouteMatch { } } +export function createPathnameHash( + hashRoute?: string | null, + basePath = location.pathname, +): string { + if (!hashRoute) return basePath; + return `${basePath}#${hashRoute}`; +} + export function getURLParam(key: string, fallback: T): T { const params = getURLParams(); if (key in params) { @@ -64,13 +72,18 @@ export function getURLParams(): { [key: string]: unknown } { } export function setURLParams(params: { [key: string]: unknown }, replace = true): void { - const paramsString = JSON.stringify(params); - const currentUrl = window.location.hash.slice(1, Infinity).split(ROUTE_SEPARATOR)[0]; - const newUrl = `#${currentUrl};${encodeURIComponent(paramsString)}`; + const serializedParams = JSON.stringify(params); + + const [currentRoute] = window.location.hash.slice(1).split(ROUTE_SEPARATOR); + + const nextPathname = createPathnameHash( + `${currentRoute};${encodeURIComponent(serializedParams)}`, + ); + if (replace) { - history.replaceState(undefined, "", newUrl); + history.replaceState(undefined, "", nextPathname); } else { - history.pushState(undefined, "", newUrl); + history.pushState(undefined, "", nextPathname); } } @@ -79,5 +92,6 @@ export function updateURLParams(params: { [key: string]: unknown }, replace = tr for (const key in params) { currentParams[key] = params[key] as string; } + setURLParams(currentParams, replace); } diff --git a/web/src/elements/sidebar/SidebarItem.ts b/web/src/elements/sidebar/SidebarItem.ts index 4ff9e1dd20..0138786be5 100644 --- a/web/src/elements/sidebar/SidebarItem.ts +++ b/web/src/elements/sidebar/SidebarItem.ts @@ -1,6 +1,7 @@ import { ROUTE_SEPARATOR } from "#common/constants"; import { AKElement } from "#elements/Base"; +import { createPathnameHash } from "#elements/router/RouteMatch"; import { msg, str } from "@lit/localize"; import { css, CSSResult, html, nothing, TemplateResult } from "lit"; @@ -89,10 +90,7 @@ export class SidebarItem extends AKElement { public current?: boolean; @property({ type: Boolean }) - public isAbsoluteLink = false; - - @property({ type: Boolean }) - public highlight?: boolean; + public highlight = false; public parent?: SidebarItem; @@ -217,8 +215,8 @@ export class SidebarItem extends AKElement { renderWithPath() { return html`