Initial commit
This commit is contained in:
122
functions/_middleware.ts
Normal file
122
functions/_middleware.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { renderPage } from "vite-plugin-ssr";
|
||||
|
||||
async function constructHTML(context: RequestContext) {
|
||||
const { pathname } = new URL(context.request.url);
|
||||
|
||||
if (pathname.startsWith("/api/")) return await context.next();
|
||||
|
||||
if (
|
||||
pathname.startsWith("/assets/") ||
|
||||
["/app.webmanifest", "/favicon.ico", "/robots.txt"].includes(pathname) ||
|
||||
pathname.startsWith("/files/")
|
||||
)
|
||||
return await context.env.ASSETS.fetch(context.request);
|
||||
|
||||
const { httpResponse, status } = await renderPage({
|
||||
current_user: context.data.current_user,
|
||||
kv: context.env.DATA,
|
||||
status: 200,
|
||||
urlOriginal: context.request.url,
|
||||
});
|
||||
|
||||
return new Response(httpResponse?.getReadableWebStream(), {
|
||||
headers: {
|
||||
"content-type": httpResponse?.contentType ?? "text/html;charset=utf-8",
|
||||
},
|
||||
status: [200, 404, 500].includes(status)
|
||||
? httpResponse?.statusCode
|
||||
: status,
|
||||
});
|
||||
}
|
||||
|
||||
async function generateTokenHash(token: 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, "");
|
||||
}
|
||||
|
||||
async function setAuth(context: RequestContext) {
|
||||
const cookies = context.request.headers.get("cookie");
|
||||
|
||||
if (!cookies) return await context.next();
|
||||
|
||||
const cookieList = cookies.split(/; /);
|
||||
|
||||
for (const c of cookieList) {
|
||||
const [name, value] = c.split("=");
|
||||
|
||||
if (name !== "_s") continue;
|
||||
|
||||
const userData = await context.env.DATA.get(
|
||||
`auth_${await generateTokenHash(value)}`
|
||||
);
|
||||
|
||||
if (userData) context.data.current_user = JSON.parse(userData);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return await context.next();
|
||||
}
|
||||
|
||||
async function setBody(context: RequestContext) {
|
||||
if (context.request.method === "POST") {
|
||||
if (context.request.headers.get("content-type") !== "application/json")
|
||||
return new Response('{"error":"Invalid content-type"}', {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
status: 400,
|
||||
});
|
||||
|
||||
let body: { [k: string]: any };
|
||||
|
||||
try {
|
||||
body = await context.request.json();
|
||||
} catch {
|
||||
return new Response('{"error":"Invalid JSON"}', {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
context.data.body = body;
|
||||
}
|
||||
|
||||
return await context.next();
|
||||
}
|
||||
|
||||
async function setHeaders(context: RequestContext) {
|
||||
const response = await context.next();
|
||||
|
||||
const rtvValues = [
|
||||
"Aldaria",
|
||||
"Altadena",
|
||||
"DEMA",
|
||||
"Dragonborn",
|
||||
"Hollywood",
|
||||
"Parkway East",
|
||||
"Parkway North",
|
||||
"Parkway West",
|
||||
"Tokyo",
|
||||
"Wintervale",
|
||||
];
|
||||
|
||||
response.headers.set(
|
||||
"RTV",
|
||||
rtvValues[Math.round(Math.random() * (rtvValues.length - 1))]
|
||||
);
|
||||
response.headers.set("X-Frame-Options", "DENY");
|
||||
response.headers.set("X-XSS-Protection", "1; mode=block");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const onRequest = [setAuth, constructHTML, setBody, setHeaders];
|
113
functions/api/appeals/submit.ts
Normal file
113
functions/api/appeals/submit.ts
Normal 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,
|
||||
});
|
||||
}
|
21
functions/api/appeals/toggle.ts
Normal file
21
functions/api/appeals/toggle.ts
Normal 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,
|
||||
});
|
||||
}
|
20
functions/api/auth/oauth.ts
Normal file
20
functions/api/auth/oauth.ts
Normal 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}`
|
||||
);
|
||||
}
|
207
functions/api/auth/session.ts
Normal file
207
functions/api/auth/session.ts
Normal 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,
|
||||
});
|
||||
}
|
54
functions/api/reports/complete.ts
Normal file
54
functions/api/reports/complete.ts
Normal 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,
|
||||
});
|
||||
}
|
27
functions/api/reports/recall.ts
Normal file
27
functions/api/reports/recall.ts
Normal 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,
|
||||
});
|
||||
}
|
154
functions/api/reports/submit.ts
Normal file
154
functions/api/reports/submit.ts
Normal 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",
|
||||
},
|
||||
});
|
||||
}
|
109
functions/gcloud.ts
Normal file
109
functions/gcloud.ts
Normal file
@ -0,0 +1,109 @@
|
||||
function arrBufToB64Url(buf: ArrayBuffer) {
|
||||
const b64data = btoa(String.fromCharCode(...new Uint8Array(buf)));
|
||||
return b64data.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
|
||||
function stringToBuffer(str: string) {
|
||||
const buffer = new ArrayBuffer(str.length);
|
||||
const ui8 = new Uint8Array(buffer);
|
||||
for (let i = 0, bufLen = str.length; i < bufLen; i++) {
|
||||
ui8[i] = str.charCodeAt(i);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
export async function GenerateUploadURL(
|
||||
env: Env,
|
||||
path: string,
|
||||
size: number,
|
||||
fileExt: string
|
||||
): Promise<string> {
|
||||
const accessToken = await GetAccessToken(env);
|
||||
const contentTypes: { [k: string]: string } = {
|
||||
gif: "image/gif",
|
||||
heic: "image/heic",
|
||||
heif: "image/heif",
|
||||
jfif: "image/jpeg",
|
||||
jpeg: "image/jpeg",
|
||||
jpg: "image/jpeg",
|
||||
m4v: "video/x-m4v",
|
||||
mkv: "video/x-matroska",
|
||||
mov: "video/quicktime",
|
||||
mp4: "video/mp4",
|
||||
png: "image/png",
|
||||
webp: "image/webp",
|
||||
webm: "video/webm",
|
||||
wmv: "video/x-ms-wmv",
|
||||
};
|
||||
|
||||
const resumableUploadReq = await fetch(
|
||||
`https://storage.googleapis.com/upload/storage/v1/b/portal-carcrushers-cc/o?uploadType=resumable&name=${encodeURIComponent(
|
||||
path
|
||||
)}`,
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
"x-upload-content-type": contentTypes[fileExt],
|
||||
"x-upload-content-length": size.toString(),
|
||||
},
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
|
||||
if (!resumableUploadReq.ok)
|
||||
throw new Error(
|
||||
`Failed to create resumable upload: ${await resumableUploadReq.text()}`
|
||||
);
|
||||
|
||||
const url = resumableUploadReq.headers.get("location");
|
||||
|
||||
if (!url) throw new Error("No location header returned");
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
async function GetAccessToken(env: Env): Promise<string> {
|
||||
const claimSet = btoa(
|
||||
JSON.stringify({
|
||||
aud: "https://oauth2.googleapis.com/token",
|
||||
exp: Math.floor(Date.now() / 1000) + 120,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
iss: env.WORKER_GSERVICEACCOUNT,
|
||||
scope:
|
||||
"https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/devstorage.read_write",
|
||||
})
|
||||
)
|
||||
.replaceAll("+", "-")
|
||||
.replaceAll("/", "_")
|
||||
.replaceAll("=", "");
|
||||
|
||||
const signingKey = await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
stringToBuffer(
|
||||
atob(env.STORAGE_ACCOUNT_KEY.replace(/(\r\n|\n|\r)/gm, ""))
|
||||
),
|
||||
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"]
|
||||
);
|
||||
const signature = await crypto.subtle.sign(
|
||||
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
||||
signingKey,
|
||||
stringToBuffer(`eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.${claimSet}`)
|
||||
);
|
||||
const assertion = `eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.${claimSet}.${arrBufToB64Url(
|
||||
signature
|
||||
)}`;
|
||||
const tokenRequest = await fetch("https://oauth2.googleapis.com/token", {
|
||||
body: `grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=${assertion}`,
|
||||
headers: {
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!tokenRequest.ok)
|
||||
throw new Error(`Failed to get access token: ${await tokenRequest.text()}`);
|
||||
|
||||
return ((await tokenRequest.json()) as { [k: string]: any }).access_token;
|
||||
}
|
36
functions/permissions.ts
Normal file
36
functions/permissions.ts
Normal file
@ -0,0 +1,36 @@
|
||||
export default function (userid: string, roles?: string[]): number {
|
||||
let permissions = 0;
|
||||
|
||||
if (roles?.includes("374851061233614849")) permissions |= 1 << 0; // Administration
|
||||
if (!roles) permissions |= 1 << 1;
|
||||
if (roles?.includes("593209890949038082")) permissions |= 1 << 2; // Discord Moderator
|
||||
if (roles?.includes("391986108763996160")) permissions |= 1 << 3; // Events Team
|
||||
if (roles?.includes("607697704419852289")) permissions |= 1 << 4; // Events Team Management
|
||||
if (
|
||||
[
|
||||
"165594923586945025",
|
||||
"289372404541554689",
|
||||
"320758924850757633",
|
||||
"396347223736057866",
|
||||
"704247919259156521",
|
||||
"891710683371167795",
|
||||
].includes(userid)
|
||||
)
|
||||
permissions |= 1 << 5;
|
||||
if (
|
||||
roles?.includes("542750631161626636") ||
|
||||
roles?.includes("542750839291248640")
|
||||
)
|
||||
permissions |= 1 << 6; // Head of Wall Moderation
|
||||
if (roles?.includes("684406593214742548")) permissions |= 1 << 7; // Head of Forum Moderation
|
||||
if (roles?.includes("784870326990405672")) permissions |= 1 << 8; // Data Team
|
||||
if (roles?.includes("298438715380858881")) permissions |= 1 << 9; // Wall Moderator
|
||||
if (roles?.includes("681632342346825879")) permissions |= 1 << 10; // Forum Moderator
|
||||
if (
|
||||
roles?.includes("321710070519955457") ||
|
||||
roles?.includes("338102086095077376")
|
||||
)
|
||||
permissions |= 1 << 11; // Head of Discord Moderation
|
||||
|
||||
return permissions;
|
||||
}
|
Reference in New Issue
Block a user