feat(profile): editable identity + username-led leaderboard - #202
Merged
Conversation
Students could not change any of their identity after registering: the profile page was read-only, there was no update endpoint, and the admin PATCH route only toggled role flags. A typo'd roll number was permanent, and a first year who registered before their SRN was assigned could never add it — which blocked them outright, since joining a Monstr contest requires a non-null SRN. Profile editing - updateProfile() in server/profile.ts: applies only the fields sent, so a partial PATCH works. Normalizes exactly as registration does (lowercase username, uppercase roll numbers) so edited values stay comparable to registered ones, and re-checks the three unique columns with ne(users.id, userId) — without that, every save collides with its own row. Returns a discriminated result, mirroring deleteUser(). - PATCH /api/profile: session-gated, rate limited 10/min per user. No session re-issue needed — getCurrentUser() re-reads the row per request, so edits are live immediately; sessionEpoch stays reserved for resets. - ProfileEditForm: an "Edit details" toggle over the Identity panel. Email stays read-only; it drives the OTP verification flow. - No SRN/PRN format regex: none has ever existed, so existing rows may not match one we invent, and re-validating an untouched value would block otherwise-valid saves. Length-capped instead. Leaderboard - Solver / Name / SRN columns, replacing the single SRN-else-PRN cell. Solver shows @username; SRN falls back to PRN; a missing name renders "—". - Board identity moves back to username across all five comparison sites, so the "you" highlight and profile rank matching stay correct. - Note: the board is unauthenticated, so this makes usernames, real names and SRNs public. Reverses 73505aa, which had hidden them deliberately. No migration: every column already exists.
barunaniket
requested review from
hagemaruwu,
shivanshpap and
vaibhavtulsian
as code owners
September 7, 2026 20:54
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
Summary
Makes a student's identity editable from
/profile(name, username, SRN, PRN), and reworks the CP Arena leaderboard to identify solvers by Solver / Name / SRN columns instead of a single SRN-else-PRN cell.Nothing about a user's identity could be changed after registration — the profile page was read-only, there was no update endpoint, and
PATCH /api/admin/users/[id]only toggles role flags. So a typo'd roll number was permanent, and a first year who registered before their SRN was assigned could never add it later, which blocked them outright since/api/monstr/joinrequires a non-null SRN.Type of change
challenges/README.md+ a committed authoring templateRelated issues
Closes #
How was this tested?
Manually against a local dev server (
npm run dev) with two registered accounts, plus a third seeded withname = NULLandsrn = NULLto exercise both fallbacks.Profile editing —
PATCH /api/profile200— proves thene(users.id, userId)self-exclusion works409"That username is taken."409with the field-specific messageAB,has spaces)400400400"Nothing to update."400401429(rate limit holds)200; the old username correctly fails with401nullSession survives a rename (it is keyed by user id, and
getCurrentUser()re-reads the row per request, so edits are live with no re-issue).Leaderboard —
/api/leaderboardreturnsdisplay/name/identityon all three scopes (today,month,all). Verified an account with no SRN falls back to its PRN, and an account with no name renders—.LeaderboardTablewas server-rendered for both thetodayandmonthcolumn sets to confirm the headers, the@prefix, the fallbacks, and the "you" highlight.Checks —
npx tsc --noEmit,npm run lint(0 errors; the 4 warnings are pre-existing and unchanged on a clean tree),npm run test(72/72),npm run build,npm run cf:build(OpenNext bundle builds and includes/api/profile),npm run challenges:validate.Checklist
npx tsc --noEmit,npm run lint,npm run test,npm run build— note: on Node 24, not Node 22; worth a look at the CI runpackage-lock.jsonchanges — my working tree had the native-binding lockfile churn described in CONTRIBUTING.md §3 and it was deliberately left out of this branchtext-charcoal/…/mecha-*tokens, which are already theme-aware, but nobody has actually looked at it in dark modenpm run challenges:validatepassesNotes for reviewers
1. The leaderboard change makes member data public — please make this a conscious call.
/leaderboardand/api/leaderboardneed no session, so the board now exposes every solver's login handle, real name, and SRN to anyone.usernameis what/api/auth/loginlooks users up by, so the board becomes an enumerable list of valid login identifiers paired with names and roll numbers.This deliberately reverses 73505aa, whose message reads "The login handle (username) and email stay private" — and goes further by adding name and SRN. It is what was asked for, and passwords plus rate limiting remain the real gate, but it is a one-way door once it is live and indexed. The cheap mitigation, if wanted later, is a
getCurrentUser()gate on the leaderboard route.2. Known staleness.
aggregateLeaderboardsits behind the existing 30sAGGREGATE_TTL_MScache, so after an edit the month/all-time boards and the profile rank can show old values — and briefly drop the "you" highlight — for up to 30s. Today's board is uncached. Self-healing; no invalidation added.3. No SRN/PRN format validation. None has ever existed in this codebase, so existing rows may not match any pattern we invent, and re-validating an untouched value on save would block otherwise-valid edits. Length-capped only. Say the word if you want a real PESU SRN regex plus a backfill plan.
4. Validation is now triplicated.
USERNAME_RE+ normalization + the uniqueness clash-check now exist inapi/auth/register,api/admin/users, andupdateProfile. Extracting a shared helper would mean touching two auth-critical routes this PR does not otherwise change, so I left it — worth a follow-up.5.
challenges/EXAMPLE.jsonneeded two supporting changes to work at all: a!/challenges/EXAMPLE.jsonnegation in.gitignore(the folder's*.jsonis ignored, so the template would never have reached contributors), and a skip inscripts/seed-challenges.ts(which otherwise publishes every.jsonit finds, including to production D1 on--target remote). It is still validated, so it cannot rot. I also corrected the README's schema table, which markeddateas required — the Zod schema has it.nullish(), and omitting it is the meaningful "unscheduled pool" state.6. No migration.
username,name,srnandprnall already exist with their constraints.