2023-10-19 16:51:13 -04:00

195 lines
4.8 KiB
TypeScript

import { jsonError } from "./common.js";
async function constructHTML(context: RequestContext) {
const { pathname } = new URL(context.request.url);
if (pathname.startsWith("/api/")) return await context.next();
if (
pathname.startsWith("/assets/") ||
["/app.webmanifest", "/favicon.ico", "/robots.txt"].includes(pathname) ||
pathname.startsWith("/files/")
)
return await context.env.ASSETS.fetch(context.request);
return await context.next();
}
async function generateTokenHash(token: string) {
const hash = await crypto.subtle.digest(
"SHA-512",
new TextEncoder().encode(token),
);
return btoa(String.fromCharCode(...new Uint8Array(hash)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
async function setAuth(context: RequestContext) {
const cookies = context.request.headers.get("cookie");
const auth = context.request.headers.get("authorization");
if (auth) {
const jwtSegments = auth.replace("Bearer ", "").split(".");
if (jwtSegments.length !== 3) return jsonError("Malformed token", 401);
const { alg } = JSON.parse(atob(jwtSegments[0]));
if (alg !== "HS256") return jsonError("Invalid token", 400);
const key = await crypto.subtle.importKey(
"raw",
// @ts-expect-error
Uint8Array.from(
atob(
context.env.JWT_SIGNING_KEY.replaceAll("-", "+").replaceAll("_", "/"),
),
(m) => m.codePointAt(0),
),
{ hash: "SHA-256", name: "HMAC" },
false,
["verify"],
);
if (
!(await crypto.subtle.verify(
"HMAC",
key,
// @ts-expect-error
Uint8Array.from(
atob(jwtSegments[2].replaceAll("-", "+").replaceAll("_", "/")),
(m) => m.codePointAt(0),
),
new TextEncoder().encode(`${jwtSegments[0]}.${jwtSegments[1]}`),
))
)
return jsonError("Token could not be verified", 401);
const { jti: sessionToken }: { jti: string } = JSON.parse(jwtSegments[1]);
const linkedSessionData = await context.env.DATA.get(
`auth_${await generateTokenHash(sessionToken)}`,
);
if (linkedSessionData) {
context.data.current_user = JSON.parse(linkedSessionData);
return await context.next();
} else return jsonError("Session is invalid or expired", 401);
}
if (!cookies) return await context.next();
const cookieList = cookies.split(/; /);
for (const c of cookieList) {
const [name, value] = c.split("=");
if (name !== "_s") continue;
const userData = await context.env.DATA.get(
`auth_${await generateTokenHash(value)}`,
);
if (userData) context.data.current_user = JSON.parse(userData);
else
context.request.headers.append(
"set-cookie",
"_s=; HttpOnly; Max-Age=0; Path=/; Secure;",
);
break;
}
return await context.next();
}
async function setBody(context: RequestContext) {
if (
context.request.method === "POST" &&
!context.request.url.endsWith("/api/infractions/new")
) {
if (context.request.headers.get("content-type") !== "application/json")
return new Response('{"error":"Invalid content-type"}', {
headers: {
"content-type": "application/json",
},
status: 400,
});
let body: { [k: string]: any };
try {
body = await context.request.json();
} catch {
return new Response('{"error":"Invalid JSON"}', {
headers: {
"content-type": "application/json",
},
status: 400,
});
}
context.data.body = body;
}
return await context.next();
}
async function setHeaders(context: RequestContext) {
const response = await context.next();
const rtvValues = [
"Aldaria",
"Altadena",
"DEMA",
"Dragonborn",
"Heaven, Iowa",
"Hollywood",
"Parkway East",
"Parkway North",
"Parkway West",
"Tokyo",
"Wintervale",
];
response.headers.set("Permissions-Policy", "clipboard-write=(self)");
response.headers.set("Referrer-Policy", "same-origin");
response.headers.set(
"RTV",
rtvValues[Math.round(Math.random() * (rtvValues.length - 1))],
);
response.headers.set("X-Frame-Options", "SAMEORIGIN");
return response;
}
async function setTheme(context: RequestContext) {
const cookies = context.request.headers.get("cookie");
if (!cookies) {
context.data.theme = "dark";
return await context.next();
}
const cookieList = cookies.split("; ");
const themeCookie = cookieList.find((c) =>
c.startsWith("chakra-ui-color-mode"),
);
const theme = themeCookie?.split("=").at(1);
if (!theme || !["dark", "light"].includes(theme)) context.data.theme = "dark";
else context.data.theme = theme;
return await context.next();
}
export const onRequest = [
setAuth,
setTheme,
constructHTML,
setBody,
setHeaders
];