mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-08-14 06:59:46 +00:00
Read the entrypoint configuration from data attributes
Instead of injecting configuration through inline <script> tags setting window globals, server-rendered templates now carry the configuration as data attributes on the entrypoint mount node, validated client-side with a valibot schema over element.dataset. This removes all inline scripts from the templates, which is friendlier to CSP, and gives the frontend runtime-validated, typed configuration. The DOM read and the wiring now happen in the entrypoints themselves: main.tsx parses the mount node dataset, owns the QueryClient, and injects the basepath and GraphQL endpoint into the router and request layer instead of those being module-scope singletons.
This commit is contained in:
+2
-6
@@ -1,4 +1,5 @@
|
||||
<!--
|
||||
Copyright 2025, 2026 Element Creations Ltd.
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2022-2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
@@ -14,15 +15,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>matrix-authentication-service</title>
|
||||
<script type="application/javascript">
|
||||
window.APP_CONFIG = JSON.parse(
|
||||
'{"root": "/account/", "graphqlEndpoint": "/graphql"}',
|
||||
);
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="root" data-root="/account/" data-graphql-endpoint="/graphql"></div>
|
||||
<script type="module" src="/src/entrypoints/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
type AppConfig = {
|
||||
root: string;
|
||||
graphqlEndpoint: string;
|
||||
};
|
||||
|
||||
interface IWindow {
|
||||
APP_CONFIG?: AppConfig;
|
||||
}
|
||||
|
||||
const config: AppConfig = (typeof window !== "undefined" &&
|
||||
(window as IWindow).APP_CONFIG) || {
|
||||
root: "/",
|
||||
graphqlEndpoint: "/graphql",
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,33 +1,55 @@
|
||||
// Copyright 2025, 2026 Element Creations Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { RouterProvider } from "@tanstack/react-router";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { StrictMode, Suspense } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import * as v from "valibot";
|
||||
import ErrorBoundary from "../components/ErrorBoundary";
|
||||
import LoadingScreen from "../components/LoadingScreen";
|
||||
import { queryClient } from "../graphql";
|
||||
import { setGraphqlEndpoint } from "../graphql";
|
||||
import i18n, { setupI18n } from "../i18n";
|
||||
import { router } from "../router";
|
||||
import { makeRouter } from "../router";
|
||||
import "./vendor.css";
|
||||
import "./shared.css";
|
||||
|
||||
setupI18n();
|
||||
|
||||
createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
const configSchema = v.object({
|
||||
root: v.optional(v.string(), "/"),
|
||||
graphqlEndpoint: v.optional(v.string(), "/graphql"),
|
||||
});
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
mutations: {
|
||||
throwOnError: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
if (!rootElement) throw new Error("#root element not found");
|
||||
|
||||
const config = v.parse(configSchema, rootElement.dataset);
|
||||
setGraphqlEndpoint(config.graphqlEndpoint);
|
||||
const router = makeRouter(config.root, queryClient);
|
||||
|
||||
createRoot(rootElement).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ErrorBoundary>
|
||||
<TooltipProvider>
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<RouterProvider router={router} context={{ queryClient }} />
|
||||
<RouterProvider router={router} />
|
||||
</I18nextProvider>
|
||||
</Suspense>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Copyright 2025, 2026 Element Creations Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
@@ -6,21 +7,18 @@
|
||||
|
||||
import { SwaggerUIBundle } from "swagger-ui-dist";
|
||||
import "swagger-ui-dist/swagger-ui.css";
|
||||
|
||||
type ApiConfig = {
|
||||
openapiUrl: string;
|
||||
callbackUrl: string;
|
||||
};
|
||||
import * as v from "valibot";
|
||||
|
||||
interface IWindow {
|
||||
API_CONFIG?: ApiConfig;
|
||||
ui?: SwaggerUIBundle;
|
||||
}
|
||||
|
||||
const config = typeof window !== "undefined" && (window as IWindow).API_CONFIG;
|
||||
if (!config) {
|
||||
throw new Error("API_CONFIG is not defined");
|
||||
}
|
||||
const el = document.getElementById("swagger-ui");
|
||||
if (!el) throw new Error("swagger-ui element not found");
|
||||
const config = v.parse(
|
||||
v.object({ openapiUrl: v.string(), callbackUrl: v.string() }),
|
||||
el.dataset,
|
||||
);
|
||||
|
||||
(window as IWindow).ui = SwaggerUIBundle({
|
||||
url: config.openapiUrl,
|
||||
|
||||
+19
-26
@@ -1,26 +1,22 @@
|
||||
// Copyright 2025, 2026 Element Creations Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import type { ExecutionResult } from "graphql";
|
||||
import appConfig from "./config";
|
||||
import type { TypedDocumentString } from "./gql/graphql";
|
||||
|
||||
let graphqlEndpoint: string;
|
||||
if (import.meta.env.TEST && typeof window === "undefined") {
|
||||
graphqlEndpoint = new URL(
|
||||
appConfig.graphqlEndpoint,
|
||||
"http:://localhost/",
|
||||
).toString();
|
||||
} else {
|
||||
graphqlEndpoint = new URL(
|
||||
appConfig.graphqlEndpoint,
|
||||
window.location.toString(),
|
||||
).toString();
|
||||
}
|
||||
let graphqlEndpoint = "/graphql";
|
||||
|
||||
/**
|
||||
* Sets the GraphQL endpoint to use for requests.
|
||||
* This is called during initialization, once config has been loaded.
|
||||
*/
|
||||
export const setGraphqlEndpoint = (endpoint: string): void => {
|
||||
graphqlEndpoint = endpoint;
|
||||
};
|
||||
|
||||
type RequestOptions<TData, TVariables> = {
|
||||
query: TypedDocumentString<TData, TVariables>;
|
||||
@@ -35,9 +31,14 @@ export const graphqlRequest = async <TData, TVariables>({
|
||||
variables,
|
||||
signal,
|
||||
}: RequestOptions<TData, TVariables>): Promise<TData> => {
|
||||
const endpoint =
|
||||
import.meta.env.TEST && typeof window === "undefined"
|
||||
? new URL(graphqlEndpoint, "http://localhost/").toString()
|
||||
: new URL(graphqlEndpoint, window.location.toString()).toString();
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(graphqlEndpoint, {
|
||||
response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -49,14 +50,14 @@ export const graphqlRequest = async <TData, TVariables>({
|
||||
signal,
|
||||
});
|
||||
} catch (cause) {
|
||||
throw new Error(`GraphQL request to ${graphqlEndpoint} request failed`, {
|
||||
throw new Error(`GraphQL request to ${endpoint} request failed`, {
|
||||
cause,
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`GraphQL request to ${graphqlEndpoint} failed: ${response.status}`,
|
||||
`GraphQL request to ${endpoint} failed: ${response.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,16 +67,8 @@ export const graphqlRequest = async <TData, TVariables>({
|
||||
}
|
||||
|
||||
if (!json.data) {
|
||||
throw new Error(`GraphQL request to ${graphqlEndpoint} returned no data`);
|
||||
throw new Error(`GraphQL request to ${endpoint} returned no data`);
|
||||
}
|
||||
|
||||
return json.data;
|
||||
};
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
mutations: {
|
||||
throwOnError: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
+12
-12
@@ -1,28 +1,28 @@
|
||||
// Copyright 2025, 2026 Element Creations Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { createRouter } from "@tanstack/react-router";
|
||||
import LoadingScreen from "./components/LoadingScreen";
|
||||
import config from "./config";
|
||||
import { queryClient } from "./graphql";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
|
||||
// Create a new router instance
|
||||
export const router = createRouter({
|
||||
routeTree,
|
||||
scrollRestoration: true,
|
||||
basepath: config.root,
|
||||
defaultPendingComponent: LoadingScreen,
|
||||
defaultPreload: "intent",
|
||||
context: { queryClient },
|
||||
});
|
||||
export const makeRouter = (basepath: string, queryClient: QueryClient) =>
|
||||
createRouter({
|
||||
routeTree,
|
||||
scrollRestoration: true,
|
||||
basepath,
|
||||
defaultPendingComponent: LoadingScreen,
|
||||
defaultPreload: "intent",
|
||||
context: { queryClient },
|
||||
});
|
||||
|
||||
// Register the router instance for type safety
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
router: ReturnType<typeof makeRouter>;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-8
@@ -1,4 +1,5 @@
|
||||
{#
|
||||
Copyright 2025, 2026 Element Creations Ltd.
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
@@ -15,19 +16,12 @@ Please see LICENSE files in the repository root for full details.
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{ _("app.name") }}</title>
|
||||
{% set config = {
|
||||
'graphqlEndpoint': app_config.graphqlEndpoint,
|
||||
'root': app_config.root,
|
||||
} -%}
|
||||
<script>
|
||||
window.APP_CONFIG = JSON.parse("{{ config | tojson | add_slashes | safe }}");
|
||||
</script>
|
||||
{{ include_asset('src/entrypoints/main.tsx') | indent(4) | safe }}
|
||||
{# Pre-load the locale data for the current language #}
|
||||
{{ include_asset('locales/' ~ lang ~ '.json') | indent(4) | safe }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="root" data-root="{{ app_config.root }}" data-graphql-endpoint="{{ app_config.graphqlEndpoint }}"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{#
|
||||
Copyright 2025, 2026 Element Creations Ltd.
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
@@ -12,16 +13,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>API documentation</title>
|
||||
<script>
|
||||
window.API_CONFIG = {
|
||||
openapiUrl: "{{ openapi_url | add_slashes | safe }}",
|
||||
callbackUrl: "{{ callback_url | add_slashes | safe }}",
|
||||
};
|
||||
</script>
|
||||
{{ include_asset('src/entrypoints/swagger.ts') | indent(4) | safe }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<div id="swagger-ui" data-openapi-url="{{ openapi_url }}" data-callback-url="{{ callback_url }}"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
},
|
||||
"name": "matrix-authentication-service",
|
||||
"@name": {
|
||||
"context": "app.html:17:14-27, base.html:24:31-44",
|
||||
"context": "app.html:18:14-27, base.html:24:31-44",
|
||||
"description": "Name of the application"
|
||||
},
|
||||
"technical_description": "OpenID Connect discovery document: <a class=\"cpd-link\" data-kind=\"primary\" href=\"%(discovery_url)s\">%(discovery_url)s</a>",
|
||||
|
||||
Reference in New Issue
Block a user