feat(invites): ADR-0011 Phase 2B — claim resolution + athlete routes + auth#419
feat(invites): ADR-0011 Phase 2B — claim resolution + athlete routes + auth#419theianjones wants to merge 3 commits intofeat/competition-invites-phase-2from
Conversation
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 21 minutes and 29 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (13)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
4 issues found across 11 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/wodsmith-start/src/routes/_auth/sign-in.tsx">
<violation number="1" location="apps/wodsmith-start/src/routes/_auth/sign-in.tsx:78">
P2: Don’t lock invite flow when the invite email is missing; it can disable the email field while leaving it empty, blocking sign-in.</violation>
</file>
<file name="apps/wodsmith-start/src/routes/_auth/sign-up.tsx">
<violation number="1" location="apps/wodsmith-start/src/routes/_auth/sign-up.tsx:121">
P1: Invite mode is activated without requiring an invite email, which can lock the email field empty and make sign-up impossible.</violation>
</file>
<file name="apps/wodsmith-start/src/server/competition-invites/claim.ts">
<violation number="1" location="apps/wodsmith-start/src/server/competition-invites/claim.ts:97">
P2: `resolveInviteByToken` is missing the `activeMarker = active` predicate, so stale terminal rows with a leftover token hash can still resolve.</violation>
</file>
<file name="apps/wodsmith-start/src/routes/compete/$slug/claim/$token.tsx">
<violation number="1" location="apps/wodsmith-start/src/routes/compete/$slug/claim/$token.tsx:158">
P1: The claim CTA drops the resolved invite `divisionId`, so the registration step is no longer tied to the invited division.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
e731ec5 to
aa49035
Compare
9514d8c to
79ec211
Compare
…+ auth
- `src/server/competition-invites/claim.ts`:
- resolveInviteByToken — hash + DB lookup. Returns null on miss so the
route can render a generic "invalid link" page without leaking whether
the token ever existed.
- assertInviteClaimable — throws InviteNotClaimableError with a distinct
reason per terminal state (expired / declined / revoked / already_paid /
not_found). Belt-and-suspenders: also fails if claimTokenHash is NULL
or activeMarker drifted (race safety).
- identityMatch — pure function returning the discriminated result the
claim route branches on: { ok: true } or { ok: false, reason:
wrong_account | needs_sign_in | needs_sign_up }. Case-insensitive via
normalizeInviteEmail.
- `src/server/competition-invites/decline.ts` — atomic pending → declined
transition. Nulls activeMarker + claimTokenHash so the link dies and the
unique-active index unblocks a future re-invite. Idempotent against a
re-read when affectedRows is 0.
- `src/server-fns/competition-invite-fns.ts`:
- getInviteByTokenFn — public (session-free) server fn used by the claim
route loader. Discriminated return: not_claimable or claimable +
{ invite, championshipName, divisionLabel, accountExistsForInviteEmail }.
Verifies the invite's championship slug matches the URL so a mispasted
link doesn't silently resolve to the wrong competition.
- declineInviteFn — authenticated decline. Re-runs resolve + identity
match on the server side so a forged POST can't bypass the email-lock.
- Athlete routes:
- `/compete/$slug/claim/$token` — loader runs getInviteByTokenFn +
identityMatch and branches to happy-path CTA, wrong-account rejection,
or redirects to sign-in/sign-up with ?email=&invite= pre-fill.
- `/compete/$slug/claim/$token/decline` — confirmation dialog, POSTs to
declineInviteFn.
- `/compete/$slug/invite-pending` — informational landing for
wrong-account recovery.
- Sign-in + sign-up extended with ?email=&invite= search params. Email
field is pre-filled and disabled while invite flow is active. Uses a
dedicated `invite` param (not the existing `claim`) so the KV-backed
placeholder-user flow and the hash-backed invite flow stay separate.
- 14 new claim unit tests (64 total).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rkers Vite couldn't bundle the claim/decline routes because both transitively imported `getDb` (which imports `cloudflare:workers`) through `server/competition-invites/claim.ts`. The dev server saw the unresolvable import in the client graph and crashed every request, taking the e2e suite with it. Move the pure helpers (`identityMatch`, `assertInviteClaimable`, types, `InviteNotClaimableError`) into a sibling `identity.ts` and re-export from `claim.ts` for back-compat. Routes now import from `identity.ts` only. Also drops the useless `case "not_found":` fallthrough flagged by biome — `default` already covers it.
…mode, add active filter cubic + coderabbit review on PR #419. Four fixes in the claim handoff: - sign-in / sign-up: invite mode flipped on a bare `?invite=` token even when no email was supplied, locking the email field empty and blocking the user from typing one in. Require both `invite` and `email` before enabling the locked-field invite flow. - claim CTA: the "Continue to registration" link forwarded `canceled: undefined` and dropped the resolved division id, so the registration form had no way to pre-select the invited division. Forward `divisionId` through and extend the register route's search schema to accept it (plus the upcoming `invite=<token>` from Phase 2D). - resolveInviteByToken: filtered only on `claimTokenHash`, even though the docstring claimed an `activeMarker = "active"` filter. Add the predicate so a stale terminal row that somehow kept its hash never resolves — pure defense-in-depth, the live re-read still asserts claimability.
aa49035 to
dd5fa64
Compare
Summary
Stacked on top of #418 (Phase 2A). Adds the athlete-facing claim flow: token resolution, identity-match, the three routes the email link points at, and
?invite=<token>&email=<email>support on sign-in / sign-up.Flow (mike@wodsmith.com seeded pending invite)
/compete/2026-wodsmith-invitational/claim/<token>.getInviteByTokenFn— hashes the token, looks up the row onclaim_token_hash, verifies championship slug matches, checks claimability, resolves the target division label + whether a WODsmith account exists for the invited email.identityMatch(context.session, invite, { accountExistsForInviteEmail })and branches:/compete/$slug/registerflow; sub-arc C wires theinviteTokenparam end-to-end through Stripe).redirectto/sign-in?redirect=<claim-url>&email=<invite.email>&invite=<token>.redirectto/sign-up?redirect=<claim-url>&email=<invite.email>&invite=<token>.redirectparam lands the user back on the claim URL; the loader re-runs against the new session cookie and resolves to the happy path.What changed
src/server/competition-invites/claim.ts—resolveInviteByToken,assertInviteClaimable(distinct reason per terminal state),identityMatch(pure, case-insensitive).src/server/competition-invites/decline.ts— atomicpending → declinedtransition. NullsactiveMarker+claimTokenHashso the link dies immediately and a re-invite is unblocked.src/server-fns/competition-invite-fns.ts:getInviteByTokenFn— public (session-free) server fn used by the claim loader. Returns a discriminated union so the loader branches once.declineInviteFn— authenticated. Re-runs resolve + identity-match on the server so a forged POST can't bypass the email-lock./compete/$slug/claim/$token.tsx— claim landing./compete/$slug/claim/$token/decline.tsx— decline confirmation./compete/$slug/invite-pending.tsx— wrong-account recovery landing./sign-in+/sign-up—?email=&invite=search params; wheninviteis present the email field is pre-filled and locked.lat.md/competition-invites.md— Claim resolution, Claim routes, Auth route extensions sections.Design notes
?invite=<token>and not?claim=<token>? The existingclaimparam is wired tovalidateClaimTokenFn, which reads a plaintext KV entry (claim-token:{token}) pointing at an existing placeholderusersrow. Invite tokens are hashed at DB and the user account may not exist yet. Overloadingclaimwould muddy both flows; a fresh param keeps the two lookup mechanisms cleanly separate.inviteTokentosignUpFnand auto-verify when a valid hash resolves — similar to the KV-backed placeholder path.pendingthrough the Stripe-in-flight window. The actual flip toaccepted_paidlands in sub-arc C when the Stripe workflow readsinviteIdfrom purchase metadata.Test plan
pnpm test -- test/server/competition-invites test/lib/competition-invites— 64 greenpnpm type-checkcleanpnpm devwith seeded DB — click/compete/2026-wodsmith-invitational/claim/seed-invite-mike-pending-men-rx-phase2:mike@wodsmith.com→ claim CTA page/sign-in?email=mike@wodsmith.com&invite=…; email field pre-filled and disableddeclined,activeMarker = NULL,claim_token_hash = NULL; subsequent clicks of the original URL show "invite declined"🤖 Generated with Claude Code
mainSummary by cubic
Adds the athlete-facing invite claim and decline flow with secure token resolution and email-locked identity. Also forwards the invited division to registration and tightens token handling for safer links.
New Features
/compete/$slug/claim/$token,/compete/$slug/claim/$token/decline,/compete/$slug/invite-pending.getInviteByTokenFn(slug-checked token resolve) anddeclineInviteFn(revalidates identity; transitionspending→declinedand deactivates link)./sign-inand/sign-upaccept?inviteand?email; the email field is prefilled and locked only when both are present.divisionId;/compete/$slug/registeraccepts?inviteand?divisionIdfor preselecting the invited division (Phase 2D will wireinvitethrough payment).Bug Fixes
src/server/competition-invites/identity.ts(re-exported fromclaim.ts) so client routes don’t pullgetDb; fixes Vite bundling crash.resolveInviteByTokennow also requiresactiveMarker = "active"(defense-in-depth against stale tokens).Written for commit dd5fa64. Summary will update on new commits.