Skip to content

Conversation

@MeghP89
Copy link

@MeghP89 MeghP89 commented Nov 26, 2025

Summary by CodeRabbit

  • New Features

    • Added RSVP functionality to register, view, configure, and withdraw event RSVPs (route exposed only in development)
    • Introduced role-based permissions for managing and viewing RSVP data
    • Added configuration entry for RSVP storage
  • Tests

    • Added comprehensive unit tests covering RSVP flows and admin actions

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 26, 2025

Walkthrough

Adds a new RSVP feature: a Fastify plugin exposing RSVP lifecycle endpoints (create, list, configure, delete) persisted to DynamoDB; new config and roles; route registered under /rsvp only in development; includes Zod schema and unit tests. No existing public signatures changed except new default export rsvpRoutes.

Changes

Cohort / File(s) Summary
RSVP Route Implementation
src/api/routes/rsvp.ts
New Fastify plugin rsvpRoutes with endpoints: POST /:orgId/event/:eventId (verify UIUC token, check paid membership, transactional write to DynamoDB with rsvpCount increment, handles conflicts → 409), GET /:orgId/event/:eventId (queries EventIdIndex, unmarshals and deduplicates, requires VIEW_RSVPS), POST config, DELETE (by user and by manager). Uses AWS SDK DynamoDB commands, rate limiting, Zod/OpenAPI validation, and detailed error handling.
API Registration
src/api/index.ts
Imports and conditionally registers rsvpRoutes under /rsvp when running in development environment only.
Config
src/common/config.ts
Adds RSVPDynamoTableName: "infra-core-api-events-rsvp" to GenericConfigType and genericConfig.
Roles
src/common/roles.ts
Adds RSVPS_MANAGER = "manage:rsvps" and VIEW_RSVPS = "view:rsvps" to BaseRoles and maps human-friendly names in AppRoleHumanMapper.
Types / Schema
src/common/types/rsvp.ts
Adds exported rsvpItemSchema (Zod) defining RSVP shape: eventId, userId, isPaidMember, createdAt.
Tests
tests/unit/rsvps.test.ts
New comprehensive unit tests covering create, conflict, list, withdraw, manager delete, and config flows; mocks DynamoDB, auth, and membership checks.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API
    participant AuthSvc as Auth Service
    participant MembershipSvc as Membership Service
    participant DynamoDB

    rect rgb(240,248,255)
    Note over Client,API: Create RSVP (POST /rsvp/:orgId/event/:eventId)
    Client->>API: POST /rsvp/:orgId/event/:eventId (UIUC token)
    API->>AuthSvc: verifyUiucAccessToken(token)
    AuthSvc-->>API: {upn, netId}
    API->>MembershipSvc: checkPaidMembership(netId)
    MembershipSvc-->>API: {isPaidMember}
    API->>DynamoDB: TransactWriteItems (Put RSVP, Update rsvpCount)
    alt success
        DynamoDB-->>API: success
        API-->>Client: 201 {rsvp}
    else conflict (duplicate)
        DynamoDB-->>API: TransactionCanceledException
        API-->>Client: 409 conflict
    end
    end

    rect rgb(245,245,220)
    Note over Client,API: List RSVPs (GET /rsvp/:orgId/event/:eventId)
    Client->>API: GET /rsvp/:orgId/event/:eventId (requires VIEW_RSVPS)
    API->>DynamoDB: QueryCommand (EventIdIndex)
    DynamoDB-->>API: [items]
    API->>API: unmarshal items → deduplicate by userId
    API-->>Client: 200 [deduplicated RSVPs]
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title focuses on one endpoint (GET all RSVPs) but the PR implements a comprehensive RSVP module with 5+ endpoints, configuration, admin controls, and supporting infrastructure. Consider a broader title like 'Implement comprehensive RSVP management system with endpoints and admin controls' to better reflect the full scope of the changeset.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 26, 2025

💰 Infracost report

Monthly estimate increased by $6 📈

