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 - name: Install Dependencies
run: npm ci --include=dev run: npm ci --include=dev
- name: Generate Prisma Types
run: npx prisma generate
- name: Check Formatting - name: Check Formatting
run: npm run check-format run: npm run check-format
@@ -53,7 +56,7 @@ jobs:
}' }'
- name: Deploy - 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: Sentry-Release:
name: Create Sentry Release name: Create Sentry Release

4
.gitignore vendored
View File

@@ -37,3 +37,7 @@ public/build
# Wrangler data # Wrangler data
.wrangler .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, ChakraProvider,
Container, Container,
cookieStorageManagerSSR, cookieStorageManagerSSR,
Flex,
Heading, Heading,
Link, Link,
Spacer,
Text, Text,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { ClientStyleContext, ServerStyleContext } from "./context.js"; import { ClientStyleContext, ServerStyleContext } from "./context.js";
@@ -94,6 +96,42 @@ export function ErrorBoundary() {
</DocumentWrapper> </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: default:
captureRemixErrorBoundaryError(useRouteError()); captureRemixErrorBoundaryError(useRouteError());
return ( return (
@@ -128,6 +166,11 @@ export async function loader({
}: { }: {
context: RequestContext; context: RequestContext;
}): Promise<{ [k: string]: any }> { }): Promise<{ [k: string]: any }> {
if (context.data.mx)
throw new Response(null, {
status: 503,
});
let data: { [k: string]: string } = {}; let data: { [k: string]: string } = {};
if (context.env.COMMIT_SHA) data.commit_sha = context.env.COMMIT_SHA; 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(disabled) &&
!Boolean(await dataKV.get(`blockedappeal_${currentUser.id}`)) && !Boolean(await dataKV.get(`blockedappeal_${currentUser.id}`)) &&
!Boolean( !Boolean(
await context.env.D1.prepare( await context.data.prisma.appeal.findFirst({
"SELECT * FROM appeals WHERE approved IS NULL AND json_extract(user, '$.id') = ? LIMIT 1;", select: {
) id: true,
.bind(currentUser.id) },
.first(), where: {
approved: null,
user: {
path: "id",
equals: currentUser.id,
},
},
}),
), ),
can_toggle: can_toggle:
currentUser.permissions & (1 << 0) || currentUser.permissions & (1 << 11), currentUser.permissions & (1 << 0) || currentUser.permissions & (1 << 11),

View File

@@ -44,6 +44,7 @@ export default function () {
const [eventType, setEventType] = useState(""); const [eventType, setEventType] = useState("");
const [riddleAnswer, setRiddleAnswer] = useState(""); const [riddleAnswer, setRiddleAnswer] = useState("");
const [submitSuccess, setSubmitSuccess] = useState(false); const [submitSuccess, setSubmitSuccess] = useState(false);
const [disableSubmit, setDisableSubmit] = useState(false);
useEffect(() => { useEffect(() => {
setDatePickerMin(`${new Date().toISOString().split("T").at(0)}`); setDatePickerMin(`${new Date().toISOString().split("T").at(0)}`);
@@ -53,6 +54,7 @@ export default function () {
}, []); }, []);
async function submit() { async function submit() {
setDisableSubmit(true);
let eventResp: Response; let eventResp: Response;
try { try {
@@ -69,6 +71,7 @@ export default function () {
method: "POST", method: "POST",
}); });
} catch { } catch {
setDisableSubmit(false);
toast({ toast({
description: "Please check your internet and try again", description: "Please check your internet and try again",
isClosable: true, isClosable: true,
@@ -86,6 +89,7 @@ export default function () {
errorMessage = ((await eventResp.json()) as { error: string }).error; errorMessage = ((await eventResp.json()) as { error: string }).error;
} catch {} } catch {}
setDisableSubmit(false);
toast({ toast({
description: errorMessage, description: errorMessage,
isClosable: true, isClosable: true,
@@ -150,7 +154,11 @@ export default function () {
onChange={(e) => setRiddleAnswer(e.target.value)} onChange={(e) => setRiddleAnswer(e.target.value)}
placeholder="Riddle answer" placeholder="Riddle answer"
/> />
<Button mt="16px" onClick={async () => await submit()}> <Button
disabled={disableSubmit}
mt="16px"
onClick={async () => await submit()}
>
Book Book
</Button> </Button>
</Container> </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, status: 403,
}); });
const etData = await context.env.D1.prepare( const etData = await context.data.prisma.etMember.findMany({
"SELECT id, name, points, roblox_id FROM et_members;", select: {
).all(); id: true,
name: true,
if (etData.error) points: true,
throw new Response(null, { roblox_id: true,
status: 500, },
}); });
const members = etData.results as { [k: string]: any }[]; return { members: etData };
return { members } as {
members: { [k: string]: any }[];
};
} }
export default function () { export default function () {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
import getPermissions from "./permissions.js"; import getPermissions from "./permissions.js";
import { jsonError } from "./common.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"; import * as Sentry from "@sentry/cloudflare";
async function constructHTML(context: RequestContext) { async function constructHTML(context: RequestContext) {
@@ -28,6 +30,27 @@ async function generateTokenHash(token: string) {
.replace(/=/g, ""); .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) { async function refreshAuth(context: RequestContext) {
const { current_user: currentUser } = context.data; const { current_user: currentUser } = context.data;
@@ -382,14 +405,20 @@ async function setTheme(context: RequestContext) {
export const onRequest = [ export const onRequest = [
Sentry.sentryPagesPlugin((context: RequestContext) => ({ Sentry.sentryPagesPlugin((context: RequestContext) => ({
beforeSend(event) {
delete event.request?.cookies;
return event;
},
dsn: context.env.FUNCTIONS_DSN, dsn: context.env.FUNCTIONS_DSN,
release: context.env.COMMIT_SHA, release: context.env.COMMIT_SHA,
sendDefaultPii: true, sendDefaultPii: true,
})), })),
mxAndBypassCheck,
setAuth, setAuth,
refreshAuth, refreshAuth,
setTheme, setTheme,
constructHTML, constructHTML,
setBody, setBody,
initializePrisma,
setHeaders, setHeaders,
]; ];

View File

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

View File

@@ -13,11 +13,12 @@ export async function onRequestPost(context: RequestContext) {
fcm_token, fcm_token,
); );
await context.env.D1.prepare( await context.data.prisma.pushNotification.delete({
"DELETE FROM push_notifications WHERE event_id = ? AND event_type = 'appeal';", where: {
) event_id: appeal.id,
.bind(appeal.id) event_type: "appeal",
.run(); },
});
} else { } else {
const emailResponse = await sendEmail( const emailResponse = await sendEmail(
appeal.user.email, appeal.user.email,
@@ -37,11 +38,8 @@ export async function onRequestPost(context: RequestContext) {
const { current_user: currentUser } = context.data; const { current_user: currentUser } = context.data;
await context.env.D1.prepare( await context.data.prisma
"UPDATE appeals SET approved = 1, user = json_remove(user, '$.email') WHERE id = ?;", .$executeRaw`UPDATE appeals SET approved = TRUE, user = json_remove(user, '$.id') WHERE id = ${appeal.id};`;
)
.bind(context.params.id)
.run();
await fetch( await fetch(
`https://discord.com/api/v10/guilds/242263977986359297/bans/${appeal.user.id}`, `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) if (targetId.search(/^\d{16.19}$/) === -1)
return jsonError("Invalid target id", 400); return jsonError("Invalid target id", 400);
await context.env.D1.prepare("DELETE FROM appeal_bans WHERE user = ?;") await context.data.prisma.appealBan.delete({
.bind(targetId) where: {
.run(); user: targetId,
},
});
const { current_user: currentUser } = context.data; const { current_user: currentUser } = context.data;
@@ -46,11 +48,12 @@ export async function onRequestPost(context: RequestContext) {
if (targetId.search(/^\d{16,19}$/) === -1) if (targetId.search(/^\d{16,19}$/) === -1)
return jsonError("Invalid target id", 400); return jsonError("Invalid target id", 400);
await context.env.D1.prepare( await context.data.prisma.appealBan.create({
"INSERT INTO appeal_bans (created_at, created_by, user) VALUES (?, ?, ?);", data: {
) created_by: context.data.current_user.id,
.bind(Date.now(), context.data.current_user.id, targetId) user: targetId,
.run(); },
});
await fetch(context.env.APPEALS_WEBHOOK, { await fetch(context.env.APPEALS_WEBHOOK, {
body: JSON.stringify({ body: JSON.stringify({

View File

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

View File

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

View File

@@ -6,11 +6,12 @@ export async function onRequestGet(context: RequestContext) {
if ( if (
!currentUser.email || !currentUser.email ||
(await context.env.DATA.get("appeal_disabled")) || (await context.env.DATA.get("appeal_disabled")) ||
(await context.env.D1.prepare( (await context.data.prisma.appeal.findFirst({
"SELECT id FROM appeals WHERE open = 1 AND user = ?;", where: {
) approved: null,
.bind(currentUser.id) user: currentUser.id,
.first()) || },
})) ||
(await context.env.DATA.get(`blockedappeal_${currentUser.id}`)) (await context.env.DATA.get(`blockedappeal_${currentUser.id}`))
) )
return jsonResponse('{"can_appeal":false}'); return jsonResponse('{"can_appeal":false}');
@@ -47,18 +48,24 @@ export async function onRequestPost(context: RequestContext) {
if ( if (
existingBlockedAppeal || existingBlockedAppeal ||
(await context.env.D1.prepare( (await context.data.prisma.appeal.findFirst({
"SELECT approved FROM appeals WHERE approved IS NULL AND json_extract(user, '$.id') = ?;", where: {
) approved: null,
.bind(currentUser.id) user: {
.first()) path: "id",
equals: currentUser.id,
},
},
}))
) )
return jsonError("Appeal already submitted", 403); return jsonError("Appeal already submitted", 403);
if ( if (
await context.env.D1.prepare("SELECT * FROM appeal_bans WHERE user = ?;") await context.data.prisma.appealBan.findUnique({
.bind(currentUser.id) where: {
.first() user: currentUser.id,
},
})
) { ) {
await context.env.DATA.put(`blockedappeal_${currentUser.id}`, "1", { await context.env.DATA.put(`blockedappeal_${currentUser.id}`, "1", {
metadata: { email: currentUser.email }, metadata: { email: currentUser.email },
@@ -73,29 +80,28 @@ export async function onRequestPost(context: RequestContext) {
.randomUUID() .randomUUID()
.replaceAll("-", "")}`; .replaceAll("-", "")}`;
await context.env.D1.prepare( await context.data.prisma.appeal.create({
"INSERT INTO appeals (ban_reason, created_at, id, learned, reason_for_unban, user) VALUES (?, ?, ?, ?, ?, ?);", data: {
) ban_reason: whyBanned,
.bind( id: appealId,
whyBanned,
Date.now(),
appealId,
learned, learned,
whyUnban, reason_for_unban: whyUnban,
JSON.stringify({ user: {
email: currentUser.email, email: currentUser.email,
id: currentUser.id, id: currentUser.id,
username: currentUser.username, username: currentUser.username,
}), },
) },
.run(); });
if (typeof senderTokenId === "string") { if (typeof senderTokenId === "string") {
await context.env.D1.prepare( await context.data.prisma.pushNotification.create({
"INSERT INTO push_notifications (created_at, event_id, event_type, token) VALUES (?, ?, 'appeal', ?)", data: {
) event_id: appealId,
.bind(Date.now(), appealId, senderTokenId) event_type: "appeal",
.run(); token: senderTokenId,
},
});
} }
await fetch(context.env.APPEALS_WEBHOOK, { 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) { export async function onRequestDelete(context: RequestContext) {
const eventId = context.params.id as string; const eventId = context.params.id as string;
const eventData: const eventData = await context.data.prisma.event.findUnique({
| ({ select: {
[k: string]: number; created_by: true,
} & { created_by: string }) day: true,
| null = await context.env.D1.prepare( month: true,
"SELECT created_by, day, month, performed_at, year FROM events WHERE id = ?;", performed_at: true,
) year: true,
.bind(eventId) },
.first(); where: {
id: eventId,
},
});
if (!eventData) return jsonError("No event exists with that ID", 404); if (!eventData) return jsonError("No event exists with that ID", 404);
@@ -41,9 +44,11 @@ export async function onRequestDelete(context: RequestContext) {
400, 400,
); );
await context.env.D1.prepare("DELETE FROM events WHERE id = ?;") await context.data.prisma.event.delete({
.bind(eventId) where: {
.run(); id: eventId,
},
});
return new Response(null, { return new Response(null, {
status: 204, status: 204,
@@ -53,13 +58,18 @@ export async function onRequestDelete(context: RequestContext) {
export async function onRequestPatch(context: RequestContext) { export async function onRequestPatch(context: RequestContext) {
const eventId = context.params.id as string; const eventId = context.params.id as string;
const { body } = context.data; const { body } = context.data;
const eventData: const eventData = await context.data.prisma.event.findUnique({
| ({ [k: string]: number } & { created_by: string; type: string }) select: {
| null = await context.env.D1.prepare( created_by: true,
"SELECT created_by, day, month, type, year FROM events WHERE id = ?;", day: true,
) month: true,
.bind(eventId) type: true,
.first(); year: true,
},
where: {
id: eventId,
},
});
if (!eventData) return jsonError("No event exists with that ID", 404); 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" || typeof body.day !== "number" ||
body.day > date.getUTCDate() || body.day > date.getUTCDate() ||
body.day < 1 || body.day < 1 ||
// Check for non-integers // Check for nonintegers
Math.floor(body.day) !== body.day || Math.floor(body.day) !== body.day ||
currentDay >= body.day currentDay >= body.day
) )
@@ -107,26 +117,38 @@ export async function onRequestPatch(context: RequestContext) {
4: 35, 4: 35,
}; };
const weekRange = Math.floor(body.day / 7); const weekRange = Math.floor(body.day / 7);
const matchingROTW = await context.data.prisma.event.findMany({
const matchingROTW = await context.env.D1.prepare( select: {
"SELECT id FROM events WHERE (approved = 1 OR pending = 1) AND day BETWEEN ? AND ? AND month = ? AND type = 'rotw' AND year = ?;", id: true,
) },
.bind( where: {
weekRanges[weekRange] - 7, OR: [{ approved: true }, { pending: true }],
weekRanges[weekRange], day: {
eventData.month, gte: weekRanges[weekRange] - 7,
eventData.year, lte: weekRanges[weekRange],
) },
.first(); month: eventData.month,
year: eventData.year,
},
});
if (matchingROTW) if (matchingROTW)
return jsonError("There is already an ROTW scheduled for that week", 400); return jsonError("There is already an ROTW scheduled for that week", 400);
} else { } else {
const matchingEvent = await context.env.D1.prepare( const matchingEvent = await context.data.prisma.event.findMany({
"SELECT id FROM events WHERE (approved = 1 OR pending = 1) AND day = ? AND month = ? AND type = ? AND year = ?;", select: {
) id: true,
.bind(body.day, eventData.month, eventData.type, eventData.year) },
.first(); where: {
OR: [{ approved: true }, { pending: true }],
AND: [
{ day: body.day },
{ month: eventData.month },
{ type: eventData.type },
{ year: eventData.year },
],
},
});
if (matchingEvent) if (matchingEvent)
return jsonError( return jsonError(
@@ -135,9 +157,14 @@ export async function onRequestPatch(context: RequestContext) {
); );
} }
await context.env.D1.prepare("UPDATE events SET day = ? WHERE id = ?;") await context.data.prisma.event.update({
.bind(body.day, eventId) data: {
.run(); day: body.day,
},
where: {
id: eventId,
},
});
await fetch(context.env.EVENTS_WEBHOOK, { await fetch(context.env.EVENTS_WEBHOOK, {
body: JSON.stringify({ body: JSON.stringify({
@@ -162,22 +189,29 @@ export async function onRequestPatch(context: RequestContext) {
export async function onRequestPost(context: RequestContext) { export async function onRequestPost(context: RequestContext) {
const eventId = context.params.id as string; const eventId = context.params.id as string;
const eventData = await context.env.D1.prepare( const eventData = await context.data.prisma.event.findUnique({
"SELECT approved, performed_at FROM events WHERE id = ?;", select: {
) approved: true,
.bind(eventId) performed_at: true,
.first(); },
where: {
id: eventId,
},
});
if (!eventData) return jsonError("No event exists with that ID", 404); if (!eventData) return jsonError("No event exists with that ID", 404);
if (!eventData.approved) if (!eventData.approved)
return jsonError("Cannot perform unapproved event", 403); return jsonError("Cannot perform unapproved event", 403);
await context.env.D1.prepare( await context.data.prisma.event.update({
"UPDATE events SET performed_at = ? WHERE id = ?;", data: {
) performed_at: new Date(),
.bind(Date.now(), eventId) },
.run(); where: {
id: eventId,
},
});
return new Response(null, { return new Response(null, {
status: 204, status: 204,

View File

@@ -6,12 +6,11 @@ export async function onRequest(context: RequestContext) {
// Skip checks for the by-id endpoint // Skip checks for the by-id endpoint
if (pathSegments.length <= 5) return await context.next(); if (pathSegments.length <= 5) return await context.next();
const eventInfo = await context.env.D1.prepare( const eventInfo = await context.data.prisma.event.findUnique({
"SELECT * FROM events WHERE id = ?;", where: {
) id: context.params.id as string,
.bind(context.params.id) },
.first(); });
if (!eventInfo) return jsonError("This event does not exist.", 404); if (!eventInfo) return jsonError("This event does not exist.", 404);
if (![1 << 4, 1 << 12].find((p) => context.data.current_user.permissions & p)) 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 { try {
await D1.batch([ await D1.batch([
D1.prepare( 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), ).bind(event.id),
D1.prepare( D1.prepare(
"UPDATE et_members SET points = points + 10 WHERE id = ?;", "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; const { event } = context.data;
try { try {
const completionTimeRow = await D1.prepare( const completionTimeRow = await context.data.prisma.event.findUnique({
"SELECT performed_at FROM events WHERE id = ?;", select: {
) performed_at: true,
.bind(event.id) },
.first(); where: {
id: event.id,
},
});
if (typeof completionTimeRow?.performed_at === "number") if (completionTimeRow?.performed_at instanceof Date)
return jsonError( return jsonError(
"The event is already marked as complete or forgotten", "The event is already marked as complete or forgotten",
400, 400,
); );
await D1.batch([ await D1.batch([
D1.prepare("UPDATE events SET performed_at = ? WHERE id = ?;").bind( D1.prepare(
Date.now(), "UPDATE events SET performed_at = CURRENT_TIMESTAMP WHERE id = ?;",
event.id, ).bind(event.id),
),
D1.prepare( D1.prepare(
"UPDATE et_members SET points = points + 10 WHERE id = ?;", "UPDATE et_members SET points = points + 10 WHERE id = ?;",
).bind(event.created_by), ).bind(event.created_by),

View File

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

View File

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

View File

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

View File

@@ -22,11 +22,18 @@ export async function onRequestPost(context: RequestContext) {
return jsonError("Invalid body", 400); return jsonError("Invalid body", 400);
if ( if (
await context.env.D1.prepare( await context.data.prisma.event.findFirst({
"SELECT id FROM events WHERE (approved = 1 OR pending = 1) AND day = ? AND month = ? AND type = ? AND year = ?;", select: {
) id: true,
.bind(day, currentMonth, type, currentYear) },
.first() where: {
OR: [{ approved: true }, { pending: true }],
day,
month: currentMonth,
type,
year: currentYear,
},
})
) )
return jsonError( return jsonError(
"Event with that type already exists for the specified date", "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 weekRange = Math.floor(day / 7);
const existingEventInRange = await context.env.D1.prepare( const existingEventInRange = await context.data.prisma.event.findFirst({
"SELECT id FROM events WHERE (approved = 1 OR pending = 1) AND day BETWEEN ? AND ? AND month = ? AND type = 'rotw' AND year = ?;", select: {
) id: true,
.bind( },
weekRanges[weekRange] - 7, where: {
weekRanges[weekRange], OR: [{ approved: true }, { pending: true }],
currentMonth, day: {
currentYear, gte: weekRanges[weekRange] - 7,
) lte: weekRanges[weekRange],
.first(); },
month: currentMonth,
type: "rotw",
year: currentYear,
},
});
if (existingEventInRange) if (existingEventInRange)
return jsonError("There is already an rotw for that week", 400); 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("-", "")}`; const id = `${now.getTime()}${crypto.randomUUID().replaceAll("-", "")}`;
await context.env.D1.prepare( await context.data.prisma.event.create({
"INSERT INTO events (answer, approved, created_at, created_by, day, details, id, month, pending, type, year) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", data: {
) answer: context.data.body.answer || null,
.bind( approved: type === "gamenight",
context.data.body.answer || null, created_by: context.data.current_user.id,
Number(type === "gamenight"),
now.getTime(),
context.data.current_user.id,
day, day,
details, details,
id, id,
currentMonth, month: currentMonth,
Number(type !== "gamenight"), pending: type !== "gamenight",
type, type,
currentYear, year: currentYear,
) },
.run(); });
await fetch(context.env.EVENTS_WEBHOOK, { await fetch(context.env.EVENTS_WEBHOOK, {
body: JSON.stringify({ 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); if (typeof points !== "number") return jsonError("Invalid point count", 400);
await context.env.D1.prepare("UPDATE et_members SET points = ? WHERE id = ?;") await context.data.prisma.etMember.update({
.bind(points, context.params.id) data: {
.run(); points,
},
where: {
id: context.params.id as string,
},
});
return new Response(null, { return new Response(null, {
status: 204, status: 204,

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,37 @@
import { jsonError } from "../../../common.js"; import { jsonError } from "../../../common.js";
import { getBanList } from "../../../roblox-open-cloud.js";
export async function onRequestPost(context: RequestContext) { export async function onRequestPost(context: RequestContext) {
const appealId = context.params.id as string; const appealId = context.params.id as string;
const appeal = await context.data.prisma.gameAppeal.findUnique({
const appeal = await context.env.D1.prepare( select: {
"SELECT * FROM game_appeals WHERE id = ?;", roblox_id: true,
) type: true,
.bind(appealId) },
.first(); where: {
id: appealId,
},
});
if (!appeal) return jsonError("Appeal not found", 404); if (!appeal) return jsonError("Appeal not found", 404);
await context.env.D1.prepare("DELETE FROM game_appeals WHERE id = ?;") await context.data.prisma.gameAppeal.delete({
.bind(appealId) where: {
.run(); 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( await context.env.DATA.put(
`gameappealblock_${appeal.roblox_id}`, `gameappealblock_${appeal.roblox_id}`,

View File

@@ -10,11 +10,14 @@ export default async function (
types?: string[]; types?: string[];
}> { }> {
if ( if (
await context.env.D1.prepare( await context.data.prisma.gameAppeal.findFirst({
"SELECT * FROM game_appeals WHERE roblox_id = ?;", select: {
) id: true,
.bind(user) },
.first() where: {
roblox_id: user,
},
})
) )
return { return {
can_appeal: false, can_appeal: false,
@@ -47,22 +50,20 @@ export default async function (
).toLocaleString()} to submit another appeal`, ).toLocaleString()} to submit another appeal`,
}; };
const userLogs = await context.env.D1.prepare( const userLogs = await context.data.prisma.gameModLog.findMany({
"SELECT action, executed_at FROM game_mod_logs WHERE target = ? ORDER BY executed_at DESC;", select: {
) action: true,
.bind(user) executed_at: true,
.all(); },
where: {
if (userLogs.error) target: user,
return { },
error: "Could not determine your eligibility", });
};
// Legacy bans // Legacy bans
if (!userLogs.results.length) if (!userLogs.length) return { can_appeal: true, reason: "", types: ["ban"] };
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) if (Date.now() < allowedTime)
return { return {
@@ -72,11 +73,7 @@ export default async function (
).toLocaleString()} to submit an appeal`, ).toLocaleString()} to submit an appeal`,
}; };
if ( if (userLogs.find((r) => r.action.startsWith("accept appeal")))
userLogs.results.find((r: Record<string, any>) =>
r.action.startsWith("accept appeal"),
)
)
return { return {
can_appeal: false, can_appeal: false,
reason: "We do not accept appeals from repeat offenders", 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] context.request.headers.get("cf-ray")?.split("-")[0]
}${Date.now()}`; }${Date.now()}`;
await context.env.D1.prepare( await context.data.prisma.gameAppeal.create({
"INSERT INTO game_appeals (created_at, id, reason_for_unban, roblox_id, roblox_username, type, what_happened) VALUES (?, ?, ?, ?, ?, ?, ?);", data: {
) id: appealId,
.bind( reason_for_unban: reasonForUnban,
Date.now(), roblox_id: id,
appealId, roblox_username: username,
reasonForUnban,
id,
username,
type, type,
whatHappened, what_happened: whatHappened,
) },
.run(); });
await fetch(context.env.REPORTS_WEBHOOK, { await fetch(context.env.REPORTS_WEBHOOK, {
body: JSON.stringify({ body: JSON.stringify({

View File

@@ -17,7 +17,7 @@ export async function onRequestGet(context: RequestContext) {
); );
if (!robloxUserReq.ok) { if (!robloxUserReq.ok) {
console.log(await robloxUserReq.json()); console.log(await robloxUserReq.text());
return jsonError("Failed to resolve username", 500); 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"; } else if (banData.BanType === 2) current_status = "Banned";
const response = { const response = {
history: ( history: await context.data.prisma.gameModLog.findMany({
await context.env.D1.prepare( orderBy: {
"SELECT * FROM game_mod_logs WHERE target = ? ORDER BY executed_at DESC;", executed_at: "desc",
) },
.bind(users[0].id) where: {
.all() target: users[0].id,
).results, },
}),
user: { user: {
avatar: thumbnailRequest.ok 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); if (isNaN(parseInt(user))) return jsonError("Invalid user ID", 400);
await context.env.D1.prepare( await context.data.prisma.gameModLog.create({
"INSERT INTO game_mod_logs (action, evidence, executed_at, executor, id, target) VALUES (?, ?, ?, ?, ?, ?);", data: {
) action: "revoke",
.bind( evidence: ticket_link,
"revoke", executor: context.data.current_user.id,
ticket_link, id: crypto.randomUUID(),
Date.now(), target: parseInt(user),
context.data.current_user.id, },
crypto.randomUUID(), });
parseInt(user),
)
.run();
const { etag, value: banList } = await getBanList(context); 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) { export async function onRequestDelete(context: RequestContext) {
const noteId = context.params.id as string; const noteId = context.params.id as string;
const creatorIdResult: null | Record<string, string> = const creatorIdResult = await context.data.prisma.gameModNote.findUnique({
await context.env.D1.prepare( select: {
"SELECT created_by FROM game_mod_logs WHERE id = ?;", created_by: true,
) },
.bind(noteId) where: {
.first(); 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); 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 }); return new Response(null, { status: 204 });
} }
export async function onRequestGet(context: RequestContext) { export async function onRequestGet(context: RequestContext) {
const noteId = context.params.id as string; const noteId = context.params.id as string;
const result = await context.env.D1.prepare( const result = await context.data.prisma.gameModNote.findUnique({
"SELECT * FROM game_mod_notes WHERE id = ?;", where: {
) id: noteId,
.bind(noteId) },
.first(); });
if (!result) return jsonError("Note not found", 404); 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 } = const gmeEntry: null | { time: number; user: string; name: string } =
await context.env.DATA.get(`gamemod_${result.created_by}`, "json"); 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)); return jsonResponse(JSON.stringify(noteData));
} }

View File

@@ -1,18 +1,22 @@
import { jsonError, jsonResponse } from "../../common.js"; import { jsonError, jsonResponse } from "../../common.js";
import sendEmail from "../../email.js"; import sendEmail from "../../email.js";
import { sendPushNotification } from "../../gcloud.js"; import { sendPushNotification } from "../../gcloud.js";
import { type JsonArray, type JsonObject } from "@prisma/client/runtime/client";
export async function onRequestDelete(context: RequestContext) { export async function onRequestDelete(context: RequestContext) {
const result = await context.env.D1.prepare( const result = await context.data.prisma.inactivityNotice.findUnique({
"SELECT json_extract(user, '*.id') AS uid FROM inactivity_notices WHERE id = ?;", select: {
) user: true,
.bind(context.params.id) },
.first(); where: {
id: context.params.id as string,
},
});
if (!result) return jsonError("No inactivity notice with that ID", 404); if (!result) return jsonError("No inactivity notice with that ID", 404);
if ( 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)) !(context.data.current_user.permissions & (1 << 0))
) )
return jsonError( return jsonError(
@@ -39,26 +43,17 @@ export async function onRequestGet(context: RequestContext) {
) )
return jsonError("Forbidden", 403); return jsonError("Forbidden", 403);
const result: Record< const result = await context.data.prisma.inactivityNotice.findUnique({
string, where: {
string | number | { [k: string]: string } id: context.params.id as 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);
return jsonResponse(JSON.stringify(result)); return jsonResponse(JSON.stringify(result));
} }
export async function onRequestPost(context: RequestContext) { export async function onRequestPost(context: RequestContext) {
const { accepted }: { accepted?: boolean } = context.data.body; const { accepted }: { accepted?: any } = context.data.body;
if (typeof accepted !== "boolean") if (typeof accepted !== "boolean")
return jsonError("'accepted' must be a boolean", 400); return jsonError("'accepted' must be a boolean", 400);
@@ -77,32 +72,45 @@ export async function onRequestPost(context: RequestContext) {
if (!userAdminDepartments.length) if (!userAdminDepartments.length)
return jsonError("You are not a manager of any departments", 403); return jsonError("You are not a manager of any departments", 403);
const requestedNotice: { [k: string]: any } | null = const requestedNotice = await context.data.prisma.inactivityNotice.findUnique(
await context.env.D1.prepare( {
"SELECT decisions, departments, user FROM inactivity_notices WHERE id = ?;", select: {
) decisions: true,
.bind(context.params.id) departments: true,
.first(); user: true,
},
where: {
id: context.params.id as string,
},
},
);
if (!requestedNotice) if (!requestedNotice)
return jsonError("Inactivity notices does not exist", 404); return jsonError("Inactivity notices does not exist", 404);
const decisions: { [dept: string]: boolean } = JSON.parse( const decisions = requestedNotice.decisions as { [k: string]: boolean };
requestedNotice.decisions,
);
for (const department of userAdminDepartments) { for (const department of userAdminDepartments) {
if (!JSON.parse(requestedNotice.departments).includes(department)) continue; if (!(requestedNotice.departments as JsonArray).includes(department))
continue;
decisions[department] = accepted; 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( delete user.email;
"UPDATE inactivity_notices SET decisions = ?, user = json_remove(user, '$.email') WHERE id = ?;",
) await context.data.prisma.inactivityNotice.update({
.bind(JSON.stringify(decisions), context.params.id) data: {
.run(); decisions,
user,
},
where: {
id: context.params.id as string,
},
});
if (Object.values(decisions).length === applicableDepartments) { if (Object.values(decisions).length === applicableDepartments) {
const approved = const approved =
@@ -111,11 +119,16 @@ export async function onRequestPost(context: RequestContext) {
const denied = const denied =
Object.values(decisions).filter((d) => !d).length !== Object.values(decisions).filter((d) => !d).length !==
applicableDepartments; applicableDepartments;
const fcmTokenResult: FCMTokenResult | null = await context.env.D1.prepare( const fcmTokenResult =
"SELECT token FROM push_notifications WHERE event_id = ? AND event_type = 'inactivity';", await context.data.prisma.pushNotification.findUnique({
) select: {
.bind(context.params.id) token: true,
.first(); },
where: {
event_id: context.params.id as string,
event_type: "inactivity",
},
});
if (fcmTokenResult) { if (fcmTokenResult) {
let status = "Approved"; let status = "Approved";
@@ -132,16 +145,19 @@ export async function onRequestPost(context: RequestContext) {
fcmTokenResult.token, fcmTokenResult.token,
); );
await context.env.D1.prepare( await context.data.prisma.pushNotification.delete({
"DELETE FROM push_notifications WHERE event_id = ? AND event_type = 'inactivity';", where: {
).bind(context.params.id); event_id: context.params.id as string,
event_type: "inactivity",
},
});
} else { } else {
await sendEmail( await sendEmail(
requestedNotice.user.email, (requestedNotice.user as JsonObject).email as string,
context.env.MAILGUN_API_KEY, context.env.MAILGUN_API_KEY,
`Inactivity Request ${approved ? "Approved" : "Denied"}`, `Inactivity Request ${approved ? "Approved" : "Denied"}`,
`inactivity_${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] + (context.request.headers.get("cf-ray") as string).split("-")[0] +
Date.now().toString(); Date.now().toString();
await context.env.D1.prepare( await context.data.prisma.inactivityNotice.create({
"INSERT INTO inactivity_notices (created_at, departments, end, hiatus, id, reason, start, user) VALUES (?, ?, ?, ?, ?, ?, ?, ?);", data: {
) decisions: {},
.bind( departments,
Date.now(),
JSON.stringify(departments),
end, end,
typeof hiatus === "boolean" ? Number(hiatus) : 0, hiatus,
inactivityId, id: inactivityId,
reason, reason,
start, start,
JSON.stringify({ user: {
id: context.data.current_user.id, id: context.data.current_user.id,
email: context.data.current_user.email, email: context.data.current_user.email,
username: context.data.current_user.username, username: context.data.current_user.username,
}), },
) },
.run(); });
if (typeof senderTokenId === "string") { if (typeof senderTokenId === "string") {
await context.env.D1.prepare( await context.data.prisma.pushNotification.create({
"INSERT INTO push_notifications (created_at, event_id, event_type) VALUES (?, ?, ?);", data: {
) event_id: inactivityId,
.bind(Date.now(), inactivityId, "inactivity") event_type: "inactivity",
.run(); token: senderTokenId,
},
});
} }
const departmentsToNotify = []; 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) { 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( return jsonResponse(
JSON.stringify( JSON.stringify(
results.map((result) => { await context.data.prisma.appeal.findMany({
result.user = JSON.parse(result.user as string); select: {
approved: true,
return result; 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 { jsonError, jsonResponse } from "../../../../common.js";
import {
type JsonArray,
type JsonObject,
} from "../../../../../generated/prisma/internal/prismaNamespace.js";
export async function onRequestGet(context: RequestContext) { export async function onRequestGet(context: RequestContext) {
const { id, type } = context.params; const { id, type } = context.params;
@@ -6,57 +10,68 @@ export async function onRequestGet(context: RequestContext) {
if (!["appeal", "inactivity", "report"].includes(type as string)) if (!["appeal", "inactivity", "report"].includes(type as string))
return jsonError("Invalid type", 400); return jsonError("Invalid type", 400);
const tables: { [k: string]: string } = { const { prisma } = context.data;
appeal: "appeals", let item;
inactivity: "inactivity_notices",
report: "reports",
};
const data: Record<string, any> | null = await context.env.D1.prepare( switch (type as string) {
`SELECT * case "appeal":
FROM ${tables[type as string]} item = await prisma.appeal.findUnique({
WHERE id = ?;`, where: {
) id: id as string,
.bind(id) },
.first(); });
if (data?.user) data.user = JSON.parse(data.user); break;
if (!data || data.user?.id !== context.data.current_user.id) case "inactivity":
return jsonError("Item does not exist", 404); item = await prisma.inactivityNotice.findUnique({
where: {
id: id as string,
},
});
if (type === "inactivity") { break;
data.decisions = JSON.parse(data.decisions); case "report":
data.departments = JSON.parse(data.departments); 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 { AwsClient } = await import("aws4fetch");
const aws = new AwsClient({ const aws = new AwsClient({
accessKeyId: context.env.R2_ACCESS_KEY, accessKeyId: context.env.R2_ACCESS_KEY,
secretAccessKey: context.env.R2_SECRET_KEY, secretAccessKey: context.env.R2_SECRET_KEY,
}); });
let urlPromises = [];
let urls = []; for (const attachment of item.attachments as JsonArray) {
urlPromises.push(
for (const attachment of data.attachments) { aws.sign(
const { url } = await aws.sign(
`https://car-crushers.${context.env.R2_ZONE}.r2.cloudflarestorage.com/${attachment}?X-Amz-Expires=1800`, `https://car-crushers.${context.env.R2_ZONE}.r2.cloudflarestorage.com/${attachment}?X-Amz-Expires=1800`,
{ {
aws: { aws: {
signQuery: true, 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) { export async function onRequestGet(context: RequestContext) {
const { return jsonResponse(
results, JSON.stringify(
success, await context.data.prisma.report.findMany({
}: { select: {
results: { id: string }[]; created_at: true,
success: boolean; id: true,
} = await context.env.D1.prepare( open: true,
"SELECT created_at, id, open, target_usernames FROM reports WHERE json_extract(user, '$.id') = ? ORDER BY created_at LIMIT 50;", target_usernames: true,
) },
.bind(context.data.current_user.id) where: {
.all(); user: {
path: "id",
if (!success) return jsonError("Failed to retrieve reports", 500); equals: context.data.current_user.id,
},
return jsonResponse(JSON.stringify(results)); },
}),
),
);
} }

View File

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

View File

@@ -1,7 +1,13 @@
import { jsonError, jsonResponse } from "../../../common.js"; 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> { export async function onRequestGet(context: RequestContext): Promise<any> {
const type = context.params.type as string; const type = context.params.type as string;
const { prisma } = context.data;
const { searchParams } = new URL(context.request.url); const { searchParams } = new URL(context.request.url);
const before = parseInt(searchParams.get("before") || `${Date.now()}`); const before = parseInt(searchParams.get("before") || `${Date.now()}`);
const showClosed = searchParams.get("showClosed") === "true"; 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); if (isNaN(before)) return jsonError("Invalid `before` parameter", 400);
let rows: D1Result<Record<string, any>>; let rows;
switch (type) { switch (type) {
case "appeal": case "appeal":
rows = await context.env.D1.prepare( rows = await prisma.appeal.findMany({
`SELECT * orderBy: {
FROM appeals created_at: "desc",
WHERE created_at < ? },
AND approved ${showClosed ? "IS NOT" : "IS"} NULL take: 25,
ORDER BY created_at DESC LIMIT 25;`, where: {
) created_at: {
.bind(before) lt: new Date(before),
.all(); },
rows.results = rows.results.map((r) => { approved: showClosed ? { not: null } : null,
r.user = JSON.parse(r.user); },
delete r.user.email; });
rows.map((r) => {
delete (r.user as JsonObject).email;
return r; return r;
}); });
break; break;
case "gma": case "gma":
rows = await context.env.D1.prepare( rows = await prisma.gameAppeal.findMany({
"SELECT * FROM game_appeals WHERE created_at < ? ORDER BY created_at DESC LIMIT 25;", orderBy: {
) created_at: "desc",
.bind(before) },
.all(); take: 25,
where: {
created_at: {
lt: new Date(before),
},
},
});
break; break;
case "inactivity": case "inactivity":
rows = await context.env.D1.prepare( rows = await prisma.$queryRaw<
`SELECT *, InactivityNotice[] & { decision_count: number }[]
(SELECT COUNT(*) FROM json_each(decisions)) as decision_count >`
FROM inactivity_notices 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);`;
WHERE created_at < ?
AND decision_count ${showClosed ? "=" : "!="} json_array_length(departments)`,
)
.bind(before)
.all();
rows.results.map((r) => { rows.map((r) => {
r.decisions = JSON.parse(r.decisions); // These come back as strings when using $queryRaw
r.departments = JSON.parse(r.departments); r.decisions = JSON.parse(r.decisions as string);
r.user = JSON.parse(r.user); 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; return r;
}); });
break; break;
case "report": case "report":
rows = await context.env.D1.prepare( rows = await prisma.report.findMany({
"SELECT * FROM reports WHERE created_at < ? AND open = ? ORDER BY created_at DESC LIMIT 25;", orderBy: {
) created_at: "desc",
.bind(before, !Number(showClosed)) },
.all(); take: 25,
where: {
created_at: {
lt: new Date(before),
},
open: !showClosed,
},
});
rows.results = rows.results.map((r) => { rows.map((r) => {
r.attachments = JSON.parse(r.attachments); delete (r.user as JsonObject | null)?.email;
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;
}
return r; return r;
}); });
@@ -103,5 +113,5 @@ export async function onRequestGet(context: RequestContext): Promise<any> {
return jsonError("Unknown filter error", 500); 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"; import { sendPushNotification } from "../../../gcloud.js";
export async function onRequestPost(context: RequestContext) { export async function onRequestPost(context: RequestContext) {
const { prisma } = context.data;
const reportId = context.params.id as string; const reportId = context.params.id as string;
const report: { const report: {
[k: string]: any; [k: string]: any;
@@ -83,12 +84,15 @@ export async function onRequestPost(context: RequestContext) {
await setBanList(context, Object.assign(banList, newActions), etag); await setBanList(context, Object.assign(banList, newActions), etag);
} }
const pushNotificationData: Record<string, string> | null = const pushNotificationData = await prisma.pushNotification.findUnique({
await context.env.D1.prepare( select: {
"SELECT token FROM push_notifications WHERE event_id = ? AND event_type = 'report';", token: true,
) },
.bind(reportId) where: {
.first(); event_id: reportId,
event_type: "report",
},
});
if (user?.email) if (user?.email)
await sendEmail( await sendEmail(
@@ -100,26 +104,33 @@ export async function onRequestPost(context: RequestContext) {
username: user.username as string, username: user.username as string,
}, },
); );
else if (pushNotificationData) else if (pushNotificationData) {
await sendPushNotification( await sendPushNotification(
context.env, context.env,
"Report Processed", "Report Processed",
`Your report for ${JSON.parse(report.target_usernames).toString()} has been reviewed.`, `Your report for ${JSON.parse(report.target_usernames).toString()} has been reviewed.`,
pushNotificationData.token, pushNotificationData.token,
); );
await prisma.pushNotification.delete({
where: {
event_id: reportId,
event_type: "report",
},
});
}
delete (report.user as { email?: string; id: string; username: string }) delete (report.user as { email?: string; id: string; username: string })
?.email; ?.email;
await context.env.D1.prepare("UPDATE reports SET open = 0 WHERE id = ?;") await prisma.report.update({
.bind(reportId) data: {
.run(); open: false,
user: report.user,
await context.env.D1.prepare( },
"DELETE FROM push_notifications WHERE event_id = ? AND event_type = 'report';", where: {
) id: reportId,
.bind(reportId) },
.run(); });
return new Response(null, { return new Response(null, {
status: 204, status: 204,

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,18 @@
import { jsonResponse } from "../../common.js"; import { jsonResponse } from "../../common.js";
export async function onRequestGet(context: RequestContext) { export async function onRequestGet(context: RequestContext) {
const { results } = await context.env.D1.prepare( return jsonResponse(
"SELECT created_at, destination, path FROM short_links WHERE user = ?;", JSON.stringify(
) await context.data.prisma.shortLink.findMany({
.bind(context.data.current_user.id) select: {
.all(); created_at: true,
destination: true,
return jsonResponse(JSON.stringify(results)); 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) if (typeof path !== "string" || path.length > 256)
return jsonError("Invalid path", 400); return jsonError("Invalid path", 400);
const result = await context.env.D1.prepare( const result = await context.data.prisma.shortLink.findUnique({
"SELECT path FROM short_links WHERE path = ?;", select: {
) path: true,
.bind(path) },
.first(); where: {
path,
},
});
if (result) if (result)
return jsonError( return jsonError(
@@ -78,11 +81,13 @@ export async function onRequestPost(context: RequestContext) {
400, 400,
); );
await context.env.D1.prepare( await context.data.prisma.shortLink.create({
"INSERT INTO short_links (created_at, destination, path, user) VALUES (?, ?, ?, ?);", data: {
) destination,
.bind(Date.now(), destination, path, context.data.current_user.id) path,
.run(); user: context.data.current_user.id,
},
});
return new Response(null, { return new Response(null, {
status: 204, status: 204,

View File

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

View File

@@ -1,6 +1,9 @@
const DATASTORE_URL = const DATASTORE_URL =
"https://apis.roblox.com/cloud/v2/universes/274816972/data-stores/BanData/entries/CloudBanList"; "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 = const SAVE_DATA_URL =
"https://apis.roblox.com/cloud/v2/universes/274816972/data-stores/RealData/entries"; "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( export async function getSaveData(
context: RequestContext, context: RequestContext,
user: number, user: number,

View File

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

8
index.d.ts vendored
View File

@@ -1,3 +1,5 @@
import { type PrismaClient } from "./generated/prisma/client.js";
declare global { declare global {
module "*.css"; module "*.css";
@@ -17,7 +19,11 @@ declare global {
token: string; token: string;
}; };
type RequestContext = EventContext<Env, string, { [k: string]: any }>; type RequestContext = EventContext<
Env,
string,
{ prisma: PrismaClient; [k: string]: any }
>;
interface AppealCardProps { interface AppealCardProps {
approved: number | null; 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", "build": "remix build --sourcemap",
"check-format": "prettier -c .", "check-format": "prettier -c .",
"format": "prettier -wc .", "format": "prettier -wc .",
"publish": "remix build --sourcemap && wrangler pages deploy public" "publish": "remix build --sourcemap && wrangler pages deploy --upload-source-maps public"
}, },
"dependencies": { "dependencies": {
"@chakra-ui/react": "^2.10.9", "@chakra-ui/react": "^2.10.9",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@fontsource-variable/plus-jakarta-sans": "^5.2.8", "@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": "^2.17.4",
"@remix-run/cloudflare-pages": "^2.17.4", "@remix-run/cloudflare-pages": "^2.17.4",
"@remix-run/react": "^2.17.4", "@remix-run/react": "^2.17.4",
"@sentry/cloudflare": "^10.46.0", "@sentry/cloudflare": "^10.52.0",
"@sentry/remix": "^10.46.0", "@sentry/remix": "^10.52.0",
"aws4fetch": "^1.0.20", "aws4fetch": "^1.0.20",
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
"framer-motion": "^12.38.0", "framer-motion": "^12.38.0",
@@ -28,16 +30,17 @@
}, },
"devDependencies": { "devDependencies": {
"@remix-run/dev": "^2.17.4", "@remix-run/dev": "^2.17.4",
"@types/node": "^24.12.0", "@types/node": "^24.12.3",
"@types/react": "^18.3.28", "@types/react": "^18.3.28",
"@types/react-big-calendar": "^1.16.3", "@types/react-big-calendar": "^1.16.3",
"@types/react-dom": "^18.3.7", "@types/react-dom": "^18.3.7",
"dotenv": "^17.3.1", "dotenv": "^17.4.1",
"prettier": "^3.8.1", "prettier": "^3.8.3",
"prisma": "^7.8.0",
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"overrides": { "overrides": {
"@cloudflare/workers-types": "^4.20260329.1" "@cloudflare/workers-types": "^4.20260511.1"
}, },
"prettier": { "prettier": {
"endOfLine": "auto" "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")
}