From dd2d9f26729f6a979395b8996bbae96f365fda8f Mon Sep 17 00:00:00 2001
From: regalijan <r@regalijan.com>
Date: Thu, 19 Oct 2023 16:50:48 -0400
Subject: [PATCH] Greatly reduce repeated code

---
 functions/api/appeals/[id]/_middleware.ts     | 24 ++------
 functions/api/appeals/[id]/accept.ts          |  9 +--
 functions/api/appeals/[id]/ban.ts             |  9 +--
 functions/api/appeals/[id]/deny.ts            |  9 +--
 functions/api/appeals/_middleware.ts          | 10 +---
 functions/api/appeals/submit.ts               | 27 ++-------
 functions/api/appeals/toggle.ts               | 16 ++---
 functions/api/auth/session.ts                 | 24 +++-----
 functions/api/data-transfers/create.ts        | 25 ++------
 .../api/events-team/events/_middleware.ts     |  9 +--
 .../events-team/team-members/_middleware.ts   |  9 +--
 .../api/events-team/team-members/user.ts      | 34 ++++-------
 functions/api/game-appeals/[id]/accept.ts     | 16 +----
 functions/api/game-appeals/[id]/deny.ts       | 10 +---
 functions/api/game-appeals/_middleware.ts     |  9 +--
 functions/api/game-bans/[user]/history.ts     | 23 ++------
 functions/api/game-bans/[user]/revoke.ts      | 18 ++----
 functions/api/game-bans/_middleware.ts        | 18 ++----
 functions/api/gme/_middleware.ts              |  9 +--
 functions/api/gme/add.ts                      | 17 ++----
 functions/api/gme/list.ts                     |  8 +--
 functions/api/gme/remove.ts                   | 10 +---
 functions/api/inactivity/[id].ts              | 59 +++++++++++++------
 functions/api/inactivity/_middleware.ts       | 11 +---
 functions/api/inactivity/validate.ts          | 18 ++----
 functions/api/infractions/new.ts              | 55 +++--------------
 functions/api/mod-queue/[type]/[id].ts        | 25 +++-----
 functions/api/mod-queue/list.ts               | 29 ++-------
 functions/api/reports/[id]/action.ts          |  9 +--
 functions/api/reports/complete.ts             | 25 ++------
 functions/api/reports/recall.ts               | 17 ++----
 functions/api/reports/submit.ts               | 50 ++++++----------
 functions/api/uploads/_middleware.ts          |  9 +--
 functions/api/uploads/status.ts               | 27 ++-------
 34 files changed, 196 insertions(+), 481 deletions(-)

