diff --git a/functions/api/data-requests/session.ts b/functions/api/data-requests/session.ts new file mode 100644 index 0000000..67fbbcb --- /dev/null +++ b/functions/api/data-requests/session.ts @@ -0,0 +1,93 @@ +import { jsonError } from "../../common.js"; + +export async function onRequestDelete(context: RequestContext) { + const cookie = context.request.headers.get("cookie"); + + if (!cookie) return jsonError("Invalid session", 401); + + const cookies = cookie.split(/; */g); + let session = cookies.find((c) => c.startsWith("dr-auth=")); + + if (!session) return jsonError("Invalid session", 401); + + session = session.replace("dr-auth=", ""); + + await context.env.DATA.delete(`data_request_session_${session}`); + + return new Response(null, { + headers: { + "set-cookie": + "dr-auth=gone; HttpOnly; Max-Age=0; SameSite=Strict; Secure", + }, + status: 204, + }); +} + +export async function onRequestGet(context: RequestContext) { + const url = new URL(context.request.url); + const code = url.searchParams.get("code"); + + if (!code) return jsonError("No code provided", 400); + + const basicToken = btoa( + `${context.env.ROBLOX_OAUTH_CLIENT_ID}:${context.env.ROBLOX_OAUTH_CLIENT_SECRET}`, + ); + + const userResponse = await fetch("https://apis.roblox.com/oauth/v1/token", { + body: `code=${code}&grant_type=authorization_code`, + headers: { + authorization: `Basic ${basicToken}`, + "content-type": "application/x-www-form-urlencoded", + }, + method: "POST", + }); + + if (!userResponse.ok) return jsonError("Failed to redeem code", 500); + + const { + id_token, + refresh_token, + }: { id_token: string; refresh_token: string } = await userResponse.json(); + const tokenPayload = id_token.split(".")[1]; + let decodedToken: { [k: string]: any }; + + try { + decodedToken = JSON.parse( + atob(tokenPayload.replaceAll("-", "+").replaceAll("_", "/")), + ); + } catch { + return jsonError("Failed to decode ID token", 500); + } + + const sessionId = crypto.randomUUID().replace(/-/g, ""); + + await context.env.DATA.put( + `data_request_session_${sessionId}`, + JSON.stringify({ + id: parseInt(decodedToken.sub), + username: decodedToken.preferred_username, + }), + { + expirationTtl: 3600, + }, + ); + + try { + await fetch("https://apis.roblox.com/oauth/v1/token/revoke", { + body: `token=${refresh_token}`, + headers: { + authorization: `Basic ${basicToken}`, + "content-type": "application/x-www-form-urlencoded", + }, + method: "POST", + }); + } catch {} + + return new Response(null, { + headers: { + location: "/data-request", + "set-cookie": `dr-auth=${sessionId}; HttpOnly; Max-Age=3600; SameSite=Strict; Secure`, + }, + status: 302, + }); +}