import getPermissions from "./permissions.js"; 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 refreshAuth(context: RequestContext) { const { current_user: currentUser } = context.data; if (!currentUser || currentUser.refresh_at > Date.now()) return await context.next(); const oauthData = await context.env.DATA.get( `oauthcredentials_${currentUser.id}`, { type: "json" }, ); if (!oauthData) return await context.next(); const refreshedTokenResponse = await fetch( "https://discord.com/api/v10/oauth2/token", { body: `grant_type=refresh_token&refresh_token=${currentUser.refresh_token}`, headers: { authorization: `Basic ${btoa( context.env.DISCORD_ID + ":" + context.env.DISCORD_SECRET, )}`, "content-type": "application/x-www-form-urlencoded", }, method: "POST", }, ); if (!refreshedTokenResponse.ok) return await context.next(); const accessData: { [k: string]: any } = await refreshedTokenResponse.json(); let tokenData: { [k: string]: any } = { ...accessData, refresh_at: Date.now() + 3600000, }; const newDiscordData = await fetch("https://discord.com/api/v10/users/@me", { headers: { authorization: `Bearer ${accessData.access_token}`, }, }); if (!newDiscordData.ok) return await context.next(); const userData: { [k: string]: any } = await newDiscordData.json(); const updatedServerMemberReq = await fetch( "https://discord.com/api/v10/users/@me/guilds/242263977986359297/member", { headers: { authorization: `Bearer ${accessData.access_token}`, }, }, ); userData.permissions = await getPermissions( userData.id, context, updatedServerMemberReq.ok ? ( (await updatedServerMemberReq.json()) as { [k: string]: any; } ).roles : undefined, ); const tokenHash = await generateTokenHash(context.data.sid); await context.env.DATA.put(`auth_${tokenHash}`, JSON.stringify(userData), { expirationTtl: accessData.expires_in, }); await context.env.DATA.put( `oauthcredentials_${userData.id}`, JSON.stringify(tokenData), { expirationTtl: 1209600000, }, ); delete context.data.sid; const jwtPayload = context.request.headers .get("authorization") ?.replace("Bearer ", "") .split(".") .at(1); if (jwtPayload) { let jwtData: { [k: string]: any }; try { jwtData = JSON.parse( atob(jwtPayload.replaceAll("-", "+").replaceAll("_", "/")), ); } catch { return jsonError("JWT is malformed", 400); } jwtData.email = userData.email; jwtData.exp = Math.floor(Date.now() / 1000) + userData.expires_in; jwtData.iat = Math.floor(Date.now() / 1000); jwtData.name = userData.username; jwtData.permissions = userData.permissions; jwtData.picture = userData.avatar ?? "https://carcrushers.cc/files/logo192.png"; 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, ["sign"], ); const jwtBase = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.${btoa( JSON.stringify(jwtData), ) .replaceAll("+", "-") .replaceAll("/", "_") .replaceAll("=", "")}`; const signature = btoa( String.fromCodePoint( ...new Uint8Array( await crypto.subtle.sign( "HMAC", key, new TextEncoder().encode(jwtBase), ), ), ), ) .replaceAll("+", "-") .replace("/", "_") .replaceAll("=", ""); const response = await context.next(); response.headers.set("refreshed-token", `${jwtBase}.${signature}`); return response; } return await context.next(); } 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( atob(jwtSegments[1].replaceAll("-", "+").replaceAll("_", "/")), ); const linkedSessionData = await context.env.DATA.get( `auth_${await generateTokenHash(sessionToken)}`, ); if (linkedSessionData) { context.data.current_user = JSON.parse(linkedSessionData); context.data.sid = sessionToken; 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); context.data.sid = value; } 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 ( ["PATCH", "POST", "PUT"].includes(context.request.method) && !context.request.url.endsWith("/api/infractions/new") ) { if ( !context.request.headers .get("content-type") ?.startsWith("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: string) => 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, refreshAuth, setTheme, constructHTML, setBody, setHeaders, ];