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