Disclaimer page, self-service member alerts, and admin login lockout

- /disclaimer: independent-resource, affiliate, earnings, risk, and
  not-advice disclosures; linked from all footers.
- Self-service alerts: members opt in with their email on /my/:id to get
  "you've been paid" + "upgrade needed" emails for their own position
  (same watcher as the owner alerts, extended). Signed unsubscribe link
  (/unsubscribe?id=&t=HMAC), on-chain-registration check, rate-limited,
  masked-email status, confirmation email.
- Admin login: timing-safe compare + per-IP lockout (8 fails -> 15 min,
  escalating). Previously unlimited/brute-forceable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-14 14:45:57 -05:00
parent 8b3e82d960
commit 0f5630e6d0
9 changed files with 159 additions and 28 deletions
+29
View File
@@ -100,6 +100,7 @@
?`<div class="table-wrap"><table class="table" style="min-width:520px"><thead><tr><th>When</th><th>From member</th><th>For</th><th>Amount</th></tr></thead><tbody>${inc.map(p=>`<tr><td>${date(p.ts)}</td><td>#${p.fromId}</td><td>${esc(p.desc)}</td><td><strong>${fmt(p.pol)} POL</strong></td></tr>`).join('')}</tbody></table></div>`
:'<div class="empty">No payments yet — they appear here the moment they land on-chain.</div>';
renderPipeline(d);
renderAlerts(d);
renderShare(d);
document.getElementById('dLineage').innerHTML=d.uplineChain&&d.uplineChain.length
?`<strong style="color:var(--text)">#${d.id}</strong> → ${d.uplineChain.map(i=>'#'+i).join(' → ')} <span style="color:#8498aa">(root)</span>`
@@ -204,6 +205,34 @@
L.push(`See it live and get a red alert the moment you'd miss one: rmcircle.saasy.top/my/${d.id}`);
return L.join('\n');
}
async function renderAlerts(d){
const el=document.getElementById('dAlerts');
if(!el)return;
let st={subscribed:false};
try{st=await (await fetch('/api/public/alert-status?id='+d.id)).json();}catch(e){}
function subscribedView(email){
el.innerHTML=`<p style="margin:0 0 10px"><span class="live-badge"><span class="dot"></span> Alerts ON</span> for <strong>${esc(email||'your email')}</strong></p><button id="alertOff" class="btn btn-secondary btn-sm">Turn off alerts</button><span id="alertMsg" class="micro" style="margin-left:8px"></span>`;
document.getElementById('alertOff').addEventListener('click',async()=>{
try{await fetch('/api/public/alert-signup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:d.id,email:''})});renderAlerts(d);}catch(e){document.getElementById('alertMsg').textContent='Try again.';}
});
}
function offView(){
el.innerHTML=`<form id="alertForm" style="display:flex;gap:8px;flex-wrap:wrap;max-width:460px"><input name="email" class="input" type="email" placeholder="you@example.com" required style="flex:1;min-width:200px"><button class="btn btn-primary btn-sm">Turn on alerts</button></form><div id="alertMsg" class="micro" style="margin-top:8px"></div>`;
document.getElementById('alertForm').addEventListener('submit',async e=>{
e.preventDefault();
const email=e.currentTarget.elements.email.value.trim(),msg=document.getElementById('alertMsg');
msg.style.color='var(--muted)';msg.textContent='Turning on…';
try{
const r=await fetch('/api/public/alert-signup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:d.id,email})});
const j=await r.json();
if(!r.ok)throw new Error(j.error||'Failed');
msg.style.color='var(--ok)';msg.textContent='Done — check your inbox for a confirmation.';
setTimeout(()=>renderAlerts(d),1200);
}catch(x){msg.style.color='var(--danger)';msg.textContent=x.message;}
});
}
if(st.subscribed)subscribedView(st.email);else offView();
}
function renderShare(d){
const el=document.getElementById('dShare');
if(!el)return;