Add Vercel serverless function for Roblox OAuth start
cgen-01de34c2ac5e42398f28e05e25c15395
This commit is contained in:
parent
393db05a70
commit
bca6901a88
1 changed files with 83 additions and 0 deletions
83
api/roblox/oauth/start.ts
Normal file
83
api/roblox/oauth/start.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import type { VercelRequest, VercelResponse } from "@vercel/node";
|
||||
import { randomUUID, createHash } from "crypto";
|
||||
|
||||
function isHttps(req: VercelRequest) {
|
||||
const xfProto = (req.headers["x-forwarded-proto"] || "").toString();
|
||||
return xfProto.includes("https");
|
||||
}
|
||||
|
||||
function toBase64Url(input: Buffer) {
|
||||
return input
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/g, "");
|
||||
}
|
||||
|
||||
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||
if (req.method !== "GET") {
|
||||
res.setHeader("Allow", "GET");
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
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 redirectParam = Array.isArray(req.query.redirect_uri)
|
||||
? req.query.redirect_uri[0]
|
||||
: (req.query.redirect_uri as string | undefined);
|
||||
|
||||
const redirectUri = redirectParam && redirectParam.startsWith("http")
|
||||
? redirectParam
|
||||
: process.env.ROBLOX_OAUTH_REDIRECT_URI || `${baseSite}/roblox-callback`;
|
||||
|
||||
const scope = (Array.isArray(req.query.scope) ? req.query.scope[0] : (req.query.scope as string)) ||
|
||||
process.env.ROBLOX_OAUTH_SCOPE ||
|
||||
"openid";
|
||||
|
||||
const state = (Array.isArray(req.query.state) ? req.query.state[0] : (req.query.state as string)) || randomUUID();
|
||||
|
||||
const codeVerifier = toBase64Url(Buffer.from(randomUUID() + randomUUID())).slice(0, 64);
|
||||
const codeChallenge = toBase64Url(
|
||||
createHash("sha256").update(codeVerifier).digest(),
|
||||
);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
client_id: clientId,
|
||||
response_type: "code",
|
||||
redirect_uri: redirectUri,
|
||||
scope: String(scope),
|
||||
state: String(state),
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: "S256",
|
||||
});
|
||||
|
||||
const authorizeUrl = `https://apis.roblox.com/oauth/authorize?${params.toString()}`;
|
||||
|
||||
const secure = isHttps(req) || process.env.NODE_ENV === "production";
|
||||
const cookieCommon = `Path=/; HttpOnly; SameSite=Lax; Max-Age=${10 * 60}${secure ? "; Secure" : ""}`;
|
||||
res.setHeader("Set-Cookie", [
|
||||
`roblox_oauth_state=${encodeURIComponent(state)}; ${cookieCommon}`,
|
||||
`roblox_oauth_code_verifier=${encodeURIComponent(codeVerifier)}; ${cookieCommon}`,
|
||||
]);
|
||||
|
||||
// CORS for JSON fetch testing
|
||||
const origin = (req.headers.origin as string) || "*";
|
||||
res.setHeader("Access-Control-Allow-Origin", origin);
|
||||
res.setHeader("Vary", "Origin");
|
||||
|
||||
const json = ((Array.isArray(req.query.json) ? req.query.json[0] : req.query.json) || "").toString().toLowerCase();
|
||||
if (json === "true") {
|
||||
return res.status(200).json({ authorizeUrl, state });
|
||||
}
|
||||
|
||||
res.status(302).setHeader("Location", authorizeUrl);
|
||||
return res.end();
|
||||
} catch (e: any) {
|
||||
return res.status(500).json({ error: e?.message || String(e) });
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue