Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions apps/remix/app/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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') ?? '');

Expand All @@ -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;
Expand Down
13 changes: 13 additions & 0 deletions apps/remix/app/middleware/admin.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 8 additions & 0 deletions apps/remix/app/middleware/nonce.ts
Original file line number Diff line number Diff line change
@@ -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());
};
8 changes: 5 additions & 3 deletions apps/remix/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,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 }];

Expand Down Expand Up @@ -71,7 +73,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,
Expand Down
8 changes: 5 additions & 3 deletions apps/remix/app/routes/_authenticated+/admin+/_layout.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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();

Expand Down
8 changes: 7 additions & 1 deletion apps/remix/app/utils/nonce.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
3 changes: 3 additions & 0 deletions apps/remix/react-router.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ import type { Config } from '@react-router/dev/config';
export default {
appDirectory: 'app',
ssr: true,
future: {
v8_middleware: true,
},
} satisfies Config;
5 changes: 2 additions & 3 deletions apps/remix/server/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
29 changes: 6 additions & 23 deletions apps/remix/server/load-context.ts
Original file line number Diff line number Diff line change
@@ -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();
3 changes: 2 additions & 1 deletion apps/remix/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { lingui } from '@lingui/vite-plugin';
import { reactRouter } from '@react-router/dev/vite';
import autoprefixer from 'autoprefixer';
import serverAdapter from 'hono-react-router-adapter/vite';
import type { AppLoadContext } from 'react-router';
import tailwindcss from 'tailwindcss';
import { defineConfig, normalizePath } from 'vite';
import macrosPlugin from 'vite-plugin-babel-macros';
Expand Down Expand Up @@ -49,7 +50,7 @@ export default defineConfig({
entry: 'server/router.ts',
getLoadContext: async () => {
const { getLoadContext } = await import('./server/load-context');
return getLoadContext();
return getLoadContext() as unknown as AppLoadContext;
},
exclude: [
// Spread the defaults but replace the /.css$/ rule so that Bull
Expand Down
Loading