Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Handle new report submission process
  • Loading branch information
regalijan committed Oct 19, 2023
1 parent a132fd6 commit a3a5308
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 86 deletions.
122 changes: 83 additions & 39 deletions app/routes/report.tsx
Expand Up @@ -10,6 +10,7 @@ import {
Input,
Link,
Text,
Textarea,
useToast,
} from "@chakra-ui/react";
import { useEffect, useState } from "react";
Expand Down Expand Up @@ -96,9 +97,8 @@ export default function () {
).value
.replaceAll(" ", "")
.split(",");
const file = (
document.getElementById("evidence") as HTMLInputElement
).files?.item(0);
const files = (document.getElementById("evidence") as HTMLInputElement)
.files;

if (!usernames.length)
return toast({
Expand All @@ -108,9 +108,9 @@ export default function () {
title: "Error",
});

if (!file)
if (!files?.length)
return toast({
description: "Must attach a file",
description: "Must attach at least one file",
isClosable: true,
status: "error",
title: "Error",
Expand All @@ -124,10 +124,29 @@ export default function () {
title: "Too Many Usernames",
});

if (!logged_in && !turnstileToken)
return toast({
description: "Please complete the captcha and try again",
isClosable: true,
status: "error",
title: "Captcha not completed",
});

const description = (
document.getElementById("description") as HTMLTextAreaElement
).value;

const filelist = [];

for (const file of files) {
filelist.push({ name: file.name, size: file.size });
}

const submitReq = await fetch("/api/reports/submit", {
body: JSON.stringify({
filename: file.name,
filesize: file.size,
description: description || undefined,
files: filelist,
turnstileResponse: logged_in ? undefined : turnstileToken,
usernames,
}),
headers: {
Expand All @@ -152,42 +171,62 @@ export default function () {
});
}

const { id, upload_url }: { id: string; upload_url: string } =
const { id, upload_urls }: { id: string; upload_urls: string[] } =
await submitReq.json();

setUploading(true);
const reader = file.stream().getReader();
const totalSize = filelist.reduce((a, b) => a + b.size, 0);
let bytesRead = 0;
let shouldRecall = false;

const uploadReq = await fetch(upload_url, {
body: supportsRequestStreams
? new ReadableStream({
async pull(controller) {
const chunk = await reader.read();

if (chunk.done) {
controller.close();
setUploading(false);
return;
}

controller.enqueue(chunk.value);
bytesRead += chunk.value.length;
setFileProgress(Math.floor((bytesRead / file.size) * 100));
},
})
: file,
// @ts-expect-error
duplex: supportsRequestStreams ? "half" : undefined,
headers: {
"content-type":
file.type ||
fileTypes[file.name.split(".")[file.name.split(".").length - 1]],
},
method: "PUT",
}).catch(console.error);
setUploading(true);

for (let i = 0; i < upload_urls.length; i++) {
const reader = files[i].stream().getReader();

if (!uploadReq?.ok) {
try {
const uploadReq = await fetch(upload_urls[i], {
body: supportsRequestStreams
? new ReadableStream({
async pull(controller) {
const chunk = await reader.read();

if (chunk.done) {
controller.close();

if (i === upload_urls.length - 1) setUploading(false);

return;
}

controller.enqueue(chunk.value);
bytesRead += chunk.value.length;
setFileProgress(Math.floor((bytesRead / totalSize) * 100));
},
})
: files[i],
// @ts-expect-error
duplex: supportsRequestStreams ? "half" : undefined,
headers: {
"content-type":
files[i].type ||
fileTypes[files[i].name.split(".").at(-1) as string],
},
method: "PUT",
});

if (!uploadReq.ok) {
shouldRecall = true;
break;
}
} catch (e) {
console.error(e);

shouldRecall = true;
break;
}
}

if (shouldRecall) {
await fetch("/api/reports/recall", {
body: JSON.stringify({ id }),
headers: {
Expand Down Expand Up @@ -245,7 +284,7 @@ export default function () {
</FormControl>
<br />
<FormControl isRequired>
<FormLabel>Your Evidence (Max Size: 512MB)</FormLabel>
<FormLabel>Your Evidence (Max size per file: 512MB)</FormLabel>
<Button
colorScheme="blue"
mr="8px"
Expand All @@ -256,6 +295,11 @@ export default function () {
<input id="evidence" multiple type="file" />
</FormControl>
<br />
<FormControl>
<FormLabel>Optional description</FormLabel>
<Textarea id="description" maxLength={512} />
</FormControl>
<br />
<div
className="cf-turnstile"
data-callback="setToken"
Expand Down
154 changes: 107 additions & 47 deletions functions/api/reports/submit.ts
Expand Up @@ -10,8 +10,16 @@ function errorResponse(error: string, status: number): Response {
}

export async function onRequestPost(context: RequestContext) {
const { actions, bypass, filename, filesize, turnstileResponse, usernames } =
context.data.body;
const {
actions,
bypass,
description,
filename,
files,
filesize,
turnstileResponse,
usernames,
} = context.data.body;

if (!context.data.current_user) {
if (typeof turnstileResponse !== "string")
Expand All @@ -21,6 +29,7 @@ export async function onRequestPost(context: RequestContext) {
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{
body: JSON.stringify({
remoteip: context.request.headers.get("CF-Connecting-IP"),
response: turnstileResponse,
secret: context.env.TURNSTILE_SECRETKEY,
}),
Expand All @@ -45,12 +54,42 @@ export async function onRequestPost(context: RequestContext) {
if (!Array.isArray(usernames))
return errorResponse("Usernames must be type of array", 400);

if (
!["string", "undefined"].includes(typeof description) ||
description?.length > 512
)
return errorResponse("Invalid description", 400);

if (
!Array.isArray(files) ||
files.find((file) => {
const keys = Object.keys(file);

return !keys.includes("name") || !keys.includes("size");
})
)
return errorResponse("File list missing name(s) and/or size(s)", 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 (
files.find(
(file) =>
typeof file.name !== "string" ||
typeof file.size !== "number" ||
file.size < 0 ||
file.size > 536870912
)
)
return errorResponse(
"One or more files contain an invalid name or size",
400
);

if (!usernames.length || usernames.length > 20)
return errorResponse(
"Number of usernames provided must be between 1 and 20",
Expand Down Expand Up @@ -111,45 +150,53 @@ export async function onRequestPost(context: RequestContext) {
metaNames.push(data.name);
}

const fileParts = filename.split(".");
let fileExt = fileParts[fileParts.length - 1];
const uploadUrlPromises: Promise<string>[] = [];

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);
for (const file of files) {
const filePartes = file.name.split(".");
let fileExten = filePartes.at(-1);

const fileKey = `${crypto.randomUUID().replaceAll("-", "")}/${crypto
.randomUUID()
.replaceAll("-", "")}${context.request.headers.get("cf-ray")}${Date.now()}`;
if (
filePartes.length < 2 ||
![
"mkv",
"mp4",
"wmv",
"jpg",
"png",
"m4v",
"jpeg",
"jfif",
"gif",
"webm",
"heif",
"heic",
"webp",
"mov",
].includes(fileExten.toLowerCase())
)
return errorResponse(
`File ${file.name} cannot be uploaded as it is unsupported`,
415
);

const fileUploadKey = `${crypto.randomUUID().replaceAll("-", "")}/${crypto
.randomUUID()
.replaceAll("-", "")}${context.request.headers.get(
"cf-ray"
)}${Date.now()}`;

uploadUrlPromises.push(
GenerateUploadURL(context.env, `t/${fileUploadKey}`, file.size, fileExten)
);
}

const uploadUrls = await Promise.allSettled(uploadUrlPromises);

const reportId = `${Date.now()}${context.request.headers.get(
"cf-ray"
)}${crypto.randomUUID().replaceAll("-", "")}`;

const uploadUrl = await GenerateUploadURL(
context.env,
fileKey,
filesize,
fileExt
);

const { current_user: currentUser } = context.data;

await context.env.DATA.put(
Expand All @@ -158,22 +205,32 @@ export async function onRequestPost(context: RequestContext) {
{ expirationTtl: 3600 }
);

if (["mkv", "mov", "wmv"].includes(fileExt.toLowerCase()))
await context.env.DATA.put(`videoprocessing_${fileKey}.${fileExt}`, "1", {
expirationTtl: 3600,
});
if (uploadUrls.find((uploadUrl) => uploadUrl.status === "rejected"))
return errorResponse("Failed to generate upload url", 500);

const attachments: string[] = [];

for (const urlResult of uploadUrls) {
let url = urlResult.toString().replace("t/", "");
const extension = (url.split(".").at(-1) as string).toLowerCase();

if (["mkv", "mov", "wmv"].includes(extension)) {
await context.env.DATA.put(`videoprocessing_${url}.${extension}`, "1");

url = url.replace(`.${extension}`, ".mp4");
}

attachments.push(url);
}

await context.env.DATA.put(
`report_${reportId}`,
JSON.stringify({
attachment: `${fileKey}.${
["mkv", "mov", "wmv"].includes(fileExt.toLowerCase()) ? "mp4" : fileExt
}`,
attachments,
id: reportId,
open: !bypass,
user: currentUser
? {
discriminator: currentUser.discriminator,
email: currentUser.email,
id: currentUser.id,
username: currentUser.username,
Expand All @@ -192,9 +249,12 @@ export async function onRequestPost(context: RequestContext) {
.run();
} catch {}

return new Response(JSON.stringify({ id: reportId, upload_url: uploadUrl }), {
headers: {
"content-type": "application/json",
},
});
return new Response(
JSON.stringify({ id: reportId, upload_urls: uploadUrls }),
{
headers: {
"content-type": "application/json",
},
}
);
}

0 comments on commit a3a5308

Please sign in to comment.