diff --git a/functions/api/appeals/[id]/_middleware.ts b/functions/api/appeals/[id]/_middleware.ts
index c0fc509..b2eca47 100644
--- a/functions/api/appeals/[id]/_middleware.ts
+++ b/functions/api/appeals/[id]/_middleware.ts
@@ -1,3 +1,5 @@
+import { jsonError } from "../../../common.js";
+
 export async function onRequestPost(context: RequestContext) {
   const { pathname } = new URL(context.request.url);
 
@@ -8,12 +10,7 @@ export async function onRequestPost(context: RequestContext) {
   const { permissions } = context.data.current_user;
 
   if (!(permissions & (1 << 0)) && !(permissions & (1 << 11)))
-    return new Response('{"error":"Forbidden"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 403,
-    });
+    return jsonError("Forbidden", 403);
 
   const { body } = context.data;
   const id = context.params.id as string;
@@ -23,13 +20,7 @@ export async function onRequestPost(context: RequestContext) {
   if (!pathname.endsWith("/ban")) {
     const key = await context.env.DATA.get(`appeal_${id}`);
 
-    if (!key)
-      return new Response('{"error":"No appeal with that ID exists"}', {
-        headers: {
-          "content-type": "application/json",
-        },
-        status: 404,
-      });
+    if (!key) return jsonError("No appeal with that ID exists", 404);
 
     context.data.appeal = JSON.parse(key);
   }
@@ -38,12 +29,7 @@ export async function onRequestPost(context: RequestContext) {
     body.feedback &&
     (typeof body.feedback !== "string" || body.feedback.length > 512)
   )
-    return new Response('{"error":"Invalid feedback"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+    return jsonError("Invalid feedback", 400);
 
   return await context.next();
 }
diff --git a/functions/api/appeals/[id]/accept.ts b/functions/api/appeals/[id]/accept.ts
index 8a4a300..b97f90d 100644
--- a/functions/api/appeals/[id]/accept.ts
+++ b/functions/api/appeals/[id]/accept.ts
@@ -1,3 +1,5 @@
+import { jsonError } from "../../../common.js";
+
 export async function onRequestPost(context: RequestContext) {
   const { appeal } = context.data;
   const body = new FormData();
@@ -20,12 +22,7 @@ export async function onRequestPost(context: RequestContext) {
 
   if (!emailReq.ok) {
     console.log(await emailReq.json());
-    return new Response('{"error":"Failed to accept appeal"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 500,
-    });
+    return jsonError("Failed to accept appeal", 500);
   }
 
   const { current_user: currentUser } = context.data;
diff --git a/functions/api/appeals/[id]/ban.ts b/functions/api/appeals/[id]/ban.ts
index 2ff9310..69190ac 100644
--- a/functions/api/appeals/[id]/ban.ts
+++ b/functions/api/appeals/[id]/ban.ts
@@ -1,13 +1,10 @@
+import { jsonError } from "../../../common.js";
+
 export async function onRequestPost(context: RequestContext) {
   const { current_user: currentUser } = context.data;
 
   if (context.data.targetId.search(/^\d{16,19}$/) === -1)
-    return new Response('{"error":"Invalid target id"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+    return jsonError("Invalid target id", 400);
 
   await context.env.D1.prepare(
     "INSERT INTO appeal_bans (created_at, created_by, user) VALUES (?, ?, ?);",
diff --git a/functions/api/appeals/[id]/deny.ts b/functions/api/appeals/[id]/deny.ts
index cd64d72..e104158 100644
--- a/functions/api/appeals/[id]/deny.ts
+++ b/functions/api/appeals/[id]/deny.ts
@@ -1,3 +1,5 @@
+import { jsonError } from "../../../common.js";
+
 export async function onRequestPost(context: RequestContext) {
   const { appeal } = context.data;
   const body = new FormData();
@@ -20,12 +22,7 @@ export async function onRequestPost(context: RequestContext) {
 
   if (!emailReq.ok) {
     console.log(await emailReq.json());
-    return new Response('{"error":"Failed to deny appeal"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 500,
-    });
+    return jsonError("Failed to deny appeal", 500);
   }
 
   await context.env.D1.prepare("UPDATE appeals SET open = 0 WHERE id = ?;")
diff --git a/functions/api/appeals/_middleware.ts b/functions/api/appeals/_middleware.ts
index 9d9ed16..e05b5bf 100644
--- a/functions/api/appeals/_middleware.ts
+++ b/functions/api/appeals/_middleware.ts
@@ -1,11 +1,7 @@
+import { jsonError } from "../../common.js";
+
 export async function onRequest(context: RequestContext) {
-  if (!context.data.current_user)
-    return new Response('{"error":"Not logged in"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 401,
-    });
+  if (!context.data.current_user) return jsonError("Not logged in", 401);
 
   return await context.next();
 }
diff --git a/functions/api/appeals/submit.ts b/functions/api/appeals/submit.ts
index e823654..a7db0eb 100644
--- a/functions/api/appeals/submit.ts
+++ b/functions/api/appeals/submit.ts
@@ -1,3 +1,5 @@
+import { jsonError } from "../../common.js";
+
 export async function onRequestPost(context: RequestContext) {
   const { learned, whyBanned, whyUnban } = context.data.body;
 
@@ -12,25 +14,11 @@ export async function onRequestPost(context: RequestContext) {
     !whyUnban.length ||
     whyUnban.length > 2000
   )
-    return new Response(
-      '{"error":"One or more fields are missing or invalid"}',
-      {
-        headers: {
-          "content-type": "application/json",
-        },
-        status: 400,
-      },
-    );
+    return jsonError("One or more fields are missing or invalid", 400);
 
   const { current_user: currentUser } = context.data;
 
-  if (!currentUser.email)
-    return new Response('{"error":"No email for this session"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 403,
-    });
+  if (!currentUser.email) return jsonError("No email for this session", 403);
 
   const existingAppeals = await context.env.DATA.list({
     prefix: `appeal_${currentUser.id}`,
@@ -45,12 +33,7 @@ export async function onRequestPost(context: RequestContext) {
       (appeal) => (appeal.metadata as { [k: string]: any })?.open,
     )
   )
-    return new Response('{"error":"Appeal already submitted"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 403,
-    });
+    return jsonError("Appeal already submitted", 403);
 
   if (
     await context.env.D1.prepare("SELECT * FROM appeal_bans WHERE user = ?;")
diff --git a/functions/api/appeals/toggle.ts b/functions/api/appeals/toggle.ts
index cf45de2..022ce36 100644
--- a/functions/api/appeals/toggle.ts
+++ b/functions/api/appeals/toggle.ts
@@ -1,22 +1,14 @@
+import { jsonError } from "../../common.js";
+
 export async function onRequestPost(context: RequestContext) {
   const { active } = context.data.body;
   const { permissions } = context.data.current_user;
 
   if (!(permissions & (1 << 0)) && !(permissions & (1 << 11)))
-    return new Response('{"error":"Forbidden"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 403,
-    });
+    return jsonError("Forbidden", 403);
 
   if (typeof active !== "boolean")
-    return new Response('{"error":"Active property must be a boolean"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+    return jsonError("Active property must be a boolean", 400);
 
   if (active) {
     await context.env.DATA.delete("appeal_disabled");
diff --git a/functions/api/auth/session.ts b/functions/api/auth/session.ts
index 08cc941..071af9c 100644
--- a/functions/api/auth/session.ts
+++ b/functions/api/auth/session.ts
@@ -1,4 +1,5 @@
 import GetPermissions from "../../permissions.js";
+import { jsonError } from "../../common.js";
 import tokenPrefixes from "../../../data/token_prefixes.json";
 
 async function generateTokenHash(token: string): Promise<string> {
@@ -12,19 +13,10 @@ async function generateTokenHash(token: string): Promise<string> {
     .replace(/=/g, "");
 }
 
-function response(body: string, status: number) {
-  return new Response(body, {
-    headers: {
-      "content-type": "application/json",
-    },
-    status,
-  });
-}
-
 export async function onRequestDelete(context: RequestContext) {
   const cookies = context.request.headers.get("cookie")?.split("; ");
 
-  if (!cookies) return response('{"error":"Not logged in"}', 401);
+  if (!cookies) return jsonError("Not logged in", 401);
 
   for (const cookie of cookies) {
     const [name, value] = cookie.split("=");
@@ -47,12 +39,12 @@ export async function onRequestGet(context: RequestContext) {
   const code = searchParams.get("code");
   const state = searchParams.get("state");
 
-  if (!code) return response('{"error":"Missing code"}', 400);
-  if (!state) return response('{"error":"Missing state"}', 400);
+  if (!code) return jsonError("Missing code", 400);
+  if (!state) return jsonError("Missing state", 400);
 
   const stateRedirect = await context.env.DATA.get(`state_${state}`);
 
-  if (!stateRedirect) return response('{"error":"Invalid state"}', 400);
+  if (!stateRedirect) return jsonError("Invalid state", 400);
 
   const tokenReq = await fetch("https://discord.com/api/oauth2/token", {
     body: new URLSearchParams({
@@ -72,7 +64,7 @@ export async function onRequestGet(context: RequestContext) {
   if (!tokenReq.ok) {
     console.log(await tokenReq.text());
 
-    return response('{"error":"Failed to redeem code"}', 500);
+    return jsonError("Failed to redeem code", 500);
   }
 
   const tokenData: {
@@ -84,7 +76,7 @@ export async function onRequestGet(context: RequestContext) {
   } = await tokenReq.json();
 
   if (tokenData.scope.search("guilds.members.read") === -1)
-    return response('{"error":"Do not touch the scopes!"}', 400);
+    return jsonError("Do not touch the scopes!", 400);
 
   let userData: { [k: string]: any } = {
     ...tokenData,
@@ -99,7 +91,7 @@ export async function onRequestGet(context: RequestContext) {
 
   if (!userReq.ok) {
     console.log(await userReq.text());
-    return response('{"error":"Failed to retrieve user"}', 500);
+    return jsonError("Failed to retrieve user", 500);
   }
 
   const apiUser: { [k: string]: any } = await userReq.json();
diff --git a/functions/api/data-transfers/create.ts b/functions/api/data-transfers/create.ts
index 1b9adc3..daf081e 100644
--- a/functions/api/data-transfers/create.ts
+++ b/functions/api/data-transfers/create.ts
@@ -1,3 +1,5 @@
+import { jsonError } from "../../common.js";
+
 export async function onRequestPost(context: RequestContext) {
   const { cookie, has_access } = context.data.body;
 
@@ -9,12 +11,7 @@ export async function onRequestPost(context: RequestContext) {
         /_\|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 new Response('{"error":"Invalid request"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+    return jsonError("Invalid request", 400);
 
   const id =
     (context.request.headers.get("cf-ray")?.split("-")[0] as string) +
@@ -53,13 +50,7 @@ export async function onRequestPost(context: RequestContext) {
     },
   );
 
-  if (!authedUserReq.ok)
-    return new Response('{"error":"Cookie is invalid"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+  if (!authedUserReq.ok) return jsonError("Cookie is invalid", 400);
 
   const authedUser: { id: number; name: string } = await authedUserReq.json();
 
@@ -79,13 +70,7 @@ export async function onRequestPost(context: RequestContext) {
     },
   );
 
-  if (!createCardReq.ok)
-    return new Response('{"error":"Failed to create entry"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 500,
-    });
+  if (!createCardReq.ok) return jsonError("Failed to create entry", 500);
 
   await context.env.DATA.put(
     `datatransfer_${id}`,
diff --git a/functions/api/events-team/events/_middleware.ts b/functions/api/events-team/events/_middleware.ts
index 79e2ab5..fbe4a89 100644
--- a/functions/api/events-team/events/_middleware.ts
+++ b/functions/api/events-team/events/_middleware.ts
@@ -1,15 +1,12 @@
+import { jsonError } from "../../../common.js";
+
 export async function onRequest(context: RequestContext) {
   if (
     ![1 << 3, 1 << 4, 1 << 12].find(
       (int) => context.data.current_user?.permissions & int,
     )
   )
-    return new Response('{"error":"Forbidden"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 401,
-    });
+    return jsonError("Forbidden", 403);
 
   return await context.next();
 }
diff --git a/functions/api/events-team/team-members/_middleware.ts b/functions/api/events-team/team-members/_middleware.ts
index f16342b..6c6d43c 100644
--- a/functions/api/events-team/team-members/_middleware.ts
+++ b/functions/api/events-team/team-members/_middleware.ts
@@ -1,13 +1,10 @@
+import { jsonError } from "../../../common.js";
+
 export async function onRequest(context: RequestContext) {
   if (
     ![1 << 4, 1 << 12].find((p) => context.data.current_user?.permissions & p)
   )
-    return new Response('{"error":"Forbidden"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 403,
-    });
+    return jsonError("Forbidden", 403);
 
   return await context.next();
 }
diff --git a/functions/api/events-team/team-members/user.ts b/functions/api/events-team/team-members/user.ts
index 865a562..c689dcd 100644
--- a/functions/api/events-team/team-members/user.ts
+++ b/functions/api/events-team/team-members/user.ts
@@ -1,3 +1,5 @@
+import { jsonError } from "../../../common.js";
+
 export async function onRequestDelete(context: RequestContext) {
   const { id } = context.data.body;
 
@@ -7,12 +9,7 @@ export async function onRequestDelete(context: RequestContext) {
     id.length > 19 ||
     id.length < 17
   )
-    return new Response('{"error":"Invalid ID"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+    return jsonError("Invalid ID", 400);
 
   await context.env.DATA.delete(`etmember_${id}`);
   await context.env.D1.prepare("DELETE FROM et_members WHERE id = ?;")
@@ -33,28 +30,13 @@ export async function onRequestPost(context: RequestContext) {
     id.length > 19 ||
     id.length < 17
   )
-    return new Response('{"error":"Invalid user ID"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+    return jsonError("Invalid user ID", 400);
 
   if (typeof name !== "string" || !name.length || name.length > 32)
-    return new Response('{"error":"Invalid name"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+    return jsonError("Invalid name", 400);
 
   if (await context.env.DATA.get(`etmember_${id}`))
-    return new Response('{"error":"User is already a member"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+    return jsonError("User is already a member", 400);
 
   const createdAt = Date.now();
   const addingUser = context.data.current_user.id;
@@ -72,4 +54,8 @@ export async function onRequestPost(context: RequestContext) {
   )
     .bind(createdAt, addingUser, id, name)
     .run();
+
+  return new Response(null, {
+    status: 204,
+  });
 }
diff --git a/functions/api/game-appeals/[id]/accept.ts b/functions/api/game-appeals/[id]/accept.ts
index 8d2cd6a..a7819aa 100644
--- a/functions/api/game-appeals/[id]/accept.ts
+++ b/functions/api/game-appeals/[id]/accept.ts
@@ -1,28 +1,18 @@
 import { insertLogs } from "../../../gcloud.js";
 import { getBanList, setBanList } from "../../../roblox-open-cloud.js";
+import { jsonError } from "../../../common.js";
 
 export async function onRequestPost(context: RequestContext) {
   const { statsReduction } = context.data.body;
 
   if (statsReduction && typeof statsReduction !== "number")
-    return new Response('{"error":"Invalid stat reduction"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+    return jsonError("Invalid stat reduction", 400);
 
   const appeal = await context.env.DATA.get(
     `gameappeal_${context.params.id as string}`,
   );
 
-  if (!appeal)
-    return new Response('{"error":"Appeal not found"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 404,
-    });
+  if (!appeal) return jsonError("Appeal not found", 400);
 
   const data = JSON.parse(appeal);
   const banList = (await getBanList(context)) as {
diff --git a/functions/api/game-appeals/[id]/deny.ts b/functions/api/game-appeals/[id]/deny.ts
index 3c9eda1..4dbf10a 100644
--- a/functions/api/game-appeals/[id]/deny.ts
+++ b/functions/api/game-appeals/[id]/deny.ts
@@ -1,15 +1,11 @@
+import { jsonError } from "../../../common.js";
+
 export async function onRequestPost(context: RequestContext) {
   const appealId = context.params.id as string;
 
   const appeal = await context.env.DATA.get(`gameappeal_${appealId}`);
 
-  if (!appeal)
-    return new Response('{"error":"Appeal not found"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 404,
-    });
+  if (!appeal) return jsonError("Appeal not found", 404);
 
   const appealData = JSON.parse(appeal);
 
diff --git a/functions/api/game-appeals/_middleware.ts b/functions/api/game-appeals/_middleware.ts
index 578bc69..774bab8 100644
--- a/functions/api/game-appeals/_middleware.ts
+++ b/functions/api/game-appeals/_middleware.ts
@@ -1,11 +1,8 @@
+import { jsonError } from "../../common.js";
+
 export async function onRequest(context: RequestContext) {
   if (!(context.data.current_user.permissions & (1 << 5)))
-    return new Response('{"error":"Forbidden"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 403,
-    });
+    return jsonError("Forbidden", 403);
 
   return await context.next();
 }
diff --git a/functions/api/game-bans/[user]/history.ts b/functions/api/game-bans/[user]/history.ts
index c4be834..a425ed7 100644
--- a/functions/api/game-bans/[user]/history.ts
+++ b/functions/api/game-bans/[user]/history.ts
@@ -1,3 +1,4 @@
+import { jsonError, jsonResponse } from "../../../common.js";
 import { queryLogs } from "../../../gcloud.js";
 
 export async function onRequestGet(context: RequestContext) {
@@ -17,26 +18,15 @@ export async function onRequestGet(context: RequestContext) {
 
   if (!robloxUserReq.ok) {
     console.log(await robloxUserReq.json());
-    return new Response('{"error":"Failed to resolve username"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 500,
-    });
+    return jsonError("Failed to resolve username", 500);
   }
 
   const { data: users }: { data: { [k: string]: any }[] } =
     await robloxUserReq.json();
 
-  if (!users.length)
-    return new Response('{"error":"No user found with that name"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+  if (!users.length) return jsonError("No user found with that name", 400);
 
-  return new Response(
+  return jsonResponse(
     JSON.stringify(
       (await queryLogs(users[0].id, context)).sort((a, b) =>
         a.entity.properties.executed_at.integerValue >
@@ -45,10 +35,5 @@ export async function onRequestGet(context: RequestContext) {
           : -1,
       ),
     ),
-    {
-      headers: {
-        "content-type": "application/json",
-      },
-    },
   );
 }
diff --git a/functions/api/game-bans/[user]/revoke.ts b/functions/api/game-bans/[user]/revoke.ts
index ecf4fb6..1b8453f 100644
--- a/functions/api/game-bans/[user]/revoke.ts
+++ b/functions/api/game-bans/[user]/revoke.ts
@@ -1,5 +1,6 @@
-import { insertLogs } from "../../../gcloud.js";
 import { getBanList, setBanList } from "../../../roblox-open-cloud.js";
+import { insertLogs } from "../../../gcloud.js";
+import { jsonError } from "../../../common.js";
 
 export async function onRequestPost(context: RequestContext) {
   const { ticket_link } = context.data.body;
@@ -9,22 +10,11 @@ export async function onRequestPost(context: RequestContext) {
       /^https?:\/\/carcrushers\.modmail\.dev\/logs\/[a-z\d]{12}$/,
     )
   )
-    return new Response('{"error":"Invalid ticket link provided"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+    return jsonError("Invalid ticket link provided", 400);
 
   const user = context.params.user as string;
 
-  if (isNaN(parseInt(user)))
-    return new Response('{"error":"Invalid user ID"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+  if (isNaN(parseInt(user))) return jsonError("Invalid user ID", 400);
 
   await insertLogs({ [user]: 3 }, ticket_link, context);
 
diff --git a/functions/api/game-bans/_middleware.ts b/functions/api/game-bans/_middleware.ts
index c86d8ca..569715e 100644
--- a/functions/api/game-bans/_middleware.ts
+++ b/functions/api/game-bans/_middleware.ts
@@ -1,21 +1,11 @@
+import { jsonError } from "../../common.js";
+
 export async function onRequest(context: RequestContext) {
   const { current_user: currentUser } = context.data;
 
-  if (!currentUser)
-    return new Response('{"error":Not logged in"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 401,
-    });
+  if (!currentUser) return jsonError("Not logged in", 401);
 
-  if (!(currentUser.permissions & (1 << 5)))
-    return new Response('{"error":"Forbidden"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 403,
-    });
+  if (!(currentUser.permissions & (1 << 5))) return jsonError("Forbidden", 403);
 
   return await context.next();
 }
diff --git a/functions/api/gme/_middleware.ts b/functions/api/gme/_middleware.ts
index cae98e8..c606a47 100644
--- a/functions/api/gme/_middleware.ts
+++ b/functions/api/gme/_middleware.ts
@@ -1,3 +1,5 @@
+import { jsonError } from "../../common.js";
+
 export async function onRequest(context: RequestContext) {
   const { current_user: currentUser } = context.data;
 
@@ -9,12 +11,7 @@ export async function onRequest(context: RequestContext) {
       "396347223736057866",
     ].includes(currentUser.id)
   )
-    return new Response('{"error":"Forbidden"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 403,
-    });
+    return jsonError("Forbidden", 403);
 
   return await context.next();
 }
diff --git a/functions/api/gme/add.ts b/functions/api/gme/add.ts
index 669b0c1..08cce03 100644
--- a/functions/api/gme/add.ts
+++ b/functions/api/gme/add.ts
@@ -1,21 +1,13 @@
-function makeResponse(body: string, status: number): Response {
-  return new Response(body, {
-    headers: {
-      "content-type": "application/json",
-    },
-    status,
-  });
-}
+import { jsonError } from "../../common.js";
 
 export async function onRequestPost(context: RequestContext) {
   const { user } = context.data.body;
 
-  if (!user) return makeResponse('{"error":"No user provided"}', 400);
+  if (!user) return jsonError("No user provided", 400);
 
   const existingUser = await context.env.DATA.get(`gamemod_${user}`);
 
-  if (existingUser)
-    return makeResponse('{"error":"Cannot add an existing user"}', 400);
+  if (existingUser) return jsonError("Cannot add an existing user", 400);
 
   if (
     ["165594923586945025", "289372404541554689", "396347223736057866"].includes(
@@ -26,8 +18,7 @@ export async function onRequestPost(context: RequestContext) {
       status: 204,
     });
 
-  if (!user.match(/^\d{17,19}$/))
-    return makeResponse('{"error":"Invalid User ID"}', 400);
+  if (!user.match(/^\d{17,19}$/)) return jsonError("Invalid User ID", 400);
 
   const data = { time: Date.now(), user: context.data.current_user.id };
 
diff --git a/functions/api/gme/list.ts b/functions/api/gme/list.ts
index 84851f9..0d3a2ad 100644
--- a/functions/api/gme/list.ts
+++ b/functions/api/gme/list.ts
@@ -1,3 +1,5 @@
+import { jsonResponse } from "../../common.js";
+
 export async function onRequestGet(context: RequestContext) {
   const list = await context.env.DATA.list({ prefix: "gamemod_" });
   const entries = [];
@@ -8,9 +10,5 @@ export async function onRequestGet(context: RequestContext) {
       user: key.name.replace("gamemod_", ""),
     });
 
-  return new Response(JSON.stringify(entries), {
-    headers: {
-      "content-type": "application/json",
-    },
-  });
+  return jsonResponse(JSON.stringify(entries));
 }
diff --git a/functions/api/gme/remove.ts b/functions/api/gme/remove.ts
index 2b0b5dd..022939d 100644
--- a/functions/api/gme/remove.ts
+++ b/functions/api/gme/remove.ts
@@ -1,13 +1,9 @@
+import { jsonError } from "../../common.js";
+
 export async function onRequestPost(context: RequestContext) {
   const { user } = context.data.body;
 
-  if (!user)
-    return new Response('{"error":"No user provided"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+  if (!user) return jsonError("No user provided", 400);
 
   await context.env.DATA.delete(`gamemod_${user}`);
 
diff --git a/functions/api/inactivity/[id].ts b/functions/api/inactivity/[id].ts
index 9d88ebd..7dfdcd4 100644
--- a/functions/api/inactivity/[id].ts
+++ b/functions/api/inactivity/[id].ts
@@ -1,28 +1,19 @@
+import { jsonError } from "../../common.js";
 import validateInactivityNotice from "./validate.js";
 
-function jsonResponse(body: string, status = 200): Response {
-  return new Response(body, {
-    headers: {
-      "content-type": "application/json",
-    },
-    status,
-  });
-}
-
 export async function onRequestDelete(context: RequestContext) {
   const kvResult = await context.env.DATA.get(
     `inactivity_${context.params.id}`,
   );
 
-  if (!kvResult)
-    return jsonResponse('{"error":"No inactivity notice with that ID"}', 404);
+  if (!kvResult) return jsonError("No inactivity notice with that ID", 404);
 
   if (
     JSON.parse(kvResult).user.id !== context.data.current_user.id &&
     !(context.data.current_user.permissions & (1 << 0))
   )
-    return jsonResponse(
-      '{"error":"You do not have permission to delete this inactivity notice"}',
+    return jsonError(
+      "You do not have permission to delete this inactivity notice",
       403,
     );
 
@@ -36,18 +27,46 @@ export async function onRequestDelete(context: RequestContext) {
   });
 }
 
+export async function onRequestPost(context: RequestContext) {
+  const { accepted }: { accepted?: boolean } = context.data.body;
+
+  if (typeof accepted !== "boolean")
+    return jsonError("'accepted' must be a boolean", 400);
+
+  const adminDepartments: { [k: string]: number } = {
+    DM: 1 << 11,
+    ET: 1 << 4,
+    FM: 1 << 7,
+    WM: 1 << 6,
+  };
+
+  const userAdminDepartments = Object.values(adminDepartments).filter(
+    (dept) => context.data.current_user.permissions & dept,
+  );
+
+  if (!userAdminDepartments.length)
+    return jsonError("You are not a manager of any departments", 403);
+
+  const requestedNotice = await context.env.DATA.get(
+    `inactivity_${context.params.id as string}`,
+    { type: "json" },
+  );
+
+  if (!requestedNotice)
+    return jsonError("Inactivity notices does not exist", 404);
+}
+
 export async function onRequestPut(context: RequestContext) {
   const kvResult: InactivityNoticeProps | null = await context.env.DATA.get(
     `inactivity_${context.params.id}`,
     { type: "json" },
   );
 
-  if (!kvResult)
-    return jsonResponse('{"error":"No inactivity notice with that ID"}', 404);
+  if (!kvResult) return jsonError("No inactivity notice with that ID", 404);
 
   if (kvResult.user.id !== context.data.current_user.id)
-    return jsonResponse(
-      '{"error":"You do not have permission to modify this inactivity notice"}',
+    return jsonError(
+      "You do not have permission to modify this inactivity notice",
       403,
     );
 
@@ -58,7 +77,7 @@ export async function onRequestPut(context: RequestContext) {
     .run();
 
   if (!Boolean(d1entry.results.at(0)?.open))
-    return jsonResponse("Cannot modify a closed inactivity notice", 403);
+    return jsonError("Cannot modify a closed inactivity notice", 403);
 
   const { departments, end, reason, start } = context.data.body;
 
@@ -84,4 +103,8 @@ export async function onRequestPut(context: RequestContext) {
       expirationTtl: 63072000,
     },
   );
+
+  return new Response(null, {
+    status: 204,
+  });
 }
diff --git a/functions/api/inactivity/_middleware.ts b/functions/api/inactivity/_middleware.ts
index 3a9e704..cb94ca4 100644
--- a/functions/api/inactivity/_middleware.ts
+++ b/functions/api/inactivity/_middleware.ts
@@ -1,15 +1,8 @@
-function makeResponse(body: string, status: number): Response {
-  return new Response(body, {
-    headers: {
-      "content-type": "application/json",
-    },
-    status,
-  });
-}
+import { jsonError } from "../../common.js";
 
 export async function onRequest(context: RequestContext) {
   if (!context.data.current_user)
-    return makeResponse('{"error":"You are not logged in"}', 401);
+    return jsonError("You are not logged in", 401);
 
   const { permissions } = context.data.current_user;
   const departments = {
diff --git a/functions/api/inactivity/validate.ts b/functions/api/inactivity/validate.ts
index 2ab9909..4b31fb9 100644
--- a/functions/api/inactivity/validate.ts
+++ b/functions/api/inactivity/validate.ts
@@ -1,11 +1,4 @@
-function errorResponse(error: string, status = 400): Response {
-  return new Response(JSON.stringify({ error }), {
-    headers: {
-      "content-type": "application/json",
-    },
-    status,
-  });
-}
+import { jsonError } from "../../common.js";
 
 export default function (
   selectedDepartments: string[],
@@ -14,8 +7,7 @@ export default function (
   start: any,
   userDepartments?: string[],
 ): void | Response {
-  if (!userDepartments)
-    return errorResponse("Not part of any departments", 403);
+  if (!userDepartments) return jsonError("Not part of any departments", 403);
 
   if (
     !Array.isArray(selectedDepartments) ||
@@ -24,10 +16,10 @@ export default function (
     typeof reason !== "string" ||
     typeof start !== "string"
   )
-    return errorResponse("Invalid notice");
+    return jsonError("Invalid notice", 400);
 
   if (!selectedDepartments.every((dept) => userDepartments.includes(dept)))
-    return errorResponse(
+    return jsonError(
       "Cannot file an inactivity notice in a department you are not part of",
       403,
     );
@@ -45,5 +37,5 @@ export default function (
     startDate.getFullYear() > now.getFullYear() + 1 ||
     endDate.valueOf() < startDate.valueOf()
   )
-    return errorResponse("Dates are invalid");
+    return jsonError("Dates are invalid", 400);
 }
diff --git a/functions/api/infractions/new.ts b/functions/api/infractions/new.ts
index aa30ef8..5a4bd9f 100644
--- a/functions/api/infractions/new.ts
+++ b/functions/api/infractions/new.ts
@@ -1,4 +1,5 @@
 import { GenerateUploadURL } from "../../gcloud.js";
+import { jsonError } from "../../common.js";
 
 const allowedFileTypes = [
   "image/gif",
@@ -17,36 +18,21 @@ export async function onRequestPost(context: RequestContext) {
       (p) => context.data.current_user.permissions & p,
     )
   )
-    return new Response('{"error":"Forbidden"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 403,
-    });
+    return jsonError("Forbidden", 403);
 
   if (
     context.request.headers
       .get("content-type")
       ?.startsWith("multipart/form-data; boundary=")
   )
-    return new Response('{"error":"Invalid content type"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+    return jsonError("Invalid content type", 400);
 
   let body: FormData;
 
   try {
     body = await context.request.formData();
   } catch {
-    return new Response('{"error":"Invalid form data"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+    return jsonError("Invalid form data", 400);
   }
 
   if (
@@ -59,39 +45,21 @@ export async function onRequestPost(context: RequestContext) {
       "ban_unappealable",
     ].includes(body.get("punishment") as string)
   )
-    return new Response('{"error":"Invalid punishment"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+    return jsonError("Invalid punishment", 400);
 
   if (!(body.get("user") as string).match(/^\d{17,19}$/))
-    return new Response('{"error":"Invalid user"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+    return jsonError("Invalid user", 400);
 
   // @ts-expect-error
   const files: File[] = body.keys().find((key) => key.match(/^files\[\d]$/));
   const urlPromises = [];
   const origin = context.request.headers.get("Origin");
 
-  if (!origin)
-    return new Response('{"error":"Origin header missing"}', {
-      status: 400,
-    });
+  if (!origin) return jsonError("Origin header missing", 400);
 
   for (const file of files) {
     if (!allowedFileTypes.includes(file.type))
-      return new Response(`{"error":"File ${file.name} is not valid"}`, {
-        headers: {
-          "content-type": "application/json",
-        },
-        status: 415,
-      });
+      return jsonError(`File ${file.name} is not valid`, 415);
 
     const attachmentKey = `${Date.now()}${Math.round(
       Math.random() * 10000000,
@@ -111,12 +79,7 @@ export async function onRequestPost(context: RequestContext) {
   const settledURLPromises = await Promise.allSettled(urlPromises);
 
   if (settledURLPromises.find((p) => p.status === "rejected"))
-    return new Response('{"error":"Failed to process one or more files"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 500,
-    });
+    return jsonError("Failed to process one or more files", 500);
 
   const infractionId = `${body.get(
     "user",
diff --git a/functions/api/mod-queue/[type]/[id].ts b/functions/api/mod-queue/[type]/[id].ts
index 0766eb0..db70681 100644
--- a/functions/api/mod-queue/[type]/[id].ts
+++ b/functions/api/mod-queue/[type]/[id].ts
@@ -1,3 +1,5 @@
+import { jsonError, jsonResponse } from "../../../common.js";
+
 export async function onRequestGet(context: RequestContext) {
   const types: { [k: string]: { permissions: number[]; prefix: string } } = {
     appeal: {
@@ -26,12 +28,7 @@ export async function onRequestGet(context: RequestContext) {
       (p) => context.data.current_user.permissions & p,
     )
   )
-    return new Response('{"error":"You cannot use this filter"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 403,
-    });
+    return jsonError("You cannot use this filter", 403);
 
   let item: {
     [k: string]: any;
@@ -48,19 +45,11 @@ export async function onRequestGet(context: RequestContext) {
     type === "report" &&
     (await context.env.DATA.get(`reportprocessing_${itemId}`))
   )
-    return new Response('{"error":"Report is processing"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 409,
-    });
+    return jsonError("Report is processing", 409);
 
   if (item) delete item.user?.email;
 
-  return new Response(item ? JSON.stringify(item) : '{"error":"Not found"}', {
-    headers: {
-      "content-type": "application/json",
-    },
-    status: item ? 200 : 404,
-  });
+  return item
+    ? jsonResponse(JSON.stringify(item))
+    : jsonError("Not found", 404);
 }
diff --git a/functions/api/mod-queue/list.ts b/functions/api/mod-queue/list.ts
index a0f664e..1e28a9d 100644
--- a/functions/api/mod-queue/list.ts
+++ b/functions/api/mod-queue/list.ts
@@ -1,3 +1,5 @@
+import { jsonError, jsonResponse } from "../../common.js";
+
 export async function onRequestGet(context: RequestContext) {
   const { searchParams } = new URL(context.request.url);
   const before = parseInt(searchParams.get("before") || `${Date.now()}`);
@@ -21,28 +23,13 @@ export async function onRequestGet(context: RequestContext) {
   const { current_user: currentUser } = context.data;
 
   if (!entryType || !types[entryType])
-    return new Response('{"error":"Invalid filter type"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+    return jsonError("Invalid filter type", 400);
 
   if (!permissions[entryType].find((p) => currentUser.permissions & p))
-    return new Response('{"error":"You cannot use this filter"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 403,
-    });
+    return jsonError("You cannot use this filter", 403);
 
   if (isNaN(before) || before > Date.now())
-    return new Response('{"error":"Invalid `before` parameter"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+    return jsonError("Invalid `before` parameter", 400);
 
   const prefix = types[entryType];
   const table = tables[entryType];
@@ -77,9 +64,5 @@ export async function onRequestGet(context: RequestContext) {
       }
     }
 
-  return new Response(JSON.stringify(items.filter((v) => v !== null)), {
-    headers: {
-      "content-type": "application/json",
-    },
-  });
+  return jsonResponse(JSON.stringify(items.filter((v) => v !== null)));
 }
diff --git a/functions/api/reports/[id]/action.ts b/functions/api/reports/[id]/action.ts
index 7accad7..f9fe11c 100644
--- a/functions/api/reports/[id]/action.ts
+++ b/functions/api/reports/[id]/action.ts
@@ -1,5 +1,6 @@
-import { insertLogs } from "../../../gcloud.js";
 import { getBanList, setBanList } from "../../../roblox-open-cloud.js";
+import { insertLogs } from "../../../gcloud.js";
+import { jsonError } from "../../../common.js";
 
 export async function onRequestPost(context: RequestContext) {
   const actionMap = context.data.body;
@@ -13,11 +14,7 @@ export async function onRequestPost(context: RequestContext) {
       action < 0 ||
       action > 2
     )
-      return new Response('{"error":"Invalid action map"}', {
-        headers: {
-          "content-type": "application/json",
-        },
-      });
+      return jsonError("Invalid action map", 400);
 
     if (action === 0) continue;
 
diff --git a/functions/api/reports/complete.ts b/functions/api/reports/complete.ts
index 0ca2186..4e67ca9 100644
--- a/functions/api/reports/complete.ts
+++ b/functions/api/reports/complete.ts
@@ -1,13 +1,9 @@
+import { jsonError } from "../../common.js";
+
 export async function onRequestPost(context: RequestContext) {
   const { id } = context.data.body;
 
-  if (!id)
-    return new Response('{"error":"No ID provided"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+  if (!id) return jsonError("No ID provided", 400);
 
   const user = await context.env.DATA.get(`reportprocessing_${id}`);
 
@@ -17,24 +13,13 @@ export async function onRequestPost(context: RequestContext) {
       ? user !== context.data.current_user.id
       : user !== context.request.headers.get("CF-Connecting-IP"))
   )
-    return new Response('{"error":"No report with that ID is processing"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 404,
-    });
+    return jsonError("No report with that ID is processing", 404);
 
   await context.env.DATA.delete(`reportprocessing_${id}`);
 
   const value = await context.env.DATA.get(`report_${id}`);
 
-  if (!value)
-    return new Response('{"error":"Report is missing"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 500,
-    });
+  if (!value) return jsonError("Report is missing", 500);
 
   if (context.env.REPORTS_WEBHOOK) {
     await fetch(context.env.REPORTS_WEBHOOK, {
diff --git a/functions/api/reports/recall.ts b/functions/api/reports/recall.ts
index 9dbe166..dbd16e9 100644
--- a/functions/api/reports/recall.ts
+++ b/functions/api/reports/recall.ts
@@ -1,13 +1,9 @@
+import { jsonError } from "../../common.js";
+
 export async function onRequestPost(context: RequestContext) {
   const { id } = context.data.body;
 
-  if (!id)
-    return new Response('{"error":"No ID provided"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+  if (!id) return jsonError("No ID provided", 400);
 
   const reportUserId = await context.env.DATA.get(`reportprocessing_${id}`);
 
@@ -16,12 +12,7 @@ export async function onRequestPost(context: RequestContext) {
     (context.data.current_user?.id !== reportUserId &&
       context.request.headers.get("CF-Connecting-IP") !== reportUserId)
   )
-    return new Response('{"error":"No processing report with that ID found"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 404,
-    });
+    return jsonError("No processing report with that ID found", 404);
 
   await context.env.DATA.delete(`report_${id}`);
 
diff --git a/functions/api/reports/submit.ts b/functions/api/reports/submit.ts
index 50a0198..b66e6e3 100644
--- a/functions/api/reports/submit.ts
+++ b/functions/api/reports/submit.ts
@@ -1,13 +1,5 @@
 import { GenerateUploadURL } from "../../gcloud.js";
-
-function errorResponse(error: string, status: number): Response {
-  return new Response(JSON.stringify({ error }), {
-    headers: {
-      "content-type": "application/json",
-    },
-    status,
-  });
-}
+import { jsonError, jsonResponse } from "../../common.js";
 
 export async function onRequestPost(context: RequestContext) {
   const { actions, bypass, description, files, turnstileResponse, usernames } =
@@ -15,7 +7,7 @@ export async function onRequestPost(context: RequestContext) {
 
   if (!context.data.current_user) {
     if (typeof turnstileResponse !== "string")
-      return errorResponse("You must complete the captcha", 401);
+      return jsonError("You must complete the captcha", 401);
 
     const turnstileAPIResponse = await fetch(
       "https://challenges.cloudflare.com/turnstile/v0/siteverify",
@@ -34,26 +26,26 @@ export async function onRequestPost(context: RequestContext) {
 
     const { success }: { success: boolean } = await turnstileAPIResponse.json();
 
-    if (!success) return errorResponse("Captcha test failed", 403);
+    if (!success) return jsonError("Captcha test failed", 403);
   }
 
   const origin = context.request.headers.get("Origin");
-  if (!origin) return errorResponse("No origin header", 400);
+  if (!origin) return jsonError("No origin header", 400);
 
   if (bypass && !(context.data.current_user?.permissions & (1 << 5)))
-    return errorResponse("Bypass directive cannot be used", 403);
+    return jsonError("Bypass directive cannot be used", 403);
 
   if (typeof bypass !== "boolean")
-    return errorResponse("Bypass must be a boolean", 400);
+    return jsonError("Bypass must be a boolean", 400);
 
   if (!Array.isArray(usernames))
-    return errorResponse("Usernames must be type of array", 400);
+    return jsonError("Usernames must be type of array", 400);
 
   if (
     !["string", "undefined"].includes(typeof description) ||
     description?.length > 512
   )
-    return errorResponse("Invalid description", 400);
+    return jsonError("Invalid description", 400);
 
   if (
     !Array.isArray(files) ||
@@ -63,7 +55,7 @@ export async function onRequestPost(context: RequestContext) {
       return !keys.includes("name") || !keys.includes("size");
     })
   )
-    return errorResponse("File list missing name(s) and/or size(s)", 400);
+    return jsonError("File list missing name(s) and/or size(s)", 400);
 
   if (
     files.find(
@@ -74,13 +66,10 @@ export async function onRequestPost(context: RequestContext) {
         file.size > 536870912,
     )
   )
-    return errorResponse(
-      "One or more files contain an invalid name or size",
-      400,
-    );
+    return jsonError("One or more files contain an invalid name or size", 400);
 
   if (!usernames.length || usernames.length > 20)
-    return errorResponse(
+    return jsonError(
       "Number of usernames provided must be between 1 and 20",
       400,
     );
@@ -91,7 +80,7 @@ export async function onRequestPost(context: RequestContext) {
       username.length > 20 ||
       username.match(/_/g)?.length > 1
     )
-      return errorResponse(`Username "${username}" is invalid`, 400);
+      return jsonError(`Username "${username}" is invalid`, 400);
   }
 
   const rbxSearchReq = await fetch(
@@ -109,7 +98,7 @@ export async function onRequestPost(context: RequestContext) {
   );
 
   if (!rbxSearchReq.ok)
-    return errorResponse(
+    return jsonError(
       "Failed to locate Roblox users due to upstream error",
       500,
     );
@@ -125,7 +114,7 @@ export async function onRequestPost(context: RequestContext) {
         missingUsers.push(userData.requestedUsername);
     }
 
-    return errorResponse(
+    return jsonError(
       `The following users do not exist or are banned from Roblox: ${missingUsers.toString()}`,
       400,
     );
@@ -165,7 +154,7 @@ export async function onRequestPost(context: RequestContext) {
         "webp",
       ].includes(fileExten.toLowerCase())
     )
-      return errorResponse(
+      return jsonError(
         `File ${file.name} cannot be uploaded as it is unsupported`,
         415,
       );
@@ -202,7 +191,7 @@ export async function onRequestPost(context: RequestContext) {
   );
 
   if (uploadUrlResults.find((uploadUrl) => uploadUrl.status === "rejected"))
-    return errorResponse("Failed to generate upload url", 500);
+    return jsonError("Failed to generate upload url", 500);
 
   const attachments: string[] = [];
   const uploadUrls: string[] = [];
@@ -251,12 +240,7 @@ export async function onRequestPost(context: RequestContext) {
       .run();
   } catch {}
 
-  return new Response(
+  return jsonResponse(
     JSON.stringify({ id: reportId, upload_urls: uploadUrls }),
-    {
-      headers: {
-        "content-type": "application/json",
-      },
-    },
   );
 }
diff --git a/functions/api/uploads/_middleware.ts b/functions/api/uploads/_middleware.ts
index e3109cc..7e7aea9 100644
--- a/functions/api/uploads/_middleware.ts
+++ b/functions/api/uploads/_middleware.ts
@@ -1,3 +1,5 @@
+import { jsonError } from "../../common.js";
+
 export async function onRequest(context: RequestContext) {
   const { current_user: currentUser } = context.data;
 
@@ -5,12 +7,7 @@ export async function onRequest(context: RequestContext) {
     !(currentUser?.permissions & (1 << 5)) &&
     !(currentUser?.permissions & (1 << 12))
   )
-    return new Response('{"error":"Forbidden"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 403,
-    });
+    return jsonError("Forbidden", 403);
 
   return await context.next();
 }
diff --git a/functions/api/uploads/status.ts b/functions/api/uploads/status.ts
index 609baa8..134091f 100644
--- a/functions/api/uploads/status.ts
+++ b/functions/api/uploads/status.ts
@@ -1,3 +1,5 @@
+import { jsonError } from "../../common.js";
+
 export async function onRequestPost(context: RequestContext) {
   const { body } = context.data;
 
@@ -5,23 +7,9 @@ export async function onRequestPost(context: RequestContext) {
     !Array.isArray(body) ||
     body.find((attachment) => typeof attachment !== "string")
   )
-    return new Response(
-      '{"error":"Request body must be an array of strings"}',
-      {
-        headers: {
-          "content-type": "application/json",
-        },
-        status: 400,
-      },
-    );
+    return jsonError("Request body must be an array of strings", 400);
 
-  if (body.length > 3)
-    return new Response('{"error":"Too many video ids"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 400,
-    });
+  if (body.length > 3) return jsonError("Too many video ids", 400);
 
   const kvPromises = [];
 
@@ -31,12 +19,7 @@ export async function onRequestPost(context: RequestContext) {
   const kvResults = await Promise.allSettled(kvPromises);
 
   if (kvResults.find((result) => result.status === "rejected"))
-    return new Response('{"error":"Failed to check status of attachments"}', {
-      headers: {
-        "content-type": "application/json",
-      },
-      status: 500,
-    });
+    return jsonError("Failed to check status of attachments", 500);
 
   return new Response(null, {
     status: kvResults.find((result) => result !== null) ? 409 : 204,