Repository navigation
Conversation
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
left a comment
There was a problem hiding this comment.
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 switchesprimary_org/rsi_handlefromcoalesce(p_x, x)tocase 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:
handleSavepassesrsiHandle || undefined, so saving an empty handle leaves the DB row unchanged but the client setssavedRsiHandle = ''and flips status to'unverified'. UI and DB diverge until reload. handleCopyhas no fallback:navigator.clipboard.writeTextrejects on insecure contexts and older browsers, and the rejection is unhandled. Atry/catchwould 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.
## 🧾 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
🎯 Purpose
🔗 Related Issues / Epics
🧪 Testing
🧰 Developer Notes