File size: 3,765 Bytes
154cecb
98051f8
20e7f44
 
d72b096
67577d7
20e7f44
38c2b30
 
566c2fc
 
5882bc9
 
a2475e2
5882bc9
a06a46a
d7b4e1d
 
566c2fc
d7b4e1d
 
d72b096
 
 
 
 
 
 
 
 
 
 
 
a06a46a
 
 
 
 
 
 
 
 
 
 
 
d72b096
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b4aeb2
566c2fc
d7b4e1d
 
3b4aeb2
154cecb
 
 
 
 
d72b096
38c2b30
d7b4e1d
67577d7
 
566c2fc
67577d7
 
 
 
 
d7b4e1d
 
 
 
 
 
d72b096
d7b4e1d
 
38c2b30
 
566c2fc
5882bc9
41442b0
 
 
 
 
60253ec
41442b0
 
 
 
60253ec
41442b0
 
5882bc9
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import { COOKIE_NAME, MESSAGES_BEFORE_LOGIN } from "$env/static/private";
import type { Handle } from "@sveltejs/kit";
import {
	PUBLIC_GOOGLE_ANALYTICS_ID,
	PUBLIC_ORIGIN,
	PUBLIC_APP_DISCLAIMER,
} from "$env/static/public";
import { collections } from "$lib/server/database";
import { base } from "$app/paths";
import { refreshSessionCookie, requiresUser } from "$lib/server/auth";
import { ERROR_MESSAGES } from "$lib/stores/errors";

export const handle: Handle = async ({ event, resolve }) => {
	const token = event.cookies.get(COOKIE_NAME);

	const user = token ? await collections.users.findOne({ sessionId: token }) : null;

	if (user) {
		event.locals.user = user;
	}

	function errorResponse(status: number, message: string) {
		const sendJson =
			event.request.headers.get("accept")?.includes("application/json") ||
			event.request.headers.get("content-type")?.includes("application/json");
		return new Response(sendJson ? JSON.stringify({ error: message }) : message, {
			status,
			headers: {
				"content-type": sendJson ? "application/json" : "text/plain",
			},
		});
	}

	if (!token) {
		const sessionId = crypto.randomUUID();
		if (await collections.users.findOne({ sessionId })) {
			return errorResponse(500, "Session ID collision");
		}
		event.locals.sessionId = sessionId;
	} else {
		event.locals.sessionId = token;
	}

	Object.freeze(event.locals);

	// CSRF protection
	const requestContentType = event.request.headers.get("content-type")?.split(";")[0] ?? "";
	/** https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype */
	const nativeFormContentTypes = [
		"multipart/form-data",
		"application/x-www-form-urlencoded",
		"text/plain",
	];
	if (event.request.method === "POST" && nativeFormContentTypes.includes(requestContentType)) {
		const referer = event.request.headers.get("referer");

		if (!referer) {
			return errorResponse(403, "Non-JSON form requests need to have a referer");
		}

		const validOrigins = [
			new URL(event.request.url).origin,
			...(PUBLIC_ORIGIN ? [new URL(PUBLIC_ORIGIN).origin] : []),
		];

		if (!validOrigins.includes(new URL(referer).origin)) {
			return errorResponse(403, "Invalid referer for POST request");
		}
	}

	if (
		!event.url.pathname.startsWith(`${base}/login`) &&
		!event.url.pathname.startsWith(`${base}/admin`) &&
		!["GET", "OPTIONS", "HEAD"].includes(event.request.method)
	) {
		if (
			!user &&
			requiresUser &&
			!((MESSAGES_BEFORE_LOGIN ? parseInt(MESSAGES_BEFORE_LOGIN) : 0) > 0)
		) {
			return errorResponse(401, ERROR_MESSAGES.authOnly);
		}

		// if login is not required and the call is not from /settings and we display the ethics modal with PUBLIC_APP_DISCLAIMER
		//  we check if the user has accepted the ethics modal first.
		// If login is required, `ethicsModalAcceptedAt` is already true at this point, so do not pass this condition. This saves a DB call.
		if (
			!requiresUser &&
			!event.url.pathname.startsWith(`${base}/settings`) &&
			!!PUBLIC_APP_DISCLAIMER
		) {
			const hasAcceptedEthicsModal = await collections.settings.countDocuments({
				sessionId: event.locals.sessionId,
				ethicsModalAcceptedAt: { $exists: true },
			});

			if (!hasAcceptedEthicsModal) {
				return errorResponse(405, "You need to accept the welcome modal first");
			}
		}
	}

	refreshSessionCookie(event.cookies, event.locals.sessionId);

	let replaced = false;

	const response = await resolve(event, {
		transformPageChunk: (chunk) => {
			// For some reason, Sveltekit doesn't let us load env variables from .env in the app.html template
			if (replaced || !chunk.html.includes("%gaId%")) {
				return chunk.html;
			}
			replaced = true;

			return chunk.html.replace("%gaId%", PUBLIC_GOOGLE_ANALYTICS_ID);
		},
	});

	return response;
};