Required member profile: username + verified email per position, gated at proof of ownership

Marty, 2026-09-16: leaders can write but 94% of positions cannot receive (47 of 771 have ever signed
in to messaging, 482 messages sit 85% unread). profiles.js stores username + verified email per
POSITION (one wallet holds one position, so a Triple Play holder has three; the person is the email
and one email may hold several positions). Seeds the 40 emails already on file from
member-alerts.json, pre-filled but unverified so confirming costs one tap.

Writes are only ever accepted from a session that PROVED ownership: wallet personal_sign
(messages.verifyChallenge) or the Telegram Mini App bridge. The public /my/<id> page is untouched and
cannot write a profile, verified by test: all four endpoints 401 unauthenticated while /my/21 stays
200. Endpoints GET /api/public/profile, POST .../username, .../email-start, .../email-verify, plus
GET /api/admin/profiles for coverage. Email codes: 6 digits, 15 min, 60s cooldown, 5/day, 6 tries.

profile-gate.js is a two-step modal that cannot be dismissed, fired on dashboard boot (covers the
Mini App landing) and right after a wallet sign-in. Chatbot canned answer + AI prompt updated.
28 unit tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-16 16:56:35 -05:00
parent 8521f7a3f2
commit 7d07fc01f1
6 changed files with 397 additions and 4 deletions
+4
View File
@@ -419,6 +419,7 @@
const sig=await eth.request({method:'personal_sign',params:[hex,account]});
const v=await(await fetch('/api/public/msg-verify',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({address:account,signature:sig})})).json();
if(!v.ok)throw new Error(v.error||'Verification failed.');
try{ if(window.RMCProfile)await window.RMCProfile.require(); }catch(ge){}
loadMsgUI(d);
}catch(e){if(err)err.textContent=e.message||String(e);}
}
@@ -756,4 +757,7 @@
const id=pathId();
if(id&&!pathHasId)openDashTab=true; // bare /my resolved from storage = returning to your own page
if(id)load(id);
// Required member profile (username + verified email). No-ops for a visitor who
// has not proved they own a position: the API 401s and the gate never shows.
try{ if(window.RMCProfile)window.RMCProfile.require(); }catch(e){}
})();