feat: the forum — old-school boards, modernized#3
Open
tsvb wants to merge 41 commits into
Open
Conversation
XenForo-style categories/boards/threads built in-repo (MIT), magic-link identity (first accounts on the site), BBCode subset, reactions, unread markers, generated avatars, standard moderation kit. Server-first: plain forms + server actions, all three experience modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Identity (magic links, sessions), core forum (schema, BBCode, pages), trimmings (reactions, unread, avatars, quoting), moderation kit (reports, admin, throttles, honeypot). TDD steps with complete code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… emails) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…name-taken) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on timing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nsert Add safeBack() to app/forum/actions.ts so the "back" hidden field can only redirect to same-origin forum paths, closing an open-redirect vector in reactAction (and giving later back-field consumers a shared, safe helper). Make toggleReaction's add branch use onConflictDoNothing on the (post_id, user_id) PK so a raced double-submit is a silent no-op instead of an unhandled unique-violation throw, matching the action's low-stakes fall-through intent. Adds a regression test proving the duplicate insert no longer throws.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep aria-label in original username case per accessibility spec, while using lowercased hash for deterministic color + cell computation. Update determinism test to compare only visual output (fill + rects), ignoring aria-label, since case variants now differ in the label text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add regression tests for quoteBBCode to capture expected behavior: - Sibling quote blocks: text between/after blocks is preserved - Unclosed quote tag: doesn't match innermost regex, passes through - Triple-nested quotes: all innermost pairs stripped, leaving final content Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tes 500'd) A prior hardening commit exported a synchronous safeBack from app/forum/actions.ts, which carries "use server" — Next requires every export there to be an async server action, so the non-async export made every /forum/* route 500 at runtime (vitest and tsc both passed, masking it). Moved safeBack + its test to lib/forum/safe-back.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ion)
requestLogin's RateLimitError handler returned {status:"error"}, which
loginAction rendered as a distinct visible state from {sent:true}. Since
unknown emails always return "silent" and only real emails accrue enough
issueToken calls to hit the 3/hr cap, submitting the login form 4x let an
attacker distinguish real from unknown emails. Rate-limited real emails
now return "silent" too — indistinguishable from unknown, no token issued
either way, limit still enforced.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ail-change race A banned user retained a valid session and could still update their signature (shown forum-wide under all posts) or change their account email via /forum/settings, since bannedError() only gated the content mutations, not the settings/email-change actions. Gate both actions at the requireUser layer using the same bannedError() check, now exported from lib/forum/mutations. Also hardens the email-change confirm path: two users racing to claim the same new address would 500 the second confirmation instead of failing gracefully as an invalid link. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
|
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.
Adds an old-school forum — modernized — to Goose Index: XenForo-style categories → boards → threads → flat replies, the site's first-ever user accounts, and a full moderation kit. Built in-repo (MIT) on the existing Vercel + Neon + Drizzle stack — no new hosting, no new npm dependencies (Resend is called with
fetch).Follows the spec + plan committed at the base of this branch:
docs/superpowers/specs/2026-07-15-forum-design.mddocs/superpowers/plans/2026-07-15-forum.mdWhat's in it
Identity (passwordless). Username + email, magic-link sign-in via Resend (dev prints the link to the console). Sessions and login tokens are stored only as sha256 hashes; sliding 90-day sessions; single-use 15-minute tokens. Deliberately site-level (
users/sessions/login_tokens) so Phase 4 fan-tracking can reuse it.The forum. Board index → board (paginated, pinned-first) → thread (20/page, canonical
{id}-{slug}redirects) → member profiles. A hand-rolled BBCode parser ([b] [i] [u] [s] [url] [quote] [code], autolinked URLs) renders to a safe React AST — neverdangerouslySetInnerHTML, withjavascript:/data:schemes refused and an XSS corpus in the tests. Transactional denormalized counters, guarded by averifycheck.The XenForo feel. Like 👍 / Honk 🪿 reactions, per-member unread markers with jump-to-first-unread, deterministic goose-palette identicon avatars, signatures, quoting (nested-quote stripping), and a "who's online" line.
Moderation. Report queue + admin page, soft-delete tombstones (counters untouched), lock/pin/ban, posting throttles (30s/60s by account age, daily caps, link caps for new accounts), a signup honeypot (hidden field + HMAC time-trap), and magic-link issuance rate limits.
Everywhere. Every page renders in all three experience modes (3.0 / 2.0 / 1.0), and every mutation is a plain HTML form + server action — the whole forum works with JavaScript disabled (1.0 is fully operational). New routes are dynamic;
/forum+ boards join the sitemap.How it was built
27 tasks across 4 phases (identity → core forum → trimmings → moderation), each TDD'd, individually reviewed, and gated. The suite went from 511 → 642 tests;
typecheckclean; production build green;npm run verifypasses including four new forum-counter integrity checks. A final whole-branch review caught one cross-task seam bug — banned users could still write a (publicly-displayed) signature — now fixed and verified live.Before this can be announced (see
docs/DEPLOY.md)RESEND_API_KEY,AUTH_EMAIL_FROM,AUTH_SECRET(openssl rand -hex 32) in Vercel.npm run make-admin -- <your-email>against prod.🤖 Generated with Claude Code