Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import { docsHtml, securitySchemes } from "./docs.js";
import syncIdentityPlugin from "./routes/syncIdentity.js";
import { createRedisModule } from "./redis.js";
import userRoute from "./routes/user.js";
import rsvpRoutes from "./routes/rsvp.js";
/** END ROUTES */

export const instanceId = randomUUID();
Expand Down Expand Up @@ -377,6 +378,9 @@ Otherwise, email [infra@acm.illinois.edu](mailto:infra@acm.illinois.edu) for sup
api.register(apiKeyRoute, { prefix: "/apiKey" });
api.register(clearSessionRoute, { prefix: "/clearSession" });
api.register(userRoute, { prefix: "/users" });
if (app.runEnvironment === "dev") {
api.register(rsvpRoutes, { prefix: "/rsvp" });
}
if (app.runEnvironment === "dev") {
api.register(vendingPlugin, { prefix: "/vending" });
}
Expand Down
126 changes: 126 additions & 0 deletions src/api/routes/rsvp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { FastifyPluginAsync } from "fastify";
import rateLimiter from "api/plugins/rateLimiter.js";
import { withRoles, withTags } from "api/components/index.js";
import { QueryCommand, PutItemCommand } from "@aws-sdk/client-dynamodb";
import { unmarshall, marshall } from "@aws-sdk/util-dynamodb";
import {
DatabaseFetchError,
UnauthenticatedError,
UnauthorizedError,
ValidationError,
} from "common/errors/index.js";
import * as z from "zod/v4";
import { verifyUiucAccessToken } from "api/functions/uin.js";
import { checkPaidMembership } from "api/functions/membership.js";
import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
import { genericConfig } from "common/config.js";
import { AppRoles } from "common/roles.js";

