Skip to content

fix: align request payloads with backend DTOs ahead of forbidNonWhitelisted#594

Merged
0xdevcollins merged 2 commits into
mainfrom
fix/forbid-non-whitelisted-payloads
May 28, 2026
Merged

fix: align request payloads with backend DTOs ahead of forbidNonWhitelisted#594
0xdevcollins merged 2 commits into
mainfrom
fix/forbid-non-whitelisted-payloads

Conversation

@0xdevcollins
Copy link
Copy Markdown
Collaborator

Companion to backend PR boundlessfi/boundless-nestjs#187, which flips the global ValidationPipe from forbidNonWhitelisted: false to true. Today any unknown body field is silently stripped; after the flip, sending such a field returns a 400 with class-validator's property X should not exist.

This PR fixes every front-end -> back-end payload an audit flagged.

Changes

1. Team posts — contactInfo shape + contactMethod dropped

Backend CreateTeamDto / UpdateTeamDto expect contactInfo as a sparse object { telegram?, discord?, email? }; the channel is implicit in the populated key. There is no separate contactMethod field.

  • lib/api/hackathons/teams.ts: introduces TeamContactInfo type and readTeamContact(info) helper that flattens the sparse object back to { method, value } for UI display. Team.contactInfo and CreateTeamRequest.contactInfo switch to the new shape; contactMethod is removed from both.
  • CreateTeamPostModal.tsx: keeps the form UX (single channel selector + string input), but the submit handler projects to { contactInfo: { [method]: value } } before calling the API. Edit-mode initial data hydrates via readTeamContact(). github and other were removed from the method enum because the backend cannot store them.
  • ContactTeamModal.tsx: replaced contactInfo / contactMethod direct access with readTeamContact() + contact.method / contact.value. The github icon branch was dropped.
  • app/(landing)/hackathons/[slug]/teams/[teamId]/page.tsx: same readTeamContact() flattening on the team detail page.

2. Judging score — comment -> notes

SubmitJudgingScoreRequest.comment was renamed to notes to match the backend ScoreSubmissionDto.notes. Per-criterion comment stays because CriterionScoreDto does accept it.

  • lib/api/hackathons/judging.ts: field rename.
  • components/organization/cards/GradeSubmissionModal/useScoreForm.ts: call site updated.

3. Submission update — whitelist filter at the hook layer

hooks/hackathon/use-submission.ts now exposes a pickUpdateSubmissionFields() mapping to the backend UpdateSubmissionDto. The update path strips:

  • participationType, teamId, teamName (Create-only)
  • organizationId, hackathonId, participantId (server-derived)
  • per-entry email on teamMembers (TeamMemberDto has no email field)

before PATCH /hackathons/submissions/:id. The form continues to send the full shape; the hook filters at the API boundary so future callers are also protected.

4. User profile — removed preferences block

