Initial commit

This commit is contained in:
2023-10-19 16:49:05 -04:00
commit d731041378
63 changed files with 10037 additions and 0 deletions

View File

@ -0,0 +1,113 @@
export async function onRequestPost(context: RequestContext) {
const { learned, whyBanned, whyUnban } = context.data.body;
if (
typeof learned !== "string" ||
typeof whyBanned !== "string" ||
typeof whyUnban !== "string" ||
!learned.length ||
learned.length > 2000 ||
!whyBanned.length ||
whyBanned.length > 500 ||
!whyUnban.length ||
whyUnban.length > 2000
)
return new Response(
'{"error":"One or more fields are missing or invalid"}',
{
headers: {
"content-type": "application/json",
},
status: 400,
}
);
const { current_user: currentUser } = context.data;
if (!currentUser.email)
return new Response('{"error":"No email for this session"}', {
headers: {
"content-type": "application/json",
},
status: 403,
});
const existingAppeals = await context.env.DATA.list({
prefix: `appeal_${currentUser.id}`,
});
const existingBlockedAppeal = await context.env.DATA.get(
`blockedappeal_${currentUser.id}`
);
if (
existingBlockedAppeal ||
existingAppeals.keys.find(
(appeal) => (appeal.metadata as { [k: string]: any })?.open
)
)
return new Response('{"error":"Appeal already submitted"}', {
headers: {
"content-type": "application/json",
},
status: 403,
});
if (await context.env.DATA.get(`appealban_${currentUser.id}`)) {
await context.env.DATA.put(`blockedappeal_${currentUser.id}`, "1", {
metadata: { email: currentUser.email },
});
return new Response(null, {
status: 204,
});
}
const appealId = `${currentUser.id}${Date.now()}${crypto
.randomUUID()
.replaceAll("-", "")}`;
await context.env.DATA.put(
`appeal_${appealId}`,
JSON.stringify({
learned,
user: currentUser,
whyBanned,
whyUnban,
}),
{
expirationTtl: 94608000,
metadata: {
created_at: Date.now(),
id: currentUser.id,
open: true,
tag: `${currentUser.id}#${currentUser.discriminator}`,
},
}
);
await fetch(context.env.APPEALS_WEBHOOK, {
body: JSON.stringify({
embeds: [
{
title: "Appeal Submitted",
color: 3756250,
description: `View this appeal at https://carcrushers.cc/mod-queue?id=${appealId}&type=appeal`,
fields: [
{
name: "Submitter",
value: `${currentUser.username}#${currentUser.discriminator} (${currentUser.id})`,
},
],
},
],
}),
headers: {
"content-type": "application/json",
},
method: "POST",
});
return new Response(null, {
status: 204,
});
}

View File

@ -0,0 +1,21 @@
export async function onRequestPost(context: RequestContext) {
const { active } = context.data.body;
if (typeof active !== "boolean")
return new Response('{"error":"Active property must be a boolean"}', {
headers: {
"content-type": "application/json",
},
status: 400,
});
if (active) {
await context.env.DATA.delete("appeal_disabled");
} else {
await context.env.DATA.put("appeal_disabled", "1");
}
return new Response(null, {
status: 204,
});
}

View File

@ -0,0 +1,20 @@
export async function onRequestGet(context: RequestContext) {
const { env, request } = context;
const { host, protocol } = new URL(request.url);
let returnPath = "/";
const referer = request.headers.get("referer");
if (referer) returnPath = new URL(referer).pathname;
const state = crypto.randomUUID().replaceAll("-", "");
await env.DATA.put(`state_${state}`, returnPath, { expirationTtl: 300 });
return Response.redirect(
`https://discord.com/oauth2/authorize?client_id=${
env.DISCORD_ID
}&redirect_uri=${encodeURIComponent(
`${protocol}//${host}/api/auth/session`
)}&response_type=code&scope=identify%20email%20guilds.members.read&state=${state}`
);
}

View File

@ -0,0 +1,207 @@
import GetPermissions from "../../permissions";
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: {
"set-cookie": "_s=; Max-Age=0",
},
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 = GetPermissions(userData.id, memberData.roles);
userData.roles = memberData.roles;
} else {
userData.permissions = GetPermissions(userData.id);
}
const tokenPrefixes = [
"ABOVE-THE-SKY",
"BANDITO",
"BE-CONCERNED",
"CAR-RADIO",
"CHEESE",
"CHLORINE",
"CRAZY-EQUALS-GENIUS",
"CUBICLES",
"DEAD",
"DEMOLITION-LOVERS",
"DEVIL-DOGS",
"DOUBT",
"DREADNOUGHT",
"DYING-IN-LA",
"FAIRLY-LOCAL",
"FORMIDABLE",
"GATES-OF-GLORY",
"GIRLS-GIRLS-BOYS",
"GONER",
"HEATHENS",
"HEAVYDIRTYSOUL",
"HELENA",
"HYDRA",
"I-WRITE-SINS-NOT-TRAGEDIES",
"KITCHEN-SINK",
"LEVITATE",
"LOCAL-GOD",
"MAGGIE",
"MAMA",
"MONTANA",
"NERO-FORTE",
"NOOB",
"NOT-TODAY",
"NO-CHANCES",
"POLARIZE",
"PSYCHO",
"ROMANCE",
"SAD-CLOWN",
"SATURDAY",
"SAY-IT-LOUDER",
"SEMI-AUTOMATIC",
"TEENAGERS",
"THUNDERSWORD",
"TOKYO-DRIFTING",
"TRAPDOOR",
"TREES",
"UMA-THURMAN",
"UNSAINTED",
"VERMILION",
"VERSAILLES",
"VICTORIOUS",
"VIVA-LAS-VENGEANCE",
"XIX",
];
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,
});
}

View File

@ -0,0 +1,54 @@
export async function onRequestPost(context: RequestContext) {
const { id } = context.data.body;
if (!id)
return new Response('{"error":"No ID provided"}', {
headers: {
"content-type": "application/json",
},
status: 400,
});
const userId = await context.env.DATA.get(`reportprocessing_${id}`);
if (!userId || userId !== context.data.current_user.id)
return new Response('{"error":"No report with that ID is processing"}', {
headers: {
"content-type": "application/json",
},
status: 404,
});
await context.env.DATA.delete(`reportprocessing_${id}`);
const {
metadata,
value,
}: KVNamespaceGetWithMetadataResult<string, { [k: string]: any }> =
await context.env.DATA.getWithMetadata(`report_${id}`);
delete metadata?.p;
await context.env.DATA.put(`report_${id}`, value as string, { metadata });
if (context.env.REPORTS_WEBHOOK) {
await fetch(context.env.REPORTS_WEBHOOK, {
body: JSON.stringify({
embeds: [
{
title: "Report Submitted",
color: 3756250,
description: `View this report at https://carcrushers.cc/mod-queue?id=${id}&type=game-report`,
},
],
}),
headers: {
"content-type": "application/json",
},
method: "POST",
});
}
return new Response(null, {
status: 204,
});
}

