Compare commits

..

60 Commits

Author SHA1 Message Date
73b6c85171 ow my formatting
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m35s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-05-11 00:52:14 -04:00
037eb7fcac Possibly fix appeals? 2026-05-11 00:51:51 -04:00
abc1389dbb Bump dependencies 2026-05-11 00:51:40 -04:00
cd566248fd Bump node version 2026-05-11 00:51:09 -04:00
3d7e499ec1 Create data requests table 2026-05-06 03:18:57 -04:00
40dd0b5a5c Remove data transfer code
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m25s
Test, Build, Deploy / Create Sentry Release (push) Successful in 7s
2026-04-29 02:05:08 -04:00
229398e401 Disable button while event is submitting
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 58s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-17 01:47:42 -04:00
a9863f5680 Remove unneeded unix time param
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 55s
Test, Build, Deploy / Create Sentry Release (push) Successful in 9s
2026-04-16 04:08:20 -04:00
2e76bd9f28 Make buttons hide again when performed_at is not null
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m7s
Test, Build, Deploy / Create Sentry Release (push) Successful in 7s
2026-04-16 03:52:40 -04:00
171240bc7d Add db setup migration
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m1s
Test, Build, Deploy / Create Sentry Release (push) Successful in 7s
2026-04-15 02:45:39 -04:00
ffce17d7aa Add more stuff to gitignore 2026-04-15 02:43:38 -04:00
06fdfe9d10 Log appeal denials
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m8s
Test, Build, Deploy / Create Sentry Release (push) Successful in 7s
2026-04-14 22:09:43 -04:00
ba643bf986 Apparently these are causing problems
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m1s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-14 04:16:45 -04:00
12f91dca7d Move input button styles hook to top level
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m2s
Test, Build, Deploy / Create Sentry Release (push) Successful in 5s
2026-04-14 03:49:06 -04:00
b6de1aa462 Change order of state changes
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m2s
Test, Build, Deploy / Create Sentry Release (push) Successful in 5s
2026-04-14 03:44:06 -04:00
b65b62dac5 Upload functions sourcemaps from CI 2026-04-14 03:36:42 -04:00
0ec5399726 Accept multiple input files again
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m1s
Test, Build, Deploy / Create Sentry Release (push) Successful in 5s
2026-04-14 03:25:09 -04:00
f0c4e178aa Actually hide border this time
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m0s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-14 03:21:11 -04:00
6ad4fa0514 Remove outline from input
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m1s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-14 03:16:16 -04:00
42275fcb0f Remove old button 2026-04-14 03:15:08 -04:00
0854d72449 Try new way of fixing this nonsense
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m3s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-14 03:11:32 -04:00
cb0be09c0d Use react state to keep track of file names
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m2s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-14 02:07:15 -04:00
16ecab6881 Fix safari annoyingness
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m2s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-14 01:48:37 -04:00
7d5ec1183c Bump node version
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m20s
Test, Build, Deploy / Create Sentry Release (push) Successful in 5s
2026-04-14 00:23:24 -04:00
96d221be2a Fix this one too 2026-04-14 00:20:57 -04:00
f5e3e3cca6 Add messaging service publish method
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 59s
Test, Build, Deploy / Create Sentry Release (push) Successful in 5s
2026-04-13 03:04:02 -04:00
2d9f03c394 Fix weird token element null issue
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m0s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-13 02:46:17 -04:00
c51b29ce57 Upload sourcemaps to pages 2026-04-13 02:45:58 -04:00
546842c4dd These need to still be parsed
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 58s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-12 01:34:32 -04:00
1f2a8770a1 Set keys on list elements 2026-04-12 01:30:18 -04:00
4b15c65092 Spit out as text in error console 2026-04-12 01:13:59 -04:00
b671aefd6e Possibly fix reports queue not working
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m2s
Test, Build, Deploy / Create Sentry Release (push) Successful in 7s
2026-04-12 01:03:39 -04:00
f32a7912b4 Fix media signing
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 56s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-11 04:59:05 -04:00
da0ce2b188 Rest of it
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 55s
Test, Build, Deploy / Create Sentry Release (push) Successful in 5s
2026-04-11 04:54:39 -04:00
6da49d191a Fix missing attachments 2026-04-11 04:53:58 -04:00
5457898ff9 Get almost everything else
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 55s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-11 04:49:02 -04:00
fe206e2fbd Commit the right files this time...
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 53s
Test, Build, Deploy / Create Sentry Release (push) Successful in 5s
2026-04-11 04:45:01 -04:00
02cac814da Use internally generated types for jsonobject and raw
Some checks failed
Test, Build, Deploy / Test, Build, and Deploy (push) Failing after 50s
Test, Build, Deploy / Create Sentry Release (push) Has been skipped
2026-04-11 04:42:39 -04:00
f184389ffd Migrate mod queue list endpoint
Some checks failed
Test, Build, Deploy / Test, Build, and Deploy (push) Failing after 50s
Test, Build, Deploy / Create Sentry Release (push) Has been skipped
2026-04-11 04:36:25 -04:00
5c17f87f89 Migrate the rest of the easy stuff
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 58s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-11 04:32:09 -04:00
48631e32be Finish rest of game appeal stuff 2026-04-11 04:29:25 -04:00
b60f211d7b Migrate report recall endpoint 2026-04-11 04:29:15 -04:00
1a891e5898 Even more migrations 2026-04-11 04:29:04 -04:00
7b72f815b0 More stuff to migrate 2026-04-11 04:28:45 -04:00
4860288d11 Oops need schema update 2026-04-11 04:28:11 -04:00
930128c0d4 Migrate reports submission
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 54s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-11 03:50:41 -04:00
b5e230e7f2 Maybe fix et member updating?
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 56s
Test, Build, Deploy / Create Sentry Release (push) Successful in 5s
2026-04-11 03:37:43 -04:00
cfc57c838e More events team nonsense
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 55s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-11 03:30:46 -04:00
465bb30966 Move some events team stuff to prisma
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 54s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-11 03:16:58 -04:00
95ab13775b Make quantifier not lazy
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 56s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-11 03:12:18 -04:00
91ec421450 Oops it actually needs to be added
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 55s
Test, Build, Deploy / Create Sentry Release (push) Successful in 5s
2026-04-11 03:06:11 -04:00
8e52692fc8 Set runtime to cloudflare
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 54s
Test, Build, Deploy / Create Sentry Release (push) Successful in 7s
2026-04-11 02:59:50 -04:00
a4470310e8 Bump dependencies
Some checks failed
Test, Build, Deploy / Test, Build, and Deploy (push) Failing after 53s
Test, Build, Deploy / Create Sentry Release (push) Has been skipped
2026-04-11 02:37:15 -04:00
02f0a299e0 Move mx logic to top level functions middleware
Some checks failed
Test, Build, Deploy / Test, Build, and Deploy (push) Failing after 54s
Test, Build, Deploy / Create Sentry Release (push) Has been skipped
2026-04-11 02:26:31 -04:00
c92f4d31f2 Trigger mx screen on http 503 internal response
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 53s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-11 01:26:12 -04:00
d5203e236a Add prisma to requestcontext type
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 1m9s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
2026-04-11 00:59:03 -04:00
71f56769c1 Add maintenance screen 2026-04-09 03:53:20 -04:00
61c75df368 Add prisma schema (and update) 2026-04-09 03:48:13 -04:00
91fa274df8 Add prisma 2026-04-06 01:53:22 -04:00
0ca9bc1164 Remove user cookies from sentry events
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 44s
Test, Build, Deploy / Create Sentry Release (push) Successful in 5s
2026-03-31 22:05:29 -04:00
76 changed files with 2939 additions and 1641 deletions

View File

@@ -27,6 +27,9 @@ jobs:
- name: Install Dependencies
run: npm ci --include=dev
- name: Generate Prisma Types
run: npx prisma generate
- name: Check Formatting
run: npm run check-format
@@ -53,7 +56,7 @@ jobs:
}'
- name: Deploy
run: wrangler pages deploy public --project-name $CLOUDFLARE_PROJECT_NAME
run: wrangler pages deploy public --upload-source-maps --project-name $CLOUDFLARE_PROJECT_NAME
Sentry-Release:
name: Create Sentry Release

4
.gitignore vendored
View File

@@ -37,3 +37,7 @@ public/build
# Wrangler data
.wrangler
wrangler.jsonc
wrangler.toml
/generated/prisma

View File

@@ -1 +1 @@
v24.14.0
v24.15.0

View File