const rsvpRoutes: FastifyPluginAsync = async (fastify, _options) => {
await fastify.register(rateLimiter, {
limit: 30,
duration: 30,
rateLimitIdentifier: "rsvp",
});
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().post(
"/:orgId/event/:eventId",
{
schema: withTags(["RSVP"], {
summary: "Submit an RSVP for an event.",
params: z.object({
eventId: z.string().min(1).meta({
description: "The previously-created event ID in the events API.",
}),
orgId: z.string().min(1).meta({
description: "The organization ID the event belongs to.",
}),
}),
headers: z.object({
"x-uiuc-token": z.jwt().min(1).meta({
description:
"An access token for the user in the UIUC Entra ID tenant.",
}),
Comment on lines +39 to +42
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Script executed:

#!/bin/bash
# Search for z.jwt() usage and any custom JWT schema definitions
rg -n "z\.jwt|\.jwt\(\)" --type ts -C 3

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

src/api/routes/syncIdentity.ts-64-      {
src/api/routes/syncIdentity.ts-65-        schema: withTags(["Generic"], {
src/api/routes/syncIdentity.ts-66-          headers: z.object({
src/api/routes/syncIdentity.ts:67:            "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/syncIdentity.ts-68-              description:
src/api/routes/syncIdentity.ts-69-                "An access token for the user in the UIUC Entra ID tenant.",
src/api/routes/syncIdentity.ts-70-            }),
--
src/api/routes/syncIdentity.ts-150-      {
src/api/routes/syncIdentity.ts-151-        schema: withTags(["Generic"], {
src/api/routes/syncIdentity.ts-152-          headers: z.object({
src/api/routes/syncIdentity.ts:153:            "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/syncIdentity.ts-154-              description:
src/api/routes/syncIdentity.ts-155-                "An access token for the user in the UIUC Entra ID tenant.",
src/api/routes/syncIdentity.ts-156-            }),
--
src/api/routes/rsvp.ts-40-          }),
src/api/routes/rsvp.ts-41-        }),
src/api/routes/rsvp.ts-42-        headers: z.object({
src/api/routes/rsvp.ts:43:          "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/rsvp.ts-44-            description:
src/api/routes/rsvp.ts-45-              "An access token for the user in the UIUC Entra ID tenant.",
src/api/routes/rsvp.ts-46-          }),
--
src/api/routes/rsvp.ts-92-          }),
src/api/routes/rsvp.ts-93-        }),
src/api/routes/rsvp.ts-94-        headers: z.object({
src/api/routes/rsvp.ts:95:          "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/rsvp.ts-96-            description:
src/api/routes/rsvp.ts-97-              "An access token for the user in the UIUC Entra ID tenant.",
src/api/routes/rsvp.ts-98-          }),
--
src/api/routes/v2/mobileWallet.ts-27-      schema: withTags(["Mobile Wallet"], {
src/api/routes/v2/mobileWallet.ts-28-        summary: "Retrieve mobile wallet pass for ACM member.",
src/api/routes/v2/mobileWallet.ts-29-        headers: z.object({
src/api/routes/v2/mobileWallet.ts:30:          "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/v2/mobileWallet.ts-31-            description:
src/api/routes/v2/mobileWallet.ts-32-              "An access token for the user in the UIUC Entra ID tenant.",
src/api/routes/v2/mobileWallet.ts-33-          }),
--
src/api/routes/v2/membership.ts-77-      {
src/api/routes/v2/membership.ts-78-        schema: withTags(["Membership"], {
src/api/routes/v2/membership.ts-79-          headers: z.object({
src/api/routes/v2/membership.ts:80:            "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/v2/membership.ts-81-              description:
src/api/routes/v2/membership.ts-82-                "An access token for the user in the UIUC Entra ID tenant.",
src/api/routes/v2/membership.ts-83-            }),
--
src/api/routes/membership.ts-45-      {
src/api/routes/membership.ts-46-        schema: withTags(["Membership"], {
src/api/routes/membership.ts-47-          headers: z.object({
src/api/routes/membership.ts:48:            "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/membership.ts-49-              description:
src/api/routes/membership.ts-50-                "An access token for the user in the UIUC Entra ID tenant.",
src/api/routes/membership.ts-51-            }),

</shell_result>


Script executed:

#!/bin/bash
# Check for Zod version in package.json and any custom extensions
cat package.json | grep -A2 -B2 "zod"

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 1

</shell_result>


Script executed:

#!/bin/bash
# Read the specific file to see context around lines 43-46 and 95-98
head -100 src/api/routes/rsvp.ts | tail -60

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

        }),
        headers: z.object({
          "x-uiuc-token": z.jwt().min(1).meta({
            description:
              "An access token for the user in the UIUC Entra ID tenant.",
          }),
        }),
      }),
    },
    async (request, reply) => {
      const accessToken = request.headers["x-uiuc-token"];
      const verifiedData = await verifyUiucAccessToken({
        accessToken,
        logger: request.log,
      });
      const { userPrincipalName: upn, givenName, surname } = verifiedData;
      const netId = upn.replace("@illinois.edu", "");
      if (netId.includes("@")) {
        request.log.error(
          `Found UPN ${upn} which cannot be turned into NetID via simple replacement.`,
        );
        throw new ValidationError({
          message: "ID token could not be parsed.",
        });
      }
      const isPaidMember = await checkPaidMembership({
        netId,
        dynamoClient: fastify.dynamoClient,
        redisClient: fastify.redisClient,
        logger: request.log,
      });
      const entry = {
        partitionKey: `${request.params.eventId}#${upn}`,
        eventId: request.params.eventId,
        userId: upn,
        isPaidMember,
        createdAt: "",
      };
    },
  );
  fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().get(
    "/:orgId/event/:eventId",
    {
      schema: withTags(["RSVP"], {
        summary: "Get all RSVPs for an event.",
        params: z.object({
          eventId: z.string().min(1).meta({
            description: "The previously-created event ID in the events API.",
          }),
          orgId: z.string().min(1).meta({
            description: "The organization ID the event belongs to.",
          }),
        }),
        headers: z.object({
          "x-uiuc-token": z.jwt().min(1).meta({
            description:
              "An access token for the user in the UIUC Entra ID tenant.",
          }),
        }),
      }),

</shell_result>


Script executed:

#!/bin/bash
# Search for any zod extensions or custom schema definitions
rg -n "z\.|extend\(|.jwt" src/api/routes/rsvp.ts -C 2

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

16-import { genericConfig } from "common/config.js";
17-
18:const rsvpItemSchema = z.object({
19:  eventId: z.string(),
20:  userId: z.string(),
21:  isPaidMember: z.boolean(),
22:  createdAt: z.string(),
23-});
24:const rsvpListSchema = z.array(rsvpItemSchema);
25-
26-const rsvpRoutes: FastifyPluginAsync = async (fastify, _options) => {
--
35-      schema: withTags(["RSVP"], {
36-        summary: "Submit an RSVP for an event.",
37:        params: z.object({
38:          eventId: z.string().min(1).meta({
39-            description: "The previously-created event ID in the events API.",
40-          }),
41-        }),
42:        headers: z.object({
43:          "x-uiuc-token": z.jwt().min(1).meta({
44-            description:
45-              "An access token for the user in the UIUC Entra ID tenant.",
--
84-      schema: withTags(["RSVP"], {
85-        summary: "Get all RSVPs for an event.",
86:        params: z.object({
87:          eventId: z.string().min(1).meta({
88-            description: "The previously-created event ID in the events API.",
89-          }),
90:          orgId: z.string().min(1).meta({
91-            description: "The organization ID the event belongs to.",
92-          }),
93-        }),
94:        headers: z.object({
95:          "x-uiuc-token": z.jwt().min(1).meta({
96-            description:
97-              "An access token for the user in the UIUC Entra ID tenant.",
--
111-      const response = await fastify.dynamoClient.send(commnand);
112-      const items = response.Items?.map((item) => unmarshall(item)) || [];
113:      return reply.send(items as z.infer<typeof rsvpListSchema>);
114-    },
115-  );

</shell_result>


Script executed:

#!/bin/bash
# Search for jwt function definition or Zod extension
rg -n "jwt\s*[:=]|\.jwt\s*=|extend.*jwt" --type ts

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 1

</shell_result>


Script executed:

#!/bin/bash
# Check imports in rsvp.ts to understand where z comes from
head -20 src/api/routes/rsvp.ts

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

import { FastifyPluginAsync } from "fastify";
import rateLimiter from "api/plugins/rateLimiter.js";
import { withRoles, withTags } from "api/components/index.js";
import { QueryCommand } from "@aws-sdk/client-dynamodb";
import { unmarshall } from "@aws-sdk/util-dynamodb";
import { getUserOrgRoles } from "api/functions/organizations.js";
import {
  UnauthenticatedError,
  UnauthorizedError,
  ValidationError,
} from "common/errors/index.js";
import * as z from "zod/v4";
import { verifyUiucAccessToken } from "api/functions/uin.js";
import { checkPaidMembership } from "api/functions/membership.js";
import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
import { genericConfig } from "common/config.js";

const rsvpItemSchema = z.object({
  eventId: z.string(),
  userId: z.string(),

</shell_result>


Script executed:

#!/bin/bash
# Look for zod configuration or extensions files
fd -e ts -e js | xargs rg -l "z\.extend|ZodType.*jwt" | head -20

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

</shell_result>


Script executed:

#!/bin/bash
# Check package.json for zod version and look at dependencies
cat package.json | head -50

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

{
  "name": "infra-core",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "workspaces": [
    "src/api",
    "src/ui",
    "src/archival",
    "src/linkryEdgeFunction"
  ],
  "packageManager": "yarn@1.22.22",
  "scripts": {
    "postinstall": "npm run setup",
    "setup": "git config blame.ignoreRevsFile .git-blame-ignore-revs",
    "build": "concurrently --names 'api,ui,archival,linkryEdge' 'yarn workspace infra-core-api run build' 'yarn workspace infra-core-ui run build' 'yarn workspace infra-core-archival run build' 'yarn workspace infra-core-linkry-edge run build'",
    "postbuild": "node src/api/createLambdaPackage.js && yarn lockfile-manage",
    "dev": "cross-env DISABLE_AUDIT_LOG=true concurrently --names 'api,ui' 'yarn workspace infra-core-api run dev' 'yarn workspace infra-core-ui run dev'",
    "lockfile-manage": "cp src/api/package.lambda.json dist/lambda/package.json && cp src/api/package.lambda.json dist/sqsConsumer/package.json && cp yarn.lock dist/lambda/ && cp yarn.lock dist/sqsConsumer/",
    "prettier": "yarn workspaces run prettier && prettier --check tests/**/*.ts",
    "prettier:write": "yarn workspaces run prettier:write && prettier --write tests/**/*.ts",
    "lint": "yarn workspaces run lint",
    "prepare": "node .husky/install.mjs || true",
    "typecheck": "yarn workspaces run typecheck",
    "test:unit": "cross-env RunEnvironment='dev' concurrently --names 'api,ui' 'yarn run test:unit-api' 'yarn workspace infra-core-ui run test:unit'",
    "test:unit-api": "cross-env RunEnvironment='dev' vitest run --coverage --config tests/unit/vitest.config.ts tests/unit",
    "test:unit-ui": "yarn test:unit --ui",
    "test:unit-watch": "vitest tests/unit",
    "test:live": "vitest tests/live",
    "test:live-ui": "yarn test:live --ui",
    "test:e2e": "playwright test",
    "test:e2e-ui": "playwright test --ui"
  },
  "dependencies": {
    "@acm-uiuc/js-shared": "^3.2.1"
  },
  "devDependencies": {
    "@eslint/compat": "^1.3.2",
    "@eslint/eslintrc": "^3.3.1",
    "@eslint/js": "^9.33.0",
    "@playwright/test": "^1.56.1",
    "@smithy/types": "^4.5.0",
    "@tsconfig/node22": "^22.0.1",
    "@types/ioredis-mock": "^8.2.5",
    "@types/node": "^24.3.0",
    "@types/pluralize": "^0.0.33",
    "@types/react": "^19.2.2",
    "@types/react-dom": "^19.2.2",
    "@types/supertest": "^6.0.3",
    "@typescript-eslint/eslint-plugin": "^8.40.0",

</shell_result>


Script executed:

#!/bin/bash
# Search for any z.jwt or jwt extension setup
rg -n "jwt" --type ts -g "!node_modules" | grep -i "extend\|z\.jwt\|define" | head -20

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

src/api/routes/rsvp.ts:43:          "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/rsvp.ts:95:          "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/v2/membership.ts:80:            "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/v2/mobileWallet.ts:30:          "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/syncIdentity.ts:67:            "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/syncIdentity.ts:153:            "x-uiuc-token": z.jwt().min(1).meta({
src/api/routes/membership.ts:48:            "x-uiuc-token": z.jwt().min(1).meta({

</shell_result>


Script executed:

#!/bin/bash
# Look for zod setup or configuration files
fd -type f \( -name "*zod*" -o -name "*config*" \) --type ts --type js | head -20

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stderr:

error: 'ype' isn't a valid value for '--type <filetype>'
  [possible values: file, directory, symlink, executable, empty, socket, pipe]

For more information try '--help'

</shell_result>


Script executed:

#!/bin/bash
# Search in common directory for any Zod extensions
rg -n "z\.|jwt" src/common --type ts -C 2 | head -50

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

src/common/utils.ts-38-
src/common/utils.ts-39-
src/common/utils.ts:40:export const nonEmptyCommaSeparatedStringSchema = z.
src/common/utils.ts:41:  array(z.string().min(1)).
src/common/utils.ts-42-  min(1, { message: "Filter expression must select at least one item." }).
src/common/utils.ts-43-  transform((val) => val.map((item) => item.trim()))
--
src/common/utils.ts-48-export const getDefaultFilteringQuerystring = ({ defaultSelect }: GetDefaultFilteringQuerystringInput) => {
src/common/utils.ts-49-  return {
src/common/utils.ts:50:    select: z.optional(nonEmptyCommaSeparatedStringSchema).default(defaultSelect).meta({
src/common/utils.ts-51-      description: "A list of attributes to return.",
src/common/utils.ts-52-      ...(defaultSelect.length === 0 ? { default: ["<ALL ATTRIBUTES>"] } : { example: defaultSelect })
--
src/common/types/user.ts-2-import { illinoisUin } from "./generic.js";
src/common/types/user.ts-3-
src/common/types/user.ts:4:export const searchUserByUinRequest = z.object({
src/common/types/user.ts-5-  uin: illinoisUin
src/common/types/user.ts-6-});
src/common/types/user.ts-7-
src/common/types/user.ts:8:export const searchUserByUinResponse = z.object({
src/common/types/user.ts:9:  email: z.email(),
src/common/types/user.ts-10-});
--
src/common/types/tickets.ts-1-import * as z from "zod/v4";
src/common/types/tickets.ts:2:export const postMetadataSchema = z.object({
src/common/types/tickets.ts:3:  type: z.union([z.literal("merch"), z.literal("ticket")]),
src/common/types/tickets.ts:4:  itemSalesActive: z.union([z.date(), z.boolean()])
src/common/types/tickets.ts-5-});
src/common/types/tickets.ts-6-
src/common/types/tickets.ts:7:export type ItemPostData = z.infer<typeof postMetadataSchema>;
--
src/common/types/stripe.ts-1-import * as z from "zod/v4";
src/common/types/stripe.ts-2-
src/common/types/stripe.ts:3:export const invoiceLinkPostResponseSchema = z.object({
src/common/types/stripe.ts:4:  id: z.string().min(1),
src/common/types/stripe.ts:5:  link: z.string().url()
src/common/types/stripe.ts-6-});
src/common/types/stripe.ts-7-
src/common/types/stripe.ts:8:export const invoiceLinkPostRequestSchema = z.object({
src/common/types/stripe.ts:9:  invoiceId: z.string().min(1),
src/common/types/stripe.ts:10:  invoiceAmountUsd: z.number().min(50),
src/common/types/stripe.ts:11:  contactName: z.string().min(1),
src/common/types/stripe.ts:12:  contactEmail: z.string().email(),
src/common/types/stripe.ts:13:  achPaymentsEnabled: z.optional(z.boolean()).default(false),
src/common/types/stripe.ts-14-});
src/common/types/stripe.ts-15-
src/common/types/stripe.ts:16:export type PostInvoiceLinkRequest = z.infer<
src/common/types/stripe.ts-17-  typeof invoiceLinkPostRequestSchema>;
src/common/types/stripe.ts-18-
src/common/types/stripe.ts-19-

</shell_result>


Script executed:

#!/bin/bash
# Check API workspace for zod version
cat src/api/package.json | grep -A 2 -B 2 "zod"

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

    "fastify-plugin": "^5.0.1",
    "fastify-raw-body": "^5.0.0",
    "fastify-zod-openapi": "^5.0.1",
    "handlebars": "^4.7.8",
    "ical-generator": "^10.0.0",
--
    "stripe": "^19.2.0",
    "uuid": "^13.0.0",
    "zod": "^4.0.14",
    "zod-validation-error": "^4.0.1"
  },
  "devDependencies": {

</shell_result>


Script executed:

#!/bin/bash
# Search for where jwt() might be extended onto z
rg -n "\.jwt\s*=" --type ts

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 1

</shell_result>


Script executed:

#!/bin/bash
# Look for any custom Zod types or extensions
rg -n "extend\|\.jwt\|jwt\(" --type ts | grep -v "x-uiuc-token" | head -30

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

</shell_result>


Script executed:

#!/bin/bash
# Check if zod has jwt method in v4
web_search "Zod v4 jwt method"

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 127

stderr:

/bin/bash: line 3: web_search: command not found

</shell_result>


Script executed:

#!/bin/bash
# Look for .d.ts files or type augmentation
find . -name "*.d.ts" -o -name "*types*" | grep -v node_modules | head -20

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

./src/api/resources/types.d.ts
./src/api/types.d.ts
./src/common/types
./src/ui/types.d.ts
./src/ui/vite-env.d.ts

</shell_result>


Script executed:

#!/bin/bash
# Search for any Zod augmentation or type declaration
rg -n "declare|augment|namespace.*z|interface.*z" --type ts | grep -i zod | head -20

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

</shell_result>


Script executed:

#!/bin/bash
# Check if there's a tsconfig that might reveal anything
cat tsconfig.json | head -50

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stderr:

cat: tsconfig.json: No such file or directory

</shell_result>


Script executed:

#!/bin/bash
# More thorough search for "jwt" across all files
rg -n "jwt" --type ts -C 1 | head -50

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

tests/unit/tickets.test.ts-31-const ddbMock = mockClient(DynamoDBClient);
tests/unit/tickets.test.ts:32:const jwt_secret = testSecretObject["jwt_key"];
tests/unit/tickets.test.ts:33:vi.stubEnv("JwtSigningKey", jwt_secret);
tests/unit/tickets.test.ts-34-
--
tests/unit/secret.testdata.ts-18-const testSecretObject = {
tests/unit/secret.testdata.ts:19:  jwt_key: "somethingreallysecret",
tests/unit/secret.testdata.ts-20-};
--
tests/unit/secret.testdata.ts-28-
tests/unit/secret.testdata.ts:29:const jwtPayload = {
tests/unit/secret.testdata.ts:30:  aud: "custom_jwt",
tests/unit/secret.testdata.ts:31:  iss: "custom_jwt",
tests/unit/secret.testdata.ts-32-  iat: Math.floor(Date.now() / 1000),
--
tests/unit/secret.testdata.ts-54-
tests/unit/secret.testdata.ts:55:const jwtPayloadNoGroups = {
tests/unit/secret.testdata.ts:56:  aud: "custom_jwt",
tests/unit/secret.testdata.ts:57:  iss: "custom_jwt",
tests/unit/secret.testdata.ts-58-  iat: Math.floor(Date.now() / 1000),
--
tests/unit/secret.testdata.ts-85-  testSecretObject,
tests/unit/secret.testdata.ts:86:  jwtPayload,
tests/unit/secret.testdata.ts:87:  jwtPayloadNoGroups,
tests/unit/secret.testdata.ts-88-  uinSecretJson,
--
tests/unit/logs.test.ts-13-const ddbMock = mockClient(DynamoDBClient);
tests/unit/logs.test.ts:14:const jwt_secret = testSecretObject["jwt_key"];
tests/unit/logs.test.ts:15:vi.stubEnv("JwtSigningKey", jwt_secret);
tests/unit/logs.test.ts-16-
--
tests/unit/linkry.test.ts-17-const ddbMock = mockClient(DynamoDBClient);
tests/unit/linkry.test.ts:18:const jwt_secret = testSecretObject["jwt_key"];
tests/unit/linkry.test.ts:19:vi.stubEnv("JwtSigningKey", jwt_secret);
tests/unit/linkry.test.ts-20-
--
tests/unit/ical.test.ts-12-const ddbMock = mockClient(DynamoDBClient);
tests/unit/ical.test.ts:13:const jwt_secret = testSecretObject["jwt_key"];
tests/unit/ical.test.ts:14:vi.stubEnv("JwtSigningKey", jwt_secret);
tests/unit/ical.test.ts-15-
--
tests/unit/events.test.ts-15-const ddbMock = mockClient(DynamoDBClient);
tests/unit/events.test.ts:16:const jwt_secret = testSecretObject["jwt_key"];
tests/unit/events.test.ts:17:vi.stubEnv("JwtSigningKey", jwt_secret);
tests/unit/events.test.ts-18-
--
tests/unit/eventPost.test.ts-19-const ddbMock = mockClient(DynamoDBClient);
tests/unit/eventPost.test.ts:20:const jwt_secret = testSecretObject["jwt_key"];
tests/unit/eventPost.test.ts:21:vi.stubEnv("JwtSigningKey", jwt_secret);
tests/unit/eventPost.test.ts-22-

</shell_result>


Script executed:

#!/bin/bash
# Read the API types file which likely contains the jwt() extension
cat src/api/types.d.ts

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

/* eslint-disable @typescript-eslint/no-unused-vars */
import { FastifyRequest, FastifyInstance, FastifyReply } from "fastify";
import { AppRoles, RunEnvironment } from "../common/roles.js";
import { AadToken } from "./plugins/auth.js";
import { ConfigType, SecretConfig, SecretTesting } from "../common/config.js";
import NodeCache from "node-cache";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
import { SQSClient } from "@aws-sdk/client-sqs";
import { AvailableAuthorizationPolicy } from "common/policies/definition.js";
import type RedisModule from "ioredis";
import { type S3Client } from "@aws-sdk/client-s3";
export type Redis = RedisModule.default;
export type ValidLoggers = FastifyBaseLogger | pino.Logger;

interface CloudfrontLocation {
  country: string | undefined;
  city: string | undefined;
  region: string | undefined;
  latitude: string | undefined;
  longitude: string | undefined;
  postalCode: string | undefined;
}

declare module "fastify" {
  interface FastifyInstance {
    authenticate: (
      request: FastifyRequest,
      reply: FastifyReply,
    ) => Promise<void>;
    authorize: (
      request: FastifyRequest,
      reply: FastifyReply,
      validRoles: AppRoles[],
      disableApiKeyAuth: boolean,
    ) => Promise<Set<AppRoles>>;
    authorizeFromSchema: (
      request: FastifyRequest,
      reply: FastifyReply,
    ) => Promise<void>;
    runEnvironment: RunEnvironment;
    environmentConfig: ConfigType;
    nodeCache: NodeCache;
    dynamoClient: DynamoDBClient;
    sqsClient?: SQSClient;
    s3Client?: S3Client;
    redisClient: Redis;
    secretsManagerClient: SecretsManagerClient;
    secretConfig: SecretConfig | (SecretConfig & SecretTesting);
    refreshSecretConfig: CallableFunction;
  }
  interface FastifyRequest {
    startTime: number;
    username?: string;
    userRoles?: Set<AppRoles>;
    tokenPayload?: AadToken;
    policyRestrictions?: AvailableAuthorizationPolicy[];
    location: CloudfrontLocation;
  }
}

export type NoDataRequest = {
  Params: undefined;
  Querystring: undefined;
  Body: undefined;
};

</shell_result>


Script executed:

#!/bin/bash
# Also check the resources types
cat src/api/resources/types.d.ts

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

/* eslint-disable @typescript-eslint/no-explicit-any */

declare module "*.png" {
  const value: string;
  export default value;
}

declare module "*.json" {
  const value: Record<string, any>;
  export default value;
}

</shell_result>


Script executed:

#!/bin/bash
# Search for fastify-zod-openapi imports and usage
rg -n "fastify-zod-openapi" --type ts

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

src/api/components/index.ts:2:import { FastifyZodOpenApiSchema } from "fastify-zod-openapi";
src/api/index.ts:24:} from "fastify-zod-openapi";
src/api/routes/ics.ts:24:} from "fastify-zod-openapi";
src/api/routes/logs.ts:12:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/membership.ts:23:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/organizations.ts:12:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/mobileWallet.ts:15:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/rsvp.ts:15:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/roomRequests.ts:29:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/vending.ts:3:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/user.ts:16:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/syncIdentity.ts:8:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/tickets.ts:28:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/stripe.ts:42:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/iam.ts:46:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/linkry.ts:47:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/v2/mobileWallet.ts:12:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/v2/membership.ts:18:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/apiKey.ts:5:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/plugins/authorizeFromSchema.ts:4:import { FastifyZodOpenApiSchema } from "fastify-zod-openapi";
src/api/routes/events.ts:44:} from "fastify-zod-openapi";

</shell_result>


Script executed:

#!/bin/bash
# Check if there's any setup file that might extend Zod
rg -n "import.*z|from.*zod" src/api --type ts | head -20

Repository: acm-uiuc/core


<shell_result>
Repository: acm-uiuc/core
Exit code: 0

stdout:

src/api/index.ts:24:} from "fastify-zod-openapi";
src/api/index.ts:25:import { type ZodOpenApiVersion } from "zod-openapi";
src/api/index.ts:39:import authorizeFromSchemaPlugin from "./plugins/authorizeFromSchema.js";
src/api/index.ts:44:import organizationsPlugin from "./routes/organizations.js";
src/api/types.d.ts:10:import { AvailableAuthorizationPolicy } from "common/policies/definition.js";
src/api/routes/mobileWallet.ts:6:import * as z from "zod/v4";
src/api/routes/mobileWallet.ts:15:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/logs.ts:12:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/logs.ts:13:import * as z from "zod/v4";
src/api/routes/tickets.ts:2:import * as z from "zod/v4";
src/api/routes/tickets.ts:28:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/membership.ts:23:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/membership.ts:24:import * as z from "zod/v4";
src/api/routes/vending.ts:3:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/vending.ts:4:import * as z from "zod/v4";
src/api/routes/rsvp.ts:6:import { getUserOrgRoles } from "api/functions/organizations.js";
src/api/routes/rsvp.ts:12:import * as z from "zod/v4";
src/api/routes/rsvp.ts:15:import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
src/api/routes/user.ts:4:import { getUserOrgRoles } from "api/functions/organizations.js";
src/api/routes/user.ts:10:import * as z from "zod/v4";

</shell_result>


Zod v4 jwt method


<web_search_result>
Short answer: Zod v4 adds a string validator for JWTs: use z.string().jwt() (optionally z.string().jwt({ alg: "RS256" }) to require a specific alg). It only checks the three-part/base64url structure (and decodes header/payload), it does NOT verify signatures — use a JWT library (e.g., jsonwebtoken or jose) for cryptographic verification. [1][2]
</web_search_result>


Fix z.jwt() to z.string().jwt()z.jwt() is not a valid Zod v4 method.

Zod v4 provides z.string().jwt() to validate JWT strings, not z.jwt(). The current code will fail at runtime. Replace z.jwt().min(1) with z.string().jwt().min(1).

Note: z.string().jwt() validates the JWT structure (three-part base64url format) but does not verify signatures—use a JWT library like jose for cryptographic verification.

🤖 Prompt for AI Agents
In src/api/routes/rsvp.ts around lines 43-46, replace the invalid Zod call
z.jwt().min(1) with z.string().jwt().min(1) so the header schema uses
z.string().jwt().min(1) to validate JWT-formatted strings; ensure imports still
reference z from zod and keep min(1) in the chain (or move before jwt() if
preferred).

}),
}),
},
async (request, reply) => {
const accessToken = request.headers["x-uiuc-token"];
const verifiedData = await verifyUiucAccessToken({
accessToken,
logger: request.log,
});
const { userPrincipalName: upn, givenName, surname } = verifiedData;
const netId = upn.replace("@illinois.edu", "");
if (netId.includes("@")) {
request.log.error(
`Found UPN ${upn} which cannot be turned into NetID via simple replacement.`,
);
throw new ValidationError({
message: "ID token could not be parsed.",
});
}
const isPaidMember = await checkPaidMembership({
netId,
dynamoClient: fastify.dynamoClient,
redisClient: fastify.redisClient,
logger: request.log,
});
const entry = {
partitionKey: `${request.params.eventId}#${upn}`,
eventId: request.params.eventId,
userId: upn,
isPaidMember,
createdAt: "",
};
const putCommand = new PutItemCommand({
TableName: genericConfig.RSVPDynamoTableName,
Item: marshall(entry),
});
await fastify.dynamoClient.send(putCommand);
return reply.status(201).send(entry);
},
);
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().get(
"/:orgId/event/:eventId",
{
schema: withRoles(
[AppRoles.VIEW_RSVPS],
withTags(["RSVP"], {
summary: "Get all RSVPs for an event.",
params: z.object({
eventId: z.string().min(1).meta({
description: "The previously-created event ID in the events API.",
}),
orgId: z.string().min(1).meta({
description: "The organization ID the event belongs to.",
}),
}),
}),
),
onRequest: fastify.authorizeFromSchema,
},
async (request, reply) => {
const command = new QueryCommand({
TableName: genericConfig.RSVPDynamoTableName,
IndexName: "EventIdIndex",
KeyConditionExpression: "eventId = :eid",
ExpressionAttributeValues: {
":eid": { S: request.params.eventId },
},
});
const response = await fastify.dynamoClient.send(command);
if (!response || !response.Items) {
throw new DatabaseFetchError({
message: "Failed to get all member lists.",
});
}
Comment on lines +112 to +116
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix misleading error message.

The error message says "Failed to get all member lists" but this endpoint retrieves RSVPs.

       if (!response || !response.Items) {
         throw new DatabaseFetchError({
-          message: "Failed to get all member lists.",
+          message: "Failed to fetch RSVPs for event.",
         });
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!response || !response.Items) {
throw new DatabaseFetchError({
message: "Failed to get all member lists.",
});
}
if (!response || !response.Items) {
throw new DatabaseFetchError({
message: "Failed to fetch RSVPs for event.",
});
}
🤖 Prompt for AI Agents
In src/api/routes/rsvp.ts around lines 112 to 116, the thrown DatabaseFetchError
uses a misleading message "Failed to get all member lists" even though this
endpoint retrieves RSVPs; update the error message to accurately reflect the
operation (e.g., "Failed to get all RSVPs" or "Failed to fetch RSVPs") while
keeping the same error type and payload structure so callers and logs report the
correct failure context.

const rsvps = response.Items.map((x) => unmarshall(x));
const uniqueRsvps = [
...new Map(rsvps.map((item) => [item.userId, item])).values(),
];
return reply.send(uniqueRsvps);
},
);
};

export default rsvpRoutes;
2 changes: 2 additions & 0 deletions src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type ConfigType = {

export type GenericConfigType = {
EventsDynamoTableName: string;
RSVPDynamoTableName: string;
CacheDynamoTableName: string;
LinkryDynamoTableName: string;
StripeLinksDynamoTableName: string;
Expand Down Expand Up @@ -84,6 +85,7 @@ export const commChairsGroupId = "105e7d32-7289-435e-a67a-552c7f215507";

const genericConfig: GenericConfigType = {
EventsDynamoTableName: "infra-core-api-events",
RSVPDynamoTableName: "infra-core-api-events-rsvp",
StripeLinksDynamoTableName: "infra-core-api-stripe-links",
StripePaymentsDynamoTableName: "infra-core-api-stripe-payments",
CacheDynamoTableName: "infra-core-api-cache",
Expand Down
4 changes: 4 additions & 0 deletions src/common/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const META_ROLE_PREFIX = "__metaRole:"

export enum BaseRoles {
EVENTS_MANAGER = "manage:events",
RSVPS_MANAGER = "manage:rsvps",
VIEW_RSVPS = "view:rsvps",
TICKETS_SCANNER = "scan:tickets",
TICKETS_MANAGER = "manage:tickets",
IAM_ADMIN = "admin:iam",
Expand Down Expand Up @@ -47,6 +49,8 @@ export const AppRoleHumanMapper: Record<AppRoles, string> = {
[AppRoles.EVENTS_MANAGER]: "Events Manager",
[AppRoles.TICKETS_SCANNER]: "Tickets Scanner",
[AppRoles.TICKETS_MANAGER]: "Tickets Manager",
[AppRoles.RSVPS_MANAGER]: "RSVPs Manager",
[AppRoles.VIEW_RSVPS]: "RSVPs Viewer",
[AppRoles.IAM_ADMIN]: "IAM Admin",
[AppRoles.IAM_INVITE_ONLY]: "IAM Inviter",
[AppRoles.LINKS_MANAGER]: "Links Manager",
Expand Down
8 changes: 8 additions & 0 deletions src/common/types/rsvp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as z from "zod/v4";

export const rsvpItemSchema = z.object({
eventId: z.string(),
userId: z.string(),
isPaidMember: z.boolean(),
createdAt: z.string(),
});
33 changes: 33 additions & 0 deletions terraform/modules/dynamo/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -567,3 +567,36 @@ resource "aws_dynamodb_table" "store_limits" {
}
}
}

resource "aws_dynamodb_table" "events_rsvp" {
region = "us-east-2"
billing_mode = "PAY_PER_REQUEST"
name = "${var.ProjectId}-events-rsvp"
deletion_protection_enabled = true
hash_key = "partitionKey"
point_in_time_recovery {
enabled = true
}
attribute {
name = "partitionKey"
type = "S"
}
attribute {
name = "eventId"
type = "S"
}
global_secondary_index {
name = "EventIdIndex"
hash_key = "eventId"
projection_type = "ALL"
}
stream_enabled = true
stream_view_type = "NEW_AND_OLD_IMAGES"
dynamic "replica" {
for_each = var.ReplicationRegions
content {
region_name = replica.value
deletion_protection_enabled = true
}
}
}
157 changes: 157 additions & 0 deletions tests/unit/rsvps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { expect, test, vi, describe, beforeEach } from "vitest";
import {
DynamoDBClient,
PutItemCommand,
QueryCommand,
} from "@aws-sdk/client-dynamodb";
import { marshall } from "@aws-sdk/util-dynamodb";
import { mockClient } from "aws-sdk-client-mock";
import init from "../../src/api/index.js";
import { createJwt } from "./auth.test.js";
import { testSecretObject } from "./secret.testdata.js";
import { Redis } from "../../src/api/types.js";
import { FastifyBaseLogger } from "fastify";

const DUMMY_JWT =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";

vi.mock("../../src/api/functions/uin.js", async () => {
const actual = await vi.importActual("../../src/api/functions/uin.js");
return {
...actual,
verifyUiucAccessToken: vi
.fn()
.mockImplementation(
async ({
token,
logger,
}: {
token: string;
logger: FastifyBaseLogger;
}) => {
if (token === DUMMY_JWT) {
console.log("DUMMY_JWT matched in mock implementation");
}
return {
userPrincipalName: "jd3@illinois.edu",
givenName: "John",
surname: "Doe",
mail: "johndoe@gmail.com",
};
},
),
};
});

vi.mock("../../src/api/functions/membership.js", async () => {
const actual = await vi.importActual("../../src/api/functions/membership.js");
return {
...actual,
checkPaidMembership: vi
.fn()
.mockImplementation(
async ({
netId,
redisClient,
dynamoClient,
logger,
}: {
netId: string;
redisClient: Redis;
dynamoClient: DynamoDBClient;
logger: FastifyBaseLogger;
}) => {
if (netId === "jd3") {
return true;
}
return false;
},
),
};
});

const ddbMock = mockClient(DynamoDBClient);
const jwt_secret = testSecretObject["jwt_key"];
vi.stubEnv("JwtSigningKey", jwt_secret);

const app = await init();

describe("RSVP API tests", () => {
beforeEach(() => {
ddbMock.reset();
vi.clearAllMocks();
});

test("Test posting an RSVP for an event", async () => {
ddbMock.on(PutItemCommand).resolves({});

const testJwt = createJwt();
const mockUpn = "jd3@illinois.edu";
const eventId = "Make Your Own Database";
const orgId = "SIGDatabase";

const response = await app.inject({
method: "POST",
url: `/api/v1/rsvp/${orgId}/event/${encodeURIComponent(eventId)}`,
headers: {
Authorization: `Bearer ${testJwt}`,
"x-uiuc-token": DUMMY_JWT,
},
});

if (response.statusCode !== 201) {
console.log("Test Failed Response:", response.body);
}

expect(response.statusCode).toBe(201);

const body = JSON.parse(response.body);
expect(body.userId).toBe(mockUpn);
expect(body.eventId).toBe(eventId);
expect(body.isPaidMember).toBe(true);
expect(body.partitionKey).toBe(`${eventId}#${mockUpn}`);

expect(ddbMock.calls()).toHaveLength(1);
const putItemInput = ddbMock.call(0).args[0].input as any;
expect(putItemInput.TableName).toBe("infra-core-api-events-rsvp");
});

test("Test getting RSVPs for an event (Mocking Query Response)", async () => {
const eventId = "Make Your Own Database";
const orgId = "SIGDatabase";
const mockRsvps = [
{
eventId,
userId: "user1@illinois.edu",
isPaidMember: true,
createdAt: "2023-01-01",
},
{
eventId,
userId: "user2@illinois.edu",
isPaidMember: false,
createdAt: "2023-01-02",
},
];
ddbMock.on(QueryCommand).resolves({
Items: mockRsvps.map((item) => marshall(item)),
});

const adminJwt = await createJwt();

const response = await app.inject({
method: "GET",
url: `/api/v1/rsvp/${orgId}/event/${encodeURIComponent(eventId)}`,
headers: {
Authorization: `Bearer ${adminJwt}`,
},
});

expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);

expect(body).toHaveLength(2);
expect(body[0].userId).toBe("user1@illinois.edu");
expect(body[1].userId).toBe("user2@illinois.edu");
});
});
Loading
Loading