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`