Skip to content

Commit f5c2bf3

Browse files
committed
WIP
1 parent 12facbe commit f5c2bf3

File tree

3 files changed

+207
-169
lines changed

3 files changed

+207
-169
lines changed

src/hooks.server.ts

Lines changed: 173 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { adminTokenManager } from "$lib/server/adminToken";
2020
import { isHostLocalhost } from "$lib/server/isURLLocal";
2121
import { MetricsServer } from "$lib/server/metrics";
2222
import { loadMcpServersOnStartup } from "$lib/server/mcp/registry";
23-
import { runWithRequestContext } from "$lib/server/requestContext";
23+
import { runWithRequestContext, updateRequestContext } from "$lib/server/requestContext";
2424

2525
export const init: ServerInit = async () => {
2626
// Wait for config to be fully loaded
@@ -105,196 +105,207 @@ export const handle: Handle = async ({ event, resolve }) => {
105105
const requestId = crypto.randomUUID();
106106

107107
// Run the entire request handling within the request context
108-
return runWithRequestContext(async () => {
109-
await ready.then(() => {
110-
config.checkForUpdates();
111-
});
112-
113-
logger.debug({
114-
locals: event.locals,
115-
url: event.url.pathname,
116-
params: event.params,
117-
request: event.request,
118-
});
119-
120-
function errorResponse(status: number, message: string) {
121-
const sendJson =
122-
event.request.headers.get("accept")?.includes("application/json") ||
123-
event.request.headers.get("content-type")?.includes("application/json");
124-
return new Response(sendJson ? JSON.stringify({ error: message }) : message, {
125-
status,
126-
headers: {
127-
"content-type": sendJson ? "application/json" : "text/plain",
128-
},
108+
return runWithRequestContext(
109+
async () => {
110+
await ready.then(() => {
111+
config.checkForUpdates();
129112
});
130-
}
131113

132-
if (event.url.pathname.startsWith(`${base}/admin/`) || event.url.pathname === `${base}/admin`) {
133-
const ADMIN_SECRET = config.ADMIN_API_SECRET || config.PARQUET_EXPORT_SECRET;
134-
135-
if (!ADMIN_SECRET) {
136-
return errorResponse(500, "Admin API is not configured");
137-
}
114+
logger.debug({
115+
locals: event.locals,
116+
url: event.url.pathname,
117+
params: event.params,
118+
request: event.request,
119+
});
138120

139-
if (event.request.headers.get("Authorization") !== `Bearer ${ADMIN_SECRET}`) {
140-
return errorResponse(401, "Unauthorized");
121+
function errorResponse(status: number, message: string) {
122+
const sendJson =
123+
event.request.headers.get("accept")?.includes("application/json") ||
124+
event.request.headers.get("content-type")?.includes("application/json");
125+
return new Response(sendJson ? JSON.stringify({ error: message }) : message, {
126+
status,
127+
headers: {
128+
"content-type": sendJson ? "application/json" : "text/plain",
129+
},
130+
});
141131
}
142-
}
143-
144-
const auth = await authenticateRequest(
145-
{ type: "svelte", value: event.request.headers },
146-
{ type: "svelte", value: event.cookies },
147-
event.url
148-
);
149132

150-
event.locals.sessionId = auth.sessionId;
133+
if (
134+
event.url.pathname.startsWith(`${base}/admin/`) ||
135+
event.url.pathname === `${base}/admin`
136+
) {
137+
const ADMIN_SECRET = config.ADMIN_API_SECRET || config.PARQUET_EXPORT_SECRET;
151138

152-
if (loginEnabled && !auth.user && !event.url.pathname.startsWith(`${base}/.well-known/`)) {
153-
if (config.AUTOMATIC_LOGIN === "true") {
154-
// AUTOMATIC_LOGIN: always redirect to OAuth flow (unless already on login or healthcheck pages)
155-
if (
156-
!event.url.pathname.startsWith(`${base}/login`) &&
157-
!event.url.pathname.startsWith(`${base}/healthcheck`)
158-
) {
159-
// To get the same CSRF token after callback
160-
refreshSessionCookie(event.cookies, auth.secretSessionId);
161-
return await triggerOauthFlow(event);
139+
if (!ADMIN_SECRET) {
140+
return errorResponse(500, "Admin API is not configured");
162141
}
163-
} else {
164-
// Redirect to OAuth flow unless on the authorized pages (home, shared conversation, login, healthcheck, model thumbnails)
165-
if (
166-
event.url.pathname !== `${base}/` &&
167-
event.url.pathname !== `${base}` &&
168-
!event.url.pathname.startsWith(`${base}/login`) &&
169-
!event.url.pathname.startsWith(`${base}/login/callback`) &&
170-
!event.url.pathname.startsWith(`${base}/healthcheck`) &&
171-
!event.url.pathname.startsWith(`${base}/r/`) &&
172-
!event.url.pathname.startsWith(`${base}/conversation/`) &&
173-
!event.url.pathname.startsWith(`${base}/models/`) &&
174-
!event.url.pathname.startsWith(`${base}/api`)
175-
) {
176-
refreshSessionCookie(event.cookies, auth.secretSessionId);
177-
return triggerOauthFlow(event);
142+
143+
if (event.request.headers.get("Authorization") !== `Bearer ${ADMIN_SECRET}`) {
144+
return errorResponse(401, "Unauthorized");
178145
}
179146
}
180-
}
181-
182-
event.locals.user = auth.user || undefined;
183-
event.locals.token = auth.token;
184147

185-
event.locals.isAdmin =
186-
event.locals.user?.isAdmin || adminTokenManager.isAdmin(event.locals.sessionId);
187-
188-
// CSRF protection
189-
const requestContentType = event.request.headers.get("content-type")?.split(";")[0] ?? "";
190-
/** https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype */
191-
const nativeFormContentTypes = [
192-
"multipart/form-data",
193-
"application/x-www-form-urlencoded",
194-
"text/plain",
195-
];
196-
197-
if (event.request.method === "POST") {
198-
if (nativeFormContentTypes.includes(requestContentType)) {
199-
const origin = event.request.headers.get("origin");
148+
const auth = await authenticateRequest(
149+
{ type: "svelte", value: event.request.headers },
150+
{ type: "svelte", value: event.cookies },
151+
event.url
152+
);
200153

201-
if (!origin) {
202-
return errorResponse(403, "Non-JSON form requests need to have an origin");
154+
event.locals.sessionId = auth.sessionId;
155+
156+
if (loginEnabled && !auth.user && !event.url.pathname.startsWith(`${base}/.well-known/`)) {
157+
if (config.AUTOMATIC_LOGIN === "true") {
158+
// AUTOMATIC_LOGIN: always redirect to OAuth flow (unless already on login or healthcheck pages)
159+
if (
160+
!event.url.pathname.startsWith(`${base}/login`) &&
161+
!event.url.pathname.startsWith(`${base}/healthcheck`)
162+
) {
163+
// To get the same CSRF token after callback
164+
refreshSessionCookie(event.cookies, auth.secretSessionId);
165+
return await triggerOauthFlow(event);
166+
}
167+
} else {
168+
// Redirect to OAuth flow unless on the authorized pages (home, shared conversation, login, healthcheck, model thumbnails)
169+
if (
170+
event.url.pathname !== `${base}/` &&
171+
event.url.pathname !== `${base}` &&
172+
!event.url.pathname.startsWith(`${base}/login`) &&
173+
!event.url.pathname.startsWith(`${base}/login/callback`) &&
174+
!event.url.pathname.startsWith(`${base}/healthcheck`) &&
175+
!event.url.pathname.startsWith(`${base}/r/`) &&
176+
!event.url.pathname.startsWith(`${base}/conversation/`) &&
177+
!event.url.pathname.startsWith(`${base}/models/`) &&
178+
!event.url.pathname.startsWith(`${base}/api`)
179+
) {
180+
refreshSessionCookie(event.cookies, auth.secretSessionId);
181+
return triggerOauthFlow(event);
182+
}
203183
}
184+
}
185+
186+
event.locals.user = auth.user || undefined;
187+
event.locals.token = auth.token;
204188

205-
const validOrigins = [
206-
new URL(event.request.url).host,
207-
...(config.PUBLIC_ORIGIN ? [new URL(config.PUBLIC_ORIGIN).host] : []),
208-
];
189+
// Update request context with user after authentication
190+
if (auth.user?.username) {
191+
updateRequestContext({ user: auth.user.username });
192+
}
209193

210-
if (!validOrigins.includes(new URL(origin).host)) {
211-
return errorResponse(403, "Invalid referer for POST request");
194+
event.locals.isAdmin =
195+
event.locals.user?.isAdmin || adminTokenManager.isAdmin(event.locals.sessionId);
196+
197+
// CSRF protection
198+
const requestContentType = event.request.headers.get("content-type")?.split(";")[0] ?? "";
199+
/** https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype */
200+
const nativeFormContentTypes = [
201+
"multipart/form-data",
202+
"application/x-www-form-urlencoded",
203+
"text/plain",
204+
];
205+
206+
if (event.request.method === "POST") {
207+
if (nativeFormContentTypes.includes(requestContentType)) {
208+
const origin = event.request.headers.get("origin");
209+
210+
if (!origin) {
211+
return errorResponse(403, "Non-JSON form requests need to have an origin");
212+
}
213+
214+
const validOrigins = [
215+
new URL(event.request.url).host,
216+
...(config.PUBLIC_ORIGIN ? [new URL(config.PUBLIC_ORIGIN).host] : []),
217+
];
218+
219+
if (!validOrigins.includes(new URL(origin).host)) {
220+
return errorResponse(403, "Invalid referer for POST request");
221+
}
212222
}
213223
}
214-
}
215224

216-
if (
217-
event.request.method === "POST" ||
218-
event.url.pathname.startsWith(`${base}/login`) ||
219-
event.url.pathname.startsWith(`${base}/login/callback`)
220-
) {
221-
// if the request is a POST request or login-related we refresh the cookie
222-
refreshSessionCookie(event.cookies, auth.secretSessionId);
223-
224-
await collections.sessions.updateOne(
225-
{ sessionId: auth.sessionId },
226-
{ $set: { updatedAt: new Date(), expiresAt: addWeeks(new Date(), 2) } }
227-
);
228-
}
225+
if (
226+
event.request.method === "POST" ||
227+
event.url.pathname.startsWith(`${base}/login`) ||
228+
event.url.pathname.startsWith(`${base}/login/callback`)
229+
) {
230+
// if the request is a POST request or login-related we refresh the cookie
231+
refreshSessionCookie(event.cookies, auth.secretSessionId);
229232

230-
if (
231-
loginEnabled &&
232-
!event.locals.user &&
233-
!event.url.pathname.startsWith(`${base}/login`) &&
234-
!event.url.pathname.startsWith(`${base}/admin`) &&
235-
!event.url.pathname.startsWith(`${base}/settings`) &&
236-
!["GET", "OPTIONS", "HEAD"].includes(event.request.method)
237-
) {
238-
return errorResponse(401, ERROR_MESSAGES.authOnly);
239-
}
233+
await collections.sessions.updateOne(
234+
{ sessionId: auth.sessionId },
235+
{ $set: { updatedAt: new Date(), expiresAt: addWeeks(new Date(), 2) } }
236+
);
237+
}
240238

241-
let replaced = false;
239+
if (
240+
loginEnabled &&
241+
!event.locals.user &&
242+
!event.url.pathname.startsWith(`${base}/login`) &&
243+
!event.url.pathname.startsWith(`${base}/admin`) &&
244+
!event.url.pathname.startsWith(`${base}/settings`) &&
245+
!["GET", "OPTIONS", "HEAD"].includes(event.request.method)
246+
) {
247+
return errorResponse(401, ERROR_MESSAGES.authOnly);
248+
}
242249

243-
const response = await resolve(event, {
244-
transformPageChunk: (chunk) => {
245-
// For some reason, Sveltekit doesn't let us load env variables from .env in the app.html template
246-
if (replaced || !chunk.html.includes("%gaId%")) {
247-
return chunk.html;
248-
}
249-
replaced = true;
250-
251-
return chunk.html.replace("%gaId%", config.PUBLIC_GOOGLE_ANALYTICS_ID);
252-
},
253-
filterSerializedResponseHeaders: (header) => {
254-
return header.includes("content-type");
255-
},
256-
});
257-
258-
// Add CSP header to disallow framing if ALLOW_IFRAME is not "true"
259-
if (config.ALLOW_IFRAME !== "true") {
260-
response.headers.append("Content-Security-Policy", "frame-ancestors 'none';");
261-
}
250+
let replaced = false;
262251

263-
if (
264-
event.url.pathname.startsWith(`${base}/login/callback`) ||
265-
event.url.pathname.startsWith(`${base}/login`)
266-
) {
267-
response.headers.append("Cache-Control", "no-store");
268-
}
252+
const response = await resolve(event, {
253+
transformPageChunk: (chunk) => {
254+
// For some reason, Sveltekit doesn't let us load env variables from .env in the app.html template
255+
if (replaced || !chunk.html.includes("%gaId%")) {
256+
return chunk.html;
257+
}
258+
replaced = true;
269259

270-
if (event.url.pathname.startsWith(`${base}/api/`)) {
271-
// get origin from the request
272-
const requestOrigin = event.request.headers.get("origin");
260+
return chunk.html.replace("%gaId%", config.PUBLIC_GOOGLE_ANALYTICS_ID);
261+
},
262+
filterSerializedResponseHeaders: (header) => {
263+
return header.includes("content-type");
264+
},
265+
});
273266

274-
// get origin from the config if its defined
275-
let allowedOrigin = config.PUBLIC_ORIGIN ? new URL(config.PUBLIC_ORIGIN).origin : undefined;
267+
// Add CSP header to disallow framing if ALLOW_IFRAME is not "true"
268+
if (config.ALLOW_IFRAME !== "true") {
269+
response.headers.append("Content-Security-Policy", "frame-ancestors 'none';");
270+
}
276271

277272
if (
278-
dev || // if we're in dev mode
279-
!requestOrigin || // or the origin is null (SSR)
280-
isHostLocalhost(new URL(requestOrigin).hostname) // or the origin is localhost
273+
event.url.pathname.startsWith(`${base}/login/callback`) ||
274+
event.url.pathname.startsWith(`${base}/login`)
281275
) {
282-
allowedOrigin = "*"; // allow all origins
283-
} else if (allowedOrigin === requestOrigin) {
284-
allowedOrigin = requestOrigin; // echo back the caller
276+
response.headers.append("Cache-Control", "no-store");
285277
}
286278

287-
if (allowedOrigin) {
288-
response.headers.set("Access-Control-Allow-Origin", allowedOrigin);
289-
response.headers.set(
290-
"Access-Control-Allow-Methods",
291-
"GET, POST, PUT, PATCH, DELETE, OPTIONS"
292-
);
293-
response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
279+
if (event.url.pathname.startsWith(`${base}/api/`)) {
280+
// get origin from the request
281+
const requestOrigin = event.request.headers.get("origin");
282+
283+
// get origin from the config if its defined
284+
let allowedOrigin = config.PUBLIC_ORIGIN ? new URL(config.PUBLIC_ORIGIN).origin : undefined;
285+
286+
if (
287+
dev || // if we're in dev mode
288+
!requestOrigin || // or the origin is null (SSR)
289+
isHostLocalhost(new URL(requestOrigin).hostname) // or the origin is localhost
290+
) {
291+
allowedOrigin = "*"; // allow all origins
292+
} else if (allowedOrigin === requestOrigin) {
293+
allowedOrigin = requestOrigin; // echo back the caller
294+
}
295+
296+
if (allowedOrigin) {
297+
response.headers.set("Access-Control-Allow-Origin", allowedOrigin);
298+
response.headers.set(
299+
"Access-Control-Allow-Methods",
300+
"GET, POST, PUT, PATCH, DELETE, OPTIONS"
301+
);
302+
response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
303+
}
294304
}
295-
}
296-
return response;
297-
}, requestId);
305+
return response;
306+
},
307+
{ requestId, url: event.url.pathname, ip: event.getClientAddress() }
308+
);
298309
};
299310

300311
export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {

0 commit comments

Comments
 (0)