Changed project Baseline cost Usage cost* Total change New monthly cost
acm-uiuc/core/terraform/envs/prod +$0.00 +$3 +$3 (+4%) $86
acm-uiuc/core/terraform/envs/qa +$0.00 +$3 +$3 (+3%) $90

*Usage costs were estimated using infracost-usage.yml, see docs for other options.

Estimate details
Key: * usage cost, ~ changed, + added, - removed

──────────────────────────────────
Project: envs-prod
Module path: envs/prod

+ module.dynamo.aws_dynamodb_table.events_rsvp
  +$3

    + Write request unit (WRU)
      +$0.00, +150 WRUs*

    + Read request unit (RRU)
      +$0.00, +300 RRUs*

    + Data storage
      +$0.75, +3 GB*

    + Point-In-Time Recovery (PITR) backup storage
      +$1, +6 GB*

    + On-demand backup storage
      +$0.60, +6 GB*

    + Table data restored
      +$0.45, +3 GB*

    + Streams read request unit (sRRU)
      +$0.00, +700 sRRUs*

    + Global table (us-west-2)
    
        + Replicated write request unit (rWRU)
          +$0.00

Monthly cost change for acm-uiuc/core/terraform/envs/prod (Module path: envs/prod)
Amount:  +$3 ($83 → $86)
Percent: +4%

──────────────────────────────────
Project: envs-qa
Module path: envs/qa

+ module.dynamo.aws_dynamodb_table.events_rsvp
  +$3

    + Write request unit (WRU)
      +$0.00, +150 WRUs*

    + Read request unit (RRU)
      +$0.00, +300 RRUs*

    + Data storage
      +$0.75, +3 GB*

    + Point-In-Time Recovery (PITR) backup storage
      +$1, +6 GB*

    + On-demand backup storage
      +$0.60, +6 GB*

    + Table data restored
      +$0.45, +3 GB*

    + Streams read request unit (sRRU)
      +$0.00, +700 sRRUs*

    + Global table (us-west-2)
    
        + Replicated write request unit (rWRU)
          +$0.00

Monthly cost change for acm-uiuc/core/terraform/envs/qa (Module path: envs/qa)
Amount:  +$3 ($87 → $90)
Percent: +3%

──────────────────────────────────
Key: * usage cost, ~ changed, + added, - removed

*Usage costs were estimated using infracost-usage.yml, see docs for other options.

340 cloud resources were detected:
∙ 134 were estimated
∙ 206 were free

Infracost estimate: Monthly estimate increased by $6 ↑
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Changed project                                    ┃ Baseline cost ┃ Usage cost* ┃ Total change ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━╋━━━━━━━━━━━━━━┫
┃ acm-uiuc/core/terraform/envs/prod                  ┃        +$0.00 ┃         +$3 ┃    +$3 (+4%) ┃
┃ acm-uiuc/core/terraform/envs/qa                    ┃        +$0.00 ┃         +$3 ┃    +$3 (+3%) ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━┻━━━━━━━━━━━━━━┛
This comment will be updated when code changes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
src/api/routes/rsvp.ts (1)

3-3: Unused imports: withRoles and getUserOrgRoles.

These imports are not currently used. If authorization is added per the previous comment, they will be needed. Otherwise, remove them.

Also applies to: 6-6

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8ae6f87 and 9e2f331.

