Skip to content

Application Form — submit + validation + closed-window enforcement #499

Description

@adityapat24

Context

Last sprint shipped draft save/load (getDraft, saveDraft, GET/POST /api/v1/registration). What's missing is the actual submit flow, server-side validation of responses, and enforcement of the registration-closed cutoff.

Tasks

Submit endpoint

  • Implement submit(userId, payload) in src/lib/application/service.ts. Validates payload against the Zod schema, checks applicationStatus isn't already 'submitted', sets applicationStatus: 'submitted', sets appSubmissionTime: new Date().
    • What this accomplishes: The function that turns an in-progress application into a submitted one. Also prevents double-submits by refusing to re-submit an already-submitted application.
  • Wire PUT /api/v1/registration in src/app/api/v1/registration/route.ts to call submit. Returns { ok: true, submittedAt } on success, 400 with error details on validation failure, 403 if registration is closed, 409 if already submitted.
    • What this accomplishes: The endpoint the submit button on the frontend calls. The distinct status codes let the frontend show specific error messages ("form has errors" vs. "registration closed" vs. "you already submitted") instead of a generic failure.

Zod schema

  • Confirm src/lib/application/schema.ts has a Zod schema derived from the frozen questions.ts question list. Every question has a rule matching its type: short-text is a non-empty string with max length, long-text has a max length, select has an enum of allowed options, multi-select is an array of allowed options, file-upload is a non-empty upload ID string.
    • What this accomplishes: Server-side validation catches everything the frontend catches, plus malicious payloads. Frontend validation is a UX enhancement; server validation is the actual guardrail.
  • Required questions produce specific errors when missing. Optional questions accept null/undefined.
    • What this accomplishes: The error messages the frontend displays are useful ("this question is required" vs. a generic 400).
  • The submit endpoint runs the schema in strict mode — no unknown keys allowed.
    • What this accomplishes: Prevents applicants from stuffing extra fields into the payload that could be reflected back somewhere unsafe or bloat the database.

Registration-closed enforcement

  • Before allowing any write (both saveDraft and submit), read the registration-closed singleton via Ticket 5's getSingleton('registration-closed').
    • What this accomplishes: Uses the real admin-configured cutoff, which Ticket 5 shipped last sprint. Consumers of singletons should call the real service, not read mock files.
  • If the current time is past registration-closed, saveDraft still allows changes to already-submitted applications for now but blocks in-progress saves and new submits. Actually, simpler rule: past registration-closed, block both endpoints entirely with 403.
    • What this accomplishes: Clean cutoff behavior. Once closed, the application is frozen — no more edits, no new submissions. Matches the legacy behavior.
  • Same check for registration-open: before now, reject with 403 and a "not yet open" error.
    • What this accomplishes: Prevents applicants from finding the endpoint before the official opening and getting in early.

Auth gating

  • Wire requireUser() from Ticket 1 into all three methods (GET, POST, PUT) of /api/v1/registration. Replace the placeholder userId query param with session.user.id (or email, depending on how Ticket 1 keys users).
    • What this accomplishes: The endpoint now knows who's making the request from their session cookie, not a URL parameter that anyone can spoof. Also every write is tied to the correct user.
  • Remove any temporary userId query param handling and update the frontend to not send it.
    • What this accomplishes: Cleans up the placeholder shim from the previous sprint. Once auth is real, the query param becomes a security hole.

Frontend integration

  • Verify the frontend submit button, currently calling PUT /api/v1/registration, gets the right responses and displays appropriate UI for each status code (400, 403, 409).
    • What this accomplishes: Confirms the round trip works end-to-end. The frontend was built against mocks; this is the moment it starts hitting real logic.

Definition of done

  • Submitting the form successfully changes applicationStatus to 'submitted' in Mongo and records appSubmissionTime.
  • Submitting with invalid data returns a 400 with specific field errors.
  • Submitting after registration-closed returns 403.
  • Submitting an already-submitted application returns 409.
  • All three endpoints are gated by requireUser().

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions