From 9c9199a215d1f9fe71642a04d59e105def740fae Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:18:47 -0500 Subject: [PATCH 01/27] feat(utils): create package used `pnpm turbo gen` --- packages/utils/eslint.config.js | 9 +++ packages/utils/package.json | 27 ++++++++ packages/utils/src/index.ts | 1 + packages/utils/tsconfig.json | 6 ++ pnpm-lock.yaml | 108 +++++++++++++++++++++++++++++--- 5 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 packages/utils/eslint.config.js create mode 100644 packages/utils/package.json create mode 100644 packages/utils/src/index.ts create mode 100644 packages/utils/tsconfig.json diff --git a/packages/utils/eslint.config.js b/packages/utils/eslint.config.js new file mode 100644 index 000000000..66675f5b8 --- /dev/null +++ b/packages/utils/eslint.config.js @@ -0,0 +1,9 @@ +import baseConfig from "@forge/eslint-config/base"; + +/** @type {import('typescript-eslint').Config} */ +export default [ + { + ignores: [], + }, + ...baseConfig, +]; diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 000000000..1119fa157 --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,27 @@ +{ + "name": "@forge/utils", + "private": true, + "version": "0.1.0", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "license": "MIT", + "scripts": { + "build": "tsc", + "clean": "git clean -xdf .cache .turbo dist node_modules", + "dev": "tsc", + "format": "prettier --check . --ignore-path ../../.gitignore", + "lint": "eslint", + "typecheck": "tsc --noEmit --emitDeclarationOnly false" + }, + "devDependencies": { + "@forge/eslint-config": "workspace:*", + "@forge/prettier-config": "workspace:*", + "@forge/tsconfig": "workspace:*", + "eslint": "catalog:", + "prettier": "catalog:", + "typescript": "catalog:" + }, + "prettier": "@forge/prettier-config" +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts new file mode 100644 index 000000000..000c25319 --- /dev/null +++ b/packages/utils/src/index.ts @@ -0,0 +1 @@ +export const name = "utils"; diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 000000000..a8e81d09e --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@forge/tsconfig/internal-package.json", + "compilerOptions": {}, + "include": ["*.ts", "src"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index caf4f75b4..7294ede07 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -240,7 +240,7 @@ importers: version: 6.6.0 geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) google-auth-library: specifier: ^9.15.0 version: 9.15.1 @@ -358,7 +358,7 @@ importers: version: 12.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) gsap: specifier: ^3.12.7 version: 3.14.2 @@ -404,7 +404,7 @@ importers: version: 7.4.4 eslint: specifier: 'catalog:' - version: 9.39.2(jiti@2.6.1) + version: 9.39.2(jiti@1.21.7) prettier: specifier: 'catalog:' version: 3.8.1 @@ -531,7 +531,7 @@ importers: version: 12.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) gsap: specifier: ^3.12.7 version: 3.14.2 @@ -622,7 +622,7 @@ importers: version: 12.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) gsap: specifier: ^3.12.7 version: 3.14.2 @@ -1154,6 +1154,27 @@ importers: specifier: 'catalog:' version: 3.25.76 + packages/utils: + devDependencies: + '@forge/eslint-config': + specifier: workspace:* + version: link:../../tooling/eslint + '@forge/prettier-config': + specifier: workspace:* + version: link:../../tooling/prettier + '@forge/tsconfig': + specifier: workspace:* + version: link:../../tooling/typescript + eslint: + specifier: 'catalog:' + version: 9.39.2(jiti@2.6.1) + prettier: + specifier: 'catalog:' + version: 3.8.1 + typescript: + specifier: 'catalog:' + version: 5.7.3 + packages/validators: dependencies: minimatch: @@ -1195,7 +1216,7 @@ importers: version: 14.2.35 eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: specifier: ^6.10.0 version: 6.10.2(eslint@9.39.2(jiti@2.6.1)) @@ -10084,6 +10105,11 @@ snapshots: '@esbuild/win32-x64@0.19.12': optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@1.21.7))': + dependencies: + eslint: 9.39.2(jiti@1.21.7) + eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': dependencies: eslint: 9.39.2(jiti@2.6.1) @@ -13472,6 +13498,33 @@ snapshots: - eslint-import-resolver-webpack - supports-color + eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.2(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.6.1)): dependencies: aria-query: 5.3.2 @@ -13536,6 +13589,47 @@ snapshots: eslint-visitor-keys@4.2.1: {} + eslint@9.39.2(jiti@1.21.7): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 1.21.7 + transitivePeerDependencies: + - supports-color + eslint@9.39.2(jiti@2.6.1): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) @@ -13772,7 +13866,7 @@ snapshots: - encoding - supports-color - geist@1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + geist@1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: next: 14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) From c8456d589168cd1c7942e0d7b59b6229cc12a91f Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:59:37 -0500 Subject: [PATCH 02/27] feat(utils): migrate over discord from api --- packages/utils/package.json | 10 +- packages/utils/src/discord.ts | 167 ++++++++++++++++++++++++++++++++++ packages/utils/src/env.ts | 12 +++ packages/utils/src/index.ts | 1 + pnpm-lock.yaml | 19 ++++ 5 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 packages/utils/src/discord.ts create mode 100644 packages/utils/src/env.ts diff --git a/packages/utils/package.json b/packages/utils/package.json index 1119fa157..42526f420 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -16,6 +16,9 @@ "typecheck": "tsc --noEmit --emitDeclarationOnly false" }, "devDependencies": { + "@forge/auth": "workspace:*", + "@forge/consts": "workspace:*", + "@forge/db": "workspace:*", "@forge/eslint-config": "workspace:*", "@forge/prettier-config": "workspace:*", "@forge/tsconfig": "workspace:*", @@ -23,5 +26,10 @@ "prettier": "catalog:", "typescript": "catalog:" }, - "prettier": "@forge/prettier-config" + "prettier": "@forge/prettier-config", + "dependencies": { + "@t3-oss/env-nextjs": "^0.11.1", + "discord-api-types": "^0.37.113", + "discord.js": "^14.16.3" + } } diff --git a/packages/utils/src/discord.ts b/packages/utils/src/discord.ts new file mode 100644 index 000000000..a635e73a2 --- /dev/null +++ b/packages/utils/src/discord.ts @@ -0,0 +1,167 @@ +// +// Discord utils package. Holds all of the routes as well as the discord rest +// api client. +// + +import type { APIGuildMember } from "discord-api-types/v10"; +import { REST, Routes } from "discord.js"; +import { and, desc, eq } from "drizzle-orm"; +import Stripe from "stripe"; + +import type { Session } from "@forge/auth/server"; +import { DISCORD } from "@forge/consts"; +import { db } from "@forge/db/client"; +import { Account } from "@forge/db/schemas/auth"; + +import { env } from "./env"; + +export const api = new REST({ version: "10" }).setToken( + env.DISCORD_BOT_TOKEN, +); + +export async function addRoleToMember(discordUserId: string, roleId: string) { + await api.put( + Routes.guildMemberRole(DISCORD.KNIGHTHACKS_GUILD, discordUserId, roleId), + ); +} + +export async function removeRoleFromMember( + discordUserId: string, + roleId: string, +) { + await api.delete( + Routes.guildMemberRole(DISCORD.KNIGHTHACKS_GUILD, discordUserId, roleId), + ); +} + +export async function addMemberToServer( + discordUserId: string, + accessToken: string, +): Promise { + try { + await api.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 api.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 api.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 isDiscordMember = async (user: Session["user"]) => { + try { + await api.get( + Routes.guildMember(DISCORD.KNIGHTHACKS_GUILD, user.discordUserId), + ); + return true; + } catch { + return false; + } +}; + +export async function isDiscordVIP(discordUserId: string) { + const guildMember = (await api.get( + Routes.guildMember(DISCORD.KNIGHTHACKS_GUILD, discordUserId), + )) as APIGuildMember; + return guildMember.roles.includes(DISCORD.VIP_ROLE); +} + +export async function log({ + title, + message, + color, + userId, +}: { + title: string; + message: string; + color: "tk_blue" | "blade_purple" | "uhoh_red" | "success_green"; + userId: string; +}) { + await api.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(), + }, + }, + ], + }, + }); +} diff --git a/packages/utils/src/env.ts b/packages/utils/src/env.ts new file mode 100644 index 000000000..346317191 --- /dev/null +++ b/packages/utils/src/env.ts @@ -0,0 +1,12 @@ +import { createEnv } from "@t3-oss/env-nextjs"; // TODO: look into not using the nextjs version +import { z } from "zod"; + +export const env = createEnv({ + server: { + DISCORD_BOT_TOKEN: z.string(), + STRIPE_SECRET_KEY: z.string(), + }, + experimental__runtimeEnv: {}, + skipValidation: + !!process.env.CI || process.env.npm_lifecycle_event === "lint", +}); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 000c25319..59022da0a 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1 +1,2 @@ +export * as discord from "./discord"; export const name = "utils"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7294ede07..97222e1af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1155,7 +1155,26 @@ importers: version: 3.25.76 packages/utils: + dependencies: + '@t3-oss/env-nextjs': + specifier: ^0.11.1 + version: 0.11.1(typescript@5.7.3)(zod@4.3.6) + discord-api-types: + specifier: ^0.37.113 + version: 0.37.120 + discord.js: + specifier: ^14.16.3 + version: 14.25.1 devDependencies: + '@forge/auth': + specifier: workspace:* + version: link:../auth + '@forge/consts': + specifier: workspace:* + version: link:../consts + '@forge/db': + specifier: workspace:* + version: link:../db '@forge/eslint-config': specifier: workspace:* version: link:../../tooling/eslint From 8b191a69a70a174f8d96d4b2269fcc467c9c1b44 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:02:02 -0500 Subject: [PATCH 03/27] feat(utils): remove discord from api utils --- packages/api/src/env.ts | 1 - packages/api/src/utils.ts | 211 +------------------------------------- 2 files changed, 3 insertions(+), 209 deletions(-) diff --git a/packages/api/src/env.ts b/packages/api/src/env.ts index 067a42c7c..f57d234cf 100644 --- a/packages/api/src/env.ts +++ b/packages/api/src/env.ts @@ -4,7 +4,6 @@ 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(), diff --git a/packages/api/src/utils.ts b/packages/api/src/utils.ts index aac26ac8d..36027cd1d 100644 --- a/packages/api/src/utils.ts +++ b/packages/api/src/utils.ts @@ -1,126 +1,20 @@ -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 { and, eq, gt } 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 { EVENTS, FORMS, MINIO, PERMISSIONS } from "@forge/consts"; import { db } from "@forge/db/client"; -import { Account, JudgeSession, Roles } from "@forge/db/schemas/auth"; +import { JudgeSession } 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, @@ -129,55 +23,6 @@ export const hasPermission = ( 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: { @@ -210,24 +55,6 @@ export const controlPerms = { }, }; -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, @@ -262,38 +89,6 @@ export const sendEmail = async ({ } }; -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; From b943dfb84422bbfcdee98039c57d1c789060155f Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:06:06 -0500 Subject: [PATCH 04/27] feat(utils): migrate over discord stuff for seed script TODO: look into moving this into some other package. IMO: `scripts` --- packages/db/scripts/seed_devdb.ts | 32 +++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/db/scripts/seed_devdb.ts b/packages/db/scripts/seed_devdb.ts index ae8126bac..9ee221d8b 100644 --- a/packages/db/scripts/seed_devdb.ts +++ b/packages/db/scripts/seed_devdb.ts @@ -2,7 +2,19 @@ // Usage: // pnpm --filter @forge/db with-env tsx scripts/seed_devdb.ts -// A script to be run on prod only, this will take the prod db and make a backup sql script to insert all rows that don't have sensitive user data. It will only keep data from our admin members and delete any judging data/other sensitive data. It will also take all the server specific discord IDs in the DB and then sync them up with an event/role in the dev server and change the ID in the db for the local version. This sql file is uploaded to our minio client to be pulled by the get_prod_db.ts script. There's no realistic reason for this script to ever be ran on dev unless you're updating it cause I probably messed a lot up :D. See get_prod_db.ts for how to get prod data into your local db for deving. +// A script to be run on prod only, this will take the prod db and make a +// backup sql script to insert all rows that don't have sensitive user data. It +// will only keep data from our admin members and delete any judging data/other +// sensitive data. It will also take all the server specific discord IDs in the +// DB and then sync them up with an event/role in the dev server and change the +// ID in the db for the local version. This sql file is uploaded to our minio +// client to be pulled by the get_prod_db.ts script. There's no realistic +// reason for this script to ever be ran on dev unless you're updating it cause +// I probably messed a lot up :D. See get_prod_db.ts for how to get prod data +// into your local db for deving. + +// TODO: look into moving into a separate area so we don't have to do the BS +// that we do with `../../api` and `../../utils` import { exec } from "child_process"; import { unlink } from "fs/promises"; @@ -16,9 +28,9 @@ import Pool from "pg-pool"; import { stringify } from "superjson"; import { DISCORD, MINIO } from "@forge/consts"; +import { discord } from "../../utils/src"; import { minioClient } from "../../api/src/minio/minio-client"; -import { discord, log } from "../../api/src/utils"; import { env } from "../src/env"; import * as authSchema from "../src/schemas/auth"; import * as knightHacksSchema from "../src/schemas/knight-hacks"; @@ -228,12 +240,12 @@ async function syncRoles() { await backupDb.query.Roles.findMany({ columns: { discordRoleId: true } }) ).map((row) => row.discordRoleId), ); - let prodRoles = (await discord.get( + let prodRoles = (await discord.api.get( Routes.guildRoles(DISCORD.PROD_KNIGHTHACKS_GUILD), )) as DiscordRole[]; prodRoles = prodRoles.filter((role) => prodRolesWithPerms.has(role.id)); - const devRolesArr = (await discord.get( + const devRolesArr = (await discord.api.get( Routes.guildRoles(DISCORD.DEV_KNIGHTHACKS_GUILD), )) as DiscordRole[]; const devRoles = Object.fromEntries( @@ -246,7 +258,7 @@ async function syncRoles() { roleIdMappings[role.id] = devRoles[hash].id; } else { await new Promise((resolve) => setTimeout(resolve, 100)); - const newRole = (await discord.post( + const newRole = (await discord.api.post( Routes.guildRoles(DISCORD.DEV_KNIGHTHACKS_GUILD), { body: { @@ -288,11 +300,11 @@ interface DiscordGuildScheduledEvent { async function syncEvents() { if (!backupDb) return; - const prodEvents = (await discord.get( + const prodEvents = (await discord.api.get( Routes.guildScheduledEvents(DISCORD.PROD_KNIGHTHACKS_GUILD), )) as DiscordGuildScheduledEvent[]; - const devEventsArr = (await discord.get( + const devEventsArr = (await discord.api.get( Routes.guildScheduledEvents(DISCORD.DEV_KNIGHTHACKS_GUILD), )) as DiscordGuildScheduledEvent[]; const devEvents = Object.fromEntries( @@ -305,7 +317,7 @@ async function syncEvents() { eventIdMappings[event.id] = devEvents[hash].id; } else { await new Promise((resolve) => setTimeout(resolve, 100)); - const newEvent = (await discord.post( + const newEvent = (await discord.api.post( Routes.guildScheduledEvents(DISCORD.DEV_KNIGHTHACKS_GUILD), { body: { @@ -415,7 +427,7 @@ async function main() { console.log("Cleaning up backup db"); await cleanUp(); - await log({ + await discord.log({ title: `Successfully saved limited prod db to minio`, message: `Successfully saved limited prod db to minio. Run the get_prod_db.ts script to get it into your local dev db.`, color: "success_green", @@ -425,7 +437,7 @@ async function main() { process.exit(0); } catch (error) { console.error("Error during database seeding:", error); - await log({ + await discord.log({ title: `Failed to save limited prod db to minio`, message: `Failed to sav limited prod db to minio. Error: ${stringify(error)}`, color: "uhoh_red", From a387a2c01ea7c09dd163faebbc6a36ca2be68ecb Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:12:30 -0500 Subject: [PATCH 05/27] feat(utils): migrate over auth to use api as well --- packages/auth/src/config.ts | 4 ++-- packages/utils/src/discord.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/auth/src/config.ts b/packages/auth/src/config.ts index 28a11f9cf..43c425368 100644 --- a/packages/auth/src/config.ts +++ b/packages/auth/src/config.ts @@ -7,7 +7,7 @@ import { eq } from "drizzle-orm"; import { db } from "@forge/db/client"; import { Account, Session, User, Verifications } from "@forge/db/schemas/auth"; -import { handleDiscordOAuthCallback } from "../../api/src/utils"; +import { discord } from "../../utils/src"; import { env } from "./env"; export const isSecureContext = env.NODE_ENV !== "development"; @@ -72,7 +72,7 @@ export const auth = betterAuth({ const discordUserId = user?.discordUserId; if (!discordUserId) return; - void handleDiscordOAuthCallback(discordUserId); + await discord.handleDiscordOAuthCallback(discordUserId); } catch (error) { // eslint-disable-next-line no-console console.error("Error in Discord auto join hook:", error); diff --git a/packages/utils/src/discord.ts b/packages/utils/src/discord.ts index a635e73a2..ddbf97ab6 100644 --- a/packages/utils/src/discord.ts +++ b/packages/utils/src/discord.ts @@ -104,6 +104,9 @@ export async function resolveDiscordUserId( export const stripe = new Stripe(env.STRIPE_SECRET_KEY, { typescript: true }); +// TODO: look into not using Session here so we can remove the auth import +// which will let us clean up our imports. + export const isDiscordAdmin = async (user: Session["user"]) => { try { const guildMember = (await api.get( From fb45249bb7f20d0daad81c3607c696553ec12995 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:42:17 -0500 Subject: [PATCH 06/27] feat(utils): move over discord from api package --- packages/api/package.json | 1 + packages/api/src/routers/auth.ts | 7 +- packages/api/src/routers/dues-payment.ts | 5 +- packages/api/src/routers/event-feedback.ts | 4 +- packages/api/src/routers/event.ts | 19 ++-- packages/api/src/routers/forms.ts | 16 +-- packages/api/src/routers/hackers/mutations.ts | 39 +++---- packages/api/src/routers/member.ts | 19 ++-- packages/api/src/routers/misc.ts | 104 +++++++++--------- packages/api/src/routers/roles.ts | 40 +++---- packages/api/src/trpc.ts | 9 +- packages/utils/src/discord.ts | 3 - packages/utils/src/index.ts | 2 + packages/utils/src/permissions.ts | 0 packages/utils/src/stripe.ts | 5 + pnpm-lock.yaml | 3 + 16 files changed, 136 insertions(+), 140 deletions(-) create mode 100644 packages/utils/src/permissions.ts create mode 100644 packages/utils/src/stripe.ts diff --git a/packages/api/package.json b/packages/api/package.json index f9a9b997c..8b4ee4fc0 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -32,6 +32,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/routers/auth.ts b/packages/api/src/routers/auth.ts index 9b0dc6e34..be9871714 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 { discord } from "@forge/utils"; import { protectedProcedure, publicProcedure } from "../trpc"; -import { isDiscordAdmin, isDiscordMember, isJudgeAdmin } from "../utils"; +import { isJudgeAdmin } from "../utils"; export const authRouter = { getSession: publicProcedure.query(({ ctx }) => { @@ -26,13 +27,13 @@ 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(); diff --git a/packages/api/src/routers/dues-payment.ts b/packages/api/src/routers/dues-payment.ts index 5d60e4075..9d6de32f5 100644 --- a/packages/api/src/routers/dues-payment.ts +++ b/packages/api/src/routers/dues-payment.ts @@ -7,10 +7,11 @@ 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 { discord, stripe } from "@forge/utils"; import { env } from "../env"; import { permProcedure, protectedProcedure } from "../trpc"; -import { controlPerms, log, stripe } from "../utils"; +import { controlPerms } from "../utils"; export const duesPaymentRouter = { createCheckout: protectedProcedure.mutation(async ({ ctx }) => { @@ -80,7 +81,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", diff --git a/packages/api/src/routers/event-feedback.ts b/packages/api/src/routers/event-feedback.ts index 71e10a8fc..8f44d18a6 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 { discord } from "@forge/utils"; 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..053a6df80 100644 --- a/packages/api/src/routers/event.ts +++ b/packages/api/src/routers/event.ts @@ -29,9 +29,10 @@ import { InsertEventSchema, Member, } from "@forge/db/schemas/knight-hacks"; +import { discord } from "@forge/utils"; import { permProcedure, protectedProcedure, publicProcedure } from "../trpc"; -import { calendar, controlPerms, createForm, discord, log } from "../utils"; +import { calendar, controlPerms, createForm } from "../utils"; export const eventRouter = { getEvents: publicProcedure.query(async () => { @@ -205,7 +206,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: { @@ -256,7 +257,7 @@ export const eventRouter = { // 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, @@ -307,7 +308,7 @@ export const eventRouter = { // 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, @@ -334,7 +335,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", @@ -399,7 +400,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, @@ -518,7 +519,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", @@ -563,7 +564,7 @@ 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, @@ -592,7 +593,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", diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index 86c5195b3..216226552 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -20,6 +20,7 @@ import { TrpcFormConnection, TrpcFormConnectionSchema, } from "@forge/db/schemas/knight-hacks"; +import { discord } from "@forge/utils"; import { minioClient } from "../minio/minio-client"; import { permProcedure, protectedProcedure } from "../trpc"; @@ -28,7 +29,6 @@ import { createForm, CreateFormSchema, generateJsonSchema, - log, regenerateMediaUrls, } from "../utils"; @@ -441,7 +441,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", @@ -549,7 +549,7 @@ export const formsRouter = { 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", @@ -924,7 +924,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", @@ -952,7 +952,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", @@ -1057,7 +1057,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", @@ -1163,7 +1163,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", @@ -1221,7 +1221,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", diff --git a/packages/api/src/routers/hackers/mutations.ts b/packages/api/src/routers/hackers/mutations.ts index fdab3085d..59e996bbe 100644 --- a/packages/api/src/routers/hackers/mutations.ts +++ b/packages/api/src/routers/hackers/mutations.ts @@ -19,13 +19,8 @@ import { import { minioClient } from "../../minio/minio-client"; import { permProcedure, protectedProcedure } from "../../trpc"; -import { - addRoleToMember, - controlPerms, - isDiscordVIP, - log, - resolveDiscordUserId, -} from "../../utils"; +import { controlPerms } from "../../utils"; +import { discord } from "@forge/utils"; export const hackerMutationRouter = { createHacker: protectedProcedure @@ -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}`, @@ -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", @@ -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,15 +589,15 @@ export const hackerMutationRouter = { }); if (!discordId) { - await log({ - title: "Discord role assign skipped", + await discord.log({ + title: "Discord role assign skipped", message: `Could not resolve Discord ID for "${hacker.discordUser}".`, color: "uhoh_red", userId: ctx.session.user.discordUserId, }); } else { try { - await addRoleToMember( + await discord.addRoleToMember( discordId, HACKATHONS.KNIGHT_HACKS_8.KH_EVENT_ROLE_ID, ); @@ -611,7 +606,7 @@ export const hackerMutationRouter = { ); // 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,7 +615,7 @@ 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", @@ -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", @@ -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", @@ -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/member.ts b/packages/api/src/routers/member.ts index eacd2344a..e0f26715b 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 { discord } from "@forge/utils"; import { minioClient } from "../minio/minio-client"; import { permProcedure, protectedProcedure } from "../trpc"; -import { controlPerms, log } from "../utils"; +import { controlPerms } from "../utils"; export const memberRouter = { createMember: protectedProcedure @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -679,7 +680,7 @@ export const memberRouter = { 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.", @@ -747,7 +748,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..58bedb0a3 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 { discord } from "@forge/utils"; 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/roles.ts b/packages/api/src/routers/roles.ts index c669d4e2b..edce1c479 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 { discord } from "@forge/utils"; import { permProcedure, protectedProcedure } from "../trpc"; -import { - addRoleToMember, - controlPerms, - discord, - getPermsAsList, - log, - removeRoleFromMember, -} from "../utils"; +import { controlPerms, getPermsAsList } from "../utils"; export const rolesRouter = { // ROLES @@ -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,7 +92,7 @@ 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(", ")} @@ -107,7 +101,7 @@ export const rolesRouter = { 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(", ")} @@ -160,7 +154,7 @@ 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} @@ -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; }, @@ -344,7 +338,7 @@ 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); + await discord.addRoleToMember(user.discordUserId, role.discordRoleId); console.log( `Successfully added Discord role ${role.discordRoleId} to user ${user.discordUserId}`, ); @@ -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", @@ -407,7 +401,7 @@ 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); + await discord.removeRoleFromMember(user.discordUserId, role.discordRoleId); console.log( `āœ… Successfully removed Discord role ${role.discordRoleId} from user ${user.discordUserId}`, ); @@ -423,7 +417,7 @@ export const rolesRouter = { 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", @@ -494,7 +488,7 @@ 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, ); @@ -512,7 +506,7 @@ 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, ); @@ -545,7 +539,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/trpc.ts b/packages/api/src/trpc.ts index a82242edc..82072e9db 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -16,12 +16,9 @@ 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 { discord } from "@forge/utils"; -import { - getJudgeSessionFromCookie, - isDiscordAdmin, - isJudgeAdmin, -} from "./utils"; +import { getJudgeSessionFromCookie, isJudgeAdmin } from "./utils"; /** * 1. CONTEXT @@ -183,7 +180,7 @@ 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(); diff --git a/packages/utils/src/discord.ts b/packages/utils/src/discord.ts index ddbf97ab6..76c023c3d 100644 --- a/packages/utils/src/discord.ts +++ b/packages/utils/src/discord.ts @@ -6,7 +6,6 @@ import type { APIGuildMember } from "discord-api-types/v10"; import { REST, Routes } from "discord.js"; import { and, desc, eq } from "drizzle-orm"; -import Stripe from "stripe"; import type { Session } from "@forge/auth/server"; import { DISCORD } from "@forge/consts"; @@ -102,8 +101,6 @@ export async function resolveDiscordUserId( return members[0]?.user.id ?? null; } -export const stripe = new Stripe(env.STRIPE_SECRET_KEY, { typescript: true }); - // TODO: look into not using Session here so we can remove the auth import // which will let us clean up our imports. diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 59022da0a..287c7d34f 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,2 +1,4 @@ export * as discord from "./discord"; +export { stripe } from "./stripe"; + export const name = "utils"; diff --git a/packages/utils/src/permissions.ts b/packages/utils/src/permissions.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/utils/src/stripe.ts b/packages/utils/src/stripe.ts new file mode 100644 index 000000000..1b3c63a85 --- /dev/null +++ b/packages/utils/src/stripe.ts @@ -0,0 +1,5 @@ +import Stripe from "stripe"; + +import { env } from "./env"; + +export const stripe = new Stripe(env.STRIPE_SECRET_KEY, { typescript: true }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97222e1af..05ccd7082 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -766,6 +766,9 @@ importers: '@forge/email': specifier: workspace:^ version: link:../email + '@forge/utils': + specifier: workspace:* + version: link:../utils '@forge/validators': specifier: workspace:* version: link:../validators From c06e62de15af4d0912894f69ab6eadd39e1bf91b Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:48:21 -0500 Subject: [PATCH 07/27] feat(utils): migrate the rest of discord utils --- apps/blade/package.json | 1 + .../app/_components/forms/connection-handler.ts | 6 +++--- apps/cron/package.json | 1 + apps/cron/src/crons/alumni-assign.ts | 15 ++++++--------- apps/cron/src/crons/leetcode.ts | 15 +++++++++------ apps/cron/src/crons/role-sync.ts | 4 ++-- packages/auth/src/config.ts | 1 - pnpm-lock.yaml | 6 ++++++ 8 files changed, 28 insertions(+), 21 deletions(-) diff --git a/apps/blade/package.json b/apps/blade/package.json index 0b9eec8ea..25cf77499 100644 --- a/apps/blade/package.json +++ b/apps/blade/package.json @@ -26,6 +26,7 @@ "@forge/db": "workspace:*", "@forge/email": "workspace:^", "@forge/ui": "workspace:*", + "@forge/utils": "workspace:*", "@forge/validators": "workspace:*", "@react-email/render": "1.1.0", "@stripe/react-stripe-js": "^3.0.0", diff --git a/apps/blade/src/app/_components/forms/connection-handler.ts b/apps/blade/src/app/_components/forms/connection-handler.ts index 6ecd7a6cd..87647dae6 100644 --- a/apps/blade/src/app/_components/forms/connection-handler.ts +++ b/apps/blade/src/app/_components/forms/connection-handler.ts @@ -3,8 +3,8 @@ import { stringify } from "superjson"; import { appRouter } from "@forge/api"; -import { log } from "@forge/api/utils"; import { auth } from "@forge/auth/server"; +import { discord } from "@forge/utils"; import { extractProcedures } from "~/lib/utils"; import { api } from "~/trpc/server"; @@ -46,7 +46,7 @@ export const handleCallbacks = async ( try { await proc(data); - await log({ + await discord.log({ title: `Successfully automatically fired procedure`, message: `**Successfully fired procedure**\n\`${con.proc}\`\n\nTriggered after **${name}** submission from **${session.user.name}**`, color: "success_green", @@ -54,7 +54,7 @@ export const handleCallbacks = async ( }); } catch (error) { const errorMessage = JSON.stringify(error, null, 2); - await log({ + await discord.log({ title: `Failed to automatically fire procedure`, message: `**Failed to fire procedure**\n\`${con.proc}\`\n\nTriggered after **${name}** submission from **${session.user.name}**\n\n**Data:**\n\`\`\`json\n${stringify(data)}\`\`\`` + 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/alumni-assign.ts b/apps/cron/src/crons/alumni-assign.ts index bc017b3a0..7ec1c916e 100644 --- a/apps/cron/src/crons/alumni-assign.ts +++ b/apps/cron/src/crons/alumni-assign.ts @@ -1,13 +1,9 @@ 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 { discord } from "@forge/utils"; import { CronBuilder } from "../structs/CronBuilder"; @@ -27,8 +23,9 @@ 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); } @@ -49,9 +46,9 @@ 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); } diff --git a/apps/cron/src/crons/leetcode.ts b/apps/cron/src/crons/leetcode.ts index 2c839badb..2183d4dd6 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 { discord } from "@forge/utils"; 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/role-sync.ts b/apps/cron/src/crons/role-sync.ts index 02946a487..9744b6332 100644 --- a/apps/cron/src/crons/role-sync.ts +++ b/apps/cron/src/crons/role-sync.ts @@ -1,11 +1,11 @@ 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 { discord } from "@forge/utils"; import { CronBuilder } from "../structs/CronBuilder"; @@ -40,7 +40,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; diff --git a/packages/auth/src/config.ts b/packages/auth/src/config.ts index 43c425368..14a0909ef 100644 --- a/packages/auth/src/config.ts +++ b/packages/auth/src/config.ts @@ -74,7 +74,6 @@ export const auth = betterAuth({ await discord.handleDiscordOAuthCallback(discordUserId); } catch (error) { - // eslint-disable-next-line no-console console.error("Error in Discord auto join hook:", error); } }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05ccd7082..a27a0cc72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -199,6 +199,9 @@ importers: '@forge/ui': specifier: workspace:* version: link:../../packages/ui + '@forge/utils': + specifier: workspace:* + version: link:../../packages/utils '@forge/validators': specifier: workspace:* version: link:../../packages/validators @@ -426,6 +429,9 @@ importers: '@forge/db': specifier: workspace:* version: link:../../packages/db + '@forge/utils': + specifier: workspace:* + version: link:../../packages/utils '@forge/validators': specifier: workspace:* version: link:../../packages/validators From 35841f4182615071fd7287a43883bf28dd44d353 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:20:04 -0500 Subject: [PATCH 08/27] feat(utils): move over permissons --- apps/blade/src/app/judge/session/route.ts | 4 +- packages/api/src/routers/auth.ts | 5 +- packages/api/src/routers/csv-importer.ts | 4 +- packages/api/src/routers/dues-payment.ts | 5 +- packages/api/src/routers/email.ts | 5 +- packages/api/src/routers/event.ts | 16 +-- packages/api/src/routers/forms.ts | 43 ++++---- packages/api/src/routers/hackers/mutations.ts | 11 +- .../api/src/routers/hackers/pagination.ts | 8 +- packages/api/src/routers/hackers/queries.ts | 8 +- packages/api/src/routers/judge.ts | 6 +- packages/api/src/routers/member.ts | 27 +++-- packages/api/src/routers/roles.ts | 26 ++--- packages/api/src/routers/user.ts | 4 +- packages/api/src/trpc.ts | 8 +- packages/api/src/utils.ts | 102 +---------------- packages/utils/src/index.ts | 2 + packages/utils/src/logger.ts | 2 + packages/utils/src/permissions.ts | 104 ++++++++++++++++++ 19 files changed, 196 insertions(+), 194 deletions(-) create mode 100644 packages/utils/src/logger.ts diff --git a/apps/blade/src/app/judge/session/route.ts b/apps/blade/src/app/judge/session/route.ts index 757a18ca6..c32cd0d44 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 { permissions } from "@forge/utils"; export async function GET() { - const row = await getJudgeSessionFromCookie(); + const row = await permissions.getJudgeSessionFromCookie(); if (!row) return NextResponse.json({ ok: false }, { status: 401 }); return NextResponse.json({ ok: true, roomName: row.roomName }); } diff --git a/packages/api/src/routers/auth.ts b/packages/api/src/routers/auth.ts index be9871714..4bc2a93cd 100644 --- a/packages/api/src/routers/auth.ts +++ b/packages/api/src/routers/auth.ts @@ -1,10 +1,9 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { invalidateSessionToken } from "@forge/auth/server"; -import { discord } from "@forge/utils"; +import { discord, permissions } from "@forge/utils"; import { protectedProcedure, publicProcedure } from "../trpc"; -import { isJudgeAdmin } from "../utils"; export const authRouter = { getSession: publicProcedure.query(({ ctx }) => { @@ -36,7 +35,7 @@ export const authRouter = { return discord.isDiscordMember(ctx.session.user); }), getJudgeStatus: publicProcedure.query(async () => { - const isJudge = await isJudgeAdmin(); + const isJudge = await permissions.isJudgeAdmin(); return isJudge; }), diff --git a/packages/api/src/routers/csv-importer.ts b/packages/api/src/routers/csv-importer.ts index d5a7aca74..44fce5a47 100644 --- a/packages/api/src/routers/csv-importer.ts +++ b/packages/api/src/routers/csv-importer.ts @@ -7,7 +7,7 @@ import { db } from "@forge/db/client"; import { Challenges, Submissions, Teams } from "@forge/db/schemas/knight-hacks"; import { permProcedure } from "../trpc"; -import { controlPerms } from "../utils"; +import { permissions } from "@forge/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 diff --git a/packages/api/src/routers/dues-payment.ts b/packages/api/src/routers/dues-payment.ts index 9d6de32f5..7d57f7720 100644 --- a/packages/api/src/routers/dues-payment.ts +++ b/packages/api/src/routers/dues-payment.ts @@ -7,11 +7,10 @@ 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 { discord, stripe } from "@forge/utils"; +import { discord, permissions, stripe } from "@forge/utils"; import { env } from "../env"; import { permProcedure, protectedProcedure } from "../trpc"; -import { controlPerms } from "../utils"; export const duesPaymentRouter = { createCheckout: protectedProcedure.mutation(async ({ ctx }) => { @@ -97,7 +96,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..f54908df0 100644 --- a/packages/api/src/routers/email.ts +++ b/packages/api/src/routers/email.ts @@ -2,7 +2,8 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { z } from "zod"; import { permProcedure } from "../trpc"; -import { controlPerms, sendEmail } from "../utils"; +import { sendEmail } from "../utils"; +import { permissions } from "@forge/utils"; export const emailRouter = { sendEmail: permProcedure @@ -16,7 +17,7 @@ export const emailRouter = { }), ) .mutation(async ({ input, ctx }) => { - controlPerms.or(["EMAIL_PORTAL"], ctx); + permissions.controlPerms.or(["EMAIL_PORTAL"], ctx); console.log(input.data); try { const response = await sendEmail({ diff --git a/packages/api/src/routers/event.ts b/packages/api/src/routers/event.ts index 053a6df80..b9178ae0e 100644 --- a/packages/api/src/routers/event.ts +++ b/packages/api/src/routers/event.ts @@ -29,10 +29,10 @@ import { InsertEventSchema, Member, } from "@forge/db/schemas/knight-hacks"; -import { discord } from "@forge/utils"; +import { discord, permissions } from "@forge/utils"; import { permProcedure, protectedProcedure, publicProcedure } from "../trpc"; -import { calendar, controlPerms, createForm } from "../utils"; +import { calendar, createForm } from "../utils"; export const eventRouter = { getEvents: publicProcedure.query(async () => { @@ -127,7 +127,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({ @@ -143,7 +143,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({ @@ -168,7 +168,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); @@ -346,7 +346,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({ @@ -553,7 +553,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({ @@ -700,7 +700,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 216226552..b2bfa4992 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -20,12 +20,11 @@ import { TrpcFormConnection, TrpcFormConnectionSchema, } from "@forge/db/schemas/knight-hacks"; -import { discord } from "@forge/utils"; +import { discord, permissions } from "@forge/utils"; import { minioClient } from "../minio/minio-client"; import { permProcedure, protectedProcedure } from "../trpc"; import { - controlPerms, createForm, CreateFormSchema, generateJsonSchema, @@ -36,7 +35,7 @@ export const formsRouter = { createForm: permProcedure .input(CreateFormSchema) .mutation(async ({ input, ctx }) => { - controlPerms.or(["EDIT_FORMS"], ctx); + permissions.controlPerms.or(["EDIT_FORMS"], ctx); await createForm(input); }), @@ -53,7 +52,7 @@ export const formsRouter = { .extend({ responseRoleIds: z.array(z.string().uuid()).optional() }), ) .mutation(async ({ input, ctx }) => { - controlPerms.or(["EDIT_FORMS"], ctx); + permissions.controlPerms.or(["EDIT_FORMS"], ctx); const jsonSchema = generateJsonSchema(input.formData); const slug_name = input.formData.name.toLowerCase().replaceAll(" ", "-"); @@ -201,7 +200,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 +233,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 +284,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 +326,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) @@ -524,7 +523,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, @@ -546,7 +545,7 @@ 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 discord.log({ @@ -709,7 +708,7 @@ export const formsRouter = { }), ) .mutation(async ({ input, ctx }) => { - controlPerms.or(["EDIT_FORMS"], ctx); + permissions.controlPerms.or(["EDIT_FORMS"], ctx); const { objectName } = input; try { @@ -734,7 +733,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 { @@ -754,7 +753,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; @@ -873,7 +872,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, @@ -896,7 +895,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)), @@ -940,7 +939,7 @@ export const formsRouter = { }), ) .mutation(async ({ input, ctx }) => { - controlPerms.or(["EDIT_FORMS"], ctx); + permissions.controlPerms.or(["EDIT_FORMS"], ctx); await db .update(FormSections) @@ -963,7 +962,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 }) @@ -982,7 +981,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), @@ -1068,7 +1067,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), @@ -1107,7 +1106,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), @@ -1179,7 +1178,7 @@ export const formsRouter = { }), ) .mutation(async ({ input, ctx }) => { - controlPerms.or(["EDIT_FORMS"], ctx); + permissions.controlPerms.or(["EDIT_FORMS"], ctx); const allSections = await db .select({ @@ -1232,7 +1231,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; diff --git a/packages/api/src/routers/hackers/mutations.ts b/packages/api/src/routers/hackers/mutations.ts index 59e996bbe..9a4d7af6a 100644 --- a/packages/api/src/routers/hackers/mutations.ts +++ b/packages/api/src/routers/hackers/mutations.ts @@ -19,8 +19,7 @@ import { import { minioClient } from "../../minio/minio-client"; import { permProcedure, protectedProcedure } from "../../trpc"; -import { controlPerms } from "../../utils"; -import { discord } from "@forge/utils"; +import { discord, permissions } from "@forge/utils"; export const hackerMutationRouter = { createHacker: protectedProcedure @@ -245,7 +244,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({ @@ -458,7 +457,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), @@ -717,7 +716,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({ @@ -795,7 +794,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({ diff --git a/packages/api/src/routers/hackers/pagination.ts b/packages/api/src/routers/hackers/pagination.ts index 2d215587a..c7593441a 100644 --- a/packages/api/src/routers/hackers/pagination.ts +++ b/packages/api/src/routers/hackers/pagination.ts @@ -5,7 +5,7 @@ import { db } from "@forge/db/client"; import { Hacker, HackerAttendee } from "@forge/db/schemas/knight-hacks"; import { permProcedure } from "../../trpc"; -import { controlPerms } from "../../utils"; +import { permissions } from "@forge/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 e0f26715b..a5006ac9d 100644 --- a/packages/api/src/routers/member.ts +++ b/packages/api/src/routers/member.ts @@ -28,11 +28,10 @@ import { Member, OtherCompanies, } from "@forge/db/schemas/knight-hacks"; -import { discord } from "@forge/utils"; +import { discord, permissions } from "@forge/utils"; import { minioClient } from "../minio/minio-client"; import { permProcedure, protectedProcedure } from "../trpc"; -import { controlPerms } from "../utils"; export const memberRouter = { createMember: protectedProcedure @@ -358,7 +357,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 ( @@ -455,7 +454,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 = []; @@ -501,7 +500,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) @@ -512,7 +511,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) @@ -522,7 +521,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({ @@ -557,7 +556,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), @@ -584,7 +583,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() @@ -600,7 +599,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 @@ -628,7 +627,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({ @@ -656,7 +655,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({ @@ -677,7 +676,7 @@ export const memberRouter = { }), clearAllDues: permProcedure.mutation(async ({ ctx }) => { - controlPerms.or(["IS_OFFICER"], ctx); + permissions.controlPerms.or(["IS_OFFICER"], ctx); await db.delete(DuesPayment); await discord.log({ @@ -698,7 +697,7 @@ 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), diff --git a/packages/api/src/routers/roles.ts b/packages/api/src/routers/roles.ts index edce1c479..52cc4e82b 100644 --- a/packages/api/src/routers/roles.ts +++ b/packages/api/src/routers/roles.ts @@ -11,7 +11,7 @@ import { Permissions, Roles, User } from "@forge/db/schemas/auth"; import { discord } from "@forge/utils"; import { permProcedure, protectedProcedure } from "../trpc"; -import { controlPerms, getPermsAsList } from "../utils"; +import { permissions } from "@forge/utils"; export const rolesRouter = { // ROLES @@ -25,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({ @@ -95,7 +95,7 @@ export const rolesRouter = { 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, @@ -104,7 +104,7 @@ export const rolesRouter = { 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, @@ -122,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({ @@ -158,8 +158,8 @@ export const rolesRouter = { 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, }); @@ -168,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({ @@ -294,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; } @@ -306,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 }) => @@ -368,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 }) => @@ -434,7 +434,7 @@ export const rolesRouter = { }), ) .mutation(async ({ input, ctx }) => { - controlPerms.or(["ASSIGN_ROLES"], ctx); + permissions.controlPerms.or(["ASSIGN_ROLES"], ctx); interface Return { roleName: string; diff --git a/packages/api/src/routers/user.ts b/packages/api/src/routers/user.ts index 0983aa0b1..3b19afcc2 100644 --- a/packages/api/src/routers/user.ts +++ b/packages/api/src/routers/user.ts @@ -3,7 +3,7 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { db } from "@forge/db/client"; import { permProcedure, protectedProcedure } from "../trpc"; -import { controlPerms } from "../utils"; +import { permissions } from "@forge/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 82072e9db..65e8a8408 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -16,9 +16,7 @@ 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 { discord } from "@forge/utils"; - -import { getJudgeSessionFromCookie, isJudgeAdmin } from "./utils"; +import { discord, permissions } from "@forge/utils"; /** * 1. CONTEXT @@ -182,13 +180,13 @@ export const judgeProcedure = publicProcedure.use(async ({ ctx, next }) => { if (ctx.session) { isAdmin = await discord.isDiscordAdmin(ctx.session.user); } - const isJudge = await isJudgeAdmin(); + const isJudge = await permissions.isJudgeAdmin(); if (!isAdmin && !isJudge) { throw new TRPCError({ code: "UNAUTHORIZED" }); } - const judgeSession = await getJudgeSessionFromCookie(); + const judgeSession = await permissions.getJudgeSessionFromCookie(); return next({ ctx: { diff --git a/packages/api/src/utils.ts b/packages/api/src/utils.ts index 36027cd1d..385aea4ad 100644 --- a/packages/api/src/utils.ts +++ b/packages/api/src/utils.ts @@ -1,60 +1,17 @@ import type { JSONSchema7 } from "json-schema"; -import { cookies } from "next/headers"; import { TRPCError } from "@trpc/server"; -import { and, eq, gt } from "drizzle-orm"; import { google } from "googleapis"; import z from "zod"; import type { Form } from "@forge/db/schemas/knight-hacks"; -import { EVENTS, FORMS, MINIO, PERMISSIONS } from "@forge/consts"; +import { EVENTS, FORMS, MINIO } from "@forge/consts"; import { db } from "@forge/db/client"; -import { JudgeSession } 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 hasPermission = ( - userPermissions: string, - permission: PERMISSIONS.PermissionIndex, -): boolean => { - const permissionBit = userPermissions[permission]; - return permissionBit === "1"; -}; - -// 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 sendEmail = async ({ to, subject, @@ -89,50 +46,6 @@ export const sendEmail = async ({ } }; -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"); @@ -335,19 +248,6 @@ export async function regenerateMediaUrls( 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, diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 287c7d34f..44ba77456 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,4 +1,6 @@ export * as discord from "./discord"; +export * as permissions from "./permissions"; +export { logger } from "./logger"; export { stripe } from "./stripe"; export const name = "utils"; diff --git a/packages/utils/src/logger.ts b/packages/utils/src/logger.ts new file mode 100644 index 000000000..03abcfb59 --- /dev/null +++ b/packages/utils/src/logger.ts @@ -0,0 +1,2 @@ +// TODO: implement a real logger +export const logger = console; diff --git a/packages/utils/src/permissions.ts b/packages/utils/src/permissions.ts index e69de29bb..4b25f7cb7 100644 --- a/packages/utils/src/permissions.ts +++ b/packages/utils/src/permissions.ts @@ -0,0 +1,104 @@ +import { cookies } from "next/headers"; +import { TRPCError } from "@trpc/server"; +import { and, eq, gt } from "drizzle-orm"; + +import { PERMISSIONS } from "@forge/consts"; +import { db } from "@forge/db/client"; +import { JudgeSession } from "@forge/db/schemas/auth"; + +export const hasPermission = ( + userPermissions: string, + permission: PERMISSIONS.PermissionIndex, +): boolean => { + const permissionBit = userPermissions[permission]; + return permissionBit === "1"; +}; + +// 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 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; +}; + +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; +} From ae29578841766544b32611b1d5ffb91deb5d1a21 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:31:16 -0500 Subject: [PATCH 09/27] feat(utils): move over date utils Added jsdocs from chat too. not sure if they're accurate --- apps/club/package.json | 1 + .../src/app/_components/landing/calendar.tsx | 6 ++- apps/club/src/lib/utils.ts | 18 --------- packages/utils/src/index.ts | 1 + packages/utils/src/time.ts | 40 +++++++++++++++++++ pnpm-lock.yaml | 3 ++ 6 files changed, 49 insertions(+), 20 deletions(-) delete mode 100644 apps/club/src/lib/utils.ts create mode 100644 packages/utils/src/time.ts 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/landing/calendar.tsx b/apps/club/src/app/_components/landing/calendar.tsx index edd01141f..496e3e014 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.formatDateRange(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/packages/utils/src/index.ts b/packages/utils/src/index.ts index 44ba77456..5477eff89 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,5 +1,6 @@ export * as discord from "./discord"; export * as permissions from "./permissions"; +export * as time from "./time"; export { logger } from "./logger"; export { stripe } from "./stripe"; diff --git a/packages/utils/src/time.ts b/packages/utils/src/time.ts new file mode 100644 index 000000000..acaffde0a --- /dev/null +++ b/packages/utils/src/time.ts @@ -0,0 +1,40 @@ +/** + * Formats a given date into a 12-hour time string with AM/PM. + * + * @param {Date} date - The date object to format. + * @returns {string} The formatted time in "h:mm am/pm" format. + * + * @example + * const date = new Date('2023-02-19T14:30:00'); + * console.log(formatHourTime(date)); // "2:30pm" + */ +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}`; +} + +/** + * Formats a date range (start and end date) into a readable time range string. + * + * @param {Date} startDate - The start date of the range. + * @param {Date} endDate - The end date of the range. + * @returns {string} The formatted time range in "h:mm am/pm - h:mm am/pm" format. + * + * @example + * const start = new Date('2023-02-19T09:00:00'); + * const end = new Date('2023-02-19T17:00:00'); + * console.log(formatDateRange(start, end)); // "9:00am - 5:00pm" + */ +export const formatDateRange = (startDate: Date, endDate: Date) => { + const start = formatHourTime(startDate); + const end = formatHourTime(endDate); + return `${start} - ${end}`; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a27a0cc72..de362d7e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -350,6 +350,9 @@ importers: '@forge/ui': specifier: workspace:* version: link:../../packages/ui + '@forge/utils': + specifier: workspace:* + version: link:../../packages/utils '@gsap/react': specifier: ^2.1.2 version: 2.1.2(gsap@3.14.2)(react@18.3.1) From 96c5766603636cf515f1eed2af45cf3ef30e73f6 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:35:18 -0500 Subject: [PATCH 10/27] chore(utils): lint and format --- packages/api/src/routers/csv-importer.ts | 2 +- packages/api/src/routers/email.ts | 3 ++- packages/api/src/routers/hackers/mutations.ts | 4 ++-- packages/api/src/routers/hackers/pagination.ts | 2 +- packages/api/src/routers/member.ts | 5 ++++- packages/api/src/routers/roles.ts | 8 +++++--- packages/api/src/routers/user.ts | 2 +- packages/db/scripts/seed_devdb.ts | 4 ++-- packages/utils/src/discord.ts | 4 +--- 9 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/api/src/routers/csv-importer.ts b/packages/api/src/routers/csv-importer.ts index 44fce5a47..deaa27422 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 { permissions } from "@forge/utils"; import { permProcedure } from "../trpc"; -import { permissions } from "@forge/utils"; interface CsvImporterRecord { "Opt-In Prize": string | null; diff --git a/packages/api/src/routers/email.ts b/packages/api/src/routers/email.ts index f54908df0..1a08bc87c 100644 --- a/packages/api/src/routers/email.ts +++ b/packages/api/src/routers/email.ts @@ -1,9 +1,10 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { z } from "zod"; +import { permissions } from "@forge/utils"; + import { permProcedure } from "../trpc"; import { sendEmail } from "../utils"; -import { permissions } from "@forge/utils"; export const emailRouter = { sendEmail: permProcedure diff --git a/packages/api/src/routers/hackers/mutations.ts b/packages/api/src/routers/hackers/mutations.ts index 9a4d7af6a..8fb5d73da 100644 --- a/packages/api/src/routers/hackers/mutations.ts +++ b/packages/api/src/routers/hackers/mutations.ts @@ -16,10 +16,10 @@ import { HackerEventAttendee, InsertHackerSchema, } from "@forge/db/schemas/knight-hacks"; +import { discord, permissions } from "@forge/utils"; import { minioClient } from "../../minio/minio-client"; import { permProcedure, protectedProcedure } from "../../trpc"; -import { discord, permissions } from "@forge/utils"; export const hackerMutationRouter = { createHacker: protectedProcedure @@ -589,7 +589,7 @@ export const hackerMutationRouter = { if (!discordId) { await discord.log({ - title: "Discord role assign skipped", + title: "Discord role assign skipped", message: `Could not resolve Discord ID for "${hacker.discordUser}".`, color: "uhoh_red", userId: ctx.session.user.discordUserId, diff --git a/packages/api/src/routers/hackers/pagination.ts b/packages/api/src/routers/hackers/pagination.ts index c7593441a..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 { permissions } from "@forge/utils"; const SOFT_BLACKLIST_HACKER_ID = "7f89fe4d-26f0-42fe-ac98-22d8f648d7a7"; diff --git a/packages/api/src/routers/member.ts b/packages/api/src/routers/member.ts index a5006ac9d..28606d0d3 100644 --- a/packages/api/src/routers/member.ts +++ b/packages/api/src/routers/member.ts @@ -697,7 +697,10 @@ export const memberRouter = { }), ) .mutation(async ({ input, ctx }) => { - permissions.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), diff --git a/packages/api/src/routers/roles.ts b/packages/api/src/routers/roles.ts index 52cc4e82b..926861908 100644 --- a/packages/api/src/routers/roles.ts +++ b/packages/api/src/routers/roles.ts @@ -8,10 +8,9 @@ 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 { discord } from "@forge/utils"; +import { discord, permissions } from "@forge/utils"; import { permProcedure, protectedProcedure } from "../trpc"; -import { permissions } from "@forge/utils"; export const rolesRouter = { // ROLES @@ -401,7 +400,10 @@ 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 discord.removeRoleFromMember(user.discordUserId, role.discordRoleId); + await discord.removeRoleFromMember( + user.discordUserId, + role.discordRoleId, + ); console.log( `āœ… Successfully removed Discord role ${role.discordRoleId} from user ${user.discordUserId}`, ); diff --git a/packages/api/src/routers/user.ts b/packages/api/src/routers/user.ts index 3b19afcc2..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 { permissions } from "@forge/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 diff --git a/packages/db/scripts/seed_devdb.ts b/packages/db/scripts/seed_devdb.ts index 9ee221d8b..6bae8c2d0 100644 --- a/packages/db/scripts/seed_devdb.ts +++ b/packages/db/scripts/seed_devdb.ts @@ -8,7 +8,7 @@ // sensitive data. It will also take all the server specific discord IDs in the // DB and then sync them up with an event/role in the dev server and change the // ID in the db for the local version. This sql file is uploaded to our minio -// client to be pulled by the get_prod_db.ts script. There's no realistic +// client to be pulled by the get_prod_db.ts script. There's no realistic // reason for this script to ever be ran on dev unless you're updating it cause // I probably messed a lot up :D. See get_prod_db.ts for how to get prod data // into your local db for deving. @@ -28,9 +28,9 @@ import Pool from "pg-pool"; import { stringify } from "superjson"; import { DISCORD, MINIO } from "@forge/consts"; -import { discord } from "../../utils/src"; import { minioClient } from "../../api/src/minio/minio-client"; +import { discord } from "../../utils/src"; import { env } from "../src/env"; import * as authSchema from "../src/schemas/auth"; import * as knightHacksSchema from "../src/schemas/knight-hacks"; diff --git a/packages/utils/src/discord.ts b/packages/utils/src/discord.ts index 76c023c3d..dae0dbdcf 100644 --- a/packages/utils/src/discord.ts +++ b/packages/utils/src/discord.ts @@ -14,9 +14,7 @@ import { Account } from "@forge/db/schemas/auth"; import { env } from "./env"; -export const api = new REST({ version: "10" }).setToken( - env.DISCORD_BOT_TOKEN, -); +export const api = new REST({ version: "10" }).setToken(env.DISCORD_BOT_TOKEN); export async function addRoleToMember(discordUserId: string, roleId: string) { await api.put( From 033d76dc2bfdd0ae25117dc220b5f370596ac0f2 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:37:40 -0500 Subject: [PATCH 11/27] feat(repo): fix pnpm-locl --- pnpm-lock.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 662483afe..29eb7f2e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11953,6 +11953,12 @@ snapshots: optionalDependencies: typescript: 5.7.3 + '@t3-oss/env-core@0.11.1(typescript@5.7.3)(zod@4.3.6)': + dependencies: + zod: 4.3.6 + optionalDependencies: + typescript: 5.7.3 + '@t3-oss/env-core@0.11.1(typescript@5.9.3)(zod@3.25.76)': dependencies: zod: 3.25.76 @@ -11972,6 +11978,13 @@ snapshots: optionalDependencies: typescript: 5.7.3 + '@t3-oss/env-nextjs@0.11.1(typescript@5.7.3)(zod@4.3.6)': + dependencies: + '@t3-oss/env-core': 0.11.1(typescript@5.7.3)(zod@4.3.6) + zod: 4.3.6 + optionalDependencies: + typescript: 5.7.3 + '@t3-oss/env-nextjs@0.11.1(typescript@5.9.3)(zod@3.25.76)': dependencies: '@t3-oss/env-core': 0.11.1(typescript@5.9.3)(zod@3.25.76) From b9d4ec1c5c1436ab96309c07aaf30c52ff9a55e0 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:52:57 -0500 Subject: [PATCH 12/27] chore(utils): test change --- packages/utils/package.json | 4 ++-- packages/utils/src/discord.ts | 3 ++- pnpm-lock.yaml | 38 +++++++++++++++++++++++------------ 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/packages/utils/package.json b/packages/utils/package.json index 42526f420..7dac64608 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -28,8 +28,8 @@ }, "prettier": "@forge/prettier-config", "dependencies": { + "@discordjs/rest": "^2.4.0", "@t3-oss/env-nextjs": "^0.11.1", - "discord-api-types": "^0.37.113", - "discord.js": "^14.16.3" + "discord-api-types": "^0.37.113" } } diff --git a/packages/utils/src/discord.ts b/packages/utils/src/discord.ts index dae0dbdcf..73b10a58c 100644 --- a/packages/utils/src/discord.ts +++ b/packages/utils/src/discord.ts @@ -4,7 +4,8 @@ // import type { APIGuildMember } from "discord-api-types/v10"; -import { REST, Routes } from "discord.js"; +import { REST } from "@discordjs/rest"; +import { Routes } from "discord-api-types/v10"; import { and, desc, eq } from "drizzle-orm"; import type { Session } from "@forge/auth/server"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29eb7f2e6..fa69f389d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -243,7 +243,7 @@ importers: version: 6.6.0 geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) google-auth-library: specifier: ^9.15.0 version: 9.15.1 @@ -364,7 +364,7 @@ importers: version: 12.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) gsap: specifier: ^3.12.7 version: 3.14.2 @@ -410,7 +410,7 @@ importers: version: 7.4.4 eslint: specifier: 'catalog:' - version: 9.39.2(jiti@1.21.7) + version: 9.39.2(jiti@2.6.1) prettier: specifier: 'catalog:' version: 3.8.1 @@ -540,7 +540,7 @@ importers: version: 12.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) gsap: specifier: ^3.12.7 version: 3.14.2 @@ -631,7 +631,7 @@ importers: version: 12.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) gsap: specifier: ^3.12.7 version: 3.14.2 @@ -1168,15 +1168,15 @@ importers: packages/utils: dependencies: + '@discordjs/rest': + specifier: ^2.4.0 + version: 2.6.0 '@t3-oss/env-nextjs': specifier: ^0.11.1 version: 0.11.1(typescript@5.7.3)(zod@4.3.6) discord-api-types: specifier: ^0.37.113 version: 0.37.120 - discord.js: - specifier: ^14.16.3 - version: 14.25.1 devDependencies: '@forge/auth': specifier: workspace:* @@ -1247,7 +1247,7 @@ importers: version: 14.2.35 eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: specifier: ^6.10.0 version: 6.10.2(eslint@9.39.2(jiti@2.6.1)) @@ -1327,7 +1327,7 @@ importers: version: link:../typescript eslint: specifier: 'catalog:' - version: 9.39.2(jiti@2.6.1) + version: 9.39.2(jiti@1.21.7) prettier: specifier: 'catalog:' version: 3.8.1 @@ -13628,6 +13628,16 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 @@ -13657,7 +13667,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -13668,7 +13678,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -13679,6 +13689,8 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -14025,7 +14037,7 @@ snapshots: - encoding - supports-color - geist@1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + geist@1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: next: 14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) From afd5aaa19677f43091effa049b9178eeb973ec1b Mon Sep 17 00:00:00 2001 From: DGoel1602 Date: Thu, 19 Feb 2026 15:24:57 -0500 Subject: [PATCH 13/27] feat: move sendEmail util to forge/emails --- packages/api/src/env.ts | 1 - packages/api/src/routers/email.ts | 2 +- packages/api/src/utils.ts | 35 ------------------------------- packages/email/src/env.ts | 1 + packages/email/src/index.ts | 35 +++++++++++++++++++++++++++++++ 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/packages/api/src/env.ts b/packages/api/src/env.ts index f57d234cf..def43c33e 100644 --- a/packages/api/src/env.ts +++ b/packages/api/src/env.ts @@ -5,7 +5,6 @@ export const env = createEnv({ server: { STRIPE_SECRET_KEY: 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/email.ts b/packages/api/src/routers/email.ts index 1a08bc87c..26851688c 100644 --- a/packages/api/src/routers/email.ts +++ b/packages/api/src/routers/email.ts @@ -1,10 +1,10 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { z } from "zod"; +import { sendEmail } from "@forge/email"; import { permissions } from "@forge/utils"; import { permProcedure } from "../trpc"; -import { sendEmail } from "../utils"; export const emailRouter = { sendEmail: permProcedure diff --git a/packages/api/src/utils.ts b/packages/api/src/utils.ts index 385aea4ad..4470a425e 100644 --- a/packages/api/src/utils.ts +++ b/packages/api/src/utils.ts @@ -7,45 +7,10 @@ import type { Form } from "@forge/db/schemas/knight-hacks"; import { EVENTS, FORMS, MINIO } from "@forge/consts"; import { db } from "@forge/db/client"; 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 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" - }`, - ); - } -}; - const GOOGLE_PRIVATE_KEY = Buffer.from(env.GOOGLE_PRIVATE_KEY_B64, "base64") .toString("utf-8") .replace(/\\n/g, "\n"); diff --git a/packages/email/src/env.ts b/packages/email/src/env.ts index 7acf1c513..7af470038 100644 --- a/packages/email/src/env.ts +++ b/packages/email/src/env.ts @@ -6,6 +6,7 @@ export const env = createEnv({ LISTMONK_URL: z.string(), LISTMONK_USER: z.string(), LISTMONK_TOKEN: z.string(), + LISTMONK_FROM_EMAIL: z.string(), }, experimental__runtimeEnv: {}, skipValidation: diff --git a/packages/email/src/index.ts b/packages/email/src/index.ts index 1474e5d3a..9b1e141c8 100644 --- a/packages/email/src/index.ts +++ b/packages/email/src/index.ts @@ -9,3 +9,38 @@ export const client = new Listmonk({ password: env.LISTMONK_TOKEN, }, }); + +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" + }`, + ); + } +}; + From 509245ddd5a7ad61a8d039af8178921dca819d17 Mon Sep 17 00:00:00 2001 From: DGoel1602 Date: Thu, 19 Feb 2026 15:29:50 -0500 Subject: [PATCH 14/27] chore: format --- packages/email/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/email/src/index.ts b/packages/email/src/index.ts index 9b1e141c8..67b9390ee 100644 --- a/packages/email/src/index.ts +++ b/packages/email/src/index.ts @@ -43,4 +43,3 @@ export const sendEmail = async ({ ); } }; - From 374ed20fc0bfbf2905eb6ebb3cf16ffc75dfebf6 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:11:01 -0500 Subject: [PATCH 15/27] chore(utils): make console an eslint error Made console an eslint error not warn. This forces us to actually choose when to use console, when to use logger, and when to do nothing. We still need to look into actually handling errors on the frontend. --- .../club/events/view-attendance-button.tsx | 1 + .../admin/club/members/member-profile.tsx | 1 + .../events/view-attendance-button.tsx | 1 + .../judge-assignment/judges-client.tsx | 1 + .../hacker/hacker-application-form.tsx | 1 + .../member-dashboard/download-qr-pass.tsx | 2 + apps/blade/src/app/api/membership/route.ts | 10 ++--- apps/blade/src/app/api/trpc/[trpc]/route.ts | 1 + .../app/_components/contact/contact-form.tsx | 1 + apps/cron/eslint.config.js | 5 --- apps/cron/src/crons/_example.ts | 4 +- apps/cron/src/crons/alumni-assign.ts | 6 +-- apps/cron/src/crons/animals.ts | 3 +- apps/cron/src/crons/backup-filtered-db.ts | 3 +- apps/cron/src/crons/reminder.ts | 13 ++++--- apps/cron/src/crons/role-sync.ts | 12 +++--- apps/cron/src/structs/CronBuilder.ts | 10 +++-- apps/tk/eslint.config.js | 5 --- apps/tk/package.json | 1 + apps/tk/src/commands/capybara.ts | 6 ++- apps/tk/src/commands/cat.ts | 6 ++- apps/tk/src/commands/dog.ts | 6 ++- apps/tk/src/commands/duck.ts | 6 ++- apps/tk/src/commands/fact.ts | 6 ++- apps/tk/src/commands/fox.ts | 6 ++- apps/tk/src/commands/goat.ts | 11 +++--- apps/tk/src/commands/joke.ts | 6 ++- apps/tk/src/commands/weather.ts | 6 ++- apps/tk/src/deploy-commands.ts | 8 ++-- apps/tk/src/index.ts | 4 +- packages/api/eslint.config.js | 5 --- packages/api/src/routers/csv-importer.ts | 6 +-- packages/api/src/routers/email.ts | 6 +-- packages/api/src/routers/event.ts | 22 +++++------ packages/api/src/routers/forms.ts | 8 ++-- packages/api/src/routers/guild.ts | 13 ++++--- packages/api/src/routers/hackers/mutations.ts | 8 ++-- packages/api/src/routers/member.ts | 8 ++-- packages/api/src/routers/passkit.ts | 3 +- packages/api/src/routers/resume.ts | 5 ++- packages/api/src/routers/roles.ts | 20 +++++----- packages/api/src/trpc.ts | 2 + packages/api/src/utils.ts | 1 + packages/auth/src/config.ts | 2 + packages/db/scripts/bootstrap-superadmin.ts | 2 + packages/db/scripts/get_prod_db.ts | 2 + packages/db/scripts/seed_devdb.ts | 2 + packages/email/package.json | 1 + packages/email/src/index.ts | 3 +- packages/utils/src/discord.ts | 9 +++-- packages/utils/src/logger.ts | 15 ++++++++ packages/utils/src/permissions.ts | 4 +- pnpm-lock.yaml | 38 ++++++++----------- tooling/eslint/base.js | 2 +- 54 files changed, 195 insertions(+), 144 deletions(-) diff --git a/apps/blade/src/app/_components/admin/club/events/view-attendance-button.tsx b/apps/blade/src/app/_components/admin/club/events/view-attendance-button.tsx index a4fa02a95..52b13d6d9 100644 --- a/apps/blade/src/app/_components/admin/club/events/view-attendance-button.tsx +++ b/apps/blade/src/app/_components/admin/club/events/view-attendance-button.tsx @@ -52,6 +52,7 @@ function Attendees({ eventId }: { eventId: string }) { } invalidateAttendees().catch((error) => { + // TODO: look into not using the console // eslint-disable-next-line no-console console.error( "Error invalidating members in gathering attendees: ", diff --git a/apps/blade/src/app/_components/admin/club/members/member-profile.tsx b/apps/blade/src/app/_components/admin/club/members/member-profile.tsx index 514bd10ae..b4daed8b8 100644 --- a/apps/blade/src/app/_components/admin/club/members/member-profile.tsx +++ b/apps/blade/src/app/_components/admin/club/members/member-profile.tsx @@ -32,6 +32,7 @@ export default function MemberProfileButton({ } invalidateMembers().catch((error) => { + // TODO: why are we logging to the browser console // eslint-disable-next-line no-console console.error("Error invalidating members in member profile: ", error); }); diff --git a/apps/blade/src/app/_components/admin/hackathon/events/view-attendance-button.tsx b/apps/blade/src/app/_components/admin/hackathon/events/view-attendance-button.tsx index ea34deaad..35fecb6df 100644 --- a/apps/blade/src/app/_components/admin/hackathon/events/view-attendance-button.tsx +++ b/apps/blade/src/app/_components/admin/hackathon/events/view-attendance-button.tsx @@ -51,6 +51,7 @@ function Attendees({ eventId }: { eventId: string }) { } invalidateAttendees().catch((error) => { + // TODO: look into not logging into the console // eslint-disable-next-line no-console console.error( "Error invalidating members in gathering attendees: ", diff --git a/apps/blade/src/app/_components/admin/hackathon/judge-assignment/judges-client.tsx b/apps/blade/src/app/_components/admin/hackathon/judge-assignment/judges-client.tsx index f02614c0a..bcc5c58d9 100644 --- a/apps/blade/src/app/_components/admin/hackathon/judge-assignment/judges-client.tsx +++ b/apps/blade/src/app/_components/admin/hackathon/judge-assignment/judges-client.tsx @@ -81,6 +81,7 @@ export default function QRCodesClient() { setSelectedRoom(null); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); + // TODO: look into not logging into the console // eslint-disable-next-line no-console console.error("Failed to generate room:", err); alert(`Failed to generate room: ${message}`); diff --git a/apps/blade/src/app/_components/dashboard/hacker/hacker-application-form.tsx b/apps/blade/src/app/_components/dashboard/hacker/hacker-application-form.tsx index 66593c897..259aaac84 100644 --- a/apps/blade/src/app/_components/dashboard/hacker/hacker-application-form.tsx +++ b/apps/blade/src/app/_components/dashboard/hacker/hacker-application-form.tsx @@ -378,6 +378,7 @@ export function HackerFormPage({ }, }); } catch (error) { + // TODO: look into not logging into the console // eslint-disable-next-line no-console console.error("Error uploading resume or creating hacker:", error); toast.error( diff --git a/apps/blade/src/app/_components/dashboard/member-dashboard/download-qr-pass.tsx b/apps/blade/src/app/_components/dashboard/member-dashboard/download-qr-pass.tsx index 3161ba790..560e9c42f 100644 --- a/apps/blade/src/app/_components/dashboard/member-dashboard/download-qr-pass.tsx +++ b/apps/blade/src/app/_components/dashboard/member-dashboard/download-qr-pass.tsx @@ -45,6 +45,7 @@ export function DownloadQRPass() { toast.success("Apple Wallet pass downloaded successfully!"); } catch (error) { + // TODO: look into not logging into the console console.error("Error downloading pass:", error); // eslint-disable-line no-console toast.error("Failed to download pass"); } finally { @@ -52,6 +53,7 @@ export function DownloadQRPass() { } }, onError: (error: { message?: string }) => { + // TODO: look into not logging into the console console.error("Error generating pass:", error); // eslint-disable-line no-console toast.error(error.message ?? "Failed to generate pass"); setIsDownloading(false); 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/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/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/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 7ec1c916e..1b9b08e53 100644 --- a/apps/cron/src/crons/alumni-assign.ts +++ b/apps/cron/src/crons/alumni-assign.ts @@ -3,7 +3,7 @@ import { and, gt, isNotNull, isNull, lte, or } from "drizzle-orm"; import { DISCORD } from "@forge/consts"; import { db } from "@forge/db/client"; import { Member } from "@forge/db/schemas/knight-hacks"; -import { discord } from "@forge/utils"; +import { discord, logger } from "@forge/utils"; import { CronBuilder } from "../structs/CronBuilder"; @@ -27,7 +27,7 @@ export const alumniAssign = new CronBuilder({ 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); } } @@ -50,7 +50,7 @@ export const alumniAssign = new CronBuilder({ if (discordId) 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..6d373641e 100644 --- a/apps/cron/src/crons/backup-filtered-db.ts +++ b/apps/cron/src/crons/backup-filtered-db.ts @@ -2,6 +2,7 @@ import { spawn } from "child_process"; import { createInterface } from "readline/promises"; import { CronBuilder } from "../structs/CronBuilder"; +import { logger } from "@forge/utils"; const COMMAND = "pnpm"; const COMMAND_ARGS = [ @@ -36,7 +37,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/reminder.ts b/apps/cron/src/crons/reminder.ts index 545353c91..5c0975844 100644 --- a/apps/cron/src/crons/reminder.ts +++ b/apps/cron/src/crons/reminder.ts @@ -8,6 +8,7 @@ import { Event } from "@forge/db/schemas/knight-hacks"; import { env } from "../env"; import { CronBuilder } from "../structs/CronBuilder"; +import { logger } from "@forge/utils"; const REMINDERS_WEBHOOK = new WebhookClient({ url: env.DISCORD_WEBHOOK_REMINDERS, @@ -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 9744b6332..8d09f2776 100644 --- a/apps/cron/src/crons/role-sync.ts +++ b/apps/cron/src/crons/role-sync.ts @@ -5,7 +5,7 @@ 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 { discord } from "@forge/utils"; +import { discord, logger } from "@forge/utils"; import { CronBuilder } from "../structs/CronBuilder"; @@ -24,11 +24,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; @@ -96,14 +96,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/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/src/routers/csv-importer.ts b/packages/api/src/routers/csv-importer.ts index deaa27422..2f3a5c27a 100644 --- a/packages/api/src/routers/csv-importer.ts +++ b/packages/api/src/routers/csv-importer.ts @@ -5,7 +5,7 @@ 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 { permissions } from "@forge/utils"; +import { logger, permissions } from "@forge/utils"; import { permProcedure } from "../trpc"; @@ -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/email.ts b/packages/api/src/routers/email.ts index 26851688c..480ded29a 100644 --- a/packages/api/src/routers/email.ts +++ b/packages/api/src/routers/email.ts @@ -2,7 +2,7 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { z } from "zod"; import { sendEmail } from "@forge/email"; -import { permissions } from "@forge/utils"; +import { logger, permissions } from "@forge/utils"; import { permProcedure } from "../trpc"; @@ -19,7 +19,7 @@ export const emailRouter = { ) .mutation(async ({ input, ctx }) => { permissions.controlPerms.or(["EMAIL_PORTAL"], ctx); - console.log(input.data); + logger.log(input.data); try { const response = await sendEmail({ to: input.to, @@ -31,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.ts b/packages/api/src/routers/event.ts index b9178ae0e..345bd73ce 100644 --- a/packages/api/src/routers/event.ts +++ b/packages/api/src/routers/event.ts @@ -29,7 +29,7 @@ import { InsertEventSchema, Member, } from "@forge/db/schemas/knight-hacks"; -import { discord, permissions } from "@forge/utils"; +import { discord, logger, permissions } from "@forge/utils"; import { permProcedure, protectedProcedure, publicProcedure } from "../trpc"; import { calendar, createForm } from "../utils"; @@ -224,7 +224,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", @@ -252,7 +252,7 @@ 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) { @@ -264,7 +264,7 @@ export const eventRouter = { ), ); } catch (cleanupErr) { - console.error(JSON.stringify(cleanupErr, null, 2)); + logger.error(JSON.stringify(cleanupErr, null, 2)); } } @@ -304,7 +304,7 @@ 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 { @@ -315,7 +315,7 @@ export const eventRouter = { ), ); } 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 @@ -325,7 +325,7 @@ export const eventRouter = { eventId: googleEventId, }); } catch (cleanupErr) { - console.error(JSON.stringify(cleanupErr, null, 2)); + logger.error(JSON.stringify(cleanupErr, null, 2)); } throw new TRPCError({ @@ -420,7 +420,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", @@ -447,7 +447,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", @@ -571,7 +571,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 delete event in Discord", code: "BAD_REQUEST", @@ -585,7 +585,7 @@ export const eventRouter = { 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", diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index b2bfa4992..3b66b05db 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -20,7 +20,7 @@ import { TrpcFormConnection, TrpcFormConnectionSchema, } from "@forge/db/schemas/knight-hacks"; -import { discord, permissions } from "@forge/utils"; +import { discord, logger, permissions } from "@forge/utils"; import { minioClient } from "../minio/minio-client"; import { permProcedure, protectedProcedure } from "../trpc"; @@ -693,7 +693,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", @@ -718,7 +718,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", @@ -744,7 +744,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", 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 8fb5d73da..bf37ceb1c 100644 --- a/packages/api/src/routers/hackers/mutations.ts +++ b/packages/api/src/routers/hackers/mutations.ts @@ -16,7 +16,7 @@ import { HackerEventAttendee, InsertHackerSchema, } from "@forge/db/schemas/knight-hacks"; -import { discord, permissions } from "@forge/utils"; +import { discord, logger, permissions } from "@forge/utils"; import { minioClient } from "../../minio/minio-client"; import { permProcedure, protectedProcedure } from "../../trpc"; @@ -93,7 +93,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(); @@ -600,7 +600,7 @@ export const hackerMutationRouter = { 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 @@ -620,7 +620,7 @@ export const hackerMutationRouter = { color: "uhoh_red", userId: ctx.session.user.discordUserId, }); - console.error( + logger.error( "Failed to assign Discord roles:", (e as Error).message, ); diff --git a/packages/api/src/routers/member.ts b/packages/api/src/routers/member.ts index 28606d0d3..8b3e4315c 100644 --- a/packages/api/src/routers/member.ts +++ b/packages/api/src/routers/member.ts @@ -28,7 +28,7 @@ import { Member, OtherCompanies, } from "@forge/db/schemas/knight-hacks"; -import { discord, permissions } from "@forge/utils"; +import { discord, logger, permissions } from "@forge/utils"; import { minioClient } from "../minio/minio-client"; import { permProcedure, protectedProcedure } from "../trpc"; @@ -76,7 +76,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 +100,7 @@ export const memberRouter = { name: company, }); } catch (error) { - console.log("Unable to insert company: ", error); + logger.log("Unable to insert company: ", error); } } @@ -194,7 +194,7 @@ export const memberRouter = { name: company, }); } catch (error) { - console.log("Unable to insert company: ", error); + logger.log("Unable to insert company: ", error); } } 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 926861908..e2f3fd31f 100644 --- a/packages/api/src/routers/roles.ts +++ b/packages/api/src/routers/roles.ts @@ -8,7 +8,7 @@ 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 { discord, permissions } from "@forge/utils"; +import { discord, logger, permissions } from "@forge/utils"; import { permProcedure, protectedProcedure } from "../trpc"; @@ -338,15 +338,15 @@ export const rolesRouter = { // We log the error but don't break the flow - Blade permission is still granted try { await discord.addRoleToMember(user.discordUserId, role.discordRoleId); - console.log( + 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.`, ); } @@ -404,15 +404,15 @@ export const rolesRouter = { user.discordUserId, role.discordRoleId, ); - console.log( + 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.`, ); } @@ -495,7 +495,7 @@ export const rolesRouter = { roleData.discordRoleId, ); } catch (discordError) { - console.error( + logger.error( `Discord role grant failed for ${userData.name} -> ${roleData.name}:`, discordError, ); @@ -513,7 +513,7 @@ export const rolesRouter = { roleData.discordRoleId, ); } catch (discordError) { - console.error( + logger.error( `Discord role revoke failed for ${userData.name} -> ${roleData.name}:`, discordError, ); @@ -525,7 +525,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, ); diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index 65e8a8408..120ba36da 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) diff --git a/packages/api/src/utils.ts b/packages/api/src/utils.ts index 4470a425e..937114bae 100644 --- a/packages/api/src/utils.ts +++ b/packages/api/src/utils.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import type { JSONSchema7 } from "json-schema"; import { TRPCError } from "@trpc/server"; import { google } from "googleapis"; diff --git a/packages/auth/src/config.ts b/packages/auth/src/config.ts index 14a0909ef..3516e0978 100644 --- a/packages/auth/src/config.ts +++ b/packages/auth/src/config.ts @@ -74,6 +74,8 @@ export const auth = betterAuth({ await discord.handleDiscordOAuthCallback(discordUserId); } catch (error) { + // TODO: remove this eslint-disable + // eslint-disable-next-line no-console console.error("Error in Discord auto join hook:", error); } }, diff --git a/packages/db/scripts/bootstrap-superadmin.ts b/packages/db/scripts/bootstrap-superadmin.ts index 2d4d0ee2c..0a924579e 100644 --- a/packages/db/scripts/bootstrap-superadmin.ts +++ b/packages/db/scripts/bootstrap-superadmin.ts @@ -1,4 +1,6 @@ +// TODO: use a real logger to avoid this issue /* eslint-disable no-console */ + /** * ONE-TIME BOOTSTRAP SCRIPT // This script creates a superadmin role with all permissions and assigns it to a user. diff --git a/packages/db/scripts/get_prod_db.ts b/packages/db/scripts/get_prod_db.ts index def6f3b6d..d39cc970c 100644 --- a/packages/db/scripts/get_prod_db.ts +++ b/packages/db/scripts/get_prod_db.ts @@ -1,4 +1,6 @@ +// TODO: use a real logger to avoid this issue /* eslint-disable no-console */ + /** * Usage: * pnpm --filter=@forge/db with-env tsx scripts/get_prod_db.ts [--truncate] diff --git a/packages/db/scripts/seed_devdb.ts b/packages/db/scripts/seed_devdb.ts index 6bae8c2d0..b33ccf72e 100644 --- a/packages/db/scripts/seed_devdb.ts +++ b/packages/db/scripts/seed_devdb.ts @@ -1,4 +1,6 @@ +// TODO: use a real logger to avoid this issue /* eslint-disable no-console */ + // Usage: // pnpm --filter @forge/db with-env tsx scripts/seed_devdb.ts diff --git a/packages/email/package.json b/packages/email/package.json index 78a5ab6f0..87afde0a3 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -25,6 +25,7 @@ "with-env": "dotenv -e ../../.env --" }, "dependencies": { + "@forge/utils": "workspace:*", "@maloma/listmonk": "^1.0.1", "@t3-oss/env-nextjs": "^0.11.1", "minimatch": "^10.2.1" diff --git a/packages/email/src/index.ts b/packages/email/src/index.ts index 67b9390ee..1205ce38f 100644 --- a/packages/email/src/index.ts +++ b/packages/email/src/index.ts @@ -1,6 +1,7 @@ import { Listmonk } from "@maloma/listmonk"; import { env } from "./env"; +import { logger } from "@forge/utils"; export const client = new Listmonk({ url: env.LISTMONK_URL, @@ -35,7 +36,7 @@ export const sendEmail = async ({ return { success: true }; } catch (error) { - console.error("Error sending email:", error); + logger.error("Error sending email:", error); throw new Error( `Failed to send email: ${ error instanceof Error ? error.message : "Unknown error" diff --git a/packages/utils/src/discord.ts b/packages/utils/src/discord.ts index 73b10a58c..173e174ae 100644 --- a/packages/utils/src/discord.ts +++ b/packages/utils/src/discord.ts @@ -14,6 +14,7 @@ import { db } from "@forge/db/client"; import { Account } from "@forge/db/schemas/auth"; import { env } from "./env"; +import { logger } from "./logger"; export const api = new REST({ version: "10" }).setToken(env.DISCORD_BOT_TOKEN); @@ -46,10 +47,10 @@ export async function addMemberToServer( }, ); - console.log(`Added ${discordUserId} to the KH discord server`); + logger.log(`Added ${discordUserId} to the KH discord server`); return; } catch (error) { - console.error( + logger.error( `Failed to add user ${discordUserId} to the KH discord server:`, error instanceof Error ? error.message : "Unknown error", ); @@ -83,7 +84,7 @@ export async function handleDiscordOAuthCallback( void addMemberToServer(discordUserId, accessToken); } } catch (error) { - console.error( + logger.error( `Failed to handle Discord OAuth callback for ${discordUserId}:`, error instanceof Error ? error.message : "Unknown error", ); @@ -110,7 +111,7 @@ export const isDiscordAdmin = async (user: Session["user"]) => { )) as APIGuildMember; return guildMember.roles.includes(DISCORD.ADMIN_ROLE); } catch (err) { - console.error("Error: ", err); + logger.error("Error: ", err); return false; } }; diff --git a/packages/utils/src/logger.ts b/packages/utils/src/logger.ts index 03abcfb59..2014ec08f 100644 --- a/packages/utils/src/logger.ts +++ b/packages/utils/src/logger.ts @@ -1,2 +1,17 @@ +// +// Right now logger will just export console. This lets us pretend that we have +// logging setup, when in reality we don't. But in the future, we can just do +// +// export { createLogger, COLORS }; +// +// ... +// +// const logger = createLogger({ color: COLORS.orange, "trpc/member/mutate" }) +// +// ... +// +// logger.error("Something happened!!!", err) +// + // TODO: implement a real logger export const logger = console; diff --git a/packages/utils/src/permissions.ts b/packages/utils/src/permissions.ts index 4b25f7cb7..91a0f5466 100644 --- a/packages/utils/src/permissions.ts +++ b/packages/utils/src/permissions.ts @@ -6,6 +6,8 @@ import { PERMISSIONS } from "@forge/consts"; import { db } from "@forge/db/client"; import { JudgeSession } from "@forge/db/schemas/auth"; +import { logger } from "./logger"; + export const hasPermission = ( userPermissions: string, permission: PERMISSIONS.PermissionIndex, @@ -65,7 +67,7 @@ export const isJudgeAdmin = async () => { return rows.length > 0; } catch (err) { - console.error("isJudgeAdmin DB check error:", err); + logger.error("isJudgeAdmin DB check error:", err); return false; } }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa69f389d..fd42baa6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -243,7 +243,7 @@ importers: version: 6.6.0 geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) google-auth-library: specifier: ^9.15.0 version: 9.15.1 @@ -364,7 +364,7 @@ importers: version: 12.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) gsap: specifier: ^3.12.7 version: 3.14.2 @@ -410,7 +410,7 @@ importers: version: 7.4.4 eslint: specifier: 'catalog:' - version: 9.39.2(jiti@2.6.1) + version: 9.39.2(jiti@1.21.7) prettier: specifier: 'catalog:' version: 3.8.1 @@ -540,7 +540,7 @@ importers: version: 12.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) gsap: specifier: ^3.12.7 version: 3.14.2 @@ -631,7 +631,7 @@ importers: version: 12.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.3.1 - version: 1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + version: 1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) gsap: specifier: ^3.12.7 version: 3.14.2 @@ -696,6 +696,9 @@ importers: '@forge/db': specifier: workspace:* version: link:../../packages/db + '@forge/utils': + specifier: workspace:* + version: link:../../packages/utils '@forge/validators': specifier: workspace:* version: link:../../packages/validators @@ -995,6 +998,9 @@ importers: packages/email: dependencies: + '@forge/utils': + specifier: workspace:* + version: link:../utils '@maloma/listmonk': specifier: ^1.0.1 version: 1.0.1 @@ -1247,7 +1253,7 @@ importers: version: 14.2.35 eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: specifier: ^6.10.0 version: 6.10.2(eslint@9.39.2(jiti@2.6.1)) @@ -1327,7 +1333,7 @@ importers: version: link:../typescript eslint: specifier: 'catalog:' - version: 9.39.2(jiti@1.21.7) + version: 9.39.2(jiti@2.6.1) prettier: specifier: 'catalog:' version: 3.8.1 @@ -13628,16 +13634,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) - eslint-import-resolver-node: 0.3.9 - transitivePeerDependencies: - - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 @@ -13667,7 +13663,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -13678,7 +13674,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -13689,8 +13685,6 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -14037,7 +14031,7 @@ snapshots: - encoding - supports-color - geist@1.5.1(next@14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + geist@1.5.1(next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: next: 14.2.35(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) diff --git a/tooling/eslint/base.js b/tooling/eslint/base.js index c37b61194..0c975eff0 100644 --- a/tooling/eslint/base.js +++ b/tooling/eslint/base.js @@ -75,7 +75,7 @@ export default tseslint.config( ], "@typescript-eslint/no-non-null-assertion": "error", "import/consistent-type-specifier-style": ["error", "prefer-top-level"], - "no-console": "warn", + "no-console": "error", }, }, { From 0a7fb71f7745e67bd9dac8884321310ed842cf10 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:13:17 -0500 Subject: [PATCH 16/27] chore(*): upgrade .nvmrc --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index 9bdb657cd..385d7caac 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.16 \ No newline at end of file +v25.6.1 From 98a6c58b12e702feece391e1a1f8a225b77c97a1 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:14:19 -0500 Subject: [PATCH 17/27] chore(*): format:fix --- apps/cron/src/crons/backup-filtered-db.ts | 3 ++- apps/cron/src/crons/reminder.ts | 2 +- packages/email/src/index.ts | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/cron/src/crons/backup-filtered-db.ts b/apps/cron/src/crons/backup-filtered-db.ts index 6d373641e..9f648dc24 100644 --- a/apps/cron/src/crons/backup-filtered-db.ts +++ b/apps/cron/src/crons/backup-filtered-db.ts @@ -1,9 +1,10 @@ import { spawn } from "child_process"; import { createInterface } from "readline/promises"; -import { CronBuilder } from "../structs/CronBuilder"; import { logger } from "@forge/utils"; +import { CronBuilder } from "../structs/CronBuilder"; + const COMMAND = "pnpm"; const COMMAND_ARGS = [ "--filter", diff --git a/apps/cron/src/crons/reminder.ts b/apps/cron/src/crons/reminder.ts index 5c0975844..6b22b147d 100644 --- a/apps/cron/src/crons/reminder.ts +++ b/apps/cron/src/crons/reminder.ts @@ -5,10 +5,10 @@ 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"; -import { logger } from "@forge/utils"; const REMINDERS_WEBHOOK = new WebhookClient({ url: env.DISCORD_WEBHOOK_REMINDERS, diff --git a/packages/email/src/index.ts b/packages/email/src/index.ts index 1205ce38f..4f93a4d5a 100644 --- a/packages/email/src/index.ts +++ b/packages/email/src/index.ts @@ -1,8 +1,9 @@ import { Listmonk } from "@maloma/listmonk"; -import { env } from "./env"; import { logger } from "@forge/utils"; +import { env } from "./env"; + export const client = new Listmonk({ url: env.LISTMONK_URL, auth: { From d21cf7c1c8df5071d974b561f9e3e7673e020b7f Mon Sep 17 00:00:00 2001 From: Dylan Vidal Date: Sat, 7 Mar 2026 17:54:58 -0500 Subject: [PATCH 18/27] migrate api utils --- MIGRATION_STATUS.md | 147 +++++++++ .../app/_components/admin/roles/roleedit.tsx | 8 +- .../app/_components/admin/roles/roletable.tsx | 8 +- .../_components/navigation/session-navbar.tsx | 6 +- apps/blade/src/lib/utils.ts | 25 -- .../src/app/_components/landing/calendar.tsx | 4 +- packages/api/src/routers/event.ts | 13 +- packages/api/src/routers/forms.ts | 23 +- packages/utils/package.json | 9 +- packages/utils/src/env.ts | 2 + .../{api/src/utils.ts => utils/src/forms.ts} | 48 +-- packages/utils/src/google.ts | 32 ++ packages/utils/src/index.ts | 6 +- packages/utils/src/time.ts | 29 +- pnpm-lock.yaml | 29 +- scripts/analyze-duplicates.ts | 303 ++++++++++++++++++ scripts/analyze-utils-migration.ts | 286 +++++++++++++++++ 17 files changed, 860 insertions(+), 118 deletions(-) create mode 100644 MIGRATION_STATUS.md rename packages/{api/src/utils.ts => utils/src/forms.ts} (86%) create mode 100644 packages/utils/src/google.ts create mode 100755 scripts/analyze-duplicates.ts create mode 100644 scripts/analyze-utils-migration.ts diff --git a/MIGRATION_STATUS.md b/MIGRATION_STATUS.md new file mode 100644 index 000000000..cdf81e878 --- /dev/null +++ b/MIGRATION_STATUS.md @@ -0,0 +1,147 @@ +# Utils Package Migration Status + +## Overview +This document tracks the progress of migrating utility functions from various locations into the centralized `@forge/utils` package. + +## What Has Been Done āœ… + +### 1. Created `@forge/utils` Package +- Created new package at `packages/utils/` +- Set up package.json with proper dependencies +- Configured TypeScript, ESLint, and build setup + +### 2. Migrated Functions to `@forge/utils` + +#### Discord Utilities (`packages/utils/src/discord.ts`) +- āœ… `api` - Discord REST API client +- āœ… `addRoleToMember` +- āœ… `removeRoleFromMember` +- āœ… `addMemberToServer` +- āœ… `handleDiscordOAuthCallback` +- āœ… `resolveDiscordUserId` +- āœ… `isDiscordAdmin` +- āœ… `isDiscordMember` +- āœ… `isDiscordVIP` +- āœ… `log` - Discord logging function + +#### Permissions (`packages/utils/src/permissions.ts`) +- āœ… `hasPermission` +- āœ… `controlPerms` (with `or` and `and` methods) +- āœ… `isJudgeAdmin` +- āœ… `getJudgeSessionFromCookie` +- āœ… `getPermsAsList` + +#### Time Utilities (`packages/utils/src/time.ts`) +- āœ… `formatHourTime` +- āœ… `formatDateRange` + +#### Other Utilities +- āœ… `logger` (`packages/utils/src/logger.ts`) - Console logger wrapper +- āœ… `stripe` (`packages/utils/src/stripe.ts`) - Stripe client +- āœ… `env` (`packages/utils/src/env.ts`) - Environment variables + +### 3. Updated Imports Across Codebase +- āœ… All API package routers now import from `@forge/utils` +- āœ… Auth package updated to use `@forge/utils` +- āœ… Email package updated to use `@forge/utils` +- āœ… DB scripts updated to use `@forge/utils` +- āœ… No remaining imports from old `../utils` path in API package + +### 4. Email Package Migration +- āœ… Moved `sendEmail` function to `@forge/email` package +- āœ… Updated email package to use `@forge/utils` logger + +## What's Left To Do āš ļø + +### 1. Duplicate Functions (High Priority) + +#### `formatDateRange` - NAMING CONFLICT āš ļø +- **Location 1**: `apps/blade/src/lib/utils.ts:29` + - Formats date ranges: "Jan 1 - Jan 15, 2024" (dates only) + - Uses `toLocaleDateString` with month/day/year +- **Location 2**: `packages/utils/src/time.ts:36` + - Formats time ranges: "9:00am - 5:00pm" (times only) + - Uses `formatHourTime` helper +- **Status**: These are DIFFERENT functions with the same name! +- **Action Required**: + - Rename one of them to avoid confusion + - Recommended: Rename utils version to `formatTimeRange` (more accurate) + - Or: Rename blade version to `formatDateRangeOnly` or similar + - These serve different purposes and both should exist + +#### `getPermsAsList` +- **Location 1**: `apps/blade/src/lib/utils.ts:120` +- **Location 2**: `packages/utils/src/permissions.ts:95` +- **Status**: Function exists in both places +- **Used in**: + - `apps/blade/src/app/_components/admin/roles/roleedit.tsx` + - `apps/blade/src/app/_components/admin/roles/roletable.tsx` + - `apps/blade/src/app/_components/navigation/session-navbar.tsx` +- **Action Required**: + - Update all imports in blade app to use `@forge/utils` + - Remove duplicate definition from `apps/blade/src/lib/utils.ts` + +### 2. Remaining Functions in Old `packages/api/src/utils.ts` + +The following functions are still in the old utils file and may need to be migrated or kept: + +- `gmail` - Google Gmail API client (may stay in API package) +- `calendar` - Google Calendar API client (may stay in API package) +- `generateJsonSchema` - Form schema generation (form-specific, may stay) +- `regenerateMediaUrls` - Form media URL regeneration (form-specific, may stay) +- `CreateFormSchema` - Form schema type (form-specific, may stay) +- `createForm` - Form creation function (form-specific, may stay) + +**Decision Needed**: These are form-specific utilities. Should they: +1. Stay in API package (recommended - they're domain-specific) +2. Move to a separate `@forge/forms` package +3. Move to `@forge/utils` (not recommended - too domain-specific) + +### 3. Other App-Specific Utils + +#### `apps/blade/src/lib/utils.ts` +Contains app-specific utilities that should likely stay: +- `formatDateTime` - Blade-specific date formatting +- `getFormattedDate` - Blade-specific date formatting +- `getTagColor` - Event tag color mapping (Blade-specific) +- `getClassTeam` - Hackathon class team mapping (Blade-specific) +- `extractProcedures` - tRPC procedure extraction (Blade-specific) + +**Status**: These are app-specific and should remain in the blade app. + +## Migration Statistics + +- **Old utils.ts exports**: 6 items (mostly form-specific) +- **New @forge/utils exports**: 21 items +- **Files importing from old utils**: 0 āœ… +- **Files importing from new utils**: 23 āœ… +- **Duplicate utility functions**: 2 āš ļø + +## Next Steps + +1. **Immediate Actions**: + - [ ] **Resolve naming conflict**: Rename `formatDateRange` in `@forge/utils` to `formatTimeRange` (or rename blade version) + - [ ] Update `apps/blade/src/lib/utils.ts` to import `getPermsAsList` from `@forge/utils` + - [ ] Update all blade app files using `getPermsAsList` to import from `@forge/utils` + - [ ] Remove duplicate `getPermsAsList` definition from `apps/blade/src/lib/utils.ts` + +2. **Verification**: + - [ ] Test all affected components after migration + - [ ] Run static analysis again to confirm no duplicates remain + - [ ] Verify both date/time formatting functions work correctly after renaming + +3. **Documentation**: + - [ ] Update any documentation referencing old utils paths + - [ ] Document which utilities belong in `@forge/utils` vs app-specific utils + +## Running Analysis + +To re-run the analysis scripts: + +```bash +# Find duplicate functions and code blocks +npx tsx scripts/analyze-duplicates.ts + +# Find utils migration status +npx tsx scripts/analyze-utils-migration.ts +``` diff --git a/apps/blade/src/app/_components/admin/roles/roleedit.tsx b/apps/blade/src/app/_components/admin/roles/roleedit.tsx index ca26209fc..5e479b9ed 100644 --- a/apps/blade/src/app/_components/admin/roles/roleedit.tsx +++ b/apps/blade/src/app/_components/admin/roles/roleedit.tsx @@ -1,9 +1,9 @@ "use client"; import type { APIRole } from "discord-api-types/v10"; -import type { ZodBoolean } from "zod"; -import { useCallback, useEffect, useState } from "react"; import { Link, Loader2, Pencil, User, X } from "lucide-react"; +import { useCallback, useEffect, useState } from "react"; +import type { ZodBoolean } from "zod"; import { z } from "zod"; import { PERMISSIONS } from "@forge/consts"; @@ -21,7 +21,7 @@ import { Input } from "@forge/ui/input"; import { Label } from "@forge/ui/label"; import { toast } from "@forge/ui/toast"; -import { getPermsAsList } from "~/lib/utils"; +import { permissions } from "@forge/utils"; import { api } from "~/trpc/react"; export default function RoleEdit({ @@ -296,7 +296,7 @@ export default function RoleEdit({
-
{`${getPermsAsList(permString).length} permission(s) applied`}
+
{`${permissions.getPermsAsList(permString).length} permission(s) applied`}
@@ -147,7 +147,7 @@ export default function RoleTable() { This role has the following permissions:
    - {getPermsAsList(v.permissions).map((p) => { + {permissions.getPermsAsList(v.permissions).map((p) => { return (
  • {p} diff --git a/apps/blade/src/app/_components/navigation/session-navbar.tsx b/apps/blade/src/app/_components/navigation/session-navbar.tsx index 08179665e..537e8826e 100644 --- a/apps/blade/src/app/_components/navigation/session-navbar.tsx +++ b/apps/blade/src/app/_components/navigation/session-navbar.tsx @@ -1,5 +1,5 @@ -import Link from "next/link"; import { ChevronDown, Shield } from "lucide-react"; +import Link from "next/link"; import { DropdownMenu, @@ -13,7 +13,7 @@ import { } from "@forge/ui/navigation-menu"; import { Separator } from "@forge/ui/separator"; -import { getPermsAsList } from "~/lib/utils"; +import { permissions } from "@forge/utils"; import { api } from "~/trpc/server"; import ClubLogo from "./club-logo"; import { UserDropdown } from "./user-dropdown"; @@ -26,7 +26,7 @@ export async function SessionNavbar() { permString += v ? "1" : "0"; }); - const permList = getPermsAsList(permString); + const permList = permissions.getPermsAsList(permString); return (
    diff --git a/apps/blade/src/lib/utils.ts b/apps/blade/src/lib/utils.ts index 842737ff5..b84ed8029 100644 --- a/apps/blade/src/lib/utils.ts +++ b/apps/blade/src/lib/utils.ts @@ -3,7 +3,6 @@ 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 @@ -26,18 +25,6 @@ export const getFormattedDate = (start_datetime: string | Date) => { 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 = { @@ -117,15 +104,3 @@ export function extractProcedures(router: AnyTRPCRouter) { 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/src/app/_components/landing/calendar.tsx b/apps/club/src/app/_components/landing/calendar.tsx index 496e3e014..526213d8a 100644 --- a/apps/club/src/app/_components/landing/calendar.tsx +++ b/apps/club/src/app/_components/landing/calendar.tsx @@ -1,9 +1,9 @@ "use client"; -import React, { useRef } from "react"; import { useGSAP } from "@gsap/react"; import { gsap } from "gsap"; import { ScrollTrigger } from "gsap/dist/ScrollTrigger"; +import React, { useRef } from "react"; import { Calendar, List } from "rsuite"; import type { RouterOutputs } from "@forge/api"; @@ -94,7 +94,7 @@ export default function CalendarEventsPage({ >
    - {time.formatDateRange(item.start_datetime, item.end_datetime)} + {time.formatTimeRange(item.start_datetime, item.end_datetime)} {item.name}
    diff --git a/packages/api/src/routers/event.ts b/packages/api/src/routers/event.ts index 345bd73ce..35a6df8fb 100644 --- a/packages/api/src/routers/event.ts +++ b/packages/api/src/routers/event.ts @@ -29,10 +29,9 @@ import { InsertEventSchema, Member, } from "@forge/db/schemas/knight-hacks"; -import { discord, logger, permissions } from "@forge/utils"; +import { discord, forms, google, logger, permissions } from "@forge/utils"; import { permProcedure, protectedProcedure, publicProcedure } from "../trpc"; -import { calendar, createForm } from "../utils"; export const eventRouter = { getEvents: publicProcedure.query(async () => { @@ -234,7 +233,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: { @@ -320,7 +319,7 @@ export const eventRouter = { // 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, }); @@ -429,7 +428,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: { @@ -580,7 +579,7 @@ 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); @@ -631,7 +630,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!`, diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index 3b66b05db..660aa3ca6 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -1,7 +1,7 @@ import type { TRPCRouterRecord } from "@trpc/server"; -import type { JSONSchema7 } from "json-schema"; import { TRPCError } from "@trpc/server"; import { and, count, desc, eq, inArray, lt, sql } from "drizzle-orm"; +import type { JSONSchema7 } from "json-schema"; import jsonSchemaToZod from "json-schema-to-zod"; import * as z from "zod"; @@ -20,23 +20,17 @@ import { TrpcFormConnection, TrpcFormConnectionSchema, } from "@forge/db/schemas/knight-hacks"; -import { discord, logger, permissions } from "@forge/utils"; +import { discord, forms, logger, permissions } from "@forge/utils"; import { minioClient } from "../minio/minio-client"; import { permProcedure, protectedProcedure } from "../trpc"; -import { - createForm, - CreateFormSchema, - generateJsonSchema, - regenerateMediaUrls, -} from "../utils"; export const formsRouter = { createForm: permProcedure - .input(CreateFormSchema) + .input(forms.CreateFormSchema) .mutation(async ({ input, ctx }) => { permissions.controlPerms.or(["EDIT_FORMS"], ctx); - await createForm(input); + await forms.createForm(input); }), updateForm: permProcedure @@ -53,7 +47,7 @@ export const formsRouter = { ) .mutation(async ({ input, ctx }) => { permissions.controlPerms.or(["EDIT_FORMS"], ctx); - const jsonSchema = generateJsonSchema(input.formData); + const jsonSchema = forms.generateJsonSchema(input.formData); const slug_name = input.formData.name.toLowerCase().replaceAll(" ", "-"); @@ -153,8 +147,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 { @@ -398,7 +393,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({ @@ -484,7 +479,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({ diff --git a/packages/utils/package.json b/packages/utils/package.json index 7dac64608..e387067a7 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -22,14 +22,17 @@ "@forge/eslint-config": "workspace:*", "@forge/prettier-config": "workspace:*", "@forge/tsconfig": "workspace:*", + "@trpc/server": "catalog:", "eslint": "catalog:", "prettier": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "zod": "catalog:" }, "prettier": "@forge/prettier-config", "dependencies": { "@discordjs/rest": "^2.4.0", "@t3-oss/env-nextjs": "^0.11.1", - "discord-api-types": "^0.37.113" + "discord-api-types": "^0.37.113", + "googleapis": "^144.0.0" } -} +} \ No newline at end of file diff --git a/packages/utils/src/env.ts b/packages/utils/src/env.ts index 346317191..df5c6802e 100644 --- a/packages/utils/src/env.ts +++ b/packages/utils/src/env.ts @@ -5,6 +5,8 @@ export const env = createEnv({ server: { DISCORD_BOT_TOKEN: z.string(), STRIPE_SECRET_KEY: z.string(), + GOOGLE_CLIENT_EMAIL: z.string(), + GOOGLE_PRIVATE_KEY_B64: z.string(), }, experimental__runtimeEnv: {}, skipValidation: diff --git a/packages/api/src/utils.ts b/packages/utils/src/forms.ts similarity index 86% rename from packages/api/src/utils.ts rename to packages/utils/src/forms.ts index 937114bae..b2b406822 100644 --- a/packages/api/src/utils.ts +++ b/packages/utils/src/forms.ts @@ -1,44 +1,12 @@ -/* eslint-disable no-console */ -import type { JSONSchema7 } from "json-schema"; import { TRPCError } from "@trpc/server"; -import { google } from "googleapis"; +import type { JSONSchema7 } from "json-schema"; import z from "zod"; -import type { Form } from "@forge/db/schemas/knight-hacks"; -import { EVENTS, FORMS, MINIO } from "@forge/consts"; +import { FORMS, MINIO } from "@forge/consts"; import { db } from "@forge/db/client"; +import type { Form } from "@forge/db/schemas/knight-hacks"; import { FormSchemaSchema, FormsSchemas } from "@forge/db/schemas/knight-hacks"; -import { env } from "./env"; -import { minioClient } from "./minio/minio-client"; - -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 }; @@ -175,6 +143,13 @@ export function generateJsonSchema(form: FORMS.FormType): OptionalSchema { // Helper to regenerate presigned URLs for media export async function regenerateMediaUrls( instructions: FORMS.FormType["instructions"], + minioClient: { + presignedGetObject: ( + bucket: string, + objectName: string, + expiry: number, + ) => Promise; + }, ) { if (!instructions) return []; const updatedQuestions = await Promise.all( @@ -190,6 +165,7 @@ export async function regenerateMediaUrls( MINIO.PRESIGNED_URL_EXPIRY, ); } catch (e) { + // eslint-disable-next-line no-console console.error("Failed to regenerate image URL:", e); } } @@ -203,6 +179,7 @@ export async function regenerateMediaUrls( MINIO.PRESIGNED_URL_EXPIRY, ); } catch (e) { + // eslint-disable-next-line no-console console.error("Failed to regenerate video URL:", e); } } @@ -214,7 +191,6 @@ export async function regenerateMediaUrls( return updatedQuestions; } -// All of this will be moved to @forge/utils but its here for now export const CreateFormSchema = FormSchemaSchema.omit({ id: true, name: true, diff --git a/packages/utils/src/google.ts b/packages/utils/src/google.ts new file mode 100644 index 000000000..8ba0ba83e --- /dev/null +++ b/packages/utils/src/google.ts @@ -0,0 +1,32 @@ +import { google } from "googleapis"; + +import { EVENTS } from "@forge/consts"; + +import { env } from "./env"; + +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, +}); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 5477eff89..b3e6e3d24 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,7 +1,9 @@ export * as discord from "./discord"; -export * as permissions from "./permissions"; -export * as time from "./time"; +export * as forms from "./forms"; +export * as google from "./google"; export { logger } from "./logger"; +export * as permissions from "./permissions"; export { stripe } from "./stripe"; +export * as time from "./time"; export const name = "utils"; diff --git a/packages/utils/src/time.ts b/packages/utils/src/time.ts index acaffde0a..3b92c1e5e 100644 --- a/packages/utils/src/time.ts +++ b/packages/utils/src/time.ts @@ -31,10 +31,35 @@ export function formatHourTime(date: Date): string { * @example * const start = new Date('2023-02-19T09:00:00'); * const end = new Date('2023-02-19T17:00:00'); - * console.log(formatDateRange(start, end)); // "9:00am - 5:00pm" + * console.log(formatTimeRange(start, end)); // "9:00am - 5:00pm" */ -export const formatDateRange = (startDate: Date, endDate: Date) => { +export const formatTimeRange = (startDate: Date, endDate: Date) => { const start = formatHourTime(startDate); const end = formatHourTime(endDate); return `${start} - ${end}`; }; + +/** + * Formats a date range (start and end date) into a readable date range string. + * + * @param {Date} startDate - The start date of the range. + * @param {Date} endDate - The end date of the range. + * @returns {string} The formatted date range in "Jan 1 - Jan 15, 2024" format. + * + * @example + * const start = new Date('2024-01-01'); + * const end = new Date('2024-01-15'); + * console.log(formatDateRange(start, end)); // "Jan 1 - Jan 15, 2024" + */ +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}`; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd42baa6b..1f8a7ed6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -410,7 +410,7 @@ importers: version: 7.4.4 eslint: specifier: 'catalog:' - version: 9.39.2(jiti@1.21.7) + version: 9.39.2(jiti@2.6.1) prettier: specifier: 'catalog:' version: 3.8.1 @@ -486,7 +486,7 @@ importers: version: 0.37.120 eslint: specifier: 'catalog:' - version: 9.39.2(jiti@2.6.1) + version: 9.39.2(jiti@1.21.7) prettier: specifier: 'catalog:' version: 3.8.1 @@ -1179,10 +1179,13 @@ importers: version: 2.6.0 '@t3-oss/env-nextjs': specifier: ^0.11.1 - version: 0.11.1(typescript@5.7.3)(zod@4.3.6) + version: 0.11.1(typescript@5.7.3)(zod@3.25.76) discord-api-types: specifier: ^0.37.113 version: 0.37.120 + googleapis: + specifier: ^144.0.0 + version: 144.0.0 devDependencies: '@forge/auth': specifier: workspace:* @@ -1202,6 +1205,9 @@ importers: '@forge/tsconfig': specifier: workspace:* version: link:../../tooling/typescript + '@trpc/server': + specifier: 'catalog:' + version: 11.9.0(typescript@5.7.3) eslint: specifier: 'catalog:' version: 9.39.2(jiti@2.6.1) @@ -1211,6 +1217,9 @@ importers: typescript: specifier: 'catalog:' version: 5.7.3 + zod: + specifier: 'catalog:' + version: 3.25.76 packages/validators: dependencies: @@ -4825,6 +4834,7 @@ packages: basic-ftp@5.1.0: resolution: {integrity: sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==} engines: {node: '>=10.0.0'} + deprecated: Security vulnerability fixed in 5.2.0, please upgrade better-auth@1.4.18: resolution: {integrity: sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg==} @@ -11959,12 +11969,6 @@ snapshots: optionalDependencies: typescript: 5.7.3 - '@t3-oss/env-core@0.11.1(typescript@5.7.3)(zod@4.3.6)': - dependencies: - zod: 4.3.6 - optionalDependencies: - typescript: 5.7.3 - '@t3-oss/env-core@0.11.1(typescript@5.9.3)(zod@3.25.76)': dependencies: zod: 3.25.76 @@ -11984,13 +11988,6 @@ snapshots: optionalDependencies: typescript: 5.7.3 - '@t3-oss/env-nextjs@0.11.1(typescript@5.7.3)(zod@4.3.6)': - dependencies: - '@t3-oss/env-core': 0.11.1(typescript@5.7.3)(zod@4.3.6) - zod: 4.3.6 - optionalDependencies: - typescript: 5.7.3 - '@t3-oss/env-nextjs@0.11.1(typescript@5.9.3)(zod@3.25.76)': dependencies: '@t3-oss/env-core': 0.11.1(typescript@5.9.3)(zod@3.25.76) diff --git a/scripts/analyze-duplicates.ts b/scripts/analyze-duplicates.ts new file mode 100755 index 000000000..7956fcb00 --- /dev/null +++ b/scripts/analyze-duplicates.ts @@ -0,0 +1,303 @@ +#!/usr/bin/env tsx +/** + * Static analysis script to find: + * 1. Functions with the same name declared in multiple places + * 2. Duplicate code blocks (lines of code that appear multiple times) + */ + +import { readFileSync, readdirSync, statSync } from "fs"; +import { join, relative } from "path"; + +interface FunctionDefinition { + name: string; + file: string; + line: number; + type: "function" | "const" | "async" | "class" | "method"; + signature: string; +} + +interface DuplicateCodeBlock { + lines: string[]; + occurrences: Array<{ file: string; startLine: number }>; +} + +// Patterns to match function definitions +const FUNCTION_PATTERNS = [ + // export function name(...) + /^export\s+(?:async\s+)?function\s+(\w+)\s*\(/, + // export const name = function(...) + /^export\s+const\s+(\w+)\s*=\s*(?:async\s+)?function\s*\(/, + // export const name = (...) + /^export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(/, + // export const name = { ... } + /^export\s+const\s+(\w+)\s*=\s*\{/, + // export class Name + /^export\s+class\s+(\w+)/, + // export const name = class + /^export\s+const\s+(\w+)\s*=\s*class/, + // method: function(...) or method(...) + /^\s*(\w+)\s*:\s*(?:async\s+)?function\s*\(/, + /^\s*(\w+)\s*:\s*(?:async\s+)?\(/, + // method() { or async method() { + /^\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{/, +]; + +// Patterns to match non-exported functions (for internal duplicates) +const INTERNAL_FUNCTION_PATTERNS = [ + /^(?:async\s+)?function\s+(\w+)\s*\(/, + /^const\s+(\w+)\s*=\s*(?:async\s+)?function\s*\(/, + /^const\s+(\w+)\s*=\s*(?:async\s+)?\(/, + /^class\s+(\w+)/, +]; + +function shouldIgnoreFile(filePath: string): boolean { + const ignorePatterns = [ + /node_modules/, + /\.git/, + /dist/, + /\.next/, + /out/, + /\.cache/, + /coverage/, + /\.turbo/, + /pnpm-lock\.yaml/, + /package-lock\.json/, + /yarn\.lock/, + /\.d\.ts$/, + /\.map$/, + /\.log$/, + ]; + return ignorePatterns.some((pattern) => pattern.test(filePath)); +} + +function getAllFiles(dir: string, fileList: string[] = []): string[] { + try { + const files = readdirSync(dir); + for (const file of files) { + const filePath = join(dir, file); + if (shouldIgnoreFile(filePath)) continue; + + try { + const stat = statSync(filePath); + if (stat.isDirectory()) { + getAllFiles(filePath, fileList); + } else if (file.endsWith(".ts") || file.endsWith(".tsx") || file.endsWith(".js") || file.endsWith(".jsx")) { + fileList.push(filePath); + } + } catch { + // Skip files we can't access + } + } + } catch { + // Skip directories we can't access + } + return fileList; +} + +function extractFunctions(filePath: string, content: string): FunctionDefinition[] { + const functions: FunctionDefinition[] = []; + const lines = content.split("\n"); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Check export patterns first + for (const pattern of FUNCTION_PATTERNS) { + const match = line.match(pattern); + if (match) { + const name = match[1]; + functions.push({ + name, + file: filePath, + line: i + 1, + type: line.includes("class") ? "class" : line.includes("async") ? "async" : line.includes("function") ? "function" : "const", + signature: line.trim(), + }); + break; + } + } + } + + return functions; +} + +function findDuplicateFunctions(functions: FunctionDefinition[]): Map { + const byName = new Map(); + + for (const func of functions) { + if (!byName.has(func.name)) { + byName.set(func.name, []); + } + byName.get(func.name)!.push(func); + } + + // Filter to only duplicates + const duplicates = new Map(); + for (const [name, defs] of byName) { + if (defs.length > 1) { + duplicates.set(name, defs); + } + } + + return duplicates; +} + +function findDuplicateCodeBlocks(files: string[], minLines: number = 5): DuplicateCodeBlock[] { + const codeBlocks = new Map>(); + + for (const file of files) { + try { + const content = readFileSync(file, "utf-8"); + const lines = content.split("\n"); + + // Extract code blocks of minLines length + for (let i = 0; i <= lines.length - minLines; i++) { + const block = lines.slice(i, i + minLines).join("\n").trim(); + + // Skip blocks that are too generic (mostly whitespace, comments, etc.) + const nonWhitespace = block.replace(/\s+/g, ""); + if (nonWhitespace.length < 20) continue; + + // Skip blocks that are mostly comments + const commentRatio = (block.match(/\/\/|\/\*|\*/g) || []).length / block.split("\n").length; + if (commentRatio > 0.5) continue; + + const key = block; + if (!codeBlocks.has(key)) { + codeBlocks.set(key, []); + } + codeBlocks.get(key)!.push({ file, startLine: i + 1 }); + } + } catch { + // Skip files we can't read + } + } + + // Filter to only duplicates (appearing in multiple files or multiple times in same file) + const duplicates: DuplicateCodeBlock[] = []; + for (const [block, occurrences] of codeBlocks) { + // Group by file to find true duplicates across files + const byFile = new Map(); + for (const occ of occurrences) { + byFile.set(occ.file, (byFile.get(occ.file) || 0) + 1); + } + + // Only consider it a duplicate if it appears in multiple files OR multiple times in one file + if (byFile.size > 1 || Array.from(byFile.values()).some(count => count > 1)) { + duplicates.push({ + lines: block.split("\n"), + occurrences, + }); + } + } + + return duplicates; +} + +function main() { + const rootDir = join(__dirname, ".."); + console.log("šŸ” Analyzing codebase for duplicates...\n"); + console.log(`Root directory: ${rootDir}\n`); + + // Get all TypeScript/JavaScript files + const files = getAllFiles(rootDir); + console.log(`Found ${files.length} files to analyze\n`); + + // Find duplicate functions + console.log("=".repeat(80)); + console.log("DUPLICATE FUNCTION ANALYSIS"); + console.log("=".repeat(80)); + + const allFunctions: FunctionDefinition[] = []; + for (const file of files) { + try { + const content = readFileSync(file, "utf-8"); + const functions = extractFunctions(file, content); + allFunctions.push(...functions); + } catch { + // Skip files we can't read + } + } + + const duplicateFunctions = findDuplicateFunctions(allFunctions); + + if (duplicateFunctions.size === 0) { + console.log("āœ… No duplicate function names found!\n"); + } else { + console.log(`āš ļø Found ${duplicateFunctions.size} function(s) with duplicate names:\n`); + + for (const [name, defs] of Array.from(duplicateFunctions.entries()).sort()) { + console.log(`\nšŸ“Œ Function: ${name}`); + console.log(` Found ${defs.length} definition(s):`); + for (const def of defs) { + const relPath = relative(rootDir, def.file); + console.log(` - ${relPath}:${def.line} (${def.type})`); + console.log(` ${def.signature.substring(0, 80)}${def.signature.length > 80 ? "..." : ""}`); + } + } + console.log("\n"); + } + + // Find duplicate code blocks + console.log("=".repeat(80)); + console.log("DUPLICATE CODE BLOCK ANALYSIS"); + console.log("=".repeat(80)); + console.log("(Looking for blocks of 5+ lines that appear multiple times)\n"); + + const duplicateBlocks = findDuplicateCodeBlocks(files, 5); + + if (duplicateBlocks.length === 0) { + console.log("āœ… No significant duplicate code blocks found!\n"); + } else { + // Sort by number of occurrences + duplicateBlocks.sort((a, b) => b.occurrences.length - a.occurrences.length); + + console.log(`āš ļø Found ${duplicateBlocks.length} duplicate code block(s):\n`); + + // Show top 20 duplicates + const topDuplicates = duplicateBlocks.slice(0, 20); + for (let i = 0; i < topDuplicates.length; i++) { + const block = topDuplicates[i]; + console.log(`\nšŸ“Œ Duplicate Block #${i + 1} (${block.occurrences.length} occurrence(s)):`); + + // Group by file + const byFile = new Map(); + for (const occ of block.occurrences) { + if (!byFile.has(occ.file)) { + byFile.set(occ.file, []); + } + byFile.get(occ.file)!.push(occ.startLine); + } + + for (const [file, lines] of byFile) { + const relPath = relative(rootDir, file); + console.log(` ${relPath}:`); + for (const line of lines) { + console.log(` - Line ${line}`); + } + } + + console.log(` Preview (first 3 lines):`); + for (let j = 0; j < Math.min(3, block.lines.length); j++) { + console.log(` ${block.lines[j]?.substring(0, 70)}${(block.lines[j]?.length || 0) > 70 ? "..." : ""}`); + } + } + + if (duplicateBlocks.length > 20) { + console.log(`\n ... and ${duplicateBlocks.length - 20} more duplicate blocks`); + } + console.log("\n"); + } + + // Summary + console.log("=".repeat(80)); + console.log("SUMMARY"); + console.log("=".repeat(80)); + console.log(`Total files analyzed: ${files.length}`); + console.log(`Total functions found: ${allFunctions.length}`); + console.log(`Duplicate function names: ${duplicateFunctions.size}`); + console.log(`Duplicate code blocks: ${duplicateBlocks.length}`); + console.log("=".repeat(80)); +} + +main(); diff --git a/scripts/analyze-utils-migration.ts b/scripts/analyze-utils-migration.ts new file mode 100644 index 000000000..05d6d89a0 --- /dev/null +++ b/scripts/analyze-utils-migration.ts @@ -0,0 +1,286 @@ +#!/usr/bin/env tsx +/** + * Analysis script specifically for the utils package migration. + * Finds: + * 1. Functions that exist in both old utils.ts and new utils package + * 2. Functions that should be migrated but haven't been + * 3. Remaining imports from old utils + */ + +import { readFileSync, readdirSync, statSync } from "fs"; +import { join, relative } from "path"; + +interface FunctionInfo { + name: string; + file: string; + line: number; + signature: string; +} + +function shouldIgnoreFile(filePath: string): boolean { + const ignorePatterns = [ + /node_modules/, + /\.git/, + /dist/, + /\.next/, + /out/, + /\.cache/, + /coverage/, + /\.turbo/, + /pnpm-lock\.yaml/, + /package-lock\.json/, + /yarn\.lock/, + /\.d\.ts$/, + /\.map$/, + /\.log$/, + /scripts\/analyze/, + ]; + return ignorePatterns.some((pattern) => pattern.test(filePath)); +} + +function getAllFiles(dir: string, fileList: string[] = []): string[] { + try { + const files = readdirSync(dir); + for (const file of files) { + const filePath = join(dir, file); + if (shouldIgnoreFile(filePath)) continue; + + try { + const stat = statSync(filePath); + if (stat.isDirectory()) { + getAllFiles(filePath, fileList); + } else if (file.endsWith(".ts") || file.endsWith(".tsx") || file.endsWith(".js") || file.endsWith(".jsx")) { + fileList.push(filePath); + } + } catch { + // Skip files we can't access + } + } + } catch { + // Skip directories we can't access + } + return fileList; +} + +function extractExportedFunctions(filePath: string, content: string): FunctionInfo[] { + const functions: FunctionInfo[] = []; + const lines = content.split("\n"); + + // Pattern to match exported functions/constants + const exportPattern = /^export\s+(?:async\s+)?(?:function|const|class)\s+(\w+)/; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const match = line.match(exportPattern); + if (match) { + functions.push({ + name: match[1], + file: filePath, + line: i + 1, + signature: line.trim(), + }); + } + } + + return functions; +} + +function findImports(content: string, importPath: string): string[] { + const imports: string[] = []; + const lines = content.split("\n"); + + // Match: import { ... } from "path" or import ... from "path" + const importPattern = new RegExp(`from\\s+["']${importPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["']`); + + for (const line of lines) { + if (importPattern.test(line)) { + // Extract imported names + const namedImports = line.match(/\{([^}]+)\}/); + if (namedImports) { + const names = namedImports[1].split(",").map(n => n.trim().split(/\s+as\s+/)[0]); + imports.push(...names); + } else { + // Default import + const defaultMatch = line.match(/import\s+(\w+)/); + if (defaultMatch) { + imports.push(defaultMatch[1]); + } + } + } + } + + return imports; +} + +function main() { + const rootDir = join(__dirname, ".."); + console.log("šŸ” Analyzing utils package migration status...\n"); + + // Get all files + const files = getAllFiles(rootDir); + + // 1. Check what's exported from old utils.ts + const oldUtilsPath = join(rootDir, "packages/api/src/utils.ts"); + let oldUtilsExports: FunctionInfo[] = []; + try { + const oldUtilsContent = readFileSync(oldUtilsPath, "utf-8"); + oldUtilsExports = extractExportedFunctions(oldUtilsPath, oldUtilsContent); + console.log(`šŸ“¦ Old utils.ts exports: ${oldUtilsExports.length} items`); + for (const exp of oldUtilsExports) { + console.log(` - ${exp.name}`); + } + } catch { + console.log("āš ļø Could not read old utils.ts"); + } + + // 2. Check what's exported from new utils package + const newUtilsPath = join(rootDir, "packages/utils/src"); + const newUtilsFiles = getAllFiles(newUtilsPath); + const newUtilsExports: FunctionInfo[] = []; + for (const file of newUtilsFiles) { + try { + const content = readFileSync(file, "utf-8"); + const exports = extractExportedFunctions(file, content); + newUtilsExports.push(...exports); + } catch { + // Skip + } + } + console.log(`\nšŸ“¦ New @forge/utils exports: ${newUtilsExports.length} items`); + const newUtilsNames = new Set(newUtilsExports.map(e => e.name)); + for (const name of Array.from(newUtilsNames).sort()) { + console.log(` - ${name}`); + } + + // 3. Find imports from old utils + console.log("\n" + "=".repeat(80)); + console.log("IMPORTS FROM OLD UTILS"); + console.log("=".repeat(80)); + + const oldUtilsImports: Map = new Map(); + for (const file of files) { + try { + const content = readFileSync(file, "utf-8"); + const imports = findImports(content, "../utils"); + if (imports.length > 0) { + oldUtilsImports.set(file, imports); + } + } catch { + // Skip + } + } + + if (oldUtilsImports.size === 0) { + console.log("āœ… No imports from old utils found!\n"); + } else { + console.log(`āš ļø Found ${oldUtilsImports.size} file(s) importing from old utils:\n`); + for (const [file, imports] of Array.from(oldUtilsImports.entries()).sort()) { + const relPath = relative(rootDir, file); + console.log(` ${relPath}:`); + for (const imp of imports) { + console.log(` - ${imp}`); + } + } + console.log(); + } + + // 4. Find imports from new utils + console.log("=".repeat(80)); + console.log("IMPORTS FROM NEW @forge/utils"); + console.log("=".repeat(80)); + + const newUtilsImports: Map = new Map(); + for (const file of files) { + try { + const content = readFileSync(file, "utf-8"); + const imports = findImports(content, "@forge/utils"); + if (imports.length > 0) { + newUtilsImports.set(file, imports); + } + } catch { + // Skip + } + } + + console.log(`āœ… Found ${newUtilsImports.size} file(s) importing from new utils\n`); + + // 5. Find duplicate function names (actual utility functions) + console.log("=".repeat(80)); + console.log("DUPLICATE UTILITY FUNCTIONS"); + console.log("=".repeat(80)); + + const allExports = new Map(); + for (const file of files) { + try { + const content = readFileSync(file, "utf-8"); + const exports = extractExportedFunctions(file, content); + for (const exp of exports) { + if (!allExports.has(exp.name)) { + allExports.set(exp.name, []); + } + allExports.get(exp.name)!.push(exp); + } + } catch { + // Skip + } + } + + // Filter to utility-like function names (not React components, not common keywords) + const utilityNames = [ + "formatDateRange", + "getPermsAsList", + "formatHourTime", + "formatDateTime", + "getFormattedDate", + "hasPermission", + "controlPerms", + "isDiscordAdmin", + "isDiscordMember", + "isDiscordVIP", + "resolveDiscordUserId", + "addRoleToMember", + "removeRoleFromMember", + "log", + "logger", + "sendEmail", + "createForm", + "generateJsonSchema", + "regenerateMediaUrls", + ]; + + const duplicates: Array<{ name: string; locations: FunctionInfo[] }> = []; + for (const name of utilityNames) { + const locations = allExports.get(name) || []; + if (locations.length > 1) { + duplicates.push({ name, locations }); + } + } + + if (duplicates.length === 0) { + console.log("āœ… No duplicate utility functions found!\n"); + } else { + console.log(`āš ļø Found ${duplicates.length} duplicate utility function(s):\n`); + for (const { name, locations } of duplicates) { + console.log(`\nšŸ“Œ ${name} (${locations.length} definition(s)):`); + for (const loc of locations) { + const relPath = relative(rootDir, loc.file); + console.log(` - ${relPath}:${loc.line}`); + console.log(` ${loc.signature.substring(0, 80)}${loc.signature.length > 80 ? "..." : ""}`); + } + } + console.log(); + } + + // 6. Summary + console.log("=".repeat(80)); + console.log("SUMMARY"); + console.log("=".repeat(80)); + console.log(`Old utils.ts exports: ${oldUtilsExports.length}`); + console.log(`New @forge/utils exports: ${newUtilsExports.length}`); + console.log(`Files importing from old utils: ${oldUtilsImports.size}`); + console.log(`Files importing from new utils: ${newUtilsImports.size}`); + console.log(`Duplicate utility functions: ${duplicates.length}`); + console.log("=".repeat(80)); +} + +main(); From 0ea5c1faa2a7f6e8f701ed2a4770b0d27597a9cb Mon Sep 17 00:00:00 2001 From: Dylan Vidal Date: Sat, 7 Mar 2026 18:17:01 -0500 Subject: [PATCH 19/27] migrate blade utils --- .../admin/club/events/event-details.tsx | 13 +-- .../admin/club/events/events-table.tsx | 8 +- .../admin/club/members/scanner.tsx | 6 +- .../_components/admin/forms/editor/client.tsx | 4 +- .../_components/admin/forms/editor/linker.tsx | 4 +- .../admin/hackathon/events/event-details.tsx | 12 +- .../admin/hackathon/events/events-table.tsx | 8 +- .../app/_components/admin/roles/roleedit.tsx | 6 +- .../app/_components/admin/roles/roletable.tsx | 4 +- .../hackathon-dashboard/point-leaderboard.tsx | 8 +- .../hackathon-dashboard/team-points.tsx | 4 +- .../hackathon-dashboard/upcoming-events.tsx | 5 +- .../hacker-dashboard/past-hackathons.tsx | 6 +- .../member-dashboard/event/event-showcase.tsx | 17 +-- .../_components/forms/connection-handler.ts | 4 +- .../_components/navigation/session-navbar.tsx | 4 +- .../src/app/_components/shared/scanner.tsx | 6 +- .../blade/src/app/admin/forms/[slug]/page.tsx | 4 +- apps/blade/src/lib/utils.ts | 106 ------------------ .../src/app/_components/landing/calendar.tsx | 2 +- packages/api/src/routers/forms.ts | 4 +- packages/utils/package.json | 3 + packages/utils/src/events.ts | 34 ++++++ packages/utils/src/forms.ts | 4 +- packages/utils/src/hackathons.ts | 25 +++++ packages/utils/src/index.ts | 3 + packages/utils/src/time.ts | 43 +++++++ packages/utils/src/trpc.ts | 55 +++++++++ 28 files changed, 231 insertions(+), 171 deletions(-) delete mode 100644 apps/blade/src/lib/utils.ts create mode 100644 packages/utils/src/events.ts create mode 100644 packages/utils/src/hackathons.ts create mode 100644 packages/utils/src/trpc.ts diff --git a/apps/blade/src/app/_components/admin/club/events/event-details.tsx b/apps/blade/src/app/_components/admin/club/events/event-details.tsx index 719058784..812d036b7 100644 --- a/apps/blade/src/app/_components/admin/club/events/event-details.tsx +++ b/apps/blade/src/app/_components/admin/club/events/event-details.tsx @@ -1,7 +1,7 @@ "use client"; -import { useState } from "react"; import { CalendarDays, MapPin, Star, Users } from "lucide-react"; +import { useState } from "react"; import ReactMarkdown from "react-markdown"; import type { ReturnEvent } from "@forge/db/schemas/knight-hacks"; @@ -15,8 +15,7 @@ import { DialogTitle, DialogTrigger, } from "@forge/ui/dialog"; - -import { formatDateTime, getTagColor } from "~/lib/utils"; +import { events, time } from "@forge/utils"; export function EventDetailsButton({ event, @@ -41,12 +40,12 @@ export function EventDetailsButton({
    {event.name} - + {event.tag} {event.hackathonName && ( {event.hackathonName} @@ -63,14 +62,14 @@ export function EventDetailsButton({
    Start - {formatDateTime(event.start_datetime)} + {time.formatDateTime(event.start_datetime)}
    End - {formatDateTime(event.end_datetime)} + {time.formatDateTime(event.end_datetime)}
    diff --git a/apps/blade/src/app/_components/admin/club/events/events-table.tsx b/apps/blade/src/app/_components/admin/club/events/events-table.tsx index b8df2f7af..bda0847c6 100644 --- a/apps/blade/src/app/_components/admin/club/events/events-table.tsx +++ b/apps/blade/src/app/_components/admin/club/events/events-table.tsx @@ -1,7 +1,7 @@ "use client"; -import { useState } from "react"; import { Search } from "lucide-react"; +import { useState } from "react"; import type { ReturnEvent } from "@forge/db/schemas/knight-hacks"; import { Input } from "@forge/ui/input"; @@ -16,8 +16,8 @@ import { TableRow, } from "@forge/ui/table"; +import { time } from "@forge/utils"; import SortButton from "~/app/_components/shared/SortButton"; -import { getFormattedDate } from "~/lib/utils"; import { api } from "~/trpc/react"; import { CreateEventButton } from "./create-event"; import { DeleteEventButton } from "./delete-event"; @@ -188,7 +188,7 @@ export function EventsTable() { {event.tag} - {getFormattedDate(event.start_datetime)} + {time.getFormattedDate(event.start_datetime)} {event.location} @@ -244,7 +244,7 @@ export function EventsTable() { {event.tag} - {getFormattedDate(event.start_datetime)} + {time.getFormattedDate(event.start_datetime)} {event.location} diff --git a/apps/blade/src/app/_components/admin/club/members/scanner.tsx b/apps/blade/src/app/_components/admin/club/members/scanner.tsx index 2c1b43cae..820bdb281 100644 --- a/apps/blade/src/app/_components/admin/club/members/scanner.tsx +++ b/apps/blade/src/app/_components/admin/club/members/scanner.tsx @@ -28,7 +28,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@forge/ui/tabs"; import { toast } from "@forge/ui/toast"; import ToggleButton from "~/app/_components/admin/hackathon/hackers/toggle-button"; -import { getClassTeam } from "~/lib/utils"; +import { hackathons } from "@forge/utils"; import { api } from "~/trpc/react"; const ScannerPopUp = ({ eventType }: { eventType: "Member" | "Hacker" }) => { @@ -337,8 +337,8 @@ const ScannerPopUp = ({ eventType }: { eventType: "Member" | "Hacker" }) => {
    {assignedClass} diff --git a/apps/blade/src/app/_components/admin/forms/editor/client.tsx b/apps/blade/src/app/_components/admin/forms/editor/client.tsx index 3eecdca26..a335941d7 100644 --- a/apps/blade/src/app/_components/admin/forms/editor/client.tsx +++ b/apps/blade/src/app/_components/admin/forms/editor/client.tsx @@ -52,7 +52,9 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@forge/ui/tabs"; import { Textarea } from "@forge/ui/textarea"; import type { MatchingType } from "./linker"; -import type { ProcedureMeta } from "~/lib/utils"; +import type { trpc } from "@forge/utils"; + +type ProcedureMeta = trpc.ProcedureMeta; import { InstructionEditCard } from "~/app/_components/forms/shared/instruction-edit-card"; import { QuestionEditCard } from "~/app/_components/forms/shared/question-edit-card"; import { api } from "~/trpc/react"; diff --git a/apps/blade/src/app/_components/admin/forms/editor/linker.tsx b/apps/blade/src/app/_components/admin/forms/editor/linker.tsx index cb6b3c597..cc01b0832 100644 --- a/apps/blade/src/app/_components/admin/forms/editor/linker.tsx +++ b/apps/blade/src/app/_components/admin/forms/editor/linker.tsx @@ -16,7 +16,9 @@ import { } from "@forge/ui/select"; import { toast } from "@forge/ui/toast"; -import type { ProcedureMeta } from "~/lib/utils"; +import type { trpc } from "@forge/utils"; + +type ProcedureMeta = trpc.ProcedureMeta; import { api } from "~/trpc/react"; const matchingSchema = z.object({ diff --git a/apps/blade/src/app/_components/admin/hackathon/events/event-details.tsx b/apps/blade/src/app/_components/admin/hackathon/events/event-details.tsx index 719058784..61c72c17d 100644 --- a/apps/blade/src/app/_components/admin/hackathon/events/event-details.tsx +++ b/apps/blade/src/app/_components/admin/hackathon/events/event-details.tsx @@ -1,7 +1,7 @@ "use client"; -import { useState } from "react"; import { CalendarDays, MapPin, Star, Users } from "lucide-react"; +import { useState } from "react"; import ReactMarkdown from "react-markdown"; import type { ReturnEvent } from "@forge/db/schemas/knight-hacks"; @@ -16,7 +16,7 @@ import { DialogTrigger, } from "@forge/ui/dialog"; -import { formatDateTime, getTagColor } from "~/lib/utils"; +import { events, time } from "@forge/utils"; export function EventDetailsButton({ event, @@ -41,12 +41,12 @@ export function EventDetailsButton({
    {event.name} - + {event.tag} {event.hackathonName && ( {event.hackathonName} @@ -63,14 +63,14 @@ export function EventDetailsButton({
    Start - {formatDateTime(event.start_datetime)} + {time.formatDateTime(event.start_datetime)}
    End - {formatDateTime(event.end_datetime)} + {time.formatDateTime(event.end_datetime)}
    diff --git a/apps/blade/src/app/_components/admin/hackathon/events/events-table.tsx b/apps/blade/src/app/_components/admin/hackathon/events/events-table.tsx index 403ebceed..bbcb7e95f 100644 --- a/apps/blade/src/app/_components/admin/hackathon/events/events-table.tsx +++ b/apps/blade/src/app/_components/admin/hackathon/events/events-table.tsx @@ -1,7 +1,7 @@ "use client"; -import { useState } from "react"; import { Search } from "lucide-react"; +import { useState } from "react"; import type { ReturnEvent } from "@forge/db/schemas/knight-hacks"; import { Input } from "@forge/ui/input"; @@ -16,8 +16,8 @@ import { TableRow, } from "@forge/ui/table"; +import { time } from "@forge/utils"; import SortButton from "~/app/_components/shared/SortButton"; -import { getFormattedDate } from "~/lib/utils"; import { api } from "~/trpc/react"; import { CreateEventButton } from "./create-event"; import { DeleteEventButton } from "./delete-event"; @@ -194,7 +194,7 @@ export function EventsTable() { - {getFormattedDate(event.start_datetime)} + {time.getFormattedDate(event.start_datetime)} {event.location} @@ -251,7 +251,7 @@ export function EventsTable() { - {getFormattedDate(event.start_datetime)} + {time.getFormattedDate(event.start_datetime)} {event.location} diff --git a/apps/blade/src/app/_components/admin/roles/roleedit.tsx b/apps/blade/src/app/_components/admin/roles/roleedit.tsx index 5e479b9ed..db13e3390 100644 --- a/apps/blade/src/app/_components/admin/roles/roleedit.tsx +++ b/apps/blade/src/app/_components/admin/roles/roleedit.tsx @@ -1,9 +1,9 @@ "use client"; import type { APIRole } from "discord-api-types/v10"; -import { Link, Loader2, Pencil, User, X } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; import type { ZodBoolean } from "zod"; +import { useCallback, useEffect, useState } from "react"; +import { Link, Loader2, Pencil, User, X } from "lucide-react"; import { z } from "zod"; import { PERMISSIONS } from "@forge/consts"; @@ -20,8 +20,8 @@ import { import { Input } from "@forge/ui/input"; import { Label } from "@forge/ui/label"; import { toast } from "@forge/ui/toast"; - import { permissions } from "@forge/utils"; + import { api } from "~/trpc/react"; export default function RoleEdit({ diff --git a/apps/blade/src/app/_components/admin/roles/roletable.tsx b/apps/blade/src/app/_components/admin/roles/roletable.tsx index 57b6f461c..0d888bded 100644 --- a/apps/blade/src/app/_components/admin/roles/roletable.tsx +++ b/apps/blade/src/app/_components/admin/roles/roletable.tsx @@ -1,6 +1,7 @@ "use client"; import type { APIRole } from "discord-api-types/v10"; +import { useEffect, useState } from "react"; import { Check, ChevronDown, @@ -11,7 +12,6 @@ import { User, X, } from "lucide-react"; -import { useEffect, useState } from "react"; import { Button } from "@forge/ui/button"; import { Dialog, DialogContent, DialogTrigger } from "@forge/ui/dialog"; @@ -29,8 +29,8 @@ import { TableRow, } from "@forge/ui/table"; import { toast } from "@forge/ui/toast"; - import { permissions } from "@forge/utils"; + import { api } from "~/trpc/react"; import RoleEdit from "./roleedit"; diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx index 0e95103f5..228fa4a29 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx @@ -7,7 +7,7 @@ import type { HackerClass } from "@forge/db/schemas/knight-hacks"; import { HACKER_TEAMS } from "@forge/db/schemas/knight-hacks"; import type { api as serverCall } from "~/trpc/server"; -import { getClassTeam } from "~/lib/utils"; +import { hackathons } from "@forge/utils"; import { api } from "~/trpc/react"; interface LeaderboardEntry { @@ -38,7 +38,7 @@ export function PointLeaderboard({ hPoints: hacker?.points || 0, hClass: hacker?.class || "Alchemist", }); - const team = getClassTeam(hacker?.class || "Alchemist"); + const team = hackathons.getClassTeam(hacker?.class || "Alchemist"); const [overall, setOverall] = useState(); const [showYours, setShowYours] = useState(false); @@ -143,7 +143,7 @@ export function PointLeaderboard({
    {!activeTop ? ( dummy.map((v, i) => { - const t = getClassTeam(v); + const t = hackathons.getClassTeam(v); return (
    {activeTop.map((v, i) => { - const t = getClassTeam(v.class || "Alchemist"); + const t = hackathons.getClassTeam(v.class || "Alchemist"); return (
    ([0, 0]); - const team = getClassTeam(hClass); + const team = hackathons.getClassTeam(hClass); function formatPts(pt: number) { const fmt = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }); diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx index 01e9edd26..8d0aac567 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { Star } from "lucide-react"; import { Badge } from "@forge/ui/badge"; @@ -10,7 +9,7 @@ import { CardTitle, } from "@forge/ui/card"; -import { formatDateTime } from "~/lib/utils"; +import { time } from "@forge/utils"; import { api } from "~/trpc/server"; export default async function UpcomingEvents() { @@ -52,7 +51,7 @@ export default async function UpcomingEvents() { {event.name} - {formatDateTime(event.start_datetime)} @ {event.location} + {time.formatDateTime(event.start_datetime)} @ {event.location} diff --git a/apps/blade/src/app/_components/dashboard/hacker-dashboard/past-hackathons.tsx b/apps/blade/src/app/_components/dashboard/hacker-dashboard/past-hackathons.tsx index c3d581a1e..6fce770d8 100644 --- a/apps/blade/src/app/_components/dashboard/hacker-dashboard/past-hackathons.tsx +++ b/apps/blade/src/app/_components/dashboard/hacker-dashboard/past-hackathons.tsx @@ -10,8 +10,8 @@ import { DialogTrigger, } from "@forge/ui/dialog"; +import { time } from "@forge/utils"; import type { api } from "~/trpc/server"; -import { formatDateTime } from "~/lib/utils"; export function PastHackathonButton({ hackathons, @@ -80,14 +80,14 @@ export function PastHackathonButton({
    Start - {formatDateTime(hackathon.startDate)} + {time.formatDateTime(hackathon.startDate)}
    End - {formatDateTime(hackathon.endDate)} + {time.formatDateTime(hackathon.endDate)}
    diff --git a/apps/blade/src/app/_components/dashboard/member-dashboard/event/event-showcase.tsx b/apps/blade/src/app/_components/dashboard/member-dashboard/event/event-showcase.tsx index 322fb4b90..b7ae841e2 100644 --- a/apps/blade/src/app/_components/dashboard/member-dashboard/event/event-showcase.tsx +++ b/apps/blade/src/app/_components/dashboard/member-dashboard/event/event-showcase.tsx @@ -21,9 +21,10 @@ import { DialogTrigger, } from "@forge/ui/dialog"; -import type { api } from "~/trpc/server"; +import { events as eventUtils, time } from "@forge/utils"; import { DASHBOARD_ICON_SIZE } from "~/consts"; -import { formatDateTime, getTagColor } from "~/lib/utils"; +import type { api } from "~/trpc/server"; + import { EventFeedbackForm } from "./event-feedback"; export function EventShowcase({ @@ -73,7 +74,7 @@ export function EventShowcase({
    {mostRecent.tag} @@ -86,14 +87,14 @@ export function EventShowcase({
    Start - {formatDateTime(mostRecent.start_datetime)} + {time.formatDateTime(mostRecent.start_datetime)}
    End - {formatDateTime(mostRecent.end_datetime)} + {time.formatDateTime(mostRecent.end_datetime)}
    @@ -145,7 +146,7 @@ export function EventShowcase({
    {event.tag} @@ -160,7 +161,7 @@ export function EventShowcase({ Start - {formatDateTime(mostRecent.start_datetime)} + {time.formatDateTime(mostRecent.start_datetime)}
    @@ -169,7 +170,7 @@ export function EventShowcase({ End - {formatDateTime(mostRecent.end_datetime)} + {time.formatDateTime(mostRecent.end_datetime)}
    diff --git a/apps/blade/src/app/_components/forms/connection-handler.ts b/apps/blade/src/app/_components/forms/connection-handler.ts index 87647dae6..06bf46200 100644 --- a/apps/blade/src/app/_components/forms/connection-handler.ts +++ b/apps/blade/src/app/_components/forms/connection-handler.ts @@ -6,7 +6,7 @@ import { appRouter } from "@forge/api"; import { auth } from "@forge/auth/server"; import { discord } from "@forge/utils"; -import { extractProcedures } from "~/lib/utils"; +import { trpc } from "@forge/utils"; import { api } from "~/trpc/server"; export const handleCallbacks = async ( @@ -19,7 +19,7 @@ export const handleCallbacks = async ( if (!session) return; const connections = await api.forms.getConnections({ id }); - const procs = extractProcedures(appRouter); + const procs = trpc.extractProcedures(appRouter); for (const con of connections) { const data: Record = {}; diff --git a/apps/blade/src/app/_components/navigation/session-navbar.tsx b/apps/blade/src/app/_components/navigation/session-navbar.tsx index 537e8826e..2c8bd9d10 100644 --- a/apps/blade/src/app/_components/navigation/session-navbar.tsx +++ b/apps/blade/src/app/_components/navigation/session-navbar.tsx @@ -1,5 +1,5 @@ -import { ChevronDown, Shield } from "lucide-react"; import Link from "next/link"; +import { ChevronDown, Shield } from "lucide-react"; import { DropdownMenu, @@ -12,8 +12,8 @@ import { NavigationMenuList, } from "@forge/ui/navigation-menu"; import { Separator } from "@forge/ui/separator"; - import { permissions } from "@forge/utils"; + import { api } from "~/trpc/server"; import ClubLogo from "./club-logo"; import { UserDropdown } from "./user-dropdown"; diff --git a/apps/blade/src/app/_components/shared/scanner.tsx b/apps/blade/src/app/_components/shared/scanner.tsx index 2c1b43cae..820bdb281 100644 --- a/apps/blade/src/app/_components/shared/scanner.tsx +++ b/apps/blade/src/app/_components/shared/scanner.tsx @@ -28,7 +28,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@forge/ui/tabs"; import { toast } from "@forge/ui/toast"; import ToggleButton from "~/app/_components/admin/hackathon/hackers/toggle-button"; -import { getClassTeam } from "~/lib/utils"; +import { hackathons } from "@forge/utils"; import { api } from "~/trpc/react"; const ScannerPopUp = ({ eventType }: { eventType: "Member" | "Hacker" }) => { @@ -337,8 +337,8 @@ 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..229f0f6b0 100644 --- a/apps/blade/src/app/admin/forms/[slug]/page.tsx +++ b/apps/blade/src/app/admin/forms/[slug]/page.tsx @@ -4,7 +4,7 @@ import { appRouter } from "@forge/api"; import { auth } from "@forge/auth/server"; import { EditorClient } from "~/app/_components/admin/forms/editor/client"; -import { extractProcedures } from "~/lib/utils"; +import { trpc } from "@forge/utils"; import { api } from "~/trpc/server"; export default async function FormEditorPage({ @@ -35,7 +35,7 @@ export default async function FormEditorPage({ return ( <> - + ); } diff --git a/apps/blade/src/lib/utils.ts b/apps/blade/src/lib/utils.ts deleted file mode 100644 index b84ed8029..000000000 --- a/apps/blade/src/lib/utils.ts +++ /dev/null @@ -1,106 +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"; - -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 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; -} - diff --git a/apps/club/src/app/_components/landing/calendar.tsx b/apps/club/src/app/_components/landing/calendar.tsx index 526213d8a..8f5e19fab 100644 --- a/apps/club/src/app/_components/landing/calendar.tsx +++ b/apps/club/src/app/_components/landing/calendar.tsx @@ -1,9 +1,9 @@ "use client"; +import React, { useRef } from "react"; import { useGSAP } from "@gsap/react"; import { gsap } from "gsap"; import { ScrollTrigger } from "gsap/dist/ScrollTrigger"; -import React, { useRef } from "react"; import { Calendar, List } from "rsuite"; import type { RouterOutputs } from "@forge/api"; diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index a937a19a4..d9210b973 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -1330,7 +1330,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({ @@ -1357,7 +1357,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/utils/package.json b/packages/utils/package.json index e387067a7..c87ff09e5 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -28,6 +28,9 @@ "typescript": "catalog:", "zod": "catalog:" }, + "peerDependencies": { + "@trpc/server": "catalog:" + }, "prettier": "@forge/prettier-config", "dependencies": { "@discordjs/rest": "^2.4.0", diff --git a/packages/utils/src/events.ts b/packages/utils/src/events.ts new file mode 100644 index 000000000..f910ebe9f --- /dev/null +++ b/packages/utils/src/events.ts @@ -0,0 +1,34 @@ +import type { EVENTS } from "@forge/consts"; + +/** + * Gets the Tailwind CSS color classes for an event tag. + * + * @param {EVENTS.EventTagsColor} tag - The event tag. + * @returns {string} Tailwind CSS classes for the tag color. + * + * @example + * getTagColor("GBM") // "bg-blue-100 text-blue-800" + */ +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]; +}; diff --git a/packages/utils/src/forms.ts b/packages/utils/src/forms.ts index b2b406822..2db186490 100644 --- a/packages/utils/src/forms.ts +++ b/packages/utils/src/forms.ts @@ -1,10 +1,10 @@ -import { TRPCError } from "@trpc/server"; import type { JSONSchema7 } from "json-schema"; +import { TRPCError } from "@trpc/server"; import z from "zod"; +import type { Form } from "@forge/db/schemas/knight-hacks"; import { FORMS, MINIO } from "@forge/consts"; import { db } from "@forge/db/client"; -import type { Form } from "@forge/db/schemas/knight-hacks"; import { FormSchemaSchema, FormsSchemas } from "@forge/db/schemas/knight-hacks"; type OptionalSchema = diff --git a/packages/utils/src/hackathons.ts b/packages/utils/src/hackathons.ts new file mode 100644 index 000000000..180e9e54e --- /dev/null +++ b/packages/utils/src/hackathons.ts @@ -0,0 +1,25 @@ +import type { HackerClass } from "@forge/db/schemas/knight-hacks"; + +/** + * Gets the team information for a hackathon class. + * + * @param {HackerClass} tag - The hacker class. + * @returns {object} Team information including name, color, and image URL. + * + * @example + * getClassTeam("Harbinger") // { team: "Monstrosity", teamColor: "#e03131", imgUrl: "/khviii/lenneth.jpg" } + */ +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", + }; +}; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index b3e6e3d24..dbc5b0523 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,9 +1,12 @@ export * as discord from "./discord"; +export * as events from "./events"; export * as forms from "./forms"; export * as google from "./google"; +export * as hackathons from "./hackathons"; export { logger } from "./logger"; export * as permissions from "./permissions"; export { stripe } from "./stripe"; export * as time from "./time"; +export * as trpc from "./trpc"; export const name = "utils"; diff --git a/packages/utils/src/time.ts b/packages/utils/src/time.ts index 3b92c1e5e..4620a3929 100644 --- a/packages/utils/src/time.ts +++ b/packages/utils/src/time.ts @@ -63,3 +63,46 @@ export const formatDateRange = (startDate: Date, endDate: Date) => { }); return `${start} - ${end}`; }; + +/** + * Formats a date into a readable date-time string with timezone adjustment. + * Creates a new Date object adjusted by 1 day and formats it. + * + * @param {Date} date - The date object to format. + * @returns {string} The formatted date-time in "MMM D, YYYY, h:mm AM/PM" format. + * + * @example + * const date = new Date('2023-02-19T14:30:00'); + * console.log(formatDateTime(date)); // "Feb 20, 2023, 2:30 PM" + */ +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, + }); +}; + +/** + * Formats a date into a simple date string with timezone adjustment. + * Creates a new Date object adjusted by 1 day and formats it. + * + * @param {string | Date} start_datetime - The date to format. + * @returns {string} The formatted date string. + * + * @example + * const date = new Date('2023-02-19'); + * console.log(getFormattedDate(date)); // "2/20/2023" + */ +export const getFormattedDate = (start_datetime: string | Date) => { + const date = new Date(start_datetime); + date.setDate(date.getDate() + 1); + return date.toLocaleDateString(); +}; diff --git a/packages/utils/src/trpc.ts b/packages/utils/src/trpc.ts new file mode 100644 index 000000000..aae566008 --- /dev/null +++ b/packages/utils/src/trpc.ts @@ -0,0 +1,55 @@ +import type { AnyTRPCProcedure, AnyTRPCRouter } from "@trpc/server"; +import type { z } from "zod"; + +/** + * Metadata for a tRPC procedure. + */ +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 + ); +} + +/** + * Extracts procedure metadata from a tRPC router. + * Useful for form connections and other dynamic tRPC usage. + * + * @param {AnyTRPCRouter} router - The tRPC router to extract procedures from. + * @returns {Record} A record of procedure IDs to their metadata. + * + * @example + * const procedures = extractProcedures(appRouter); + * // { "procedureId": { inputSchema: ["field1", "field2"], route: "router.procedure" } } + */ +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; +} From 14439cb9b9be2165044f74fff8f71c9bd41a3b2d Mon Sep 17 00:00:00 2001 From: Dylan Vidal Date: Sat, 7 Mar 2026 18:21:22 -0500 Subject: [PATCH 20/27] migrate gemiknights utils --- .../app/_components/admin/club/events/event-details.tsx | 6 ++++-- .../src/app/_components/admin/club/events/events-table.tsx | 4 ++-- .../src/app/_components/admin/club/members/scanner.tsx | 5 +++-- .../src/app/_components/admin/forms/editor/client.tsx | 6 +++--- .../src/app/_components/admin/forms/editor/linker.tsx | 4 ++-- .../_components/admin/hackathon/events/event-details.tsx | 7 ++++--- .../_components/admin/hackathon/events/events-table.tsx | 4 ++-- .../dashboard/hackathon-dashboard/point-leaderboard.tsx | 2 +- .../dashboard/hackathon-dashboard/team-points.tsx | 2 +- .../dashboard/hackathon-dashboard/upcoming-events.tsx | 5 +++-- .../dashboard/hacker-dashboard/past-hackathons.tsx | 2 +- .../dashboard/member-dashboard/event/event-showcase.tsx | 5 ++--- apps/blade/src/app/_components/forms/connection-handler.ts | 3 +-- apps/blade/src/app/_components/shared/scanner.tsx | 5 +++-- apps/blade/src/app/admin/forms/[slug]/page.tsx | 7 +++++-- .../app/_components/ui/background-gradient-animation.tsx | 2 +- apps/gemiknights/src/lib/utils.ts | 7 ------- packages/utils/package.json | 2 +- 18 files changed, 39 insertions(+), 39 deletions(-) delete mode 100644 apps/gemiknights/src/lib/utils.ts diff --git a/apps/blade/src/app/_components/admin/club/events/event-details.tsx b/apps/blade/src/app/_components/admin/club/events/event-details.tsx index 812d036b7..cd4239f93 100644 --- a/apps/blade/src/app/_components/admin/club/events/event-details.tsx +++ b/apps/blade/src/app/_components/admin/club/events/event-details.tsx @@ -1,7 +1,7 @@ "use client"; -import { CalendarDays, MapPin, Star, Users } from "lucide-react"; import { useState } from "react"; +import { CalendarDays, MapPin, Star, Users } from "lucide-react"; import ReactMarkdown from "react-markdown"; import type { ReturnEvent } from "@forge/db/schemas/knight-hacks"; @@ -40,7 +40,9 @@ export function EventDetailsButton({
    {event.name} - + {event.tag} {event.hackathonName && ( diff --git a/apps/blade/src/app/_components/admin/club/events/events-table.tsx b/apps/blade/src/app/_components/admin/club/events/events-table.tsx index bda0847c6..57e2d5e61 100644 --- a/apps/blade/src/app/_components/admin/club/events/events-table.tsx +++ b/apps/blade/src/app/_components/admin/club/events/events-table.tsx @@ -1,7 +1,7 @@ "use client"; -import { Search } from "lucide-react"; import { useState } from "react"; +import { Search } from "lucide-react"; import type { ReturnEvent } from "@forge/db/schemas/knight-hacks"; import { Input } from "@forge/ui/input"; @@ -15,8 +15,8 @@ import { TableHeader, TableRow, } from "@forge/ui/table"; - import { time } from "@forge/utils"; + import SortButton from "~/app/_components/shared/SortButton"; import { api } from "~/trpc/react"; import { CreateEventButton } from "./create-event"; diff --git a/apps/blade/src/app/_components/admin/club/members/scanner.tsx b/apps/blade/src/app/_components/admin/club/members/scanner.tsx index 820bdb281..071384c84 100644 --- a/apps/blade/src/app/_components/admin/club/members/scanner.tsx +++ b/apps/blade/src/app/_components/admin/club/members/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 { hackathons } from "@forge/utils"; import { api } from "~/trpc/react"; const ScannerPopUp = ({ eventType }: { eventType: "Member" | "Hacker" }) => { @@ -337,7 +337,8 @@ const ScannerPopUp = ({ eventType }: { eventType: "Member" | "Hacker" }) => {
    diff --git a/apps/blade/src/app/_components/admin/forms/editor/client.tsx b/apps/blade/src/app/_components/admin/forms/editor/client.tsx index a335941d7..d02269b21 100644 --- a/apps/blade/src/app/_components/admin/forms/editor/client.tsx +++ b/apps/blade/src/app/_components/admin/forms/editor/client.tsx @@ -25,6 +25,7 @@ import { CSS } from "@dnd-kit/utilities"; import { ArrowLeft, CogIcon, Loader2, Plus, Save, Users } from "lucide-react"; import type { FORMS } from "@forge/consts"; +import type { trpc } from "@forge/utils"; import { Button } from "@forge/ui/button"; import { Card } from "@forge/ui/card"; import { Checkbox } from "@forge/ui/checkbox"; @@ -52,15 +53,14 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@forge/ui/tabs"; import { Textarea } from "@forge/ui/textarea"; import type { MatchingType } from "./linker"; -import type { trpc } from "@forge/utils"; - -type ProcedureMeta = trpc.ProcedureMeta; import { InstructionEditCard } from "~/app/_components/forms/shared/instruction-edit-card"; import { QuestionEditCard } from "~/app/_components/forms/shared/question-edit-card"; import { api } from "~/trpc/react"; import { ConnectionViewer } from "./con-viewer"; import ListMatcher from "./linker"; +type ProcedureMeta = trpc.ProcedureMeta; + type FormQuestion = z.infer; type FormInstruction = z.infer; type UIQuestion = FormQuestion & { id: string }; diff --git a/apps/blade/src/app/_components/admin/forms/editor/linker.tsx b/apps/blade/src/app/_components/admin/forms/editor/linker.tsx index cc01b0832..4e6343e89 100644 --- a/apps/blade/src/app/_components/admin/forms/editor/linker.tsx +++ b/apps/blade/src/app/_components/admin/forms/editor/linker.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { Loader2 } from "lucide-react"; import { z } from "zod"; +import type { trpc } from "@forge/utils"; import { Button } from "@forge/ui/button"; import { Input } from "@forge/ui/input"; import { Label } from "@forge/ui/label"; @@ -16,10 +17,9 @@ import { } from "@forge/ui/select"; import { toast } from "@forge/ui/toast"; -import type { trpc } from "@forge/utils"; +import { api } from "~/trpc/react"; type ProcedureMeta = trpc.ProcedureMeta; -import { api } from "~/trpc/react"; const matchingSchema = z.object({ proc: z.string().optional(), diff --git a/apps/blade/src/app/_components/admin/hackathon/events/event-details.tsx b/apps/blade/src/app/_components/admin/hackathon/events/event-details.tsx index 61c72c17d..cd4239f93 100644 --- a/apps/blade/src/app/_components/admin/hackathon/events/event-details.tsx +++ b/apps/blade/src/app/_components/admin/hackathon/events/event-details.tsx @@ -1,7 +1,7 @@ "use client"; -import { CalendarDays, MapPin, Star, Users } from "lucide-react"; import { useState } from "react"; +import { CalendarDays, MapPin, Star, Users } from "lucide-react"; import ReactMarkdown from "react-markdown"; import type { ReturnEvent } from "@forge/db/schemas/knight-hacks"; @@ -15,7 +15,6 @@ import { DialogTitle, DialogTrigger, } from "@forge/ui/dialog"; - import { events, time } from "@forge/utils"; export function EventDetailsButton({ @@ -41,7 +40,9 @@ export function EventDetailsButton({
    {event.name} - + {event.tag} {event.hackathonName && ( diff --git a/apps/blade/src/app/_components/admin/hackathon/events/events-table.tsx b/apps/blade/src/app/_components/admin/hackathon/events/events-table.tsx index bbcb7e95f..80172b499 100644 --- a/apps/blade/src/app/_components/admin/hackathon/events/events-table.tsx +++ b/apps/blade/src/app/_components/admin/hackathon/events/events-table.tsx @@ -1,7 +1,7 @@ "use client"; -import { Search } from "lucide-react"; import { useState } from "react"; +import { Search } from "lucide-react"; import type { ReturnEvent } from "@forge/db/schemas/knight-hacks"; import { Input } from "@forge/ui/input"; @@ -15,8 +15,8 @@ import { TableHeader, TableRow, } from "@forge/ui/table"; - import { time } from "@forge/utils"; + import SortButton from "~/app/_components/shared/SortButton"; import { api } from "~/trpc/react"; import { CreateEventButton } from "./create-event"; diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx index 228fa4a29..8ad25122e 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx @@ -5,9 +5,9 @@ import { Dot, Loader } from "lucide-react"; import type { HackerClass } from "@forge/db/schemas/knight-hacks"; import { HACKER_TEAMS } from "@forge/db/schemas/knight-hacks"; +import { hackathons } from "@forge/utils"; import type { api as serverCall } from "~/trpc/server"; -import { hackathons } from "@forge/utils"; import { api } from "~/trpc/react"; interface LeaderboardEntry { diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/team-points.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/team-points.tsx index 66e534438..d930ab30c 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/team-points.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/team-points.tsx @@ -6,8 +6,8 @@ import { Crown, Dot, Loader } from "lucide-react"; import type { HackerClass } from "@forge/db/schemas/knight-hacks"; import { HACKER_TEAMS } from "@forge/db/schemas/knight-hacks"; import { Card, CardContent, CardHeader } from "@forge/ui/card"; - import { hackathons } from "@forge/utils"; + import { api } from "~/trpc/react"; export function TeamPoints({ diff --git a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx index 8d0aac567..f11b6c1ef 100644 --- a/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx +++ b/apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx @@ -8,8 +8,8 @@ import { CardHeader, CardTitle, } from "@forge/ui/card"; - import { time } from "@forge/utils"; + import { api } from "~/trpc/server"; export default async function UpcomingEvents() { @@ -51,7 +51,8 @@ export default async function UpcomingEvents() { {event.name} - {time.formatDateTime(event.start_datetime)} @ {event.location} + {time.formatDateTime(event.start_datetime)} @{" "} + {event.location} diff --git a/apps/blade/src/app/_components/dashboard/hacker-dashboard/past-hackathons.tsx b/apps/blade/src/app/_components/dashboard/hacker-dashboard/past-hackathons.tsx index 6fce770d8..c228ecdea 100644 --- a/apps/blade/src/app/_components/dashboard/hacker-dashboard/past-hackathons.tsx +++ b/apps/blade/src/app/_components/dashboard/hacker-dashboard/past-hackathons.tsx @@ -9,8 +9,8 @@ import { DialogTitle, DialogTrigger, } from "@forge/ui/dialog"; - import { time } from "@forge/utils"; + import type { api } from "~/trpc/server"; export function PastHackathonButton({ diff --git a/apps/blade/src/app/_components/dashboard/member-dashboard/event/event-showcase.tsx b/apps/blade/src/app/_components/dashboard/member-dashboard/event/event-showcase.tsx index b7ae841e2..d73ddb18f 100644 --- a/apps/blade/src/app/_components/dashboard/member-dashboard/event/event-showcase.tsx +++ b/apps/blade/src/app/_components/dashboard/member-dashboard/event/event-showcase.tsx @@ -20,11 +20,10 @@ import { DialogTitle, DialogTrigger, } from "@forge/ui/dialog"; - import { events as eventUtils, time } from "@forge/utils"; -import { DASHBOARD_ICON_SIZE } from "~/consts"; -import type { api } from "~/trpc/server"; +import type { api } from "~/trpc/server"; +import { DASHBOARD_ICON_SIZE } from "~/consts"; import { EventFeedbackForm } from "./event-feedback"; export function EventShowcase({ diff --git a/apps/blade/src/app/_components/forms/connection-handler.ts b/apps/blade/src/app/_components/forms/connection-handler.ts index 06bf46200..925a3ca89 100644 --- a/apps/blade/src/app/_components/forms/connection-handler.ts +++ b/apps/blade/src/app/_components/forms/connection-handler.ts @@ -4,9 +4,8 @@ import { stringify } from "superjson"; import { appRouter } from "@forge/api"; import { auth } from "@forge/auth/server"; -import { discord } from "@forge/utils"; +import { discord, trpc } from "@forge/utils"; -import { trpc } from "@forge/utils"; import { api } from "~/trpc/server"; export const handleCallbacks = async ( diff --git a/apps/blade/src/app/_components/shared/scanner.tsx b/apps/blade/src/app/_components/shared/scanner.tsx index 820bdb281..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 { hackathons } from "@forge/utils"; import { api } from "~/trpc/react"; const ScannerPopUp = ({ eventType }: { eventType: "Member" | "Hacker" }) => { @@ -337,7 +337,8 @@ const ScannerPopUp = ({ eventType }: { eventType: "Member" | "Hacker" }) => {
    diff --git a/apps/blade/src/app/admin/forms/[slug]/page.tsx b/apps/blade/src/app/admin/forms/[slug]/page.tsx index 229f0f6b0..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 { trpc } from "@forge/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/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/packages/utils/package.json b/packages/utils/package.json index c87ff09e5..65acfe5d7 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -38,4 +38,4 @@ "discord-api-types": "^0.37.113", "googleapis": "^144.0.0" } -} \ No newline at end of file +} From 29d67cd3250159c6871379514e56eafa5e4a1e8e Mon Sep 17 00:00:00 2001 From: Dylan Vidal Date: Sat, 7 Mar 2026 18:22:59 -0500 Subject: [PATCH 21/27] migrate form client utils --- .../forms/form-responder-client.tsx | 4 +- .../src/app/_components/forms/form-runner.tsx | 12 ++- .../forms/form-view-edit-client.tsx | 5 +- apps/blade/src/app/_components/forms/utils.ts | 88 ------------------ packages/utils/src/forms.ts | 91 ++++++++++++++++++- 5 files changed, 102 insertions(+), 98 deletions(-) delete mode 100644 apps/blade/src/app/_components/forms/utils.ts diff --git a/apps/blade/src/app/_components/forms/form-responder-client.tsx b/apps/blade/src/app/_components/forms/form-responder-client.tsx index 8c637d99e..bd9d57d7b 100644 --- a/apps/blade/src/app/_components/forms/form-responder-client.tsx +++ b/apps/blade/src/app/_components/forms/form-responder-client.tsx @@ -8,7 +8,9 @@ import type { FORMS } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Card } from "@forge/ui/card"; -import type { FormResponsePayload } from "./utils"; +import type { forms } from "@forge/utils"; + +type FormResponsePayload = forms.FormResponsePayload; import { api } from "~/trpc/react"; import { useSubmissionSuccess } from "./_hooks/useSubmissionSuccess"; import { handleCallbacks } from "./connection-handler"; diff --git a/apps/blade/src/app/_components/forms/form-runner.tsx b/apps/blade/src/app/_components/forms/form-runner.tsx index 0f0053067..e206c2f74 100644 --- a/apps/blade/src/app/_components/forms/form-runner.tsx +++ b/apps/blade/src/app/_components/forms/form-runner.tsx @@ -6,10 +6,12 @@ import type { FORMS } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Card } from "@forge/ui/card"; -import type { FormResponsePayload, FormResponseUI } from "./utils"; +import { forms } from "@forge/utils"; import { InstructionResponseCard } from "~/app/_components/forms/instruction-response-card"; import { QuestionResponseCard } from "~/app/_components/forms/question-response-card"; -import { getValidationError, isFormValid, normalizeResponses } from "./utils"; + +type FormResponsePayload = forms.FormResponsePayload; +type FormResponseUI = forms.FormResponseUI; /** * Shared renderer for "fill out form" and "review/edit response". @@ -82,10 +84,10 @@ export function FormRunner({ }; const canSubmit = - allowEdit && !isSubmitting && isFormValid(zodValidator, responses, form); + allowEdit && !isSubmitting && forms.isFormValid(zodValidator, responses, form); const handleSubmit = () => { - const payload = normalizeResponses(responses, form); + const payload = forms.normalizeResponses(responses, form); onSubmit(payload); }; @@ -167,7 +169,7 @@ export function FormRunner({ handleResponseChange(item.question, value); }} formId={formId} - error={getValidationError( + error={forms.getValidationError( item, zodValidator, responses, diff --git a/apps/blade/src/app/_components/forms/form-view-edit-client.tsx b/apps/blade/src/app/_components/forms/form-view-edit-client.tsx index f317ca349..1e15dadf4 100644 --- a/apps/blade/src/app/_components/forms/form-view-edit-client.tsx +++ b/apps/blade/src/app/_components/forms/form-view-edit-client.tsx @@ -5,7 +5,10 @@ import { Loader2 } from "lucide-react"; import type { FORMS } from "@forge/consts"; -import type { FormResponsePayload, FormResponseUI } from "./utils"; +import type { forms } from "@forge/utils"; + +type FormResponsePayload = forms.FormResponsePayload; +type FormResponseUI = forms.FormResponseUI; import { api } from "~/trpc/react"; import { useSubmissionSuccess } from "./_hooks/useSubmissionSuccess"; import FormNotFound from "./form-not-found"; diff --git a/apps/blade/src/app/_components/forms/utils.ts b/apps/blade/src/app/_components/forms/utils.ts deleted file mode 100644 index 72733f928..000000000 --- a/apps/blade/src/app/_components/forms/utils.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { z } from "zod"; - -import type { FORMS } from "@forge/consts"; - -/** UI state in the client */ -export type FormResponseUI = Partial< - Record ->; - -/** JSON-safe payload what zodValidator will validate */ -export type FormResponsePayload = Partial< - Record ->; - -export const getValidatorResponse = ( - zodValidator: string, - responses: FormResponseUI, - form: FORMS.FormType, -) => { - // eslint-disable-next-line @typescript-eslint/no-implied-eval, @typescript-eslint/no-unsafe-call - const zodSchema = new Function("z", `return ${zodValidator}`)( - z, - ) as z.ZodSchema; - - const payload = normalizeResponses(responses, form); - - return zodSchema.safeParse(payload); -}; - -// normalized responses needed for zod validation and therefore proc responseData -export const normalizeResponses = ( - responses: FormResponseUI, - form: FORMS.FormType, -): FormResponsePayload => { - const out: FormResponsePayload = {}; - - for (const q of form.questions) { - const key = q.question; - const v = responses[key]; - - // drop missing/empty values - if (v == null || v === "" || (Array.isArray(v) && v.length === 0)) continue; - - // dates -> strings based on question type - if (v instanceof Date) { - if (q.type === "DATE") out[key] = v.toISOString().slice(0, 10); - else if (q.type === "TIME") out[key] = v.toTimeString().slice(0, 5); - else continue; // unexpected Date for non-date/time question - continue; - } - - // if your UI sometimes passes "true"/"false" strings, normalize them here - if (q.type === "BOOLEAN" && typeof v === "string") { - if (v === "true") out[key] = true; - else if (v === "false") out[key] = false; - else continue; - continue; - } - - out[key] = v; - } - - return out; -}; - -// get specific validator error for question -export const getValidationError = ( - question: FORMS.QuestionValidatorType, - zodValidator: string, - responses: FormResponseUI, - form: FORMS.FormType, -) => { - const validatorResponse = getValidatorResponse(zodValidator, responses, form); - if (validatorResponse.success) return null; - - const issue = validatorResponse.error.issues.find((i) => { - const k = i.path[0]; - return typeof k === "string" && k === question.question; - }); - - return issue?.message ?? null; -}; - -export const isFormValid = ( - zodValidator: string, - responses: FormResponseUI, - form: FORMS.FormType, -) => getValidatorResponse(zodValidator, responses, form).success; diff --git a/packages/utils/src/forms.ts b/packages/utils/src/forms.ts index 2db186490..d7c95750b 100644 --- a/packages/utils/src/forms.ts +++ b/packages/utils/src/forms.ts @@ -22,9 +22,9 @@ function createJsonSchemaValidator({ }: FORMS.ValidatorOptions): OptionalSchema { const schema: JSONSchema7 = {}; - const resolvedOptions = optionsConst - ? [...FORMS.getDropdownOptionsFromConst(optionsConst)] - : options; + const resolvedOptions = optionsConst + ? [...FORMS.getDropdownOptionsFromConst(optionsConst)] + : options; switch (type) { case "SHORT_ANSWER": @@ -246,3 +246,88 @@ export async function createForm(input: CreateFormType): Promise
    { return form; } + +/** UI state in the client */ +export type FormResponseUI = Partial< + Record +>; + +/** JSON-safe payload what zodValidator will validate */ +export type FormResponsePayload = Partial< + Record +>; + +export const getValidatorResponse = ( + zodValidator: string, + responses: FormResponseUI, + form: FORMS.FormType, +) => { + // eslint-disable-next-line @typescript-eslint/no-implied-eval, @typescript-eslint/no-unsafe-call + const zodSchema = new Function("z", `return ${zodValidator}`)( + z, + ) as z.ZodSchema; + + const payload = normalizeResponses(responses, form); + + return zodSchema.safeParse(payload); +}; + +// normalized responses needed for zod validation and therefore proc responseData +export const normalizeResponses = ( + responses: FormResponseUI, + form: FORMS.FormType, +): FormResponsePayload => { + const out: FormResponsePayload = {}; + + for (const q of form.questions) { + const key = q.question; + const v = responses[key]; + + // drop missing/empty values + if (v == null || v === "" || (Array.isArray(v) && v.length === 0)) continue; + + // dates -> strings based on question type + if (v instanceof Date) { + if (q.type === "DATE") out[key] = v.toISOString().slice(0, 10); + else if (q.type === "TIME") out[key] = v.toTimeString().slice(0, 5); + else continue; // unexpected Date for non-date/time question + continue; + } + + // if your UI sometimes passes "true"/"false" strings, normalize them here + if (q.type === "BOOLEAN" && typeof v === "string") { + if (v === "true") out[key] = true; + else if (v === "false") out[key] = false; + else continue; + continue; + } + + out[key] = v; + } + + return out; +}; + +// get specific validator error for question +export const getValidationError = ( + question: FORMS.QuestionValidatorType, + zodValidator: string, + responses: FormResponseUI, + form: FORMS.FormType, +) => { + const validatorResponse = getValidatorResponse(zodValidator, responses, form); + if (validatorResponse.success) return null; + + const issue = validatorResponse.error.issues.find((i) => { + const k = i.path[0]; + return typeof k === "string" && k === question.question; + }); + + return issue?.message ?? null; +}; + +export const isFormValid = ( + zodValidator: string, + responses: FormResponseUI, + form: FORMS.FormType, +) => getValidatorResponse(zodValidator, responses, form).success; From 013acb7a6cadc69dedf5e2d576060fd30933fc44 Mon Sep 17 00:00:00 2001 From: Dylan Vidal Date: Sat, 7 Mar 2026 18:24:14 -0500 Subject: [PATCH 22/27] chore: format --- packages/utils/src/forms.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/forms.ts b/packages/utils/src/forms.ts index d7c95750b..d537f6748 100644 --- a/packages/utils/src/forms.ts +++ b/packages/utils/src/forms.ts @@ -1,10 +1,10 @@ -import type { JSONSchema7 } from "json-schema"; import { TRPCError } from "@trpc/server"; +import type { JSONSchema7 } from "json-schema"; import z from "zod"; -import type { Form } from "@forge/db/schemas/knight-hacks"; import { FORMS, MINIO } from "@forge/consts"; import { db } from "@forge/db/client"; +import type { Form } from "@forge/db/schemas/knight-hacks"; import { FormSchemaSchema, FormsSchemas } from "@forge/db/schemas/knight-hacks"; type OptionalSchema = From 39c381e9cc903a1231f74b4129cd78aeed747ab7 Mon Sep 17 00:00:00 2001 From: Dylan Vidal Date: Sat, 7 Mar 2026 18:26:02 -0500 Subject: [PATCH 23/27] chore: format (again) --- .../app/_components/forms/form-responder-client.tsx | 6 +++--- apps/blade/src/app/_components/forms/form-runner.tsx | 6 ++++-- .../app/_components/forms/form-view-edit-client.tsx | 6 +++--- packages/utils/src/forms.ts | 10 +++++----- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/apps/blade/src/app/_components/forms/form-responder-client.tsx b/apps/blade/src/app/_components/forms/form-responder-client.tsx index bd9d57d7b..5866b05ee 100644 --- a/apps/blade/src/app/_components/forms/form-responder-client.tsx +++ b/apps/blade/src/app/_components/forms/form-responder-client.tsx @@ -5,12 +5,10 @@ import Link from "next/link"; import { CheckCircle2, Loader2, XCircle } from "lucide-react"; import type { FORMS } from "@forge/consts"; +import type { forms } from "@forge/utils"; import { Button } from "@forge/ui/button"; import { Card } from "@forge/ui/card"; -import type { forms } from "@forge/utils"; - -type FormResponsePayload = forms.FormResponsePayload; import { api } from "~/trpc/react"; import { useSubmissionSuccess } from "./_hooks/useSubmissionSuccess"; import { handleCallbacks } from "./connection-handler"; @@ -18,6 +16,8 @@ import FormNotFound from "./form-not-found"; import { FormRunner } from "./form-runner"; import { SubmissionSuccessCard } from "./form-submitted-success"; +type FormResponsePayload = forms.FormResponsePayload; + interface FormResponderWrapperProps { formName: string; userName: string; diff --git a/apps/blade/src/app/_components/forms/form-runner.tsx b/apps/blade/src/app/_components/forms/form-runner.tsx index e206c2f74..7b8d49fcb 100644 --- a/apps/blade/src/app/_components/forms/form-runner.tsx +++ b/apps/blade/src/app/_components/forms/form-runner.tsx @@ -5,8 +5,8 @@ import { useEffect, useState } from "react"; import type { FORMS } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Card } from "@forge/ui/card"; - import { forms } from "@forge/utils"; + import { InstructionResponseCard } from "~/app/_components/forms/instruction-response-card"; import { QuestionResponseCard } from "~/app/_components/forms/question-response-card"; @@ -84,7 +84,9 @@ export function FormRunner({ }; const canSubmit = - allowEdit && !isSubmitting && forms.isFormValid(zodValidator, responses, form); + allowEdit && + !isSubmitting && + forms.isFormValid(zodValidator, responses, form); const handleSubmit = () => { const payload = forms.normalizeResponses(responses, form); diff --git a/apps/blade/src/app/_components/forms/form-view-edit-client.tsx b/apps/blade/src/app/_components/forms/form-view-edit-client.tsx index 1e15dadf4..5c6b6fec2 100644 --- a/apps/blade/src/app/_components/forms/form-view-edit-client.tsx +++ b/apps/blade/src/app/_components/forms/form-view-edit-client.tsx @@ -4,11 +4,8 @@ import { useMemo, useState } from "react"; import { Loader2 } from "lucide-react"; import type { FORMS } from "@forge/consts"; - import type { forms } from "@forge/utils"; -type FormResponsePayload = forms.FormResponsePayload; -type FormResponseUI = forms.FormResponseUI; import { api } from "~/trpc/react"; import { useSubmissionSuccess } from "./_hooks/useSubmissionSuccess"; import FormNotFound from "./form-not-found"; @@ -16,6 +13,9 @@ import { FormRunner } from "./form-runner"; import { SubmissionSuccessCard } from "./form-submitted-success"; import ResponseNotFound from "./response-not-found"; +type FormResponsePayload = forms.FormResponsePayload; +type FormResponseUI = forms.FormResponseUI; + interface FormReviewWrapperProps { formName: string; userName: string; diff --git a/packages/utils/src/forms.ts b/packages/utils/src/forms.ts index d537f6748..0b74c36fe 100644 --- a/packages/utils/src/forms.ts +++ b/packages/utils/src/forms.ts @@ -1,10 +1,10 @@ -import { TRPCError } from "@trpc/server"; import type { JSONSchema7 } from "json-schema"; +import { TRPCError } from "@trpc/server"; import z from "zod"; +import type { Form } from "@forge/db/schemas/knight-hacks"; import { FORMS, MINIO } from "@forge/consts"; import { db } from "@forge/db/client"; -import type { Form } from "@forge/db/schemas/knight-hacks"; import { FormSchemaSchema, FormsSchemas } from "@forge/db/schemas/knight-hacks"; type OptionalSchema = @@ -22,9 +22,9 @@ function createJsonSchemaValidator({ }: FORMS.ValidatorOptions): OptionalSchema { const schema: JSONSchema7 = {}; - const resolvedOptions = optionsConst - ? [...FORMS.getDropdownOptionsFromConst(optionsConst)] - : options; + const resolvedOptions = optionsConst + ? [...FORMS.getDropdownOptionsFromConst(optionsConst)] + : options; switch (type) { case "SHORT_ANSWER": From 8e37f80b1262908b660c34e85b915422f45bd0e9 Mon Sep 17 00:00:00 2001 From: Dylan Vidal Date: Sat, 7 Mar 2026 18:30:41 -0500 Subject: [PATCH 24/27] chore: remove scripts --- scripts/analyze-duplicates.ts | 303 ----------------------------- scripts/analyze-utils-migration.ts | 286 --------------------------- 2 files changed, 589 deletions(-) delete mode 100755 scripts/analyze-duplicates.ts delete mode 100644 scripts/analyze-utils-migration.ts diff --git a/scripts/analyze-duplicates.ts b/scripts/analyze-duplicates.ts deleted file mode 100755 index 7956fcb00..000000000 --- a/scripts/analyze-duplicates.ts +++ /dev/null @@ -1,303 +0,0 @@ -#!/usr/bin/env tsx -/** - * Static analysis script to find: - * 1. Functions with the same name declared in multiple places - * 2. Duplicate code blocks (lines of code that appear multiple times) - */ - -import { readFileSync, readdirSync, statSync } from "fs"; -import { join, relative } from "path"; - -interface FunctionDefinition { - name: string; - file: string; - line: number; - type: "function" | "const" | "async" | "class" | "method"; - signature: string; -} - -interface DuplicateCodeBlock { - lines: string[]; - occurrences: Array<{ file: string; startLine: number }>; -} - -// Patterns to match function definitions -const FUNCTION_PATTERNS = [ - // export function name(...) - /^export\s+(?:async\s+)?function\s+(\w+)\s*\(/, - // export const name = function(...) - /^export\s+const\s+(\w+)\s*=\s*(?:async\s+)?function\s*\(/, - // export const name = (...) - /^export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(/, - // export const name = { ... } - /^export\s+const\s+(\w+)\s*=\s*\{/, - // export class Name - /^export\s+class\s+(\w+)/, - // export const name = class - /^export\s+const\s+(\w+)\s*=\s*class/, - // method: function(...) or method(...) - /^\s*(\w+)\s*:\s*(?:async\s+)?function\s*\(/, - /^\s*(\w+)\s*:\s*(?:async\s+)?\(/, - // method() { or async method() { - /^\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{/, -]; - -// Patterns to match non-exported functions (for internal duplicates) -const INTERNAL_FUNCTION_PATTERNS = [ - /^(?:async\s+)?function\s+(\w+)\s*\(/, - /^const\s+(\w+)\s*=\s*(?:async\s+)?function\s*\(/, - /^const\s+(\w+)\s*=\s*(?:async\s+)?\(/, - /^class\s+(\w+)/, -]; - -function shouldIgnoreFile(filePath: string): boolean { - const ignorePatterns = [ - /node_modules/, - /\.git/, - /dist/, - /\.next/, - /out/, - /\.cache/, - /coverage/, - /\.turbo/, - /pnpm-lock\.yaml/, - /package-lock\.json/, - /yarn\.lock/, - /\.d\.ts$/, - /\.map$/, - /\.log$/, - ]; - return ignorePatterns.some((pattern) => pattern.test(filePath)); -} - -function getAllFiles(dir: string, fileList: string[] = []): string[] { - try { - const files = readdirSync(dir); - for (const file of files) { - const filePath = join(dir, file); - if (shouldIgnoreFile(filePath)) continue; - - try { - const stat = statSync(filePath); - if (stat.isDirectory()) { - getAllFiles(filePath, fileList); - } else if (file.endsWith(".ts") || file.endsWith(".tsx") || file.endsWith(".js") || file.endsWith(".jsx")) { - fileList.push(filePath); - } - } catch { - // Skip files we can't access - } - } - } catch { - // Skip directories we can't access - } - return fileList; -} - -function extractFunctions(filePath: string, content: string): FunctionDefinition[] { - const functions: FunctionDefinition[] = []; - const lines = content.split("\n"); - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - // Check export patterns first - for (const pattern of FUNCTION_PATTERNS) { - const match = line.match(pattern); - if (match) { - const name = match[1]; - functions.push({ - name, - file: filePath, - line: i + 1, - type: line.includes("class") ? "class" : line.includes("async") ? "async" : line.includes("function") ? "function" : "const", - signature: line.trim(), - }); - break; - } - } - } - - return functions; -} - -function findDuplicateFunctions(functions: FunctionDefinition[]): Map { - const byName = new Map(); - - for (const func of functions) { - if (!byName.has(func.name)) { - byName.set(func.name, []); - } - byName.get(func.name)!.push(func); - } - - // Filter to only duplicates - const duplicates = new Map(); - for (const [name, defs] of byName) { - if (defs.length > 1) { - duplicates.set(name, defs); - } - } - - return duplicates; -} - -function findDuplicateCodeBlocks(files: string[], minLines: number = 5): DuplicateCodeBlock[] { - const codeBlocks = new Map>(); - - for (const file of files) { - try { - const content = readFileSync(file, "utf-8"); - const lines = content.split("\n"); - - // Extract code blocks of minLines length - for (let i = 0; i <= lines.length - minLines; i++) { - const block = lines.slice(i, i + minLines).join("\n").trim(); - - // Skip blocks that are too generic (mostly whitespace, comments, etc.) - const nonWhitespace = block.replace(/\s+/g, ""); - if (nonWhitespace.length < 20) continue; - - // Skip blocks that are mostly comments - const commentRatio = (block.match(/\/\/|\/\*|\*/g) || []).length / block.split("\n").length; - if (commentRatio > 0.5) continue; - - const key = block; - if (!codeBlocks.has(key)) { - codeBlocks.set(key, []); - } - codeBlocks.get(key)!.push({ file, startLine: i + 1 }); - } - } catch { - // Skip files we can't read - } - } - - // Filter to only duplicates (appearing in multiple files or multiple times in same file) - const duplicates: DuplicateCodeBlock[] = []; - for (const [block, occurrences] of codeBlocks) { - // Group by file to find true duplicates across files - const byFile = new Map(); - for (const occ of occurrences) { - byFile.set(occ.file, (byFile.get(occ.file) || 0) + 1); - } - - // Only consider it a duplicate if it appears in multiple files OR multiple times in one file - if (byFile.size > 1 || Array.from(byFile.values()).some(count => count > 1)) { - duplicates.push({ - lines: block.split("\n"), - occurrences, - }); - } - } - - return duplicates; -} - -function main() { - const rootDir = join(__dirname, ".."); - console.log("šŸ” Analyzing codebase for duplicates...\n"); - console.log(`Root directory: ${rootDir}\n`); - - // Get all TypeScript/JavaScript files - const files = getAllFiles(rootDir); - console.log(`Found ${files.length} files to analyze\n`); - - // Find duplicate functions - console.log("=".repeat(80)); - console.log("DUPLICATE FUNCTION ANALYSIS"); - console.log("=".repeat(80)); - - const allFunctions: FunctionDefinition[] = []; - for (const file of files) { - try { - const content = readFileSync(file, "utf-8"); - const functions = extractFunctions(file, content); - allFunctions.push(...functions); - } catch { - // Skip files we can't read - } - } - - const duplicateFunctions = findDuplicateFunctions(allFunctions); - - if (duplicateFunctions.size === 0) { - console.log("āœ… No duplicate function names found!\n"); - } else { - console.log(`āš ļø Found ${duplicateFunctions.size} function(s) with duplicate names:\n`); - - for (const [name, defs] of Array.from(duplicateFunctions.entries()).sort()) { - console.log(`\nšŸ“Œ Function: ${name}`); - console.log(` Found ${defs.length} definition(s):`); - for (const def of defs) { - const relPath = relative(rootDir, def.file); - console.log(` - ${relPath}:${def.line} (${def.type})`); - console.log(` ${def.signature.substring(0, 80)}${def.signature.length > 80 ? "..." : ""}`); - } - } - console.log("\n"); - } - - // Find duplicate code blocks - console.log("=".repeat(80)); - console.log("DUPLICATE CODE BLOCK ANALYSIS"); - console.log("=".repeat(80)); - console.log("(Looking for blocks of 5+ lines that appear multiple times)\n"); - - const duplicateBlocks = findDuplicateCodeBlocks(files, 5); - - if (duplicateBlocks.length === 0) { - console.log("āœ… No significant duplicate code blocks found!\n"); - } else { - // Sort by number of occurrences - duplicateBlocks.sort((a, b) => b.occurrences.length - a.occurrences.length); - - console.log(`āš ļø Found ${duplicateBlocks.length} duplicate code block(s):\n`); - - // Show top 20 duplicates - const topDuplicates = duplicateBlocks.slice(0, 20); - for (let i = 0; i < topDuplicates.length; i++) { - const block = topDuplicates[i]; - console.log(`\nšŸ“Œ Duplicate Block #${i + 1} (${block.occurrences.length} occurrence(s)):`); - - // Group by file - const byFile = new Map(); - for (const occ of block.occurrences) { - if (!byFile.has(occ.file)) { - byFile.set(occ.file, []); - } - byFile.get(occ.file)!.push(occ.startLine); - } - - for (const [file, lines] of byFile) { - const relPath = relative(rootDir, file); - console.log(` ${relPath}:`); - for (const line of lines) { - console.log(` - Line ${line}`); - } - } - - console.log(` Preview (first 3 lines):`); - for (let j = 0; j < Math.min(3, block.lines.length); j++) { - console.log(` ${block.lines[j]?.substring(0, 70)}${(block.lines[j]?.length || 0) > 70 ? "..." : ""}`); - } - } - - if (duplicateBlocks.length > 20) { - console.log(`\n ... and ${duplicateBlocks.length - 20} more duplicate blocks`); - } - console.log("\n"); - } - - // Summary - console.log("=".repeat(80)); - console.log("SUMMARY"); - console.log("=".repeat(80)); - console.log(`Total files analyzed: ${files.length}`); - console.log(`Total functions found: ${allFunctions.length}`); - console.log(`Duplicate function names: ${duplicateFunctions.size}`); - console.log(`Duplicate code blocks: ${duplicateBlocks.length}`); - console.log("=".repeat(80)); -} - -main(); diff --git a/scripts/analyze-utils-migration.ts b/scripts/analyze-utils-migration.ts deleted file mode 100644 index 05d6d89a0..000000000 --- a/scripts/analyze-utils-migration.ts +++ /dev/null @@ -1,286 +0,0 @@ -#!/usr/bin/env tsx -/** - * Analysis script specifically for the utils package migration. - * Finds: - * 1. Functions that exist in both old utils.ts and new utils package - * 2. Functions that should be migrated but haven't been - * 3. Remaining imports from old utils - */ - -import { readFileSync, readdirSync, statSync } from "fs"; -import { join, relative } from "path"; - -interface FunctionInfo { - name: string; - file: string; - line: number; - signature: string; -} - -function shouldIgnoreFile(filePath: string): boolean { - const ignorePatterns = [ - /node_modules/, - /\.git/, - /dist/, - /\.next/, - /out/, - /\.cache/, - /coverage/, - /\.turbo/, - /pnpm-lock\.yaml/, - /package-lock\.json/, - /yarn\.lock/, - /\.d\.ts$/, - /\.map$/, - /\.log$/, - /scripts\/analyze/, - ]; - return ignorePatterns.some((pattern) => pattern.test(filePath)); -} - -function getAllFiles(dir: string, fileList: string[] = []): string[] { - try { - const files = readdirSync(dir); - for (const file of files) { - const filePath = join(dir, file); - if (shouldIgnoreFile(filePath)) continue; - - try { - const stat = statSync(filePath); - if (stat.isDirectory()) { - getAllFiles(filePath, fileList); - } else if (file.endsWith(".ts") || file.endsWith(".tsx") || file.endsWith(".js") || file.endsWith(".jsx")) { - fileList.push(filePath); - } - } catch { - // Skip files we can't access - } - } - } catch { - // Skip directories we can't access - } - return fileList; -} - -function extractExportedFunctions(filePath: string, content: string): FunctionInfo[] { - const functions: FunctionInfo[] = []; - const lines = content.split("\n"); - - // Pattern to match exported functions/constants - const exportPattern = /^export\s+(?:async\s+)?(?:function|const|class)\s+(\w+)/; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const match = line.match(exportPattern); - if (match) { - functions.push({ - name: match[1], - file: filePath, - line: i + 1, - signature: line.trim(), - }); - } - } - - return functions; -} - -function findImports(content: string, importPath: string): string[] { - const imports: string[] = []; - const lines = content.split("\n"); - - // Match: import { ... } from "path" or import ... from "path" - const importPattern = new RegExp(`from\\s+["']${importPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["']`); - - for (const line of lines) { - if (importPattern.test(line)) { - // Extract imported names - const namedImports = line.match(/\{([^}]+)\}/); - if (namedImports) { - const names = namedImports[1].split(",").map(n => n.trim().split(/\s+as\s+/)[0]); - imports.push(...names); - } else { - // Default import - const defaultMatch = line.match(/import\s+(\w+)/); - if (defaultMatch) { - imports.push(defaultMatch[1]); - } - } - } - } - - return imports; -} - -function main() { - const rootDir = join(__dirname, ".."); - console.log("šŸ” Analyzing utils package migration status...\n"); - - // Get all files - const files = getAllFiles(rootDir); - - // 1. Check what's exported from old utils.ts - const oldUtilsPath = join(rootDir, "packages/api/src/utils.ts"); - let oldUtilsExports: FunctionInfo[] = []; - try { - const oldUtilsContent = readFileSync(oldUtilsPath, "utf-8"); - oldUtilsExports = extractExportedFunctions(oldUtilsPath, oldUtilsContent); - console.log(`šŸ“¦ Old utils.ts exports: ${oldUtilsExports.length} items`); - for (const exp of oldUtilsExports) { - console.log(` - ${exp.name}`); - } - } catch { - console.log("āš ļø Could not read old utils.ts"); - } - - // 2. Check what's exported from new utils package - const newUtilsPath = join(rootDir, "packages/utils/src"); - const newUtilsFiles = getAllFiles(newUtilsPath); - const newUtilsExports: FunctionInfo[] = []; - for (const file of newUtilsFiles) { - try { - const content = readFileSync(file, "utf-8"); - const exports = extractExportedFunctions(file, content); - newUtilsExports.push(...exports); - } catch { - // Skip - } - } - console.log(`\nšŸ“¦ New @forge/utils exports: ${newUtilsExports.length} items`); - const newUtilsNames = new Set(newUtilsExports.map(e => e.name)); - for (const name of Array.from(newUtilsNames).sort()) { - console.log(` - ${name}`); - } - - // 3. Find imports from old utils - console.log("\n" + "=".repeat(80)); - console.log("IMPORTS FROM OLD UTILS"); - console.log("=".repeat(80)); - - const oldUtilsImports: Map = new Map(); - for (const file of files) { - try { - const content = readFileSync(file, "utf-8"); - const imports = findImports(content, "../utils"); - if (imports.length > 0) { - oldUtilsImports.set(file, imports); - } - } catch { - // Skip - } - } - - if (oldUtilsImports.size === 0) { - console.log("āœ… No imports from old utils found!\n"); - } else { - console.log(`āš ļø Found ${oldUtilsImports.size} file(s) importing from old utils:\n`); - for (const [file, imports] of Array.from(oldUtilsImports.entries()).sort()) { - const relPath = relative(rootDir, file); - console.log(` ${relPath}:`); - for (const imp of imports) { - console.log(` - ${imp}`); - } - } - console.log(); - } - - // 4. Find imports from new utils - console.log("=".repeat(80)); - console.log("IMPORTS FROM NEW @forge/utils"); - console.log("=".repeat(80)); - - const newUtilsImports: Map = new Map(); - for (const file of files) { - try { - const content = readFileSync(file, "utf-8"); - const imports = findImports(content, "@forge/utils"); - if (imports.length > 0) { - newUtilsImports.set(file, imports); - } - } catch { - // Skip - } - } - - console.log(`āœ… Found ${newUtilsImports.size} file(s) importing from new utils\n`); - - // 5. Find duplicate function names (actual utility functions) - console.log("=".repeat(80)); - console.log("DUPLICATE UTILITY FUNCTIONS"); - console.log("=".repeat(80)); - - const allExports = new Map(); - for (const file of files) { - try { - const content = readFileSync(file, "utf-8"); - const exports = extractExportedFunctions(file, content); - for (const exp of exports) { - if (!allExports.has(exp.name)) { - allExports.set(exp.name, []); - } - allExports.get(exp.name)!.push(exp); - } - } catch { - // Skip - } - } - - // Filter to utility-like function names (not React components, not common keywords) - const utilityNames = [ - "formatDateRange", - "getPermsAsList", - "formatHourTime", - "formatDateTime", - "getFormattedDate", - "hasPermission", - "controlPerms", - "isDiscordAdmin", - "isDiscordMember", - "isDiscordVIP", - "resolveDiscordUserId", - "addRoleToMember", - "removeRoleFromMember", - "log", - "logger", - "sendEmail", - "createForm", - "generateJsonSchema", - "regenerateMediaUrls", - ]; - - const duplicates: Array<{ name: string; locations: FunctionInfo[] }> = []; - for (const name of utilityNames) { - const locations = allExports.get(name) || []; - if (locations.length > 1) { - duplicates.push({ name, locations }); - } - } - - if (duplicates.length === 0) { - console.log("āœ… No duplicate utility functions found!\n"); - } else { - console.log(`āš ļø Found ${duplicates.length} duplicate utility function(s):\n`); - for (const { name, locations } of duplicates) { - console.log(`\nšŸ“Œ ${name} (${locations.length} definition(s)):`); - for (const loc of locations) { - const relPath = relative(rootDir, loc.file); - console.log(` - ${relPath}:${loc.line}`); - console.log(` ${loc.signature.substring(0, 80)}${loc.signature.length > 80 ? "..." : ""}`); - } - } - console.log(); - } - - // 6. Summary - console.log("=".repeat(80)); - console.log("SUMMARY"); - console.log("=".repeat(80)); - console.log(`Old utils.ts exports: ${oldUtilsExports.length}`); - console.log(`New @forge/utils exports: ${newUtilsExports.length}`); - console.log(`Files importing from old utils: ${oldUtilsImports.size}`); - console.log(`Files importing from new utils: ${newUtilsImports.size}`); - console.log(`Duplicate utility functions: ${duplicates.length}`); - console.log("=".repeat(80)); -} - -main(); From 8801f7617d02b37cce10c6571e6b69c3dbdb1897 Mon Sep 17 00:00:00 2001 From: Dylan Vidal Date: Sat, 7 Mar 2026 18:32:29 -0500 Subject: [PATCH 25/27] chore: format --- packages/api/src/routers/forms.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index d9210b973..9a9c66b1a 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -1,7 +1,7 @@ import type { TRPCRouterRecord } from "@trpc/server"; +import type { JSONSchema7 } from "json-schema"; import { TRPCError } from "@trpc/server"; import { and, count, desc, eq, inArray, lt, sql } from "drizzle-orm"; -import type { JSONSchema7 } from "json-schema"; import jsonSchemaToZod from "json-schema-to-zod"; import * as z from "zod"; From af2dd92109fa6def450a332488b61004aa7a1c6d Mon Sep 17 00:00:00 2001 From: Dylan Vidal Date: Sat, 7 Mar 2026 18:58:31 -0500 Subject: [PATCH 26/27] chore: format and client/server separation --- .../_components/forms/connection-handler.ts | 3 +- .../forms/form-responder-client.tsx | 2 +- .../src/app/_components/forms/form-runner.tsx | 2 +- .../forms/form-view-edit-client.tsx | 2 +- apps/blade/src/app/judge/session/route.ts | 4 +- packages/api/package.json | 1 + packages/api/src/routers/auth.ts | 6 +- packages/api/src/routers/dues-payment.ts | 4 +- packages/api/src/routers/event-feedback.ts | 2 +- packages/api/src/routers/event.ts | 5 +- packages/api/src/routers/forms.ts | 4 +- packages/api/src/routers/hackers/mutations.ts | 3 +- packages/api/src/routers/member.ts | 3 +- packages/api/src/routers/misc.ts | 2 +- packages/api/src/routers/roles.ts | 3 +- packages/api/src/trpc.ts | 8 +- packages/auth/src/config.ts | 2 +- packages/db/scripts/seed_devdb.ts | 3 +- packages/utils/package.json | 11 ++- packages/utils/src/discord.ts | 2 + packages/utils/src/forms.client.ts | 88 +++++++++++++++++++ packages/utils/src/forms.ts | 87 +----------------- packages/utils/src/google.ts | 2 + packages/utils/src/index.ts | 6 +- packages/utils/src/permissions.server.ts | 61 +++++++++++++ packages/utils/src/permissions.ts | 50 ----------- packages/utils/src/stripe.ts | 2 + pnpm-lock.yaml | 7 +- 28 files changed, 211 insertions(+), 164 deletions(-) create mode 100644 packages/utils/src/forms.client.ts create mode 100644 packages/utils/src/permissions.server.ts diff --git a/apps/blade/src/app/_components/forms/connection-handler.ts b/apps/blade/src/app/_components/forms/connection-handler.ts index 925a3ca89..493148bcd 100644 --- a/apps/blade/src/app/_components/forms/connection-handler.ts +++ b/apps/blade/src/app/_components/forms/connection-handler.ts @@ -4,7 +4,8 @@ import { stringify } from "superjson"; import { appRouter } from "@forge/api"; import { auth } from "@forge/auth/server"; -import { discord, trpc } from "@forge/utils"; +import { trpc } from "@forge/utils"; +import * as discord from "@forge/utils/discord"; import { api } from "~/trpc/server"; diff --git a/apps/blade/src/app/_components/forms/form-responder-client.tsx b/apps/blade/src/app/_components/forms/form-responder-client.tsx index 5866b05ee..5cef9b545 100644 --- a/apps/blade/src/app/_components/forms/form-responder-client.tsx +++ b/apps/blade/src/app/_components/forms/form-responder-client.tsx @@ -5,7 +5,7 @@ import Link from "next/link"; import { CheckCircle2, Loader2, XCircle } from "lucide-react"; import type { FORMS } from "@forge/consts"; -import type { forms } from "@forge/utils"; +import type * as forms from "@forge/utils/forms.client"; import { Button } from "@forge/ui/button"; import { Card } from "@forge/ui/card"; diff --git a/apps/blade/src/app/_components/forms/form-runner.tsx b/apps/blade/src/app/_components/forms/form-runner.tsx index 7b8d49fcb..fafbdb15a 100644 --- a/apps/blade/src/app/_components/forms/form-runner.tsx +++ b/apps/blade/src/app/_components/forms/form-runner.tsx @@ -5,7 +5,7 @@ import { useEffect, useState } from "react"; import type { FORMS } from "@forge/consts"; import { Button } from "@forge/ui/button"; import { Card } from "@forge/ui/card"; -import { forms } from "@forge/utils"; +import * as forms from "@forge/utils/forms.client"; import { InstructionResponseCard } from "~/app/_components/forms/instruction-response-card"; import { QuestionResponseCard } from "~/app/_components/forms/question-response-card"; diff --git a/apps/blade/src/app/_components/forms/form-view-edit-client.tsx b/apps/blade/src/app/_components/forms/form-view-edit-client.tsx index 5c6b6fec2..52ee03eb0 100644 --- a/apps/blade/src/app/_components/forms/form-view-edit-client.tsx +++ b/apps/blade/src/app/_components/forms/form-view-edit-client.tsx @@ -4,7 +4,7 @@ import { useMemo, useState } from "react"; import { Loader2 } from "lucide-react"; import type { FORMS } from "@forge/consts"; -import type { forms } from "@forge/utils"; +import type * as forms from "@forge/utils/forms.client"; import { api } from "~/trpc/react"; import { useSubmissionSuccess } from "./_hooks/useSubmissionSuccess"; diff --git a/apps/blade/src/app/judge/session/route.ts b/apps/blade/src/app/judge/session/route.ts index c32cd0d44..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 { permissions } from "@forge/utils"; +import * as permissionsServer from "@forge/utils/permissions.server"; export async function GET() { - const row = await permissions.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/packages/api/package.json b/packages/api/package.json index fb3c69ff3..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" diff --git a/packages/api/src/routers/auth.ts b/packages/api/src/routers/auth.ts index 4bc2a93cd..b30b43b90 100644 --- a/packages/api/src/routers/auth.ts +++ b/packages/api/src/routers/auth.ts @@ -1,7 +1,9 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { invalidateSessionToken } from "@forge/auth/server"; -import { discord, permissions } from "@forge/utils"; +import { permissions } from "@forge/utils"; +import * as discord from "@forge/utils/discord"; +import * as permissionsServer from "@forge/utils/permissions.server"; import { protectedProcedure, publicProcedure } from "../trpc"; @@ -35,7 +37,7 @@ export const authRouter = { return discord.isDiscordMember(ctx.session.user); }), getJudgeStatus: publicProcedure.query(async () => { - const isJudge = await permissions.isJudgeAdmin(); + const isJudge = await permissionsServer.isJudgeAdmin(); return isJudge; }), diff --git a/packages/api/src/routers/dues-payment.ts b/packages/api/src/routers/dues-payment.ts index 7d57f7720..0db13fabc 100644 --- a/packages/api/src/routers/dues-payment.ts +++ b/packages/api/src/routers/dues-payment.ts @@ -7,7 +7,9 @@ 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 { discord, permissions, stripe } from "@forge/utils"; +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"; diff --git a/packages/api/src/routers/event-feedback.ts b/packages/api/src/routers/event-feedback.ts index 8f44d18a6..aaa5ebaa1 100644 --- a/packages/api/src/routers/event-feedback.ts +++ b/packages/api/src/routers/event-feedback.ts @@ -2,7 +2,7 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { z } from "zod"; import { DISCORD } from "@forge/consts"; -import { discord } from "@forge/utils"; +import * as discord from "@forge/utils/discord"; import { permProcedure } from "../trpc"; diff --git a/packages/api/src/routers/event.ts b/packages/api/src/routers/event.ts index 35a6df8fb..839cdb584 100644 --- a/packages/api/src/routers/event.ts +++ b/packages/api/src/routers/event.ts @@ -29,7 +29,10 @@ import { InsertEventSchema, Member, } from "@forge/db/schemas/knight-hacks"; -import { discord, forms, google, logger, permissions } from "@forge/utils"; +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"; diff --git a/packages/api/src/routers/forms.ts b/packages/api/src/routers/forms.ts index 9a9c66b1a..b19f2fcf9 100644 --- a/packages/api/src/routers/forms.ts +++ b/packages/api/src/routers/forms.ts @@ -20,7 +20,9 @@ import { TrpcFormConnection, TrpcFormConnectionSchema, } from "@forge/db/schemas/knight-hacks"; -import { discord, forms, logger, permissions } from "@forge/utils"; +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"; diff --git a/packages/api/src/routers/hackers/mutations.ts b/packages/api/src/routers/hackers/mutations.ts index bf37ceb1c..38b54597c 100644 --- a/packages/api/src/routers/hackers/mutations.ts +++ b/packages/api/src/routers/hackers/mutations.ts @@ -16,7 +16,8 @@ import { HackerEventAttendee, InsertHackerSchema, } from "@forge/db/schemas/knight-hacks"; -import { discord, logger, permissions } from "@forge/utils"; +import { logger, permissions } from "@forge/utils"; +import * as discord from "@forge/utils/discord"; import { minioClient } from "../../minio/minio-client"; import { permProcedure, protectedProcedure } from "../../trpc"; diff --git a/packages/api/src/routers/member.ts b/packages/api/src/routers/member.ts index 8b3e4315c..05bbb58b3 100644 --- a/packages/api/src/routers/member.ts +++ b/packages/api/src/routers/member.ts @@ -28,7 +28,8 @@ import { Member, OtherCompanies, } from "@forge/db/schemas/knight-hacks"; -import { discord, logger, permissions } from "@forge/utils"; +import { logger, permissions } from "@forge/utils"; +import * as discord from "@forge/utils/discord"; import { minioClient } from "../minio/minio-client"; import { permProcedure, protectedProcedure } from "../trpc"; diff --git a/packages/api/src/routers/misc.ts b/packages/api/src/routers/misc.ts index 58bedb0a3..ee98cd3c0 100644 --- a/packages/api/src/routers/misc.ts +++ b/packages/api/src/routers/misc.ts @@ -4,7 +4,7 @@ import { Routes } from "discord-api-types/v10"; import { z } from "zod"; import { DISCORD, FORMS, TEAM } from "@forge/consts"; -import { discord } from "@forge/utils"; +import * as discord from "@forge/utils/discord"; import { protectedProcedure } from "../trpc"; diff --git a/packages/api/src/routers/roles.ts b/packages/api/src/routers/roles.ts index e2f3fd31f..784b711c9 100644 --- a/packages/api/src/routers/roles.ts +++ b/packages/api/src/routers/roles.ts @@ -8,7 +8,8 @@ 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 { discord, logger, permissions } from "@forge/utils"; +import { logger, permissions } from "@forge/utils"; +import * as discord from "@forge/utils/discord"; import { permProcedure, protectedProcedure } from "../trpc"; diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index 120ba36da..6f756e658 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -18,7 +18,9 @@ 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 { discord, permissions } from "@forge/utils"; +import { permissions } from "@forge/utils"; +import * as discord from "@forge/utils/discord"; +import * as permissionsServer from "@forge/utils/permissions.server"; /** * 1. CONTEXT @@ -182,13 +184,13 @@ export const judgeProcedure = publicProcedure.use(async ({ ctx, next }) => { if (ctx.session) { isAdmin = await discord.isDiscordAdmin(ctx.session.user); } - const isJudge = await permissions.isJudgeAdmin(); + const isJudge = await permissionsServer.isJudgeAdmin(); if (!isAdmin && !isJudge) { throw new TRPCError({ code: "UNAUTHORIZED" }); } - const judgeSession = await permissions.getJudgeSessionFromCookie(); + const judgeSession = await permissionsServer.getJudgeSessionFromCookie(); return next({ ctx: { diff --git a/packages/auth/src/config.ts b/packages/auth/src/config.ts index 3516e0978..6059f02f8 100644 --- a/packages/auth/src/config.ts +++ b/packages/auth/src/config.ts @@ -7,7 +7,7 @@ import { eq } from "drizzle-orm"; import { db } from "@forge/db/client"; import { Account, Session, User, Verifications } from "@forge/db/schemas/auth"; -import { discord } from "../../utils/src"; +import * as discord from "../../utils/src/discord"; import { env } from "./env"; export const isSecureContext = env.NODE_ENV !== "development"; diff --git a/packages/db/scripts/seed_devdb.ts b/packages/db/scripts/seed_devdb.ts index b33ccf72e..56b5566ab 100644 --- a/packages/db/scripts/seed_devdb.ts +++ b/packages/db/scripts/seed_devdb.ts @@ -31,8 +31,9 @@ import { stringify } from "superjson"; import { DISCORD, MINIO } from "@forge/consts"; +// Scripts can use relative imports to avoid circular dependencies import { minioClient } from "../../api/src/minio/minio-client"; -import { discord } from "../../utils/src"; +import * as discord from "../../utils/src/discord"; import { env } from "../src/env"; import * as authSchema from "../src/schemas/auth"; import * as knightHacksSchema from "../src/schemas/knight-hacks"; diff --git a/packages/utils/package.json b/packages/utils/package.json index 65acfe5d7..678f5c08f 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -4,7 +4,13 @@ "version": "0.1.0", "type": "module", "exports": { - ".": "./src/index.ts" + ".": "./src/index.ts", + "./discord": "./src/discord.ts", + "./forms": "./src/forms.ts", + "./forms.client": "./src/forms.client.ts", + "./google": "./src/google.ts", + "./permissions.server": "./src/permissions.server.ts", + "./stripe": "./src/stripe.ts" }, "license": "MIT", "scripts": { @@ -36,6 +42,7 @@ "@discordjs/rest": "^2.4.0", "@t3-oss/env-nextjs": "^0.11.1", "discord-api-types": "^0.37.113", - "googleapis": "^144.0.0" + "googleapis": "^144.0.0", + "server-only": "^0.0.1" } } diff --git a/packages/utils/src/discord.ts b/packages/utils/src/discord.ts index 173e174ae..928286330 100644 --- a/packages/utils/src/discord.ts +++ b/packages/utils/src/discord.ts @@ -3,6 +3,8 @@ // api client. // +import "server-only"; + import type { APIGuildMember } from "discord-api-types/v10"; import { REST } from "@discordjs/rest"; import { Routes } from "discord-api-types/v10"; diff --git a/packages/utils/src/forms.client.ts b/packages/utils/src/forms.client.ts new file mode 100644 index 000000000..5de9c8615 --- /dev/null +++ b/packages/utils/src/forms.client.ts @@ -0,0 +1,88 @@ +import z from "zod"; + +import type { FORMS } from "@forge/consts"; + +/** UI state in the client */ +export type FormResponseUI = Partial< + Record +>; + +/** JSON-safe payload what zodValidator will validate */ +export type FormResponsePayload = Partial< + Record +>; + +export const getValidatorResponse = ( + zodValidator: string, + responses: FormResponseUI, + form: FORMS.FormType, +) => { + // eslint-disable-next-line @typescript-eslint/no-implied-eval, @typescript-eslint/no-unsafe-call + const zodSchema = new Function("z", `return ${zodValidator}`)( + z, + ) as z.ZodSchema; + + const payload = normalizeResponses(responses, form); + + return zodSchema.safeParse(payload); +}; + +// normalized responses needed for zod validation and therefore proc responseData +export const normalizeResponses = ( + responses: FormResponseUI, + form: FORMS.FormType, +): FormResponsePayload => { + const out: FormResponsePayload = {}; + + for (const q of form.questions) { + const key = q.question; + const v = responses[key]; + + // drop missing/empty values + if (v == null || v === "" || (Array.isArray(v) && v.length === 0)) continue; + + // dates -> strings based on question type + if (v instanceof Date) { + if (q.type === "DATE") out[key] = v.toISOString().slice(0, 10); + else if (q.type === "TIME") out[key] = v.toTimeString().slice(0, 5); + else continue; // unexpected Date for non-date/time question + continue; + } + + // if your UI sometimes passes "true"/"false" strings, normalize them here + if (q.type === "BOOLEAN" && typeof v === "string") { + if (v === "true") out[key] = true; + else if (v === "false") out[key] = false; + else continue; + continue; + } + + out[key] = v; + } + + return out; +}; + +// get specific validator error for question +export const getValidationError = ( + question: FORMS.QuestionValidatorType, + zodValidator: string, + responses: FormResponseUI, + form: FORMS.FormType, +) => { + const validatorResponse = getValidatorResponse(zodValidator, responses, form); + if (validatorResponse.success) return null; + + const issue = validatorResponse.error.issues.find((i) => { + const k = i.path[0]; + return typeof k === "string" && k === question.question; + }); + + return issue?.message ?? null; +}; + +export const isFormValid = ( + zodValidator: string, + responses: FormResponseUI, + form: FORMS.FormType, +) => getValidatorResponse(zodValidator, responses, form).success; diff --git a/packages/utils/src/forms.ts b/packages/utils/src/forms.ts index 0b74c36fe..68e67dd5d 100644 --- a/packages/utils/src/forms.ts +++ b/packages/utils/src/forms.ts @@ -1,3 +1,5 @@ +import "server-only"; + import type { JSONSchema7 } from "json-schema"; import { TRPCError } from "@trpc/server"; import z from "zod"; @@ -246,88 +248,3 @@ export async function createForm(input: CreateFormType): Promise { return form; } - -/** UI state in the client */ -export type FormResponseUI = Partial< - Record ->; - -/** JSON-safe payload what zodValidator will validate */ -export type FormResponsePayload = Partial< - Record ->; - -export const getValidatorResponse = ( - zodValidator: string, - responses: FormResponseUI, - form: FORMS.FormType, -) => { - // eslint-disable-next-line @typescript-eslint/no-implied-eval, @typescript-eslint/no-unsafe-call - const zodSchema = new Function("z", `return ${zodValidator}`)( - z, - ) as z.ZodSchema; - - const payload = normalizeResponses(responses, form); - - return zodSchema.safeParse(payload); -}; - -// normalized responses needed for zod validation and therefore proc responseData -export const normalizeResponses = ( - responses: FormResponseUI, - form: FORMS.FormType, -): FormResponsePayload => { - const out: FormResponsePayload = {}; - - for (const q of form.questions) { - const key = q.question; - const v = responses[key]; - - // drop missing/empty values - if (v == null || v === "" || (Array.isArray(v) && v.length === 0)) continue; - - // dates -> strings based on question type - if (v instanceof Date) { - if (q.type === "DATE") out[key] = v.toISOString().slice(0, 10); - else if (q.type === "TIME") out[key] = v.toTimeString().slice(0, 5); - else continue; // unexpected Date for non-date/time question - continue; - } - - // if your UI sometimes passes "true"/"false" strings, normalize them here - if (q.type === "BOOLEAN" && typeof v === "string") { - if (v === "true") out[key] = true; - else if (v === "false") out[key] = false; - else continue; - continue; - } - - out[key] = v; - } - - return out; -}; - -// get specific validator error for question -export const getValidationError = ( - question: FORMS.QuestionValidatorType, - zodValidator: string, - responses: FormResponseUI, - form: FORMS.FormType, -) => { - const validatorResponse = getValidatorResponse(zodValidator, responses, form); - if (validatorResponse.success) return null; - - const issue = validatorResponse.error.issues.find((i) => { - const k = i.path[0]; - return typeof k === "string" && k === question.question; - }); - - return issue?.message ?? null; -}; - -export const isFormValid = ( - zodValidator: string, - responses: FormResponseUI, - form: FORMS.FormType, -) => getValidatorResponse(zodValidator, responses, form).success; diff --git a/packages/utils/src/google.ts b/packages/utils/src/google.ts index 8ba0ba83e..51f0b1b0e 100644 --- a/packages/utils/src/google.ts +++ b/packages/utils/src/google.ts @@ -1,3 +1,5 @@ +import "server-only"; + import { google } from "googleapis"; import { EVENTS } from "@forge/consts"; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index dbc5b0523..c805bd0e0 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,12 +1,10 @@ -export * as discord from "./discord"; export * as events from "./events"; -export * as forms from "./forms"; -export * as google from "./google"; export * as hackathons from "./hackathons"; export { logger } from "./logger"; export * as permissions from "./permissions"; -export { stripe } from "./stripe"; export * as time from "./time"; export * as trpc from "./trpc"; +// Note: stripe is server-only and should be imported from @forge/utils/stripe + export const name = "utils"; diff --git a/packages/utils/src/permissions.server.ts b/packages/utils/src/permissions.server.ts new file mode 100644 index 000000000..0567c5fbe --- /dev/null +++ b/packages/utils/src/permissions.server.ts @@ -0,0 +1,61 @@ +import "server-only"; + +import { cookies } from "next/headers"; +import { and, eq, gt } from "drizzle-orm"; + +import { db } from "@forge/db/client"; +import { JudgeSession } from "@forge/db/schemas/auth"; + +import { logger } from "./logger"; + +/** + * Server-only function to check if the current user is a judge admin. + * Uses cookies() from next/headers, so this can only be used in Server Components or Server Actions. + */ +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) { + logger.error("isJudgeAdmin DB check error:", err); + return false; + } +}; + +/** + * Server-only function to get judge session from cookie. + * Uses cookies() from next/headers, so this can only be used in Server Components or Server Actions. + */ +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; +}; diff --git a/packages/utils/src/permissions.ts b/packages/utils/src/permissions.ts index 91a0f5466..a4c7a68a7 100644 --- a/packages/utils/src/permissions.ts +++ b/packages/utils/src/permissions.ts @@ -1,12 +1,6 @@ -import { cookies } from "next/headers"; import { TRPCError } from "@trpc/server"; -import { and, eq, gt } from "drizzle-orm"; import { PERMISSIONS } from "@forge/consts"; -import { db } from "@forge/db/client"; -import { JudgeSession } from "@forge/db/schemas/auth"; - -import { logger } from "./logger"; export const hasPermission = ( userPermissions: string, @@ -48,50 +42,6 @@ export const controlPerms = { }, }; -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) { - logger.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; -}; - export function getPermsAsList(perms: string) { const list = []; const permKeys = Object.keys(PERMISSIONS.PERMISSIONS); diff --git a/packages/utils/src/stripe.ts b/packages/utils/src/stripe.ts index 1b3c63a85..90f8fa73a 100644 --- a/packages/utils/src/stripe.ts +++ b/packages/utils/src/stripe.ts @@ -1,3 +1,5 @@ +import "server-only"; + import Stripe from "stripe"; import { env } from "./env"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f8a7ed6a..26a88c37b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -410,7 +410,7 @@ importers: version: 7.4.4 eslint: specifier: 'catalog:' - version: 9.39.2(jiti@2.6.1) + version: 9.39.2(jiti@1.21.7) prettier: specifier: 'catalog:' version: 3.8.1 @@ -486,7 +486,7 @@ importers: version: 0.37.120 eslint: specifier: 'catalog:' - version: 9.39.2(jiti@1.21.7) + version: 9.39.2(jiti@2.6.1) prettier: specifier: 'catalog:' version: 3.8.1 @@ -1186,6 +1186,9 @@ importers: googleapis: specifier: ^144.0.0 version: 144.0.0 + server-only: + specifier: ^0.0.1 + version: 0.0.1 devDependencies: '@forge/auth': specifier: workspace:* From 87e8e1f5278057f7e3b77767ebdc9de0c726ac7e Mon Sep 17 00:00:00 2001 From: Dylan Vidal Date: Sat, 7 Mar 2026 19:02:44 -0500 Subject: [PATCH 27/27] chore: lint and type --- apps/cron/src/crons/alumni-assign.ts | 3 ++- apps/cron/src/crons/leetcode.ts | 2 +- apps/cron/src/crons/role-sync.ts | 3 ++- packages/api/src/routers/auth.ts | 1 - packages/api/src/trpc.ts | 1 - 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/cron/src/crons/alumni-assign.ts b/apps/cron/src/crons/alumni-assign.ts index 1b9b08e53..b463d1f98 100644 --- a/apps/cron/src/crons/alumni-assign.ts +++ b/apps/cron/src/crons/alumni-assign.ts @@ -3,7 +3,8 @@ import { and, gt, isNotNull, isNull, lte, or } from "drizzle-orm"; import { DISCORD } from "@forge/consts"; import { db } from "@forge/db/client"; import { Member } from "@forge/db/schemas/knight-hacks"; -import { discord, logger } from "@forge/utils"; +import { logger } from "@forge/utils"; +import * as discord from "@forge/utils/discord"; import { CronBuilder } from "../structs/CronBuilder"; diff --git a/apps/cron/src/crons/leetcode.ts b/apps/cron/src/crons/leetcode.ts index 2183d4dd6..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/utils"; +import * as discord from "@forge/utils/discord"; import { env } from "../env"; import { CronBuilder } from "../structs/CronBuilder"; diff --git a/apps/cron/src/crons/role-sync.ts b/apps/cron/src/crons/role-sync.ts index 8d09f2776..36c1a2307 100644 --- a/apps/cron/src/crons/role-sync.ts +++ b/apps/cron/src/crons/role-sync.ts @@ -5,7 +5,8 @@ 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 { discord, logger } from "@forge/utils"; +import { logger } from "@forge/utils"; +import * as discord from "@forge/utils/discord"; import { CronBuilder } from "../structs/CronBuilder"; diff --git a/packages/api/src/routers/auth.ts b/packages/api/src/routers/auth.ts index b30b43b90..36201c6ad 100644 --- a/packages/api/src/routers/auth.ts +++ b/packages/api/src/routers/auth.ts @@ -1,7 +1,6 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { invalidateSessionToken } from "@forge/auth/server"; -import { permissions } from "@forge/utils"; import * as discord from "@forge/utils/discord"; import * as permissionsServer from "@forge/utils/permissions.server"; diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index 6f756e658..904280825 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -18,7 +18,6 @@ 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 { permissions } from "@forge/utils"; import * as discord from "@forge/utils/discord"; import * as permissionsServer from "@forge/utils/permissions.server";