📒 Files selected for processing (3)
  • src/api/index.ts (2 hunks)
  • src/api/routes/rsvp.ts (1 hunks)
  • terraform/modules/dynamo/main.tf (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/api/routes/rsvp.ts (5)
src/api/components/index.ts (1)
  • withTags (243-258)
src/api/functions/uin.ts (1)
  • verifyUiucAccessToken (30-96)
src/common/errors/index.ts (1)
  • ValidationError (99-108)
src/api/functions/membership.ts (1)
  • checkPaidMembership (359-400)
src/common/config.ts (1)
  • genericConfig (244-244)
🪛 ESLint
src/api/routes/rsvp.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/pMkMEsaHQW'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:233:12)
at checkFileExtension (/home/jailuser/git/node_modules/eslint-plugin-import/lib/rules/extensions.js:205:53)
at checkSourceValue (/home/jailuser/git/node_modules/eslint-module-utils/moduleVisitor.js:32:5)

(import/extensions)


[error] 2-2: Unexpected use of file extension "js" for "api/plugins/rateLimiter.js"

(import/extensions)


[error] 3-3: Unexpected use of file extension "js" for "api/components/index.js"

(import/extensions)


[error] 6-6: Unexpected use of file extension "js" for "api/functions/organizations.js"

(import/extensions)


[error] 11-11: Unexpected use of file extension "js" for "common/errors/index.js"

(import/extensions)


[error] 13-13: Unexpected use of file extension "js" for "api/functions/uin.js"

(import/extensions)


[error] 14-14: Unexpected use of file extension "js" for "api/functions/membership.js"

(import/extensions)


[error] 16-16: Unexpected use of file extension "js" for "common/config.js"

(import/extensions)

src/api/index.ts

[error] 65-65: Unexpected use of file extension "js" for "./routes/rsvp.js"

(import/extensions)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build Application
🔇 Additional comments (1)
src/api/index.ts (1)

65-65: LGTM!

The import and conditional registration of rsvpRoutes follows the existing patterns in the codebase. The dev-only gate is consistent with how vendingPlugin is registered.

Also applies to: 381-383

Comment on lines +43 to +46
"x-uiuc-token": z.jwt().min(1).meta({
description:
"An access token for the user in the UIUC Entra ID tenant.",
}),
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).

Comment on lines 72 to 79
const entry = {
partitionKey: `${request.params.eventId}#${upn}`,
eventId: request.params.eventId,
userId: upn,
isPaidMember,
createdAt: "",
};
},
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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
src/api/routes/rsvp.ts (2)

38-43: Fix z.jwt() to z.string().jwt().

