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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user