Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
118 changes: 118 additions & 0 deletions src/api/routes/rsvp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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(),
isPaidMember: z.boolean(),
createdAt: z.string(),
});
const rsvpListSchema = z.array(rsvpItemSchema);

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.",
}),
}),
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: "",
};
},
Comment on lines 68 to 81
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

POST handler is incomplete — entry is created but never persisted or returned.

The handler constructs an RSVP entry object but:

  1. Never writes it to DynamoDB
  2. Never sends a response to the client
  3. Sets createdAt to an empty string instead of a timestamp

The handler needs to persist the entry and return a response. Example fix:

       const entry = {
         partitionKey: `${request.params.eventId}#${upn}`,
         eventId: request.params.eventId,
         userId: upn,
         isPaidMember,
-        createdAt: "",
+        createdAt: new Date().toISOString(),
       };
+      const putCommand = new PutItemCommand({
+        TableName: genericConfig.RsvpDynamoTableName, // Add this config
+        Item: marshall(entry),
+      });
+      await fastify.dynamoClient.send(putCommand);
+      return reply.status(201).send(entry);
     },

You'll also need to import PutItemCommand from @aws-sdk/client-dynamodb and marshall from @aws-sdk/util-dynamodb.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/api/routes/rsvp.ts around lines 72-79 the POST handler builds an RSVP
entry but never persists it or responds to the client and createdAt is empty;
fix by importing PutItemCommand from @aws-sdk/client-dynamodb and marshall from
@aws-sdk/util-dynamodb, set entry.createdAt = new Date().toISOString(), create a
PutItemCommand with TableName and Item: marshall(entry), await
dynamoDbClient.send(putCmd) (wrap in try/catch to handle errors), and finally
return an appropriate HTTP response (e.g., 201 with the saved entry or a
location header) or a 500 on failure.

);
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.",
}),
}),
}),
},
async (request, reply) => {
const commnand = new QueryCommand({
TableName: genericConfig.EventsDynamoTableName,
IndexName: "EventIdIndex",
KeyConditionExpression: "eventId = :eid",
ExpressionAttributeValues: {
":eid": { S: request.params.eventId },
},
});
const response = await fastify.dynamoClient.send(commnand);
const items = response.Items?.map((item) => unmarshall(item)) || [];
return reply.send(items as z.infer<typeof rsvpListSchema>);
},
);
};

export default rsvpRoutes;
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" "store_limits" {
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
}
}
}
Loading