Zod v4 provides z.string().jwt() for JWT validation, not z.jwt(). This will cause a runtime error.

         headers: z.object({
-          "x-uiuc-token": z.jwt().min(1).meta({
+          "x-uiuc-token": z.string().jwt().min(1).meta({
             description:
               "An access token for the user in the UIUC Entra ID tenant.",
           }),
         }),

68-74: Set createdAt to actual timestamp.

createdAt is set to an empty string, which loses the timestamp information. Use new Date().toISOString() for a proper ISO 8601 timestamp.

       const entry = {
         partitionKey: `${request.params.eventId}#${upn}`,
         eventId: request.params.eventId,
         userId: upn,
         isPaidMember,
-        createdAt: "",
+        createdAt: new Date().toISOString(),
       };
🧹 Nitpick comments (2)
tests/unit/rsvps.test.ts (1)

119-156: GET test looks good, but consider adding edge case coverage.

The happy path is well tested. Consider adding tests for:

  • Empty results (no RSVPs for event)
  • Authorization failures (missing VIEW_RSVPS role)
  • Invalid event ID
src/api/routes/rsvp.ts (1)

52-52: Unused destructured variables.

givenName and surname are destructured but never used. Consider removing them or using them (e.g., for logging).

-      const { userPrincipalName: upn, givenName, surname } = verifiedData;
+      const { userPrincipalName: upn } = verifiedData;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 9e2f331 and 9e6ba13.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (6)
  • src/api/routes/rsvp.ts (1 hunks)
  • src/common/config.ts (2 hunks)
  • src/common/roles.ts (1 hunks)
  • src/common/types/rsvp.ts (1 hunks)
  • terraform/modules/dynamo/main.tf (1 hunks)
  • tests/unit/rsvps.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • terraform/modules/dynamo/main.tf
🧰 Additional context used
🧬 Code graph analysis (1)
src/api/routes/rsvp.ts (5)
src/api/components/index.ts (2)
  • withTags (243-258)
  • withRoles (194-241)
src/api/functions/uin.ts (1)
  • verifyUiucAccessToken (30-96)
src/common/errors/index.ts (2)
  • ValidationError (99-108)
  • DatabaseFetchError (121-130)
src/api/functions/membership.ts (1)
  • checkPaidMembership (359-400)
src/common/roles.ts (2)
  • AppRoles (35-35)
  • AppRoles (36-36)
🪛 ESLint
tests/unit/rsvps.test.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/XtUyUAvhlL'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:233:12)
at checkFileExtension (/home/jailuser/git/node_modules/eslint-plugin-import/lib/rules/extensions.js:205:53)
at checkSourceValue (/home/jailuser/git/node_modules/eslint-module-utils/moduleVisitor.js:32:5)

(import/extensions)


[error] 9-9: Unexpected use of file extension "js" for "../../src/api/index.js"

(import/extensions)


[error] 10-10: Unexpected use of file extension "js" for "./auth.test.js"

(import/extensions)


[error] 11-11: Unexpected use of file extension "js" for "./secret.testdata.js"

(import/extensions)


[error] 12-12: Unexpected use of file extension "js" for "../../src/api/types.js"

(import/extensions)


[error] 74-74: ["jwt_key"] is better written in dot notation.

(dot-notation)

src/common/types/rsvp.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/txSPdyxJiv'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:233:12)
at checkFileExtension (/home/jailuser/git/node_modules/eslint-plugin-import/lib/rules/extensions.js:205:53)
at checkSourceValue (/home/jailuser/git/node_modules/eslint-module-utils/moduleVisitor.js:32:5)

(import/extensions)

src/api/routes/rsvp.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/MgMsEepBoK'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:233:12)
at checkFileExtension (/home/jailuser/git/node_modules/eslint-plugin-import/lib/rules/extensions.js:205:53)
at checkSourceValue (/home/jailuser/git/node_modules/eslint-module-utils/moduleVisitor.js:32:5)

(import/extensions)


[error] 2-2: Unexpected use of file extension "js" for "api/plugins/rateLimiter.js"

(import/extensions)


[error] 3-3: Unexpected use of file extension "js" for "api/components/index.js"

(import/extensions)


[error] 11-11: Unexpected use of file extension "js" for "common/errors/index.js"

(import/extensions)


[error] 13-13: Unexpected use of file extension "js" for "api/functions/uin.js"

(import/extensions)


[error] 14-14: Unexpected use of file extension "js" for "api/functions/membership.js"

(import/extensions)


[error] 16-16: Unexpected use of file extension "js" for "common/config.js"

(import/extensions)


[error] 17-17: Unexpected use of file extension "js" for "common/roles.js"

(import/extensions)


[error] 119-119: Insert ,

(prettier/prettier)

🪛 GitHub Actions: QA deploy - @MeghP89
src/api/routes/rsvp.ts

[error] 119-119: prettier/prettier: Insert ,

🪛 GitHub Check: Run Unit Tests
src/api/routes/rsvp.ts

[failure] 119-119:
Insert ,

🪛 Gitleaks (8.29.1)
tests/unit/rsvps.test.ts

[high] 16-16: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🔇 Additional comments (4)
src/common/config.ts (1)

41-41: LGTM!

The RSVPDynamoTableName configuration is properly added to both the type definition and the config object, following the existing naming conventions.

Also applies to: 88-88

tests/unit/rsvps.test.ts (2)

15-16: Test JWT constant is fine — Gitleaks flag is a false positive.

This is a well-known example JWT from jwt.io documentation used purely for testing. It contains no sensitive data and is safe to use in test fixtures.


85-117: POST test looks good.

The test correctly verifies the RSVP creation flow, including the response structure and DynamoDB interaction.

src/common/types/rsvp.ts (1)

1-8: I'll verify the concerns in this review comment by checking if the schema is actually unused and whether there's duplication in the codebase.
<function_calls>

shell
#!/bin/bash

Check if rsvpItemSchema from common/types/rsvp is imported anywhere

echo "=== Checking for imports from common/types/rsvp ==="
rg -n "from.*common/types/rsvp" --type ts

echo ""
echo "=== Checking for all rsvpItemSchema definitions ==="
rg -n "rsvpItemSchema" --type ts -B2 -A2

echo ""
echo "=== Checking for rsvpListSchema ==="
rg -n "rsvpListSchema" --type ts -B2 -A2


</function_calls>

Comment on lines +112 to +116
if (!response || !response.Items) {
throw new DatabaseFetchError({
message: "Failed to get all member lists.",
});
}
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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
src/api/routes/rsvp.ts (3)

38-43: Fix z.jwt() to z.string().jwt().

z.jwt() is not a valid Zod v4 method. This will cause a runtime error. Use z.string().jwt() instead.

         headers: z.object({
-          "x-uiuc-token": z.jwt().min(1).meta({
+          "x-uiuc-token": z.string().jwt().min(1).meta({
             description:
               "An access token for the user in the UIUC Entra ID tenant.",
           }),
         }),

112-116: 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.",
         });
       }

68-74: Set createdAt to an actual timestamp.

The createdAt field is set to an empty string, which defeats its purpose. This should be a proper ISO timestamp for auditing and sorting.

       const entry = {
         partitionKey: `${request.params.eventId}#${upn}`,
         eventId: request.params.eventId,
         userId: upn,
         isPaidMember,
-        createdAt: "",
+        createdAt: new Date().toISOString(),
       };
🧹 Nitpick comments (1)
src/api/routes/rsvp.ts (1)

52-52: Remove unused destructured variables.

givenName and surname are destructured but never used in the handler. Either remove them or use them (e.g., for logging or storing in the RSVP entry).

-      const { userPrincipalName: upn, givenName, surname } = verifiedData;
+      const { userPrincipalName: upn } = verifiedData;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 9e6ba13 and 96a29a1.

📒 Files selected for processing (2)
  • src/api/routes/rsvp.ts (1 hunks)
  • src/common/roles.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/api/routes/rsvp.ts (5)
src/api/components/index.ts (2)
  • withTags (243-258)
  • withRoles (194-241)
src/api/functions/uin.ts (1)
  • verifyUiucAccessToken (30-96)
src/common/errors/index.ts (2)
  • ValidationError (99-108)
  • DatabaseFetchError (121-130)
src/api/functions/membership.ts (1)
  • checkPaidMembership (359-400)
src/common/config.ts (1)
  • genericConfig (246-246)
🪛 ESLint
src/api/routes/rsvp.ts

[error] 1-1: Resolve error: EACCES: permission denied, open '/jZkjPRImBn'
at Object.writeFileSync (node:fs:2409:20)
at l (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:13685)
at createFilesMatcher (/home/jailuser/git/node_modules/get-tsconfig/dist/index.cjs:7:14437)
at Object.resolve (/home/jailuser/git/node_modules/eslint-import-resolver-typescript/lib/index.cjs:298:107)
at withResolver (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:180:23)
at fullResolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:201:22)
at relative (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:217:10)
at resolve (/home/jailuser/git/node_modules/eslint-module-utils/resolve.js:233:12)
at checkFileExtension (/home/jailuser/git/node_modules/eslint-plugin-import/lib/rules/extensions.js:205:53)
at checkSourceValue (/home/jailuser/git/node_modules/eslint-module-utils/moduleVisitor.js:32:5)