@@ -2,8 +2,10 @@ import {
ChakraProvider,
Container,
cookieStorageManagerSSR,
Flex,
Heading,
Link,
Spacer,
Text,
} from "@chakra-ui/react";
import { ClientStyleContext, ServerStyleContext } from "./context.js";
@@ -94,6 +96,42 @@ export function ErrorBoundary() {
</DocumentWrapper>
);
case 503:
return (
<DocumentWrapper loaderData={{ hide: true }}>
<Container
left="50%"
maxW="container.md"
pos="absolute"
top="50%"
transform="translate(-50%, -50%)"
>
<Flex>
<Spacer />
<svg
xmlns="http://www.w3.org/2000/svg"
width="128"
height="128"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M12.496 8a4.5 4.5 0 0 1-1.703 3.526L9.497 8.5l2.959-1.11q.04.3.04.61" />
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-1 0a7 7 0 1 0-13.202 3.249l1.988-1.657a4.5 4.5 0 0 1 7.537-4.623L7.497 6.5l1 2.5 1.333 3.11c-.56.251-1.18.39-1.833.39a4.5 4.5 0 0 1-1.592-.29L4.747 14.2A7 7 0 0 0 15 8m-8.295.139a.25.25 0 0 0-.288-.376l-1.5.5.159.474.808-.27-.595.894a.25.25 0 0 0 .287.376l.808-.27-.595.894a.25.25 0 0 0 .287.376l1.5-.5-.159-.474-.808.27.596-.894a.25.25 0 0 0-.288-.376l-.808.27z" />
</svg>
<Spacer />
</Flex>
<br />
<Heading textAlign="center">
The engineers are breaking stuff again
</Heading>
<br />
<Text textAlign="center">
Someday they will finish, come back later.
</Text>
</Container>
</DocumentWrapper>
);
default:
captureRemixErrorBoundaryError(useRouteError());
return (
@@ -128,6 +166,11 @@ export async function loader({
}: {
context: RequestContext;
}): Promise<{ [k: string]: any }> {
if (context.data.mx)
throw new Response(null, {
status: 503,
});
let data: { [k: string]: string } = {};
if (context.env.COMMIT_SHA) data.commit_sha = context.env.COMMIT_SHA;

View File

@@ -40,11 +40,18 @@ export async function loader({ context }: { context: RequestContext }) {
!Boolean(disabled) &&
!Boolean(await dataKV.get(`blockedappeal_${currentUser.id}`)) &&
!Boolean(
await context.env.D1.prepare(
"SELECT * FROM appeals WHERE approved IS NULL AND json_extract(user, '$.id') = ? LIMIT 1;",
)
.bind(currentUser.id)
.first(),
await context.data.prisma.appeal.findFirst({
select: {
id: true,
},
where: {
approved: null,
user: {
path: "id",
equals: currentUser.id,
},
},
}),
),
can_toggle:
currentUser.permissions & (1 << 0) || currentUser.permissions & (1 << 11),

View File

@@ -44,6 +44,7 @@ export default function () {
const [eventType, setEventType] = useState("");
const [riddleAnswer, setRiddleAnswer] = useState("");
const [submitSuccess, setSubmitSuccess] = useState(false);
const [disableSubmit, setDisableSubmit] = useState(false);
useEffect(() => {
setDatePickerMin(`${new Date().toISOString().split("T").at(0)}`);
@@ -53,6 +54,7 @@ export default function () {
}, []);
async function submit() {
setDisableSubmit(true);
let eventResp: Response;
try {
@@ -69,6 +71,7 @@ export default function () {
method: "POST",
});
} catch {
setDisableSubmit(false);
toast({
description: "Please check your internet and try again",
isClosable: true,
@@ -86,6 +89,7 @@ export default function () {
errorMessage = ((await eventResp.json()) as { error: string }).error;
} catch {}
setDisableSubmit(false);
toast({
description: errorMessage,
isClosable: true,
@@ -150,7 +154,11 @@ export default function () {
onChange={(e) => setRiddleAnswer(e.target.value)}
placeholder="Riddle answer"
/>
<Button mt="16px" onClick={async () => await submit()}>
<Button
disabled={disableSubmit}
mt="16px"
onClick={async () => await submit()}
>
Book
</Button>
</Container>

View File

@@ -1,42 +0,0 @@
import { Button, Container, Heading, Text } from "@chakra-ui/react";
export default function () {
return (
<Container maxW="container.md">
<Heading size="lg">Transfer your Save Data</Heading>
<br />
<br />
<Text>Lost your account? Want to shake off a stalker?</Text>
<br />
<br />
<Text size="lg">We can help!</Text>
<br />
<br />
<Text>Some information you should know:</Text>
<br />
<Text>
We might require your .ROBLOSECURITY cookie, depending on your
circumstances. This is because Roblox does not allow terminated accounts
to utilize OAuth. Normally this would be a very bad idea, and we don't
like doing this either, but it may be the only option. Security is also
less of a concern for terminated accounts as they are blocked from
accessing almost all Roblox API endpoints (the exceptions being login,
logout, and creating support tickets - and only the logout endpoint
doesn't require completing a captcha). If you are concerned about your
account's security, we suggest logging in to your terminated account in
a private/incognito window, copying the .ROBLOSECURITY cookie from
there, and logging out once we have verified your old account (which
normally only takes a few seconds). The ultra paranoid may also consider
resetting their password. If you are not convinced or still have
questions, join our Discord server (link is on the about page) and open
a ticket with ModMail for us to verify you manually (no cookie
required).
</Text>
<br />
<br />
<Button as="a" href="/data-transfer/start" colorScheme="blue">
Start my Transfer
</Button>
</Container>
);
}

View File

@@ -1,10 +0,0 @@
import Success from "../../components/Success.js";
export default function () {
return (
<Success
heading="Data Transfer Submitted"
message="Your request is now being processed; this normally takes 1-2 weeks."
/>
);
}

View File

@@ -1,34 +0,0 @@
import { Button, Card, Container, Heading, VStack } from "@chakra-ui/react";
import { useLoaderData } from "@remix-run/react";
export async function loader({ context }: { context: RequestContext }) {
const { host, protocol } = new URL(context.request.url);
return { client_id: context.env.ROBLOX_OAUTH_CLIENT_ID, host, protocol };
}
export default function () {
const loaderData = useLoaderData<typeof loader>();
return (
<Container pt="16vh">
<Card borderRadius="32px" p="4vh">
<VStack alignContent="center" gap="2vh">
<Heading>Verify your new Roblox account</Heading>
<br />
<Button
as="a"
borderRadius="24px"
colorScheme="blue"
href={`https://apis.roblox.com/oauth/v1/authorize?client_id=${
loaderData.client_id
}&redirect_uri=${encodeURIComponent(
`${loaderData.protocol}//${loaderData.host}/api/data-transfers/verify`,
)}&response_type=code&scope=openid%20profile`}
>
Verify
</Button>
</VStack>
</Card>
</Container>
);
}

View File

@@ -1,81 +0,0 @@
import {
Button,
Container,
Heading,
HStack,
Radio,
RadioGroup,
Text,
Textarea,
useToast,
} from "@chakra-ui/react";
import { useState } from "react";
export default function () {
const [showCookieBox, setShowCookieBox] = useState(false);
const [loading, setLoading] = useState(false);
return (
<Container maxW="container.md">
<Heading pt="36px">Let's get started</Heading>
<Text pt="128px">Is your old Roblox account banned?</Text>
<RadioGroup onChange={(val) => setShowCookieBox(JSON.parse(val))}>
<HStack>
<Radio value="false">No</Radio>
<Radio value="true">Yes</Radio>
</HStack>
</RadioGroup>
<Textarea
id="cookie-box"
placeholder="Paste your .ROBLOSECURITY cookie here"
mt="16px"
style={{ display: showCookieBox ? "initial" : "none" }}
/>
<Button
colorScheme="blue"
isLoading={loading}
loadingText="Processing..."
mt="16px"
onClick={async () => {
setLoading(true);
const createTransferReq = await fetch("/api/data-transfers/create", {
body: JSON.stringify({
can_access: !showCookieBox,
cookie: (
document.getElementById("cookie-box") as HTMLInputElement
).value,
}),
headers: {
"content-type": "application/json",
},
method: "POST",
});
if (!createTransferReq.ok) {
setLoading(false);
useToast()({
description: (
(await createTransferReq.json()) as { error: string }
).error,
isClosable: true,
status: "error",
title: "Failed to create transfer request",
});
return;
}
location.assign(
((await createTransferReq.json()) as { url: string }).url,
);
}}
>
Continue
</Button>
<br />
<Text pt="16px">
If you cannot login at all, please visit the support page and join our
server.
</Text>
</Container>
);
}

View File

@@ -42,20 +42,16 @@ export async function loader({ context }: { context: RequestContext }) {
status: 403,
});
const etData = await context.env.D1.prepare(
"SELECT id, name, points, roblox_id FROM et_members;",
).all();
if (etData.error)
throw new Response(null, {
status: 500,
const etData = await context.data.prisma.etMember.findMany({
select: {
id: true,
name: true,
points: true,
roblox_id: true,
},
});
const members = etData.results as { [k: string]: any }[];
return { members } as {
members: { [k: string]: any }[];
};
return { members: etData };
}
export default function () {

View File

@@ -45,15 +45,15 @@ export async function loader({
status: 403,
});
const strikeData = await context.env.D1.prepare(
"SELECT * FROM et_strikes WHERE user = ?;",
)
.bind(params.uid)
.all();
const strikes = await context.data.prisma.etStrike.findMany({
where: {
user: params.uid,
},
});
return {
can_manage: Boolean([1 << 4, 1 << 12].find((p) => user.permissions & p)),
strikes: strikeData.results,
strikes,
user: params.uid,
};
}

View File

@@ -31,6 +31,7 @@ import {
import { useLoaderData } from "@remix-run/react";
import { useState } from "react";
import utc from "dayjs/plugin/utc.js";
import { EtMember } from "../../generated/prisma/client.js";
export const links: LinksFunction = () => {
return [
@@ -56,18 +57,31 @@ export async function loader({ context }: { context: RequestContext }) {
});
const now = new Date();
const eventsData = await context.env.D1.prepare(
"SELECT answer, approved, created_by, day, details, id, month, pending, performed_at, reached_minimum_player_count, type, year FROM events WHERE month = ? AND year = ? ORDER BY day;",
)
.bind(now.getUTCMonth() + 1, now.getUTCFullYear())
.all();
if (eventsData.error)
throw new Response(null, {
status: 500,
const eventsData = await context.data.prisma.event.findMany({
orderBy: {
day: "asc",
},
select: {
answer: true,
approved: true,
created_by: true,
day: true,
details: true,
id: true,
month: true,
pending: true,
performed_at: true,
reached_minimum_player_count: true,
type: true,
year: true,
},
where: {
month: now.getUTCMonth() + 1,
year: now.getUTCFullYear(),
},
});
const calendarData = eventsData.results.map((e) => {
const calendarData = eventsData.map((e) => {
return {
id: e.id,
title: (e.type as string).toUpperCase(),
@@ -78,19 +92,17 @@ export async function loader({ context }: { context: RequestContext }) {
};
});
const memberData = await context.env.D1.prepare(
"SELECT id, name FROM et_members WHERE id IN (SELECT created_by FROM events WHERE month = ? AND year = ?);",
)
.bind(now.getUTCMonth() + 1, now.getUTCFullYear())
.all();
const memberData = await context.data.prisma.$queryRaw<
EtMember[]
>`SELECT id, name FROM et_members WHERE id IN (SELECT created_by FROM events WHERE month = ${now.getUTCMonth() + 1} AND year = ${now.getUTCFullYear()});`;
return {
calendarData,
canManage: Boolean(
[1 << 4, 1 << 12].find((p) => context.data.current_user.permissions & p),
),
eventList: eventsData.results,
memberData: memberData.results,
eventList: eventsData,
memberData,
};
}
@@ -111,15 +123,7 @@ export default function () {
<ModalBody>
<Heading size="md">Host</Heading>
<Text>
{
(
data.memberData.find(
(m) => m.id === eventData.created_by,
) as {
[k: string]: any;
}
)?.name
}
{data.memberData.find((m) => m.id === eventData.created_by)?.name}
</Text>
<br />
<Heading size="md">Event Type</Heading>

View File

@@ -30,6 +30,7 @@ import { useLoaderData } from "@remix-run/react";
import { useState } from "react";
import calendarStyles from "react-big-calendar/lib/css/react-big-calendar.css";
import { type LinksFunction } from "@remix-run/cloudflare";
import { EtMember } from "../../generated/prisma/client.js";
export const links: LinksFunction = () => {
return [{ href: calendarStyles, rel: "stylesheet" }];
@@ -51,34 +52,41 @@ export async function loader({ context }: { context: RequestContext }) {
});
const now = new Date();
const monthEventList = await context.env.D1.prepare(
"SELECT answer, approved, created_by, day, details, id, month, pending, performed_at, reached_minimum_player_count, type, year FROM events WHERE month = ? AND year = ? ORDER BY day ASC;",
)
.bind(now.getUTCMonth() + 1, now.getUTCFullYear())
.all();
if (monthEventList.error)
throw new Response(null, {
status: 500,
const { prisma } = context.data;
const monthEventList = await prisma.event.findMany({
orderBy: {
day: "asc",
},
select: {
answer: true,
approved: true,
created_by: true,
day: true,
details: true,
id: true,
month: true,
pending: true,
performed_at: true,
reached_minimum_player_count: true,
type: true,
year: true,
},
where: {
month: now.getUTCMonth() + 1,
year: now.getUTCFullYear(),
},
});
const membersList = await context.env.D1.prepare(
"SELECT id, name FROM et_members WHERE id IN (SELECT created_by FROM events WHERE month = ? AND year = ?);",
)
.bind(now.getUTCMonth() + 1, now.getUTCFullYear())
.all();
if (membersList.error)
throw new Response(null, {
status: 500,
});
const membersList = await prisma.$queryRaw<
EtMember[]
>`SELECT id, name FROM et_members WHERE id IN (SELECT created_by FROM events WHERE month = ${now.getUTCMonth() + 1} AND year = ${now.getUTCFullYear()});`;
return {
can_approve: Boolean(
[1 << 4, 1 << 12].find((p) => context.data.current_user.permissions & p),
),
events: monthEventList.results,
members: membersList.results as { id: string; name: string }[],
events: monthEventList,
members: membersList,
user_id: context.data.current_user.id as string,
};
}
@@ -263,7 +271,7 @@ export default function () {
// Technically this won't be the same as the time in the db, but that doesn't matter since this is just to hide the button
newEventData[eventData.findIndex((e) => e.id === eventId)].performed_at =
Date.now();
new Date().toISOString();
setEventData([...newEventData]);
setSelectedEvent("");
@@ -305,7 +313,8 @@ export default function () {
const newEventData = eventData;
newEventData[eventData.findIndex((e) => e.id === eventId)].performed_at = 0;
newEventData[eventData.findIndex((e) => e.id === eventId)].performed_at =
new Date(0).toISOString();
setEventData([...newEventData]);
setSelectedEvent("");
setDisableClicks(false);
@@ -656,9 +665,7 @@ export default function () {
</Button>
</>
) : null}
{can_approve &&
!event.pending &&
typeof event.performed_at !== "number" ? (
{can_approve && !event.pending && !event.performed_at ? (
<>
<Button
colorScheme="blue"

View File

@@ -25,13 +25,12 @@ export async function loader({ context }: { context: RequestContext }) {
status: 403,
});
const memberResults = await context.env.D1.prepare(
"SELECT id, name FROM et_members;",
).all();
if (!memberResults.success)
throw new Response(null, {
status: 500,
const { prisma } = context.data;
const memberResults = await prisma.etMember.findMany({
select: {
id: true,
name: true,
},
});
const data: {
@@ -45,7 +44,7 @@ export async function loader({ context }: { context: RequestContext }) {
};
} = {};
for (const row of memberResults.results as Record<string, string>[]) {
for (const row of memberResults) {
data[row.id].fotd = 0;
data[row.id].gamenight = 0;
data[row.id].name = row.name;
@@ -53,22 +52,26 @@ export async function loader({ context }: { context: RequestContext }) {
data[row.id].qotd = 0;
}
const eventsResult = await context.env.D1.prepare(
"SELECT answered_at, created_by, day, month, performed_at, reached_minimum_player_count, type, year FROM events;",
).all();
if (!eventsResult.success)
throw new Response(null, {
status: 500,
const eventsResult = await prisma.event.findMany({
select: {
answered_at: true,
created_by: true,
day: true,
month: true,
performed_at: true,
reached_minimum_player_count: true,
type: true,
year: true,
},
});
for (const row of eventsResult.results) {
const creator = row.created_by as string;
const type = row.type as string;
for (const row of eventsResult) {
const creator = row.created_by;
const type = row.type;
if (!data[creator]) continue;
if (row.performed_at) data[creator][type as string] += 10;
if (row.performed_at) data[creator][type] += 10;
else {
const now = new Date();
const currentYear = now.getUTCFullYear();
@@ -76,33 +79,17 @@ export async function loader({ context }: { context: RequestContext }) {
const currentDay = now.getUTCDate();
if (
(row.year as number) < currentYear ||
(currentYear === row.year && currentMonth > (row.month as number)) ||
row.year < currentYear ||
(currentYear === row.year && currentMonth > row.month) ||
(currentMonth === row.month &&
currentYear === row.year &&
(row.day as number) < currentDay)
row.day < currentDay)
)
data[creator][type] -= 5;
}
switch (row.type) {
case "gamenight":
if (row.reached_minimum_player_count) data[creator].gamenight += 10;
break;
case "rotw":
if (
(row.answered_at as number) - (row.performed_at as number) >=
86400000
)
data[creator].rotw += 10;
break;
default:
break;
}
if (row.type === "gamenight" && row.reached_minimum_player_count)
data[creator].gamenight += 10;
}
return data;

View File

@@ -44,9 +44,12 @@ export async function loader({ context }: { context: RequestContext }) {
status: 403,
});
return (
await context.env.D1.prepare("SELECT id, name FROM et_members;").all()
).results;
return await context.data.prisma.etMember.findMany({
select: {
id: true,
name: true,
},
});
}
export default function () {

View File

@@ -37,14 +37,40 @@ export async function loader({ context }: { context: RequestContext }) {
year--;
}
const data = await context.env.D1.prepare(
"SELECT day, details, id FROM events WHERE approved = 1 AND month = ? AND year = ? AND (performed_at IS NULL OR (reached_minimum_player_count = 0 AND type = 'gamenight')) ORDER BY day;",
)
.bind(month, year)
.all();
const data = await context.data.prisma.event.findMany({
orderBy: {
day: "asc",
},
select: {
answered_at: true,
day: true,
details: true,
id: true,
performed_at: true,
reached_minimum_player_count: true,
type: true,
},
where: {
AND: {
approved: true,
month,
year,
OR: [
{
AND: [
{
reached_minimum_player_count: false,
type: "gamenight",
},
],
},
],
},
},
});
return {
events: data.results as Record<string, string | number>[],
events: data,
past_cutoff: now.getUTCDate() > 7,
};
}
@@ -52,7 +78,7 @@ export async function loader({ context }: { context: RequestContext }) {
export default function () {
const { events, past_cutoff } = useLoaderData<typeof loader>();
const { isOpen, onClose, onOpen } = useDisclosure();
const [eventData, setEventData] = useState({} as { [k: string]: any });
const [eventData, setEventData] = useState({} as (typeof events)[number]);
const [isBrowserSupported, setIsBrowserSupported] = useState(true);
const toast = useToast();
@@ -74,10 +100,10 @@ export default function () {
});
}
function getStatus(event: { [k: string]: string | number }) {
function getStatus(event: (typeof events)[number]) {
if (!event.performed_at) return "Approved";
if (event.type === "rotw" && event.answered_at) return "Solved";
if (event.type === "gamenight" && event.areached_minimum_player_count)
if (event.type === "gamenight" && event.reached_minimum_player_count)
return "Certified";
return "Completed";
@@ -106,7 +132,7 @@ export default function () {
});
const newData = structuredClone(eventData);
newData.reached_minimum_player_count = 1;
newData.reached_minimum_player_count = true;
setEventData(newData);
}
@@ -134,9 +160,12 @@ export default function () {
});
const newData = structuredClone(eventData);
newData.performed_at = Date.now();
setEventData(newData);
setEventData(
Object.defineProperty(newData, "performed_at", {
value: new Date().toISOString(),
}),
);
}
async function forgotten() {
@@ -162,9 +191,12 @@ export default function () {
});
const newData = structuredClone(eventData);
newData.performed_at = 0;
setEventData(newData);
setEventData(
Object.defineProperty(newData, "performed_at", {
value: new Date().toISOString(),
}),
);
}
async function solve() {
@@ -190,9 +222,12 @@ export default function () {
});
const newData = structuredClone(eventData);
newData.answered_at = Date.now();
setEventData(newData);
setEventData(
Object.defineProperty(newData, "performed_at", {
value: new Date().toISOString(),
}),
);
}
return (
@@ -216,13 +251,13 @@ export default function () {
<Box gap="8px">
<Heading size="xs">Completion</Heading>
<Button
disabled={typeof eventData.completed_at === "number"}
disabled={Boolean(eventData.performed_at)}
onClick={async () => await completed()}
>
Mark as Complete
</Button>
<Button
disabled={typeof eventData.completed_at === "number"}
disabled={typeof eventData.performed_at === "number"}
onClick={async () => await forgotten()}
>
Mark as Forgotten
@@ -243,7 +278,7 @@ export default function () {
<Box gap="8px">
<Heading size="xs">Certified Status</Heading>
<Button
disabled={Boolean(eventData.reached_minimum_player_count)}
disabled={eventData.reached_minimum_player_count}
onClick={async () => await certify()}
>
{eventData.reached_minimum_player_count
@@ -277,8 +312,8 @@ export default function () {
<Tr>
<Td>{event.day}</Td>
<Td>
{(event.details as string).length > 100
? `${(event.details as string).substring(0, 97)}...`
{event.details.length > 100
? `${event.details.substring(0, 97)}...`
: event.details}
</Td>
<Td>{getStatus(event)}</Td>

View File

@@ -268,6 +268,7 @@ export default function () {
element: (
<AppealCard
{...(entry as AppealCardProps & { port?: MessagePort })}
key={`appeal_${entry.id}`}
port={messageChannel.current?.port2}
/>
),
@@ -281,6 +282,7 @@ export default function () {
element: (
<GameAppealCard
{...(entry as GameAppealProps & { port?: MessagePort })}
key={`gma_${entry.id}`}
port={messageChannel.current?.port2}
/>
),
@@ -294,6 +296,7 @@ export default function () {
element: (
<InactivityNoticeCard
{...(entry as InactivityNoticeProps & { port?: MessagePort })}
key={`inactivity_${entry.id}`}
port={messageChannel.current?.port2}
/>
),
@@ -307,6 +310,7 @@ export default function () {
element: (
<ReportCard
{...(entry as ReportCardProps & { port?: MessagePort })}
key={`report_${entry.id}`}
port={messageChannel.current?.port2}
/>
),

View File

@@ -14,6 +14,7 @@ import {
Stack,
Text,
Textarea,
useMultiStyleConfig,
useToast,
} from "@chakra-ui/react";
import { useEffect, useState } from "react";
@@ -50,6 +51,9 @@ export default function () {
const toast = useToast();
const [uploading, setUploading] = useState(false);
const [loading, setLoading] = useState(false);
const inputSelectorProps = useMultiStyleConfig("Button", {
colorScheme: "blue",
});
const fileTypes: { [k: string]: string } = {
avif: "image/avif",
gif: "image/gif",
@@ -112,9 +116,9 @@ export default function () {
if (!logged_in) {
const tokenElem = document
.getElementsByName("cf-turnstile-response")
.item(0) as HTMLInputElement;
.item(0) as HTMLInputElement | null;
if (!tokenElem.value) {
if (!tokenElem?.value) {
setLoading(false);
return toast({
description: "Please complete the captcha and try again",
@@ -250,8 +254,8 @@ export default function () {
method: "POST",
});
setShowSuccess(true);
setLoading(false);
setShowSuccess(true);
}
useEffect(() => {
@@ -304,14 +308,19 @@ export default function () {
<br />
<FormControl isRequired>
<FormLabel>Your Evidence (Max size per file: 512MB)</FormLabel>
<Button
colorScheme="blue"
mr="8px"
onClick={() => document.getElementById("evidence")?.click()}
>
Select File
</Button>
<input id="evidence" multiple type="file" />
<Input
border="none"
id="evidence"
sx={{
"::file-selector-button": {
border: "none",
outline: "none",
...inputSelectorProps,
},
}}
multiple={true}
type="file"
/>
</FormControl>
<br />
<FormControl>

View File

@@ -40,13 +40,15 @@ export async function loader({ context }: { context: RequestContext }) {
status: 403,
});
const { results } = await context.env.D1.prepare(
"SELECT destination, path FROM short_links WHERE user = ?;",
)
.bind(userId)
.all();
return results as Record<string, string>[];
return await context.data.prisma.shortLink.findMany({
select: {
destination: true,
path: true,
},
where: {
user: userId,
},
});
}
export default function () {

View File

@@ -91,7 +91,7 @@ export default function (props: { isOpen: boolean; onClose: () => void }) {
</Thead>
<Tbody>
{entries.map((entry) => (
<Tr>
<Tr key={`appealban_${entry.user}`}>
<Td>{entry.user}</Td>
<Td>{entry.created_by}</Td>
<Td>{new Date(entry.created_at).toUTCString()}</Td>

View File

@@ -173,7 +173,6 @@ export default function (props: {
)}
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
@@ -223,7 +222,6 @@ export default function (props: {
{data.id ? data.username : ""}
</Text>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"

View File

@@ -17,13 +17,7 @@ export default function ({
>
<Flex>
<Spacer />
<svg
xmlns="http://www.w3.org/2000/svg"
width="128"
height="128"
fill="currentColor"
viewBox="0 0 16 16"
>
<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>

View File

@@ -1,5 +1,7 @@
import getPermissions from "./permissions.js";
import { jsonError } from "./common.js";
import { PrismaClient } from "../generated/prisma/client.js";
import { PrismaD1 } from "@prisma/adapter-d1";
import * as Sentry from "@sentry/cloudflare";
async function constructHTML(context: RequestContext) {
@@ -28,6 +30,27 @@ async function generateTokenHash(token: string) {
.replace(/=/g, "");
}
async function initializePrisma(context: RequestContext) {
const adapter = new PrismaD1(context.env.D1);
context.data.prisma = new PrismaClient({ adapter });
return await context.next();
}
async function mxAndBypassCheck(context: RequestContext) {
if (!(await context.env.DATA.get("mx"))) return await context.next();
const cookies = context.request.headers.get("cookie")?.split(/; */);
const isAPI = new URL(context.request.url).pathname.startsWith("/api");
if (!cookies?.length || !cookies.find((c) => c.startsWith("mxb="))) {
if (isAPI) return jsonError("API is undergoing maintenance", 503);
context.data.mx = true;
}
return await context.next();
}
async function refreshAuth(context: RequestContext) {
const { current_user: currentUser } = context.data;
@@ -382,14 +405,20 @@ async function setTheme(context: RequestContext) {
export const onRequest = [
Sentry.sentryPagesPlugin((context: RequestContext) => ({
beforeSend(event) {
delete event.request?.cookies;
return event;
},
dsn: context.env.FUNCTIONS_DSN,
release: context.env.COMMIT_SHA,
sendDefaultPii: true,
})),
mxAndBypassCheck,
setAuth,
refreshAuth,
setTheme,
constructHTML,
setBody,
initializePrisma,
setHeaders,
];

View File

@@ -1,4 +1,8 @@
import { jsonError } from "../../../common.js";
import {
Appeal,
PushNotification,
} from "../../../../generated/prisma/client.js";
export async function onRequestPost(context: RequestContext) {
const { pathname } = new URL(context.request.url);
@@ -20,22 +24,23 @@ export async function onRequestPost(context: RequestContext) {
context.data.targetId = id;
if (!pathname.endsWith("/ban")) {
const appeal: Record<string, any> | null = await context.env.D1.prepare(
"SELECT * FROM appeals WHERE id = ?;",
)
.bind(id)
.first();
const appeal: Appeal | null = await context.data.prisma.appeal.findUnique({
where: {
id: id,
},
});
if (!appeal) return jsonError("No appeal with that ID exists", 404);
appeal.user = JSON.parse(appeal.user);
context.data.appeal = appeal;
const pushNotificationData = await context.env.D1.prepare(
"SELECT token FROM push_notifications WHERE event_id = ? AND event_type = 'appeal';",
)
.bind(id)
.first();
const pushNotificationData: PushNotification | null =
await context.data.prisma.pushNotification.findUnique({
where: {
event_id: id,
event_type: "appeal",
},
});
if (pushNotificationData)
context.data.fcm_token = pushNotificationData.token;

View File

@@ -13,11 +13,12 @@ export async function onRequestPost(context: RequestContext) {
fcm_token,
);
await context.env.D1.prepare(
"DELETE FROM push_notifications WHERE event_id = ? AND event_type = 'appeal';",
)
.bind(appeal.id)
.run();
await context.data.prisma.pushNotification.delete({
where: {
event_id: appeal.id,
event_type: "appeal",
},
});
} else {
const emailResponse = await sendEmail(
appeal.user.email,
@@ -37,11 +38,8 @@ export async function onRequestPost(context: RequestContext) {
const { current_user: currentUser } = context.data;
await context.env.D1.prepare(
"UPDATE appeals SET approved = 1, user = json_remove(user, '$.email') WHERE id = ?;",
)
.bind(context.params.id)
.run();
await context.data.prisma
.$executeRaw`UPDATE appeals SET approved = TRUE, user = json_remove(user, '$.id') WHERE id = ${appeal.id};`;
await fetch(
`https://discord.com/api/v10/guilds/242263977986359297/bans/${appeal.user.id}`,

View File

@@ -6,9 +6,11 @@ export async function onRequestDelete(context: RequestContext) {
if (targetId.search(/^\d{16.19}$/) === -1)
return jsonError("Invalid target id", 400);
await context.env.D1.prepare("DELETE FROM appeal_bans WHERE user = ?;")
.bind(targetId)
.run();
await context.data.prisma.appealBan.delete({
where: {
user: targetId,
},
});
const { current_user: currentUser } = context.data;
@@ -46,11 +48,12 @@ export async function onRequestPost(context: RequestContext) {
if (targetId.search(/^\d{16,19}$/) === -1)
return jsonError("Invalid target id", 400);
await context.env.D1.prepare(
"INSERT INTO appeal_bans (created_at, created_by, user) VALUES (?, ?, ?);",
)
.bind(Date.now(), context.data.current_user.id, targetId)
.run();
await context.data.prisma.appealBan.create({
data: {
created_by: context.data.current_user.id,
user: targetId,
},
});
await fetch(context.env.APPEALS_WEBHOOK, {
body: JSON.stringify({

View File

@@ -14,11 +14,12 @@ export async function onRequestPost(context: RequestContext) {
fcm_token,
);
await context.env.D1.prepare(
"DELETE FROM push_notifications WHERE event_id = ? AND event_type = 'appeal';",
)
.bind(appeal.id)
.run();
await context.data.prisma.pushNotification.delete({
where: {
event_id: appeal.id,
event_type: "appeal",
},
});
} else {
const emailResponse = await sendEmail(
appeal.user.email,
@@ -36,11 +37,8 @@ export async function onRequestPost(context: RequestContext) {
}
}
await context.env.D1.prepare(
"UPDATE appeals SET approved = 0, user = json_remove(user, '$.email') WHERE id = ?;",
)
.bind(context.params.id)
.run();
await context.data.prisma
.$executeRaw`UPDATE appeals SET approved = FALSE, user = json_remove(user, '$.id') WHERE id = ${appeal.id};`;
const { current_user: currentUser } = context.data;

View File

@@ -1,9 +1,11 @@
import { jsonResponse } from "../../common.js";
export async function onRequestGet(context: RequestContext) {
const { results } = await context.env.D1.prepare(
"SELECT * FROM appeal_bans ORDER BY created_by DESC;",
).all();
const results = await context.data.prisma.appealBan.findMany({
orderBy: {
created_at: "desc",
},
});
return jsonResponse(JSON.stringify(results));
}

View File

@@ -6,11 +6,12 @@ export async function onRequestGet(context: RequestContext) {
if (
!currentUser.email ||
(await context.env.DATA.get("appeal_disabled")) ||
(await context.env.D1.prepare(
"SELECT id FROM appeals WHERE open = 1 AND user = ?;",
)
.bind(currentUser.id)
.first()) ||
(await context.data.prisma.appeal.findFirst({
where: {
approved: null,
user: currentUser.id,
},
})) ||
(await context.env.DATA.get(`blockedappeal_${currentUser.id}`))
)
return jsonResponse('{"can_appeal":false}');
@@ -47,18 +48,24 @@ export async function onRequestPost(context: RequestContext) {
if (
existingBlockedAppeal ||
(await context.env.D1.prepare(
"SELECT approved FROM appeals WHERE approved IS NULL AND json_extract(user, '$.id') = ?;",
)
.bind(currentUser.id)
.first())
(await context.data.prisma.appeal.findFirst({
where: {
approved: null,
user: {
path: "id",
equals: currentUser.id,
},
},
}))
)
return jsonError("Appeal already submitted", 403);
if (
await context.env.D1.prepare("SELECT * FROM appeal_bans WHERE user = ?;")
.bind(currentUser.id)
.first()
await context.data.prisma.appealBan.findUnique({
where: {
user: currentUser.id,
},
})
) {
await context.env.DATA.put(`blockedappeal_${currentUser.id}`, "1", {
metadata: { email: currentUser.email },
@@ -73,29 +80,28 @@ export async function onRequestPost(context: RequestContext) {
.randomUUID()
.replaceAll("-", "")}`;
await context.env.D1.prepare(
"INSERT INTO appeals (ban_reason, created_at, id, learned, reason_for_unban, user) VALUES (?, ?, ?, ?, ?, ?);",
)
.bind(
whyBanned,
Date.now(),
appealId,
await context.data.prisma.appeal.create({
data: {
ban_reason: whyBanned,
id: appealId,
learned,
whyUnban,
JSON.stringify({
reason_for_unban: whyUnban,
user: {
email: currentUser.email,
id: currentUser.id,
username: currentUser.username,
}),
)
.run();
},
},
});
if (typeof senderTokenId === "string") {
await context.env.D1.prepare(
"INSERT INTO push_notifications (created_at, event_id, event_type, token) VALUES (?, ?, 'appeal', ?)",
)
.bind(Date.now(), appealId, senderTokenId)
.run();
await context.data.prisma.pushNotification.create({
data: {
event_id: appealId,
event_type: "appeal",
token: senderTokenId,
},
});
}
await fetch(context.env.APPEALS_WEBHOOK, {

View File

@@ -1,19 +0,0 @@
export async function onRequest(context: RequestContext) {
const cookies = context.request.headers.get("cookie");
if (!cookies) return await context.next();
const cookieList = cookies.split("; ").map((cookie: string) => {
const [name, value] = cookie.split("=");
return { name, value };
});
const transferId = cookieList.find(
(cookie: { name: string; value: string }) => cookie.name === "__dtid",
);
if (transferId) context.data.data_transfer_id = transferId;
return await context.next();
}

View File

@@ -1,74 +0,0 @@
import { jsonError } from "../../common.js";
export async function onRequestPost(context: RequestContext) {
const { cookie, is_banned } = context.data.body;
if (
typeof is_banned !== "boolean" ||
(is_banned && typeof cookie !== "string") ||
(is_banned &&
!cookie.match(
/_\|WARNING:-DO-NOT-SHARE-THIS\.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items\.\|_[A-F\d]+/,
))
)
return jsonError("Invalid request", 400);
const id =
(context.request.headers.get("cf-ray")?.split("-")[0] as string) +
Date.now().toString() +
crypto.randomUUID().replaceAll("-", "");
if (!is_banned) {
await context.env.DATA.put(`datatransfer_${id}`, "{}", {
expirationTtl: 3600,
});
const host = context.request.headers.get("Host") as string;
return new Response(
`{"url":"https://apis.roblox.com/oauth/v1/authorize?client_id=${
context.env.ROBLOX_OAUTH_CLIENT_ID
}&redirect_uri=${encodeURIComponent(
`http${host.startsWith(
"localhost" ? "" : "s",
)}://${host}/api/data-transfers/verify`,
)}"}`,
{
headers: {
"set-cookie": `__dtid=${id}; HttpOnly; Max-Age=3600; Path=/; SameSite=Lax; Secure`,
},
},
);
}
const authedUserReq = await fetch(
"https://users.roblox.com/v1/users/authenticated",
{
headers: {
cookie: `.ROBLOSECURITY=${cookie}`,
},
},
);
if (!authedUserReq.ok) return jsonError("Cookie is invalid", 400);
const authedUser: { id: number; name: string } = await authedUserReq.json();
await context.env.DATA.put(
`datatransfer_${id}`,
JSON.stringify({
oldUser: authedUser,
}),
{
expirationTtl: 3600,
},
);
return new Response(null, {
headers: {
location: "/data-transfer/destination-account",
"set-cookie": `__dtid=${id}; HttpOnly; Max-Age=3600; Path=/; SameSite=Lax; Secure`,
},
status: 201,
});
}

View File

@@ -1,92 +0,0 @@
import { jsonError } from "../../common.js";
import { getBanList } from "../../roblox-open-cloud.js";
export async function onRequestGet(context: RequestContext) {
const code = new URL(context.request.url).searchParams.get("code");
if (!code) return jsonError("Missing code", 400);
const dataTransferData = (await context.env.DATA.get(
`datatransfer_${context.data.data_transfer_id}`,
{ type: "json" },
)) as { [k: string]: any } | null;
if (!dataTransferData)
return jsonError("No transfer exists with that ID", 404);
const exchangeReq = await fetch("https://apis.roblox.com/oauth/v1/token", {
body: `code=${code}&grant_type=authorization_code`,
headers: {
authorization: `Basic ${
btoa(context.env.ROBLOX_OAUTH_ID) +
":" +
context.env.ROBLOX_OAUTH_SECRET
}`,
"content-type": "application/x-www-form-urlencoded",
},
method: "POST",
});
if (!exchangeReq.ok) return jsonError("Failed to redeem code", 500);
const { id_token } = (await exchangeReq.json()) as { id_token: string };
const { name, preferred_username, sub } = JSON.parse(
atob(id_token.replaceAll("-", "+").replaceAll("_", "/")),
);
if (!preferred_username) return jsonError("Username missing", 500);
const userObj = {
displayName: name,
id: parseInt(sub),
name: preferred_username,
};
let redirectLocation = "/data-transfer/complete";
if (dataTransferData.oldUser) {
let banList;
try {
banList = (await getBanList(context)).value;
} catch {
return jsonError("Failed to create data transfer request", 500);
}
if (banList[userObj.id].BanType)
return new Response(null, {
headers: {
location: redirectLocation,
},
status: 302,
});
dataTransferData.newUser = userObj;
await fetch(
`https://api.trello.com/1/cards?key=${context.env.TRELLO_API_KEY}&token=${context.env.TRELLO_API_TOKEN}`,
{
body: JSON.stringify({
desc: `${dataTransferData.oldUser.name} -> ${userObj.name}\n${dataTransferData.oldUser.id} -> ${userObj.id}\nNO MODMAIL TICKET - WEBSITE FORM SUBMISSION`,
idList: context.env.TRELLO_LIST_ID,
name: `${dataTransferData.oldUser.name} | Data Transfer`,
}),
headers: {
"content-type": "application/json",
},
method: "POST",
},
);
} else {
dataTransferData.oldUser = userObj;
redirectLocation = "/data-transfer/destination-account";
}
return new Response(null, {
headers: {
location: redirectLocation,
},
status: 302,
});
}

View File

@@ -2,15 +2,18 @@ import { jsonError } from "../../../common.js";
export async function onRequestDelete(context: RequestContext) {
const eventId = context.params.id as string;
const eventData:
| ({
[k: string]: number;
} & { created_by: string })
| null = await context.env.D1.prepare(
"SELECT created_by, day, month, performed_at, year FROM events WHERE id = ?;",
)
.bind(eventId)
.first();
const eventData = await context.data.prisma.event.findUnique({
select: {
created_by: true,
day: true,
month: true,
performed_at: true,
year: true,
},
where: {
id: eventId,
},
});
if (!eventData) return jsonError("No event exists with that ID", 404);
@@ -41,9 +44,11 @@ export async function onRequestDelete(context: RequestContext) {
400,
);
await context.env.D1.prepare("DELETE FROM events WHERE id = ?;")
.bind(eventId)
.run();
await context.data.prisma.event.delete({
where: {
id: eventId,
},
});
return new Response(null, {
status: 204,
@@ -53,13 +58,18 @@ export async function onRequestDelete(context: RequestContext) {
export async function onRequestPatch(context: RequestContext) {
const eventId = context.params.id as string;
const { body } = context.data;
const eventData:
| ({ [k: string]: number } & { created_by: string; type: string })
| null = await context.env.D1.prepare(
"SELECT created_by, day, month, type, year FROM events WHERE id = ?;",
)
.bind(eventId)
.first();
const eventData = await context.data.prisma.event.findUnique({
select: {
created_by: true,
day: true,
month: true,
type: true,
year: true,
},
where: {
id: eventId,
},
});
if (!eventData) return jsonError("No event exists with that ID", 404);
@@ -83,7 +93,7 @@ export async function onRequestPatch(context: RequestContext) {
typeof body.day !== "number" ||
body.day > date.getUTCDate() ||
body.day < 1 ||
// Check for non-integers
// Check for nonintegers
Math.floor(body.day) !== body.day ||
currentDay >= body.day
)
@@ -107,26 +117,38 @@ export async function onRequestPatch(context: RequestContext) {
4: 35,
};
const weekRange = Math.floor(body.day / 7);
const matchingROTW = await context.env.D1.prepare(
"SELECT id FROM events WHERE (approved = 1 OR pending = 1) AND day BETWEEN ? AND ? AND month = ? AND type = 'rotw' AND year = ?;",
)
.bind(
weekRanges[weekRange] - 7,
weekRanges[weekRange],
eventData.month,
eventData.year,
)
.first();
const matchingROTW = await context.data.prisma.event.findMany({
select: {
id: true,
},
where: {
OR: [{ approved: true }, { pending: true }],
day: {
gte: weekRanges[weekRange] - 7,
lte: weekRanges[weekRange],
},
month: eventData.month,
year: eventData.year,
},
});
if (matchingROTW)
return jsonError("There is already an ROTW scheduled for that week", 400);
} else {
const matchingEvent = await context.env.D1.prepare(
"SELECT id FROM events WHERE (approved = 1 OR pending = 1) AND day = ? AND month = ? AND type = ? AND year = ?;",
)
.bind(body.day, eventData.month, eventData.type, eventData.year)
.first();
const matchingEvent = await context.data.prisma.event.findMany({
select: {
id: true,
},
where: {
OR: [{ approved: true }, { pending: true }],
AND: [
{ day: body.day },
{ month: eventData.month },
{ type: eventData.type },
{ year: eventData.year },
],
},
});
if (matchingEvent)
return jsonError(
@@ -135,9 +157,14 @@ export async function onRequestPatch(context: RequestContext) {
);
}
await context.env.D1.prepare("UPDATE events SET day = ? WHERE id = ?;")
.bind(body.day, eventId)
.run();
await context.data.prisma.event.update({
data: {
day: body.day,
},
where: {
id: eventId,
},
});
await fetch(context.env.EVENTS_WEBHOOK, {
body: JSON.stringify({
@@ -162,22 +189,29 @@ export async function onRequestPatch(context: RequestContext) {
export async function onRequestPost(context: RequestContext) {
const eventId = context.params.id as string;
const eventData = await context.env.D1.prepare(
"SELECT approved, performed_at FROM events WHERE id = ?;",
)
.bind(eventId)
.first();
const eventData = await context.data.prisma.event.findUnique({
select: {
approved: true,
performed_at: true,
},
where: {
id: eventId,
},
});
if (!eventData) return jsonError("No event exists with that ID", 404);
if (!eventData.approved)
return jsonError("Cannot perform unapproved event", 403);
await context.env.D1.prepare(
"UPDATE events SET performed_at = ? WHERE id = ?;",
)
.bind(Date.now(), eventId)
.run();
await context.data.prisma.event.update({
data: {
performed_at: new Date(),
},
where: {
id: eventId,
},
});
return new Response(null, {
status: 204,

View File

@@ -6,12 +6,11 @@ export async function onRequest(context: RequestContext) {
// Skip checks for the by-id endpoint
if (pathSegments.length <= 5) return await context.next();
const eventInfo = await context.env.D1.prepare(
"SELECT * FROM events WHERE id = ?;",
)
.bind(context.params.id)
.first();
const eventInfo = await context.data.prisma.event.findUnique({
where: {
id: context.params.id as string,
},
});
if (!eventInfo) return jsonError("This event does not exist.", 404);
if (![1 << 4, 1 << 12].find((p) => context.data.current_user.permissions & p))

View File

@@ -10,7 +10,7 @@ export async function onRequestPost(context: RequestContext) {
try {
await D1.batch([
D1.prepare(
"UPDATE events SET reached_minimum_player_count = 1 WHERE id = ?",
"UPDATE events SET reached_minimum_player_count = TRUE WHERE id = ?",
).bind(event.id),
D1.prepare(
"UPDATE et_members SET points = points + 10 WHERE id = ?;",

View File

@@ -5,23 +5,25 @@ export async function onRequestPost(context: RequestContext) {
const { event } = context.data;
try {
const completionTimeRow = await D1.prepare(
"SELECT performed_at FROM events WHERE id = ?;",
)
.bind(event.id)
.first();
const completionTimeRow = await context.data.prisma.event.findUnique({
select: {
performed_at: true,
},
where: {
id: event.id,
},
});
if (typeof completionTimeRow?.performed_at === "number")
if (completionTimeRow?.performed_at instanceof Date)
return jsonError(
"The event is already marked as complete or forgotten",
400,
);
await D1.batch([
D1.prepare("UPDATE events SET performed_at = ? WHERE id = ?;").bind(
Date.now(),
event.id,
),
D1.prepare(
"UPDATE events SET performed_at = CURRENT_TIMESTAMP WHERE id = ?;",
).bind(event.id),
D1.prepare(
"UPDATE et_members SET points = points + 10 WHERE id = ?;",
).bind(event.created_by),

View File

@@ -5,12 +5,15 @@ export async function onRequestPost(context: RequestContext) {
if (typeof context.data.body.approved !== "boolean")
return jsonError("Decision type must be a boolean", 400);
const updatedEvent: Record<string, number | string> | null =
await context.env.D1.prepare(
"UPDATE events SET approved = ?, pending = 0 WHERE id = ? RETURNING created_by, day, month, year;",
)
.bind(Number(context.data.body.approved), context.data.event.id)
.first();
const updatedEvent = await context.data.prisma.event.update({
data: {
approved: context.data.body.approved,
pending: false,
},
where: {
id: context.data.event.id,
},
});
if (!updatedEvent) return jsonError("This event does not exist", 404);
@@ -19,10 +22,14 @@ export async function onRequestPost(context: RequestContext) {
type: "json",
});
const usernameData: Record<string, string> | null =
await context.env.D1.prepare("SELECT name FROM et_members WHERE id = ?;")
.bind(updatedEvent.created_by)
.first();
const usernameData = await context.data.prisma.etMember.findUnique({
where: {
id: updatedEvent.created_by,
},
select: {
name: true,
},
});
if (emailData && usernameData) {
await sendEmail(

View File

@@ -2,22 +2,25 @@ import { jsonError } from "../../../../common.js";
export async function onRequestPost(context: RequestContext) {
const { D1 } = context.env;
const { event } = context.data;
const { event, prisma } = context.data;
try {
const row = await D1.prepare(
"SELECT performed_at FROM events WHERE id = ?;",
)
.bind(event.id)
.first();
const row = await prisma.event.findUnique({
select: {
performed_at: true,
},
where: {
id: event.id,
},
});
if (typeof row?.performed_at === "number")
if (row?.performed_at instanceof Date)
return jsonError("Event already marked as completed or forgotten", 400);
await D1.batch([
D1.prepare("UPDATE events SET performed_at = 0 WHERE id = ?;").bind(
event.id,
),
D1.prepare(
"UPDATE events SET performed_at = datetime(0, 'unixepoch') WHERE id = ?;",
).bind(event.id),
D1.prepare(
"UPDATE et_members SET points = points - 5 WHERE id = ?;",
).bind(event.created_by),

View File

@@ -14,13 +14,27 @@ export async function onRequestGet(context: RequestContext) {
if (currentYear < year || (currentYear === year && currentMonth < month))
return jsonError("Cannot get events for a time in the future", 400);
const eventRecords = await context.env.D1.prepare(
"SELECT answer, approved, created_by, day, details, month, pending, performed_at, type, year FROM events WHERE month = ? AND year = ? ORDER BY day ASC;",
)
.bind(month, year)
.all();
const eventRecords = await context.data.prisma.event.findMany({
select: {
answer: true,
approved: true,
created_by: true,
day: true,
details: true,
month: true,
pending: true,
performed_at: true,
type: true,
year: true,
},
where: {
month: month,
year: year,
},
orderBy: {
day: "asc",
},
});
if (!eventRecords.success) return jsonError("Failed to retrieve events", 400);
return jsonResponse(JSON.stringify(eventRecords.results));
return jsonResponse(JSON.stringify(eventRecords));
}

View File

@@ -22,11 +22,18 @@ export async function onRequestPost(context: RequestContext) {
return jsonError("Invalid body", 400);
if (
await context.env.D1.prepare(
"SELECT id FROM events WHERE (approved = 1 OR pending = 1) AND day = ? AND month = ? AND type = ? AND year = ?;",
)
.bind(day, currentMonth, type, currentYear)
.first()
await context.data.prisma.event.findFirst({
select: {
id: true,
},
where: {
OR: [{ approved: true }, { pending: true }],
day,
month: currentMonth,
type,
year: currentYear,
},
})
)
return jsonError(
"Event with that type already exists for the specified date",
@@ -44,16 +51,21 @@ export async function onRequestPost(context: RequestContext) {
const weekRange = Math.floor(day / 7);
const existingEventInRange = await context.env.D1.prepare(
"SELECT id FROM events WHERE (approved = 1 OR pending = 1) AND day BETWEEN ? AND ? AND month = ? AND type = 'rotw' AND year = ?;",
)
.bind(
weekRanges[weekRange] - 7,
weekRanges[weekRange],
currentMonth,
currentYear,
)
.first();
const existingEventInRange = await context.data.prisma.event.findFirst({
select: {
id: true,
},
where: {
OR: [{ approved: true }, { pending: true }],
day: {
gte: weekRanges[weekRange] - 7,
lte: weekRanges[weekRange],
},
month: currentMonth,
type: "rotw",
year: currentYear,
},
});
if (existingEventInRange)
return jsonError("There is already an rotw for that week", 400);
@@ -61,23 +73,20 @@ export async function onRequestPost(context: RequestContext) {
const id = `${now.getTime()}${crypto.randomUUID().replaceAll("-", "")}`;
await context.env.D1.prepare(
"INSERT INTO events (answer, approved, created_at, created_by, day, details, id, month, pending, type, year) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
)
.bind(
context.data.body.answer || null,
Number(type === "gamenight"),
now.getTime(),
context.data.current_user.id,
await context.data.prisma.event.create({
data: {
answer: context.data.body.answer || null,
approved: type === "gamenight",
created_by: context.data.current_user.id,
day,
details,
id,
currentMonth,
Number(type !== "gamenight"),
month: currentMonth,
pending: type !== "gamenight",
type,
currentYear,
)
.run();
year: currentYear,
},
});
await fetch(context.env.EVENTS_WEBHOOK, {
body: JSON.stringify({

View File

@@ -12,9 +12,14 @@ export async function onRequestPost(context: RequestContext) {
if (typeof points !== "number") return jsonError("Invalid point count", 400);
await context.env.D1.prepare("UPDATE et_members SET points = ? WHERE id = ?;")
.bind(points, context.params.id)
.run();
await context.data.prisma.etMember.update({
data: {
points,
},
where: {
id: context.params.id as string,
},
});
return new Response(null, {
status: 204,

View File

@@ -1,7 +1,9 @@
export async function onRequestDelete(context: RequestContext) {
await context.env.D1.prepare("DELETE FROM et_strikes WHERE id = ?;")
.bind(context.params.id)
.run();
await context.data.prisma.etStrike.delete({
where: {
id: context.params.id as string,
},
});
return new Response(null, {
status: 204,

View File

@@ -11,9 +11,11 @@ export async function onRequestPost(context: RequestContext) {
user.length > 20 ||
user.length < 17 ||
user.match(/\D/) ||
!(await D1.prepare("SELECT id FROM et_members WHERE id = ?;")
.bind(user)
.first())
!(await context.data.prisma.etMember.findUnique({
where: {
id: user,
},
}))
)
return jsonError("Invalid user id", 400);

View File

@@ -19,9 +19,9 @@ export async function onRequestDelete(context: RequestContext) {
)
return jsonError("Invalid ID", 400);
await context.env.D1.prepare("DELETE FROM et_members WHERE id = ?;")
.bind(id)
.run();
await context.data.prisma.etMember.delete({
where: { id },
});
return new Response(null, {
status: 204,
@@ -40,9 +40,9 @@ export async function onRequestPatch(context: RequestContext) {
if (typeof body.name !== "string" && typeof body.roblox_username !== "string")
return jsonError("At least one property must be provided", 400);
const updates = [];
let queryData: { name?: string; roblox_id?: number } = {};
if (body.name?.length) updates.push({ query: "name = ?", value: body.name });
if (body.name?.length) queryData.name = body.name;
if (typeof body.roblox_username === "string" && body.roblox_username) {
const robloxResolveResp = await fetch(
@@ -66,21 +66,20 @@ export async function onRequestPatch(context: RequestContext) {
if (!data.length)
return jsonError("No Roblox user exists with that name", 400);
updates.push({ query: "roblox_id = ?", value: data[0].id });
queryData.roblox_id = data[0].id;
}
await context.env.D1.prepare(
`UPDATE et_members
SET ${updates.map((u) => u.query).join(", ")}
WHERE id = ?;`,
)
.bind(...updates.map((u) => u.value), body.id)
.run();
await context.data.prisma.etMember.update({
data: queryData,
where: {
id: body.id,
},
});
return jsonResponse(
JSON.stringify({
name: body.name,
roblox_id: updates.find((u) => typeof u.value === "number")?.value,
roblox_id: queryData.roblox_id,
}),
);
}
@@ -100,9 +99,11 @@ export async function onRequestPost(context: RequestContext) {
return jsonError("Invalid name", 400);
if (
await context.env.D1.prepare("SELECT * FROM et_members WHERE id = ?;")
.bind(id)
.first()
await context.data.prisma.etMember.findUnique({
where: {
id,
},
})
)
return jsonError("User is already a member", 400);
@@ -151,14 +152,16 @@ export async function onRequestPost(context: RequestContext) {
roblox_id = data[0].id;
}
const createdAt = Date.now();
const addingUser = context.data.current_user.id;
await context.env.D1.prepare(
"INSERT INTO et_members (created_at, created_by, id, name, roblox_id) VALUES (?, ?, ?, ?, ?);",
)
.bind(createdAt, addingUser, id, name, roblox_id || null)
.run();
await context.data.prisma.etMember.create({
data: {
created_by: addingUser,
id,
name,
roblox_id,
},
});
return new Response(null, {
status: 204,

View File

@@ -7,19 +7,25 @@ export async function onRequestPost(context: RequestContext) {
if (statsReduction && typeof statsReduction !== "number")
return jsonError("Invalid stat reduction", 400);
const appeal: Record<string, any> | null = await context.env.D1.prepare(
"SELECT * FROM game_appeals WHERE id = ?;",
)
.bind(context.params.id)
.first();
const appeal = await context.data.prisma.gameAppeal.findUnique({
select: {
roblox_id: true,
type: true,
},
where: {
id: context.params.id as string,
},
});
if (!appeal) return jsonError("Appeal not found", 400);
const { etag, value: banList } = await getBanList(context);
await context.env.D1.prepare("DELETE FROM game_appeals WHERE id = ?;")
.bind(context.params.id)
.run();
await context.data.prisma.gameAppeal.delete({
where: {
id: context.params.id as string,
},
});
if (!banList[appeal.roblox_id]?.BanType)
return new Response(null, {
@@ -46,18 +52,15 @@ export async function onRequestPost(context: RequestContext) {
};
}
await context.env.D1.prepare(
"INSERT INTO game_mod_logs (action, evidence, executed_at, executor, id, target) VALUES (?, ?, ?, ?, ?, ?);",
)
.bind(
`accept appeal | ${banList[appeal.roblox_id]?.BanType === 2 ? "ban" : appeal.type}`,
`https://carcrushers.cc/mod-queue?id=${context.params.id}&type=gma`,
Date.now(),
context.data.current_user.id,
crypto.randomUUID(),
appeal.roblox_id,
)
.run();
await context.data.prisma.gameModLog.create({
data: {
action: `accept appeal | ${banList[appeal.roblox_id]?.BanType === 2 ? "ban" : appeal.type}`,
evidence: `https://carcrushers.cc/mod-queue?id=${context.params.id}&type=gma`,
executor: context.data.current_user.id,
id: crypto.randomUUID(),
target: appeal.roblox_id,
},
});
await setBanList(context, banList, etag);

View File

@@ -1,19 +1,37 @@
import { jsonError } from "../../../common.js";
import { getBanList } from "../../../roblox-open-cloud.js";
export async function onRequestPost(context: RequestContext) {
const appealId = context.params.id as string;
const appeal = await context.env.D1.prepare(
"SELECT * FROM game_appeals WHERE id = ?;",
)
.bind(appealId)
.first();
const appeal = await context.data.prisma.gameAppeal.findUnique({
select: {
roblox_id: true,
type: true,
},
where: {
id: appealId,
},
});
if (!appeal) return jsonError("Appeal not found", 404);
await context.env.D1.prepare("DELETE FROM game_appeals WHERE id = ?;")
.bind(appealId)
.run();
await context.data.prisma.gameAppeal.delete({
where: {
id: appealId,
},
});
const { value: banList } = await getBanList(context);
await context.data.prisma.gameModLog.create({
data: {
action: `deny appeal | ${banList[appeal.roblox_id]?.BanType === 2 ? "ban" : appeal.type}`,
evidence: `https://carcrushers.cc/mod-queue?id=${context.params.id}&type=gma`,
executor: context.data.current_user.id,
id: crypto.randomUUID(),
target: appeal.roblox_id,
},
});
await context.env.DATA.put(
`gameappealblock_${appeal.roblox_id}`,

View File

@@ -10,11 +10,14 @@ export default async function (
types?: string[];
}> {
if (
await context.env.D1.prepare(
"SELECT * FROM game_appeals WHERE roblox_id = ?;",
)
.bind(user)
.first()
await context.data.prisma.gameAppeal.findFirst({
select: {
id: true,
},
where: {
roblox_id: user,
},
})
)
return {
can_appeal: false,
@@ -47,22 +50,20 @@ export default async function (
).toLocaleString()} to submit another appeal`,
};
const userLogs = await context.env.D1.prepare(
"SELECT action, executed_at FROM game_mod_logs WHERE target = ? ORDER BY executed_at DESC;",
)
.bind(user)
.all();
if (userLogs.error)
return {
error: "Could not determine your eligibility",
};
const userLogs = await context.data.prisma.gameModLog.findMany({
select: {
action: true,
executed_at: true,
},
where: {
target: user,
},
});
// Legacy bans
if (!userLogs.results.length)
return { can_appeal: true, reason: "", types: ["ban"] };
if (!userLogs.length) return { can_appeal: true, reason: "", types: ["ban"] };
const allowedTime = (userLogs.results[0].executed_at as number) + 2592000000;
const allowedTime = new Date(userLogs[0].executed_at).getTime() + 2592000000;
if (Date.now() < allowedTime)
return {
@@ -72,11 +73,7 @@ export default async function (
).toLocaleString()} to submit an appeal`,
};
if (
userLogs.results.find((r: Record<string, any>) =>
r.action.startsWith("accept appeal"),
)
)
if (userLogs.find((r) => r.action.startsWith("accept appeal")))
return {
can_appeal: false,
reason: "We do not accept appeals from repeat offenders",

View File

@@ -52,19 +52,16 @@ export async function onRequestPost(context: RequestContext) {
context.request.headers.get("cf-ray")?.split("-")[0]
}${Date.now()}`;
await context.env.D1.prepare(
"INSERT INTO game_appeals (created_at, id, reason_for_unban, roblox_id, roblox_username, type, what_happened) VALUES (?, ?, ?, ?, ?, ?, ?);",
)
.bind(
Date.now(),
appealId,
reasonForUnban,
id,
username,
await context.data.prisma.gameAppeal.create({
data: {
id: appealId,
reason_for_unban: reasonForUnban,
roblox_id: id,
roblox_username: username,
type,
whatHappened,
)
.run();
what_happened: whatHappened,
},
});
await fetch(context.env.REPORTS_WEBHOOK, {
body: JSON.stringify({

View File

@@ -17,7 +17,7 @@ export async function onRequestGet(context: RequestContext) {
);
if (!robloxUserReq.ok) {
console.log(await robloxUserReq.json());
console.log(await robloxUserReq.text());
return jsonError("Failed to resolve username", 500);
}
@@ -58,13 +58,14 @@ export async function onRequestGet(context: RequestContext) {
} else if (banData.BanType === 2) current_status = "Banned";
const response = {
history: (
await context.env.D1.prepare(
"SELECT * FROM game_mod_logs WHERE target = ? ORDER BY executed_at DESC;",
)
.bind(users[0].id)
.all()
).results,
history: await context.data.prisma.gameModLog.findMany({
orderBy: {
executed_at: "desc",
},
where: {
target: users[0].id,
},
}),
user: {
avatar: thumbnailRequest.ok
? (

View File

@@ -18,18 +18,15 @@ export async function onRequestPost(context: RequestContext) {
if (isNaN(parseInt(user))) return jsonError("Invalid user ID", 400);
await context.env.D1.prepare(
"INSERT INTO game_mod_logs (action, evidence, executed_at, executor, id, target) VALUES (?, ?, ?, ?, ?, ?);",
)
.bind(
"revoke",
ticket_link,
Date.now(),
context.data.current_user.id,
crypto.randomUUID(),
parseInt(user),
)
.run();
await context.data.prisma.gameModLog.create({
data: {
action: "revoke",
evidence: ticket_link,
executor: context.data.current_user.id,
id: crypto.randomUUID(),
target: parseInt(user),
},
});
const { etag, value: banList } = await getBanList(context);

View File

@@ -2,38 +2,50 @@ import { jsonError, jsonResponse } from "../../../common.js";
export async function onRequestDelete(context: RequestContext) {
const noteId = context.params.id as string;
const creatorIdResult: null | Record<string, string> =
await context.env.D1.prepare(
"SELECT created_by FROM game_mod_logs WHERE id = ?;",
)
.bind(noteId)
.first();
const creatorIdResult = await context.data.prisma.gameModNote.findUnique({
select: {
created_by: true,
},
where: {
id: noteId,
},
});
if (creatorIdResult?.created_by !== context.data.current_user.id)
try {
await context.data.prisma.gameModNote.delete({
select: {
id: true,
},
where: {
created_by: context.data.current_user.id,
id: noteId,
},
});
} catch {
return jsonError("Cannot delete notes that are not your own", 403);
await context.env.D1.prepare("DELETE FROM game_mod_logs WHERE id = ?;")
.bind(noteId)
.first();
}
return new Response(null, { status: 204 });
}
export async function onRequestGet(context: RequestContext) {
const noteId = context.params.id as string;
const result = await context.env.D1.prepare(
"SELECT * FROM game_mod_notes WHERE id = ?;",
)
.bind(noteId)
.first();
const result = await context.data.prisma.gameModNote.findUnique({
where: {
id: noteId,
},
});
if (!result) return jsonError("Note not found", 404);
const noteData = structuredClone(result);
let noteData = structuredClone(result);
const gmeEntry: null | { time: number; user: string; name: string } =
await context.env.DATA.get(`gamemod_${result.created_by}`, "json");
if (gmeEntry) noteData.creator_name = gmeEntry.name;
if (gmeEntry)
noteData = Object.defineProperty(noteData, "creator_name", {
value: gmeEntry.name,
});
return jsonResponse(JSON.stringify(noteData));
}

View File

@@ -1,18 +1,22 @@
import { jsonError, jsonResponse } from "../../common.js";
import sendEmail from "../../email.js";
import { sendPushNotification } from "../../gcloud.js";
import { type JsonArray, type JsonObject } from "@prisma/client/runtime/client";
export async function onRequestDelete(context: RequestContext) {
const result = await context.env.D1.prepare(
"SELECT json_extract(user, '*.id') AS uid FROM inactivity_notices WHERE id = ?;",
)
.bind(context.params.id)
.first();
const result = await context.data.prisma.inactivityNotice.findUnique({
select: {
user: true,
},
where: {
id: context.params.id as string,
},
});
if (!result) return jsonError("No inactivity notice with that ID", 404);
if (
result.uid !== context.data.current_user.id &&
(result.user as JsonObject).id !== context.data.current_user.id &&
!(context.data.current_user.permissions & (1 << 0))
)
return jsonError(
@@ -39,26 +43,17 @@ export async function onRequestGet(context: RequestContext) {
)
return jsonError("Forbidden", 403);
const result: Record<
string,
string | number | { [k: string]: string }
> | null = await context.env.D1.prepare(
"SELECT * FROM inactivity_notices WHERE id = ?;",
)
.bind(context.params.id)
.first();
if (!result) return jsonError("Inactivity notice does not exist", 404);
result.decisions = JSON.parse(result.decisions as string);
result.departments = JSON.parse(result.departments as string);
result.user = JSON.parse(result.user as string);
const result = await context.data.prisma.inactivityNotice.findUnique({
where: {
id: context.params.id as string,
},
});
return jsonResponse(JSON.stringify(result));
}
export async function onRequestPost(context: RequestContext) {
const { accepted }: { accepted?: boolean } = context.data.body;
const { accepted }: { accepted?: any } = context.data.body;
if (typeof accepted !== "boolean")
return jsonError("'accepted' must be a boolean", 400);
@@ -77,32 +72,45 @@ export async function onRequestPost(context: RequestContext) {
if (!userAdminDepartments.length)
return jsonError("You are not a manager of any departments", 403);
const requestedNotice: { [k: string]: any } | null =
await context.env.D1.prepare(
"SELECT decisions, departments, user FROM inactivity_notices WHERE id = ?;",
)
.bind(context.params.id)
.first();
const requestedNotice = await context.data.prisma.inactivityNotice.findUnique(
{
select: {
decisions: true,
departments: true,
user: true,
},
where: {
id: context.params.id as string,
},
},
);
if (!requestedNotice)
return jsonError("Inactivity notices does not exist", 404);
const decisions: { [dept: string]: boolean } = JSON.parse(
requestedNotice.decisions,
);
const decisions = requestedNotice.decisions as { [k: string]: boolean };
for (const department of userAdminDepartments) {
if (!JSON.parse(requestedNotice.departments).includes(department)) continue;
if (!(requestedNotice.departments as JsonArray).includes(department))
continue;
decisions[department] = accepted;
}
const applicableDepartments = JSON.parse(requestedNotice.departments).length;
const applicableDepartments = (requestedNotice.departments as JsonArray)
.length;
const user = requestedNotice.user as JsonObject;
await context.env.D1.prepare(
"UPDATE inactivity_notices SET decisions = ?, user = json_remove(user, '$.email') WHERE id = ?;",
)
.bind(JSON.stringify(decisions), context.params.id)
.run();
delete user.email;
await context.data.prisma.inactivityNotice.update({
data: {
decisions,
user,
},
where: {
id: context.params.id as string,
},
});
if (Object.values(decisions).length === applicableDepartments) {
const approved =
@@ -111,11 +119,16 @@ export async function onRequestPost(context: RequestContext) {
const denied =
Object.values(decisions).filter((d) => !d).length !==
applicableDepartments;
const fcmTokenResult: FCMTokenResult | null = await context.env.D1.prepare(
"SELECT token FROM push_notifications WHERE event_id = ? AND event_type = 'inactivity';",
)
.bind(context.params.id)
.first();
const fcmTokenResult =
await context.data.prisma.pushNotification.findUnique({
select: {
token: true,
},
where: {
event_id: context.params.id as string,
event_type: "inactivity",
},
});
if (fcmTokenResult) {
let status = "Approved";
@@ -132,16 +145,19 @@ export async function onRequestPost(context: RequestContext) {
fcmTokenResult.token,
);
await context.env.D1.prepare(
"DELETE FROM push_notifications WHERE event_id = ? AND event_type = 'inactivity';",
).bind(context.params.id);
await context.data.prisma.pushNotification.delete({
where: {
event_id: context.params.id as string,
event_type: "inactivity",
},
});
} else {
await sendEmail(
requestedNotice.user.email,
(requestedNotice.user as JsonObject).email as string,
context.env.MAILGUN_API_KEY,
`Inactivity Request ${approved ? "Approved" : "Denied"}`,
`inactivity_${approved ? "approved" : "denied"}`,
{ username: requestedNotice.user.username },
{ username: (requestedNotice.user as JsonObject).username as string },
);
}
}

View File

@@ -20,31 +20,31 @@ export async function onRequestPost(context: RequestContext) {
(context.request.headers.get("cf-ray") as string).split("-")[0] +
Date.now().toString();
await context.env.D1.prepare(
"INSERT INTO inactivity_notices (created_at, departments, end, hiatus, id, reason, start, user) VALUES (?, ?, ?, ?, ?, ?, ?, ?);",
)
.bind(
Date.now(),
JSON.stringify(departments),
await context.data.prisma.inactivityNotice.create({
data: {
decisions: {},
departments,
end,
typeof hiatus === "boolean" ? Number(hiatus) : 0,
inactivityId,
hiatus,
id: inactivityId,
reason,
start,
JSON.stringify({
user: {
id: context.data.current_user.id,
email: context.data.current_user.email,
username: context.data.current_user.username,
}),
)
.run();
},
},
});
if (typeof senderTokenId === "string") {
await context.env.D1.prepare(
"INSERT INTO push_notifications (created_at, event_id, event_type) VALUES (?, ?, ?);",
)
.bind(Date.now(), inactivityId, "inactivity")
.run();
await context.data.prisma.pushNotification.create({
data: {
event_id: inactivityId,
event_type: "inactivity",
token: senderTokenId,
},
});
}
const departmentsToNotify = [];

View File

@@ -1,20 +1,20 @@
import { jsonError, jsonResponse } from "../../common.js";
import { jsonResponse } from "../../common.js";
export async function onRequestGet(context: RequestContext) {
const { results, success } = await context.env.D1.prepare(
"SELECT approved, created_at, id, open FROM appeals WHERE user = ?;",
)
.bind(context.data.current_user.id)
.all();
if (!success) return jsonError("Unable to retrieve appeals", 500);
return jsonResponse(
JSON.stringify(
results.map((result) => {
result.user = JSON.parse(result.user as string);
return result;
await context.data.prisma.appeal.findMany({
select: {
approved: true,
created_at: true,
id: true,
},
where: {
user: {
path: "id",
equals: context.data.current_user.id,
},
},
}),
),
);

View File

@@ -1,4 +1,8 @@
import { jsonError, jsonResponse } from "../../../../common.js";
import {
type JsonArray,
type JsonObject,
} from "../../../../../generated/prisma/internal/prismaNamespace.js";
export async function onRequestGet(context: RequestContext) {
const { id, type } = context.params;
@@ -6,57 +10,68 @@ export async function onRequestGet(context: RequestContext) {
if (!["appeal", "inactivity", "report"].includes(type as string))
return jsonError("Invalid type", 400);
const tables: { [k: string]: string } = {
appeal: "appeals",
inactivity: "inactivity_notices",
report: "reports",
};
const { prisma } = context.data;
let item;
const data: Record<string, any> | null = await context.env.D1.prepare(
`SELECT *
FROM ${tables[type as string]}
WHERE id = ?;`,
)
.bind(id)
.first();
switch (type as string) {
case "appeal":
item = await prisma.appeal.findUnique({
where: {
id: id as string,
},
});
if (data?.user) data.user = JSON.parse(data.user);
break;
if (!data || data.user?.id !== context.data.current_user.id)
return jsonError("Item does not exist", 404);
case "inactivity":
item = await prisma.inactivityNotice.findUnique({
where: {
id: id as string,
},
});
if (type === "inactivity") {
data.decisions = JSON.parse(data.decisions);
data.departments = JSON.parse(data.departments);
}
break;
case "report":
item = await prisma.report.findUnique({
where: {
id: id as string,
},
});
if (!item) break;
if (type === "report") {
data.attachments = JSON.parse(data.attachments);
data.target_ids = JSON.parse(data.target_ids);
data.target_usernames = JSON.parse(data.target_usernames);
const { AwsClient } = await import("aws4fetch");
const aws = new AwsClient({
accessKeyId: context.env.R2_ACCESS_KEY,
secretAccessKey: context.env.R2_SECRET_KEY,
});
let urls = [];
for (const attachment of data.attachments) {
const { url } = await aws.sign(
let urlPromises = [];
for (const attachment of item.attachments as JsonArray) {
urlPromises.push(
aws.sign(
`https://car-crushers.${context.env.R2_ZONE}.r2.cloudflarestorage.com/${attachment}?X-Amz-Expires=1800`,
{
aws: {
signQuery: true,
},
},
),
);
}
const urls = (await Promise.all(urlPromises)).map((p) => p.url);
item = { ...item, resolved_attachments: urls };
urls.push(url);
break;
default:
break;
}
data.resolved_attachments = urls;
}
if (
!item ||
(item.user as JsonObject | undefined)?.id !== context.data.current_user.id
)
return jsonError("Item does not exist", 404);
return jsonResponse(JSON.stringify(data));
return jsonResponse(JSON.stringify(item));
}

View File

@@ -1,19 +1,22 @@
import { jsonError, jsonResponse } from "../../common.js";
import { jsonResponse } from "../../common.js";
export async function onRequestGet(context: RequestContext) {
const {
results,
success,
}: {
results: { id: string }[];
success: boolean;
} = await context.env.D1.prepare(
"SELECT created_at, id, open, target_usernames FROM reports WHERE json_extract(user, '$.id') = ? ORDER BY created_at LIMIT 50;",
)
.bind(context.data.current_user.id)
.all();
if (!success) return jsonError("Failed to retrieve reports", 500);
return jsonResponse(JSON.stringify(results));
return jsonResponse(
JSON.stringify(
await context.data.prisma.report.findMany({
select: {
created_at: true,
id: true,
open: true,
target_usernames: true,
},
where: {
user: {
path: "id",
equals: context.data.current_user.id,
},
},
}),
),
);
}

View File

@@ -1,68 +1,71 @@
import { jsonError, jsonResponse } from "../../../common.js";
import { type JsonObject } from "../../../../generated/prisma/internal/prismaNamespace.js";
export async function onRequestGet(context: RequestContext) {
const types: {
[k: string]: { permissions: number[]; table: string };
} = {
appeal: {
permissions: [1 << 0, 1 << 11],
table: "appeals",
},
gma: {
permissions: [1 << 5],
table: "game_appeals",
},
inactivity: {
permissions: [1 << 0, 1 << 4, 1 << 6, 1 << 7, 1 << 11],
table: "inactivity_notices",
},
report: {
permissions: [1 << 5],
table: "reports",
},
const types: { [k: string]: number[] } = {
appeal: [1 << 0, 1 << 11],
gma: [1 << 5],
inactivity: [1 << 0, 1 << 4, 1 << 6, 1 << 7, 1 << 11],
report: [1 << 5],
};
const type = context.params.type as string;
const itemId = context.params.id as string;
if (
!types[type]?.permissions.find(
(p) => context.data.current_user.permissions & p,
)
)
if (!types[type]?.find((p) => context.data.current_user.permissions & p))
return jsonError("You cannot use this filter", 403);
const item: Record<string, any> | null = await context.env.D1.prepare(
`SELECT *
FROM ${types[type].table}
WHERE id = ?;`,
)
.bind(itemId)
.first();
let item;
switch (type) {
case "appeal":
item = await context.data.prisma.appeal.findUnique({
where: {
id: itemId,
},
});
if (item) delete (item.user as JsonObject).email;
break;
case "gma":
item = await context.data.prisma.gameAppeal.findUnique({
where: {
id: itemId,
},
});
break;
case "inactivity":
item = await context.data.prisma.inactivityNotice.findUnique({
where: {
id: itemId,
},
});
if (item) delete (item.user as JsonObject).email;
break;
case "report":
item = await context.data.prisma.report.findUnique({
where: {
id: itemId,
},
});
if (item) delete (item.user as JsonObject | null)?.email;
break;
default:
return jsonError("Unknown filter", 400);
}
if (!item) return jsonError("Item not found", 404);
if (type === "report") {
if (await context.env.DATA.get(`reportprocessing_${itemId}`))
return jsonError("Report is processing", 409);
item.attachments = JSON.parse(item.attachments);
item.target_ids = JSON.parse(item.target_ids);
item.target_usernames = JSON.parse(item.target_usernames);
}
if (item.user) {
item.user = JSON.parse(item.user);
delete item.user.email;
}
if (type === "inactivity") {
item.decisions = JSON.parse(item.decisions);
item.departments = JSON.parse(item.departments);
}
return item
? jsonResponse(JSON.stringify(item))
: jsonError("Not found", 404);
return jsonResponse(JSON.stringify(item));
}

View File

@@ -1,7 +1,13 @@
import { jsonError, jsonResponse } from "../../../common.js";
import { InactivityNotice } from "../../../../generated/prisma/client.js";
import {
JsonObject,
raw,
} from "../../../../generated/prisma/internal/prismaNamespace.js";
export async function onRequestGet(context: RequestContext): Promise<any> {
const type = context.params.type as string;
const { prisma } = context.data;
const { searchParams } = new URL(context.request.url);
const before = parseInt(searchParams.get("before") || `${Date.now()}`);
const showClosed = searchParams.get("showClosed") === "true";
@@ -26,73 +32,77 @@ export async function onRequestGet(context: RequestContext): Promise<any> {
if (isNaN(before)) return jsonError("Invalid `before` parameter", 400);
let rows: D1Result<Record<string, any>>;
let rows;
switch (type) {
case "appeal":
rows = await context.env.D1.prepare(
`SELECT *
FROM appeals
WHERE created_at < ?
AND approved ${showClosed ? "IS NOT" : "IS"} NULL
ORDER BY created_at DESC LIMIT 25;`,
)
.bind(before)
.all();
rows.results = rows.results.map((r) => {
r.user = JSON.parse(r.user);
delete r.user.email;
rows = await prisma.appeal.findMany({
orderBy: {
created_at: "desc",
},
take: 25,
where: {
created_at: {
lt: new Date(before),
},
approved: showClosed ? { not: null } : null,
},
});
rows.map((r) => {
delete (r.user as JsonObject).email;
return r;
});
break;
case "gma":
rows = await context.env.D1.prepare(
"SELECT * FROM game_appeals WHERE created_at < ? ORDER BY created_at DESC LIMIT 25;",
)
.bind(before)
.all();
rows = await prisma.gameAppeal.findMany({
orderBy: {
created_at: "desc",
},
take: 25,
where: {
created_at: {
lt: new Date(before),
},
},
});
break;
case "inactivity":
rows = await context.env.D1.prepare(
`SELECT *,
(SELECT COUNT(*) FROM json_each(decisions)) as decision_count
FROM inactivity_notices
WHERE created_at < ?
AND decision_count ${showClosed ? "=" : "!="} json_array_length(departments)`,
)
.bind(before)
.all();
rows = await prisma.$queryRaw<
InactivityNotice[] & { decision_count: number }[]
>`
SELECT *, (SELECT COUNT(*) FROM json_each(decisions)) AS decision_count FROM inactivity_notices WHERE created_at < datetime(${before} / 1000, 'unixepoch') AND decision_count ${showClosed ? raw("=") : raw("!=")} json_array_length(departments);`;
rows.results.map((r) => {
r.decisions = JSON.parse(r.decisions);
r.departments = JSON.parse(r.departments);
r.user = JSON.parse(r.user);
rows.map((r) => {
// These come back as strings when using $queryRaw
r.decisions = JSON.parse(r.decisions as string);
r.departments = JSON.parse(r.departments as string);
r.user = JSON.parse(r.user as string);
delete r.user.email;
delete (r.user as JsonObject).email;
return r;
});
break;
case "report":
rows = await context.env.D1.prepare(
"SELECT * FROM reports WHERE created_at < ? AND open = ? ORDER BY created_at DESC LIMIT 25;",
)
.bind(before, !Number(showClosed))
.all();
rows = await prisma.report.findMany({
orderBy: {
created_at: "desc",
},
take: 25,
where: {
created_at: {
lt: new Date(before),
},
open: !showClosed,
},
});
rows.results = rows.results.map((r) => {
r.attachments = JSON.parse(r.attachments);
r.target_ids = JSON.parse(r.target_ids);
r.target_usernames = JSON.parse(r.target_usernames);
if (r.user) {
r.user = JSON.parse(r.user);
delete r.user.email;
}
rows.map((r) => {
delete (r.user as JsonObject | null)?.email;
return r;
});
@@ -103,5 +113,5 @@ export async function onRequestGet(context: RequestContext): Promise<any> {
return jsonError("Unknown filter error", 500);
}
return jsonResponse(JSON.stringify(rows.results));
return jsonResponse(JSON.stringify(rows));
}

View File

@@ -4,6 +4,7 @@ import sendEmail from "../../../email.js";
import { sendPushNotification } from "../../../gcloud.js";
export async function onRequestPost(context: RequestContext) {
const { prisma } = context.data;
const reportId = context.params.id as string;
const report: {
[k: string]: any;
@@ -83,12 +84,15 @@ export async function onRequestPost(context: RequestContext) {
await setBanList(context, Object.assign(banList, newActions), etag);
}
const pushNotificationData: Record<string, string> | null =
await context.env.D1.prepare(
"SELECT token FROM push_notifications WHERE event_id = ? AND event_type = 'report';",
)
.bind(reportId)
.first();
const pushNotificationData = await prisma.pushNotification.findUnique({
select: {
token: true,
},
where: {
event_id: reportId,
event_type: "report",
},
});
if (user?.email)
await sendEmail(
@@ -100,26 +104,33 @@ export async function onRequestPost(context: RequestContext) {
username: user.username as string,
},
);
else if (pushNotificationData)
else if (pushNotificationData) {
await sendPushNotification(
context.env,
"Report Processed",
`Your report for ${JSON.parse(report.target_usernames).toString()} has been reviewed.`,
pushNotificationData.token,
);
await prisma.pushNotification.delete({
where: {
event_id: reportId,
event_type: "report",
},
});
}
delete (report.user as { email?: string; id: string; username: string })
?.email;
await context.env.D1.prepare("UPDATE reports SET open = 0 WHERE id = ?;")
.bind(reportId)
.run();
await context.env.D1.prepare(
"DELETE FROM push_notifications WHERE event_id = ? AND event_type = 'report';",
)
.bind(reportId)
.run();
await prisma.report.update({
data: {
open: false,
user: report.user,
},
where: {
id: reportId,
},
});
return new Response(null, {
status: 204,

View File

@@ -17,11 +17,10 @@ export async function onRequestPost(context: RequestContext) {
await context.env.DATA.delete(`reportprocessing_${id}`);
const value = await context.env.D1.prepare(
"SELECT id FROM reports WHERE id = ?;",
)
.bind(id)
.first();
const value = await context.data.prisma.report.findUnique({
select: { id: true },
where: { id },
});
if (!value) return jsonError("Report is missing", 500);

View File

@@ -15,26 +15,27 @@ export async function onRequestPost(context: RequestContext) {
)
return jsonError("No processing report with that ID found", 404);
const data: Record<string, any> | null = await context.env.D1.prepare(
"SELECT attachments FROM reports WHERE id = ?;",
)
.bind(id)
.first();
const data = await context.data.prisma.report.findUnique({
select: {
attachments: true,
},
where: {
id,
},
});
if (!data) return jsonError("No report with that ID found", 404);
data.attachments = JSON.parse(data.attachments);
const accessToken = await GetAccessToken(context.env);
const attachmentDeletePromises = [];
const existingAttachments = [...data.attachments];
const existingAttachments = [...(data.attachments as string[])];
for (const attachment of existingAttachments) {
if (!attachment.startsWith("t/")) data.attachments.push(`t/${attachment}`);
else data.attachments.push(attachment.replace("t/", ""));
for (let i = 0; i < existingAttachments.length; i++) {
if (!existingAttachments[i].startsWith("t/"))
existingAttachments[i] = existingAttachments[i].replace("t/", "");
}
for (const attachment of data.attachments)
for (const attachment of existingAttachments)
attachmentDeletePromises.push(
fetch(
`https://storage.googleapis.com/storage/v1/b/portal-carcrushers-cc/o/${encodeURIComponent(
@@ -50,9 +51,11 @@ export async function onRequestPost(context: RequestContext) {
);
await Promise.allSettled(attachmentDeletePromises);
await context.env.D1.prepare("DELETE FROM reports WHERE id = ?;")
.bind(id)
.run();
await context.data.prisma.report.delete({
where: {
id,
},
});
return new Response(null, {
status: 204,

View File

@@ -225,26 +225,31 @@ export async function onRequestPost(context: RequestContext) {
attachments.push(new URL(urlResult.value).pathname.replace(/^\/?t?\//, ""));
}
await context.env.D1.prepare(
"INSERT INTO reports (attachments, created_at, id, open, target_ids, target_usernames, type, user) VALUES (?, ?, ?, 1, ?, ?, ?, ?);",
)
.bind(
JSON.stringify(attachments),
Date.now(),
reportId,
JSON.stringify(metaIDs),
JSON.stringify(metaNames),
submissionType,
currentUser ? JSON.stringify(currentUser) : null,
)
.run();
await context.data.prisma.report.create({
data: {
attachments,
id: reportId,
target_ids: metaIDs,
target_usernames: metaNames,
type: submissionType,
user: currentUser
? {
email: currentUser.email,
id: currentUser.id,
username: currentUser.username,
}
: undefined,
},
});
if (typeof senderTokenId === "string")
await context.env.D1.prepare(
"INSERT INTO push_notifications (created_at, event_id, event_type, token) VALUES (?, ?, ?, ?);",
)
.bind(Date.now(), reportId, "report", senderTokenId)
.run();
await context.data.prisma.pushNotification.create({
data: {
event_id: reportId,
event_type: reportId,
token: senderTokenId,
},
});
return jsonResponse(
JSON.stringify({ id: reportId, upload_urls: uploadUrls }),

View File

@@ -3,13 +3,12 @@ import { jsonError } from "../../common.js";
export async function onRequestDelete(context: RequestContext) {
const path = decodeURIComponent(context.params.id as string);
if (typeof path !== "string") return jsonError("Invalid path", 400);
await context.env.D1.prepare(
"DELETE FROM short_links WHERE path = ? AND user = ?;",
)
.bind(path, context.data.current_user.id)
.run();
await context.data.prisma.shortLink.delete({
where: {
path,
user: context.data.current_user.id,
},
});
return new Response(null, {
status: 204,
@@ -39,15 +38,15 @@ export async function onRequestPatch(context: RequestContext) {
if (path.length > 256) return jsonError("Path is too long", 400);
await context.env.D1.prepare(
"UPDATE short_links SET path = ? WHERE path = ? AND user = ?;",
)
.bind(
await context.data.prisma.shortLink.update({
data: {
path,
decodeURIComponent(context.params.id as string),
context.data.current_user.id,
)
.run();
},
where: {
path: decodeURIComponent(context.params.id as string),
user: context.data.current_user.id,
},
});
return new Response(null, {
status: 204,

View File

@@ -1,11 +1,18 @@
import { jsonResponse } from "../../common.js";
export async function onRequestGet(context: RequestContext) {
const { results } = await context.env.D1.prepare(
"SELECT created_at, destination, path FROM short_links WHERE user = ?;",
)
.bind(context.data.current_user.id)
.all();
return jsonResponse(JSON.stringify(results));
return jsonResponse(
JSON.stringify(
await context.data.prisma.shortLink.findMany({
select: {
created_at: true,
destination: true,
path: true,
},
where: {
user: context.data.current_user.id,
},
}),
),
);
}

View File

@@ -6,11 +6,14 @@ export async function onRequestPost(context: RequestContext) {
if (typeof path !== "string" || path.length > 256)
return jsonError("Invalid path", 400);
const result = await context.env.D1.prepare(
"SELECT path FROM short_links WHERE path = ?;",
)
.bind(path)
.first();
const result = await context.data.prisma.shortLink.findUnique({
select: {
path: true,
},
where: {
path,
},
});
if (result)
return jsonError(
@@ -78,11 +81,13 @@ export async function onRequestPost(context: RequestContext) {
400,
);
await context.env.D1.prepare(
"INSERT INTO short_links (created_at, destination, path, user) VALUES (?, ?, ?, ?);",
)
.bind(Date.now(), destination, path, context.data.current_user.id)
.run();
await context.data.prisma.shortLink.create({
data: {
destination,
path,
user: context.data.current_user.id,
},
});
return new Response(null, {
status: 204,

View File

@@ -9,11 +9,10 @@ export default async function (
if (!roles) permissions |= 1 << 1;
if (roles?.includes("593209890949038082")) permissions |= 1 << 2; // Discord Moderator
if (
Boolean(
await context.env.D1.prepare("SELECT * FROM et_members WHERE id = ?;")
.bind(userid)
.first(),
)
await context.data.prisma.etMember.findUnique({
select: { id: true },
where: { id: userid },
})
)
permissions |= 1 << 3; // Events Team
if (roles?.includes("607594065952899072")) permissions |= 1 << 4; // Events Manager

View File

@@ -1,6 +1,9 @@
const DATASTORE_URL =
"https://apis.roblox.com/cloud/v2/universes/274816972/data-stores/BanData/entries/CloudBanList";
const MESSAGING_SERVICE_URL =
"https://apis.roblox.com/cloud/v2/universes/274816972:publishMessage";
const SAVE_DATA_URL =
"https://apis.roblox.com/cloud/v2/universes/274816972/data-stores/RealData/entries";
@@ -31,6 +34,28 @@ export async function getBanList(context: RequestContext) {
};
}
export async function publishMessage(
context: RequestContext,
topic: string,
message: string,
) {
const response = await fetch(MESSAGING_SERVICE_URL, {
body: JSON.stringify({
message,
topic,
}),
headers: {
"Content-Type": "application/json",
"x-api-key": context.env.ROBLOX_OPENCLOUD_KEY,
},
method: "POST",
});
if (!response.ok) {
throw new Error("Failed to publish message\n" + (await response.text()));
}
}
export async function getSaveData(
context: RequestContext,
user: number,

View File

@@ -70,10 +70,6 @@ button:focus-visible {
padding: 2em;
}
::file-selector-button {
display: none;
}
.desktop-nav {
visibility: visible;
}

8
index.d.ts vendored
View File

@@ -1,3 +1,5 @@
import { type PrismaClient } from "./generated/prisma/client.js";
declare global {
module "*.css";
@@ -17,7 +19,11 @@ declare global {
token: string;
};
type RequestContext = EventContext<Env, string, { [k: string]: any }>;
type RequestContext = EventContext<
Env,
string,
{ prisma: PrismaClient; [k: string]: any }
>;
interface AppealCardProps {
approved: number | null;

View File

@@ -0,0 +1,186 @@
-- CreateTable
CREATE TABLE "appeals" (
"approved" BOOLEAN,
"ban_reason" TEXT NOT NULL,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"id" TEXT NOT NULL PRIMARY KEY,
"learned" TEXT NOT NULL,
"reason_for_unban" TEXT NOT NULL,
"user" JSONB NOT NULL
);
-- CreateTable
CREATE TABLE "appeal_bans" (
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"created_by" TEXT NOT NULL,
"user" TEXT NOT NULL PRIMARY KEY
);
-- CreateTable
CREATE TABLE "et_members" (
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"created_by" TEXT NOT NULL,
"id" TEXT NOT NULL PRIMARY KEY,
"is_management" BOOLEAN NOT NULL DEFAULT false,
"name" TEXT NOT NULL,
"points" INTEGER NOT NULL DEFAULT 0,
"roblox_id" INTEGER
);
-- CreateTable
CREATE TABLE "et_strikes" (
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"created_by" TEXT NOT NULL,
"id" TEXT NOT NULL PRIMARY KEY,
"reason" TEXT NOT NULL,
"user" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "events" (
"answer" TEXT,
"answered_at" DATETIME,
"approved" BOOLEAN NOT NULL DEFAULT false,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"created_by" TEXT NOT NULL,
"day" INTEGER NOT NULL,
"details" TEXT NOT NULL,
"id" TEXT NOT NULL PRIMARY KEY,
"month" INTEGER NOT NULL,
"pending" BOOLEAN NOT NULL DEFAULT true,
"performed_at" DATETIME,
"reached_minimum_player_count" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT NOT NULL,
"year" INTEGER NOT NULL
);
-- CreateTable
CREATE TABLE "game_appeals" (
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"id" TEXT NOT NULL PRIMARY KEY,
"reason_for_unban" TEXT NOT NULL,
"roblox_id" INTEGER NOT NULL,
"roblox_username" TEXT NOT NULL,
"type" TEXT NOT NULL,
"what_happened" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "game_mod_logs" (
"action" TEXT NOT NULL,
"evidence" TEXT NOT NULL,
"executed_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"executor" TEXT NOT NULL,
"id" TEXT NOT NULL PRIMARY KEY,
"target" INTEGER NOT NULL
);
-- CreateTable
CREATE TABLE "game_mod_notes" (
"content" TEXT NOT NULL,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"created_by" TEXT NOT NULL,
"id" TEXT NOT NULL PRIMARY KEY,
"target" INTEGER NOT NULL
);
-- CreateTable
CREATE TABLE "inactivity_notices" (
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"decisions" JSONB NOT NULL,
"departments" JSONB NOT NULL DEFAULT [],
"end" TEXT NOT NULL,
"hiatus" BOOLEAN NOT NULL DEFAULT false,
"id" TEXT NOT NULL PRIMARY KEY,
"reason" TEXT NOT NULL,
"start" TEXT NOT NULL,
"user" JSONB NOT NULL
);
-- CreateTable
CREATE TABLE "push_notifications" (
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"event_id" TEXT NOT NULL,
"event_type" TEXT NOT NULL,
"token" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "reports" (
"attachments" JSONB NOT NULL DEFAULT [],
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"id" TEXT NOT NULL PRIMARY KEY,
"open" BOOLEAN NOT NULL DEFAULT true,
"target_ids" JSONB NOT NULL DEFAULT [],
"target_usernames" JSONB NOT NULL DEFAULT [],
"type" TEXT NOT NULL DEFAULT 'exploit',
"user" JSONB
);
-- CreateTable
CREATE TABLE "short_links" (
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"destination" TEXT NOT NULL,
"path" TEXT NOT NULL PRIMARY KEY,
"user" TEXT NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "appeals_id_key" ON "appeals"("id");
-- CreateIndex
CREATE INDEX "idx_appeals_approved_created_at" ON "appeals"("approved", "created_at");
-- CreateIndex
CREATE UNIQUE INDEX "appeal_bans_user_key" ON "appeal_bans"("user");
-- CreateIndex
CREATE UNIQUE INDEX "et_members_id_key" ON "et_members"("id");
-- CreateIndex
CREATE INDEX "idx_et_members_id_name" ON "et_members"("id", "name");
-- CreateIndex
CREATE UNIQUE INDEX "et_strikes_id_key" ON "et_strikes"("id");
-- CreateIndex
CREATE UNIQUE INDEX "events_id_key" ON "events"("id");
-- CreateIndex
CREATE INDEX "idx_events_month_year" ON "events"("month", "year");
-- CreateIndex
CREATE UNIQUE INDEX "game_appeals_id_key" ON "game_appeals"("id");
-- CreateIndex
CREATE INDEX "idx_game_appeals_created_at" ON "game_appeals"("created_at");
-- CreateIndex
CREATE UNIQUE INDEX "game_mod_logs_id_key" ON "game_mod_logs"("id");
-- CreateIndex
CREATE INDEX "idx_game_mod_logs_target" ON "game_mod_logs"("target");
-- CreateIndex
CREATE UNIQUE INDEX "game_mod_notes_id_key" ON "game_mod_notes"("id");
-- CreateIndex
CREATE INDEX "idx_game_mod_notes_target" ON "game_mod_notes"("target");
-- CreateIndex
CREATE UNIQUE INDEX "inactivity_notices_id_key" ON "inactivity_notices"("id");
-- CreateIndex
CREATE INDEX "idx_inactivity_notices_end_start" ON "inactivity_notices"("end", "start");
-- CreateIndex
CREATE UNIQUE INDEX "push_notifications_event_id_key" ON "push_notifications"("event_id");
-- CreateIndex
CREATE UNIQUE INDEX "reports_id_key" ON "reports"("id");
-- CreateIndex
CREATE INDEX "idx_reports_created_at_open" ON "reports"("created_at", "open");
-- CreateIndex
CREATE UNIQUE INDEX "short_links_path_key" ON "short_links"("path");

View File

@@ -0,0 +1,15 @@
-- CreateTable
CREATE TABLE "data_requests" (
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"id" TEXT NOT NULL PRIMARY KEY,
"originating_user" INTEGER,
"status" TEXT NOT NULL,
"target_user" INTEGER NOT NULL,
"type" TEXT NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "data_requests_id_key" ON "data_requests"("id");
-- CreateIndex
CREATE INDEX "idx_data_requests_created_at" ON "data_requests"("created_at");

1804
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,18 +7,20 @@
"build": "remix build --sourcemap",
"check-format": "prettier -c .",
"format": "prettier -wc .",
"publish": "remix build --sourcemap && wrangler pages deploy public"
"publish": "remix build --sourcemap && wrangler pages deploy --upload-source-maps public"
},
"dependencies": {
"@chakra-ui/react": "^2.10.9",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fontsource-variable/plus-jakarta-sans": "^5.2.8",
"@prisma/adapter-d1": "^7.8.0",
"@prisma/client": "^7.8.0",
"@remix-run/cloudflare": "^2.17.4",
"@remix-run/cloudflare-pages": "^2.17.4",
"@remix-run/react": "^2.17.4",
"@sentry/cloudflare": "^10.46.0",
"@sentry/remix": "^10.46.0",
"@sentry/cloudflare": "^10.52.0",
"@sentry/remix": "^10.52.0",
"aws4fetch": "^1.0.20",
"dayjs": "^1.11.20",
"framer-motion": "^12.38.0",
@@ -28,16 +30,17 @@
},
"devDependencies": {
"@remix-run/dev": "^2.17.4",
"@types/node": "^24.12.0",
"@types/node": "^24.12.3",
"@types/react": "^18.3.28",
"@types/react-big-calendar": "^1.16.3",
"@types/react-dom": "^18.3.7",
"dotenv": "^17.3.1",
"prettier": "^3.8.1",
"dotenv": "^17.4.1",
"prettier": "^3.8.3",
"prisma": "^7.8.0",
"typescript": "^5.9.3"
},
"overrides": {
"@cloudflare/workers-types": "^4.20260329.1"
"@cloudflare/workers-types": "^4.20260511.1"
},
"prettier": {
"endOfLine": "auto"

12
prisma.config.ts Normal file
View File

@@ -0,0 +1,12 @@
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env["DATABASE_URL"],
},
});

168
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,168 @@
generator client {
provider = "prisma-client"
output = "../generated/prisma"
runtime = "cloudflare"
}
datasource db {
provider = "sqlite"
}
model Appeal {
approved Boolean?
ban_reason String
created_at DateTime @default(now())
id String @id @unique
learned String
reason_for_unban String
user Json
@@index([approved, created_at], name: "idx_appeals_approved_created_at")
@@map("appeals")
}
model AppealBan {
created_at DateTime @default(now())
created_by String
user String @id @unique
@@map("appeal_bans")
}
model DataRequest {
created_at DateTime @default(now())
id String @id @unique
originating_user Int?
status String
target_user Int
type String
@@index([created_at], name: "idx_data_requests_created_at")
@@map("data_requests")
}
model EtMember {
created_at DateTime @default(now())
created_by String
id String @id @unique
is_management Boolean @default(false)
name String
points Int @default(0)
roblox_id Int?
@@index([id, name], name: "idx_et_members_id_name")
@@map("et_members")
}
model EtStrike {
created_at DateTime @default(now())
created_by String
id String @id @unique
reason String
user String
@@map("et_strikes")
}
model Event {
answer String?
answered_at DateTime?
approved Boolean @default(false)
created_at DateTime @default(now())
created_by String
day Int
details String
id String @id @unique
month Int
pending Boolean @default(true)
performed_at DateTime?
reached_minimum_player_count Boolean @default(false)
type String
year Int
@@index([month, year], name: "idx_events_month_year")
@@map("events")
}
model GameAppeal {
created_at DateTime @default(now())
id String @id @unique
reason_for_unban String
roblox_id Int
roblox_username String
type String
what_happened String
@@index([created_at], name: "idx_game_appeals_created_at")
@@map("game_appeals")
}
model GameModLog {
action String
evidence String
executed_at DateTime @default(now())
executor String
id String @id @unique
target Int
@@index([target], name: "idx_game_mod_logs_target")
@@map("game_mod_logs")
}
model GameModNote {
content String
created_at DateTime @default(now())
created_by String
id String @id @unique
target Int
@@index([target], name: "idx_game_mod_notes_target")
@@map("game_mod_notes")
}
model InactivityNotice {
created_at DateTime @default(now())
decisions Json
departments Json @default("[]")
end String
hiatus Boolean @default(false)
id String @id @unique
reason String
start String
user Json
@@index([end, start], name: "idx_inactivity_notices_end_start")
@@map("inactivity_notices")
}
model PushNotification {
created_at DateTime @default(now())
event_id String @unique
event_type String
token String
@@map("push_notifications")
}
model Report {
attachments Json @default("[]")
created_at DateTime @default(now())
id String @id @unique
open Boolean @default(true)
target_ids Json @default("[]")
target_usernames Json @default("[]")
type String @default("exploit")
user Json?
@@index([created_at, open], name: "idx_reports_created_at_open")
@@map("reports")
}
model ShortLink {
created_at DateTime @default(now())
destination String
path String @id @unique
user String
@@map("short_links")
}