Compare commits
65 Commits
b69b5cb4eb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
73b6c85171
|
|||
|
037eb7fcac
|
|||
|
abc1389dbb
|
|||
|
cd566248fd
|
|||
|
3d7e499ec1
|
|||
|
40dd0b5a5c
|
|||
|
229398e401
|
|||
|
a9863f5680
|
|||
|
2e76bd9f28
|
|||
|
171240bc7d
|
|||
|
ffce17d7aa
|
|||
|
06fdfe9d10
|
|||
|
ba643bf986
|
|||
|
12f91dca7d
|
|||
|
b6de1aa462
|
|||
|
b65b62dac5
|
|||
|
0ec5399726
|
|||
|
f0c4e178aa
|
|||
|
6ad4fa0514
|
|||
|
42275fcb0f
|
|||
|
0854d72449
|
|||
|
cb0be09c0d
|
|||
|
16ecab6881
|
|||
|
7d5ec1183c
|
|||
|
96d221be2a
|
|||
|
f5e3e3cca6
|
|||
|
2d9f03c394
|
|||
|
c51b29ce57
|
|||
|
546842c4dd
|
|||
|
1f2a8770a1
|
|||
|
4b15c65092
|
|||
|
b671aefd6e
|
|||
|
f32a7912b4
|
|||
|
da0ce2b188
|
|||
|
6da49d191a
|
|||
|
5457898ff9
|
|||
|
fe206e2fbd
|
|||
|
02cac814da
|
|||
|
f184389ffd
|
|||
|
5c17f87f89
|
|||
|
48631e32be
|
|||
|
b60f211d7b
|
|||
|
1a891e5898
|
|||
|
7b72f815b0
|
|||
|
4860288d11
|
|||
|
930128c0d4
|
|||
|
b5e230e7f2
|
|||
|
cfc57c838e
|
|||
|
465bb30966
|
|||
|
95ab13775b
|
|||
|
91ec421450
|
|||
|
8e52692fc8
|
|||
|
a4470310e8
|
|||
|
02f0a299e0
|
|||
|
c92f4d31f2
|
|||
|
d5203e236a
|
|||
|
71f56769c1
|
|||
|
61c75df368
|
|||
|
91fa274df8
|
|||
|
0ca9bc1164
|
|||
|
14a3e54c22
|
|||
|
b53f6ef20e
|
|||
|
8748d8f6bc
|
|||
|
b8e320fa01
|
|||
|
e3af1e4f37
|
@@ -27,6 +27,9 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
run: npm ci --include=dev
|
||||
|
||||
- name: Generate Prisma Types
|
||||
run: npx prisma generate
|
||||
|
||||
- name: Check Formatting
|
||||
run: npm run check-format
|
||||
|
||||
@@ -53,7 +56,7 @@ jobs:
|
||||
}'
|
||||
|
||||
- name: Deploy
|
||||
run: wrangler pages deploy public --project-name $CLOUDFLARE_PROJECT_NAME
|
||||
run: wrangler pages deploy public --upload-source-maps --project-name $CLOUDFLARE_PROJECT_NAME
|
||||
|
||||
Sentry-Release:
|
||||
name: Create Sentry Release
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -37,3 +37,7 @@ public/build
|
||||
|
||||
# Wrangler data
|
||||
.wrangler
|
||||
wrangler.jsonc
|
||||
wrangler.toml
|
||||
|
||||
/generated/prisma
|
||||
|
||||
@@ -1 +1 @@
|
||||
v24.14.0
|
||||
v24.15.0
|
||||
43
app/root.tsx
43
app/root.tsx
@@ -2,8 +2,10 @@ import {
|
||||
ChakraProvider,
|
||||
Container,
|
||||
cookieStorageManagerSSR,
|
||||
Flex,
|
||||
Heading,
|
||||
Link,
|
||||
Spacer,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { ClientStyleContext, ServerStyleContext } from "./context.js";
|
||||
@@ -94,6 +96,42 @@ export function ErrorBoundary() {
|
||||
</DocumentWrapper>
|
||||
);
|
||||
|
||||
case 503:
|
||||
return (
|
||||
<DocumentWrapper loaderData={{ hide: true }}>
|
||||
<Container
|
||||
left="50%"
|
||||
maxW="container.md"
|
||||
pos="absolute"
|
||||
top="50%"
|
||||
transform="translate(-50%, -50%)"
|
||||
>
|
||||
<Flex>
|
||||
<Spacer />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
height="128"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M12.496 8a4.5 4.5 0 0 1-1.703 3.526L9.497 8.5l2.959-1.11q.04.3.04.61" />
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-1 0a7 7 0 1 0-13.202 3.249l1.988-1.657a4.5 4.5 0 0 1 7.537-4.623L7.497 6.5l1 2.5 1.333 3.11c-.56.251-1.18.39-1.833.39a4.5 4.5 0 0 1-1.592-.29L4.747 14.2A7 7 0 0 0 15 8m-8.295.139a.25.25 0 0 0-.288-.376l-1.5.5.159.474.808-.27-.595.894a.25.25 0 0 0 .287.376l.808-.27-.595.894a.25.25 0 0 0 .287.376l1.5-.5-.159-.474-.808.27.596-.894a.25.25 0 0 0-.288-.376l-.808.27z" />
|
||||
</svg>
|
||||
<Spacer />
|
||||
</Flex>
|
||||
<br />
|
||||
<Heading textAlign="center">
|
||||
The engineers are breaking stuff again
|
||||
</Heading>
|
||||
<br />
|
||||
<Text textAlign="center">
|
||||
Someday they will finish, come back later.
|
||||
</Text>
|
||||
</Container>
|
||||
</DocumentWrapper>
|
||||
);
|
||||
|
||||
default:
|
||||
captureRemixErrorBoundaryError(useRouteError());
|
||||
return (
|
||||
@@ -128,6 +166,11 @@ export async function loader({
|
||||
}: {
|
||||
context: RequestContext;
|
||||
}): Promise<{ [k: string]: any }> {
|
||||
if (context.data.mx)
|
||||
throw new Response(null, {
|
||||
status: 503,
|
||||
});
|
||||
|
||||
let data: { [k: string]: string } = {};
|
||||
|
||||
if (context.env.COMMIT_SHA) data.commit_sha = context.env.COMMIT_SHA;
|
||||
|
||||
@@ -40,11 +40,18 @@ export async function loader({ context }: { context: RequestContext }) {
|
||||
!Boolean(disabled) &&
|
||||
!Boolean(await dataKV.get(`blockedappeal_${currentUser.id}`)) &&
|
||||
!Boolean(
|
||||
await context.env.D1.prepare(
|
||||
"SELECT * FROM appeals WHERE approved IS NULL AND json_extract(user, '$.id') = ? LIMIT 1;",
|
||||
)
|
||||
.bind(currentUser.id)
|
||||
.first(),
|
||||
await context.data.prisma.appeal.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
approved: null,
|
||||
user: {
|
||||
path: "id",
|
||||
equals: currentUser.id,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
can_toggle:
|
||||
currentUser.permissions & (1 << 0) || currentUser.permissions & (1 << 11),
|
||||
|
||||
@@ -44,6 +44,7 @@ export default function () {
|
||||
const [eventType, setEventType] = useState("");
|
||||
const [riddleAnswer, setRiddleAnswer] = useState("");
|
||||
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||
const [disableSubmit, setDisableSubmit] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDatePickerMin(`${new Date().toISOString().split("T").at(0)}`);
|
||||
@@ -53,6 +54,7 @@ export default function () {
|
||||
}, []);
|
||||
|
||||
async function submit() {
|
||||
setDisableSubmit(true);
|
||||
let eventResp: Response;
|
||||
|
||||
try {
|
||||
@@ -69,6 +71,7 @@ export default function () {
|
||||
method: "POST",
|
||||
});
|
||||
} catch {
|
||||
setDisableSubmit(false);
|
||||
toast({
|
||||
description: "Please check your internet and try again",
|
||||
isClosable: true,
|
||||
@@ -86,6 +89,7 @@ export default function () {
|
||||
errorMessage = ((await eventResp.json()) as { error: string }).error;
|
||||
} catch {}
|
||||
|
||||
setDisableSubmit(false);
|
||||
toast({
|
||||
description: errorMessage,
|
||||
isClosable: true,
|
||||
@@ -150,7 +154,11 @@ export default function () {
|
||||
onChange={(e) => setRiddleAnswer(e.target.value)}
|
||||
placeholder="Riddle answer"
|
||||
/>
|
||||
<Button mt="16px" onClick={async () => await submit()}>
|
||||
<Button
|
||||
disabled={disableSubmit}
|
||||
mt="16px"
|
||||
onClick={async () => await submit()}
|
||||
>
|
||||
Book
|
||||
</Button>
|
||||
</Container>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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."
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -42,20 +42,16 @@ export async function loader({ context }: { context: RequestContext }) {
|
||||
status: 403,
|
||||
});
|
||||
|
||||
const etData = await context.env.D1.prepare(
|
||||
"SELECT id, name, points, roblox_id FROM et_members;",
|
||||
).all();
|
||||
const etData = await context.data.prisma.etMember.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
points: true,
|
||||
roblox_id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (etData.error)
|
||||
throw new Response(null, {
|
||||
status: 500,
|
||||
});
|
||||
|
||||
const members = etData.results as { [k: string]: any }[];
|
||||
|
||||
return { members } as {
|
||||
members: { [k: string]: any }[];
|
||||
};
|
||||
return { members: etData };
|
||||
}
|
||||
|
||||
export default function () {
|
||||
|
||||
@@ -45,15 +45,15 @@ export async function loader({
|
||||
status: 403,
|
||||
});
|
||||
|
||||
const strikeData = await context.env.D1.prepare(
|
||||
"SELECT * FROM et_strikes WHERE user = ?;",
|
||||
)
|
||||
.bind(params.uid)
|
||||
.all();
|
||||
const strikes = await context.data.prisma.etStrike.findMany({
|
||||
where: {
|
||||
user: params.uid,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
can_manage: Boolean([1 << 4, 1 << 12].find((p) => user.permissions & p)),
|
||||
strikes: strikeData.results,
|
||||
strikes,
|
||||
user: params.uid,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import { useState } from "react";
|
||||
import utc from "dayjs/plugin/utc.js";
|
||||
import { EtMember } from "../../generated/prisma/client.js";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [
|
||||
@@ -56,18 +57,31 @@ export async function loader({ context }: { context: RequestContext }) {
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const eventsData = await context.env.D1.prepare(
|
||||
"SELECT answer, approved, created_by, day, details, id, month, pending, performed_at, reached_minimum_player_count, type, year FROM events WHERE month = ? AND year = ? ORDER BY day;",
|
||||
)
|
||||
.bind(now.getUTCMonth() + 1, now.getUTCFullYear())
|
||||
.all();
|
||||
const eventsData = await context.data.prisma.event.findMany({
|
||||
orderBy: {
|
||||
day: "asc",
|
||||
},
|
||||
select: {
|
||||
answer: true,
|
||||
approved: true,
|
||||
created_by: true,
|
||||
day: true,
|
||||
details: true,
|
||||
id: true,
|
||||
month: true,
|
||||
pending: true,
|
||||
performed_at: true,
|
||||
reached_minimum_player_count: true,
|
||||
type: true,
|
||||
year: true,
|
||||
},
|
||||
where: {
|
||||
month: now.getUTCMonth() + 1,
|
||||
year: now.getUTCFullYear(),
|
||||
},
|
||||
});
|
||||
|
||||
if (eventsData.error)
|
||||
throw new Response(null, {
|
||||
status: 500,
|
||||
});
|
||||
|
||||
const calendarData = eventsData.results.map((e) => {
|
||||
const calendarData = eventsData.map((e) => {
|
||||
return {
|
||||
id: e.id,
|
||||
title: (e.type as string).toUpperCase(),
|
||||
@@ -78,19 +92,17 @@ export async function loader({ context }: { context: RequestContext }) {
|
||||
};
|
||||
});
|
||||
|
||||
const memberData = await context.env.D1.prepare(
|
||||
"SELECT id, name FROM et_members WHERE id IN (SELECT created_by FROM events WHERE month = ? AND year = ?);",
|
||||
)
|
||||
.bind(now.getUTCMonth() + 1, now.getUTCFullYear())
|
||||
.all();
|
||||
const memberData = await context.data.prisma.$queryRaw<
|
||||
EtMember[]
|
||||
>`SELECT id, name FROM et_members WHERE id IN (SELECT created_by FROM events WHERE month = ${now.getUTCMonth() + 1} AND year = ${now.getUTCFullYear()});`;
|
||||
|
||||
return {
|
||||
calendarData,
|
||||
canManage: Boolean(
|
||||
[1 << 4, 1 << 12].find((p) => context.data.current_user.permissions & p),
|
||||
),
|
||||
eventList: eventsData.results,
|
||||
memberData: memberData.results,
|
||||
eventList: eventsData,
|
||||
memberData,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,15 +123,7 @@ export default function () {
|
||||
<ModalBody>
|
||||
<Heading size="md">Host</Heading>
|
||||
<Text>
|
||||
{
|
||||
(
|
||||
data.memberData.find(
|
||||
(m) => m.id === eventData.created_by,
|
||||
) as {
|
||||
[k: string]: any;
|
||||
}
|
||||
)?.name
|
||||
}
|
||||
{data.memberData.find((m) => m.id === eventData.created_by)?.name}
|
||||
</Text>
|
||||
<br />
|
||||
<Heading size="md">Event Type</Heading>
|
||||
|
||||
@@ -30,6 +30,7 @@ import { useLoaderData } from "@remix-run/react";
|
||||
import { useState } from "react";
|
||||
import calendarStyles from "react-big-calendar/lib/css/react-big-calendar.css";
|
||||
import { type LinksFunction } from "@remix-run/cloudflare";
|
||||
import { EtMember } from "../../generated/prisma/client.js";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ href: calendarStyles, rel: "stylesheet" }];
|
||||
@@ -51,34 +52,41 @@ export async function loader({ context }: { context: RequestContext }) {
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const monthEventList = await context.env.D1.prepare(
|
||||
"SELECT answer, approved, created_by, day, details, id, month, pending, performed_at, reached_minimum_player_count, type, year FROM events WHERE month = ? AND year = ? ORDER BY day ASC;",
|
||||
)
|
||||
.bind(now.getUTCMonth() + 1, now.getUTCFullYear())
|
||||
.all();
|
||||
const { prisma } = context.data;
|
||||
const monthEventList = await prisma.event.findMany({
|
||||
orderBy: {
|
||||
day: "asc",
|
||||
},
|
||||
select: {
|
||||
answer: true,
|
||||
approved: true,
|
||||
created_by: true,
|
||||
day: true,
|
||||
details: true,
|
||||
id: true,
|
||||
month: true,
|
||||
pending: true,
|
||||
performed_at: true,
|
||||
reached_minimum_player_count: true,
|
||||
type: true,
|
||||
year: true,
|
||||
},
|
||||
where: {
|
||||
month: now.getUTCMonth() + 1,
|
||||
year: now.getUTCFullYear(),
|
||||
},
|
||||
});
|
||||
|
||||
if (monthEventList.error)
|
||||
throw new Response(null, {
|
||||
status: 500,
|
||||
});
|
||||
|
||||
const membersList = await context.env.D1.prepare(
|
||||
"SELECT id, name FROM et_members WHERE id IN (SELECT created_by FROM events WHERE month = ? AND year = ?);",
|
||||
)
|
||||
.bind(now.getUTCMonth() + 1, now.getUTCFullYear())
|
||||
.all();
|
||||
|
||||
if (membersList.error)
|
||||
throw new Response(null, {
|
||||
status: 500,
|
||||
});
|
||||
const membersList = await prisma.$queryRaw<
|
||||
EtMember[]
|
||||
>`SELECT id, name FROM et_members WHERE id IN (SELECT created_by FROM events WHERE month = ${now.getUTCMonth() + 1} AND year = ${now.getUTCFullYear()});`;
|
||||
|
||||
return {
|
||||
can_approve: Boolean(
|
||||
[1 << 4, 1 << 12].find((p) => context.data.current_user.permissions & p),
|
||||
),
|
||||
events: monthEventList.results,
|
||||
members: membersList.results as { id: string; name: string }[],
|
||||
events: monthEventList,
|
||||
members: membersList,
|
||||
user_id: context.data.current_user.id as string,
|
||||
};
|
||||
}
|
||||
@@ -263,7 +271,7 @@ export default function () {
|
||||
|
||||
// Technically this won't be the same as the time in the db, but that doesn't matter since this is just to hide the button
|
||||
newEventData[eventData.findIndex((e) => e.id === eventId)].performed_at =
|
||||
Date.now();
|
||||
new Date().toISOString();
|
||||
|
||||
setEventData([...newEventData]);
|
||||
setSelectedEvent("");
|
||||
@@ -305,7 +313,8 @@ export default function () {
|
||||
|
||||
const newEventData = eventData;
|
||||
|
||||
newEventData[eventData.findIndex((e) => e.id === eventId)].performed_at = 0;
|
||||
newEventData[eventData.findIndex((e) => e.id === eventId)].performed_at =
|
||||
new Date(0).toISOString();
|
||||
setEventData([...newEventData]);
|
||||
setSelectedEvent("");
|
||||
setDisableClicks(false);
|
||||
@@ -656,9 +665,7 @@ export default function () {
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
{can_approve &&
|
||||
!event.pending &&
|
||||
typeof event.performed_at !== "number" ? (
|
||||
{can_approve && !event.pending && !event.performed_at ? (
|
||||
<>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
|
||||
@@ -25,14 +25,13 @@ export async function loader({ context }: { context: RequestContext }) {
|
||||
status: 403,
|
||||
});
|
||||
|
||||
const memberResults = await context.env.D1.prepare(
|
||||
"SELECT id, name FROM et_members;",
|
||||
).all();
|
||||
|
||||
if (!memberResults.success)
|
||||
throw new Response(null, {
|
||||
status: 500,
|
||||
});
|
||||
const { prisma } = context.data;
|
||||
const memberResults = await prisma.etMember.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const data: {
|
||||
[k: string]: {
|
||||
@@ -45,7 +44,7 @@ export async function loader({ context }: { context: RequestContext }) {
|
||||
};
|
||||
} = {};
|
||||
|
||||
for (const row of memberResults.results as Record<string, string>[]) {
|
||||
for (const row of memberResults) {
|
||||
data[row.id].fotd = 0;
|
||||
data[row.id].gamenight = 0;
|
||||
data[row.id].name = row.name;
|
||||
@@ -53,22 +52,26 @@ export async function loader({ context }: { context: RequestContext }) {
|
||||
data[row.id].qotd = 0;
|
||||
}
|
||||
|
||||
const eventsResult = await context.env.D1.prepare(
|
||||
"SELECT answered_at, created_by, day, month, performed_at, reached_minimum_player_count, type, year FROM events;",
|
||||
).all();
|
||||
const eventsResult = await prisma.event.findMany({
|
||||
select: {
|
||||
answered_at: true,
|
||||
created_by: true,
|
||||
day: true,
|
||||
month: true,
|
||||
performed_at: true,
|
||||
reached_minimum_player_count: true,
|
||||
type: true,
|
||||
year: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!eventsResult.success)
|
||||
throw new Response(null, {
|
||||
status: 500,
|
||||
});
|
||||
|
||||
for (const row of eventsResult.results) {
|
||||
const creator = row.created_by as string;
|
||||
const type = row.type as string;
|
||||
for (const row of eventsResult) {
|
||||
const creator = row.created_by;
|
||||
const type = row.type;
|
||||
|
||||
if (!data[creator]) continue;
|
||||
|
||||
if (row.performed_at) data[creator][type as string] += 10;
|
||||
if (row.performed_at) data[creator][type] += 10;
|
||||
else {
|
||||
const now = new Date();
|
||||
const currentYear = now.getUTCFullYear();
|
||||
@@ -76,33 +79,17 @@ export async function loader({ context }: { context: RequestContext }) {
|
||||
const currentDay = now.getUTCDate();
|
||||
|
||||
if (
|
||||
(row.year as number) < currentYear ||
|
||||
(currentYear === row.year && currentMonth > (row.month as number)) ||
|
||||
row.year < currentYear ||
|
||||
(currentYear === row.year && currentMonth > row.month) ||
|
||||
(currentMonth === row.month &&
|
||||
currentYear === row.year &&
|
||||
(row.day as number) < currentDay)
|
||||
row.day < currentDay)
|
||||
)
|
||||
data[creator][type] -= 5;
|
||||
}
|
||||
|
||||
switch (row.type) {
|
||||
case "gamenight":
|
||||
if (row.reached_minimum_player_count) data[creator].gamenight += 10;
|
||||
|
||||
break;
|
||||
|
||||
case "rotw":
|
||||
if (
|
||||
(row.answered_at as number) - (row.performed_at as number) >=
|
||||
86400000
|
||||
)
|
||||
data[creator].rotw += 10;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (row.type === "gamenight" && row.reached_minimum_player_count)
|
||||
data[creator].gamenight += 10;
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
@@ -44,9 +44,12 @@ export async function loader({ context }: { context: RequestContext }) {
|
||||
status: 403,
|
||||
});
|
||||
|
||||
return (
|
||||
await context.env.D1.prepare("SELECT id, name FROM et_members;").all()
|
||||
).results;
|
||||
return await context.data.prisma.etMember.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default function () {
|
||||
|
||||
@@ -37,14 +37,40 @@ export async function loader({ context }: { context: RequestContext }) {
|
||||
year--;
|
||||
}
|
||||
|
||||
const data = await context.env.D1.prepare(
|
||||
"SELECT day, details, id FROM events WHERE approved = 1 AND month = ? AND year = ? AND (performed_at IS NULL OR (reached_minimum_player_count = 0 AND type = 'gamenight')) ORDER BY day;",
|
||||
)
|
||||
.bind(month, year)
|
||||
.all();
|
||||
const data = await context.data.prisma.event.findMany({
|
||||
orderBy: {
|
||||
day: "asc",
|
||||
},
|
||||
select: {
|
||||
answered_at: true,
|
||||
day: true,
|
||||
details: true,
|
||||
id: true,
|
||||
performed_at: true,
|
||||
reached_minimum_player_count: true,
|
||||
type: true,
|
||||
},
|
||||
where: {
|
||||
AND: {
|
||||
approved: true,
|
||||
month,
|
||||
year,
|
||||
OR: [
|
||||
{
|
||||
AND: [
|
||||
{
|
||||
reached_minimum_player_count: false,
|
||||
type: "gamenight",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
events: data.results as Record<string, string | number>[],
|
||||
events: data,
|
||||
past_cutoff: now.getUTCDate() > 7,
|
||||
};
|
||||
}
|
||||
@@ -52,7 +78,7 @@ export async function loader({ context }: { context: RequestContext }) {
|
||||
export default function () {
|
||||
const { events, past_cutoff } = useLoaderData<typeof loader>();
|
||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||
const [eventData, setEventData] = useState({} as { [k: string]: any });
|
||||
const [eventData, setEventData] = useState({} as (typeof events)[number]);
|
||||
const [isBrowserSupported, setIsBrowserSupported] = useState(true);
|
||||
const toast = useToast();
|
||||
|
||||
@@ -74,10 +100,10 @@ export default function () {
|
||||
});
|
||||
}
|
||||
|
||||
function getStatus(event: { [k: string]: string | number }) {
|
||||
function getStatus(event: (typeof events)[number]) {
|
||||
if (!event.performed_at) return "Approved";
|
||||
if (event.type === "rotw" && event.answered_at) return "Solved";
|
||||
if (event.type === "gamenight" && event.areached_minimum_player_count)
|
||||
if (event.type === "gamenight" && event.reached_minimum_player_count)
|
||||
return "Certified";
|
||||
|
||||
return "Completed";
|
||||
@@ -106,7 +132,7 @@ export default function () {
|
||||
});
|
||||
|
||||
const newData = structuredClone(eventData);
|
||||
newData.reached_minimum_player_count = 1;
|
||||
newData.reached_minimum_player_count = true;
|
||||
|
||||
setEventData(newData);
|
||||
}
|
||||
@@ -134,9 +160,12 @@ export default function () {
|
||||
});
|
||||
|
||||
const newData = structuredClone(eventData);
|
||||
newData.performed_at = Date.now();
|
||||
|
||||
setEventData(newData);
|
||||
setEventData(
|
||||
Object.defineProperty(newData, "performed_at", {
|
||||
value: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function forgotten() {
|
||||
@@ -162,9 +191,12 @@ export default function () {
|
||||
});
|
||||
|
||||
const newData = structuredClone(eventData);
|
||||
newData.performed_at = 0;
|
||||
|
||||
setEventData(newData);
|
||||
setEventData(
|
||||
Object.defineProperty(newData, "performed_at", {
|
||||
value: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function solve() {
|
||||
@@ -190,9 +222,12 @@ export default function () {
|
||||
});
|
||||
|
||||
const newData = structuredClone(eventData);
|
||||
newData.answered_at = Date.now();
|
||||
|
||||
setEventData(newData);
|
||||
setEventData(
|
||||
Object.defineProperty(newData, "performed_at", {
|
||||
value: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -216,13 +251,13 @@ export default function () {
|
||||
<Box gap="8px">
|
||||
<Heading size="xs">Completion</Heading>
|
||||
<Button
|
||||
disabled={typeof eventData.completed_at === "number"}
|
||||
disabled={Boolean(eventData.performed_at)}
|
||||
onClick={async () => await completed()}
|
||||
>
|
||||
Mark as Complete
|
||||
</Button>
|
||||
<Button
|
||||
disabled={typeof eventData.completed_at === "number"}
|
||||
disabled={typeof eventData.performed_at === "number"}
|
||||
onClick={async () => await forgotten()}
|
||||
>
|
||||
Mark as Forgotten
|
||||
@@ -243,7 +278,7 @@ export default function () {
|
||||
<Box gap="8px">
|
||||
<Heading size="xs">Certified Status</Heading>
|
||||
<Button
|
||||
disabled={Boolean(eventData.reached_minimum_player_count)}
|
||||
disabled={eventData.reached_minimum_player_count}
|
||||
onClick={async () => await certify()}
|
||||
>
|
||||
{eventData.reached_minimum_player_count
|
||||
@@ -277,8 +312,8 @@ export default function () {
|
||||
<Tr>
|
||||
<Td>{event.day}</Td>
|
||||
<Td>
|
||||
{(event.details as string).length > 100
|
||||
? `${(event.details as string).substring(0, 97)}...`
|
||||
{event.details.length > 100
|
||||
? `${event.details.substring(0, 97)}...`
|
||||
: event.details}
|
||||
</Td>
|
||||
<Td>{getStatus(event)}</Td>
|
||||
|
||||
@@ -268,6 +268,7 @@ export default function () {
|
||||
element: (
|
||||
<AppealCard
|
||||
{...(entry as AppealCardProps & { port?: MessagePort })}
|
||||
key={`appeal_${entry.id}`}
|
||||
port={messageChannel.current?.port2}
|
||||
/>
|
||||
),
|
||||
@@ -281,6 +282,7 @@ export default function () {
|
||||
element: (
|
||||
<GameAppealCard
|
||||
{...(entry as GameAppealProps & { port?: MessagePort })}
|
||||
key={`gma_${entry.id}`}
|
||||
port={messageChannel.current?.port2}
|
||||
/>
|
||||
),
|
||||
@@ -294,6 +296,7 @@ export default function () {
|
||||
element: (
|
||||
<InactivityNoticeCard
|
||||
{...(entry as InactivityNoticeProps & { port?: MessagePort })}
|
||||
key={`inactivity_${entry.id}`}
|
||||
port={messageChannel.current?.port2}
|
||||
/>
|
||||
),
|
||||
@@ -307,6 +310,7 @@ export default function () {
|
||||
element: (
|
||||
<ReportCard
|
||||
{...(entry as ReportCardProps & { port?: MessagePort })}
|
||||
key={`report_${entry.id}`}
|
||||
port={messageChannel.current?.port2}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
Textarea,
|
||||
useMultiStyleConfig,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -50,6 +51,9 @@ export default function () {
|
||||
const toast = useToast();
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const inputSelectorProps = useMultiStyleConfig("Button", {
|
||||
colorScheme: "blue",
|
||||
});
|
||||
const fileTypes: { [k: string]: string } = {
|
||||
avif: "image/avif",
|
||||
gif: "image/gif",
|
||||
@@ -112,9 +116,9 @@ export default function () {
|
||||
if (!logged_in) {
|
||||
const tokenElem = document
|
||||
.getElementsByName("cf-turnstile-response")
|
||||
.item(0) as HTMLInputElement;
|
||||
.item(0) as HTMLInputElement | null;
|
||||
|
||||
if (!tokenElem.value) {
|
||||
if (!tokenElem?.value) {
|
||||
setLoading(false);
|
||||
return toast({
|
||||
description: "Please complete the captcha and try again",
|
||||
@@ -250,8 +254,8 @@ export default function () {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
setShowSuccess(true);
|
||||
setLoading(false);
|
||||
setShowSuccess(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -304,14 +308,19 @@ export default function () {
|
||||
<br />
|
||||
<FormControl isRequired>
|
||||
<FormLabel>Your Evidence (Max size per file: 512MB)</FormLabel>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
mr="8px"
|
||||
onClick={() => document.getElementById("evidence")?.click()}
|
||||
>
|
||||
Select File
|
||||
</Button>
|
||||
<input id="evidence" multiple type="file" />
|
||||
<Input
|
||||
border="none"
|
||||
id="evidence"
|
||||
sx={{
|
||||
"::file-selector-button": {
|
||||
border: "none",
|
||||
outline: "none",
|
||||
...inputSelectorProps,
|
||||
},
|
||||
}}
|
||||
multiple={true}
|
||||
type="file"
|
||||
/>
|
||||
</FormControl>
|
||||
<br />
|
||||
<FormControl>
|
||||
|
||||
@@ -40,13 +40,15 @@ export async function loader({ context }: { context: RequestContext }) {
|
||||
status: 403,
|
||||
});
|
||||
|
||||
const { results } = await context.env.D1.prepare(
|
||||
"SELECT destination, path FROM short_links WHERE user = ?;",
|
||||
)
|
||||
.bind(userId)
|
||||
.all();
|
||||
|
||||
return results as Record<string, string>[];
|
||||
return await context.data.prisma.shortLink.findMany({
|
||||
select: {
|
||||
destination: true,
|
||||
path: true,
|
||||
},
|
||||
where: {
|
||||
user: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default function () {
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function (props: { isOpen: boolean; onClose: () => void }) {
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{entries.map((entry) => (
|
||||
<Tr>
|
||||
<Tr key={`appealban_${entry.user}`}>
|
||||
<Td>{entry.user}</Td>
|
||||
<Td>{entry.created_by}</Td>
|
||||
<Td>{new Date(entry.created_at).toUTCString()}</Td>
|
||||
|
||||
@@ -173,7 +173,6 @@ export default function (props: {
|
||||
)}
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
@@ -223,7 +222,6 @@ export default function (props: {
|
||||
{data.id ? data.username : ""}
|
||||
</Text>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
|
||||
@@ -17,13 +17,7 @@ export default function ({
|
||||
>
|
||||
<Flex>
|
||||
<Spacer />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
height="128"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<svg width="128" height="128" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M2.5 8a5.5 5.5 0 0 1 8.25-4.764.5.5 0 0 0 .5-.866A6.5 6.5 0 1 0 14.5 8a.5.5 0 0 0-1 0 5.5 5.5 0 1 1-11 0z" />
|
||||
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z" />
|
||||
</svg>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import getPermissions from "./permissions.js";
|
||||
import { jsonError } from "./common.js";
|
||||
import { PrismaClient } from "../generated/prisma/client.js";
|
||||
import { PrismaD1 } from "@prisma/adapter-d1";
|
||||
import * as Sentry from "@sentry/cloudflare";
|
||||
|
||||
async function constructHTML(context: RequestContext) {
|
||||
@@ -28,6 +30,27 @@ async function generateTokenHash(token: string) {
|
||||
.replace(/=/g, "");
|
||||
}
|
||||
|
||||
async function initializePrisma(context: RequestContext) {
|
||||
const adapter = new PrismaD1(context.env.D1);
|
||||
context.data.prisma = new PrismaClient({ adapter });
|
||||
|
||||
return await context.next();
|
||||
}
|
||||
|
||||
async function mxAndBypassCheck(context: RequestContext) {
|
||||
if (!(await context.env.DATA.get("mx"))) return await context.next();
|
||||
const cookies = context.request.headers.get("cookie")?.split(/; */);
|
||||
const isAPI = new URL(context.request.url).pathname.startsWith("/api");
|
||||
|
||||
if (!cookies?.length || !cookies.find((c) => c.startsWith("mxb="))) {
|
||||
if (isAPI) return jsonError("API is undergoing maintenance", 503);
|
||||
|
||||
context.data.mx = true;
|
||||
}
|
||||
|
||||
return await context.next();
|
||||
}
|
||||
|
||||
async function refreshAuth(context: RequestContext) {
|
||||
const { current_user: currentUser } = context.data;
|
||||
|
||||
@@ -382,14 +405,20 @@ async function setTheme(context: RequestContext) {
|
||||
|
||||
export const onRequest = [
|
||||
Sentry.sentryPagesPlugin((context: RequestContext) => ({
|
||||
beforeSend(event) {
|
||||
delete event.request?.cookies;
|
||||
return event;
|
||||
},
|
||||
dsn: context.env.FUNCTIONS_DSN,
|
||||
release: context.env.COMMIT_SHA,
|
||||
sendDefaultPii: true,
|
||||
})),
|
||||
mxAndBypassCheck,
|
||||
setAuth,
|
||||
refreshAuth,
|
||||
setTheme,
|
||||
constructHTML,
|
||||
setBody,
|
||||
initializePrisma,
|
||||
setHeaders,
|
||||
];
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { jsonError } from "../../../common.js";
|
||||
import {
|
||||
Appeal,
|
||||
PushNotification,
|
||||
} from "../../../../generated/prisma/client.js";
|
||||
|
||||
export async function onRequestPost(context: RequestContext) {
|
||||
const { pathname } = new URL(context.request.url);
|
||||
@@ -20,22 +24,23 @@ export async function onRequestPost(context: RequestContext) {
|
||||
context.data.targetId = id;
|
||||
|
||||
if (!pathname.endsWith("/ban")) {
|
||||
const appeal: Record<string, any> | null = await context.env.D1.prepare(
|
||||
"SELECT * FROM appeals WHERE id = ?;",
|
||||
)
|
||||
.bind(id)
|
||||
.first();
|
||||
const appeal: Appeal | null = await context.data.prisma.appeal.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!appeal) return jsonError("No appeal with that ID exists", 404);
|
||||
|
||||
appeal.user = JSON.parse(appeal.user);
|
||||
context.data.appeal = appeal;
|
||||
|
||||
const pushNotificationData = await context.env.D1.prepare(
|
||||
"SELECT token FROM push_notifications WHERE event_id = ? AND event_type = 'appeal';",
|
||||
)
|
||||
.bind(id)
|
||||
.first();
|
||||
const pushNotificationData: PushNotification | null =
|
||||
await context.data.prisma.pushNotification.findUnique({
|
||||
where: {
|
||||
event_id: id,
|
||||
event_type: "appeal",
|
||||
},
|
||||
});
|
||||
|
||||
if (pushNotificationData)
|
||||
context.data.fcm_token = pushNotificationData.token;
|
||||
|
||||
@@ -13,11 +13,12 @@ export async function onRequestPost(context: RequestContext) {
|
||||
fcm_token,
|
||||
);
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"DELETE FROM push_notifications WHERE event_id = ? AND event_type = 'appeal';",
|
||||
)
|
||||
.bind(appeal.id)
|
||||
.run();
|
||||
await context.data.prisma.pushNotification.delete({
|
||||
where: {
|
||||
event_id: appeal.id,
|
||||
event_type: "appeal",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const emailResponse = await sendEmail(
|
||||
appeal.user.email,
|
||||
@@ -37,11 +38,8 @@ export async function onRequestPost(context: RequestContext) {
|
||||
|
||||
const { current_user: currentUser } = context.data;
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"UPDATE appeals SET approved = 1, user = json_remove(user, '$.email') WHERE id = ?;",
|
||||
)
|
||||
.bind(context.params.id)
|
||||
.run();
|
||||
await context.data.prisma
|
||||
.$executeRaw`UPDATE appeals SET approved = TRUE, user = json_remove(user, '$.id') WHERE id = ${appeal.id};`;
|
||||
|
||||
await fetch(
|
||||
`https://discord.com/api/v10/guilds/242263977986359297/bans/${appeal.user.id}`,
|
||||
|
||||
@@ -6,9 +6,11 @@ export async function onRequestDelete(context: RequestContext) {
|
||||
if (targetId.search(/^\d{16.19}$/) === -1)
|
||||
return jsonError("Invalid target id", 400);
|
||||
|
||||
await context.env.D1.prepare("DELETE FROM appeal_bans WHERE user = ?;")
|
||||
.bind(targetId)
|
||||
.run();
|
||||
await context.data.prisma.appealBan.delete({
|
||||
where: {
|
||||
user: targetId,
|
||||
},
|
||||
});
|
||||
|
||||
const { current_user: currentUser } = context.data;
|
||||
|
||||
@@ -46,11 +48,12 @@ export async function onRequestPost(context: RequestContext) {
|
||||
if (targetId.search(/^\d{16,19}$/) === -1)
|
||||
return jsonError("Invalid target id", 400);
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"INSERT INTO appeal_bans (created_at, created_by, user) VALUES (?, ?, ?);",
|
||||
)
|
||||
.bind(Date.now(), context.data.current_user.id, targetId)
|
||||
.run();
|
||||
await context.data.prisma.appealBan.create({
|
||||
data: {
|
||||
created_by: context.data.current_user.id,
|
||||
user: targetId,
|
||||
},
|
||||
});
|
||||
|
||||
await fetch(context.env.APPEALS_WEBHOOK, {
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -14,11 +14,12 @@ export async function onRequestPost(context: RequestContext) {
|
||||
fcm_token,
|
||||
);
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"DELETE FROM push_notifications WHERE event_id = ? AND event_type = 'appeal';",
|
||||
)
|
||||
.bind(appeal.id)
|
||||
.run();
|
||||
await context.data.prisma.pushNotification.delete({
|
||||
where: {
|
||||
event_id: appeal.id,
|
||||
event_type: "appeal",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const emailResponse = await sendEmail(
|
||||
appeal.user.email,
|
||||
@@ -36,11 +37,8 @@ export async function onRequestPost(context: RequestContext) {
|
||||
}
|
||||
}
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"UPDATE appeals SET approved = 0, user = json_remove(user, '$.email') WHERE id = ?;",
|
||||
)
|
||||
.bind(context.params.id)
|
||||
.run();
|
||||
await context.data.prisma
|
||||
.$executeRaw`UPDATE appeals SET approved = FALSE, user = json_remove(user, '$.id') WHERE id = ${appeal.id};`;
|
||||
|
||||
const { current_user: currentUser } = context.data;
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { jsonResponse } from "../../common.js";
|
||||
|
||||
export async function onRequestGet(context: RequestContext) {
|
||||
const { results } = await context.env.D1.prepare(
|
||||
"SELECT * FROM appeal_bans ORDER BY created_by DESC;",
|
||||
).all();
|
||||
const results = await context.data.prisma.appealBan.findMany({
|
||||
orderBy: {
|
||||
created_at: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
return jsonResponse(JSON.stringify(results));
|
||||
}
|
||||
|
||||
@@ -6,11 +6,12 @@ export async function onRequestGet(context: RequestContext) {
|
||||
if (
|
||||
!currentUser.email ||
|
||||
(await context.env.DATA.get("appeal_disabled")) ||
|
||||
(await context.env.D1.prepare(
|
||||
"SELECT id FROM appeals WHERE open = 1 AND user = ?;",
|
||||
)
|
||||
.bind(currentUser.id)
|
||||
.first()) ||
|
||||
(await context.data.prisma.appeal.findFirst({
|
||||
where: {
|
||||
approved: null,
|
||||
user: currentUser.id,
|
||||
},
|
||||
})) ||
|
||||
(await context.env.DATA.get(`blockedappeal_${currentUser.id}`))
|
||||
)
|
||||
return jsonResponse('{"can_appeal":false}');
|
||||
@@ -47,18 +48,24 @@ export async function onRequestPost(context: RequestContext) {
|
||||
|
||||
if (
|
||||
existingBlockedAppeal ||
|
||||
(await context.env.D1.prepare(
|
||||
"SELECT approved FROM appeals WHERE approved IS NULL AND json_extract(user, '$.id') = ?;",
|
||||
)
|
||||
.bind(currentUser.id)
|
||||
.first())
|
||||
(await context.data.prisma.appeal.findFirst({
|
||||
where: {
|
||||
approved: null,
|
||||
user: {
|
||||
path: "id",
|
||||
equals: currentUser.id,
|
||||
},
|
||||
},
|
||||
}))
|
||||
)
|
||||
return jsonError("Appeal already submitted", 403);
|
||||
|
||||
if (
|
||||
await context.env.D1.prepare("SELECT * FROM appeal_bans WHERE user = ?;")
|
||||
.bind(currentUser.id)
|
||||
.first()
|
||||
await context.data.prisma.appealBan.findUnique({
|
||||
where: {
|
||||
user: currentUser.id,
|
||||
},
|
||||
})
|
||||
) {
|
||||
await context.env.DATA.put(`blockedappeal_${currentUser.id}`, "1", {
|
||||
metadata: { email: currentUser.email },
|
||||
@@ -73,29 +80,28 @@ export async function onRequestPost(context: RequestContext) {
|
||||
.randomUUID()
|
||||
.replaceAll("-", "")}`;
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"INSERT INTO appeals (ban_reason, created_at, id, learned, reason_for_unban, user) VALUES (?, ?, ?, ?, ?, ?);",
|
||||
)
|
||||
.bind(
|
||||
whyBanned,
|
||||
Date.now(),
|
||||
appealId,
|
||||
await context.data.prisma.appeal.create({
|
||||
data: {
|
||||
ban_reason: whyBanned,
|
||||
id: appealId,
|
||||
learned,
|
||||
whyUnban,
|
||||
JSON.stringify({
|
||||
reason_for_unban: whyUnban,
|
||||
user: {
|
||||
email: currentUser.email,
|
||||
id: currentUser.id,
|
||||
username: currentUser.username,
|
||||
}),
|
||||
)
|
||||
.run();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (typeof senderTokenId === "string") {
|
||||
await context.env.D1.prepare(
|
||||
"INSERT INTO push_notifications (created_at, event_id, event_type, token) VALUES (?, ?, 'appeal', ?)",
|
||||
)
|
||||
.bind(Date.now(), appealId, senderTokenId)
|
||||
.run();
|
||||
await context.data.prisma.pushNotification.create({
|
||||
data: {
|
||||
event_id: appealId,
|
||||
event_type: "appeal",
|
||||
token: senderTokenId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await fetch(context.env.APPEALS_WEBHOOK, {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -1,94 +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)) as {
|
||||
[k: string]: { BanType: number };
|
||||
};
|
||||
} 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,
|
||||
});
|
||||
}
|
||||
@@ -2,15 +2,18 @@ import { jsonError } from "../../../common.js";
|
||||
|
||||
export async function onRequestDelete(context: RequestContext) {
|
||||
const eventId = context.params.id as string;
|
||||
const eventData:
|
||||
| ({
|
||||
[k: string]: number;
|
||||
} & { created_by: string })
|
||||
| null = await context.env.D1.prepare(
|
||||
"SELECT created_by, day, month, performed_at, year FROM events WHERE id = ?;",
|
||||
)
|
||||
.bind(eventId)
|
||||
.first();
|
||||
const eventData = await context.data.prisma.event.findUnique({
|
||||
select: {
|
||||
created_by: true,
|
||||
day: true,
|
||||
month: true,
|
||||
performed_at: true,
|
||||
year: true,
|
||||
},
|
||||
where: {
|
||||
id: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!eventData) return jsonError("No event exists with that ID", 404);
|
||||
|
||||
@@ -41,9 +44,11 @@ export async function onRequestDelete(context: RequestContext) {
|
||||
400,
|
||||
);
|
||||
|
||||
await context.env.D1.prepare("DELETE FROM events WHERE id = ?;")
|
||||
.bind(eventId)
|
||||
.run();
|
||||
await context.data.prisma.event.delete({
|
||||
where: {
|
||||
id: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
@@ -53,13 +58,18 @@ export async function onRequestDelete(context: RequestContext) {
|
||||
export async function onRequestPatch(context: RequestContext) {
|
||||
const eventId = context.params.id as string;
|
||||
const { body } = context.data;
|
||||
const eventData:
|
||||
| ({ [k: string]: number } & { created_by: string; type: string })
|
||||
| null = await context.env.D1.prepare(
|
||||
"SELECT created_by, day, month, type, year FROM events WHERE id = ?;",
|
||||
)
|
||||
.bind(eventId)
|
||||
.first();
|
||||
const eventData = await context.data.prisma.event.findUnique({
|
||||
select: {
|
||||
created_by: true,
|
||||
day: true,
|
||||
month: true,
|
||||
type: true,
|
||||
year: true,
|
||||
},
|
||||
where: {
|
||||
id: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!eventData) return jsonError("No event exists with that ID", 404);
|
||||
|
||||
@@ -83,7 +93,7 @@ export async function onRequestPatch(context: RequestContext) {
|
||||
typeof body.day !== "number" ||
|
||||
body.day > date.getUTCDate() ||
|
||||
body.day < 1 ||
|
||||
// Check for non-integers
|
||||
// Check for nonintegers
|
||||
Math.floor(body.day) !== body.day ||
|
||||
currentDay >= body.day
|
||||
)
|
||||
@@ -107,26 +117,38 @@ export async function onRequestPatch(context: RequestContext) {
|
||||
4: 35,
|
||||
};
|
||||
const weekRange = Math.floor(body.day / 7);
|
||||
|
||||
const matchingROTW = await context.env.D1.prepare(
|
||||
"SELECT id FROM events WHERE (approved = 1 OR pending = 1) AND day BETWEEN ? AND ? AND month = ? AND type = 'rotw' AND year = ?;",
|
||||
)
|
||||
.bind(
|
||||
weekRanges[weekRange] - 7,
|
||||
weekRanges[weekRange],
|
||||
eventData.month,
|
||||
eventData.year,
|
||||
)
|
||||
.first();
|
||||
const matchingROTW = await context.data.prisma.event.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
OR: [{ approved: true }, { pending: true }],
|
||||
day: {
|
||||
gte: weekRanges[weekRange] - 7,
|
||||
lte: weekRanges[weekRange],
|
||||
},
|
||||
month: eventData.month,
|
||||
year: eventData.year,
|
||||
},
|
||||
});
|
||||
|
||||
if (matchingROTW)
|
||||
return jsonError("There is already an ROTW scheduled for that week", 400);
|
||||
} else {
|
||||
const matchingEvent = await context.env.D1.prepare(
|
||||
"SELECT id FROM events WHERE (approved = 1 OR pending = 1) AND day = ? AND month = ? AND type = ? AND year = ?;",
|
||||
)
|
||||
.bind(body.day, eventData.month, eventData.type, eventData.year)
|
||||
.first();
|
||||
const matchingEvent = await context.data.prisma.event.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
OR: [{ approved: true }, { pending: true }],
|
||||
AND: [
|
||||
{ day: body.day },
|
||||
{ month: eventData.month },
|
||||
{ type: eventData.type },
|
||||
{ year: eventData.year },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (matchingEvent)
|
||||
return jsonError(
|
||||
@@ -135,9 +157,14 @@ export async function onRequestPatch(context: RequestContext) {
|
||||
);
|
||||
}
|
||||
|
||||
await context.env.D1.prepare("UPDATE events SET day = ? WHERE id = ?;")
|
||||
.bind(body.day, eventId)
|
||||
.run();
|
||||
await context.data.prisma.event.update({
|
||||
data: {
|
||||
day: body.day,
|
||||
},
|
||||
where: {
|
||||
id: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
await fetch(context.env.EVENTS_WEBHOOK, {
|
||||
body: JSON.stringify({
|
||||
@@ -162,22 +189,29 @@ export async function onRequestPatch(context: RequestContext) {
|
||||
|
||||
export async function onRequestPost(context: RequestContext) {
|
||||
const eventId = context.params.id as string;
|
||||
const eventData = await context.env.D1.prepare(
|
||||
"SELECT approved, performed_at FROM events WHERE id = ?;",
|
||||
)
|
||||
.bind(eventId)
|
||||
.first();
|
||||
const eventData = await context.data.prisma.event.findUnique({
|
||||
select: {
|
||||
approved: true,
|
||||
performed_at: true,
|
||||
},
|
||||
where: {
|
||||
id: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!eventData) return jsonError("No event exists with that ID", 404);
|
||||
|
||||
if (!eventData.approved)
|
||||
return jsonError("Cannot perform unapproved event", 403);
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"UPDATE events SET performed_at = ? WHERE id = ?;",
|
||||
)
|
||||
.bind(Date.now(), eventId)
|
||||
.run();
|
||||
await context.data.prisma.event.update({
|
||||
data: {
|
||||
performed_at: new Date(),
|
||||
},
|
||||
where: {
|
||||
id: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
|
||||
@@ -6,12 +6,11 @@ export async function onRequest(context: RequestContext) {
|
||||
// Skip checks for the by-id endpoint
|
||||
if (pathSegments.length <= 5) return await context.next();
|
||||
|
||||
const eventInfo = await context.env.D1.prepare(
|
||||
"SELECT * FROM events WHERE id = ?;",
|
||||
)
|
||||
.bind(context.params.id)
|
||||
.first();
|
||||
|
||||
const eventInfo = await context.data.prisma.event.findUnique({
|
||||
where: {
|
||||
id: context.params.id as string,
|
||||
},
|
||||
});
|
||||
if (!eventInfo) return jsonError("This event does not exist.", 404);
|
||||
|
||||
if (![1 << 4, 1 << 12].find((p) => context.data.current_user.permissions & p))
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function onRequestPost(context: RequestContext) {
|
||||
try {
|
||||
await D1.batch([
|
||||
D1.prepare(
|
||||
"UPDATE events SET reached_minimum_player_count = 1 WHERE id = ?",
|
||||
"UPDATE events SET reached_minimum_player_count = TRUE WHERE id = ?",
|
||||
).bind(event.id),
|
||||
D1.prepare(
|
||||
"UPDATE et_members SET points = points + 10 WHERE id = ?;",
|
||||
|
||||
@@ -5,23 +5,25 @@ export async function onRequestPost(context: RequestContext) {
|
||||
const { event } = context.data;
|
||||
|
||||
try {
|
||||
const completionTimeRow = await D1.prepare(
|
||||
"SELECT performed_at FROM events WHERE id = ?;",
|
||||
)
|
||||
.bind(event.id)
|
||||
.first();
|
||||
const completionTimeRow = await context.data.prisma.event.findUnique({
|
||||
select: {
|
||||
performed_at: true,
|
||||
},
|
||||
where: {
|
||||
id: event.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (typeof completionTimeRow?.performed_at === "number")
|
||||
if (completionTimeRow?.performed_at instanceof Date)
|
||||
return jsonError(
|
||||
"The event is already marked as complete or forgotten",
|
||||
400,
|
||||
);
|
||||
|
||||
await D1.batch([
|
||||
D1.prepare("UPDATE events SET performed_at = ? WHERE id = ?;").bind(
|
||||
Date.now(),
|
||||
event.id,
|
||||
),
|
||||
D1.prepare(
|
||||
"UPDATE events SET performed_at = CURRENT_TIMESTAMP WHERE id = ?;",
|
||||
).bind(event.id),
|
||||
D1.prepare(
|
||||
"UPDATE et_members SET points = points + 10 WHERE id = ?;",
|
||||
).bind(event.created_by),
|
||||
|
||||
@@ -5,12 +5,15 @@ export async function onRequestPost(context: RequestContext) {
|
||||
if (typeof context.data.body.approved !== "boolean")
|
||||
return jsonError("Decision type must be a boolean", 400);
|
||||
|
||||
const updatedEvent: Record<string, number | string> | null =
|
||||
await context.env.D1.prepare(
|
||||
"UPDATE events SET approved = ?, pending = 0 WHERE id = ? RETURNING created_by, day, month, year;",
|
||||
)
|
||||
.bind(Number(context.data.body.approved), context.data.event.id)
|
||||
.first();
|
||||
const updatedEvent = await context.data.prisma.event.update({
|
||||
data: {
|
||||
approved: context.data.body.approved,
|
||||
pending: false,
|
||||
},
|
||||
where: {
|
||||
id: context.data.event.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!updatedEvent) return jsonError("This event does not exist", 404);
|
||||
|
||||
@@ -19,10 +22,14 @@ export async function onRequestPost(context: RequestContext) {
|
||||
type: "json",
|
||||
});
|
||||
|
||||
const usernameData: Record<string, string> | null =
|
||||
await context.env.D1.prepare("SELECT name FROM et_members WHERE id = ?;")
|
||||
.bind(updatedEvent.created_by)
|
||||
.first();
|
||||
const usernameData = await context.data.prisma.etMember.findUnique({
|
||||
where: {
|
||||
id: updatedEvent.created_by,
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (emailData && usernameData) {
|
||||
await sendEmail(
|
||||
|
||||
@@ -2,22 +2,25 @@ import { jsonError } from "../../../../common.js";
|
||||
|
||||
export async function onRequestPost(context: RequestContext) {
|
||||
const { D1 } = context.env;
|
||||
const { event } = context.data;
|
||||
const { event, prisma } = context.data;
|
||||
|
||||
try {
|
||||
const row = await D1.prepare(
|
||||
"SELECT performed_at FROM events WHERE id = ?;",
|
||||
)
|
||||
.bind(event.id)
|
||||
.first();
|
||||
const row = await prisma.event.findUnique({
|
||||
select: {
|
||||
performed_at: true,
|
||||
},
|
||||
where: {
|
||||
id: event.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (typeof row?.performed_at === "number")
|
||||
if (row?.performed_at instanceof Date)
|
||||
return jsonError("Event already marked as completed or forgotten", 400);
|
||||
|
||||
await D1.batch([
|
||||
D1.prepare("UPDATE events SET performed_at = 0 WHERE id = ?;").bind(
|
||||
event.id,
|
||||
),
|
||||
D1.prepare(
|
||||
"UPDATE events SET performed_at = datetime(0, 'unixepoch') WHERE id = ?;",
|
||||
).bind(event.id),
|
||||
D1.prepare(
|
||||
"UPDATE et_members SET points = points - 5 WHERE id = ?;",
|
||||
).bind(event.created_by),
|
||||
|
||||
@@ -14,13 +14,27 @@ export async function onRequestGet(context: RequestContext) {
|
||||
if (currentYear < year || (currentYear === year && currentMonth < month))
|
||||
return jsonError("Cannot get events for a time in the future", 400);
|
||||
|
||||
const eventRecords = await context.env.D1.prepare(
|
||||
"SELECT answer, approved, created_by, day, details, month, pending, performed_at, type, year FROM events WHERE month = ? AND year = ? ORDER BY day ASC;",
|
||||
)
|
||||
.bind(month, year)
|
||||
.all();
|
||||
const eventRecords = await context.data.prisma.event.findMany({
|
||||
select: {
|
||||
answer: true,
|
||||
approved: true,
|
||||
created_by: true,
|
||||
day: true,
|
||||
details: true,
|
||||
month: true,
|
||||
pending: true,
|
||||
performed_at: true,
|
||||
type: true,
|
||||
year: true,
|
||||
},
|
||||
where: {
|
||||
month: month,
|
||||
year: year,
|
||||
},
|
||||
orderBy: {
|
||||
day: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
if (!eventRecords.success) return jsonError("Failed to retrieve events", 400);
|
||||
|
||||
return jsonResponse(JSON.stringify(eventRecords.results));
|
||||
return jsonResponse(JSON.stringify(eventRecords));
|
||||
}
|
||||
|
||||
@@ -22,11 +22,18 @@ export async function onRequestPost(context: RequestContext) {
|
||||
return jsonError("Invalid body", 400);
|
||||
|
||||
if (
|
||||
await context.env.D1.prepare(
|
||||
"SELECT id FROM events WHERE (approved = 1 OR pending = 1) AND day = ? AND month = ? AND type = ? AND year = ?;",
|
||||
)
|
||||
.bind(day, currentMonth, type, currentYear)
|
||||
.first()
|
||||
await context.data.prisma.event.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
OR: [{ approved: true }, { pending: true }],
|
||||
day,
|
||||
month: currentMonth,
|
||||
type,
|
||||
year: currentYear,
|
||||
},
|
||||
})
|
||||
)
|
||||
return jsonError(
|
||||
"Event with that type already exists for the specified date",
|
||||
@@ -44,16 +51,21 @@ export async function onRequestPost(context: RequestContext) {
|
||||
|
||||
const weekRange = Math.floor(day / 7);
|
||||
|
||||
const existingEventInRange = await context.env.D1.prepare(
|
||||
"SELECT id FROM events WHERE (approved = 1 OR pending = 1) AND day BETWEEN ? AND ? AND month = ? AND type = 'rotw' AND year = ?;",
|
||||
)
|
||||
.bind(
|
||||
weekRanges[weekRange] - 7,
|
||||
weekRanges[weekRange],
|
||||
currentMonth,
|
||||
currentYear,
|
||||
)
|
||||
.first();
|
||||
const existingEventInRange = await context.data.prisma.event.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
OR: [{ approved: true }, { pending: true }],
|
||||
day: {
|
||||
gte: weekRanges[weekRange] - 7,
|
||||
lte: weekRanges[weekRange],
|
||||
},
|
||||
month: currentMonth,
|
||||
type: "rotw",
|
||||
year: currentYear,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingEventInRange)
|
||||
return jsonError("There is already an rotw for that week", 400);
|
||||
@@ -61,23 +73,20 @@ export async function onRequestPost(context: RequestContext) {
|
||||
|
||||
const id = `${now.getTime()}${crypto.randomUUID().replaceAll("-", "")}`;
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"INSERT INTO events (answer, approved, created_at, created_by, day, details, id, month, pending, type, year) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
)
|
||||
.bind(
|
||||
context.data.body.answer || null,
|
||||
Number(type === "gamenight"),
|
||||
now.getTime(),
|
||||
context.data.current_user.id,
|
||||
await context.data.prisma.event.create({
|
||||
data: {
|
||||
answer: context.data.body.answer || null,
|
||||
approved: type === "gamenight",
|
||||
created_by: context.data.current_user.id,
|
||||
day,
|
||||
details,
|
||||
id,
|
||||
currentMonth,
|
||||
Number(type !== "gamenight"),
|
||||
month: currentMonth,
|
||||
pending: type !== "gamenight",
|
||||
type,
|
||||
currentYear,
|
||||
)
|
||||
.run();
|
||||
year: currentYear,
|
||||
},
|
||||
});
|
||||
|
||||
await fetch(context.env.EVENTS_WEBHOOK, {
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -12,9 +12,14 @@ export async function onRequestPost(context: RequestContext) {
|
||||
|
||||
if (typeof points !== "number") return jsonError("Invalid point count", 400);
|
||||
|
||||
await context.env.D1.prepare("UPDATE et_members SET points = ? WHERE id = ?;")
|
||||
.bind(points, context.params.id)
|
||||
.run();
|
||||
await context.data.prisma.etMember.update({
|
||||
data: {
|
||||
points,
|
||||
},
|
||||
where: {
|
||||
id: context.params.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export async function onRequestDelete(context: RequestContext) {
|
||||
await context.env.D1.prepare("DELETE FROM et_strikes WHERE id = ?;")
|
||||
.bind(context.params.id)
|
||||
.run();
|
||||
await context.data.prisma.etStrike.delete({
|
||||
where: {
|
||||
id: context.params.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
|
||||
@@ -11,9 +11,11 @@ export async function onRequestPost(context: RequestContext) {
|
||||
user.length > 20 ||
|
||||
user.length < 17 ||
|
||||
user.match(/\D/) ||
|
||||
!(await D1.prepare("SELECT id FROM et_members WHERE id = ?;")
|
||||
.bind(user)
|
||||
.first())
|
||||
!(await context.data.prisma.etMember.findUnique({
|
||||
where: {
|
||||
id: user,
|
||||
},
|
||||
}))
|
||||
)
|
||||
return jsonError("Invalid user id", 400);
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@ export async function onRequestDelete(context: RequestContext) {
|
||||
)
|
||||
return jsonError("Invalid ID", 400);
|
||||
|
||||
await context.env.D1.prepare("DELETE FROM et_members WHERE id = ?;")
|
||||
.bind(id)
|
||||
.run();
|
||||
await context.data.prisma.etMember.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
@@ -40,9 +40,9 @@ export async function onRequestPatch(context: RequestContext) {
|
||||
if (typeof body.name !== "string" && typeof body.roblox_username !== "string")
|
||||
return jsonError("At least one property must be provided", 400);
|
||||
|
||||
const updates = [];
|
||||
let queryData: { name?: string; roblox_id?: number } = {};
|
||||
|
||||
if (body.name?.length) updates.push({ query: "name = ?", value: body.name });
|
||||
if (body.name?.length) queryData.name = body.name;
|
||||
|
||||
if (typeof body.roblox_username === "string" && body.roblox_username) {
|
||||
const robloxResolveResp = await fetch(
|
||||
@@ -66,21 +66,20 @@ export async function onRequestPatch(context: RequestContext) {
|
||||
if (!data.length)
|
||||
return jsonError("No Roblox user exists with that name", 400);
|
||||
|
||||
updates.push({ query: "roblox_id = ?", value: data[0].id });
|
||||
queryData.roblox_id = data[0].id;
|
||||
}
|
||||
|
||||
await context.env.D1.prepare(
|
||||
`UPDATE et_members
|
||||
SET ${updates.map((u) => u.query).join(", ")}
|
||||
WHERE id = ?;`,
|
||||
)
|
||||
.bind(...updates.map((u) => u.value), body.id)
|
||||
.run();
|
||||
await context.data.prisma.etMember.update({
|
||||
data: queryData,
|
||||
where: {
|
||||
id: body.id,
|
||||
},
|
||||
});
|
||||
|
||||
return jsonResponse(
|
||||
JSON.stringify({
|
||||
name: body.name,
|
||||
roblox_id: updates.find((u) => typeof u.value === "number")?.value,
|
||||
roblox_id: queryData.roblox_id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -100,9 +99,11 @@ export async function onRequestPost(context: RequestContext) {
|
||||
return jsonError("Invalid name", 400);
|
||||
|
||||
if (
|
||||
await context.env.D1.prepare("SELECT * FROM et_members WHERE id = ?;")
|
||||
.bind(id)
|
||||
.first()
|
||||
await context.data.prisma.etMember.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
})
|
||||
)
|
||||
return jsonError("User is already a member", 400);
|
||||
|
||||
@@ -151,14 +152,16 @@ export async function onRequestPost(context: RequestContext) {
|
||||
roblox_id = data[0].id;
|
||||
}
|
||||
|
||||
const createdAt = Date.now();
|
||||
const addingUser = context.data.current_user.id;
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"INSERT INTO et_members (created_at, created_by, id, name, roblox_id) VALUES (?, ?, ?, ?, ?);",
|
||||
)
|
||||
.bind(createdAt, addingUser, id, name, roblox_id || null)
|
||||
.run();
|
||||
await context.data.prisma.etMember.create({
|
||||
data: {
|
||||
created_by: addingUser,
|
||||
id,
|
||||
name,
|
||||
roblox_id,
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
|
||||
@@ -7,27 +7,25 @@ export async function onRequestPost(context: RequestContext) {
|
||||
if (statsReduction && typeof statsReduction !== "number")
|
||||
return jsonError("Invalid stat reduction", 400);
|
||||
|
||||
const appeal: Record<string, any> | null = await context.env.D1.prepare(
|
||||
"SELECT * FROM game_appeals WHERE id = ?;",
|
||||
)
|
||||
.bind(context.params.id)
|
||||
.first();
|
||||
const appeal = await context.data.prisma.gameAppeal.findUnique({
|
||||
select: {
|
||||
roblox_id: true,
|
||||
type: true,
|
||||
},
|
||||
where: {
|
||||
id: context.params.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (!appeal) return jsonError("Appeal not found", 400);
|
||||
|
||||
const banList = (await getBanList(context)) as {
|
||||
[k: string]: {
|
||||
BanType: number;
|
||||
hidden_from_leaderboards?: boolean;
|
||||
serverconfigurator_blacklist?: boolean;
|
||||
Unbanned?: boolean;
|
||||
UnbanReduct?: number;
|
||||
};
|
||||
};
|
||||
const { etag, value: banList } = await getBanList(context);
|
||||
|
||||
await context.env.D1.prepare("DELETE FROM game_appeals WHERE id = ?;")
|
||||
.bind(context.params.id)
|
||||
.run();
|
||||
await context.data.prisma.gameAppeal.delete({
|
||||
where: {
|
||||
id: context.params.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (!banList[appeal.roblox_id]?.BanType)
|
||||
return new Response(null, {
|
||||
@@ -54,20 +52,17 @@ export async function onRequestPost(context: RequestContext) {
|
||||
};
|
||||
}
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"INSERT INTO game_mod_logs (action, evidence, executed_at, executor, id, target) VALUES (?, ?, ?, ?, ?, ?);",
|
||||
)
|
||||
.bind(
|
||||
`accept appeal | ${banList[appeal.roblox_id]?.BanType === 2 ? "ban" : appeal.type}`,
|
||||
`https://carcrushers.cc/mod-queue?id=${context.params.id}&type=gma`,
|
||||
Date.now(),
|
||||
context.data.current_user.id,
|
||||
crypto.randomUUID(),
|
||||
appeal.roblox_id,
|
||||
)
|
||||
.run();
|
||||
await context.data.prisma.gameModLog.create({
|
||||
data: {
|
||||
action: `accept appeal | ${banList[appeal.roblox_id]?.BanType === 2 ? "ban" : appeal.type}`,
|
||||
evidence: `https://carcrushers.cc/mod-queue?id=${context.params.id}&type=gma`,
|
||||
executor: context.data.current_user.id,
|
||||
id: crypto.randomUUID(),
|
||||
target: appeal.roblox_id,
|
||||
},
|
||||
});
|
||||
|
||||
await setBanList(context, banList);
|
||||
await setBanList(context, banList, etag);
|
||||
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
|
||||
@@ -1,19 +1,37 @@
|
||||
import { jsonError } from "../../../common.js";
|
||||
import { getBanList } from "../../../roblox-open-cloud.js";
|
||||
|
||||
export async function onRequestPost(context: RequestContext) {
|
||||
const appealId = context.params.id as string;
|
||||
|
||||
const appeal = await context.env.D1.prepare(
|
||||
"SELECT * FROM game_appeals WHERE id = ?;",
|
||||
)
|
||||
.bind(appealId)
|
||||
.first();
|
||||
const appeal = await context.data.prisma.gameAppeal.findUnique({
|
||||
select: {
|
||||
roblox_id: true,
|
||||
type: true,
|
||||
},
|
||||
where: {
|
||||
id: appealId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!appeal) return jsonError("Appeal not found", 404);
|
||||
|
||||
await context.env.D1.prepare("DELETE FROM game_appeals WHERE id = ?;")
|
||||
.bind(appealId)
|
||||
.run();
|
||||
await context.data.prisma.gameAppeal.delete({
|
||||
where: {
|
||||
id: appealId,
|
||||
},
|
||||
});
|
||||
|
||||
const { value: banList } = await getBanList(context);
|
||||
|
||||
await context.data.prisma.gameModLog.create({
|
||||
data: {
|
||||
action: `deny appeal | ${banList[appeal.roblox_id]?.BanType === 2 ? "ban" : appeal.type}`,
|
||||
evidence: `https://carcrushers.cc/mod-queue?id=${context.params.id}&type=gma`,
|
||||
executor: context.data.current_user.id,
|
||||
id: crypto.randomUUID(),
|
||||
target: appeal.roblox_id,
|
||||
},
|
||||
});
|
||||
|
||||
await context.env.DATA.put(
|
||||
`gameappealblock_${appeal.roblox_id}`,
|
||||
|
||||
@@ -10,11 +10,14 @@ export default async function (
|
||||
types?: string[];
|
||||
}> {
|
||||
if (
|
||||
await context.env.D1.prepare(
|
||||
"SELECT * FROM game_appeals WHERE roblox_id = ?;",
|
||||
)
|
||||
.bind(user)
|
||||
.first()
|
||||
await context.data.prisma.gameAppeal.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
roblox_id: user,
|
||||
},
|
||||
})
|
||||
)
|
||||
return {
|
||||
can_appeal: false,
|
||||
@@ -24,13 +27,7 @@ export default async function (
|
||||
let banList;
|
||||
|
||||
try {
|
||||
banList = (await getBanList(context)) as {
|
||||
[k: number]: {
|
||||
BanType: number;
|
||||
hidden_from_leaderboards?: boolean;
|
||||
serverconfigurator_blacklist?: boolean;
|
||||
};
|
||||
};
|
||||
banList = (await getBanList(context)).value;
|
||||
} catch {
|
||||
return {
|
||||
error: "Failed to check your ban status",
|
||||
@@ -53,22 +50,20 @@ export default async function (
|
||||
).toLocaleString()} to submit another appeal`,
|
||||
};
|
||||
|
||||
const userLogs = await context.env.D1.prepare(
|
||||
"SELECT executed_at FROM game_mod_logs WHERE target = ? ORDER BY executed_at DESC;",
|
||||
)
|
||||
.bind(user)
|
||||
.all();
|
||||
|
||||
if (userLogs.error)
|
||||
return {
|
||||
error: "Could not determine your eligibility",
|
||||
};
|
||||
const userLogs = await context.data.prisma.gameModLog.findMany({
|
||||
select: {
|
||||
action: true,
|
||||
executed_at: true,
|
||||
},
|
||||
where: {
|
||||
target: user,
|
||||
},
|
||||
});
|
||||
|
||||
// Legacy bans
|
||||
if (!userLogs.results.length)
|
||||
return { can_appeal: true, reason: "", types: ["ban"] };
|
||||
if (!userLogs.length) return { can_appeal: true, reason: "", types: ["ban"] };
|
||||
|
||||
const allowedTime = (userLogs.results[0].executed_at as number) + 2592000000;
|
||||
const allowedTime = new Date(userLogs[0].executed_at).getTime() + 2592000000;
|
||||
|
||||
if (Date.now() < allowedTime)
|
||||
return {
|
||||
@@ -78,11 +73,7 @@ export default async function (
|
||||
).toLocaleString()} to submit an appeal`,
|
||||
};
|
||||
|
||||
if (
|
||||
userLogs.results.find((r: Record<string, any>) =>
|
||||
r.action.startsWith("accept appeal"),
|
||||
)
|
||||
)
|
||||
if (userLogs.find((r) => r.action.startsWith("accept appeal")))
|
||||
return {
|
||||
can_appeal: false,
|
||||
reason: "We do not accept appeals from repeat offenders",
|
||||
|
||||
@@ -52,19 +52,16 @@ export async function onRequestPost(context: RequestContext) {
|
||||
context.request.headers.get("cf-ray")?.split("-")[0]
|
||||
}${Date.now()}`;
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"INSERT INTO game_appeals (created_at, id, reason_for_unban, roblox_id, roblox_username, type, what_happened) VALUES (?, ?, ?, ?, ?, ?, ?);",
|
||||
)
|
||||
.bind(
|
||||
Date.now(),
|
||||
appealId,
|
||||
reasonForUnban,
|
||||
id,
|
||||
username,
|
||||
await context.data.prisma.gameAppeal.create({
|
||||
data: {
|
||||
id: appealId,
|
||||
reason_for_unban: reasonForUnban,
|
||||
roblox_id: id,
|
||||
roblox_username: username,
|
||||
type,
|
||||
whatHappened,
|
||||
)
|
||||
.run();
|
||||
what_happened: whatHappened,
|
||||
},
|
||||
});
|
||||
|
||||
await fetch(context.env.REPORTS_WEBHOOK, {
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function onRequestGet(context: RequestContext) {
|
||||
);
|
||||
|
||||
if (!robloxUserReq.ok) {
|
||||
console.log(await robloxUserReq.json());
|
||||
console.log(await robloxUserReq.text());
|
||||
return jsonError("Failed to resolve username", 500);
|
||||
}
|
||||
|
||||
@@ -33,13 +33,7 @@ export async function onRequestGet(context: RequestContext) {
|
||||
let banList;
|
||||
|
||||
try {
|
||||
banList = (await getBanList(context)) as {
|
||||
[k: number]: {
|
||||
BanType: number;
|
||||
hidden_from_leaderboards?: boolean;
|
||||
serverconfigurator_blacklist?: boolean;
|
||||
};
|
||||
};
|
||||
banList = (await getBanList(context)).value;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
|
||||
@@ -64,13 +58,14 @@ export async function onRequestGet(context: RequestContext) {
|
||||
} else if (banData.BanType === 2) current_status = "Banned";
|
||||
|
||||
const response = {
|
||||
history: (
|
||||
await context.env.D1.prepare(
|
||||
"SELECT * FROM game_mod_logs WHERE target = ? ORDER BY executed_at DESC;",
|
||||
)
|
||||
.bind(users[0].id)
|
||||
.all()
|
||||
).results,
|
||||
history: await context.data.prisma.gameModLog.findMany({
|
||||
orderBy: {
|
||||
executed_at: "desc",
|
||||
},
|
||||
where: {
|
||||
target: users[0].id,
|
||||
},
|
||||
}),
|
||||
user: {
|
||||
avatar: thumbnailRequest.ok
|
||||
? (
|
||||
|
||||
@@ -18,25 +18,20 @@ export async function onRequestPost(context: RequestContext) {
|
||||
|
||||
if (isNaN(parseInt(user))) return jsonError("Invalid user ID", 400);
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"INSERT INTO game_mod_logs (action, evidence, executed_at, executor, id, target) VALUES (?, ?, ?, ?, ?, ?);",
|
||||
)
|
||||
.bind(
|
||||
"revoke",
|
||||
ticket_link,
|
||||
Date.now(),
|
||||
context.data.current_user.id,
|
||||
crypto.randomUUID(),
|
||||
parseInt(user),
|
||||
)
|
||||
.run();
|
||||
await context.data.prisma.gameModLog.create({
|
||||
data: {
|
||||
action: "revoke",
|
||||
evidence: ticket_link,
|
||||
executor: context.data.current_user.id,
|
||||
id: crypto.randomUUID(),
|
||||
target: parseInt(user),
|
||||
},
|
||||
});
|
||||
|
||||
const banList = (await getBanList(context)) as {
|
||||
[k: string]: { BanType: number };
|
||||
};
|
||||
const { etag, value: banList } = await getBanList(context);
|
||||
|
||||
delete banList[user];
|
||||
await setBanList(context, banList);
|
||||
await setBanList(context, banList, etag);
|
||||
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
|
||||
@@ -2,38 +2,50 @@ import { jsonError, jsonResponse } from "../../../common.js";
|
||||
|
||||
export async function onRequestDelete(context: RequestContext) {
|
||||
const noteId = context.params.id as string;
|
||||
const creatorIdResult: null | Record<string, string> =
|
||||
await context.env.D1.prepare(
|
||||
"SELECT created_by FROM game_mod_logs WHERE id = ?;",
|
||||
)
|
||||
.bind(noteId)
|
||||
.first();
|
||||
const creatorIdResult = await context.data.prisma.gameModNote.findUnique({
|
||||
select: {
|
||||
created_by: true,
|
||||
},
|
||||
where: {
|
||||
id: noteId,
|
||||
},
|
||||
});
|
||||
|
||||
if (creatorIdResult?.created_by !== context.data.current_user.id)
|
||||
try {
|
||||
await context.data.prisma.gameModNote.delete({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
created_by: context.data.current_user.id,
|
||||
id: noteId,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return jsonError("Cannot delete notes that are not your own", 403);
|
||||
|
||||
await context.env.D1.prepare("DELETE FROM game_mod_logs WHERE id = ?;")
|
||||
.bind(noteId)
|
||||
.first();
|
||||
}
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
export async function onRequestGet(context: RequestContext) {
|
||||
const noteId = context.params.id as string;
|
||||
const result = await context.env.D1.prepare(
|
||||
"SELECT * FROM game_mod_notes WHERE id = ?;",
|
||||
)
|
||||
.bind(noteId)
|
||||
.first();
|
||||
const result = await context.data.prisma.gameModNote.findUnique({
|
||||
where: {
|
||||
id: noteId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!result) return jsonError("Note not found", 404);
|
||||
|
||||
const noteData = structuredClone(result);
|
||||
let noteData = structuredClone(result);
|
||||
const gmeEntry: null | { time: number; user: string; name: string } =
|
||||
await context.env.DATA.get(`gamemod_${result.created_by}`, "json");
|
||||
|
||||
if (gmeEntry) noteData.creator_name = gmeEntry.name;
|
||||
if (gmeEntry)
|
||||
noteData = Object.defineProperty(noteData, "creator_name", {
|
||||
value: gmeEntry.name,
|
||||
});
|
||||
|
||||
return jsonResponse(JSON.stringify(noteData));
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { jsonError, jsonResponse } from "../../common.js";
|
||||
import sendEmail from "../../email.js";
|
||||
import { sendPushNotification } from "../../gcloud.js";
|
||||
import { type JsonArray, type JsonObject } from "@prisma/client/runtime/client";
|
||||
|
||||
export async function onRequestDelete(context: RequestContext) {
|
||||
const result = await context.env.D1.prepare(
|
||||
"SELECT json_extract(user, '*.id') AS uid FROM inactivity_notices WHERE id = ?;",
|
||||
)
|
||||
.bind(context.params.id)
|
||||
.first();
|
||||
const result = await context.data.prisma.inactivityNotice.findUnique({
|
||||
select: {
|
||||
user: true,
|
||||
},
|
||||
where: {
|
||||
id: context.params.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (!result) return jsonError("No inactivity notice with that ID", 404);
|
||||
|
||||
if (
|
||||
result.uid !== context.data.current_user.id &&
|
||||
(result.user as JsonObject).id !== context.data.current_user.id &&
|
||||
!(context.data.current_user.permissions & (1 << 0))
|
||||
)
|
||||
return jsonError(
|
||||
@@ -39,26 +43,17 @@ export async function onRequestGet(context: RequestContext) {
|
||||
)
|
||||
return jsonError("Forbidden", 403);
|
||||
|
||||
const result: Record<
|
||||
string,
|
||||
string | number | { [k: string]: string }
|
||||
> | null = await context.env.D1.prepare(
|
||||
"SELECT * FROM inactivity_notices WHERE id = ?;",
|
||||
)
|
||||
.bind(context.params.id)
|
||||
.first();
|
||||
|
||||
if (!result) return jsonError("Inactivity notice does not exist", 404);
|
||||
|
||||
result.decisions = JSON.parse(result.decisions as string);
|
||||
result.departments = JSON.parse(result.departments as string);
|
||||
result.user = JSON.parse(result.user as string);
|
||||
const result = await context.data.prisma.inactivityNotice.findUnique({
|
||||
where: {
|
||||
id: context.params.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
return jsonResponse(JSON.stringify(result));
|
||||
}
|
||||
|
||||
export async function onRequestPost(context: RequestContext) {
|
||||
const { accepted }: { accepted?: boolean } = context.data.body;
|
||||
const { accepted }: { accepted?: any } = context.data.body;
|
||||
|
||||
if (typeof accepted !== "boolean")
|
||||
return jsonError("'accepted' must be a boolean", 400);
|
||||
@@ -77,32 +72,45 @@ export async function onRequestPost(context: RequestContext) {
|
||||
if (!userAdminDepartments.length)
|
||||
return jsonError("You are not a manager of any departments", 403);
|
||||
|
||||
const requestedNotice: { [k: string]: any } | null =
|
||||
await context.env.D1.prepare(
|
||||
"SELECT decisions, departments, user FROM inactivity_notices WHERE id = ?;",
|
||||
)
|
||||
.bind(context.params.id)
|
||||
.first();
|
||||
const requestedNotice = await context.data.prisma.inactivityNotice.findUnique(
|
||||
{
|
||||
select: {
|
||||
decisions: true,
|
||||
departments: true,
|
||||
user: true,
|
||||
},
|
||||
where: {
|
||||
id: context.params.id as string,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!requestedNotice)
|
||||
return jsonError("Inactivity notices does not exist", 404);
|
||||
|
||||
const decisions: { [dept: string]: boolean } = JSON.parse(
|
||||
requestedNotice.decisions,
|
||||
);
|
||||
const decisions = requestedNotice.decisions as { [k: string]: boolean };
|
||||
|
||||
for (const department of userAdminDepartments) {
|
||||
if (!JSON.parse(requestedNotice.departments).includes(department)) continue;
|
||||
if (!(requestedNotice.departments as JsonArray).includes(department))
|
||||
continue;
|
||||
decisions[department] = accepted;
|
||||
}
|
||||
|
||||
const applicableDepartments = JSON.parse(requestedNotice.departments).length;
|
||||
const applicableDepartments = (requestedNotice.departments as JsonArray)
|
||||
.length;
|
||||
const user = requestedNotice.user as JsonObject;
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"UPDATE inactivity_notices SET decisions = ?, user = json_remove(user, '$.email') WHERE id = ?;",
|
||||
)
|
||||
.bind(JSON.stringify(decisions), context.params.id)
|
||||
.run();
|
||||
delete user.email;
|
||||
|
||||
await context.data.prisma.inactivityNotice.update({
|
||||
data: {
|
||||
decisions,
|
||||
user,
|
||||
},
|
||||
where: {
|
||||
id: context.params.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (Object.values(decisions).length === applicableDepartments) {
|
||||
const approved =
|
||||
@@ -111,11 +119,16 @@ export async function onRequestPost(context: RequestContext) {
|
||||
const denied =
|
||||
Object.values(decisions).filter((d) => !d).length !==
|
||||
applicableDepartments;
|
||||
const fcmTokenResult: FCMTokenResult | null = await context.env.D1.prepare(
|
||||
"SELECT token FROM push_notifications WHERE event_id = ? AND event_type = 'inactivity';",
|
||||
)
|
||||
.bind(context.params.id)
|
||||
.first();
|
||||
const fcmTokenResult =
|
||||
await context.data.prisma.pushNotification.findUnique({
|
||||
select: {
|
||||
token: true,
|
||||
},
|
||||
where: {
|
||||
event_id: context.params.id as string,
|
||||
event_type: "inactivity",
|
||||
},
|
||||
});
|
||||
|
||||
if (fcmTokenResult) {
|
||||
let status = "Approved";
|
||||
@@ -132,16 +145,19 @@ export async function onRequestPost(context: RequestContext) {
|
||||
fcmTokenResult.token,
|
||||
);
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"DELETE FROM push_notifications WHERE event_id = ? AND event_type = 'inactivity';",
|
||||
).bind(context.params.id);
|
||||
await context.data.prisma.pushNotification.delete({
|
||||
where: {
|
||||
event_id: context.params.id as string,
|
||||
event_type: "inactivity",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await sendEmail(
|
||||
requestedNotice.user.email,
|
||||
(requestedNotice.user as JsonObject).email as string,
|
||||
context.env.MAILGUN_API_KEY,
|
||||
`Inactivity Request ${approved ? "Approved" : "Denied"}`,
|
||||
`inactivity_${approved ? "approved" : "denied"}`,
|
||||
{ username: requestedNotice.user.username },
|
||||
{ username: (requestedNotice.user as JsonObject).username as string },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,31 +20,31 @@ export async function onRequestPost(context: RequestContext) {
|
||||
(context.request.headers.get("cf-ray") as string).split("-")[0] +
|
||||
Date.now().toString();
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"INSERT INTO inactivity_notices (created_at, departments, end, hiatus, id, reason, start, user) VALUES (?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
)
|
||||
.bind(
|
||||
Date.now(),
|
||||
JSON.stringify(departments),
|
||||
await context.data.prisma.inactivityNotice.create({
|
||||
data: {
|
||||
decisions: {},
|
||||
departments,
|
||||
end,
|
||||
typeof hiatus === "boolean" ? Number(hiatus) : 0,
|
||||
inactivityId,
|
||||
hiatus,
|
||||
id: inactivityId,
|
||||
reason,
|
||||
start,
|
||||
JSON.stringify({
|
||||
user: {
|
||||
id: context.data.current_user.id,
|
||||
email: context.data.current_user.email,
|
||||
username: context.data.current_user.username,
|
||||
}),
|
||||
)
|
||||
.run();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (typeof senderTokenId === "string") {
|
||||
await context.env.D1.prepare(
|
||||
"INSERT INTO push_notifications (created_at, event_id, event_type) VALUES (?, ?, ?);",
|
||||
)
|
||||
.bind(Date.now(), inactivityId, "inactivity")
|
||||
.run();
|
||||
await context.data.prisma.pushNotification.create({
|
||||
data: {
|
||||
event_id: inactivityId,
|
||||
event_type: "inactivity",
|
||||
token: senderTokenId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const departmentsToNotify = [];
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { jsonError, jsonResponse } from "../../common.js";
|
||||
import { jsonResponse } from "../../common.js";
|
||||
|
||||
export async function onRequestGet(context: RequestContext) {
|
||||
const { results, success } = await context.env.D1.prepare(
|
||||
"SELECT approved, created_at, id, open FROM appeals WHERE user = ?;",
|
||||
)
|
||||
.bind(context.data.current_user.id)
|
||||
.all();
|
||||
|
||||
if (!success) return jsonError("Unable to retrieve appeals", 500);
|
||||
|
||||
return jsonResponse(
|
||||
JSON.stringify(
|
||||
results.map((result) => {
|
||||
result.user = JSON.parse(result.user as string);
|
||||
|
||||
return result;
|
||||
await context.data.prisma.appeal.findMany({
|
||||
select: {
|
||||
approved: true,
|
||||
created_at: true,
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
user: {
|
||||
path: "id",
|
||||
equals: context.data.current_user.id,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { jsonError, jsonResponse } from "../../../../common.js";
|
||||
import {
|
||||
type JsonArray,
|
||||
type JsonObject,
|
||||
} from "../../../../../generated/prisma/internal/prismaNamespace.js";
|
||||
|
||||
export async function onRequestGet(context: RequestContext) {
|
||||
const { id, type } = context.params;
|
||||
@@ -6,57 +10,68 @@ export async function onRequestGet(context: RequestContext) {
|
||||
if (!["appeal", "inactivity", "report"].includes(type as string))
|
||||
return jsonError("Invalid type", 400);
|
||||
|
||||
const tables: { [k: string]: string } = {
|
||||
appeal: "appeals",
|
||||
inactivity: "inactivity_notices",
|
||||
report: "reports",
|
||||
};
|
||||
const { prisma } = context.data;
|
||||
let item;
|
||||
|
||||
const data: Record<string, any> | null = await context.env.D1.prepare(
|
||||
`SELECT *
|
||||
FROM ${tables[type as string]}
|
||||
WHERE id = ?;`,
|
||||
switch (type as string) {
|
||||
case "appeal":
|
||||
item = await prisma.appeal.findUnique({
|
||||
where: {
|
||||
id: id as string,
|
||||
},
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case "inactivity":
|
||||
item = await prisma.inactivityNotice.findUnique({
|
||||
where: {
|
||||
id: id as string,
|
||||
},
|
||||
});
|
||||
|
||||
break;
|
||||
case "report":
|
||||
item = await prisma.report.findUnique({
|
||||
where: {
|
||||
id: id as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (!item) break;
|
||||
|
||||
const { AwsClient } = await import("aws4fetch");
|
||||
const aws = new AwsClient({
|
||||
accessKeyId: context.env.R2_ACCESS_KEY,
|
||||
secretAccessKey: context.env.R2_SECRET_KEY,
|
||||
});
|
||||
let urlPromises = [];
|
||||
for (const attachment of item.attachments as JsonArray) {
|
||||
urlPromises.push(
|
||||
aws.sign(
|
||||
`https://car-crushers.${context.env.R2_ZONE}.r2.cloudflarestorage.com/${attachment}?X-Amz-Expires=1800`,
|
||||
{
|
||||
aws: {
|
||||
signQuery: true,
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
const urls = (await Promise.all(urlPromises)).map((p) => p.url);
|
||||
item = { ...item, resolved_attachments: urls };
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
!item ||
|
||||
(item.user as JsonObject | undefined)?.id !== context.data.current_user.id
|
||||
)
|
||||
.bind(id)
|
||||
.first();
|
||||
|
||||
if (data?.user) data.user = JSON.parse(data.user);
|
||||
|
||||
if (!data || data.user?.id !== context.data.current_user.id)
|
||||
return jsonError("Item does not exist", 404);
|
||||
|
||||
if (type === "inactivity") {
|
||||
data.decisions = JSON.parse(data.decisions);
|
||||
data.departments = JSON.parse(data.departments);
|
||||
}
|
||||
|
||||
if (type === "report") {
|
||||
data.attachments = JSON.parse(data.attachments);
|
||||
data.target_ids = JSON.parse(data.target_ids);
|
||||
data.target_usernames = JSON.parse(data.target_usernames);
|
||||
const { AwsClient } = await import("aws4fetch");
|
||||
const aws = new AwsClient({
|
||||
accessKeyId: context.env.R2_ACCESS_KEY,
|
||||
secretAccessKey: context.env.R2_SECRET_KEY,
|
||||
});
|
||||
|
||||
let urls = [];
|
||||
|
||||
for (const attachment of data.attachments) {
|
||||
const { url } = await aws.sign(
|
||||
`https://car-crushers.${context.env.R2_ZONE}.r2.cloudflarestorage.com/${attachment}?X-Amz-Expires=1800`,
|
||||
{
|
||||
aws: {
|
||||
signQuery: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
urls.push(url);
|
||||
}
|
||||
|
||||
data.resolved_attachments = urls;
|
||||
}
|
||||
|
||||
return jsonResponse(JSON.stringify(data));
|
||||
return jsonResponse(JSON.stringify(item));
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { jsonError, jsonResponse } from "../../common.js";
|
||||
import { jsonResponse } from "../../common.js";
|
||||
|
||||
export async function onRequestGet(context: RequestContext) {
|
||||
const {
|
||||
results,
|
||||
success,
|
||||
}: {
|
||||
results: { id: string }[];
|
||||
success: boolean;
|
||||
} = await context.env.D1.prepare(
|
||||
"SELECT created_at, id, open, target_usernames FROM reports WHERE json_extract(user, '$.id') = ? ORDER BY created_at LIMIT 50;",
|
||||
)
|
||||
.bind(context.data.current_user.id)
|
||||
.all();
|
||||
|
||||
if (!success) return jsonError("Failed to retrieve reports", 500);
|
||||
|
||||
return jsonResponse(JSON.stringify(results));
|
||||
return jsonResponse(
|
||||
JSON.stringify(
|
||||
await context.data.prisma.report.findMany({
|
||||
select: {
|
||||
created_at: true,
|
||||
id: true,
|
||||
open: true,
|
||||
target_usernames: true,
|
||||
},
|
||||
where: {
|
||||
user: {
|
||||
path: "id",
|
||||
equals: context.data.current_user.id,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,68 +1,71 @@
|
||||
import { jsonError, jsonResponse } from "../../../common.js";
|
||||
import { type JsonObject } from "../../../../generated/prisma/internal/prismaNamespace.js";
|
||||
|
||||
export async function onRequestGet(context: RequestContext) {
|
||||
const types: {
|
||||
[k: string]: { permissions: number[]; table: string };
|
||||
} = {
|
||||
appeal: {
|
||||
permissions: [1 << 0, 1 << 11],
|
||||
table: "appeals",
|
||||
},
|
||||
gma: {
|
||||
permissions: [1 << 5],
|
||||
table: "game_appeals",
|
||||
},
|
||||
inactivity: {
|
||||
permissions: [1 << 0, 1 << 4, 1 << 6, 1 << 7, 1 << 11],
|
||||
table: "inactivity_notices",
|
||||
},
|
||||
report: {
|
||||
permissions: [1 << 5],
|
||||
table: "reports",
|
||||
},
|
||||
const types: { [k: string]: number[] } = {
|
||||
appeal: [1 << 0, 1 << 11],
|
||||
gma: [1 << 5],
|
||||
inactivity: [1 << 0, 1 << 4, 1 << 6, 1 << 7, 1 << 11],
|
||||
report: [1 << 5],
|
||||
};
|
||||
|
||||
const type = context.params.type as string;
|
||||
const itemId = context.params.id as string;
|
||||
|
||||
if (
|
||||
!types[type]?.permissions.find(
|
||||
(p) => context.data.current_user.permissions & p,
|
||||
)
|
||||
)
|
||||
if (!types[type]?.find((p) => context.data.current_user.permissions & p))
|
||||
return jsonError("You cannot use this filter", 403);
|
||||
|
||||
const item: Record<string, any> | null = await context.env.D1.prepare(
|
||||
`SELECT *
|
||||
FROM ${types[type].table}
|
||||
WHERE id = ?;`,
|
||||
)
|
||||
.bind(itemId)
|
||||
.first();
|
||||
let item;
|
||||
|
||||
switch (type) {
|
||||
case "appeal":
|
||||
item = await context.data.prisma.appeal.findUnique({
|
||||
where: {
|
||||
id: itemId,
|
||||
},
|
||||
});
|
||||
|
||||
if (item) delete (item.user as JsonObject).email;
|
||||
break;
|
||||
|
||||
case "gma":
|
||||
item = await context.data.prisma.gameAppeal.findUnique({
|
||||
where: {
|
||||
id: itemId,
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
case "inactivity":
|
||||
item = await context.data.prisma.inactivityNotice.findUnique({
|
||||
where: {
|
||||
id: itemId,
|
||||
},
|
||||
});
|
||||
|
||||
if (item) delete (item.user as JsonObject).email;
|
||||
break;
|
||||
|
||||
case "report":
|
||||
item = await context.data.prisma.report.findUnique({
|
||||
where: {
|
||||
id: itemId,
|
||||
},
|
||||
});
|
||||
|
||||
if (item) delete (item.user as JsonObject | null)?.email;
|
||||
break;
|
||||
|
||||
default:
|
||||
return jsonError("Unknown filter", 400);
|
||||
}
|
||||
|
||||
if (!item) return jsonError("Item not found", 404);
|
||||
|
||||
if (type === "report") {
|
||||
if (await context.env.DATA.get(`reportprocessing_${itemId}`))
|
||||
return jsonError("Report is processing", 409);
|
||||
|
||||
item.attachments = JSON.parse(item.attachments);
|
||||
item.target_ids = JSON.parse(item.target_ids);
|
||||
item.target_usernames = JSON.parse(item.target_usernames);
|
||||
}
|
||||
|
||||
if (item.user) {
|
||||
item.user = JSON.parse(item.user);
|
||||
|
||||
delete item.user.email;
|
||||
}
|
||||
|
||||
if (type === "inactivity") {
|
||||
item.decisions = JSON.parse(item.decisions);
|
||||
item.departments = JSON.parse(item.departments);
|
||||
}
|
||||
|
||||
return item
|
||||
? jsonResponse(JSON.stringify(item))
|
||||
: jsonError("Not found", 404);
|
||||
return jsonResponse(JSON.stringify(item));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { jsonError, jsonResponse } from "../../../common.js";
|
||||
import { InactivityNotice } from "../../../../generated/prisma/client.js";
|
||||
import {
|
||||
JsonObject,
|
||||
raw,
|
||||
} from "../../../../generated/prisma/internal/prismaNamespace.js";
|
||||
|
||||
export async function onRequestGet(context: RequestContext): Promise<any> {
|
||||
const type = context.params.type as string;
|
||||
const { prisma } = context.data;
|
||||
const { searchParams } = new URL(context.request.url);
|
||||
const before = parseInt(searchParams.get("before") || `${Date.now()}`);
|
||||
const showClosed = searchParams.get("showClosed") === "true";
|
||||
@@ -26,73 +32,77 @@ export async function onRequestGet(context: RequestContext): Promise<any> {
|
||||
|
||||
if (isNaN(before)) return jsonError("Invalid `before` parameter", 400);
|
||||
|
||||
let rows: D1Result<Record<string, any>>;
|
||||
let rows;
|
||||
|
||||
switch (type) {
|
||||
case "appeal":
|
||||
rows = await context.env.D1.prepare(
|
||||
`SELECT *
|
||||
FROM appeals
|
||||
WHERE created_at < ?
|
||||
AND approved ${showClosed ? "IS NOT" : "IS"} NULL
|
||||
ORDER BY created_at DESC LIMIT 25;`,
|
||||
)
|
||||
.bind(before)
|
||||
.all();
|
||||
rows.results = rows.results.map((r) => {
|
||||
r.user = JSON.parse(r.user);
|
||||
delete r.user.email;
|
||||
rows = await prisma.appeal.findMany({
|
||||
orderBy: {
|
||||
created_at: "desc",
|
||||
},
|
||||
take: 25,
|
||||
where: {
|
||||
created_at: {
|
||||
lt: new Date(before),
|
||||
},
|
||||
approved: showClosed ? { not: null } : null,
|
||||
},
|
||||
});
|
||||
rows.map((r) => {
|
||||
delete (r.user as JsonObject).email;
|
||||
|
||||
return r;
|
||||
});
|
||||
break;
|
||||
|
||||
case "gma":
|
||||
rows = await context.env.D1.prepare(
|
||||
"SELECT * FROM game_appeals WHERE created_at < ? ORDER BY created_at DESC LIMIT 25;",
|
||||
)
|
||||
.bind(before)
|
||||
.all();
|
||||
rows = await prisma.gameAppeal.findMany({
|
||||
orderBy: {
|
||||
created_at: "desc",
|
||||
},
|
||||
take: 25,
|
||||
where: {
|
||||
created_at: {
|
||||
lt: new Date(before),
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
case "inactivity":
|
||||
rows = await context.env.D1.prepare(
|
||||
`SELECT *,
|
||||
(SELECT COUNT(*) FROM json_each(decisions)) as decision_count
|
||||
FROM inactivity_notices
|
||||
WHERE created_at < ?
|
||||
AND decision_count ${showClosed ? "=" : "!="} json_array_length(departments)`,
|
||||
)
|
||||
.bind(before)
|
||||
.all();
|
||||
rows = await prisma.$queryRaw<
|
||||
InactivityNotice[] & { decision_count: number }[]
|
||||
>`
|
||||
SELECT *, (SELECT COUNT(*) FROM json_each(decisions)) AS decision_count FROM inactivity_notices WHERE created_at < datetime(${before} / 1000, 'unixepoch') AND decision_count ${showClosed ? raw("=") : raw("!=")} json_array_length(departments);`;
|
||||
|
||||
rows.results.map((r) => {
|
||||
r.decisions = JSON.parse(r.decisions);
|
||||
r.departments = JSON.parse(r.departments);
|
||||
r.user = JSON.parse(r.user);
|
||||
rows.map((r) => {
|
||||
// These come back as strings when using $queryRaw
|
||||
r.decisions = JSON.parse(r.decisions as string);
|
||||
r.departments = JSON.parse(r.departments as string);
|
||||
r.user = JSON.parse(r.user as string);
|
||||
|
||||
delete r.user.email;
|
||||
delete (r.user as JsonObject).email;
|
||||
|
||||
return r;
|
||||
});
|
||||
break;
|
||||
|
||||
case "report":
|
||||
rows = await context.env.D1.prepare(
|
||||
"SELECT * FROM reports WHERE created_at < ? AND open = ? ORDER BY created_at DESC LIMIT 25;",
|
||||
)
|
||||
.bind(before, !Number(showClosed))
|
||||
.all();
|
||||
rows = await prisma.report.findMany({
|
||||
orderBy: {
|
||||
created_at: "desc",
|
||||
},
|
||||
take: 25,
|
||||
where: {
|
||||
created_at: {
|
||||
lt: new Date(before),
|
||||
},
|
||||
open: !showClosed,
|
||||
},
|
||||
});
|
||||
|
||||
rows.results = rows.results.map((r) => {
|
||||
r.attachments = JSON.parse(r.attachments);
|
||||
r.target_ids = JSON.parse(r.target_ids);
|
||||
r.target_usernames = JSON.parse(r.target_usernames);
|
||||
|
||||
if (r.user) {
|
||||
r.user = JSON.parse(r.user);
|
||||
delete r.user.email;
|
||||
}
|
||||
rows.map((r) => {
|
||||
delete (r.user as JsonObject | null)?.email;
|
||||
|
||||
return r;
|
||||
});
|
||||
@@ -103,5 +113,5 @@ export async function onRequestGet(context: RequestContext): Promise<any> {
|
||||
return jsonError("Unknown filter error", 500);
|
||||
}
|
||||
|
||||
return jsonResponse(JSON.stringify(rows.results));
|
||||
return jsonResponse(JSON.stringify(rows));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import sendEmail from "../../../email.js";
|
||||
import { sendPushNotification } from "../../../gcloud.js";
|
||||
|
||||
export async function onRequestPost(context: RequestContext) {
|
||||
const { prisma } = context.data;
|
||||
const reportId = context.params.id as string;
|
||||
const report: {
|
||||
[k: string]: any;
|
||||
@@ -78,23 +79,20 @@ export async function onRequestPost(context: RequestContext) {
|
||||
|
||||
await context.env.D1.batch(batchedQueries);
|
||||
|
||||
const banList = (await getBanList(context)) as {
|
||||
[k: string]: {
|
||||
BanType: number;
|
||||
hidden_from_leaderboards?: boolean;
|
||||
serverconfigurator_blacklist?: boolean;
|
||||
};
|
||||
};
|
||||
const { etag, value: banList } = await getBanList(context);
|
||||
|
||||
await setBanList(context, Object.assign(banList, newActions));
|
||||
await setBanList(context, Object.assign(banList, newActions), etag);
|
||||
}
|
||||
|
||||
const pushNotificationData: Record<string, string> | null =
|
||||
await context.env.D1.prepare(
|
||||
"SELECT token FROM push_notifications WHERE event_id = ? AND event_type = 'report';",
|
||||
)
|
||||
.bind(reportId)
|
||||
.first();
|
||||
const pushNotificationData = await prisma.pushNotification.findUnique({
|
||||
select: {
|
||||
token: true,
|
||||
},
|
||||
where: {
|
||||
event_id: reportId,
|
||||
event_type: "report",
|
||||
},
|
||||
});
|
||||
|
||||
if (user?.email)
|
||||
await sendEmail(
|
||||
@@ -106,26 +104,33 @@ export async function onRequestPost(context: RequestContext) {
|
||||
username: user.username as string,
|
||||
},
|
||||
);
|
||||
else if (pushNotificationData)
|
||||
else if (pushNotificationData) {
|
||||
await sendPushNotification(
|
||||
context.env,
|
||||
"Report Processed",
|
||||
`Your report for ${JSON.parse(report.target_usernames).toString()} has been reviewed.`,
|
||||
pushNotificationData.token,
|
||||
);
|
||||
await prisma.pushNotification.delete({
|
||||
where: {
|
||||
event_id: reportId,
|
||||
event_type: "report",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
delete (report.user as { email?: string; id: string; username: string })
|
||||
?.email;
|
||||
|
||||
await context.env.D1.prepare("UPDATE reports SET open = 0 WHERE id = ?;")
|
||||
.bind(reportId)
|
||||
.run();
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"DELETE FROM push_notifications WHERE event_id = ? AND event_type = 'report';",
|
||||
)
|
||||
.bind(reportId)
|
||||
.run();
|
||||
await prisma.report.update({
|
||||
data: {
|
||||
open: false,
|
||||
user: report.user,
|
||||
},
|
||||
where: {
|
||||
id: reportId,
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
|
||||
@@ -17,11 +17,10 @@ export async function onRequestPost(context: RequestContext) {
|
||||
|
||||
await context.env.DATA.delete(`reportprocessing_${id}`);
|
||||
|
||||
const value = await context.env.D1.prepare(
|
||||
"SELECT id FROM reports WHERE id = ?;",
|
||||
)
|
||||
.bind(id)
|
||||
.first();
|
||||
const value = await context.data.prisma.report.findUnique({
|
||||
select: { id: true },
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!value) return jsonError("Report is missing", 500);
|
||||
|
||||
|
||||
@@ -15,26 +15,27 @@ export async function onRequestPost(context: RequestContext) {
|
||||
)
|
||||
return jsonError("No processing report with that ID found", 404);
|
||||
|
||||
const data: Record<string, any> | null = await context.env.D1.prepare(
|
||||
"SELECT attachments FROM reports WHERE id = ?;",
|
||||
)
|
||||
.bind(id)
|
||||
.first();
|
||||
const data = await context.data.prisma.report.findUnique({
|
||||
select: {
|
||||
attachments: true,
|
||||
},
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data) return jsonError("No report with that ID found", 404);
|
||||
|
||||
data.attachments = JSON.parse(data.attachments);
|
||||
|
||||
const accessToken = await GetAccessToken(context.env);
|
||||
const attachmentDeletePromises = [];
|
||||
const existingAttachments = [...data.attachments];
|
||||
const existingAttachments = [...(data.attachments as string[])];
|
||||
|
||||
for (const attachment of existingAttachments) {
|
||||
if (!attachment.startsWith("t/")) data.attachments.push(`t/${attachment}`);
|
||||
else data.attachments.push(attachment.replace("t/", ""));
|
||||
for (let i = 0; i < existingAttachments.length; i++) {
|
||||
if (!existingAttachments[i].startsWith("t/"))
|
||||
existingAttachments[i] = existingAttachments[i].replace("t/", "");
|
||||
}
|
||||
|
||||
for (const attachment of data.attachments)
|
||||
for (const attachment of existingAttachments)
|
||||
attachmentDeletePromises.push(
|
||||
fetch(
|
||||
`https://storage.googleapis.com/storage/v1/b/portal-carcrushers-cc/o/${encodeURIComponent(
|
||||
@@ -50,9 +51,11 @@ export async function onRequestPost(context: RequestContext) {
|
||||
);
|
||||
|
||||
await Promise.allSettled(attachmentDeletePromises);
|
||||
await context.env.D1.prepare("DELETE FROM reports WHERE id = ?;")
|
||||
.bind(id)
|
||||
.run();
|
||||
await context.data.prisma.report.delete({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
|
||||
@@ -225,26 +225,31 @@ export async function onRequestPost(context: RequestContext) {
|
||||
attachments.push(new URL(urlResult.value).pathname.replace(/^\/?t?\//, ""));
|
||||
}
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"INSERT INTO reports (attachments, created_at, id, open, target_ids, target_usernames, type, user) VALUES (?, ?, ?, 1, ?, ?, ?, ?);",
|
||||
)
|
||||
.bind(
|
||||
JSON.stringify(attachments),
|
||||
Date.now(),
|
||||
reportId,
|
||||
JSON.stringify(metaIDs),
|
||||
JSON.stringify(metaNames),
|
||||
submissionType,
|
||||
currentUser ? JSON.stringify(currentUser) : null,
|
||||
)
|
||||
.run();
|
||||
await context.data.prisma.report.create({
|
||||
data: {
|
||||
attachments,
|
||||
id: reportId,
|
||||
target_ids: metaIDs,
|
||||
target_usernames: metaNames,
|
||||
type: submissionType,
|
||||
user: currentUser
|
||||
? {
|
||||
email: currentUser.email,
|
||||
id: currentUser.id,
|
||||
username: currentUser.username,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
if (typeof senderTokenId === "string")
|
||||
await context.env.D1.prepare(
|
||||
"INSERT INTO push_notifications (created_at, event_id, event_type, token) VALUES (?, ?, ?, ?);",
|
||||
)
|
||||
.bind(Date.now(), reportId, "report", senderTokenId)
|
||||
.run();
|
||||
await context.data.prisma.pushNotification.create({
|
||||
data: {
|
||||
event_id: reportId,
|
||||
event_type: reportId,
|
||||
token: senderTokenId,
|
||||
},
|
||||
});
|
||||
|
||||
return jsonResponse(
|
||||
JSON.stringify({ id: reportId, upload_urls: uploadUrls }),
|
||||
|
||||
@@ -3,13 +3,12 @@ import { jsonError } from "../../common.js";
|
||||
export async function onRequestDelete(context: RequestContext) {
|
||||
const path = decodeURIComponent(context.params.id as string);
|
||||
|
||||
if (typeof path !== "string") return jsonError("Invalid path", 400);
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"DELETE FROM short_links WHERE path = ? AND user = ?;",
|
||||
)
|
||||
.bind(path, context.data.current_user.id)
|
||||
.run();
|
||||
await context.data.prisma.shortLink.delete({
|
||||
where: {
|
||||
path,
|
||||
user: context.data.current_user.id,
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
@@ -39,15 +38,15 @@ export async function onRequestPatch(context: RequestContext) {
|
||||
|
||||
if (path.length > 256) return jsonError("Path is too long", 400);
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"UPDATE short_links SET path = ? WHERE path = ? AND user = ?;",
|
||||
)
|
||||
.bind(
|
||||
await context.data.prisma.shortLink.update({
|
||||
data: {
|
||||
path,
|
||||
decodeURIComponent(context.params.id as string),
|
||||
context.data.current_user.id,
|
||||
)
|
||||
.run();
|
||||
},
|
||||
where: {
|
||||
path: decodeURIComponent(context.params.id as string),
|
||||
user: context.data.current_user.id,
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { jsonResponse } from "../../common.js";
|
||||
|
||||
export async function onRequestGet(context: RequestContext) {
|
||||
const { results } = await context.env.D1.prepare(
|
||||
"SELECT created_at, destination, path FROM short_links WHERE user = ?;",
|
||||
)
|
||||
.bind(context.data.current_user.id)
|
||||
.all();
|
||||
|
||||
return jsonResponse(JSON.stringify(results));
|
||||
return jsonResponse(
|
||||
JSON.stringify(
|
||||
await context.data.prisma.shortLink.findMany({
|
||||
select: {
|
||||
created_at: true,
|
||||
destination: true,
|
||||
path: true,
|
||||
},
|
||||
where: {
|
||||
user: context.data.current_user.id,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@ export async function onRequestPost(context: RequestContext) {
|
||||
if (typeof path !== "string" || path.length > 256)
|
||||
return jsonError("Invalid path", 400);
|
||||
|
||||
const result = await context.env.D1.prepare(
|
||||
"SELECT path FROM short_links WHERE path = ?;",
|
||||
)
|
||||
.bind(path)
|
||||
.first();
|
||||
const result = await context.data.prisma.shortLink.findUnique({
|
||||
select: {
|
||||
path: true,
|
||||
},
|
||||
where: {
|
||||
path,
|
||||
},
|
||||
});
|
||||
|
||||
if (result)
|
||||
return jsonError(
|
||||
@@ -78,11 +81,13 @@ export async function onRequestPost(context: RequestContext) {
|
||||
400,
|
||||
);
|
||||
|
||||
await context.env.D1.prepare(
|
||||
"INSERT INTO short_links (created_at, destination, path, user) VALUES (?, ?, ?, ?);",
|
||||
)
|
||||
.bind(Date.now(), destination, path, context.data.current_user.id)
|
||||
.run();
|
||||
await context.data.prisma.shortLink.create({
|
||||
data: {
|
||||
destination,
|
||||
path,
|
||||
user: context.data.current_user.id,
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
|
||||
@@ -9,11 +9,10 @@ export default async function (
|
||||
if (!roles) permissions |= 1 << 1;
|
||||
if (roles?.includes("593209890949038082")) permissions |= 1 << 2; // Discord Moderator
|
||||
if (
|
||||
Boolean(
|
||||
await context.env.D1.prepare("SELECT * FROM et_members WHERE id = ?;")
|
||||
.bind(userid)
|
||||
.first(),
|
||||
)
|
||||
await context.data.prisma.etMember.findUnique({
|
||||
select: { id: true },
|
||||
where: { id: userid },
|
||||
})
|
||||
)
|
||||
permissions |= 1 << 3; // Events Team
|
||||
if (roles?.includes("607594065952899072")) permissions |= 1 << 4; // Events Manager
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
const DATASTORE_URL =
|
||||
"https://apis.roblox.com/datastores/v1/universes/274816972/standard-datastores/datastore/entries/entry?datastoreName=BanData&entryKey=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 =
|
||||
"https://apis.roblox.com/datastores/v1/universes/274816972/standard-datastores/datastore/entries/entry?datastoreName=RealData&entryKey=";
|
||||
"https://apis.roblox.com/cloud/v2/universes/274816972/data-stores/RealData/entries";
|
||||
|
||||
export async function getBanList(context: RequestContext) {
|
||||
const data = await fetch(DATASTORE_URL, {
|
||||
@@ -16,14 +19,48 @@ export async function getBanList(context: RequestContext) {
|
||||
throw new Error("Failed to retrieve ban list");
|
||||
}
|
||||
|
||||
return await data.json();
|
||||
return (await data.json()) as {
|
||||
etag: string;
|
||||
path: string;
|
||||
value: {
|
||||
[k: string]: {
|
||||
BanType: number;
|
||||
hidden_from_leaderboards?: boolean;
|
||||
serverconfigurator_blacklist?: boolean;
|
||||
Unbanned?: boolean;
|
||||
UnbanReduct?: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export async function publishMessage(
|
||||
context: RequestContext,
|
||||
topic: string,
|
||||
message: string,
|
||||
) {
|
||||
const response = await fetch(MESSAGING_SERVICE_URL, {
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
topic,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": context.env.ROBLOX_OPENCLOUD_KEY,
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to publish message\n" + (await response.text()));
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSaveData(
|
||||
context: RequestContext,
|
||||
user: number,
|
||||
): Promise<string> {
|
||||
const data = await fetch(`${SAVE_DATA_URL}${user}`, {
|
||||
const data = await fetch(`${SAVE_DATA_URL}/${user}`, {
|
||||
headers: {
|
||||
"x-api-key": context.env.ROBLOX_OPENCLOUD_KEY,
|
||||
},
|
||||
@@ -34,20 +71,24 @@ export async function getSaveData(
|
||||
throw new Error(`Failed to retrieve save data for ${user}`);
|
||||
}
|
||||
|
||||
return await data.json();
|
||||
return ((await data.json()) as { value: string }).value;
|
||||
}
|
||||
|
||||
export async function setBanList(
|
||||
context: RequestContext,
|
||||
data: { [k: string]: { [k: string]: any } },
|
||||
value: any,
|
||||
etag?: string,
|
||||
) {
|
||||
const setRequest = await fetch(DATASTORE_URL, {
|
||||
body: JSON.stringify(data),
|
||||
body: JSON.stringify({
|
||||
etag,
|
||||
value,
|
||||
}),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-api-key": context.env.ROBLOX_OPENCLOUD_KEY,
|
||||
},
|
||||
method: "POST",
|
||||
method: "PATCH",
|
||||
});
|
||||
|
||||
if (!setRequest.ok) {
|
||||
|
||||
@@ -70,10 +70,6 @@ button:focus-visible {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.desktop-nav {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
8
index.d.ts
vendored
8
index.d.ts
vendored
@@ -1,3 +1,5 @@
|
||||
import { type PrismaClient } from "./generated/prisma/client.js";
|
||||
|
||||
declare global {
|
||||
module "*.css";
|
||||
|
||||
@@ -17,7 +19,11 @@ declare global {
|
||||
token: string;
|
||||
};
|
||||
|
||||
type RequestContext = EventContext<Env, string, { [k: string]: any }>;
|
||||
type RequestContext = EventContext<
|
||||
Env,
|
||||
string,
|
||||
{ prisma: PrismaClient; [k: string]: any }
|
||||
>;
|
||||
|
||||
interface AppealCardProps {
|
||||
approved: number | null;
|
||||
|
||||
186
migrations/0001_initialize.sql
Normal file
186
migrations/0001_initialize.sql
Normal 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");
|
||||
15
migrations/0002_create_data_request_table.sql
Normal file
15
migrations/0002_create_data_request_table.sql
Normal 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");
|
||||
1753
package-lock.json
generated
1753
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -7,37 +7,40 @@
|
||||
"build": "remix build --sourcemap",
|
||||
"check-format": "prettier -c .",
|
||||
"format": "prettier -wc .",
|
||||
"publish": "remix build --sourcemap && wrangler pages deploy public"
|
||||
"publish": "remix build --sourcemap && wrangler pages deploy --upload-source-maps public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^2.10.9",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@fontsource-variable/plus-jakarta-sans": "^5.2.8",
|
||||
"@prisma/adapter-d1": "^7.8.0",
|
||||
"@prisma/client": "^7.8.0",
|
||||
"@remix-run/cloudflare": "^2.17.4",
|
||||
"@remix-run/cloudflare-pages": "^2.17.4",
|
||||
"@remix-run/react": "^2.17.4",
|
||||
"@sentry/cloudflare": "^10.43.0",
|
||||
"@sentry/remix": "^10.43.0",
|
||||
"@sentry/cloudflare": "^10.52.0",
|
||||
"@sentry/remix": "^10.52.0",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"dayjs": "^1.11.19",
|
||||
"framer-motion": "^12.35.2",
|
||||
"dayjs": "^1.11.20",
|
||||
"framer-motion": "^12.38.0",
|
||||
"react": "^18.3.1",
|
||||
"react-big-calendar": "^1.19.4",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^2.17.4",
|
||||
"@types/node": "^24.12.0",
|
||||
"@types/node": "^24.12.3",
|
||||
"@types/react": "^18.3.28",
|
||||
"@types/react-big-calendar": "^1.16.3",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"dotenv": "^17.3.1",
|
||||
"prettier": "^3.8.1",
|
||||
"dotenv": "^17.4.1",
|
||||
"prettier": "^3.8.3",
|
||||
"prisma": "^7.8.0",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"overrides": {
|
||||
"@cloudflare/workers-types": "^4.20260310.1"
|
||||
"@cloudflare/workers-types": "^4.20260511.1"
|
||||
},
|
||||
"prettier": {
|
||||
"endOfLine": "auto"
|
||||
|
||||
12
prisma.config.ts
Normal file
12
prisma.config.ts
Normal 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
168
prisma/schema.prisma
Normal 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")
|
||||
}
|
||||
Reference in New Issue
Block a user