UpdateUserProfileRequest.preferences had no counterpart on backend UpdateProfileDto. The frontend already uses the dedicated /users/settings/* endpoints for appearance / language / timezone / notifications, so this was an unused typed door. Closed it.

5. Hackathon update — narrowed UpdateHackathonRequest

Was Partial<Hackathon> & { rewards? }, which let any Hackathon-shape field (id, organizationId, status, creatorId, ...) leak into PUT /hackathons/:id. Narrowed to just { rewards?: HackathonRewards } since that's the only field actually sent today (the rank-override save in JudgingResultsTable).

PublishHackathonRequest extends Hackathon was left alone because the publish flow legitimately sends a full hackathon shape.

Out of scope (separate pre-existing bugs)

The audit also surfaced URL mismatches between frontend and backend that are not whitelist issues:

  • PUT /hackathons/:id has no matching backend route. The rank-override save in JudgingResultsTable is broken regardless of feat: Add 'What Makes Boundless Different' Section #187.
  • POST /users/earnings/claim doesn't match backend /users/earnings/withdraw.
  • POST /organizations/:id/invite doesn't match backend /invitations.

Worth filing as separate frontend tickets.

Backend PR #188 (global ThrottlerGuard + per-route 429 limits)

No frontend changes needed. lib/api/api.ts already honours Retry-After and exponential backoff (1s / 2s / 4s) for three retries before surfacing a RATE_LIMIT_EXCEEDED error code. Verified during the audit.

Test plan

  • npm run type-check — clean
  • npm run lint — clean
  • prettier --check . — clean
  • npm run build — pre-commit build passes
  • Manual on staging once backend feat: Add 'What Makes Boundless Different' Section #187 is on a staging deploy:
    • Create + edit a team post (each contact method: email / telegram / discord)
    • Submit + edit a judging score with overall notes
    • Edit a hackathon submission (verify no 400 from stripped fields)
    • Save user profile (verify no preferences regression)

Coordination

Merge after backend PR #187 lands on staging so dev / staging do not 400 in the interim window. The changes here are backward-compatible with the current forbidNonWhitelisted: false backend.

🤖 Generated with Claude Code

…listed

The boundless-nestjs backend (PR #187) is flipping its global
ValidationPipe from forbidNonWhitelisted: false to true. Today any
unknown body field is silently stripped; after the flip, sending such
a field produces a 400. This commit fixes every front-end -> back-end
payload the audit flagged.

1. Team posts (CreateTeamPostModal, ContactTeamModal,
   team-detail page).
   - lib/api/hackathons/teams.ts: replaced `contactInfo: string` and
     `contactMethod` with `contactInfo?: TeamContactInfo` matching the
     backend shape `{ telegram?, discord?, email? }`. Added a
     readTeamContact() helper that flattens the sparse object back to
     `{ method, value }` for UI display.
   - CreateTeamPostModal: form schema keeps the `{ method, value }`
     UX, but onSubmit projects to `{ contactInfo: { [method]: value } }`
     before calling the api. github and other were removed from the
     method enum because the backend cannot store them. Edit-mode
     initial-data hydrates from readTeamContact().
   - ContactTeamModal: replaced direct string access with
     readTeamContact() and used contact.method/value throughout. The
     github icon case was dropped since github is no longer a valid
     method.
   - team-detail page: same readTeamContact() flattening for the
     Contact card.

2. Judging score (lib/api/hackathons/judging.ts, useScoreForm).
   The submitJudgingScore request used `comment` for the global
   per-judge note, but the backend ScoreSubmissionDto names the field
   `notes`. Renamed the interface field and the call site. Per-criterion
   `comment` stays because CriterionScoreDto does accept it.

3. Submission update (hooks/hackathon/use-submission.ts).
   Added a pickUpdateSubmissionFields() whitelist mapping to the
   backend UpdateSubmissionDto. The update path now strips:
     - participationType, teamId, teamName (Create-only)
     - organizationId, hackathonId, participantId (server-derived)
     - per-entry email on teamMembers (TeamMemberDto has no email)
   ...before PATCH /hackathons/submissions/:id. The form continues to
   send the full shape; the hook filters at the API boundary so future
   callers are also protected.

4. User profile (lib/api/auth.ts).
   Removed the `preferences` block from UpdateUserProfileRequest. The
   backend UpdateProfileDto has no such field; users use the dedicated
   /users/settings/* endpoints for appearance / language / timezone /
   notification toggles. The wide type was the same kind of unknown-
   field door the audit is closing.

5. Hackathon update (lib/api/hackathons.ts).
   UpdateHackathonRequest was `Partial<Hackathon> & { rewards? }`,
   which let any Hackathon-shape field (id, organizationId, status,
   creatorId, ...) leak into PUT /hackathons/:id. Narrowed to just
   `{ rewards?: HackathonRewards }` since that is the only field
   actually sent today (JudgingResultsTable's rank-override save).

Out of scope (separate pre-existing bugs to file):
- PUT /hackathons/:id has no matching backend route; the rank-
  override save in JudgingResultsTable is broken regardless of #187.
- POST /users/earnings/claim does not match backend /users/earnings/
  withdraw.
- POST /organizations/:id/invite does not match backend /invitations.

Backend #188 (global ThrottlerGuard + 429 handling): no frontend
changes needed. lib/api/api.ts already honours Retry-After and
exponential backoff (1s, 2s, 4s) for three retries before surfacing
a RATE_LIMIT_EXCEEDED error code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
boundless-kd16 Ready Ready Preview, Comment May 28, 2026 1:56pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

Warning

Review limit reached

@0xdevcollins, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 52 minutes and 53 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3c3770ff-8136-40b5-bdb1-410b266a09ec

📥 Commits

Reviewing files that changed from the base of the PR and between e083d17 and 293a29d.

📒 Files selected for processing (9)
  • app/(landing)/hackathons/[slug]/teams/[teamId]/page.tsx
  • components/hackathons/team-formation/ContactTeamModal.tsx
  • components/hackathons/team-formation/CreateTeamPostModal.tsx
  • components/organization/cards/GradeSubmissionModal/useScoreForm.ts
  • hooks/hackathon/use-submission.ts
  • lib/api/auth.ts
  • lib/api/hackathons.ts
  • lib/api/hackathons/judging.ts
  • lib/api/hackathons/teams.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/forbid-non-whitelisted-payloads

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.

…o shape

Found during code review of the previous commit on this branch.

The strip on teamMembers only removed `email`, but the frontend
SubmissionFormData also lets each entry carry `avatar` — and the
backend TeamMemberDto only has { userId, name, username?, role }.
Sending `avatar` would 400 once forbidNonWhitelisted lands.

Switched from a deny-list strip (`{ email: _email, ...rest }`) to an
explicit project-down to the four backend fields. Same defense-in-
depth pattern as the outer pickUpdateSubmissionFields whitelist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@0xdevcollins 0xdevcollins merged commit fd2b990 into main May 28, 2026
8 of 9 checks passed
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.

1 participant