fix: use react router middleware (#3351)

This commit is contained in:
Lucas Smith
2026-09-09 10:51:26 +10:00
committed by GitHub
parent 6a8bb4be04
commit 5e8a434141
9 changed files with 53 additions and 35 deletions
+4 -3
View File
@@ -7,10 +7,11 @@ import { createReadableStreamFromReadable } from '@react-router/node';
import { isbot } from 'isbot';
import type { RenderToPipeableStreamOptions } from 'react-dom/server';
import { renderToPipeableStream } from 'react-dom/server';
import type { AppLoadContext, EntryContext } from 'react-router';
import type { EntryContext, RouterContextProvider } from 'react-router';
import { ServerRouter } from 'react-router';
import { langCookie } from './storage/lang-cookie.server';
import { nonceContext } from './utils/nonce';
export const streamTimeout = 5_000;
@@ -19,7 +20,7 @@ export default async function handleRequest(
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
loadContext: AppLoadContext,
loadContext: RouterContextProvider,
) {
let language = await langCookie.parse(request.headers.get('cookie') ?? '');
@@ -33,7 +34,7 @@ export default async function handleRequest(
// scripts it injects (route manifest, hydration data, module preloads).
// The same nonce is also exposed to the React tree via the root loader so
// our own inline scripts/styles can carry it.
const nonce = loadContext.nonce || undefined;
const nonce = loadContext.get(nonceContext) || undefined;
return new Promise((resolve, reject) => {
let shellRendered = false;
+13
View File
@@ -0,0 +1,13 @@
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { isAdmin } from '@documenso/lib/utils/is-admin';
import { type MiddlewareFunction, redirect } from 'react-router';
export const adminMiddleware: MiddlewareFunction = async ({ request }, next) => {
const { user } = await getOptionalSession(request);
if (!user || !isAdmin(user)) {
throw redirect('/');
}
return next();
};
+8
View File
@@ -0,0 +1,8 @@
import type { MiddlewareFunction } from 'react-router';
import { getRequestNonce } from '../../server/load-context';
import { nonceContext } from '../utils/nonce';
export const nonceMiddleware: MiddlewareFunction = ({ context }) => {
context.set(nonceContext, getRequestNonce());
};
+5 -2
View File
@@ -23,13 +23,16 @@ import {
useMatches,
} from 'react-router';
import { PreventFlashOnWrongTheme, ThemeProvider, useTheme } from 'remix-themes';
import { nonceMiddleware } from '~/middleware/nonce';
import type { Route } from './+types/root';
import stylesheet from './app.css?url';
import { GenericErrorLayout } from './components/general/generic-error-layout';
import { langCookie } from './storage/lang-cookie.server';
import { themeSessionResolver } from './storage/theme-session.server';
import { appMetaTags } from './utils/meta';
import { nonce } from './utils/nonce';
import { nonce, nonceContext } from './utils/nonce';
export const middleware = [nonceMiddleware];
export const links: Route.LinksFunction = () => [{ rel: 'stylesheet', href: stylesheet }];
@@ -74,7 +77,7 @@ export async function loader({ context, request }: Route.LoaderArgs) {
// Surface the per-request CSP nonce produced by `securityHeadersMiddleware` so all
// SSR-rendered <script>/<style> elements in this layout (and child
// routes that need it) can carry the matching nonce attribute.
nonce: context.nonce,
nonce: context.get(nonceContext),
session: session.isAuthenticated
? {
user: session.user,
@@ -1,4 +1,4 @@
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { LicenseClient } from '@documenso/lib/server-only/license/license-client';
import { isAdmin } from '@documenso/lib/utils/is-admin';
import { cn } from '@documenso/ui/lib/utils';
@@ -20,16 +20,18 @@ import {
import { Link, Outlet, redirect, useLocation } from 'react-router';
import { AdminLicenseStatusBanner } from '~/components/general/admin-license-status-banner';
import { adminMiddleware } from '~/middleware/admin';
import { appMetaTags } from '~/utils/meta';
import type { Route } from './+types/_layout';
export function meta() {
return appMetaTags(msg`Admin`);
}
export const middleware = [adminMiddleware];
export async function loader({ request }: Route.LoaderArgs) {
const { user } = await getSession(request);
const { user } = await getOptionalSession(request);
const license = await LicenseClient.getInstance()?.getCachedLicense();
+7 -1
View File
@@ -1,4 +1,10 @@
import { useRouteLoaderData } from 'react-router';
import { createContext, useRouteLoaderData } from 'react-router';
/**
* Per-request CSP nonce. Set by the root route middleware, read with
* `context.get(nonceContext)` in loaders/actions and `entry.server`.
*/
export const nonceContext = createContext<string>('');
/**
* Returns the supplied CSP nonce only when rendering on the server.
+3
View File
@@ -8,4 +8,7 @@ export default {
// kept without a trailing slash so they match exactly, and so the bare
// sub-path URL (e.g. "/ESign") still matches the basename at runtime.
basename: process.env.NEXT_PUBLIC_BASE_PATH ? process.env.NEXT_PUBLIC_BASE_PATH.replace(/\/$/, '') : '/',
future: {
v8_middleware: true,
},
} satisfies Config;
+2 -3
View File
@@ -24,12 +24,11 @@ export const appContext = async (c: Context, next: Next) => {
// These are non page paths like API.
if (!isPageRequest(request) || noSessionCookie || blacklistedPathsRegex.test(url.pathname)) {
return next();
return await next();
}
// Add context to any pages you want here.
return next();
return await next();
};
const setAppContext = (c: Context, context: AppContext) => {
+6 -23
View File
@@ -1,33 +1,16 @@
import { getContext } from 'hono/context-storage';
import type { AppLoadContext } from 'react-router';
import { RouterContextProvider } from 'react-router';
import type { HonoEnv } from './router';
import { CSP_NONCE_KEY } from './security-headers';
/**
* Augment React Router's `AppLoadContext` so loaders, actions, and
* `entry.server` can access fields by name without casts.
* Per-request CSP nonce set by `securityHeadersMiddleware`, read via
* `hono/context-storage` (enabled in `server/router.ts`).
*/
declare module 'react-router' {
interface AppLoadContext {
/**
* Per-request CSP nonce. Populated by `securityHeadersMiddleware` and surfaced here
* so it can be threaded into `<ServerRouter nonce>` and root loader
* data, which then feeds `<Scripts>`, `<Links>`, etc.
*/
nonce: string;
}
}
export const getRequestNonce = (): string => getContext<HonoEnv>().var[CSP_NONCE_KEY] ?? '';
/**
* Builds the React Router `AppLoadContext` for both dev (vite plugin) and
* production (`hono-react-router-adapter/node`).
*
* The Hono context isn't passed directly by the adapter, so we read it via
* `hono/context-storage`, which is enabled in `server/router.ts`.
* `future.v8_middleware` requires a `RouterContextProvider` instance here.
*/
export const getLoadContext = (): AppLoadContext => {
const nonce = getContext<HonoEnv>().var[CSP_NONCE_KEY] ?? '';
return { nonce };
};
export const getLoadContext = (): RouterContextProvider => new RouterContextProvider();