diff --git a/apps/blade/src/app/_components/shared/scanner.tsx b/apps/blade/src/app/_components/shared/scanner.tsx
index 2c1b43cae..071384c84 100644
--- a/apps/blade/src/app/_components/shared/scanner.tsx
+++ b/apps/blade/src/app/_components/shared/scanner.tsx
@@ -26,9 +26,9 @@ import {
} from "@forge/ui/form";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@forge/ui/tabs";
import { toast } from "@forge/ui/toast";
+import { hackathons } from "@forge/utils";
import ToggleButton from "~/app/_components/admin/hackathon/hackers/toggle-button";
-import { getClassTeam } from "~/lib/utils";
import { api } from "~/trpc/react";
const ScannerPopUp = ({ eventType }: { eventType: "Member" | "Hacker" }) => {
@@ -337,8 +337,9 @@ const ScannerPopUp = ({ eventType }: { eventType: "Member" | "Hacker" }) => {
{assignedClass}
diff --git a/apps/blade/src/app/admin/forms/[slug]/page.tsx b/apps/blade/src/app/admin/forms/[slug]/page.tsx
index a7dc299b4..4908c0112 100644
--- a/apps/blade/src/app/admin/forms/[slug]/page.tsx
+++ b/apps/blade/src/app/admin/forms/[slug]/page.tsx
@@ -2,9 +2,9 @@ import { redirect } from "next/navigation";
import { appRouter } from "@forge/api";
import { auth } from "@forge/auth/server";
+import { trpc } from "@forge/utils";
import { EditorClient } from "~/app/_components/admin/forms/editor/client";
-import { extractProcedures } from "~/lib/utils";
import { api } from "~/trpc/server";
export default async function FormEditorPage({
@@ -35,7 +35,10 @@ export default async function FormEditorPage({
return (
<>
-
+
>
);
}
diff --git a/apps/blade/src/app/api/membership/route.ts b/apps/blade/src/app/api/membership/route.ts
index 97f754b3e..deb375633 100644
--- a/apps/blade/src/app/api/membership/route.ts
+++ b/apps/blade/src/app/api/membership/route.ts
@@ -1,9 +1,9 @@
-/* eslint-disable no-console */
import type { NextRequest } from "next/server";
import Stripe from "stripe";
import { db } from "@forge/db/client";
import { DuesPayment, DuesPaymentSchema } from "@forge/db/schemas/knight-hacks";
+import { logger } from "@forge/utils";
import { env } from "~/env";
@@ -11,10 +11,10 @@ async function membershipRecord(sessionId: string) {
const stripe = new Stripe(env.STRIPE_SECRET_KEY, { typescript: true });
// TODO: Make this function safe to run multiple times,
- // even concurrently, with the same session ID
+ // even concurrently, with the same session ID
// TODO: Make sure fulfillment hasn't already been
- // peformed for this Checkout Session
+ // peformed for this Checkout Session
// Retrieve the Checkout Session from the API
try {
@@ -32,7 +32,7 @@ async function membershipRecord(sessionId: string) {
}).safeParse(values);
if (!validatedCheckoutFields.success) {
- console.log(validatedCheckoutFields.error.issues);
+ logger.log(validatedCheckoutFields.error.issues);
throw new Error("Invalid or missing field(s)");
}
// Check the Checkout Session's payment_status property
@@ -43,7 +43,7 @@ async function membershipRecord(sessionId: string) {
}
throw new Error("Checkout session payment status is unpaid");
} catch (e) {
- console.error("Error:", e);
+ logger.error("Error:", e);
return false;
}
}
diff --git a/apps/blade/src/app/api/trpc/[trpc]/route.ts b/apps/blade/src/app/api/trpc/[trpc]/route.ts
index a969026b8..63541474c 100644
--- a/apps/blade/src/app/api/trpc/[trpc]/route.ts
+++ b/apps/blade/src/app/api/trpc/[trpc]/route.ts
@@ -60,6 +60,7 @@ const handler = async (req: Request) => {
headers: req.headers,
}),
onError({ error, path }) {
+ // TODO: look into not logging into the console
// eslint-disable-next-line no-console
console.error(`>>> tRPC Error on '${path}'`, error.message);
},
diff --git a/apps/blade/src/app/judge/session/route.ts b/apps/blade/src/app/judge/session/route.ts
index 757a18ca6..10516daa8 100644
--- a/apps/blade/src/app/judge/session/route.ts
+++ b/apps/blade/src/app/judge/session/route.ts
@@ -1,10 +1,10 @@
// apps/blade/app/api/judge/session/route.ts
import { NextResponse } from "next/server";
-import { getJudgeSessionFromCookie } from "../../../../../../packages/api/src/utils";
+import * as permissionsServer from "@forge/utils/permissions.server";
export async function GET() {
- const row = await getJudgeSessionFromCookie();
+ const row = await permissionsServer.getJudgeSessionFromCookie();
if (!row) return NextResponse.json({ ok: false }, { status: 401 });
return NextResponse.json({ ok: true, roomName: row.roomName });
}
diff --git a/apps/blade/src/lib/utils.ts b/apps/blade/src/lib/utils.ts
deleted file mode 100644
index 842737ff5..000000000
--- a/apps/blade/src/lib/utils.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-import type { AnyTRPCProcedure, AnyTRPCRouter } from "@trpc/server";
-import type { z } from "zod";
-
-import type { EVENTS } from "@forge/consts";
-import type { HackerClass } from "@forge/db/schemas/knight-hacks";
-import { PERMISSIONS } from "@forge/consts";
-
-export const formatDateTime = (date: Date) => {
- // Create a new Date object 5 hours behind the original
- const adjustedDate = new Date(date.getTime());
- adjustedDate.setDate(adjustedDate.getDate() + 1);
-
- return adjustedDate.toLocaleString("en-US", {
- month: "short",
- day: "numeric",
- year: "numeric",
- hour: "numeric",
- minute: "2-digit",
- hour12: true,
- });
-};
-
-export const getFormattedDate = (start_datetime: string | Date) => {
- const date = new Date(start_datetime);
- date.setDate(date.getDate() + 1);
- return date.toLocaleDateString();
-};
-
-export const formatDateRange = (startDate: Date, endDate: Date) => {
- const start = new Date(startDate).toLocaleDateString("en-US", {
- month: "short",
- day: "numeric",
- });
- const end = new Date(endDate).toLocaleDateString("en-US", {
- month: "short",
- day: "numeric",
- year: "numeric",
- });
- return `${start} - ${end}`;
-};
-
-export const getTagColor = (tag: EVENTS.EventTagsColor) => {
- const colors: Record
= {
- GBM: "bg-blue-100 text-blue-800",
- Social: "bg-pink-100 text-pink-800",
- Kickstart: "bg-green-100 text-green-800",
- "Project Launch": "bg-purple-100 text-purple-800",
- "Hello World": "bg-yellow-100 text-yellow-800",
- Sponsorship: "bg-orange-100 text-orange-800",
- "Tech Exploration": "bg-cyan-100 text-cyan-800",
- "Class Support": "bg-indigo-100 text-indigo-800",
- Workshop: "bg-teal-100 text-teal-800",
- OPS: "bg-purple-100 text-purple-800",
- Hackathon: "bg-violet-100 text-violet-800",
- Collabs: "bg-red-100 text-red-800",
- "Check-in": "bg-gray-100 text-gray-800",
- Ceremony: "bg-amber-100 text-amber-800",
- Merch: "bg-lime-100 text-lime-800",
- Food: "bg-rose-100 text-rose-800",
- "CAREER-FAIR": "bg-lime-100 text-lime-800", // change later
- "RSO-FAIR": "bg-lime-100 text-lime-800", // change later
- };
- return colors[tag];
-};
-
-export const getClassTeam = (tag: HackerClass) => {
- if (["Harbinger", "Alchemist", "Monstologist"].includes(tag)) {
- return {
- team: "Monstrosity",
- teamColor: "#e03131",
- imgUrl: "/khviii/lenneth.jpg",
- };
- }
- return {
- team: "Humanity",
- teamColor: "#228be6",
- imgUrl: "/khviii/tkhero.jpg",
- };
-};
-
-export interface ProcedureMeta {
- inputSchema: string[];
- route: string;
-}
-
-interface ProcedureMetaOriginal {
- id: string;
- /* eslint-disable @typescript-eslint/no-explicit-any */
- inputSchema: z.ZodObject;
-}
-
-function hasSchemaMeta(meta: unknown): meta is ProcedureMetaOriginal {
- return (
- typeof meta === "object" &&
- meta !== null &&
- "id" in meta &&
- "inputSchema" in meta
- );
-}
-
-export function extractProcedures(router: AnyTRPCRouter) {
- const procedures: Record = {};
-
- /* eslint-disable @typescript-eslint/no-unsafe-argument */
- for (const [procKey, proc] of Object.entries(router._def.procedures)) {
- const procTyped = proc as AnyTRPCProcedure;
-
- const meta = procTyped._def.meta;
- if (!hasSchemaMeta(meta)) continue;
-
- procedures[meta.id] = {
- inputSchema: Object.keys(meta.inputSchema.shape),
- route: procKey,
- };
- }
-
- return procedures;
-}
-
-export function getPermsAsList(perms: string) {
- const list = [];
- const permKeys = Object.keys(PERMISSIONS.PERMISSIONS);
- for (let i = 0; i < perms.length; i++) {
- const permKey = permKeys.at(i);
- if (perms[i] == "1" && permKey) {
- const permissionData = PERMISSIONS.PERMISSION_DATA[permKey];
- if (permissionData) list.push(permissionData.name);
- }
- }
- return list;
-}
diff --git a/apps/club/package.json b/apps/club/package.json
index c58058758..172dc94aa 100644
--- a/apps/club/package.json
+++ b/apps/club/package.json
@@ -20,6 +20,7 @@
"@forge/consts": "workspace:*",
"@forge/db": "workspace:*",
"@forge/ui": "workspace:*",
+ "@forge/utils": "workspace:*",
"@gsap/react": "^2.1.2",
"@svgr/webpack": "^8.1.0",
"framer-motion": "^12.0.1",
diff --git a/apps/club/src/app/_components/contact/contact-form.tsx b/apps/club/src/app/_components/contact/contact-form.tsx
index 9f84b582f..904a4363e 100644
--- a/apps/club/src/app/_components/contact/contact-form.tsx
+++ b/apps/club/src/app/_components/contact/contact-form.tsx
@@ -36,6 +36,7 @@ function ContactForm() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const jsonFormData = JSON.stringify(formData);
+ // TODO: look into not logging into the console
// eslint-disable-next-line no-console
console.log("The Form:", jsonFormData);
};
diff --git a/apps/club/src/app/_components/landing/calendar.tsx b/apps/club/src/app/_components/landing/calendar.tsx
index edd01141f..8f5e19fab 100644
--- a/apps/club/src/app/_components/landing/calendar.tsx
+++ b/apps/club/src/app/_components/landing/calendar.tsx
@@ -9,13 +9,14 @@ import { Calendar, List } from "rsuite";
import type { RouterOutputs } from "@forge/api";
import type { ReturnEvent } from "@forge/db/schemas/knight-hacks";
-import { formatDateRange } from "~/lib/utils";
import NeonTkSVG from "./assets/neon-tk";
import SwordSVG from "./assets/sword";
import TerminalSVG from "./assets/terminal";
import "rsuite/Calendar/styles/index.css";
+import { time } from "@forge/utils";
+
export default function CalendarEventsPage({
events,
}: {
@@ -93,7 +94,7 @@ export default function CalendarEventsPage({
>
- {formatDateRange(item.start_datetime, item.end_datetime)}
+ {time.formatTimeRange(item.start_datetime, item.end_datetime)}
{item.name}
@@ -102,6 +103,7 @@ export default function CalendarEventsPage({
);
};
+
const handleSelect = (date: Date) => {
setSelectedDate(date);
};
diff --git a/apps/club/src/lib/utils.ts b/apps/club/src/lib/utils.ts
deleted file mode 100644
index ca5846046..000000000
--- a/apps/club/src/lib/utils.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-export function formatHourTime(date: Date): string {
- const hours = date.getHours();
- const minutes = date.getMinutes();
- const ampm = hours >= 12 ? "pm" : "am";
-
- // Convert hours to 12-hour format
- const formattedHours = hours % 12 || 12;
- // Pad minutes with leading zero if necessary
- const formattedMinutes = minutes.toString().padStart(2, "0");
-
- return `${formattedHours}:${formattedMinutes}${ampm}`;
-}
-
-export const formatDateRange = (startDate: Date, endDate: Date) => {
- const start = formatHourTime(startDate);
- const end = formatHourTime(endDate);
- return `${start} - ${end}`;
-};
diff --git a/apps/cron/eslint.config.js b/apps/cron/eslint.config.js
index b2960a0c3..d2dcb3e01 100644
--- a/apps/cron/eslint.config.js
+++ b/apps/cron/eslint.config.js
@@ -7,9 +7,4 @@ export default [
},
...baseConfig,
...restrictEnvAccess,
- {
- rules: {
- "no-console": "off",
- },
- },
];
diff --git a/apps/cron/package.json b/apps/cron/package.json
index e716a1cdc..6fb213126 100644
--- a/apps/cron/package.json
+++ b/apps/cron/package.json
@@ -17,6 +17,7 @@
"@forge/api": "workspace:*",
"@forge/consts": "workspace:*",
"@forge/db": "workspace:*",
+ "@forge/utils": "workspace:*",
"@forge/validators": "workspace:*",
"@t3-oss/env-core": "^0.11.1",
"discord.js": "^14.16.3",
diff --git a/apps/cron/src/crons/_example.ts b/apps/cron/src/crons/_example.ts
index 050665d90..5574740ba 100644
--- a/apps/cron/src/crons/_example.ts
+++ b/apps/cron/src/crons/_example.ts
@@ -1,3 +1,5 @@
+import { logger } from "@forge/utils";
+
import { CronBuilder } from "../structs/CronBuilder";
export const testCron = new CronBuilder({
@@ -6,6 +8,6 @@ export const testCron = new CronBuilder({
}).addCron(
"* * * * * ", // every minute
() => {
- console.log("This is an example cron that runs every minute");
+ logger.log("This is an example cron that runs every minute");
},
);
diff --git a/apps/cron/src/crons/alumni-assign.ts b/apps/cron/src/crons/alumni-assign.ts
index bc017b3a0..b463d1f98 100644
--- a/apps/cron/src/crons/alumni-assign.ts
+++ b/apps/cron/src/crons/alumni-assign.ts
@@ -1,13 +1,10 @@
import { and, gt, isNotNull, isNull, lte, or } from "drizzle-orm";
-import {
- addRoleToMember,
- removeRoleFromMember,
- resolveDiscordUserId,
-} from "@forge/api/utils";
import { DISCORD } from "@forge/consts";
import { db } from "@forge/db/client";
import { Member } from "@forge/db/schemas/knight-hacks";
+import { logger } from "@forge/utils";
+import * as discord from "@forge/utils/discord";
import { CronBuilder } from "../structs/CronBuilder";
@@ -27,10 +24,11 @@ export const alumniAssign = new CronBuilder({
for (const { discordUser } of graduatedMembers) {
try {
- const discordId = await resolveDiscordUserId(discordUser);
- if (discordId) await addRoleToMember(discordId, DISCORD.ALUMNI_ROLE);
+ const discordId = await discord.resolveDiscordUserId(discordUser);
+ if (discordId)
+ await discord.addRoleToMember(discordId, DISCORD.ALUMNI_ROLE);
} catch (err) {
- console.error(`Failed to add alumni role for ${discordUser}:`, err);
+ logger.error(`Failed to add alumni role for ${discordUser}:`, err);
}
}
@@ -49,11 +47,11 @@ export const alumniAssign = new CronBuilder({
for (const { discordUser } of nonGraduatedMembers) {
try {
- const discordId = await resolveDiscordUserId(discordUser);
+ const discordId = await discord.resolveDiscordUserId(discordUser);
if (discordId)
- await removeRoleFromMember(discordId, DISCORD.ALUMNI_ROLE);
+ await discord.removeRoleFromMember(discordId, DISCORD.ALUMNI_ROLE);
} catch (err) {
- console.error(`Failed to remove alumni role for ${discordUser}:`, err);
+ logger.error(`Failed to remove alumni role for ${discordUser}:`, err);
}
}
},
diff --git a/apps/cron/src/crons/animals.ts b/apps/cron/src/crons/animals.ts
index af76ab0f5..e70f951ce 100644
--- a/apps/cron/src/crons/animals.ts
+++ b/apps/cron/src/crons/animals.ts
@@ -7,6 +7,7 @@ import sharp from "sharp";
import { db } from "@forge/db/client";
import { Permissions } from "@forge/db/schemas/auth";
import { Member } from "@forge/db/schemas/knight-hacks";
+import { logger } from "@forge/utils";
import { env } from "../env";
import { CronBuilder } from "../structs/CronBuilder";
@@ -102,7 +103,7 @@ export const goat = new CronBuilder({
].find((u) => u?.trim());
const name = replaceName(`${goat.firstName} ${goat.lastName}`);
- console.log("goat chosen: ", name);
+ logger.log("goat chosen: ", name);
const embed = await createEmbed(
goat.profilePictureUrl,
diff --git a/apps/cron/src/crons/backup-filtered-db.ts b/apps/cron/src/crons/backup-filtered-db.ts
index acec10dab..9f648dc24 100644
--- a/apps/cron/src/crons/backup-filtered-db.ts
+++ b/apps/cron/src/crons/backup-filtered-db.ts
@@ -1,6 +1,8 @@
import { spawn } from "child_process";
import { createInterface } from "readline/promises";
+import { logger } from "@forge/utils";
+
import { CronBuilder } from "../structs/CronBuilder";
const COMMAND = "pnpm";
@@ -36,7 +38,7 @@ export const backupFilteredDb = new CronBuilder({
input: stream,
crlfDelay: Infinity,
})) {
- if (line) console[key](line);
+ if (line) logger[key](line);
}
}),
);
diff --git a/apps/cron/src/crons/leetcode.ts b/apps/cron/src/crons/leetcode.ts
index 2c839badb..bf3554db8 100644
--- a/apps/cron/src/crons/leetcode.ts
+++ b/apps/cron/src/crons/leetcode.ts
@@ -2,7 +2,7 @@ import type { APIThreadChannel } from "discord-api-types/v10";
import { Routes, ThreadAutoArchiveDuration } from "discord-api-types/v10";
import { WebhookClient } from "discord.js";
-import { discord } from "@forge/api/utils";
+import * as discord from "@forge/utils/discord";
import { env } from "../env";
import { CronBuilder } from "../structs/CronBuilder";
@@ -90,12 +90,15 @@ export const leetcode = new CronBuilder({
embeds: [problemEmbed],
});
- const thread = (await discord.post(Routes.threads(msg.channel_id, msg.id), {
- body: {
- name: dateString,
- auto_archive_duration: ThreadAutoArchiveDuration.OneDay,
+ const thread = (await discord.api.post(
+ Routes.threads(msg.channel_id, msg.id),
+ {
+ body: {
+ name: dateString,
+ auto_archive_duration: ThreadAutoArchiveDuration.OneDay,
+ },
},
- })) as APIThreadChannel;
+ )) as APIThreadChannel;
await LEETCODE_WEBHOOK.send({
content: "Make sure to wrap your solution with spoiler tags!",
diff --git a/apps/cron/src/crons/reminder.ts b/apps/cron/src/crons/reminder.ts
index 545353c91..6b22b147d 100644
--- a/apps/cron/src/crons/reminder.ts
+++ b/apps/cron/src/crons/reminder.ts
@@ -5,6 +5,7 @@ import { asc } from "drizzle-orm";
import { db } from "@forge/db/client";
import { Event } from "@forge/db/schemas/knight-hacks";
+import { logger } from "@forge/utils";
import { env } from "../env";
import { CronBuilder } from "../structs/CronBuilder";
@@ -137,11 +138,11 @@ function genCronLogic(webhook: WebhookClient): () => Promise {
0,
);
- console.log(`Found a total of ${totalEvents} events`);
+ logger.log(`Found a total of ${totalEvents} events`);
for (const group of groupedPrefixes) {
- console.log(`Events for ${group.prefix}`);
+ logger.log(`Events for ${group.prefix}`);
for (const event of group.events) {
- console.log(`Title: ${event.name}`);
+ logger.log(`Title: ${event.name}`);
}
}
@@ -386,21 +387,21 @@ async function getEvents() {
event.end_datetime < todayEnd && event.start_datetime >= todayStart,
);
- console.log("Today's Events: ", todayEvents);
+ logger.log("Today's Events: ", todayEvents);
const tomorrowEvents = allEvents.filter(
(event) =>
event.end_datetime < tomorrowEnd && event.start_datetime >= tomorrowStart,
);
- console.log("Tomorrow's Events: ", tomorrowEvents);
+ logger.log("Tomorrow's Events: ", tomorrowEvents);
const nextWeekEvents = allEvents.filter(
(event) =>
event.end_datetime < nextWeekEnd && event.start_datetime >= nextWeekStart,
);
- console.log("Next Week's Events: ", nextWeekEvents);
+ logger.log("Next Week's Events: ", nextWeekEvents);
// Filter out "Operations Meeting" and "Project Launch Lab Hours" from nextWeek
const nextWeekFiltered = nextWeekEvents.filter((event) => {
diff --git a/apps/cron/src/crons/role-sync.ts b/apps/cron/src/crons/role-sync.ts
index 02946a487..36c1a2307 100644
--- a/apps/cron/src/crons/role-sync.ts
+++ b/apps/cron/src/crons/role-sync.ts
@@ -1,11 +1,12 @@
import type { APIGuildMember } from "discord-api-types/v10";
import { Routes } from "discord-api-types/v10";
-import { discord } from "@forge/api/utils";
import { DISCORD } from "@forge/consts";
import { eq } from "@forge/db";
import { db } from "@forge/db/client";
import { Permissions, Roles, User } from "@forge/db/schemas/auth";
+import { logger } from "@forge/utils";
+import * as discord from "@forge/utils/discord";
import { CronBuilder } from "../structs/CronBuilder";
@@ -24,11 +25,11 @@ export const roleSync = new CronBuilder({
async () => {
// Get all roles that are linked in Blade
const linkedRoles = await db.select().from(Roles);
- console.log(`Found ${linkedRoles.length} linked roles`);
+ logger.log(`Found ${linkedRoles.length} linked roles`);
// Get all users in Blade
const users = await db.select().from(User);
- console.log(`Checking ${users.length} users`);
+ logger.log(`Checking ${users.length} users`);
let addedCount = 0;
let removedCount = 0;
@@ -40,7 +41,7 @@ export const roleSync = new CronBuilder({
for (const user of users) {
try {
// Fetch the user's roles from Discord
- const guildMember = (await discord.get(
+ const guildMember = (await discord.api.get(
Routes.guildMember(DISCORD.KNIGHTHACKS_GUILD, user.discordUserId),
)) as APIGuildMember;
@@ -96,14 +97,14 @@ export const roleSync = new CronBuilder({
}
}
- console.log(
+ logger.log(
`Sync completed. Added: ${addedCount}, Removed: ${removedCount}, Skipped: ${skippedCount}, Errors: ${errorCount}`,
);
if (errorCount > 0) {
- console.warn(`First ${erroredUsers.length} users it errored for:`);
+ logger.warn(`First ${erroredUsers.length} users it errored for:`);
for (const name of erroredUsers) {
- console.warn(name);
+ logger.warn(name);
}
}
},
diff --git a/apps/cron/src/structs/CronBuilder.ts b/apps/cron/src/structs/CronBuilder.ts
index 6d50b29f3..80b276705 100644
--- a/apps/cron/src/structs/CronBuilder.ts
+++ b/apps/cron/src/structs/CronBuilder.ts
@@ -1,6 +1,8 @@
import { AsyncLocalStorage } from "node:async_hooks";
import cron from "node-cron";
+import { logger } from "@forge/utils";
+
// DO NOT TOUCH
// Basically the whole point here is to override console.log such that when
// we are inside of the CronBuilder AsyncLocalStorage, we add the logging info
@@ -83,23 +85,23 @@ export class CronBuilder {
for (const { expression, executor } of this.crons) {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
cron.schedule(expression, this._executor.bind(this, executor));
- currentCron.run(this, () => console.log(`scheduled @ ${expression}`));
+ currentCron.run(this, () => logger.log(`scheduled @ ${expression}`));
}
}
private async _executor(executor: ExecutorFunction): Promise {
return await currentCron.run(this, async () => {
const startTime = Date.now();
- console.log(`started @ ${new Date(startTime).toLocaleTimeString()}`);
+ logger.log(`started @ ${new Date(startTime).toLocaleTimeString()}`);
try {
await executor();
} catch (error) {
- console.error(error);
+ logger.error(error);
}
const endTime = Date.now();
- console.log(
+ logger.log(
`finished @ ${new Date(endTime).toLocaleTimeString()} (${endTime - startTime}ms)`,
);
});
diff --git a/apps/gemiknights/src/app/_components/ui/background-gradient-animation.tsx b/apps/gemiknights/src/app/_components/ui/background-gradient-animation.tsx
index 0fa14255a..cada9c779 100644
--- a/apps/gemiknights/src/app/_components/ui/background-gradient-animation.tsx
+++ b/apps/gemiknights/src/app/_components/ui/background-gradient-animation.tsx
@@ -2,7 +2,7 @@
import { useEffect, useRef, useState } from "react";
-import { cn } from "~/lib/utils";
+import { cn } from "@forge/ui";
export const BackgroundGradientAnimation = ({
gradientBackgroundStart = "rgb(108, 0, 162)",
diff --git a/apps/gemiknights/src/lib/utils.ts b/apps/gemiknights/src/lib/utils.ts
deleted file mode 100644
index 88283f013..000000000
--- a/apps/gemiknights/src/lib/utils.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import type { ClassValue } from "clsx";
-import { clsx } from "clsx";
-import { twMerge } from "tailwind-merge";
-
-export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs));
-}
diff --git a/apps/tk/eslint.config.js b/apps/tk/eslint.config.js
index b2960a0c3..d2dcb3e01 100644
--- a/apps/tk/eslint.config.js
+++ b/apps/tk/eslint.config.js
@@ -7,9 +7,4 @@ export default [
},
...baseConfig,
...restrictEnvAccess,
- {
- rules: {
- "no-console": "off",
- },
- },
];
diff --git a/apps/tk/package.json b/apps/tk/package.json
index d013f6ebe..35d7c1d15 100644
--- a/apps/tk/package.json
+++ b/apps/tk/package.json
@@ -16,6 +16,7 @@
"dependencies": {
"@forge/consts": "workspace:*",
"@forge/db": "workspace:*",
+ "@forge/utils": "workspace:*",
"@forge/validators": "workspace:*",
"@t3-oss/env-core": "^0.11.1",
"discord.js": "^14.16.3",
diff --git a/apps/tk/src/commands/capybara.ts b/apps/tk/src/commands/capybara.ts
index 43087b66e..eec4fd53d 100644
--- a/apps/tk/src/commands/capybara.ts
+++ b/apps/tk/src/commands/capybara.ts
@@ -2,6 +2,8 @@ import type { CommandInteraction } from "discord.js";
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
import JIMP from "jimp";
+import { logger } from "@forge/utils";
+
import { TK_CAPYBARA_URL } from "../consts";
// CAPYBARA COMMAND
@@ -46,9 +48,9 @@ export async function execute(interaction: CommandInteraction) {
// catch any errors
} catch (err: unknown) {
if (err instanceof Error) {
- console.error(err.message);
+ logger.error(err.message);
} else {
- console.error("An unknown error occurred: ", err);
+ logger.error("An unknown error occurred: ", err);
}
}
}
diff --git a/apps/tk/src/commands/cat.ts b/apps/tk/src/commands/cat.ts
index 8912fbbe0..2bcac605f 100644
--- a/apps/tk/src/commands/cat.ts
+++ b/apps/tk/src/commands/cat.ts
@@ -2,6 +2,8 @@ import type { CommandInteraction } from "discord.js";
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
import JIMP from "jimp";
+import { logger } from "@forge/utils";
+
import { TK_CAT_URL } from "../consts";
// CAT COMMAND
@@ -50,9 +52,9 @@ export async function execute(interaction: CommandInteraction) {
// catch any errors
} catch (err: unknown) {
if (err instanceof Error) {
- console.error(err.message);
+ logger.error(err.message);
} else {
- console.error("An unknown error occurred: ", err);
+ logger.error("An unknown error occurred: ", err);
}
}
}
diff --git a/apps/tk/src/commands/dog.ts b/apps/tk/src/commands/dog.ts
index f772a851a..d5a1ca0dd 100644
--- a/apps/tk/src/commands/dog.ts
+++ b/apps/tk/src/commands/dog.ts
@@ -2,6 +2,8 @@ import type { CommandInteraction } from "discord.js";
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
import JIMP from "jimp";
+import { logger } from "@forge/utils";
+
import { TK_DOG_URL } from "../consts";
// DOG COMMAND
@@ -40,9 +42,9 @@ export async function execute(interaction: CommandInteraction) {
void interaction.reply({ embeds: [embed] });
} catch (err: unknown) {
if (err instanceof Error) {
- console.error(err.message);
+ logger.error(err.message);
} else {
- console.error("An unknown error occurred: ", err);
+ logger.error("An unknown error occurred: ", err);
}
}
}
diff --git a/apps/tk/src/commands/duck.ts b/apps/tk/src/commands/duck.ts
index a0251870c..217c9ded0 100644
--- a/apps/tk/src/commands/duck.ts
+++ b/apps/tk/src/commands/duck.ts
@@ -2,6 +2,8 @@ import type { CommandInteraction } from "discord.js";
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
import JIMP from "jimp";
+import { logger } from "@forge/utils";
+
import { TK_DUCK_URL } from "../consts";
// DUCK COMMAND
@@ -42,9 +44,9 @@ export async function execute(interaction: CommandInteraction) {
void interaction.reply({ embeds: [embed] });
} catch (err: unknown) {
if (err instanceof Error) {
- console.error(err.message);
+ logger.error(err.message);
} else {
- console.error("An unknown error occurred: ", err);
+ logger.error("An unknown error occurred: ", err);
}
}
}
diff --git a/apps/tk/src/commands/fact.ts b/apps/tk/src/commands/fact.ts
index fc6be7d03..1504cc2b6 100644
--- a/apps/tk/src/commands/fact.ts
+++ b/apps/tk/src/commands/fact.ts
@@ -1,6 +1,8 @@
import type { CommandInteraction } from "discord.js";
import { SlashCommandBuilder } from "discord.js";
+import { logger } from "@forge/utils";
+
import { TK_FACTS_URL } from "../consts";
// FACT COMMAND
@@ -39,9 +41,9 @@ export async function execute(interaction: CommandInteraction) {
return interaction.reply(data.text);
} catch (err: unknown) {
if (err instanceof Error) {
- console.log(err.message);
+ logger.log(err.message);
} else {
- console.error("An unknown error occurred: ", err);
+ logger.error("An unknown error occurred: ", err);
}
}
}
diff --git a/apps/tk/src/commands/fox.ts b/apps/tk/src/commands/fox.ts
index fef543cb0..802107def 100644
--- a/apps/tk/src/commands/fox.ts
+++ b/apps/tk/src/commands/fox.ts
@@ -2,6 +2,8 @@ import type { CommandInteraction } from "discord.js";
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
import JIMP from "jimp";
+import { logger } from "@forge/utils";
+
import { TK_FOX_URL } from "../consts";
// FOX COMMAND
@@ -39,9 +41,9 @@ export async function execute(interaction: CommandInteraction) {
void interaction.reply({ embeds: [embed] });
} catch (err: unknown) {
if (err instanceof Error) {
- console.error(err.message);
+ logger.error(err.message);
} else {
- console.error("An unknown error occurred: ", err);
+ logger.error("An unknown error occurred: ", err);
}
}
}
diff --git a/apps/tk/src/commands/goat.ts b/apps/tk/src/commands/goat.ts
index 60402d74c..b7ba6c53c 100644
--- a/apps/tk/src/commands/goat.ts
+++ b/apps/tk/src/commands/goat.ts
@@ -4,12 +4,13 @@ import natural from "natural";
import sharp from "sharp";
import { db } from "@forge/db/client";
-
-const { LevenshteinDistance, Metaphone } = natural;
+import { logger } from "@forge/utils";
// GOAT COMMAND
// random G.O.A.T. image
+const { LevenshteinDistance, Metaphone } = natural;
+
const VALID_ONSETS = new Set([
"b",
"c",
@@ -169,7 +170,7 @@ export const getGoatEmbed = async () => {
if (guildProfileVisible) goat = rest;
}
- console.log(goat);
+ logger.log(goat);
const response = await fetch(goat.profilePictureUrl);
const buffer = await response.arrayBuffer();
@@ -216,7 +217,7 @@ export async function execute(interaction: CommandInteraction) {
const embed = await getGoatEmbed();
void interaction.reply({ embeds: [embed] });
} catch (err: unknown) {
- if (err instanceof Error) console.error(err.message);
- else console.error("An unknown error occurred: ", err);
+ if (err instanceof Error) logger.error(err.message);
+ else logger.error("An unknown error occurred: ", err);
}
}
diff --git a/apps/tk/src/commands/joke.ts b/apps/tk/src/commands/joke.ts
index 57eb8a12a..7b8a865af 100644
--- a/apps/tk/src/commands/joke.ts
+++ b/apps/tk/src/commands/joke.ts
@@ -1,6 +1,8 @@
import type { CommandInteraction } from "discord.js";
import { SlashCommandBuilder } from "discord.js";
+import { logger } from "@forge/utils";
+
import { TK_JOKE_URL } from "../consts";
interface JokeProps {
@@ -30,9 +32,9 @@ export async function execute(interaction: CommandInteraction) {
}
} catch (err: unknown) {
if (err instanceof Error) {
- console.error(err.message);
+ logger.error(err.message);
} else {
- console.error("An unknown error occurred: ", err);
+ logger.error("An unknown error occurred: ", err);
}
}
}
diff --git a/apps/tk/src/commands/weather.ts b/apps/tk/src/commands/weather.ts
index 8d27ff7a4..21490d02b 100644
--- a/apps/tk/src/commands/weather.ts
+++ b/apps/tk/src/commands/weather.ts
@@ -1,6 +1,8 @@
import type { CommandInteraction } from "discord.js";
import { SlashCommandBuilder } from "discord.js";
+import { logger } from "@forge/utils";
+
import type { WeatherMapKeys } from "../consts";
import { WEATHER_MAP } from "../consts";
import { env } from "../env";
@@ -83,9 +85,9 @@ export async function execute(interaction: CommandInteraction) {
return interaction.reply({ embeds: [embed] });
} catch (err: unknown) {
if (err instanceof Error) {
- console.log(err.message);
+ logger.log(err.message);
} else {
- console.error("An unknown error occurred: ", err);
+ logger.error("An unknown error occurred: ", err);
}
}
}
diff --git a/apps/tk/src/deploy-commands.ts b/apps/tk/src/deploy-commands.ts
index a321f81f3..f70bc85e9 100644
--- a/apps/tk/src/deploy-commands.ts
+++ b/apps/tk/src/deploy-commands.ts
@@ -1,5 +1,7 @@
import { REST, Routes } from "discord.js";
+import { logger } from "@forge/utils";
+
import { commands } from "./commands";
import { env } from "./env";
@@ -18,7 +20,7 @@ interface DeployCommandsProps {
export async function deployCommands({ guildId }: DeployCommandsProps) {
try {
// Log that the commands are being refreshed
- console.log("Started refreshing application (/) commands.");
+ logger.log("Started refreshing application (/) commands.");
// Load all of the commands
await rest.put(
@@ -29,9 +31,9 @@ export async function deployCommands({ guildId }: DeployCommandsProps) {
);
// Log that the commands have been successfully reloaded
- console.log("Successfully reloaded application (/) commands.");
+ logger.log("Successfully reloaded application (/) commands.");
} catch (error) {
// Log any errors that occur
- console.error(error);
+ logger.error(error);
}
}
diff --git a/apps/tk/src/index.ts b/apps/tk/src/index.ts
index 3a0c299b8..da95bf6d7 100644
--- a/apps/tk/src/index.ts
+++ b/apps/tk/src/index.ts
@@ -1,5 +1,7 @@
import { Client } from "discord.js";
+import { logger } from "@forge/utils";
+
import { commands } from "./commands";
import { deployCommands } from "./deploy-commands";
import { env } from "./env";
@@ -15,7 +17,7 @@ export const client = new Client({
// Log when T.K is ready
client.once("ready", () => {
- console.log("T.K is ready :)");
+ logger.log("T.K is ready :)");
if (client.guilds.cache.size > 0) {
for (const guild of client.guilds.cache.values()) {
diff --git a/packages/api/eslint.config.js b/packages/api/eslint.config.js
index 98642f0d6..13d70b815 100644
--- a/packages/api/eslint.config.js
+++ b/packages/api/eslint.config.js
@@ -7,9 +7,4 @@ export default [
},
...baseConfig,
...restrictEnvAccess,
- {
- rules: {
- "no-console": "off",
- },
- },
];
diff --git a/packages/api/package.json b/packages/api/package.json
index 36b7c6b20..f72dd3701 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -12,6 +12,7 @@
"types": "./dist/env.d.ts",
"default": "./src/env.ts"
},
+ "./minio/minio-client": "./src/minio/minio-client.ts",
"./utils": {
"types": "./dist/utils.d.ts",
"default": "./src/utils.ts"
@@ -32,6 +33,7 @@
"@forge/consts": "workspace:*",
"@forge/db": "workspace:*",
"@forge/email": "workspace:^",
+ "@forge/utils": "workspace:*",
"@forge/validators": "workspace:*",
"@stripe/stripe-js": "^5.2.0",
"@trpc/server": "catalog:",
diff --git a/packages/api/src/env.ts b/packages/api/src/env.ts
index 067a42c7c..def43c33e 100644
--- a/packages/api/src/env.ts
+++ b/packages/api/src/env.ts
@@ -4,9 +4,7 @@ import { z } from "zod";
export const env = createEnv({
server: {
STRIPE_SECRET_KEY: z.string(),
- DISCORD_BOT_TOKEN: z.string(),
NODE_ENV: z.enum(["development", "production"]).optional(),
- LISTMONK_FROM_EMAIL: z.string(),
STRIPE_SECRET_WEBHOOK_KEY: z.string(),
MINIO_ENDPOINT: z.string(),
MINIO_ACCESS_KEY: z.string(),
diff --git a/packages/api/src/routers/auth.ts b/packages/api/src/routers/auth.ts
index 9b0dc6e34..36201c6ad 100644
--- a/packages/api/src/routers/auth.ts
+++ b/packages/api/src/routers/auth.ts
@@ -1,9 +1,10 @@
import type { TRPCRouterRecord } from "@trpc/server";
import { invalidateSessionToken } from "@forge/auth/server";
+import * as discord from "@forge/utils/discord";
+import * as permissionsServer from "@forge/utils/permissions.server";
import { protectedProcedure, publicProcedure } from "../trpc";
-import { isDiscordAdmin, isDiscordMember, isJudgeAdmin } from "../utils";
export const authRouter = {
getSession: publicProcedure.query(({ ctx }) => {
@@ -26,16 +27,16 @@ export const authRouter = {
return Promise.resolve(false); // consistent return type
}
- return isDiscordAdmin(ctx.session.user);
+ return discord.isDiscordAdmin(ctx.session.user);
}),
getDiscordMemberStatus: publicProcedure.query(({ ctx }): Promise => {
if (!ctx.session) {
return Promise.resolve(false);
}
- return isDiscordMember(ctx.session.user);
+ return discord.isDiscordMember(ctx.session.user);
}),
getJudgeStatus: publicProcedure.query(async () => {
- const isJudge = await isJudgeAdmin();
+ const isJudge = await permissionsServer.isJudgeAdmin();
return isJudge;
}),
diff --git a/packages/api/src/routers/csv-importer.ts b/packages/api/src/routers/csv-importer.ts
index d5a7aca74..2f3a5c27a 100644
--- a/packages/api/src/routers/csv-importer.ts
+++ b/packages/api/src/routers/csv-importer.ts
@@ -5,9 +5,9 @@ import { FORMS } from "@forge/consts";
import { eq, sql } from "@forge/db";
import { db } from "@forge/db/client";
import { Challenges, Submissions, Teams } from "@forge/db/schemas/knight-hacks";
+import { logger, permissions } from "@forge/utils";
import { permProcedure } from "../trpc";
-import { controlPerms } from "../utils";
interface CsvImporterRecord {
"Opt-In Prize": string | null;
@@ -32,7 +32,7 @@ export const csvImporterRouter = {
}),
)
.mutation(async ({ ctx, input }) => {
- controlPerms.or(["IS_OFFICER"], ctx);
+ permissions.controlPerms.or(["IS_OFFICER"], ctx);
try {
// Get raw records
@@ -249,7 +249,7 @@ export const csvImporterRouter = {
([matchKey, teamRows]) => {
const teamId = teamIdMap.get(matchKey);
if (!teamId) {
- console.error(`Team not found for matchKey: ${matchKey}`);
+ logger.error(`Team not found for matchKey: ${matchKey}`);
throw new Error(`Failed to find team ID for: ${matchKey}`);
}
@@ -312,7 +312,7 @@ export const csvImporterRouter = {
return result;
} catch (error) {
- console.error("CSV import error:", error);
+ logger.error("CSV import error:", error);
throw new Error(
error instanceof Error ? error.message : "Failed to import CSV",
diff --git a/packages/api/src/routers/dues-payment.ts b/packages/api/src/routers/dues-payment.ts
index 5d60e4075..0db13fabc 100644
--- a/packages/api/src/routers/dues-payment.ts
+++ b/packages/api/src/routers/dues-payment.ts
@@ -7,10 +7,12 @@ import { CLUB } from "@forge/consts";
import { eq } from "@forge/db";
import { db } from "@forge/db/client";
import { DuesPayment, Member } from "@forge/db/schemas/knight-hacks";
+import { permissions } from "@forge/utils";
+import * as discord from "@forge/utils/discord";
+import { stripe } from "@forge/utils/stripe";
import { env } from "../env";
import { permProcedure, protectedProcedure } from "../trpc";
-import { controlPerms, log, stripe } from "../utils";
export const duesPaymentRouter = {
createCheckout: protectedProcedure.mutation(async ({ ctx }) => {
@@ -80,7 +82,7 @@ export const duesPaymentRouter = {
const stripe = new Stripe(env.STRIPE_SECRET_KEY, { typescript: true });
const session = await stripe.checkout.sessions.retrieve(input);
- await log({
+ await discord.log({
message: `A member has successfully paid their dues. ${session.amount_total}`,
title: "Dues Paid",
color: "success_green",
@@ -96,7 +98,7 @@ export const duesPaymentRouter = {
}),
getDuesPaymentDates: permProcedure.query(async ({ ctx }) => {
- controlPerms.or(["READ_MEMBERS", "READ_CLUB_DATA"], ctx);
+ permissions.controlPerms.or(["READ_MEMBERS", "READ_CLUB_DATA"], ctx);
return await db
.select({ paymentDate: DuesPayment.paymentDate })
diff --git a/packages/api/src/routers/email.ts b/packages/api/src/routers/email.ts
index c5feaf12c..480ded29a 100644
--- a/packages/api/src/routers/email.ts
+++ b/packages/api/src/routers/email.ts
@@ -1,8 +1,10 @@
import type { TRPCRouterRecord } from "@trpc/server";
import { z } from "zod";
+import { sendEmail } from "@forge/email";
+import { logger, permissions } from "@forge/utils";
+
import { permProcedure } from "../trpc";
-import { controlPerms, sendEmail } from "../utils";
export const emailRouter = {
sendEmail: permProcedure
@@ -16,8 +18,8 @@ export const emailRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EMAIL_PORTAL"], ctx);
- console.log(input.data);
+ permissions.controlPerms.or(["EMAIL_PORTAL"], ctx);
+ logger.log(input.data);
try {
const response = await sendEmail({
to: input.to,
@@ -29,7 +31,7 @@ export const emailRouter = {
return response;
} catch (error) {
- console.error("Error sending email:", {
+ logger.error("Error sending email:", {
error: error instanceof Error ? error.message : error,
input,
});
diff --git a/packages/api/src/routers/event-feedback.ts b/packages/api/src/routers/event-feedback.ts
index 71e10a8fc..aaa5ebaa1 100644
--- a/packages/api/src/routers/event-feedback.ts
+++ b/packages/api/src/routers/event-feedback.ts
@@ -2,9 +2,9 @@ import type { TRPCRouterRecord } from "@trpc/server";
import { z } from "zod";
import { DISCORD } from "@forge/consts";
+import * as discord from "@forge/utils/discord";
import { permProcedure } from "../trpc";
-import { log } from "../utils";
export const eventFeedbackRouter = {
logHackathonFeedback: permProcedure
@@ -14,7 +14,7 @@ export const eventFeedbackRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- await log({
+ await discord.log({
message: `<@&${DISCORD.OFFICER_ROLE}> ${input.description}`,
title: "Hackathon Issue",
color: "uhoh_red",
diff --git a/packages/api/src/routers/event.ts b/packages/api/src/routers/event.ts
index 07f58b3ca..839cdb584 100644
--- a/packages/api/src/routers/event.ts
+++ b/packages/api/src/routers/event.ts
@@ -29,9 +29,12 @@ import {
InsertEventSchema,
Member,
} from "@forge/db/schemas/knight-hacks";
+import { logger, permissions } from "@forge/utils";
+import * as discord from "@forge/utils/discord";
+import * as forms from "@forge/utils/forms";
+import * as google from "@forge/utils/google";
import { permProcedure, protectedProcedure, publicProcedure } from "../trpc";
-import { calendar, controlPerms, createForm, discord, log } from "../utils";
export const eventRouter = {
getEvents: publicProcedure.query(async () => {
@@ -126,7 +129,7 @@ export const eventRouter = {
getAttendees: permProcedure
.input(z.string())
.query(async ({ ctx, input }) => {
- controlPerms.or(["READ_CLUB_EVENT"], ctx);
+ permissions.controlPerms.or(["READ_CLUB_EVENT"], ctx);
const attendees = await db
.select({
@@ -142,7 +145,7 @@ export const eventRouter = {
getHackerAttendees: permProcedure
.input(z.string())
.query(async ({ ctx, input }) => {
- controlPerms.or(["READ_HACK_EVENT"], ctx);
+ permissions.controlPerms.or(["READ_HACK_EVENT"], ctx);
const attendees = await db
.select({
@@ -167,7 +170,7 @@ export const eventRouter = {
InsertEventSchema.omit({ id: true, discordId: true, googleId: true }),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_CLUB_EVENT", "EDIT_HACK_EVENT"], ctx);
+ permissions.controlPerms.or(["EDIT_CLUB_EVENT", "EDIT_HACK_EVENT"], ctx);
// Step 0: Convert provided start/end datetimes into Local Date objects
const startDatetime = new Date(input.start_datetime);
@@ -205,7 +208,7 @@ export const eventRouter = {
// Step 1: Create the event in Discord
let discordEventId: string | undefined;
try {
- const response = (await discord.post(
+ const response = (await discord.api.post(
Routes.guildScheduledEvents(DISCORD.KNIGHTHACKS_GUILD),
{
body: {
@@ -223,7 +226,7 @@ export const eventRouter = {
)) as APIExternalGuildScheduledEvent;
discordEventId = response.id;
} catch (error) {
- console.error(JSON.stringify(error, null, 2));
+ logger.error(JSON.stringify(error, null, 2));
throw new TRPCError({
message: "Failed to create event in Discord",
code: "BAD_REQUEST",
@@ -233,7 +236,7 @@ export const eventRouter = {
// Step 2: Insert the event into the Google Calendar
let googleEventId: string | undefined;
try {
- const response = await calendar.events.insert({
+ const response = await google.calendar.events.insert({
calendarId: EVENTS.GOOGLE_CALENDAR_ID,
requestBody: {
end: {
@@ -251,19 +254,19 @@ export const eventRouter = {
} as calendar_v3.Params$Resource$Events$Insert);
googleEventId = response.data.id ?? undefined;
} catch (error) {
- console.error("ERROR MESSAGE:", JSON.stringify(error, null, 2));
+ logger.error("ERROR MESSAGE:", JSON.stringify(error, null, 2));
// Clean up the event in Discord if the Google Calendar event fails
if (discordEventId) {
try {
- await discord.delete(
+ await discord.api.delete(
Routes.guildScheduledEvent(
DISCORD.KNIGHTHACKS_GUILD,
discordEventId,
),
);
} catch (cleanupErr) {
- console.error(JSON.stringify(cleanupErr, null, 2));
+ logger.error(JSON.stringify(cleanupErr, null, 2));
}
}
@@ -303,28 +306,28 @@ export const eventRouter = {
googleId: googleEventId,
});
} catch (error) {
- console.error(JSON.stringify(error, null, 2));
+ logger.error(JSON.stringify(error, null, 2));
// Clean up the event in Discord if the database insert fails
try {
- await discord.delete(
+ await discord.api.delete(
Routes.guildScheduledEvent(
DISCORD.KNIGHTHACKS_GUILD,
discordEventId,
),
);
} catch (cleanupErr) {
- console.error(JSON.stringify(cleanupErr, null, 2));
+ logger.error(JSON.stringify(cleanupErr, null, 2));
}
// Clean up the event in Google Calendar if the database insert fails
try {
- await calendar.events.delete({
+ await google.calendar.events.delete({
calendarId: EVENTS.GOOGLE_CALENDAR_ID,
eventId: googleEventId,
});
} catch (cleanupErr) {
- console.error(JSON.stringify(cleanupErr, null, 2));
+ logger.error(JSON.stringify(cleanupErr, null, 2));
}
throw new TRPCError({
@@ -334,7 +337,7 @@ export const eventRouter = {
}
// Step 4: Log the creation
- await log({
+ await discord.log({
title: "Event Created",
message: `The event **${formattedName}** was created.`,
color: "blade_purple",
@@ -345,7 +348,7 @@ export const eventRouter = {
updateEvent: permProcedure
.input(InsertEventSchema)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_CLUB_EVENT", "EDIT_HACK_EVENT"], ctx);
+ permissions.controlPerms.or(["EDIT_CLUB_EVENT", "EDIT_HACK_EVENT"], ctx);
if (!input.id) {
throw new TRPCError({
@@ -399,7 +402,7 @@ export const eventRouter = {
// Step 1: Update the event in Discord
try {
- await discord.patch(
+ await discord.api.patch(
Routes.guildScheduledEvent(
DISCORD.KNIGHTHACKS_GUILD,
input.discordId,
@@ -419,7 +422,7 @@ export const eventRouter = {
},
);
} catch (error) {
- console.error(JSON.stringify(error, null, 2));
+ logger.error(JSON.stringify(error, null, 2));
throw new TRPCError({
message: "Failed to update event in Discord",
code: "BAD_REQUEST",
@@ -428,7 +431,7 @@ export const eventRouter = {
// Step 2: Update the event in Google Calendar
try {
- await calendar.events.update({
+ await google.calendar.events.update({
calendarId: EVENTS.GOOGLE_CALENDAR_ID,
eventId: input.googleId,
requestBody: {
@@ -446,7 +449,7 @@ export const eventRouter = {
},
} as calendar_v3.Params$Resource$Events$Update);
} catch (error) {
- console.error(JSON.stringify(error, null, 2));
+ logger.error(JSON.stringify(error, null, 2));
throw new TRPCError({
message: "Failed to update event in Google Calendar",
code: "BAD_REQUEST",
@@ -518,7 +521,7 @@ export const eventRouter = {
const oldFormattedName = `[${event.tag.toUpperCase().replace(" ", "-")}] ${event.name}`;
- await log({
+ await discord.log({
title: "Event Updated",
message: `Event **${oldFormattedName}** was updated.\n**Changes:**\n${changesString}`,
color: "blade_purple",
@@ -552,7 +555,7 @@ export const eventRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_CLUB_EVENT", "EDIT_HACK_EVENT"], ctx);
+ permissions.controlPerms.or(["EDIT_CLUB_EVENT", "EDIT_HACK_EVENT"], ctx);
if (!input.id) {
throw new TRPCError({
@@ -563,14 +566,14 @@ export const eventRouter = {
// Step 1: Delete the event in Discord
try {
- await discord.delete(
+ await discord.api.delete(
Routes.guildScheduledEvent(
DISCORD.KNIGHTHACKS_GUILD,
input.discordId,
),
);
} catch (error) {
- console.error(JSON.stringify(error, null, 2));
+ logger.error(JSON.stringify(error, null, 2));
throw new TRPCError({
message: "Failed to delete event in Discord",
code: "BAD_REQUEST",
@@ -579,12 +582,12 @@ export const eventRouter = {
// Step 2: Delete the event in the Google Calendar
try {
- await calendar.events.delete({
+ await google.calendar.events.delete({
calendarId: EVENTS.GOOGLE_CALENDAR_ID,
eventId: input.googleId,
} as calendar_v3.Params$Resource$Events$Delete);
} catch (error) {
- console.error(JSON.stringify(error, null, 2));
+ logger.error(JSON.stringify(error, null, 2));
throw new TRPCError({
message: "Failed to delete event in Google Calendar",
code: "BAD_REQUEST",
@@ -592,7 +595,7 @@ export const eventRouter = {
}
const formattedName = `[${input.tag.toUpperCase().replace(" ", "-")}] ${input.name}`;
- await log({
+ await discord.log({
title: "Event Deleted",
message: `The event **${formattedName}** was deleted.`,
color: "uhoh_red",
@@ -630,7 +633,7 @@ export const eventRouter = {
if (form) return form;
try {
- return await createForm({
+ return await forms.createForm({
formData: {
name: formName,
description: `Provide feedback for ${event.name} to help us make events better in the future!`,
@@ -699,7 +702,7 @@ export const eventRouter = {
}),
)
.query(async ({ ctx, input }) => {
- controlPerms.or(["READ_CLUB_EVENT"], ctx);
+ permissions.controlPerms.or(["READ_CLUB_EVENT"], ctx);
const conditions = [];
diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts
index 4298d3f3f..b19f2fcf9 100644
--- a/packages/api/src/routers/forms.ts
+++ b/packages/api/src/routers/forms.ts
@@ -20,24 +20,19 @@ import {
TrpcFormConnection,
TrpcFormConnectionSchema,
} from "@forge/db/schemas/knight-hacks";
+import { logger, permissions } from "@forge/utils";
+import * as discord from "@forge/utils/discord";
+import * as forms from "@forge/utils/forms";
import { minioClient } from "../minio/minio-client";
import { permProcedure, protectedProcedure } from "../trpc";
-import {
- controlPerms,
- createForm,
- CreateFormSchema,
- generateJsonSchema,
- log,
- regenerateMediaUrls,
-} from "../utils";
export const formsRouter = {
createForm: permProcedure
- .input(CreateFormSchema)
+ .input(forms.CreateFormSchema)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_FORMS"], ctx);
- await createForm(input);
+ permissions.controlPerms.or(["EDIT_FORMS"], ctx);
+ await forms.createForm(input);
}),
updateForm: permProcedure
@@ -53,8 +48,8 @@ export const formsRouter = {
.extend({ responseRoleIds: z.array(z.string().uuid()).optional() }),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_FORMS"], ctx);
- const jsonSchema = generateJsonSchema(input.formData);
+ permissions.controlPerms.or(["EDIT_FORMS"], ctx);
+ const jsonSchema = forms.generateJsonSchema(input.formData);
const slug_name = input.formData.name.toLowerCase().replaceAll(" ", "-");
@@ -154,8 +149,9 @@ export const formsRouter = {
.where(eq(FormResponseRoles.formId, form.id));
// Regenerate presigned URLs for any media that has objectNames
- const instructionsWithFreshUrls = await regenerateMediaUrls(
+ const instructionsWithFreshUrls = await forms.regenerateMediaUrls(
formData.instructions,
+ minioClient,
);
return {
@@ -201,7 +197,7 @@ export const formsRouter = {
deleteForm: permProcedure
.input(z.object({ slug_name: z.string() }))
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["EDIT_FORMS"], ctx);
// find the form to delete duh
const form = await db.query.FormsSchemas.findFirst({
where: (t, { eq }) =>
@@ -234,7 +230,7 @@ export const formsRouter = {
}),
)
.query(async ({ input, ctx }) => {
- controlPerms.or(["READ_FORMS", "EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["READ_FORMS", "EDIT_FORMS"], ctx);
const { cursor, section } = input;
const limit = input.limit;
@@ -285,7 +281,7 @@ export const formsRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["EDIT_FORMS"], ctx);
const form = await db.query.FormsSchemas.findFirst({
where: (t, { eq }) => eq(t.id, input.form),
@@ -327,7 +323,7 @@ export const formsRouter = {
deleteConnection: permProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["EDIT_FORMS"], ctx);
try {
await db
.delete(TrpcFormConnection)
@@ -406,7 +402,7 @@ export const formsRouter = {
}
const formData = form.formData as FORMS.FormType;
- const jsonSchema = generateJsonSchema(formData);
+ const jsonSchema = forms.generateJsonSchema(formData);
if (!jsonSchema.success) {
throw new TRPCError({
@@ -448,7 +444,7 @@ export const formsRouter = {
...input,
});
- await log({
+ await discord.log({
title: `Form submitted to blade forms`,
message: `**Form submitted:** ${form.name}\n**User:** ${ctx.session.user.name}`,
color: "success_green",
@@ -499,7 +495,7 @@ export const formsRouter = {
// Validate responseData against form schema
const formData = form.formData as FORMS.FormType;
- const jsonSchema = generateJsonSchema(formData);
+ const jsonSchema = forms.generateJsonSchema(formData);
if (!jsonSchema.success) {
throw new TRPCError({
@@ -538,7 +534,7 @@ export const formsRouter = {
getResponses: permProcedure
.input(z.object({ form: z.string() }))
.query(async ({ input, ctx }) => {
- controlPerms.or(["READ_FORMS", "EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["READ_FORMS", "EDIT_FORMS"], ctx);
return await db
.select({
id: FormResponse.id,
@@ -560,10 +556,10 @@ export const formsRouter = {
deleteResponse: permProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["EDIT_FORMS"], ctx);
try {
await db.delete(FormResponse).where(eq(FormResponse.id, input.id));
- await log({
+ await discord.log({
title: `Form response deleted`,
message: `**Response deleted:** ${input.id}`,
color: "uhoh_red",
@@ -711,7 +707,7 @@ export const formsRouter = {
return { uploadUrl, objectName, viewUrl };
} catch (e) {
- console.error("getUploadUrl error:", e);
+ logger.error("getUploadUrl error:", e);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to generate upload URL",
@@ -726,7 +722,7 @@ export const formsRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["EDIT_FORMS"], ctx);
const { objectName } = input;
try {
@@ -736,7 +732,7 @@ export const formsRouter = {
);
return { success: true };
} catch (e) {
- console.error("deleteMedia error:", e);
+ logger.error("deleteMedia error:", e);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to delete media",
@@ -751,7 +747,7 @@ export const formsRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["READ_FORMS", "EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["READ_FORMS", "EDIT_FORMS"], ctx);
const { objectName } = input;
try {
@@ -762,7 +758,7 @@ export const formsRouter = {
);
return { viewUrl };
} catch (e) {
- console.error("getFileUrl error:", e);
+ logger.error("getFileUrl error:", e);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to generate file URL",
@@ -771,7 +767,7 @@ export const formsRouter = {
}),
getSections: permProcedure.query(async ({ ctx }) => {
- controlPerms.or(["READ_FORMS", "EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["READ_FORMS", "EDIT_FORMS"], ctx);
const isOfficer = ctx.session.permissions.IS_OFFICER;
@@ -890,7 +886,7 @@ export const formsRouter = {
}),
getSectionCounts: permProcedure.query(async ({ ctx }) => {
- controlPerms.or(["READ_FORMS", "EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["READ_FORMS", "EDIT_FORMS"], ctx);
const counts = await db
.select({
section: FormsSchemas.section,
@@ -913,7 +909,7 @@ export const formsRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["EDIT_FORMS"], ctx);
const form = await db.query.FormsSchemas.findFirst({
where: (t, { eq }) =>
eq(t.slugName, decodeURIComponent(input.slug_name)),
@@ -941,7 +937,7 @@ export const formsRouter = {
.set({ section: input.section, sectionId })
.where(eq(FormsSchemas.id, form.id));
- await log({
+ await discord.log({
title: `Form section updated`,
message: `**Form:** ${form.name}\n**Section:** ${oldSection} -> ${input.section}`,
color: "success_green",
@@ -957,7 +953,7 @@ export const formsRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["EDIT_FORMS"], ctx);
await db
.update(FormSections)
@@ -969,7 +965,7 @@ export const formsRouter = {
.set({ section: input.newName })
.where(eq(FormsSchemas.section, input.oldName));
- await log({
+ await discord.log({
title: `Form section renamed`,
message: `**Form section:** ${input.oldName} -> ${input.newName}`,
color: "success_green",
@@ -980,7 +976,7 @@ export const formsRouter = {
deleteSection: permProcedure
.input(z.object({ section: z.string() }))
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["EDIT_FORMS"], ctx);
await db
.update(FormsSchemas)
.set({ section: "General", sectionId: null })
@@ -999,7 +995,7 @@ export const formsRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["EDIT_FORMS"], ctx);
const existing = await db.query.FormSections.findFirst({
where: (t, { eq }) => eq(t.name, input.name),
@@ -1074,7 +1070,7 @@ export const formsRouter = {
.from(Roles)
.where(inArray(Roles.id, input.roleIds));
- await log({
+ await discord.log({
title: `Form section created`,
message: `**Form section:** ${input.name}. Roles: ${roleNames.map((r) => r.name).join(", ")}`,
color: "success_green",
@@ -1085,7 +1081,7 @@ export const formsRouter = {
getSectionRoles: permProcedure
.input(z.object({ sectionName: z.string() }))
.query(async ({ input, ctx }) => {
- controlPerms.or(["READ_FORMS", "EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["READ_FORMS", "EDIT_FORMS"], ctx);
const section = await db.query.FormSections.findFirst({
where: (t, { eq }) => eq(t.name, input.sectionName),
@@ -1124,7 +1120,7 @@ export const formsRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["EDIT_FORMS"], ctx);
const section = await db.query.FormSections.findFirst({
where: (t, { eq }) => eq(t.name, input.sectionName),
@@ -1180,7 +1176,7 @@ export const formsRouter = {
.from(Roles)
.where(inArray(Roles.id, input.roleIds));
- await log({
+ await discord.log({
title: `Form section roles updated`,
message: `**Form section:** ${input.sectionName}. Roles: ${roleNames.length > 0 ? roleNames.map((r) => r.name).join(", ") : "None (all users)"}`,
color: "success_green",
@@ -1196,7 +1192,7 @@ export const formsRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["EDIT_FORMS"], ctx);
const allSections = await db
.select({
@@ -1238,7 +1234,7 @@ export const formsRouter = {
.set({ order: currentSection?.order ?? currentIndex })
.where(eq(FormSections.id, targetSection?.id ?? ""));
- await log({
+ await discord.log({
title: `Form section reordered`,
message: `**Form section:** ${input.sectionName} moved ${input.direction}`,
color: "success_green",
@@ -1249,7 +1245,7 @@ export const formsRouter = {
checkFormEditAccess: permProcedure
.input(z.object({ slug_name: z.string() }))
.query(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["EDIT_FORMS"], ctx);
const isOfficer = ctx.session.permissions.IS_OFFICER;
@@ -1336,7 +1332,7 @@ export const formsRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_FORMS"], ctx);
+ permissions.controlPerms.or(["EDIT_FORMS"], ctx);
// Get the form
const form = await db.query.FormsSchemas.findFirst({
@@ -1363,7 +1359,7 @@ export const formsRouter = {
throw new TRPCError({ message: "Form not found", code: "NOT_FOUND" });
}
- await log({
+ await discord.log({
title: `Form ${updatedForm.isClosed ? "closed" : "opened"}`,
message: `**Form:** ${updatedForm.name}`,
color: updatedForm.isClosed ? "uhoh_red" : "success_green",
diff --git a/packages/api/src/routers/guild.ts b/packages/api/src/routers/guild.ts
index d41a10d30..de43eb782 100644
--- a/packages/api/src/routers/guild.ts
+++ b/packages/api/src/routers/guild.ts
@@ -8,6 +8,7 @@ import { MINIO } from "@forge/consts";
import { and, count, sql } from "@forge/db";
import { db } from "@forge/db/client";
import { Member } from "@forge/db/schemas/knight-hacks";
+import { logger } from "@forge/utils";
import { env } from "../env";
import { minioClient } from "../minio/minio-client";
@@ -42,7 +43,7 @@ export const guildRouter = {
);
if (!base64Data) {
- console.error("uploadProfilePicture: Base64 data is missing.");
+ logger.error("uploadProfilePicture: Base64 data is missing.");
throw new TRPCError({
code: "BAD_REQUEST",
message: "Base64 data is missing or invalid after stripping prefix.",
@@ -65,7 +66,7 @@ export const guildRouter = {
);
}
} catch (e) {
- console.error(
+ logger.error(
"uploadProfilePicture: Error checking/creating bucket:",
e,
);
@@ -88,7 +89,7 @@ export const guildRouter = {
}
}
} catch (e) {
- console.warn(
+ logger.warn(
"uploadProfilePicture: Error listing existing profile pictures, proceeding with upload:",
e,
);
@@ -101,7 +102,7 @@ export const guildRouter = {
existingObjects,
);
} catch (e) {
- console.error(
+ logger.error(
"uploadProfilePicture: Error removing existing profile pictures:",
e,
);
@@ -121,7 +122,7 @@ export const guildRouter = {
{ "Content-Type": contentType },
);
} catch (e) {
- console.error(
+ logger.error(
"uploadProfilePicture: Error uploading profile picture to Minio:",
e,
);
@@ -247,7 +248,7 @@ export const guildRouter = {
"response-content-disposition": `attachment; filename="${downloadName}"`,
},
);
- console.log("Resumé URL generated:", url);
+ logger.log("Resumé URL generated:", url);
return { url };
} catch {
throw new TRPCError({
diff --git a/packages/api/src/routers/hackers/mutations.ts b/packages/api/src/routers/hackers/mutations.ts
index fdab3085d..38b54597c 100644
--- a/packages/api/src/routers/hackers/mutations.ts
+++ b/packages/api/src/routers/hackers/mutations.ts
@@ -16,16 +16,11 @@ import {
HackerEventAttendee,
InsertHackerSchema,
} from "@forge/db/schemas/knight-hacks";
+import { logger, permissions } from "@forge/utils";
+import * as discord from "@forge/utils/discord";
import { minioClient } from "../../minio/minio-client";
import { permProcedure, protectedProcedure } from "../../trpc";
-import {
- addRoleToMember,
- controlPerms,
- isDiscordVIP,
- log,
- resolveDiscordUserId,
-} from "../../utils";
export const hackerMutationRouter = {
createHacker: protectedProcedure
@@ -99,7 +94,7 @@ export const hackerMutationRouter = {
);
}
} catch (error) {
- console.error("Error with generating QR code: ", error);
+ logger.error("Error with generating QR code: ", error);
}
const today = new Date();
@@ -131,7 +126,7 @@ export const hackerMutationRouter = {
status: "pending",
});
- await log({
+ await discord.log({
title: `Hacker Created for ${hackathon.displayName}`,
message: `${hackerData.firstName} ${hackerData.lastName} has signed up for the upcoming hackathon: ${hackathon.name.toUpperCase()}!`,
color: "tk_blue",
@@ -230,7 +225,7 @@ export const hackerMutationRouter = {
.join("\n");
// Log the changes
- await log({
+ await discord.log({
title: "Hacker Updated",
message: `Blade profile for ${hacker.firstName} ${hacker.lastName} has been updated.
\n**Changes:**\n${changesString}`,
@@ -250,7 +245,7 @@ export const hackerMutationRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_HACKERS"], ctx);
+ permissions.controlPerms.or(["EDIT_HACKERS"], ctx);
if (!input.id) {
throw new TRPCError({
@@ -261,7 +256,7 @@ export const hackerMutationRouter = {
await db.delete(Hacker).where(eq(Hacker.id, input.id));
- await log({
+ await discord.log({
title: `Hacker Deleted for ${input.hackathonName}`,
message: `Profile for ${input.firstName} ${input.lastName} has been deleted.`,
color: "uhoh_red",
@@ -354,7 +349,7 @@ export const hackerMutationRouter = {
),
);
- await log({
+ await discord.log({
title: "Hacker Confirmed",
message: `${hacker.firstName} ${hacker.lastName} has confirmed their attendance!`,
color: "success_green",
@@ -463,7 +458,7 @@ export const hackerMutationRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["CHECKIN_HACK_EVENT", "EDIT_HACKERS"], ctx);
+ permissions.controlPerms.or(["CHECKIN_HACK_EVENT", "EDIT_HACKERS"], ctx);
const event = await db.query.Event.findFirst({
where: eq(Event.id, input.eventId),
@@ -525,8 +520,8 @@ export const hackerMutationRouter = {
const eventTag = event.tag;
let discordId: string | null = null;
- discordId = await resolveDiscordUserId(hacker.discordUser);
- const isVIP = discordId ? await isDiscordVIP(discordId) : false;
+ discordId = await discord.resolveDiscordUserId(hacker.discordUser);
+ const isVIP = discordId ? await discord.isDiscordVIP(discordId) : false;
let assignedClass: HackerClass | null = hackerAttendee.class ?? null;
@@ -594,7 +589,7 @@ export const hackerMutationRouter = {
});
if (!discordId) {
- await log({
+ await discord.log({
title: "Discord role assign skipped",
message: `Could not resolve Discord ID for "${hacker.discordUser}".`,
color: "uhoh_red",
@@ -602,16 +597,16 @@ export const hackerMutationRouter = {
});
} else {
try {
- await addRoleToMember(
+ await discord.addRoleToMember(
discordId,
HACKATHONS.KNIGHT_HACKS_8.KH_EVENT_ROLE_ID,
);
- console.log(
+ logger.log(
`Assigned role ${HACKATHONS.KNIGHT_HACKS_8.KH_EVENT_ROLE_ID} to user ${discordId}`,
);
// VIP will already be given the discord role ahead of time, so no need to assign again
if (assignedClass) {
- await addRoleToMember(
+ await discord.addRoleToMember(
discordId,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
HACKATHONS.KNIGHT_HACKS_8.CLASS_ROLE_ID[
@@ -620,13 +615,13 @@ export const hackerMutationRouter = {
);
}
} catch (e) {
- await log({
+ await discord.log({
title: "Discord role assign failed",
message: `Failed to assign Discord roles for "${hacker.discordUser}".`,
color: "uhoh_red",
userId: ctx.session.user.discordUserId,
});
- console.error(
+ logger.error(
"Failed to assign Discord roles:",
(e as Error).message,
);
@@ -679,7 +674,7 @@ export const hackerMutationRouter = {
.where(eq(HackerAttendee.id, hackerAttendee.id));
if (eventTag === "Check-in") {
- await log({
+ await discord.log({
title: `Hacker Checked-In`,
message: `${hacker.firstName} ${hacker.lastName} has been checked in to Hackathon ${
assignedClass ? ` (Class: ${assignedClass}).` : ""
@@ -698,7 +693,7 @@ export const hackerMutationRouter = {
eventName: eventTag,
};
}
- await log({
+ await discord.log({
title: "Hacker Checked-In",
message: `Hacker ${hacker.firstName} ${hacker.lastName} has been checked in to event ${eventTag}.`,
color: "success_green",
@@ -722,7 +717,7 @@ export const hackerMutationRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_HACKERS"], ctx);
+ permissions.controlPerms.or(["EDIT_HACKERS"], ctx);
if (!input.id) {
throw new TRPCError({
@@ -776,7 +771,7 @@ export const hackerMutationRouter = {
),
);
- await log({
+ await discord.log({
title: `Gave Points`,
message: `Gave ${input.amount} points to ${hacker.firstName} ${hacker.lastName} for ${hackathon.displayName}`,
color: "tk_blue",
@@ -800,7 +795,7 @@ export const hackerMutationRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_HACKERS"], ctx);
+ permissions.controlPerms.or(["EDIT_HACKERS"], ctx);
if (!input.id) {
throw new TRPCError({
@@ -843,7 +838,7 @@ export const hackerMutationRouter = {
),
);
- await log({
+ await discord.log({
title: `Hacker Status Updated ${hackathon.displayName ? `for ${hackathon.displayName}` : ""}`,
message: `Hacker status for ${hacker.firstName} ${hacker.lastName} has changed to ${input.status}!`,
color: "tk_blue",
diff --git a/packages/api/src/routers/hackers/pagination.ts b/packages/api/src/routers/hackers/pagination.ts
index 2d215587a..5eb406dd3 100644
--- a/packages/api/src/routers/hackers/pagination.ts
+++ b/packages/api/src/routers/hackers/pagination.ts
@@ -3,9 +3,9 @@ import { z } from "zod";
import { and, asc, count, desc, eq, ilike, ne, or, sql } from "@forge/db";
import { db } from "@forge/db/client";
import { Hacker, HackerAttendee } from "@forge/db/schemas/knight-hacks";
+import { permissions } from "@forge/utils";
import { permProcedure } from "../../trpc";
-import { controlPerms } from "../../utils";
const SOFT_BLACKLIST_HACKER_ID = "7f89fe4d-26f0-42fe-ac98-22d8f648d7a7";
@@ -47,7 +47,7 @@ export const hackerPaginationRouter = {
}),
)
.query(async ({ ctx, input }) => {
- controlPerms.or(["READ_HACKERS", "CHECKIN_HACK_EVENT"], ctx);
+ permissions.controlPerms.or(["READ_HACKERS", "CHECKIN_HACK_EVENT"], ctx);
const currentPage = input.currentPage ?? 1;
const pageSize = input.pageSize ?? 10;
@@ -216,7 +216,7 @@ export const hackerPaginationRouter = {
}),
)
.query(async ({ ctx, input }) => {
- controlPerms.or(["READ_HACKERS", "CHECKIN_HACK_EVENT"], ctx);
+ permissions.controlPerms.or(["READ_HACKERS", "CHECKIN_HACK_EVENT"], ctx);
const conditions = [eq(HackerAttendee.hackathonId, input.hackathonId)];
@@ -309,7 +309,7 @@ export const hackerPaginationRouter = {
}),
)
.query(async ({ ctx, input }) => {
- controlPerms.or(["READ_HACKERS", "CHECKIN_HACK_EVENT"], ctx);
+ permissions.controlPerms.or(["READ_HACKERS", "CHECKIN_HACK_EVENT"], ctx);
const gradYearExpr = sql`EXTRACT(YEAR FROM ${Hacker.gradDate})::int`;
const isFirstTimeExpr = sql`COALESCE(${Hacker.isFirstTime}, false)`;
const whereClause = eq(HackerAttendee.hackathonId, input.hackathonId);
diff --git a/packages/api/src/routers/hackers/queries.ts b/packages/api/src/routers/hackers/queries.ts
index 4eac4b432..13bdc164f 100644
--- a/packages/api/src/routers/hackers/queries.ts
+++ b/packages/api/src/routers/hackers/queries.ts
@@ -9,9 +9,9 @@ import {
HACKER_CLASSES,
HackerAttendee,
} from "@forge/db/schemas/knight-hacks";
+import { permissions } from "@forge/utils";
import { permProcedure, protectedProcedure } from "../../trpc";
-import { controlPerms } from "../../utils";
export const hackerQueryRouter = {
getHacker: protectedProcedure
@@ -108,7 +108,7 @@ export const hackerQueryRouter = {
getHackers: permProcedure.input(z.string()).query(async ({ ctx, input }) => {
// CHECKIN_HACK_EVENT is here because people trying to check-in
// need to retrieve the member list for manual entry
- controlPerms.or(["READ_HACKERS", "CHECKIN_HACK_EVENT"], ctx);
+ permissions.controlPerms.or(["READ_HACKERS", "CHECKIN_HACK_EVENT"], ctx);
const hackers = await db
.select({
@@ -157,7 +157,7 @@ export const hackerQueryRouter = {
getAllHackers: permProcedure
.input(z.object({ hackathonName: z.string().optional() }))
.query(async ({ ctx, input }) => {
- controlPerms.or(["READ_HACKERS", "CHECKIN_HACK_EVENT"], ctx);
+ permissions.controlPerms.or(["READ_HACKERS", "CHECKIN_HACK_EVENT"], ctx);
let hackathon;
@@ -420,7 +420,7 @@ export const hackerQueryRouter = {
statusCountByHackathonId: permProcedure
.input(z.string())
.query(async ({ ctx, input: hackathonId }) => {
- controlPerms.or(["READ_HACK_DATA"], ctx);
+ permissions.controlPerms.or(["READ_HACK_DATA"], ctx);
const results = await Promise.all(
FORMS.HACKATHON_APPLICATION_STATES.map(async (s) => {
diff --git a/packages/api/src/routers/judge.ts b/packages/api/src/routers/judge.ts
index 9119e8d10..163e010bc 100644
--- a/packages/api/src/routers/judge.ts
+++ b/packages/api/src/routers/judge.ts
@@ -15,10 +15,10 @@ import {
Submissions,
Teams,
} from "@forge/db/schemas/knight-hacks";
+import { permissions } from "@forge/utils";
import { env } from "../env";
import { judgeProcedure, permProcedure, publicProcedure } from "../trpc";
-import { controlPerms } from "../utils";
const SESSION_TTL_HOURS = 8;
@@ -556,7 +556,7 @@ export const judgeRouter = {
// Admin: Get all unique rooms with session counts
getRoomsWithSessionCounts: permProcedure.query(async ({ ctx }) => {
- controlPerms.or(["IS_OFFICER"], ctx);
+ permissions.controlPerms.or(["IS_OFFICER"], ctx);
const now = new Date();
const rooms = await db
@@ -576,7 +576,7 @@ export const judgeRouter = {
deleteSessionsByRoom: permProcedure
.input(z.object({ roomName: z.string() }))
.mutation(async ({ ctx, input }) => {
- controlPerms.or(["IS_OFFICER"], ctx);
+ permissions.controlPerms.or(["IS_OFFICER"], ctx);
const result = await db
.delete(JudgeSession)
diff --git a/packages/api/src/routers/member.ts b/packages/api/src/routers/member.ts
index eacd2344a..05bbb58b3 100644
--- a/packages/api/src/routers/member.ts
+++ b/packages/api/src/routers/member.ts
@@ -28,10 +28,11 @@ import {
Member,
OtherCompanies,
} from "@forge/db/schemas/knight-hacks";
+import { logger, permissions } from "@forge/utils";
+import * as discord from "@forge/utils/discord";
import { minioClient } from "../minio/minio-client";
import { permProcedure, protectedProcedure } from "../trpc";
-import { controlPerms, log } from "../utils";
export const memberRouter = {
createMember: protectedProcedure
@@ -76,7 +77,7 @@ export const memberRouter = {
);
}
} catch (error) {
- console.error("Error with generating QR code: ", error);
+ logger.error("Error with generating QR code: ", error);
}
const today = new Date();
@@ -100,7 +101,7 @@ export const memberRouter = {
name: company,
});
} catch (error) {
- console.log("Unable to insert company: ", error);
+ logger.log("Unable to insert company: ", error);
}
}
@@ -112,7 +113,7 @@ export const memberRouter = {
phoneNumber: input.phoneNumber === "" ? null : input.phoneNumber,
});
- await log({
+ await discord.log({
title: "Member Created",
message: `${input.firstName} ${input.lastName} has signed up for Blade`,
color: "tk_blue",
@@ -194,7 +195,7 @@ export const memberRouter = {
name: company,
});
} catch (error) {
- console.log("Unable to insert company: ", error);
+ logger.log("Unable to insert company: ", error);
}
}
@@ -258,7 +259,7 @@ export const memberRouter = {
.join("\n");
// Log the changes
- await log({
+ await discord.log({
title: "Member Updated",
message: `Blade profile for ${member.firstName} ${member.lastName} has been updated.
\n**Changes:**\n${changesString}`,
@@ -281,7 +282,7 @@ export const memberRouter = {
});
}
await db.delete(Member).where(eq(Member.id, input.id));
- await log({
+ await discord.log({
title: "Member Deleted",
message: `Profile for ${memberToDelete.firstName} ${memberToDelete.lastName} (ID: ${input.id}) has been deleted.`,
color: "uhoh_red",
@@ -357,7 +358,7 @@ export const memberRouter = {
.optional(),
)
.query(async ({ input, ctx }) => {
- controlPerms.or(["READ_MEMBERS", "READ_CLUB_DATA"], ctx);
+ permissions.controlPerms.or(["READ_MEMBERS", "READ_CLUB_DATA"], ctx);
// If theres a fetch all flag set to true OR no inputs are passed in get all members
if (
@@ -454,7 +455,7 @@ export const memberRouter = {
.optional(),
)
.query(async ({ input, ctx }) => {
- controlPerms.or(["READ_MEMBERS", "READ_CLUB_DATA"], ctx);
+ permissions.controlPerms.or(["READ_MEMBERS", "READ_CLUB_DATA"], ctx);
const conditions = [];
@@ -500,7 +501,7 @@ export const memberRouter = {
}),
getDistinctSchools: permProcedure.query(async ({ ctx }) => {
- controlPerms.or(["READ_MEMBERS", "READ_CLUB_DATA"], ctx);
+ permissions.controlPerms.or(["READ_MEMBERS", "READ_CLUB_DATA"], ctx);
const results = await db
.selectDistinct({ school: Member.school })
.from(Member)
@@ -511,7 +512,7 @@ export const memberRouter = {
}),
getDistinctMajors: permProcedure.query(async ({ ctx }) => {
- controlPerms.or(["READ_MEMBERS", "READ_CLUB_DATA"], ctx);
+ permissions.controlPerms.or(["READ_MEMBERS", "READ_CLUB_DATA"], ctx);
const results = await db
.selectDistinct({ major: Member.major })
.from(Member)
@@ -521,7 +522,7 @@ export const memberRouter = {
}),
getMemberFilterOptions: permProcedure.query(async ({ ctx }) => {
- controlPerms.or(["READ_MEMBERS", "READ_CLUB_DATA"], ctx);
+ permissions.controlPerms.or(["READ_MEMBERS", "READ_CLUB_DATA"], ctx);
const rows = await db
.select({
@@ -556,7 +557,7 @@ export const memberRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_MEMBERS"], ctx);
+ permissions.controlPerms.or(["EDIT_MEMBERS"], ctx);
const member = await db.query.Member.findFirst({
where: eq(Member.id, input.id),
@@ -574,7 +575,7 @@ export const memberRouter = {
.set({ points: sql`${Member.points} + ${input.amount}` })
.where(eq(Member.id, member.id));
- await log({
+ await discord.log({
title: `Gave Points`,
message: `Gave ${input.amount} points to ${member.firstName} ${member.lastName} (Member)`,
color: "tk_blue",
@@ -583,7 +584,7 @@ export const memberRouter = {
}),
getDuesPayingMembers: permProcedure.query(async ({ ctx }) => {
- controlPerms.or(["READ_MEMBERS", "READ_CLUB_DATA"], ctx);
+ permissions.controlPerms.or(["READ_MEMBERS", "READ_CLUB_DATA"], ctx);
return await db
.select()
@@ -599,7 +600,7 @@ export const memberRouter = {
}),
getMemberAttendanceCounts: permProcedure.query(async ({ ctx }) => {
- controlPerms.or(["READ_MEMBERS", "READ_CLUB_DATA"], ctx);
+ permissions.controlPerms.or(["READ_MEMBERS", "READ_CLUB_DATA"], ctx);
// Get attendance count for each member
const memberAttendance = await db
@@ -627,7 +628,7 @@ export const memberRouter = {
createDuesPayingMember: permProcedure
.input(InsertMemberSchema.pick({ id: true }))
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_MEMBERS", "IS_OFFICER"], ctx);
+ permissions.controlPerms.or(["EDIT_MEMBERS", "IS_OFFICER"], ctx);
if (!input.id)
throw new TRPCError({
@@ -644,7 +645,7 @@ export const memberRouter = {
where: eq(Member.id, input.id),
columns: { firstName: true, lastName: true },
});
- await log({
+ await discord.log({
title: "Dues Status Accredited",
message: `${member?.firstName} ${member?.lastName} has been accredited dues status.`,
color: "success_green",
@@ -655,7 +656,7 @@ export const memberRouter = {
deleteDuesPayingMember: permProcedure
.input(InsertMemberSchema.pick({ id: true }))
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["EDIT_MEMBERS", "IS_OFFICER"], ctx);
+ permissions.controlPerms.or(["EDIT_MEMBERS", "IS_OFFICER"], ctx);
if (!input.id)
throw new TRPCError({
@@ -667,7 +668,7 @@ export const memberRouter = {
where: eq(Member.id, input.id),
columns: { firstName: true, lastName: true },
});
- await log({
+ await discord.log({
title: "Dues Status Revoked",
message: `${member?.firstName} ${member?.lastName} has been revoked of dues status.`,
color: "uhoh_red",
@@ -676,10 +677,10 @@ export const memberRouter = {
}),
clearAllDues: permProcedure.mutation(async ({ ctx }) => {
- controlPerms.or(["IS_OFFICER"], ctx);
+ permissions.controlPerms.or(["IS_OFFICER"], ctx);
await db.delete(DuesPayment);
- await log({
+ await discord.log({
title: "ALL DUES CLEARED",
message:
"ALL DUES HAVE BEEN CLEARED. THIS ACTION IS REVERSIBLE FOR ONLY 7 DAYS.",
@@ -697,7 +698,10 @@ export const memberRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["CHECKIN_CLUB_EVENT", "CHECKIN_HACK_EVENT"], ctx);
+ permissions.controlPerms.or(
+ ["CHECKIN_CLUB_EVENT", "CHECKIN_HACK_EVENT"],
+ ctx,
+ );
const member = await db.query.Member.findFirst({
where: eq(Member.userId, input.userId),
@@ -747,7 +751,7 @@ export const memberRouter = {
.update(Member)
.set({ points: sql`${Member.points} + ${input.eventPoints}` })
.where(eq(Member.id, member.id));
- await log({
+ await discord.log({
title: "User Checked-In",
message: `${member.firstName} ${member.lastName} has been checked in to event ${event.name}.`,
color: "success_green",
diff --git a/packages/api/src/routers/misc.ts b/packages/api/src/routers/misc.ts
index ae72d3141..ee98cd3c0 100644
--- a/packages/api/src/routers/misc.ts
+++ b/packages/api/src/routers/misc.ts
@@ -4,9 +4,9 @@ import { Routes } from "discord-api-types/v10";
import { z } from "zod";
import { DISCORD, FORMS, TEAM } from "@forge/consts";
+import * as discord from "@forge/utils/discord";
import { protectedProcedure } from "../trpc";
-import { discord } from "../utils";
export interface FundingRequestInput {
team: string;
@@ -94,13 +94,7 @@ export const miscRouter = {
try {
const discId = ctx.session.user.discordUserId;
- await discord.put(
- Routes.guildMemberRole(
- DISCORD.KNIGHTHACKS_GUILD,
- discId,
- input.roleId,
- ),
- );
+ await discord.addRoleToMember(discId, input.roleId);
} catch (err) {
throw new TRPCError({
message: `Could not assign role ${input.roleId} to user ${ctx.session.user.name}`,
@@ -143,53 +137,57 @@ export const miscRouter = {
// Convert hex color string to integer for Discord API
const colorInt = parseInt(team.color.replace("#", ""), 16);
- await discord.post(Routes.channelMessages(DISCORD.RECRUITING_CHANNEL), {
- body: {
- content: `<@&${directorRole}> **New Applicant for ${team.team}!**`,
- embeds: [
- {
- title: `${input.name}'s Application`,
- description: `A new applicant is interested in joining the **${team.team}** team.\n\nPlease see details below:`,
- color: colorInt,
- fields: [
- {
- name: "Name",
- value: input.name,
- inline: true,
+ // TODO: refactor to util
+ await discord.api.post(
+ Routes.channelMessages(DISCORD.RECRUITING_CHANNEL),
+ {
+ body: {
+ content: `<@&${directorRole}> **New Applicant for ${team.team}!**`,
+ embeds: [
+ {
+ title: `${input.name}'s Application`,
+ description: `A new applicant is interested in joining the **${team.team}** team.\n\nPlease see details below:`,
+ color: colorInt,
+ fields: [
+ {
+ name: "Name",
+ value: input.name,
+ inline: true,
+ },
+ {
+ name: "Email",
+ value: input.email,
+ inline: true,
+ },
+ {
+ name: "Major",
+ value: input.major,
+ inline: true,
+ },
+ {
+ name: "Grad Term",
+ value: input.gradTerm,
+ inline: true,
+ },
+ {
+ name: "Grad Year",
+ value: input.gradYear.toString(),
+ inline: true,
+ },
+ {
+ name: "Team",
+ value: team.team,
+ inline: true,
+ },
+ ],
+ footer: {
+ text: `Submitted at: ${new Date().toLocaleString()}`,
},
- {
- name: "Email",
- value: input.email,
- inline: true,
- },
- {
- name: "Major",
- value: input.major,
- inline: true,
- },
- {
- name: "Grad Term",
- value: input.gradTerm,
- inline: true,
- },
- {
- name: "Grad Year",
- value: input.gradYear.toString(),
- inline: true,
- },
- {
- name: "Team",
- value: team.team,
- inline: true,
- },
- ],
- footer: {
- text: `Submitted at: ${new Date().toLocaleString()}`,
+ timestamp: new Date().toISOString(),
},
- timestamp: new Date().toISOString(),
- },
- ],
+ ],
+ },
},
- });
+ );
}),
} satisfies TRPCRouterRecord;
diff --git a/packages/api/src/routers/passkit.ts b/packages/api/src/routers/passkit.ts
index 7eb878454..ed4910813 100644
--- a/packages/api/src/routers/passkit.ts
+++ b/packages/api/src/routers/passkit.ts
@@ -5,6 +5,7 @@ import { TRPCError } from "@trpc/server";
import { PKPass } from "passkit-generator";
import { db } from "@forge/db/client";
+import { logger } from "@forge/utils";
import { env } from "../env";
import { protectedProcedure } from "../trpc";
@@ -144,7 +145,7 @@ export const passkitRouter = {
fileName: fileName,
};
} catch (error) {
- console.error("Error generating passkit pass:", error);
+ logger.error("Error generating passkit pass:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Failed to generate passkit pass: ${error instanceof Error ? error.message : "Unknown error"}`,
diff --git a/packages/api/src/routers/resume.ts b/packages/api/src/routers/resume.ts
index af6598088..bcfdac278 100644
--- a/packages/api/src/routers/resume.ts
+++ b/packages/api/src/routers/resume.ts
@@ -5,6 +5,7 @@ import { z } from "zod";
import { MINIO } from "@forge/consts";
import { db } from "@forge/db/client";
+import { logger } from "@forge/utils";
import { env } from "../env";
import { protectedProcedure } from "../trpc";
@@ -87,14 +88,14 @@ export const resumeRouter = {
// If neither member nor hacker found, return null
if (!member && !hacker) {
- console.error("No resume found for user");
+ logger.error("No resume found for user");
return { url: null };
}
const filename = member?.resumeUrl ?? hacker?.resumeUrl;
if (!filename) {
- console.error("No resume URL found for user");
+ logger.error("No resume URL found for user");
return { url: null };
}
diff --git a/packages/api/src/routers/roles.ts b/packages/api/src/routers/roles.ts
index c669d4e2b..784b711c9 100644
--- a/packages/api/src/routers/roles.ts
+++ b/packages/api/src/routers/roles.ts
@@ -8,16 +8,10 @@ import { DISCORD, PERMISSIONS } from "@forge/consts";
import { eq, inArray, sql } from "@forge/db";
import { db } from "@forge/db/client";
import { Permissions, Roles, User } from "@forge/db/schemas/auth";
+import { logger, permissions } from "@forge/utils";
+import * as discord from "@forge/utils/discord";
import { permProcedure, protectedProcedure } from "../trpc";
-import {
- addRoleToMember,
- controlPerms,
- discord,
- getPermsAsList,
- log,
- removeRoleFromMember,
-} from "../utils";
export const rolesRouter = {
// ROLES
@@ -31,7 +25,7 @@ export const rolesRouter = {
}),
)
.mutation(async ({ ctx, input }) => {
- controlPerms.or(["CONFIGURE_ROLES"], ctx);
+ permissions.controlPerms.or(["CONFIGURE_ROLES"], ctx);
// check for duplicate discord role
const dupe = await db.query.Roles.findFirst({
@@ -70,7 +64,7 @@ export const rolesRouter = {
for (const bladeUser of bladeUsers) {
try {
- const guildMember = (await discord.get(
+ const guildMember = (await discord.api.get(
Routes.guildMember(
DISCORD.KNIGHTHACKS_GUILD,
bladeUser.discordUserId,
@@ -98,19 +92,19 @@ export const rolesRouter = {
}
}
- await log({
+ await discord.log({
title: `Created Role: ${input.name}`,
message: `Role linked to <@&${input.roleId}>
- \n**Permissions:** ${getPermsAsList(input.permissions).join(", ")}
+ \n**Permissions:** ${permissions.getPermsAsList(input.permissions).join(", ")}
\n**Auto-synced:** ${syncedCount} user(s) granted (checked ${checkedCount} Blade users)`,
color: "blade_purple",
userId: ctx.session.user.discordUserId,
});
} catch {
- await log({
+ await discord.log({
title: `Created Role: ${input.name}`,
message: `Role linked to <@&${input.roleId}>
- \n**Permissions:** ${getPermsAsList(input.permissions).join(", ")}
+ \n**Permissions:** ${permissions.getPermsAsList(input.permissions).join(", ")}
\n**Note:** Auto-sync unavailable. Checked ${checkedCount} users, synced ${syncedCount}.`,
color: "blade_purple",
userId: ctx.session.user.discordUserId,
@@ -128,7 +122,7 @@ export const rolesRouter = {
}),
)
.mutation(async ({ ctx, input }) => {
- controlPerms.or(["CONFIGURE_ROLES"], ctx);
+ permissions.controlPerms.or(["CONFIGURE_ROLES"], ctx);
// check for existing role
const exist = await db.query.Roles.findFirst({
@@ -160,12 +154,12 @@ export const rolesRouter = {
})
.where(eq(Roles.id, input.id));
- await log({
+ await discord.log({
title: `Updated Role`,
message: `The **${exist.name}** Role (<@&${input.roleId}>) role has been updated.
\n**Name:** ${exist.name} -> ${input.name}
- \n**Original Perms:**\n${getPermsAsList(exist.permissions).join("\n")}
- \n**New Perms:**\n${getPermsAsList(input.permissions).join("\n")}`,
+ \n**Original Perms:**\n${permissions.getPermsAsList(exist.permissions).join("\n")}
+ \n**New Perms:**\n${permissions.getPermsAsList(input.permissions).join("\n")}`,
color: "blade_purple",
userId: ctx.session.user.discordUserId,
});
@@ -174,7 +168,7 @@ export const rolesRouter = {
deleteRoleLink: permProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
- controlPerms.or(["CONFIGURE_ROLES"], ctx);
+ permissions.controlPerms.or(["CONFIGURE_ROLES"], ctx);
// check for existing role
const exist = await db.query.Roles.findFirst({
@@ -188,7 +182,7 @@ export const rolesRouter = {
await db.delete(Roles).where(eq(Roles.id, input.id));
- await log({
+ await discord.log({
title: `Deleted Role`,
message: `The **${exist.name}** Role (<@&${exist.discordRoleId}>) role has been deleted.`,
color: "uhoh_red",
@@ -212,7 +206,7 @@ export const rolesRouter = {
.input(z.object({ roleId: z.string() }))
.query(async ({ input }): Promise => {
try {
- return (await discord.get(
+ return (await discord.api.get(
Routes.guildRole(DISCORD.KNIGHTHACKS_GUILD, input.roleId),
)) as APIRole | null;
} catch {
@@ -230,7 +224,7 @@ export const rolesRouter = {
for (const r of input.roles) {
try {
ret.push(
- (await discord.get(
+ (await discord.api.get(
Routes.guildRole(DISCORD.KNIGHTHACKS_GUILD, r.discordRoleId),
)) as APIRole | null,
);
@@ -244,7 +238,7 @@ export const rolesRouter = {
getDiscordRoleCounts: protectedProcedure.query(
async (): Promise | null> => {
- return (await discord.get(
+ return (await discord.api.get(
`/guilds/${DISCORD.KNIGHTHACKS_GUILD}/roles/member-counts`,
)) as Record;
},
@@ -300,8 +294,8 @@ export const rolesRouter = {
)
.query(({ input, ctx }) => {
try {
- if (input.or) controlPerms.or(input.or, ctx);
- if (input.and) controlPerms.and(input.and, ctx);
+ if (input.or) permissions.controlPerms.or(input.or, ctx);
+ if (input.and) permissions.controlPerms.and(input.and, ctx);
} catch {
return false;
}
@@ -312,7 +306,7 @@ export const rolesRouter = {
grantPermission: permProcedure
.input(z.object({ roleId: z.string(), userId: z.string() }))
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["ASSIGN_ROLES"], ctx);
+ permissions.controlPerms.or(["ASSIGN_ROLES"], ctx);
const exists = await db.query.Permissions.findFirst({
where: (t, { eq, and }) =>
@@ -344,16 +338,16 @@ export const rolesRouter = {
// Note: This may fail due to role hierarchy or bot permissions
// We log the error but don't break the flow - Blade permission is still granted
try {
- await addRoleToMember(user.discordUserId, role.discordRoleId);
- console.log(
+ await discord.addRoleToMember(user.discordUserId, role.discordRoleId);
+ logger.log(
`Successfully added Discord role ${role.discordRoleId} to user ${user.discordUserId}`,
);
} catch (error) {
- console.error(
+ logger.error(
`Failed to add Discord role ${role.discordRoleId} to user ${user.discordUserId}:`,
error,
);
- console.error(
+ logger.error(
` This may be due to role hierarchy or bot permissions. Blade permission will still be granted.`,
);
}
@@ -363,7 +357,7 @@ export const rolesRouter = {
userId: input.userId,
});
- await log({
+ await discord.log({
title: `Granted Role`,
message: `The **${role.name}** role (<@&${role.discordRoleId}>) has been granted to <@${user.discordUserId}>.`,
color: "success_green",
@@ -374,7 +368,7 @@ export const rolesRouter = {
revokePermission: permProcedure
.input(z.object({ roleId: z.string(), userId: z.string() }))
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["ASSIGN_ROLES"], ctx);
+ permissions.controlPerms.or(["ASSIGN_ROLES"], ctx);
const perm = await db.query.Permissions.findFirst({
where: (t, { eq, and }) =>
@@ -407,23 +401,26 @@ export const rolesRouter = {
// Note: This may fail due to role hierarchy or bot permissions
// We log the error but don't break the flow - Blade permission is still revoked
try {
- await removeRoleFromMember(user.discordUserId, role.discordRoleId);
- console.log(
+ await discord.removeRoleFromMember(
+ user.discordUserId,
+ role.discordRoleId,
+ );
+ logger.log(
`✅ Successfully removed Discord role ${role.discordRoleId} from user ${user.discordUserId}`,
);
} catch (error) {
- console.error(
+ logger.error(
`Failed to remove Discord role ${role.discordRoleId} from user ${user.discordUserId}:`,
error,
);
- console.error(
+ logger.error(
` This may be due to role hierarchy or bot permissions. Blade permission will still be revoked.`,
);
}
await db.delete(Permissions).where(eq(Permissions.id, perm.id));
- await log({
+ await discord.log({
title: `Revoked Role`,
message: `The **${role.name}** role (<@&${role.discordRoleId}>) has been revoked from <@${user.discordUserId}>.`,
color: "uhoh_red",
@@ -440,7 +437,7 @@ export const rolesRouter = {
}),
)
.mutation(async ({ input, ctx }) => {
- controlPerms.or(["ASSIGN_ROLES"], ctx);
+ permissions.controlPerms.or(["ASSIGN_ROLES"], ctx);
interface Return {
roleName: string;
@@ -494,12 +491,12 @@ export const rolesRouter = {
if (!input.revoking) {
// Granting role - Discord may fail due to hierarchy/perms
try {
- await addRoleToMember(
+ await discord.addRoleToMember(
userData.discordUserId,
roleData.discordRoleId,
);
} catch (discordError) {
- console.error(
+ logger.error(
`Discord role grant failed for ${userData.name} -> ${roleData.name}:`,
discordError,
);
@@ -512,12 +509,12 @@ export const rolesRouter = {
} else if (perm) {
// Revoking role - Discord may fail due to hierarchy/perms
try {
- await removeRoleFromMember(
+ await discord.removeRoleFromMember(
userData.discordUserId,
roleData.discordRoleId,
);
} catch (discordError) {
- console.error(
+ logger.error(
`Discord role revoke failed for ${userData.name} -> ${roleData.name}:`,
discordError,
);
@@ -529,7 +526,7 @@ export const rolesRouter = {
}
} catch (error) {
// This catches DB errors only (Discord errors are caught above)
- console.error(
+ logger.error(
`Database error for ${input.revoking ? "revoke" : "grant"} role ${roleData.name} ${input.revoking ? "from" : "to"} ${userData.name}:`,
error,
);
@@ -545,7 +542,7 @@ export const rolesRouter = {
failed.map((v) => `${v.userName} -> ${v.roleName}`).join("\n")
: "";
- await log({
+ await discord.log({
title: `${input.revoking ? "Revoked" : "Granted"} Batch Roles`,
message:
`The following roles have been ${input.revoking ? "revoked from" : "granted to"} the following users:\n\n` +
diff --git a/packages/api/src/routers/user.ts b/packages/api/src/routers/user.ts
index 0983aa0b1..d60c1d93f 100644
--- a/packages/api/src/routers/user.ts
+++ b/packages/api/src/routers/user.ts
@@ -1,9 +1,9 @@
import type { TRPCRouterRecord } from "@trpc/server";
import { db } from "@forge/db/client";
+import { permissions } from "@forge/utils";
import { permProcedure, protectedProcedure } from "../trpc";
-import { controlPerms } from "../utils";
// // helper schema to check if a value is either of type PermissionKey or PermissionIndex
// // z.custom doesn't perform any validation by itself, so it will let any type at runtime
@@ -39,7 +39,7 @@ export const userRouter = {
// Also appends roles to returned users
getUsers: permProcedure.query(async ({ ctx }) => {
- controlPerms.or(["CONFIGURE_ROLES"], ctx);
+ permissions.controlPerms.or(["CONFIGURE_ROLES"], ctx);
const users = await db.query.User.findMany({
with: {
permissions: true,
diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts
index a82242edc..904280825 100644
--- a/packages/api/src/trpc.ts
+++ b/packages/api/src/trpc.ts
@@ -1,3 +1,5 @@
+/* eslint-disable no-console */
+
/**
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
* 1. You want to modify request context (see Part 1)
@@ -16,12 +18,8 @@ import { PERMISSIONS } from "@forge/consts";
import { eq, sql } from "@forge/db";
import { db } from "@forge/db/client";
import { Permissions, Roles } from "@forge/db/schemas/auth";
-
-import {
- getJudgeSessionFromCookie,
- isDiscordAdmin,
- isJudgeAdmin,
-} from "./utils";
+import * as discord from "@forge/utils/discord";
+import * as permissionsServer from "@forge/utils/permissions.server";
/**
* 1. CONTEXT
@@ -183,15 +181,15 @@ export const permProcedure = protectedProcedure.use(async ({ ctx, next }) => {
export const judgeProcedure = publicProcedure.use(async ({ ctx, next }) => {
let isAdmin;
if (ctx.session) {
- isAdmin = await isDiscordAdmin(ctx.session.user);
+ isAdmin = await discord.isDiscordAdmin(ctx.session.user);
}
- const isJudge = await isJudgeAdmin();
+ const isJudge = await permissionsServer.isJudgeAdmin();
if (!isAdmin && !isJudge) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
- const judgeSession = await getJudgeSessionFromCookie();
+ const judgeSession = await permissionsServer.getJudgeSessionFromCookie();
return next({
ctx: {
diff --git a/packages/api/src/utils.ts b/packages/api/src/utils.ts
deleted file mode 100644
index aac26ac8d..000000000
--- a/packages/api/src/utils.ts
+++ /dev/null
@@ -1,611 +0,0 @@
-import type { APIGuildMember } from "discord-api-types/v10";
-import type { JSONSchema7 } from "json-schema";
-import { cookies } from "next/headers";
-import { REST } from "@discordjs/rest";
-import { TRPCError } from "@trpc/server";
-import { Routes } from "discord-api-types/v10";
-import { and, desc, eq, gt, inArray } from "drizzle-orm";
-import { google } from "googleapis";
-import Stripe from "stripe";
-import z from "zod";
-
-import type { Session } from "@forge/auth/server";
-import type { Form } from "@forge/db/schemas/knight-hacks";
-import { DISCORD, EVENTS, FORMS, MINIO, PERMISSIONS } from "@forge/consts";
-import { db } from "@forge/db/client";
-import { Account, JudgeSession, Roles } from "@forge/db/schemas/auth";
-import { FormSchemaSchema, FormsSchemas } from "@forge/db/schemas/knight-hacks";
-import { client } from "@forge/email";
-
-import { env } from "./env";
-import { minioClient } from "./minio/minio-client";
-
-export const discord = new REST({ version: "10" }).setToken(
- env.DISCORD_BOT_TOKEN,
-);
-
-export async function addRoleToMember(discordUserId: string, roleId: string) {
- await discord.put(
- Routes.guildMemberRole(DISCORD.KNIGHTHACKS_GUILD, discordUserId, roleId),
- );
-}
-
-export async function removeRoleFromMember(
- discordUserId: string,
- roleId: string,
-) {
- await discord.delete(
- Routes.guildMemberRole(DISCORD.KNIGHTHACKS_GUILD, discordUserId, roleId),
- );
-}
-
-export async function addMemberToServer(
- discordUserId: string,
- accessToken: string,
-): Promise {
- try {
- await discord.put(
- Routes.guildMember(DISCORD.KNIGHTHACKS_GUILD, discordUserId),
- {
- body: {
- access_token: accessToken,
- },
- },
- );
-
- console.log(`Added ${discordUserId} to the KH discord server`);
- return;
- } catch (error) {
- console.error(
- `Failed to add user ${discordUserId} to the KH discord server:`,
- error instanceof Error ? error.message : "Unknown error",
- );
- }
-}
-
-export async function handleDiscordOAuthCallback(
- discordUserId: string,
-): Promise {
- try {
- const user = await db.query.User.findFirst({
- where: (u, { eq }) => eq(u.discordUserId, discordUserId),
- });
-
- if (!user) {
- return;
- }
-
- const accounts = await db
- .select({ account: Account })
- .from(Account)
- .where(and(eq(Account.provider, "discord"), eq(Account.userId, user.id)))
- .orderBy(desc(Account.updatedAt))
- .limit(1);
-
- const account = accounts[0]?.account;
- const accessToken = account?.access_token;
- const scope = account?.scope;
-
- if (accessToken && scope?.includes("guilds.join")) {
- void addMemberToServer(discordUserId, accessToken);
- }
- } catch (error) {
- console.error(
- `Failed to handle Discord OAuth callback for ${discordUserId}:`,
- error instanceof Error ? error.message : "Unknown error",
- );
- }
-}
-
-export async function resolveDiscordUserId(
- username: string,
-): Promise {
- const q = username.trim().toLowerCase();
- const members = (await discord.get(
- `${Routes.guildMembersSearch(DISCORD.KNIGHTHACKS_GUILD)}?query=${encodeURIComponent(q)}&limit=1`,
- )) as APIGuildMember[];
- return members[0]?.user.id ?? null;
-}
-
-export const stripe = new Stripe(env.STRIPE_SECRET_KEY, { typescript: true });
-
-export const isDiscordAdmin = async (user: Session["user"]) => {
- try {
- const guildMember = (await discord.get(
- Routes.guildMember(DISCORD.KNIGHTHACKS_GUILD, user.discordUserId),
- )) as APIGuildMember;
- return guildMember.roles.includes(DISCORD.ADMIN_ROLE);
- } catch (err) {
- console.error("Error: ", err);
- return false;
- }
-};
-
-export const hasPermission = (
- userPermissions: string,
- permission: PERMISSIONS.PermissionIndex,
-): boolean => {
- const permissionBit = userPermissions[permission];
- return permissionBit === "1";
-};
-
-export const parsePermissions = async (discordUserId: string) => {
- const guildMember = (await discord.get(
- Routes.guildMember(DISCORD.KNIGHTHACKS_GUILD, discordUserId),
- )) as APIGuildMember;
-
- const permissionsLength = Object.keys(PERMISSIONS.PERMISSIONS).length;
-
- // array of booleans. the boolean value at the index indicates if the user has that permission.
- // true means the user has the permission, false means the user doesn't have the permission.
- const permissionsBits = new Array(permissionsLength).fill(false) as boolean[];
-
- if (guildMember.roles.length > 0) {
- // get only roles the user has
- const userDbRoles = await db
- .select()
- .from(Roles)
- .where(inArray(Roles.discordRoleId, guildMember.roles));
-
- for (const role of userDbRoles) {
- if (!role.permissions) continue;
-
- for (
- let i = 0;
- i < role.permissions.length && i < permissionsLength;
- ++i
- ) {
- if (role.permissions[i] === "1") {
- permissionsBits[i] = true;
- }
- }
- }
- }
-
- // creates the map of permissions to their boolean values
- const permissionsMap = Object.keys(PERMISSIONS.PERMISSIONS).reduce(
- (accumulator, key) => {
- const index = PERMISSIONS.PERMISSIONS[key];
- if (index === undefined) return accumulator;
-
- accumulator[key] = permissionsBits[index] ?? false;
-
- return accumulator;
- },
- {} as Record,
- );
-
- return permissionsMap;
-};
-
-// Mock tRPC context for type-safety
-interface Context {
- session: {
- permissions: Record;
- };
-}
-
-export const controlPerms = {
- // Returns true if the user has any required permission OR has isOfficer role
- or: (perms: PERMISSIONS.PermissionKey[], ctx: Context) => {
- // first check if user has IS_OFFICER
- if (ctx.session.permissions.IS_OFFICER) return true;
-
- let flag = false;
- for (const p of perms) if (ctx.session.permissions[p]) flag = true;
- if (!flag) throw new TRPCError({ code: "UNAUTHORIZED" });
- return true;
- },
-
- // Returns true only if the user has ALL required permissions
- and: (perms: PERMISSIONS.PermissionKey[], ctx: Context) => {
- // first check if user has IS_OFFICER
- if (ctx.session.permissions.IS_OFFICER) return true;
-
- for (const p of perms)
- if (!ctx.session.permissions[p])
- throw new TRPCError({ code: "UNAUTHORIZED" });
-
- return true;
- },
-};
-
-export const isDiscordMember = async (user: Session["user"]) => {
- try {
- await discord.get(
- Routes.guildMember(DISCORD.KNIGHTHACKS_GUILD, user.discordUserId),
- );
- return true;
- } catch {
- return false;
- }
-};
-
-export async function isDiscordVIP(discordUserId: string) {
- const guildMember = (await discord.get(
- Routes.guildMember(DISCORD.KNIGHTHACKS_GUILD, discordUserId),
- )) as APIGuildMember;
- return guildMember.roles.includes(DISCORD.VIP_ROLE);
-}
-
-export const sendEmail = async ({
- to,
- subject,
- template_id,
- from,
- data,
-}: {
- to: string | string[];
- subject: string;
- template_id: number;
- data: Record;
- from?: string;
-}): Promise<{ success: true }> => {
- try {
- await client.tx.send({
- template_id: template_id,
- from_email: from ?? env.LISTMONK_FROM_EMAIL,
- subscriber_mode: "external",
- subscriber_emails: typeof to === "string" ? [to] : to,
- subject: subject,
- data: data,
- });
-
- return { success: true };
- } catch (error) {
- console.error("Error sending email:", error);
- throw new Error(
- `Failed to send email: ${
- error instanceof Error ? error.message : "Unknown error"
- }`,
- );
- }
-};
-
-export async function log({
- title,
- message,
- color,
- userId,
-}: {
- title: string;
- message: string;
- color: "tk_blue" | "blade_purple" | "uhoh_red" | "success_green";
- userId: string;
-}) {
- await discord.post(Routes.channelMessages(DISCORD.LOG_CHANNEL), {
- body: {
- embeds: [
- {
- title: title,
- description: message + `\n\nUser: <@${userId}>`.toString(),
- color: {
- tk_blue: 0x1a73e8,
- blade_purple: 0xcca4f4,
- uhoh_red: 0xff0000,
- success_green: 0x00ff00,
- }[color],
- footer: {
- text: new Date().toLocaleString(),
- },
- },
- ],
- },
- });
-}
-
-export const isJudgeAdmin = async () => {
- try {
- const token = cookies().get("sessionToken")?.value;
- if (!token) return false;
-
- const now = new Date();
- const rows = await db
- .select({ sessionToken: JudgeSession.sessionToken })
- .from(JudgeSession)
- .where(
- and(
- eq(JudgeSession.sessionToken, token),
- gt(JudgeSession.expires, now),
- ),
- )
- .limit(1);
-
- return rows.length > 0;
- } catch (err) {
- console.error("isJudgeAdmin DB check error:", err);
- return false;
- }
-};
-
-export const getJudgeSessionFromCookie = async () => {
- const token = cookies().get("sessionToken")?.value;
- if (!token) return null;
-
- const now = new Date();
- const rows = await db
- .select({
- sessionToken: JudgeSession.sessionToken,
- roomName: JudgeSession.roomName,
- expires: JudgeSession.expires,
- })
- .from(JudgeSession)
- .where(
- and(eq(JudgeSession.sessionToken, token), gt(JudgeSession.expires, now)),
- )
- .limit(1);
-
- return rows[0] ?? null;
-};
-
-const GOOGLE_PRIVATE_KEY = Buffer.from(env.GOOGLE_PRIVATE_KEY_B64, "base64")
- .toString("utf-8")
- .replace(/\\n/g, "\n");
-
-const gapiCalendar = "https://www.googleapis.com/auth/calendar";
-const gapiGmailSend = "https://www.googleapis.com/auth/gmail.send";
-const gapiGmailSettingsSharing =
- "https://www.googleapis.com/auth/gmail.settings.sharing";
-
-const auth = new google.auth.JWT(
- env.GOOGLE_CLIENT_EMAIL,
- undefined,
- GOOGLE_PRIVATE_KEY,
- [gapiCalendar, gapiGmailSend, gapiGmailSettingsSharing],
- EVENTS.GOOGLE_PERSONIFY_EMAIL as string,
-);
-
-export const gmail = google.gmail({
- version: "v1",
- auth: auth,
-});
-
-export const calendar = google.calendar({
- version: "v3",
- auth: auth,
-});
-
-type OptionalSchema =
- | { success: true; schema: JSONSchema7 }
- | { success: false; msg: string };
-
-function createJsonSchemaValidator({
- optional,
- type,
- options,
- optionsConst,
- min,
- max,
- allowOther,
-}: FORMS.ValidatorOptions): OptionalSchema {
- const schema: JSONSchema7 = {};
-
- const resolvedOptions = optionsConst
- ? [...FORMS.getDropdownOptionsFromConst(optionsConst)]
- : options;
-
- switch (type) {
- case "SHORT_ANSWER":
- case "PARAGRAPH":
- schema.type = "string";
- if (max === undefined) {
- schema.maxLength = type === "SHORT_ANSWER" ? 150 : 750;
- }
- break;
- case "EMAIL":
- schema.type = "string";
- schema.format = "email";
- break;
- case "PHONE":
- schema.type = "string";
- schema.pattern = "^\\+?\\d{7,15}$";
- break;
- case "DATE":
- schema.type = "string";
- schema.format = "date";
- break;
- case "TIME":
- schema.type = "string";
- schema.pattern = "^([01]\\d|2[0-3]):([0-5]\\d)$";
- break;
- case "NUMBER":
- case "LINEAR_SCALE":
- schema.type = "number";
- break;
- case "MULTIPLE_CHOICE":
- case "DROPDOWN":
- if (!resolvedOptions?.length)
- return {
- success: false,
- msg: "Options are required for multiple choice / dropdown",
- };
- schema.type = "string";
- if (!allowOther) {
- schema.enum = resolvedOptions;
- }
- break;
- case "CHECKBOXES":
- if (!resolvedOptions?.length)
- return { success: false, msg: "Options required for checkboxes" };
- schema.type = "array";
- if (allowOther) {
- schema.items = { type: "string" };
- } else {
- schema.items = { type: "string", enum: resolvedOptions };
- }
- break;
- case "FILE_UPLOAD":
- schema.type = "string";
- break;
- case "BOOLEAN":
- schema.type = "boolean";
- break;
- case "LINK":
- schema.type = "string";
- schema.format = "uri";
- break;
- default:
- schema.type = "string";
- }
-
- if (min !== undefined) {
- if (schema.type === "string") schema.minLength = min;
- if (schema.type === "array") schema.minItems = min;
- if (schema.type === "number") schema.minimum = min;
- } else {
- if (schema.type === "array" && !optional) schema.minItems = 1;
- }
-
- if (max !== undefined) {
- if (schema.type === "string") {
- // Explicit max value overrides any defaults
- schema.maxLength = max;
- }
- if (schema.type === "array") schema.maxItems = max;
- if (schema.type === "number") schema.maximum = max;
- }
-
- return { success: true, schema };
-}
-
-export function generateJsonSchema(form: FORMS.FormType): OptionalSchema {
- const schema: JSONSchema7 = {
- type: "object",
- properties: {},
- required: [],
- additionalProperties: false,
- };
-
- const properties: Record = {};
- const required: string[] = [];
-
- for (const formQuestion of form.questions) {
- const { question, optional, ...rest } = formQuestion;
- const convert = createJsonSchemaValidator({ optional, ...rest });
- if (convert.success) properties[question] = convert.schema;
- else return convert;
-
- if (!optional) {
- required.push(question);
- }
- }
-
- schema.properties = properties;
- if (required.length > 0) {
- schema.required = required;
- }
-
- return { success: true, schema };
-}
-
-// Helper to regenerate presigned URLs for media
-export async function regenerateMediaUrls(
- instructions: FORMS.FormType["instructions"],
-) {
- if (!instructions) return [];
- const updatedQuestions = await Promise.all(
- instructions.map(async (i) => {
- const updated = { ...i };
-
- // Regenerate image URL if objectName exists
- if ("imageObjectName" in i && i.imageObjectName) {
- try {
- updated.imageUrl = await minioClient.presignedGetObject(
- MINIO.FORM_ASSETS_BUCKET_NAME,
- i.imageObjectName,
- MINIO.PRESIGNED_URL_EXPIRY,
- );
- } catch (e) {
- console.error("Failed to regenerate image URL:", e);
- }
- }
-
- // Regenerate video URL if objectName exists
- if ("videoObjectName" in i && i.videoObjectName) {
- try {
- updated.videoUrl = await minioClient.presignedGetObject(
- MINIO.FORM_ASSETS_BUCKET_NAME,
- i.videoObjectName,
- MINIO.PRESIGNED_URL_EXPIRY,
- );
- } catch (e) {
- console.error("Failed to regenerate video URL:", e);
- }
- }
-
- return updated;
- }),
- );
-
- return updatedQuestions;
-}
-
-export function getPermsAsList(perms: string) {
- const list = [];
- const permKeys = Object.keys(PERMISSIONS.PERMISSIONS);
- for (let i = 0; i < perms.length; i++) {
- const permKey = permKeys.at(i);
- if (perms[i] == "1" && permKey) {
- const permissionData = PERMISSIONS.PERMISSION_DATA[permKey];
- if (permissionData) list.push(permissionData.name);
- }
- }
- return list;
-}
-
-// All of this will be moved to @forge/utils but its here for now
-export const CreateFormSchema = FormSchemaSchema.omit({
- id: true,
- name: true,
- slugName: true,
- createdAt: true,
- formData: true,
- formValidatorJson: true,
-})
- .extend({ formData: FORMS.FormSchemaValidator })
- .extend({ section: z.string().optional() });
-
-type CreateFormType = z.infer;
-
-export async function createForm(input: CreateFormType): Promise