From 84543a3558f35e69281afa348b4b9120ff1696de Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Sun, 19 Oct 2025 17:42:59 +0000 Subject: [PATCH] Implement /api/roblox/oauth/start route cgen-92651c458a5b4b988d169ccc05e1f9f0 --- server/index.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/server/index.ts b/server/index.ts index 0d9f7512..ab643f68 100644 --- a/server/index.ts +++ b/server/index.ts @@ -352,6 +352,49 @@ export function createServer() { ); }; + // Roblox OAuth: start (build authorize URL with PKCE and redirect) + app.get("/api/roblox/oauth/start", (req, res) => { + try { + const clientId = process.env.ROBLOX_OAUTH_CLIENT_ID; + if (!clientId) return res.status(500).json({ error: "Roblox OAuth not configured" }); + + const baseSite = process.env.PUBLIC_BASE_URL || process.env.SITE_URL || "https://aethex.dev"; + const redirectUri = (typeof req.query.redirect_uri === "string" && req.query.redirect_uri.startsWith("http")) + ? String(req.query.redirect_uri) + : (process.env.ROBLOX_OAUTH_REDIRECT_URI || `${baseSite}/roblox-callback`); + + const scope = String(req.query.scope || process.env.ROBLOX_OAUTH_SCOPE || "openid"); + const state = String(req.query.state || randomUUID()); + + // PKCE + const codeVerifier = Buffer.from(randomUUID() + randomUUID()).toString("base64url").slice(0, 64); + const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); + + const params = new URLSearchParams({ + client_id: clientId, + response_type: "code", + redirect_uri: redirectUri, + scope, + state, + code_challenge: codeChallenge, + code_challenge_method: "S256", + }); + const authorizeUrl = `https://apis.roblox.com/oauth/authorize?${params.toString()}`; + + // set short-lived cookies for verifier/state (for callback validation) + const secure = req.secure || (req.get("x-forwarded-proto") === "https") || process.env.NODE_ENV === "production"; + res.cookie("roblox_oauth_state", state, { httpOnly: true, sameSite: "lax", secure, maxAge: 10 * 60 * 1000, path: "/" }); + res.cookie("roblox_oauth_code_verifier", codeVerifier, { httpOnly: true, sameSite: "lax", secure, maxAge: 10 * 60 * 1000, path: "/" }); + + if (String(req.query.json || "").toLowerCase() === "true") { + return res.json({ authorizeUrl, state }); + } + return res.redirect(302, authorizeUrl); + } catch (e: any) { + return res.status(500).json({ error: e?.message || String(e) }); + } + }); + app.get("/api/health", async (_req, res) => { try { const { error } = await adminSupabase