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
15 changes: 13 additions & 2 deletions src/server/file-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ export interface FileRouteServerOptions {
basePath?: string;
/** Middleware applied to generated action routes (e.g. `csrf()`). */
middlewares?: ServerMiddleware[];
/** Middleware applied to generated loader routes. */
/**
* Middleware applied to generated loader (JSON) routes. Defaults to
* {@link middlewares} when unset — a loader mirrors the same data an
* authenticated page renders, so protecting only actions with
* `middlewares: [auth]` would otherwise leave every route's `load()` output
* exposed as unauthenticated JSON. Pass `dataMiddlewares: []` to explicitly
* opt the loader routes out of the action middleware chain.
*/
dataMiddlewares?: ServerMiddleware[];
}

Expand Down Expand Up @@ -111,6 +118,10 @@ export const createFileRouteServerRoutes = (
): ServerRoute[] => {
const routes: ServerRoute[] = [];
const actionMethod = options.actionMethod ?? 'POST';
// Default loader middleware to the action chain so `load()` output is not
// accidentally exposed as unauthenticated JSON (opt out with an explicit
// empty array).
const dataMiddlewares = options.dataMiddlewares ?? options.middlewares;

for (const route of entries) {
if (route.hasAction !== false) {
Expand All @@ -126,7 +137,7 @@ export const createFileRouteServerRoutes = (
routes.push({
path: joinPath(options.dataPath, joinPath(options.basePath, route.pattern)),
method: 'GET',
middlewares: options.dataMiddlewares,
middlewares: dataMiddlewares,
handler: loaderHandler(route),
});
}
Expand Down
38 changes: 38 additions & 0 deletions tests/server-file-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,42 @@ describe('createFileRouteServerRoutes', () => {
const routes = createFileRouteServerRoutes(entries, { basePath: '/app' });
expect(routes[0].path).toBe('/app/users/:id');
});

it('defaults loader middleware to the action middleware chain (#181)', () => {
const auth: import('../src/server/index').ServerMiddleware = (_ctx, next) => next();
const { entries } = createFileRoutes({
'routes/users/[id]/+page.ts': {
default: 'User',
action: (() => ({})) as Action,
load: (() => ({})) as Load,
},
});

const routes = createFileRouteServerRoutes(entries, {
dataPath: '/__data',
middlewares: [auth],
});
const loaderRoute = routes.find((r) => r.method === 'GET');
// The loader inherits the action middleware chain when dataMiddlewares is unset.
expect(loaderRoute?.middlewares).toEqual([auth]);
});

it('allows opting the loader out with an explicit empty dataMiddlewares (#181)', () => {
const auth: import('../src/server/index').ServerMiddleware = (_ctx, next) => next();
const { entries } = createFileRoutes({
'routes/users/[id]/+page.ts': {
default: 'User',
action: (() => ({})) as Action,
load: (() => ({})) as Load,
},
});

const routes = createFileRouteServerRoutes(entries, {
dataPath: '/__data',
middlewares: [auth],
dataMiddlewares: [],
});
const loaderRoute = routes.find((r) => r.method === 'GET');
expect(loaderRoute?.middlewares).toEqual([]);
});
});