Skip to content

Leaderboard api - #561

Open
btfcookies wants to merge 2 commits into
qbreader:mainfrom
btfcookies:feat/public-leaderboard-api
Open

Leaderboard api#561
btfcookies wants to merge 2 commits into
qbreader:mainfrom
btfcookies:feat/public-leaderboard-api

Conversation

@btfcookies

Copy link
Copy Markdown
Contributor

Summary

  • Exposes user data via a GET api/leaderboard endpoint. This is different from the existing api/admin/leaderboard, I expanded it to include numCorrect, powers, tens, negs, tossupPoints, bonusPoints, points, pptu, ppb, accuracy, and averageCorrectCelerity.
  • api/admin/leaderboard and client/admin/leaderboard are unaffected
  • api/leaderboard is gated behind a shared secret (check your dms)
  • Adds api docs for the new endpoint
  • I also fixed 3 minor bugs related to database/account-info/leaderboard.js: slice(0, limit) returned [] when limit was omitted, deleted users left rows that didn't have usernames, mergeTwoSortedArrays didn't match MongoDB's sort outside of ASCII, which split not ASCII usernames into 2 rows.

btfcookies and others added 2 commits August 6, 2026 20:59
Ranks every user by the number of questions they have heard. The data
already existed behind /api/admin/leaderboard; this exposes it on the
public, CORS-enabled API and widens it beyond raw play counts.

database/account-info/leaderboard.js now also reports numCorrect,
powers, tens, negs, tossupPoints, bonusPoints, points, pptu, ppb,
accuracy, and averageCorrectCelerity. The existing fields are kept, so
/api/admin/leaderboard and client/admin/leaderboard are unaffected.

Also fixes three problems in that aggregation:

- slice(0, limit) returned [] when limit was omitted, since
  Array.prototype.slice treats a null end index as 0. Omitting the
  limit now returns every row.
- Users deleted since their buzzes were recorded produced rows with no
  username. They are dropped.
- mergeTwoSortedArrays compares usernames with < and >, but the inputs
  were ordered by MongoDB's byte-wise sort. The two disagree outside
  ASCII, which split a non-ASCII user into two rows. Both inputs are
  now sorted in JS with the same comparison the merge uses, and the
  unindexed $sort stages are gone.

The route catches rejections from the aggregation and returns 503.
Express 4 does not forward a rejected promise to the error handler, so
without this a database error takes down the process.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This endpoint returns every user's username alongside their play stats.
Rather than deciding on the maintainers' behalf that this is fine to
publish openly, require a ?key= that matches LEADERBOARD_KEY in the
environment. Requests with a missing or wrong key get 401.

If LEADERBOARD_KEY is unset, every request is rejected rather than the
endpoint quietly falling back to public — a deploy that forgets to set
it fails closed, not open.

The comparison hashes both sides with SHA-256 before calling
timingSafeEqual, since that function throws on unequal-length buffers
and comparing raw lengths would leak the true key length to a timing
attack.

Cache-Control changes from public to private: a shared or CDN cache
must not be allowed to serve this response to a caller that supplied
no key of its own.

Docs updated to describe the key requirement and the new 401.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@geoffrey-wu

Copy link
Copy Markdown
Member

i think i understand the shape of this PR. some objections:

  • i'm pretty opposed to being able to see other people's stats without them granting permission in some way. it does feel weird to gate it behind this shared secret, too, i don't think there's a compelling reason why one person may or may not have this secret. also it's just annoying to set up (e.g. i couldn't add it to the heroku instance!), in general i'd like to not lock access behind env variables and instead behind authentication
  • the 3 minor bugs sound good to fix and i'd be happy to review in a separate PR
  • now that i look at it, the api docs seems like a good place to use SSIs

@btfcookies

Copy link
Copy Markdown
Contributor Author

Thanks for the review, I understand the need for user privacy. I will make a separate pr for the bug fixes. As for the api, would you be willing to instead setup an outbound webhook to POST the data directly to my server? It would still be secure since you could sign the HTTP request contents with your private key and I will verify the signature on my server using your public key. Additionally, I can change the functionality of the leaderboard so that data will only be displayed for users who make an account.

@btfcookies

Copy link
Copy Markdown
Contributor Author

oh yeah and as for the SSIs i will try to implement that in a seperate pr (if i have time before school starts)

@geoffrey-wu

Copy link
Copy Markdown
Member

the SSI was super fast and brought me much joy so i went ahead and implemented it

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.

2 participants