2023-10-19 16:50:21 -04:00

153 lines
4.1 KiB
TypeScript

import GetPermissions from "../../permissions.js";
import tokenPrefixes from "../../../data/token_prefixes.json";
async function generateTokenHash(token: string): Promise<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, "");
}
function response(body: string, status: number) {
return new Response(body, {
headers: {
"content-type": "application/json",
},
status,
});
}
export async function onRequestDelete(context: RequestContext) {
const cookies = context.request.headers.get("cookie")?.split("; ");
if (!cookies) return response('{"error":"Not logged in"}', 401);
for (const cookie of cookies) {
const [name, value] = cookie.split("=");
if (name !== "_s") continue;
await context.env.DATA.delete(`auth_${await generateTokenHash(value)}`);
return new Response(null, {
headers: {
"clear-site-data": '"cookies"',
},
status: 204,
});
}
}
export async function onRequestGet(context: RequestContext) {
const { host, protocol, searchParams } = new URL(context.request.url);
const code = searchParams.get("code");
const state = searchParams.get("state");
if (!code) return response('{"error":"Missing code"}', 400);
if (!state) return response('{"error":"Missing state"}', 400);
const stateRedirect = await context.env.DATA.get(`state_${state}`);
if (!stateRedirect) return response('{"error":"Invalid state"}', 400);
const tokenReq = await fetch("https://discord.com/api/oauth2/token", {
body: new URLSearchParams({
code,
grant_type: "authorization_code",
redirect_uri: `${protocol}//${host}/api/auth/session`,
}).toString(),
headers: {
authorization: `Basic ${btoa(
context.env.DISCORD_ID + ":" + context.env.DISCORD_SECRET,
)}`,
"content-type": "application/x-www-form-urlencoded",
},
method: "POST",
});
if (!tokenReq.ok) {
console.log(await tokenReq.text());
return response('{"error":"Failed to redeem code"}', 500);
}
const tokenData: {
access_token: string;
expires_in: number;
refresh_token: string;
scope: string;
token_type: string;
} = await tokenReq.json();
if (tokenData.scope.search("guilds.members.read") === -1)
return response('{"error":"Do not touch the scopes!"}', 400);
let userData: { [k: string]: any } = {
...tokenData,
refresh_at: Date.now() + tokenData.expires_in * 1000 - 86400000,
};
const userReq = await fetch("https://discord.com/api/v10/users/@me", {
headers: {
authorization: `Bearer ${tokenData.access_token}`,
},
});
if (!userReq.ok) {
console.log(await userReq.text());
return response('{"error":"Failed to retrieve user"}', 500);
}
const apiUser: { [k: string]: any } = await userReq.json();
userData = {
...userData,
...apiUser,
};
const serverMemberReq = await fetch(
"https://discord.com/api/v10/users/@me/guilds/242263977986359297/member",
{
headers: {
authorization: `Bearer ${tokenData.access_token}`,
},
},
);
const memberData: { [k: string]: any } = await serverMemberReq.json();
if (serverMemberReq.ok) {
userData.permissions = await GetPermissions(userData.id, memberData.roles);
userData.roles = memberData.roles;
} else {
userData.permissions = await GetPermissions(userData.id);
}
const selectedTokenStart =
tokenPrefixes[Math.round(Math.random() * (tokenPrefixes.length - 1))] + "_";
const authToken =
selectedTokenStart +
`${crypto.randomUUID()}${crypto.randomUUID()}${crypto.randomUUID()}${crypto.randomUUID()}`.replaceAll(
"-",
"",
);
const tokenHash = await generateTokenHash(authToken);
await context.env.DATA.put(`auth_${tokenHash}`, JSON.stringify(userData), {
expirationTtl: tokenData.expires_in,
});
return new Response(null, {
headers: {
location: stateRedirect,
"set-cookie": `_s=${authToken}; HttpOnly; Max-Age=${tokenData.expires_in}; Path=/; SameSite=Lax; Secure`,
},
status: 302,
});
}