Skip to content

feat(rsi): RSI handle verification challenge flow (#12) - #181

Open
blairg23 wants to merge 1 commit into
mainfrom
feat/rsi-verification-flow
Open

blairg23 wants to merge 1 commit into
mainfrom
feat/rsi-verification-flow

Conversation

@blairg23

@blairg23 blairg23 commented Jun 15, 2026 •

Copy link
Copy Markdown
Member

## 🧾 Title
feat(rsi): RSI handle verification challenge flow (#12)

🧠 Description

Implements the RSI handle ownership verification flow. Users generate a short challenge token, place it in their RSI bio, and submit for review. The verified badge then appears on their public profile (#14) and operation hosting screens.

🧩 Changes Included

  • Implemented new feature or enhancement

🎯 Purpose

  • Closes RSI handle verification flow #12 Establishes the full client-side and database layer for the verification challenge. Automated scraping of the RSI profile page (to close the loop without admin action) is deferred to a future Supabase Edge Function.

🔗 Related Issues / Epics

🧪 Testing

  1. Log in, open Account Settings, save an RSI handle
  2. The 'RSI handle verification' section appears below the form
  3. Click 'Generate verification code' -- verify a MUSTER-XXXXXXXX token appears
  4. Click 'Copy' -- verify the token copies to clipboard
  5. (Optional) Add the token to your RSI bio on robertsspaceindustries.com
  6. Click 'I've added it to my bio' -- verify status changes to pending confirmation copy
  7. Click 'Regenerate code' -- verify a new token replaces the old one and status resets
  8. Change the RSI handle and save -- verify the verification section resets to initial state
  9. If a profile is already verified (status = 'verified'), verify only the 'RSI Verified' confirmation copy shows

🧰 Developer Notes

  • Automated verification (fetching RSI profile page to check for token) is CORS-blocked from the browser; requires a Supabase Edge Function calling robertsspaceindustries.com server-side
  • Token format: 'MUSTER-' + 8 uppercase hex chars from a UUID -- short enough to paste, unique enough not to collide
  • Admin can manually set rsi_verification_status = 'verified' in the Supabase dashboard as a stopgap

UI flow in AccountSettings:
- 'Generate verification code' generates a MUSTER-XXXXXXXX token via RPC and
  displays it with copy button and step-by-step instructions
- 'I've added it to my bio' button marks status as submitted for admin review
- 'Regenerate code' re-triggers the challenge (handles token expiry or re-use)
- Verified/pending/submitted states each show contextually appropriate copy
- Saving a different handle resets verification status and token

supabase.ts: generateRsiVerificationToken(), submitRsiVerification()

Migration:
- rsi_verification_token column on profiles
- generate_rsi_verification_token(): creates token, sets status to 'token_generated'
- submit_rsi_verification(): sets status to 'submitted'
- update_my_profile(): resets token and status when handle changes
- get_or_create_profile(): extended to return rsi_verification_token

Actual RSI bio scraping to close the verification loop requires a Supabase
Edge Function (future issue) since robertsspaceindustries.com is CORS-blocked
from the browser.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@cmFrameShift cmFrameShift left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @blairg23, the structure here is clean and the state machine in AccountSettings.tsx reads well. There are two issues that will stop the flow from working against the current schema, plus one security gap that undermines its purpose. Details below.

Blockers

1. New status values violate the existing CHECK constraint

profiles.rsi_verification_status has a constraint from 20260520190000_shared_foundation_accounts.sql that was never altered:

check (rsi_verification_status in ('not_submitted', 'pending', 'verified', 'failed', 'revoked'))

This PR writes values outside that set:

  • generate_rsi_verification_token() sets 'token_generated'
  • submit_rsi_verification() sets 'submitted'
  • update_my_profile() reset sets 'unverified'

Each of these UPDATEs will fail with a check-constraint violation (Postgres 23514), so the flow breaks on the first button click. The migration needs to drop and recreate the constraint with the new vocabulary, or reuse the existing terms. My preference is to reuse where they map cleanly ('pending' for 'submitted', 'not_submitted' for 'unverified') and only add 'token_generated'.

2. Client status vocabulary does not match the DB

VerificationStatus is typed as 'unverified' | 'token_generated' | 'submitted' | 'verified', but a real profile column defaults to 'not_submitted'. On load:

setVerificationStatus((profile.rsi_verification_status as VerificationStatus) ?? 'unverified');

'not_submitted' is truthy, so it is kept and matches none of the four render branches. A genuine user (status 'not_submitted') sees the panel header with no body and no Generate button. The ?? 'unverified' fallback only fires on null, which never happens given the not null default 'not_submitted' column. The client and DB enums should be unified along with fix #1.

Security

3. Users can self-verify by writing the column directly

users_update_own_profile in 20260603000000_profile_rls_and_helpers.sql grants row-level UPDATE on the user's own profile, and RLS restricts rows, not columns. With no column-level grants on profiles, any authenticated user can bypass the whole challenge:

supabase.from('profiles').update({ rsi_verification_status: 'verified' }).eq('auth_user_id', myId)

That makes the token ceremony decorative and lets the verified badge (#14) be self-granted. Since this PR is what gives the status column security meaning, it should close this here: either a BEFORE UPDATE trigger that blocks non-privileged changes to rsi_verification_status / rsi_verification_token, or REVOKE UPDATE (rsi_verification_status, rsi_verification_token) ON public.profiles FROM authenticated so those values only move through the SECURITY DEFINER RPCs.

(No read leak on the token: there is no public SELECT policy on profiles, so that part is fine.)

4. submit_rsi_verification() can downgrade a verified profile

It sets 'submitted' with no guard on current status, so calling it on an already-verified row demotes it. The client guards this, but the RPC is directly callable. Add and rsi_verification_status <> 'verified' to the WHERE clause.

Minor

  • Bundled behavior change in update_my_profile: the rewrite switches primary_org / rsi_handle from coalesce(p_x, x) to case when p_x is not null then nullif(trim(p_x), '') else x end, which now lets an empty string clear the handle. Probably desirable, but it is an unannounced semantics change to a shared RPC. Worth a note.
  • Empty-handle save desync: handleSave passes rsiHandle || undefined, so saving an empty handle leaves the DB row unchanged but the client sets savedRsiHandle = '' and flips status to 'unverified'. UI and DB diverge until reload.
  • handleCopy has no fallback: navigator.clipboard.writeText rejects on insecure contexts and older browsers, and the rejection is unhandled. A try/catch would avoid a silent no-op.
  • Token entropy: 8 hex chars is about 32 bits. Fine for a bio-ownership challenge, and the PR notes already call this out. No action needed.

On testing

The manual test plan looks thorough, but as written it could not have passed given #1, since the first Generate click would hit the constraint. Worth confirming whether this was tested against a DB that actually has the 20260520190000 constraint applied, since the discrepancy suggests the schema under test differed from main.

Overall: good design and clean code. Fixing the status vocabulary (DB, client, and constraint together) and locking down direct writes to the verification columns would get this over the line. The rest is polish.

This branch has not been deployed

No deployments
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.

RSI handle verification flow

2 participants