(import/extensions)


[error] 2-2: Unexpected use of file extension "js" for "api/plugins/rateLimiter.js"

(import/extensions)


[error] 3-3: Unexpected use of file extension "js" for "api/components/index.js"

(import/extensions)


[error] 11-11: Unexpected use of file extension "js" for "common/errors/index.js"

(import/extensions)


[error] 13-13: Unexpected use of file extension "js" for "api/functions/uin.js"

(import/extensions)


[error] 14-14: Unexpected use of file extension "js" for "api/functions/membership.js"

(import/extensions)


[error] 16-16: Unexpected use of file extension "js" for "common/config.js"

(import/extensions)


[error] 17-17: Unexpected use of file extension "js" for "common/roles.js"

(import/extensions)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Application
  • GitHub Check: Run Unit Tests
🔇 Additional comments (6)
src/common/roles.ts (2)

10-11: LGTM!

The new roles follow the existing naming convention and are properly added to the enum.


52-53: LGTM!

Human-readable mappings are correctly added, addressing the previous review comment about missing entries in AppRoleHumanMapper.

src/api/routes/rsvp.ts (4)

1-17: Imports look correct.

The .js extensions flagged by ESLint are a standard pattern for TypeScript projects using ES modules with Node.js resolution. The UnauthorizedError import, while not directly used in this file, is available for future use if needed.


