Hard problems features#2
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The upvote endpoints use env.APPROVALS (production KV), not local wrangler state. The comment was left over from early development and was incorrect. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NotThatKindOfDrLiz
left a comment
There was a problem hiding this comment.
Requesting changes. The upvote feature is close, but this should not merge until the POST auth/body binding, slug validation, and vote storage correctness are fixed. Root npm test passes locally; worker tsc --noEmit currently fails on an existing nostr-tools MessageEvent type issue outside this diff.
| mutationFn: async (slug: string) => { | ||
| if (!user) throw new Error('Not logged in'); | ||
| const url = `${API_BASE}/api/hard-problems/upvote`; | ||
| const token = await createNip98Token(user, url, 'POST'); |
There was a problem hiding this comment.
This POST auth token is not bound to the request body. createNip98Token only signs URL + method, and the worker verifier does not check a NIP-98 payload hash, so a captured token can be replayed within the 60s window with a different slug/body. Please add body hashing for POST/PUT/DELETE auth tokens and verify the payload tag server-side before shipping this endpoint.
| } | ||
|
|
||
| const { slug } = body; | ||
| if (!slug || typeof slug !== 'string') { |
There was a problem hiding this comment.
Please validate slug against the seeded Hard Problems list before constructing KV keys. As written, any approved attendee can write arbitrary upvote:user:* and upvote:count:* keys and grow the APPROVALS namespace. Load insights:hard-problems, derive the allowed slug set, and reject anything else. Stable explicit slugs in the seed would be even better than deriving from headings.
| const userVoteKey = `upvote:user:${npub}:${slug}`; | ||
| const countKey = `upvote:count:${slug}`; | ||
| const existing = await env.APPROVALS.get(userVoteKey); | ||
| const current = await env.APPROVALS.get(countKey); |
There was a problem hiding this comment.
This read-modify-write counter is not safe under concurrent votes. Two attendees upvoting at once can both read the same count and overwrite each other, and the separate count/user writes can desync on partial failure. For this event scale, consider storing only per-vote keys such as upvote:vote:<slug>:<npub> and deriving counts from the vote keys, or move the counter to an atomic primitive like a Durable Object.
| {filteredSections.map((section) => { | ||
| const slug = slugify(section.heading); | ||
| const count = upvotesData?.counts[slug] ?? 0; | ||
| const hasVoted = upvotesData?.userVotes.includes(slug) ?? false; |
There was a problem hiding this comment.
This can still crash if the API returns { counts } without userVotes, or if an older/malformed cached value has a different shape. Use a normalized array check, e.g. Array.isArray(upvotesData?.userVotes) && upvotesData.userVotes.includes(slug), and apply the same defensive handling in the cache update in useUpvoteProblem.
| if (!data) return []; | ||
| const seen = new Set<string>(); | ||
| data.sections.forEach(s => s.tags?.forEach(t => seen.add(t))); | ||
| return Object.keys(TAG_COLORS).filter(t => seen.has(t)); |
There was a problem hiding this comment.
This drops any tag that exists in the data but is not already listed in TAG_COLORS, so those tags can appear on cards but never be filterable. Build the filter list from the data first, then use TAG_COLORS[tag] only as a color lookup fallback. Also note that the local worker/hard-problems-data.local.json currently has no tags, so this UI will render an empty filter bar unless the seeded production data has diverged.
Add upvote feature and tag filtering to Hard Problems
under upvote:count: and upvote:user:: keys