View File

@ -0,0 +1,27 @@
export async function onRequestPost(context: RequestContext) {
const { id } = context.data.body;
if (!id)
return new Response('{"error":"No ID provided"}', {
headers: {
"content-type": "application/json",
},
status: 400,
});
const reportUserId = await context.env.DATA.get(`reportprocessing_${id}`);
if (!reportUserId || context.data.current_user.id !== reportUserId)
return new Response('{"error":"No processing report with that ID found"}', {
headers: {
"content-type": "application/json",
},
status: 404,
});
await context.env.DATA.delete(`report_${id}`);
return new Response(null, {
status: 204,
});
}

View File

@ -0,0 +1,154 @@
import { GenerateUploadURL } from "../../gcloud";
function errorResponse(error: string, status: number): Response {
return new Response(JSON.stringify({ error }), {
headers: {
"content-type": "application/json",
},
status,
});
}
export async function onRequestPost(context: RequestContext) {
const { filename, filesize, usernames } = context.data.body;
if (!Array.isArray(usernames))
return errorResponse("Usernames must be type of array", 400);
if (typeof filename !== "string")
return errorResponse("Invalid file name", 400);
if (typeof filesize !== "number" || filesize < 0 || filesize > 536870912)
return errorResponse("Invalid file size", 400);
if (!usernames.length || usernames.length > 20)
return errorResponse(
"Number of usernames provided must be between 1 and 20",
400
);
for (const username of usernames) {
if (
username.length < 3 ||
username.length > 20 ||
username.match(/_/g)?.length > 1
)
return errorResponse(`Username "${username}" is invalid`, 400);
}
const rbxSearchReq = await fetch(
"https://users.roblox.com/v1/usernames/users",
{
body: JSON.stringify({
usernames,
excludeBannedUsers: true,
}),
headers: {
"content-type": "application/json",
},
method: "POST",
}
);
if (!rbxSearchReq.ok)
return errorResponse(
"Failed to locate Roblox users due to upstream error",
500
);
const rbxSearchData: { data: { [k: string]: any }[] } =
await rbxSearchReq.json();
if (rbxSearchData.data.length < usernames.length) {
const missingUsers = [];
for (const userData of rbxSearchData.data) {
if (!usernames.includes(userData.requestedUsername))
missingUsers.push(userData.requestedUsername);
}
return errorResponse(
`The following users do not exist or are banned from Roblox: ${missingUsers.toString()}`,
400
);
}
const metaIDs = [];
const metaNames = [];
for (const data of rbxSearchData.data) {
metaIDs.push(data.id);
metaNames.push(data.name);
}
const fileParts = filename.split(".");
let fileExt = fileParts[fileParts.length - 1];
if (
fileParts.length < 2 ||
![
"mkv",
"mp4",
"wmv",
"jpg",
"png",
"m4v",
"jpeg",
"jfif",
"gif",
"webm",
"heif",
"heic",
"webp",
"mov",
].includes(fileExt.toLowerCase())
)
return errorResponse("This type of file cannot be uploaded", 415);
const fileKey = `${crypto.randomUUID().replaceAll("-", "")}/${crypto
.randomUUID()
.replaceAll("-", "")}${context.request.headers.get("cf-ray")}${Date.now()}`;
const reportId = `${Date.now()}${context.request.headers.get(
"cf-ray"
)}${crypto.randomUUID().replaceAll("-", "")}`;
const uploadUrl = await GenerateUploadURL(
context.env,
fileKey,
filesize,
fileExt
);
await context.env.DATA.put(
`reportprocessing_${reportId}`,
context.data.current_user.id,
{ expirationTtl: 3600 }
);
await context.env.DATA.put(
`report_${reportId}`,
JSON.stringify({
attachment: `${fileKey}.${
["mkv", "mov", "wmv"].includes(fileExt.toLowerCase()) ? "mp4" : fileExt
}`,
reporter: context.data.current_user,
target_ids: metaIDs,
target_usernames: metaNames,
}),
{
metadata: {
i: context.data.current_user.id,
r: metaIDs.toString(),
p: true,
s: `${context.data.current_user.username}#${context.data.current_user.discriminator}`,
u: metaNames.toString(),
},
}
);
return new Response(JSON.stringify({ id: reportId, upload_url: uploadUrl }), {
headers: {
"content-type": "application/json",
},
});
}