May as well push now
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Heading,
|
||||
Link,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Spacer,
|
||||
Stack,
|
||||
Text,
|
||||
useToast,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import { lazy, useState } from "react";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
const DataRequestFormChunkPurchases = lazy(
|
||||
() => import("../../components/DataRequestFormChunkPurchases.js"),
|
||||
);
|
||||
const DataRequestFormChunkTransfers = lazy(
|
||||
() => import("../../components/DataRequestFormChunkTransfers.js"),
|
||||
);
|
||||
|
||||
export async function loader({ context }: { context: RequestContext }) {
|
||||
const cookies = context.request.headers.get("cookie")?.split(/; */);
|
||||
const dtSession = cookies?.find((c) => c.startsWith("dt-auth="));
|
||||
|
||||
if (!cookies || !dtSession)
|
||||
throw Response.redirect("/data-request/auth", 303);
|
||||
|
||||
const sessionId = dtSession.replace("dr-auth=", "");
|
||||
const sessionData = await context.env.DATA.get(
|
||||
`data_request_session_${sessionId}`,
|
||||
);
|
||||
|
||||
if (!sessionData) throw Response.redirect("/data-request/auth", 303);
|
||||
|
||||
return JSON.parse(sessionData) as { id: number; username: string };
|
||||
}
|
||||
|
||||
export function meta() {
|
||||
return [
|
||||
{
|
||||
title: "Data Request - Car Crushers",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function () {
|
||||
const sessionData = useLoaderData<typeof loader>();
|
||||
const [requestType, setRequestType] = useState("purchase");
|
||||
const [files, setFiles] = useState<FileList | null>(null);
|
||||
const [oldIsTerminated, setOldIsTerminated] = useState(false);
|
||||
const [processingState, setProcessingState] = useState<string | null>(null);
|
||||
const [authUrl, setAuthUrl] = useState("");
|
||||
const toast = useToast();
|
||||
|
||||
async function startRequest() {
|
||||
const body = new FormData();
|
||||
|
||||
body.append("type", requestType);
|
||||
|
||||
if (files instanceof FileList) {
|
||||
for (const file of files) {
|
||||
body.append("file", file);
|
||||
}
|
||||
}
|
||||
|
||||
const requestStartResponse = await fetch("/api/data-requests/start", {
|
||||
body,
|
||||
method: "POST",
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
if (!requestStartResponse.ok) {
|
||||
let error = "An unknown error has occurred";
|
||||
|
||||
try {
|
||||
error = ((await requestStartResponse.json()) as { error: string })
|
||||
.error;
|
||||
} catch {}
|
||||
|
||||
toast({
|
||||
description: error,
|
||||
status: "error",
|
||||
title: "Failed to create request",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { url } = (await requestStartResponse.json()) as { url?: string };
|
||||
|
||||
if (url) {
|
||||
setAuthUrl(url);
|
||||
setProcessingState("auth");
|
||||
} else {
|
||||
setProcessingState("complete");
|
||||
}
|
||||
}
|
||||
|
||||
switch (processingState) {
|
||||
case "auth":
|
||||
return (
|
||||
<Container pt="16vh">
|
||||
<Card borderRadius="32px" p="4vh">
|
||||
<VStack alignContent="center" gap="2vh">
|
||||
<Heading>Verify Old Account</Heading>
|
||||
<br />
|
||||
<Text>
|
||||
In order to process your transfer request, you must sign in to
|
||||
your old Roblox account to verify account ownership. Remember to
|
||||
switch to the correct account at the prompt.
|
||||
</Text>
|
||||
<br />
|
||||
<Button
|
||||
as="a"
|
||||
borderRadius="24px"
|
||||
colorScheme="blue"
|
||||
href={authUrl}
|
||||
>
|
||||
Verify Old Account
|
||||
</Button>
|
||||
</VStack>
|
||||
</Card>
|
||||
</Container>
|
||||
);
|
||||
|
||||
case "complete":
|
||||
return (
|
||||
<Container
|
||||
left="50%"
|
||||
maxW="container.md"
|
||||
pos="absolute"
|
||||
top="50%"
|
||||
transform="translate(-50%, -50%)"
|
||||
>
|
||||
<Flex>
|
||||
<Spacer />
|
||||
<svg
|
||||
width="128"
|
||||
height="128"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M2.5 8a5.5 5.5 0 0 1 8.25-4.764.5.5 0 0 0 .5-.866A6.5 6.5 0 1 0 14.5 8a.5.5 0 0 0-1 0 5.5 5.5 0 1 1-11 0z" />
|
||||
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z" />
|
||||
</svg>
|
||||
<Spacer />
|
||||
</Flex>
|
||||
<br />
|
||||
<Heading textAlign="center">Request Submitted</Heading>
|
||||
<br />
|
||||
<Text textAlign="center">
|
||||
We have received your request and will begin processing it shortly.
|
||||
</Text>
|
||||
</Container>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<Container maxW="container.lg">
|
||||
<Heading pb="32px" size="xl">
|
||||
Data Requests
|
||||
</Heading>
|
||||
<Alert status="info">
|
||||
<AlertIcon />
|
||||
You are signed in to {sessionData.username}.{" "}
|
||||
<Link
|
||||
onClick={async () => {
|
||||
await fetch("/api/data-requests/session", {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
location.assign("/data-request/auth");
|
||||
}}
|
||||
textDecor="underline"
|
||||
>
|
||||
Wrong account?
|
||||
</Link>
|
||||
</Alert>
|
||||
<VStack alignItems="start" spacing={12}>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>Request Type</FormLabel>
|
||||
<RadioGroup onChange={setRequestType} value={requestType}>
|
||||
<Stack>
|
||||
<Radio value="purchase">Missing Purchase</Radio>
|
||||
<Radio value="transfer">
|
||||
Transfer Save Data to New Account
|
||||
</Radio>
|
||||
<Radio value="reset">Reset Save Data</Radio>
|
||||
</Stack>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
{requestType === "purchase" ? (
|
||||
<DataRequestFormChunkPurchases fileStateSet={setFiles} />
|
||||
) : null}
|
||||
{requestType === "transfer" ? (
|
||||
<DataRequestFormChunkTransfers
|
||||
fileStateSet={setFiles}
|
||||
isTerminated={oldIsTerminated}
|
||||
setIsTerminated={setOldIsTerminated}
|
||||
/>
|
||||
) : null}
|
||||
</VStack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { jsonError, jsonResponse } from "../../common.js";
|
||||
|
||||
export async function onRequestPost(context: RequestContext) {
|
||||
let formData: FormData;
|
||||
|
||||
try {
|
||||
formData = await context.request.formData();
|
||||
} catch {
|
||||
return jsonError("Malformed form data", 400);
|
||||
}
|
||||
|
||||
const files: File[] = [];
|
||||
|
||||
for (const file of formData.getAll("file")) {
|
||||
if (typeof file === "string") {
|
||||
return jsonError("'file' must be a file", 400);
|
||||
}
|
||||
|
||||
if (file.type === "image") {
|
||||
}
|
||||
|
||||
files.push(file);
|
||||
}
|
||||
|
||||
let bytes = 0;
|
||||
for (const file of files) {
|
||||
bytes += new Blob([file]).size;
|
||||
}
|
||||
|
||||
if (bytes > 26214400) return jsonError("Files too large (max 25MB)", 413);
|
||||
|
||||
const type = formData.get("type");
|
||||
|
||||
if (
|
||||
typeof type !== "string" ||
|
||||
["purchase", "reset", "transfer"].includes(type)
|
||||
)
|
||||
return jsonError(
|
||||
"'type' must be one of ('purchase', 'reset', 'transfer')",
|
||||
400,
|
||||
);
|
||||
|
||||
const { R2 } = context.env;
|
||||
const { prisma } = context.data;
|
||||
const requestId = `${Date.now()}${crypto.randomUUID().replaceAll("-", "")}`;
|
||||
|
||||
await prisma.dataRequest.create({
|
||||
data: {
|
||||
id: requestId,
|
||||
status: type === "transfer" ? "verification_required" : "pending",
|
||||
target_user: 0,
|
||||
type,
|
||||
},
|
||||
});
|
||||
|
||||
const attachmentIds = [];
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
attachmentIds.push(
|
||||
`${Date.now()}${crypto.randomUUID().replaceAll("-", "")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const uploadPromises = [];
|
||||
|
||||
for (let i = 0; i < attachmentIds.length; i++) {
|
||||
uploadPromises.push(R2.put(`dt/${attachmentIds[i]}`, files[i]));
|
||||
}
|
||||
|
||||
const settledUploads = (await Promise.allSettled(uploadPromises)).filter(
|
||||
(p) => p.status === "fulfilled",
|
||||
);
|
||||
const attachmentList = settledUploads.map((u) =>
|
||||
(u.value as R2Object).key.replace("dt/", ""),
|
||||
);
|
||||
|
||||
if (attachmentList.length > 0) {
|
||||
await prisma.dataRequest.update({
|
||||
data: {
|
||||
attachments: attachmentList,
|
||||
},
|
||||
where: {
|
||||
id: requestId,
|
||||
},
|
||||
});
|
||||
|
||||
const url = new URL(context.request.url);
|
||||
url.pathname = "/data-transfers/verify";
|
||||
|
||||
return jsonResponse(
|
||||
JSON.stringify({
|
||||
url: `https://apis.roblox.com/oauth/v1/authorize?client_id=${context.env.ROBLOX_OAUTH_CLIENT_ID}&redirect_uri=${encodeURIComponent(url.href)}&response_type=code&scope=openid&state=${requestId}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return new Response("{}", {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Location: `/data-request/status/${requestId}`,
|
||||
},
|
||||
status: 201,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user