From 01d4bb89187f7d435953f8b31b255745d28dea71 Mon Sep 17 00:00:00 2001
From: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Date: Wed, 17 Jun 2026 03:35:28 +0200
Subject: [PATCH] web: Fix server-side message race condition, type mismatch.
---
authentik/core/templates/base/header_js.html | 24 ++++++++----------
web/src/common/objects.ts | 20 +++++++++++++++
web/src/elements/messages/MessageContainer.ts | 25 +++++++++++++++++++
web/src/styles/authentik/base/globals.css | 18 +++++++++++++
4 files changed, 73 insertions(+), 14 deletions(-)
diff --git a/authentik/core/templates/base/header_js.html b/authentik/core/templates/base/header_js.html
index 39dc374cf0..13d4eaafb2 100644
--- a/authentik/core/templates/base/header_js.html
+++ b/authentik/core/templates/base/header_js.html
@@ -16,18 +16,14 @@
relBase: "{{ base_url_rel }}",
},
};
- window.addEventListener("DOMContentLoaded", function () {
- {% for message in messages %}
- window.dispatchEvent(
- new CustomEvent("ak-message", {
- bubbles: true,
- composed: true,
- detail: {
- level: "{{ message.tags|escapejs }}",
- message: "{{ message.message|escapejs }}",
- },
- }),
- );
- {% endfor %}
- });
+
+
+
diff --git a/web/src/common/objects.ts b/web/src/common/objects.ts
index 8d826011c5..279e7b9ae0 100644
--- a/web/src/common/objects.ts
+++ b/web/src/common/objects.ts
@@ -57,3 +57,23 @@ export function trimMany(target: T, ...keys
return output as Pick;
}
+
+/**
+ * Try to parse a JSON string, returning a fallback value if parsing fails or if the input is not a string.
+ *
+ * @param input The input to parse.
+ * @param fallback The fallback value to return if parsing fails or if the input is not a string. Defaults to `null`.
+ *
+ * @returns The parsed value, or the fallback value if parsing fails or if the input is not a string.
+ */
+export function tryParsingJSON(input: unknown, fallback?: undefined): T | undefined;
+export function tryParsingJSON(input: unknown, fallback: null): T | null;
+export function tryParsingJSON(input: unknown, fallback?: F): T | F {
+ if (typeof input !== "string") return (fallback ?? null) as F;
+
+ try {
+ return JSON.parse(input);
+ } catch {
+ return (fallback ?? null) as F;
+ }
+}
diff --git a/web/src/elements/messages/MessageContainer.ts b/web/src/elements/messages/MessageContainer.ts
index 06ef03d7b1..48e174cebd 100644
--- a/web/src/elements/messages/MessageContainer.ts
+++ b/web/src/elements/messages/MessageContainer.ts
@@ -2,6 +2,7 @@ import "#elements/messages/Message";
import { parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
import { APIMessage, MessageLevel } from "#common/messages";
+import { tryParsingJSON } from "#common/objects";
import { AKElement } from "#elements/Base";
import Styles from "#elements/messages/styles.css";
@@ -106,6 +107,8 @@ export type MessageContainerAlignment = "top-left" | "top-right" | "bottom-left"
@customElement("ak-message-container")
export class MessageContainer extends AKElement {
+ public static readonly serializedSelector = "script[data-id=authentik-messages]";
+
@property({ attribute: false })
public messages: APIMessage[] = [];
@@ -129,8 +132,30 @@ export class MessageContainer extends AKElement {
super.connectedCallback();
this.popover = "manual";
+
+ requestAnimationFrame(this.drainMessages);
}
+ protected drainMessages = (): void => {
+ const selector = (this.constructor as typeof MessageContainer).serializedSelector;
+ const container = this.ownerDocument.querySelector(selector);
+
+ if (!container) {
+ logger.warn(`Expected to find a script tag with ${selector}, but none was found.`);
+ return;
+ }
+
+ const messages = tryParsingJSON(container.textContent);
+
+ if (!messages?.length) {
+ return;
+ }
+
+ for (const message of messages) {
+ this.addMessage(message);
+ }
+ };
+
public updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
diff --git a/web/src/styles/authentik/base/globals.css b/web/src/styles/authentik/base/globals.css
index 204766507f..b6afc6f886 100644
--- a/web/src/styles/authentik/base/globals.css
+++ b/web/src/styles/authentik/base/globals.css
@@ -36,6 +36,24 @@ body {
overflow: hidden;
}
+.ak-c-safe-mode {
+ position: fixed;
+ z-index: 9999999;
+ inset-block-end: 0;
+ inset-inline-end: 0;
+ pointer-events: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: var(--pf-global--FontSize--sm, 0.75rem);
+ font-weight: bold;
+ color: var(--pf-global--Color--100, CanvasText);
+ background-color: var(--pf-global--BackgroundColor--200, Canvas);
+ opacity: 0.75;
+ padding: var(--pf-global--spacer--sm, 0.5rem);
+ border-start-start-radius: 8px;
+}
+
/* #endregion */
html > form > input {