Ap/admin applicant - #511
Open
adityapat24 wants to merge 2 commits into
Open
Conversation
adityapat24
requested review from
ACoullard,
Yurika-Kan,
mehanana,
mzhang0213,
ph4iry,
roslyn-maloney and
shreeyaadhikari
August 12, 2026 16:54
mehanana
requested changes
Aug 13, 2026
mehanana
left a comment
Contributor
There was a problem hiding this comment.
Just some small changes regarding the error messages, but other than that, the upload section looks good!
I did notice 1 weird issue, not sure exactly which section this relates to, but when I refreshed the application, I was forced to answer the questions that said optional in order to move to the next section. This didn't happen when I was going through the application the first time.
| setUploadError( | ||
| typeof body?.error === "string" | ||
| ? body.error | ||
| : "Could not start the upload. Please try again.", |
Contributor
| errors.map((e) => { | ||
| if (e.code === "file-too-large") return "File exceeds 5 MB limit."; | ||
| if (e.code === "file-invalid-type") | ||
| return "Only PDF, PNG, and JPEG files are accepted."; |
Contributor
There was a problem hiding this comment.
This error message is hardcoded to include all mime types, but I think it would be clearer to the user if it was tailored to the mime types allowed for this specific dropzone (eg. for resume, it should say only pdf allowed).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


App Portal: bug-fix + 2026 application-form pass
This PR spans a broad backend/frontend cleanup pass across the applicant portal and admin panel: real 2026 application questions, a fix connecting the admin form editor to the live application form, a real resume/vaccination-card upload pipeline, a stats-dashboard data bug fix, and a long list of smaller correctness/security fixes (auth guards, validation gaps, a data-corruption race condition, stale references left over from renaming form fields).
Sections below are split by owner area so each of you can review/test just your part.
General setup: pull,
yarn install,yarn dev:app. You'll need to sign in with an@hackbeanpot.comemail to reach/admin, or any email for the applicant side.Michael — Auth + Sign-in flow (admin and application)
What changed:
middleware.tswas a no-op — despite comments claiming it redirected unauthenticated users, it just calledNextResponse.next()unconditionally, and its route matcher also didn't actually exclude/api/*(only checked for paths starting withauth, notapi/auth). It now really checks the session (via a call to the/auth/sessionendpoint, since sessions are DB-backed and can't be decoded in the Edge runtime) and redirects unauthenticated users to sign-in, and the matcher correctly excludes all API routes sofetch()callers get JSON errors instead of being redirected to an HTML page./uploads-demo(Mehana's test page for the upload component) had no auth guard at all — reachable by anyone. Gated it behind the same admin check(admin)/layout.tsxuses./) had aTODO: redirect authed users to /dashboardthat was never implemented — now does.SignInForm.tsxhad two//todocomments and raw hex/rgb colors (#FF0000,rgb(120,255,150)) for error/success text — swapped for the app's real design tokens (firecrackerRed,darkGreen), which are already used for the same purpose elsewhere.lib/auth/email-transport.ts: magic-link emails silently fell back tohttp://localhost:3000for the logo URL ifNEXTAUTH_URLwas unset — now throws loudly in production instead of shipping a broken link.Files:
middleware.ts,app/(landing)/page.tsx,app/uploads-demo/{page.tsx,UploadsDemoClient.tsx},components/auth/SignInForm.tsx,lib/auth/email-transport.tsHow to test:
/dashboard,/application,/rsvp, or/admin— should redirect to/auth/signin(not render anything, not throw an error).@hackbeanpot.comemail, confirm you land on/dashboardand that visiting/adminredirects you back to/dashboard.@hackbeanpot.comemail, confirm you land on/admin, then visit/uploads-demo— should work for you but redirect a non-admin./— should redirect straight to/dashboardinstead of showing the landing page.Mehana — GCS setup and resume handling
What changed:
FileUpload.tsx) had a dead "mock upload" branch left over from an earlier iteration — real GCS is fully configured now, so that path could never legitimately trigger, but if the sign request ever failed, the code didn't checkres.okand fell through into callingxhr.open("PUT", undefined). Removed the dead branch, added a realres.okcheck, and actual error messages are now shown to the user instead of failing silently.acceptprop toFileUploadso a given instance can restrict to specific MIME types (resume → PDF only, vaccination card → PNG/JPEG only) instead of always allowing all three app-wide types.lib/uploads/validation.ts:size <= 0/size > MAX_FILE_SIZE_BYTESare bothfalseforundefined/NaN, so a request with a missing/non-numeric size skipped size validation entirely. Fixed.getUploadRecord(uploadId)tolib/uploads/service.ts— used by the admin applicants page (Andre's area) to show a real filename instead of the raw upload ID.signUploadRequest/signUploadResponse) inlib/uploads/types.tsthat were never called anywhere.Fileobject that got silently dropped before saving. They're now wired to your real GCS upload flow.Files:
components/uploads/FileUpload.tsx,lib/uploads/{service,types,validation}.tsHow to test:
/application→ Documents section, upload a resume as a PDF — should show real upload progress (not the old instant-fake-progress-bar) and complete successfully..docxor oversized file for the vaccination card (PNG/JPEG only) — should be rejected client-side with a clear message, not silently accepted./admin/applicants/[id]) and confirm the resume shows a real filename, not the raw upload ID string.Yurika — Admin Stats page
What changed:
lib/stats/aggregations.ts:getTotals()andgetDecisionBreakdown()were matching against literal capitalized/space-separated strings ("Submitted","Admitted","Not Attending") that no real write path in the app can ever produce — every write goes through a zod enum restricted to lowercase-hyphenated values ("submitted","not-attending", etc., perlib/types/user.ts). This meant every stat tile except "Total Applications" always showed 0, and the decision-breakdown chart was always empty. Fixed to match the real values (case-insensitively, as a safety net).lib/stats/types.ts:DEMOGRAPHICS_DIMENSIONSlisted dimension names (yearOfEducation,majors,races,shirtSize,hackathonsAttended,csClassesTaken) that don't match any real question ID — those 6 of 8 demographics charts were always empty. Renamed to the real 2026 question IDs.lib/stats/service.ts: demographics were also displaying raw option-value slugs as chart labels (e.g."black_or_african_american"instead of "Black or African American"). AddedresolveDemographicsLabels(), which maps every dimension's values through the live form config's option list — so edits admins make in Form Configuration (Roslyn's area) are reflected in the chart labels too.api/v1/stats/route.tshad no auth guard at all — addedrequireAdmin().aggregations.test.ts,service.test.ts) exercising all of the above against an in-memory Mongo instance.Files:
lib/stats/{aggregations,types,service}.ts,components/admin/stats/DemographicsChart.tsx,app/api/v1/stats/route.tsHow to test:
/admin/statswith some real/seeded submitted applicants in the DB. Confirm the stat tiles (Submitted, Admitted, RSVP'd) show real non-zero numbers, and the Decision Breakdown chart isn't empty.GET /api/v1/statsdirectly (curl or browser) — should now 401, not return data.Roslyn — Admin Settings and Form Configuration
What changed:
admin/settings/page.tsxwas self-fetching its own API routes over HTTP using a hardcodedhttp://localhost:3000— worked in dev, would've been completely broken in any deployed environment. Rewired to read the underlying singletons directly (same pattern the stats/applicants pages already used)./admin/settingshad no effect on what applicants actually saw, because the real form imported a hardcoded static question list.lib/admin/types.ts'sFormConfignow aliases the sameQuestion/FormSectiontypes the real form uses, and the form itself fetches its live schema from the same Mongo-backed config your editor writes to. Editing/adding a question here now really does change the live application form (see Phaedra's section for the consumer side).FormConfigEditor.tsx: replaced a rawalert(data.error)on save failure with an inline error message.form-config-service.ts:updateFormConfighad no check that the submitted config isn't empty — POSTing{"sections": []}passed every other check and would've silently wiped the live application form to zero questions. Now rejected. Also fixed a bug where it queried the wrong (unsuffixed) Mongo collection name in dev/test.api/v1/admin/form-config/route.ts'sGEThad no auth guard (onlyPOSTdid) — added.registration-open,registration-closed,confirm-by) had no cross-field validation — you could setregistration-closedbeforeregistration-open, orconfirm-bybefore either, and each route would accept it independently. Added ordering validation (registration-open ≤ registration-closed ≤ confirm-by) shared across all three routes, which were also deduplicated from three near-identical copies into one shared handler, and fixed to return proper 401/403 instead of an uncaught 500 for unauthenticated requests.loading.tsxfor/adminand/admin/settings(previously only some admin routes had one).Files:
app/(admin)/admin/settings/page.tsx,app/(admin)/admin/{loading,page}.tsx,app/(admin)/admin/settings/loading.tsx,components/admin/FormConfigEditor.tsx,lib/admin/{types,form-config-service,singleton-service,date-route-handlers}.ts,app/api/v1/admin/form-config/route.ts,app/api/v1/dates/{registration-open,registration-closed,confirm-by}/route.tsHow to test:
/admin/settings→ Form Configuration. Add a new question or edit an existing one's label, save, then immediately open/applicationin another tab (or as another user) — the change should show up on the real form. This is the main thing to verify; it didn't work at all before this PR.Shreeya — Applicant Dashboard and RSVP
What changed:
InProgressView.tsx: the "Continue application" button linked to/instead of/application(never actually took you back to your draft). Also, the completion percentage was hardcoded to 60% or 100% based only on whether the application was submitted — now computed for real from how many questions in the live form you've actually answered (getCompletionPercent()inlib/status/service.ts/lib/application/service.ts).AdmittedView.tsx: "RSVP before XXX to hold your spot" was a literal, un-filled-in placeholder — now shows your real confirm-by date (the data was already being passed in, just never used).SubmittedView.tsx: same broken/link, plus "decisions on XXX" / "Review date: XXX" placeholders — there's no real "decision announcement date" field in the data model to back a specific date here, so rephrased to accurate, non-fabricated copy instead of inventing one.DeclinedView.tsx: "Join the mailing list" was a dead link to/. BuiltJoinMailingListButton.tsx, which actually POSTs your email to the (previously unused!)/api/joinMailingListendpoint.WaitlistedView.tsx: hardcoded contact email centralized intolib/config/site.ts.PortalShell.tsx: removed leftover dev-only placeholder copy ("Review the status mock in the route handler...") from the fallback state.post-acceptance/route.ts(RSVP submission): added a guard against a missing session user ID instead of silently querying Mongo with an empty string.Files:
app/(applicant)/dashboard/page.tsx,components/dashboard/{InProgressView,AdmittedView,SubmittedView,DeclinedView,WaitlistedView,PortalShell,JoinMailingListButton}.tsx,lib/status/{service,types}.ts,lib/config/site.ts,app/api/v1/post-acceptance/route.tsHow to test:
/dashboard— confirm the completion % actually reflects how much of the form you've filled in (not stuck at 60%), and "Continue application" takes you back into the form.XXXappears anywhere on Submitted, and Declined's "Join the mailing list" button actually works (check it doesn't error).Phaedra — Applicant Application Form
What changed:
lib/application/questions.tsnow has the real 2026 HackBeanpot questions (9 sections, 43 questions) instead of the old placeholder set. A few things from the original doc were deliberately simplified: conditional "if not listed, specify" fields render as always-visible optional fields (no show/hide-on-condition support yet), and "how did you hear about us" was flattened to one multi-select + one free-text "other" field.ApplicationForm.tsxbuilds its validation schema dynamically per-load.LongTextField.tsx.Fileobject that got silently dropped before saving (selecting a file did nothing at all, even though it looked like it worked).react-hook-formbehavior when using a resolver, regardless of which fields you pass totrigger()), which was setting "required" errors on fields in sections you hadn't even reached yet — you'd land on section 3 and immediately see a red "required" error on a field you never touched. Now clears those premature errors once the current section is confirmed valid.lib/application/service.ts:saveDraft()had no guard against writing over an already-submitted application. A stray/delayed autosave request landing just after a Submit click could silently revert the submitted answers back to stale draft content, while the status still said "submitted." Fixed — a submitted application is now treated as authoritative and autosave becomes a no-op once that's happened.registration/route.ts: draft-save requests had no body validation at all (garbage could be written straight to Mongo) and malformed JSON caused an uncaught 500. Both fixed.application/loading.tsx(previously missing).Files:
lib/application/{questions,schema,service,types}.ts,components/application/{ApplicationForm,FileUploadField,LongTextField,QuestionField}.tsx,app/api/v1/registration/route.ts,app/(applicant)/application/loading.tsxHow to test:
Andre — Admin Applicants Page
What changed:
legal_name; the 2026 rewrite splits it intofirst_name/last_name. That silently broke the applicants list's name column, sort-by-name, search-by-name, and both CSV exports — all still referenced the deadlegal_namekey and would've shown blank names for every 2026 applicant. Fixed with a sharedgetApplicantName()helper used consistently everywhere names are displayed, sorted, searched, or exported.rsvpStatusto anything other than "unconfirmed" unless the applicant's decision status is actually "admitted" — mirrors the rule the self-service RSVP flow already enforces, just wasn't checked on the admin edit path.getUploadRecord()) instead of the raw upload ID string.applicants/[id]/route.tsnow returns a proper 409 for the new RSVP-state error instead of an uncaught exception.Files:
lib/applicants/{service,queries}.ts,app/api/v1/applicants/[id]/route.ts,app/api/v1/export/{applications,post-acceptance}/route.tsHow to test:
/admin/applicantsfor an applicant who filled out the 2026 form — confirm their name shows up in the list (not blank), search for them by first or last name, and sort the list by name.