19-24: Rate limiter configuration looks appropriate.

30 requests per 30 seconds is reasonable for RSVP operations.


83-100: Authorization properly implemented.

The GET endpoint correctly uses withRoles([AppRoles.VIEW_RSVPS], ...) and onRequest: fastify.authorizeFromSchema to restrict access.


117-122: Deduplication logic is correct.

Using Map to deduplicate by userId and spreading values() back into an array is an idiomatic approach.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (4)
src/api/routes/rsvp.ts (4)

41-46: z.jwt() is invalid — use z.string().jwt() instead.

Zod v4 provides JWT validation via z.string().jwt(), not z.jwt(). The current code will fail at runtime. This was flagged in a previous review but remains unfixed.

🔎 Suggested fix
         headers: z.object({
-          "x-uiuc-token": z.jwt().min(1).meta({
+          "x-uiuc-token": z.string().jwt().min(1).meta({
             description:
               "An access token for the user in the UIUC Entra ID tenant.",
           }),
         }),

193-197: Fix misleading error message.

The error message says "Failed to get all member lists" but this endpoint retrieves RSVPs. This was flagged in a previous review but remains unfixed.

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

394-399: Same z.jwt() issue — use z.string().jwt() instead.

This is the same issue as line 42-46. Replace z.jwt() with z.string().jwt().

🔎 Suggested fix
         headers: z.object({
-          "x-uiuc-token": z.jwt().min(1).meta({
+          "x-uiuc-token": z.string().jwt().min(1).meta({
             description:
               "An access token for the user in the UIUC Entra ID tenant.",
           }),
         }),

97-103: createdAt should be set to an actual timestamp.

The createdAt field is set to an empty string, which loses the RSVP creation time. Use new Date().toISOString() for proper auditing.

🔎 Suggested fix
       const entry = {
         partitionKey: `${request.params.eventId}#${upn}`,
         eventId: request.params.eventId,
         userId: upn,
         isPaidMember,
-        createdAt: "",
+        createdAt: new Date().toISOString(),
       };
🧹 Nitpick comments (2)
tests/unit/rsvps.test.ts (2)

33-35: Remove debug console.log from mock.

This debug statement adds noise to test output and isn't needed for the test logic.

🔎 Suggested fix
         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",
           };
         },

89-89: Remove unnecessary await calls to createJwt().

createJwt() is a synchronous function (defined in tests/unit/auth.test.ts line 15). It's called with await in some tests (lines 174, 241, 268, 289, 316) but correctly without await in others (lines 89, 132, 194, 221). Remove the unnecessary await keywords for consistency.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 7d22acb and 0c5a8e5.

📒 Files selected for processing (2)
  • src/api/routes/rsvp.ts
  • tests/unit/rsvps.test.ts
🧰 Additional context used
🪛 Gitleaks (8.30.0)
tests/unit/rsvps.test.ts

[high] 17-17: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🔇 Additional comments (5)
tests/unit/rsvps.test.ts (2)

16-17: False positive from static analysis — this is a well-known example JWT.

The Gitleaks warning can be safely ignored. This JWT is a widely-used example token (sub: "1234567890", name: "John Doe") with the secret "your-256-bit-secret" from jwt.io. It contains no sensitive data and is appropriate for test fixtures.


80-333: Good test coverage for RSVP endpoints.

The test suite covers the key RSVP lifecycle scenarios including success paths, conflict handling, idempotent operations, and error cases. The mock setup and assertions are well-structured.

src/api/routes/rsvp.ts (3)

1-20: Imports are well-organized.

All necessary dependencies are imported for DynamoDB operations, error handling, Zod validation, and Fastify type providers.


205-292: Config endpoint implementation looks solid.

The handler correctly handles both setting and removing the RSVP limit, with proper conditional expressions and error handling for non-existent events.


452-457: Idempotent withdrawal behavior is well-designed.

Returning 204 even when the RSVP doesn't exist ensures idempotency — the end state (user not RSVP'd) is achieved regardless of the starting state.

Comment on lines +348 to +357
{
Update: {
TableName: genericConfig.EventsDynamoTableName,
Key: marshall({ id: request.params.eventId }),
UpdateExpression: "SET rsvpCount = rsvpCount - :dec",
ExpressionAttributeValues: marshall({
":dec": 1,
}),
},
},
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

rsvpCount could become negative.

If rsvpCount is already 0 (due to data inconsistency or race condition), decrementing it will result in a negative value. Consider adding a condition to prevent this.

🔎 Suggested fix
           {
             Update: {
               TableName: genericConfig.EventsDynamoTableName,
               Key: marshall({ id: request.params.eventId }),
-              UpdateExpression: "SET rsvpCount = rsvpCount - :dec",
+              UpdateExpression: "SET rsvpCount = if_not_exists(rsvpCount, :zero) - :dec",
+              ConditionExpression: "rsvpCount > :zero",
               ExpressionAttributeValues: marshall({
                 ":dec": 1,
+                ":zero": 0,
               }),
             },
           },

Alternatively, use SET rsvpCount = if_not_exists(rsvpCount, :one) - :dec with a floor check in application logic.

📝 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
{
Update: {
TableName: genericConfig.EventsDynamoTableName,
Key: marshall({ id: request.params.eventId }),
UpdateExpression: "SET rsvpCount = rsvpCount - :dec",
ExpressionAttributeValues: marshall({
":dec": 1,
}),
},
},
{
Update: {
TableName: genericConfig.EventsDynamoTableName,
Key: marshall({ id: request.params.eventId }),
UpdateExpression: "SET rsvpCount = if_not_exists(rsvpCount, :zero) - :dec",
ConditionExpression: "rsvpCount > :zero",
ExpressionAttributeValues: marshall({
":dec": 1,
":zero": 0,
}),
},
},
🤖 Prompt for AI Agents
In src/api/routes/rsvp.ts around lines 348-357, the update currently decrements
rsvpCount unconditionally which can drive it negative; add a DynamoDB
ConditionExpression to only apply the decrement when rsvpCount >= :dec (or when
attribute not exists treat as 0 and fail) and include the corresponding
ExpressionAttributeValues, or alternately use UpdateExpression with
if_not_exists(rsvpCount, :zero) and a ConditionExpression to ensure the
resulting value is >= 0; also handle the ConditionalCheckFailedException in the
calling code to return an appropriate error instead of allowing a negative
count.

Comment on lines +436 to +444
{
Update: {
TableName: genericConfig.EventsDynamoTableName,
Key: marshall({ id: request.params.eventId }),
UpdateExpression: "SET rsvpCount = rsvpCount - :dec",
ExpressionAttributeValues: marshall({
":dec": 1,
}),
},
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

Same rsvpCount negative issue applies here.

The same potential for negative rsvpCount exists in the user withdraw endpoint. Apply the same fix as suggested for the manager delete endpoint.

🤖 Prompt for AI Agents
In src/api/routes/rsvp.ts around lines 436-444, the Update operation that
decrements rsvpCount can drive the counter negative; add the same guard used in
the manager delete flow by adding a ConditionExpression that requires rsvpCount
> :zero and include ":zero": 0 in ExpressionAttributeValues (properly
marshalled). This ensures the update will fail instead of decrementing below
zero; handle the conditional failure where appropriate (same handling as the
manager delete endpoint).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants