153 lines
4.1 KiB
TypeScript
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,
|
|
});
|
|
}
|