Skip to content

feat: host claims + Luma verification + admin Claims tab (host analytics phase 1) - #177

Merged
snackman merged 1 commit into
masterfrom
feat/host-claims
Oct 8, 2026
Merged

snackman merged 1 commit into
masterfrom
feat/host-claims

Conversation

@snackman

@snackman snackman commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Phase 1 of plans/host-analytics.md (plan copied into the repo in this PR): hosts can claim an event, verify it automatically through Luma, or ask for a manual review. Admins work the queue in a new Claims tab.

What's in it

  • Migration (not applied yet): supabase/migrations/20261002120000_event_claims.sql
    • event_claims: the plan §4 columns, plus host_api_id, derived_from_claim_id and a new method luma_host.
    • verified_luma_hosts(user_id, host_api_id, host_name, claim_id, verified_at, revoked_at), with a partial unique index on active rows.
    • RLS: owners can only SELECT their own rows. All writes go through the service role.
    • Also adds the Phase 2 indexes: event_tracking(event_id, created_at) and a GIN index on itineraries.event_ids (it's text[]).
    • Additive only, so it's safe to apply before deploying.
  • Verification
    • Each claim gets a planwtf-XXXXXXXX code (40 random bits, Crockford base32).
    • We look for it in the event's description_mirror (text and link hrefs; a code split across marks still matches; it must be a whole token and case doesn't matter), then in each listed host's bio_short.
  • Decision 1 (host-wide coverage) and its security limit
    • Only a host_bio match proves the user controls a Luma host account. That match stores host_api_id on the claim and its evidence, and adds a verified_luma_hosts row.
    • A description match verifies that one event only. It never grants host-wide coverage.
    • Later claims on Luma events whose live hosts[] includes a host the user has verified are verified immediately: method='luma_host', and the evidence records original_claim_id.
    • When an admin revokes the original claim, the host-wide grant and every derived claim are revoked too. If the owner withdraws the original claim, the grant goes and the derived claims are withdrawn.
  • Decision 5: the manual-review copy promises a review within 48 hours by our admin team.
  • Server side
    • src/lib/server-auth.ts (getUserFromRequest: Bearer access token → auth.getUser).
    • Zod schemas in api-validation.ts.
    • luma.ts holds only pure functions, so client components can import it. luma-server.ts does the fetching through safeFetch and generates codes. This split keeps node modules out of the client bundle.
    • We only fetch the link stored in the sheet, with the slug or api id validated, an 8s timeout and a 1MB cap.
  • UI
    • ClaimEventModal, opened from a small "Host? Claim this event" link under the card in the table EventDetailModal and in the map EventPopup.
    • After verification the modal shows "Verified — dashboard coming soon".
    • I left the list view card alone to keep it dense.

New API routes and suggested per-IP Vercel firewall limits

Route Auth Suggested limit
GET /api/host/claims?eventId= user 30/min
POST /api/host/claims user 10/min
DELETE /api/host/claims/[id] owner 10/min
POST /api/host/claims/[id]/verify owner 6/min
POST /api/host/claims/[id]/manual owner 3/min
GET /api/admin/claims admin password 20/min
POST /api/admin/claims/[id] admin password 20/min

The database adds its own limits: a 10s verify cooldown (optimistic-concurrency update), at most 30 attempts per claim and at most 20 pending claims per user.

Verification done

  • npm run lint: 0 errors (the 3 warnings were already there).
  • npx tsc --noEmit: clean.
  • npm test: 359 passing, including the new luma-verify, normalize-link and host-claims-schemas tests. The fixture src/lib/__tests__/fixtures/luma-event.json is a recorded espresso-hh payload, trimmed: no guest_data, payment, tracking or personal fields.
  • npx next build: OK.
  • next dev with curl: every host route returns 401 without a token or with a bogus one, and 400 for a bad event id, claim id, JSON or note. Admin routes return 401 for a wrong password and 400 for a bad action or relink without an event id. With the real admin password the queue answers "table not yet created", so it degrades cleanly before the migration is applied.
  • A live run of fetchLumaEvent through safeFetch worked: slug → api id → refetch by id, a missing event returns not_found, and an invalid slug is refused.

Manual QA checklist (needs a signed-in user after the migration is applied)

  • Apply the migration in Supabase.
  • Signed out: "Host? Claim this event" → modal → "Sign in to claim" → OTP. The modal then shows the claim UI.
  • Luma event: "Get verification code" → code appears, copy works, "Open Luma event" opens the link.
  • Verify before adding the code → "couldn't find" message and a 10s countdown.
  • Add the code to the event description → Verify → "Verified — dashboard coming soon". Admin tab shows evidence "description" and no host-wide badge.
  • Add the code to the host bio on another claim → verified, the modal mentions host-wide, admin shows the "host-wide" badge.
  • Claim a different Luma event where that host is listed → verified instantly (Verified Luma host, "from claim …").
  • Admin Revoke the bio-verified claim → the derived claims are revoked too (an alert shows the count) and new claims on that host's events need a code again.
  • "Can't edit the Luma page? Request manual review" → note → "Under review… within 48 hours". The admin queue shows the claimant's email, name and X handle, the Luma hosts, and the "handle matches host" hint.
  • Admin Approve and Reject (with a reason) for a manual claim.
  • Non-Luma event: goes straight to the manual note form.
  • Withdraw a pending claim and a verified one.
  • Map popup: the modal and the auth modal sit above the map (they're portalled) and close cleanly.
  • Admin Relink to another event id in the same conference.
  • Add the Vercel firewall rules above.

Not in this PR (Phase 2): /host dashboard, GET /api/host/events, stats RPC, the admin "grant" action and "Preview stats".

🤖 Generated with Claude Code

…ics phase 1)

- Migration 20261002120000_event_claims.sql: event_claims + verified_luma_hosts
  (RLS: owner select only; writes via service role), Phase 2 support indexes.
- Luma verification: code in event description (this event only) or in a
  listed host's bio (proves control of that Luma host account -> host-wide
  coverage; later claims on events listing that host auto-verify as
  'luma_host'). All Luma fetches via safeFetch, slug/api-id validated.
- Routes: /api/host/claims (GET/POST), /api/host/claims/[id] (DELETE),
  /api/host/claims/[id]/verify, /api/host/claims/[id]/manual,
  /api/admin/claims (GET), /api/admin/claims/[id] (approve/reject/revoke/relink).
  Admin revoke cascades to the host-wide grant and derived claims.
- server-auth.ts (Bearer access token -> supabase.auth.getUser), zod schemas.
- ClaimEventModal + "Host? Claim this event" link in table detail modal and
  map popup; admin ClaimsTab.
- Tests: luma-verify (recorded trimmed Luma fixture), normalize-link, schemas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
sheeets Ready Ready Preview Oct 1, 2026 12:10pm UTC

Request Review

@snackman
snackman merged commit 8114fcf into master Oct 8, 2026
3 checks passed
@snackman
snackman deleted the feat/host-claims branch October 8, 2026 03:57

This branch was successfully deployed

1 active deployment
Preview — d1a001aa Deployed Oct 1, 2026 by vercel[bot]
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.

1 participant