Add public member dashboard at /my/:id
Members enter their on-chain ID (or follow a /my/46-style link) and see their own position: tier/level/directs/earnings facts, their subtree pyramid with downline rollups and open slots, classified payment history, and lineage to root — all chain-derived data only, none of the admin operational config. Public endpoint /api/public/member is cached (120s) and rate-limited (20/min/IP). Linked from the public footers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -400,4 +400,53 @@ function getMatrixTree() {
|
|||||||
return { ready: true, snapshotAt: state.snapshotAt, memberCount: Object.keys(state.members).length, root, unplaced: unplaced.length ? unplaced : undefined };
|
return { ready: true, snapshotAt: state.snapshotAt, memberCount: Object.keys(state.members).length, root, unplaced: unplaced.length ? unplaced : undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, getMatrixTree, isInTeam, CONTRACT };
|
// subtree rooted at `id` from the snapshot: nodes to `showDepth`, rollups from the FULL subtree
|
||||||
|
function getSubtree(id, showDepth = 3) {
|
||||||
|
if (!state || !state.members[id]) return null;
|
||||||
|
const seen = new Set();
|
||||||
|
function node(nid, depth) {
|
||||||
|
if (!nid || seen.has(nid)) return null;
|
||||||
|
seen.add(nid);
|
||||||
|
const m = state.members[nid];
|
||||||
|
if (!m) return null;
|
||||||
|
const l = node(m.l, depth + 1), r = node(m.r, depth + 1);
|
||||||
|
const n = {
|
||||||
|
id: nid, tier: m.tier, level: m.level, levelName: levelName(m.level || 1),
|
||||||
|
directCount: m.directCount || 0, earnedPol: m.earnedPol || 0,
|
||||||
|
downCount: 0, downPol: 0
|
||||||
|
};
|
||||||
|
for (const c of [l, r]) if (c) { n.downCount += 1 + c.downCount; n.downPol = +(n.downPol + c.earnedPol + c.downPol).toFixed(2); }
|
||||||
|
if (depth < showDepth) { n.left = l; n.right = r; }
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
return node(id, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// everything a member may see about their own position — chain data only
|
||||||
|
async function memberPublic(id) {
|
||||||
|
const m = await fetchMember(id);
|
||||||
|
if (!m) return { registered: false, id };
|
||||||
|
await fetchCosts().catch(() => {});
|
||||||
|
const out = {
|
||||||
|
registered: true, id, account: m.account, joinedAt: m.joinedAt,
|
||||||
|
tier: m.tier, tierName: tierName(m.tier), level: m.level, levelName: levelName(m.level),
|
||||||
|
directCount: m.directCount, totalEarnedPol: m.totalEarnedPol, totalPaidPol: m.totalPaidPol,
|
||||||
|
referrerId: m.referrerId, uplineId: m.uplineId
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const inc = await fetchIncome(id);
|
||||||
|
out.income = inc.map(p => ({ fromId: p.fromId, pol: p.pol, ts: p.ts, desc: describeIncome(p.fromTier, p.level, p.pol) })).reverse().slice(0, 50);
|
||||||
|
} catch (e) { out.income = []; }
|
||||||
|
out.subtree = getSubtree(id, 3);
|
||||||
|
const chain = [];
|
||||||
|
const seen = new Set([id]);
|
||||||
|
let cur = m.uplineId;
|
||||||
|
for (let i = 0; i < 40 && cur && !seen.has(cur); i++) {
|
||||||
|
seen.add(cur); chain.push(cur);
|
||||||
|
cur = state && state.members[cur] ? state.members[cur].uplineId : 0;
|
||||||
|
}
|
||||||
|
out.uplineChain = chain;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getMatrixTree, isInTeam, CONTRACT };
|
||||||
|
|||||||
+1
-1
@@ -13,5 +13,5 @@
|
|||||||
<section class="section"><div class="wrap"><div class="section-head"><div class="eyebrow">Depth over width</div><h2>2 → 4 → 8 → 16 → 32</h2><p>The first major team milestone is 30 correctly placed positions across the first four generations: 2 + 4 + 8 + 16.</p></div><div class="matrix" aria-label="Matrix growth illustration"><div class="matrix-group"><div class="people"><span class="person"></span><span class="person"></span></div><b>2</b></div><div class="matrix-group"><div class="people"><span class="person"></span><span class="person"></span><span class="person"></span><span class="person"></span></div><b>4</b></div><div class="matrix-group"><div class="people" id="p8"></div><b>8</b></div><div class="matrix-group"><div class="people" id="p16"></div><b>16</b></div></div><div class="notice"><strong>Team principle:</strong> once your two are in place, do not keep adding more directs to the same qualified link. Help the next positions become qualified so the matrix develops depth instead of extra shallow legs.</div></div></section>
|
<section class="section"><div class="wrap"><div class="section-head"><div class="eyebrow">Depth over width</div><h2>2 → 4 → 8 → 16 → 32</h2><p>The first major team milestone is 30 correctly placed positions across the first four generations: 2 + 4 + 8 + 16.</p></div><div class="matrix" aria-label="Matrix growth illustration"><div class="matrix-group"><div class="people"><span class="person"></span><span class="person"></span></div><b>2</b></div><div class="matrix-group"><div class="people"><span class="person"></span><span class="person"></span><span class="person"></span><span class="person"></span></div><b>4</b></div><div class="matrix-group"><div class="people" id="p8"></div><b>8</b></div><div class="matrix-group"><div class="people" id="p16"></div><b>16</b></div></div><div class="notice"><strong>Team principle:</strong> once your two are in place, do not keep adding more directs to the same qualified link. Help the next positions become qualified so the matrix develops depth instead of extra shallow legs.</div></div></section>
|
||||||
<section class="section" id="proof"><div class="wrap"><div class="section-head"><div class="eyebrow">Live payment proof</div><h2>Real payouts, straight from the blockchain.</h2><p>Every payment in this program happens on a public smart contract on Polygon — nobody can fake, hide, or edit it. Below are the latest member payouts, read live from the contract. Tap any row to verify the transaction yourself on Polygonscan.</p></div><div id="payoutTotals" class="pp-totals"></div><div id="payoutFeed" class="pp-feed"><div class="empty">Reading the blockchain…</div></div><div class="pp-note">Data is read directly from the RM Circle smart contract (<a href="https://polygonscan.com/address/0x33BdAEEfd6d17D80aE53816c916dFb26c4fB2DAF" target="_blank" rel="noopener noreferrer" style="color:var(--teal)">0x33Bd…2DAF</a>) on Polygon Mainnet. Member numbers are on-chain IDs, not names. Past payouts are not a promise of future results.</div></div></section>
|
<section class="section" id="proof"><div class="wrap"><div class="section-head"><div class="eyebrow">Live payment proof</div><h2>Real payouts, straight from the blockchain.</h2><p>Every payment in this program happens on a public smart contract on Polygon — nobody can fake, hide, or edit it. Below are the latest member payouts, read live from the contract. Tap any row to verify the transaction yourself on Polygonscan.</p></div><div id="payoutTotals" class="pp-totals"></div><div id="payoutFeed" class="pp-feed"><div class="empty">Reading the blockchain…</div></div><div class="pp-note">Data is read directly from the RM Circle smart contract (<a href="https://polygonscan.com/address/0x33BdAEEfd6d17D80aE53816c916dFb26c4fB2DAF" target="_blank" rel="noopener noreferrer" style="color:var(--teal)">0x33Bd…2DAF</a>) on Polygon Mainnet. Member numbers are on-chain IDs, not names. Past payouts are not a promise of future results.</div></div></section>
|
||||||
<section class="section"><div class="wrap"><div class="card" style="text-align:center;padding:34px"><div class="eyebrow">Ready to start?</div><h2 style="font-size:38px;margin:10px 0">See the current team placement.</h2><p style="max-width:680px;margin:0 auto 20px;color:var(--muted)">The onboarding page automatically shows the sponsor position the team is currently helping. Always use the sponsor shown there instead of an old screenshot or saved link.</p><a class="btn btn-primary" href="/start">Open Getting Started Instructions →</a></div></div></section>
|
<section class="section"><div class="wrap"><div class="card" style="text-align:center;padding:34px"><div class="eyebrow">Ready to start?</div><h2 style="font-size:38px;margin:10px 0">See the current team placement.</h2><p style="max-width:680px;margin:0 auto 20px;color:var(--muted)">The onboarding page automatically shows the sponsor position the team is currently helping. Always use the sponsor shown there instead of an old screenshot or saved link.</p><a class="btn btn-primary" href="/start">Open Getting Started Instructions →</a></div></div></section>
|
||||||
</main><footer class="wrap disclaimer">This independent team page is educational and is not an earnings guarantee or investment advice. Cryptocurrency and smart-contract participation involve risk, including possible loss of funds. Never use funds you cannot afford to lose. Results depend on actual participation, qualification, upgrades, smart-contract behavior, and the market value of POL.<div class="footer-links"><a href="/training">Training</a><a href="/start">Getting Started</a><a href="/admin">Team Admin</a></div></footer>
|
</main><footer class="wrap disclaimer">This independent team page is educational and is not an earnings guarantee or investment advice. Cryptocurrency and smart-contract participation involve risk, including possible loss of funds. Never use funds you cannot afford to lose. Results depend on actual participation, qualification, upgrades, smart-contract behavior, and the market value of POL.<div class="footer-links"><a href="/training">Training</a><a href="/start">Getting Started</a><a href="/my">Member Dashboard</a><a href="/admin">Team Admin</a></div></footer>
|
||||||
<script src="/track.js"></script><script src="/bridge.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script></body></html>
|
<script src="/track.js"></script><script src="/bridge.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script></body></html>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><meta name="description" content="Your RM Circle position, payments, and team — live from the blockchain."><title>My Team Dashboard | Crypto Team Build</title><link rel="stylesheet" href="/styles.css"></head>
|
||||||
|
<body>
|
||||||
|
<header class="wrap nav"><div class="brand"><div class="brand-mark">RM</div><span><span id="brandName">Crypto Team Build</span><small>Member Dashboard</small></span></div><div class="nav-actions"><a class="btn btn-secondary hide-mobile" href="/training">Training</a><a class="btn btn-primary" href="/start">Current Sponsor</a></div></header>
|
||||||
|
<main class="wrap" style="padding:26px 0 60px">
|
||||||
|
<section id="idPrompt" class="login-panel" style="margin:8vh auto"><h1>Your position, live from the blockchain.</h1><p>Enter your RM Circle member ID to see your payments, your team, and your lineage — everything verified on Polygon.</p><form id="idForm"><div class="field"><label for="memberId">Your member ID</label><input id="memberId" class="input" inputmode="numeric" placeholder="e.g. 46" required></div><button class="btn btn-primary" style="width:100%">Show My Dashboard</button><div id="idError" class="micro" style="color:var(--danger)"></div></form></section>
|
||||||
|
<section id="dash" class="hidden">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:6px"><div><div class="eyebrow">Member position</div><h1 style="margin:6px 0 0">Position <span class="gold" id="dTitle">#—</span> <span id="dVerified" class="live-badge" style="vertical-align:middle;margin-left:8px"><span class="dot"></span> Verified on-chain</span></h1></div><button id="switchId" class="btn btn-secondary btn-sm">Look up another ID</button></div>
|
||||||
|
<div id="dFacts" class="facts" style="grid-template-columns:repeat(3,1fr);margin:18px 0"></div>
|
||||||
|
<div class="table-card" style="margin-bottom:18px"><h2 style="margin:0 0 4px">Your team</h2><p style="color:var(--muted);font-size:13px;margin:0 0 14px">Your position's matrix — the rollup line on each card counts everyone underneath, all the way down. Open slots are where the next placements land.</p><div id="dTree"></div></div>
|
||||||
|
<div class="table-card" style="margin-bottom:18px"><h2 style="margin:0 0 4px">Payments received</h2><p style="color:var(--muted);font-size:13px;margin:0 0 10px">Every payment your position has received, straight from the smart contract.</p><div id="dIncome"></div></div>
|
||||||
|
<div class="table-card"><h2 style="margin:0 0 4px">Your lineage</h2><p id="dLineage" style="color:var(--muted);font-size:14px;line-height:1.8;margin:8px 0 0"></p></div>
|
||||||
|
<div class="notice" style="margin-top:18px"><strong>Team reminder:</strong> once your two directs are placed, retire your link and help your two get their two — always send new members through the <a href="/start" style="color:var(--gold)">current team sponsor page</a>.</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer class="wrap disclaimer">All figures are read live from the RM Circle smart contract on Polygon and are historical facts, not a promise of future results. Participation involves cryptocurrency and smart-contract risk. Never use funds you cannot afford to lose.</footer>
|
||||||
|
<script src="/track.js"></script><script src="/my.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script></body></html>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
(function(){
|
||||||
|
const esc=s=>String(s??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]));
|
||||||
|
const fmt=n=>Number(n||0).toLocaleString(undefined,{maximumFractionDigits:2});
|
||||||
|
const date=ts=>ts?new Date(ts*1000).toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'}):'—';
|
||||||
|
const prompt=document.getElementById('idPrompt'),dash=document.getElementById('dash');
|
||||||
|
|
||||||
|
function pathId(){const m=location.pathname.match(/^\/my\/(\d+)$/);if(m)return m[1];const q=new URLSearchParams(location.search).get('id');if(q&&/^\d+$/.test(q))return q;try{return localStorage.getItem('ctb.myId')||''}catch(e){return ''}}
|
||||||
|
|
||||||
|
function card(n,focus){
|
||||||
|
if(!n)return '<div class="mtp-card mtp-open">open<br>slot</div>';
|
||||||
|
const badge=n.tier===2?'<span class="mt-badge mt-prem">P</span>':'<span class="mt-badge">S</span>';
|
||||||
|
const down=n.downCount?`<span class="mtp-down">⬇ ${n.downCount} below · ${fmt(n.downPol)} POL</span>`:'<span class="mtp-down mtp-down-none">no downline yet</span>';
|
||||||
|
return `<div class="mtp-card${focus?' mtp-focus':''}" style="cursor:default">${badge} <span class="mt-id">#${n.id}</span><span class="mtp-lvl">${esc(n.levelName)}</span><span class="mtp-sub">${n.directCount}/2 · ${fmt(n.earnedPol)} POL</span>${down}</div>`;
|
||||||
|
}
|
||||||
|
function renderTree(root){
|
||||||
|
if(!root){document.getElementById('dTree').innerHTML='<div class="empty">Team view is still indexing — check back in a few minutes.</div>';return}
|
||||||
|
const rows=[[root]];
|
||||||
|
for(let g=1;g<4;g++)rows.push(rows[g-1].flatMap(n=>n?[n.left||null,n.right||null]:[null,null]));
|
||||||
|
if(!rows[3].some(Boolean))rows.pop();
|
||||||
|
document.getElementById('dTree').innerHTML=`<div class="mtp">${rows.map((r,i)=>`<div class="mtp-row">${r.map(n=>card(n,i===0&&n&&true)).join('')}</div>`).join('')}</div>`;
|
||||||
|
}
|
||||||
|
function render(d){
|
||||||
|
prompt.classList.add('hidden');dash.classList.remove('hidden');
|
||||||
|
document.getElementById('dTitle').textContent='#'+d.id;
|
||||||
|
document.getElementById('dFacts').innerHTML=
|
||||||
|
`<div class="fact"><small>Tier</small><strong>${esc(d.tierName)}</strong></div>`+
|
||||||
|
`<div class="fact"><small>Level</small><strong>${esc(d.levelName)}</strong></div>`+
|
||||||
|
`<div class="fact"><small>Directs</small><strong>${d.directCount}/2${d.directCount>=2?' ✓ qualified':''}</strong></div>`+
|
||||||
|
`<div class="fact"><small>Total received</small><strong style="color:var(--ok)">${fmt(d.totalEarnedPol)} POL</strong></div>`+
|
||||||
|
`<div class="fact"><small>Team below you</small><strong>${d.subtree?`${d.subtree.downCount} member${d.subtree.downCount===1?'':'s'}`:'—'}</strong></div>`+
|
||||||
|
`<div class="fact"><small>Member since</small><strong>${date(d.joinedAt)}</strong></div>`;
|
||||||
|
renderTree(d.subtree);
|
||||||
|
const inc=d.income||[];
|
||||||
|
document.getElementById('dIncome').innerHTML=inc.length
|
||||||
|
?`<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>';
|
||||||
|
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>`
|
||||||
|
:'You are at the top of your line.';
|
||||||
|
}
|
||||||
|
async function load(id){
|
||||||
|
try{
|
||||||
|
const r=await fetch('/api/public/member?id='+id);
|
||||||
|
const d=await r.json();
|
||||||
|
if(!r.ok)throw new Error(d.error||'Lookup failed');
|
||||||
|
if(!d.registered)throw new Error(`ID ${id} isn't registered on the smart contract — double-check the number.`);
|
||||||
|
try{localStorage.setItem('ctb.myId',String(id))}catch(e){}
|
||||||
|
if(!/^\/my\/\d+$/.test(location.pathname))history.replaceState(null,'','/my/'+id);
|
||||||
|
render(d);
|
||||||
|
}catch(x){
|
||||||
|
prompt.classList.remove('hidden');dash.classList.add('hidden');
|
||||||
|
document.getElementById('idError').textContent=x.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('idForm').addEventListener('submit',e=>{
|
||||||
|
e.preventDefault();
|
||||||
|
const v=document.getElementById('memberId').value.trim();
|
||||||
|
if(!/^\d{1,15}$/.test(v)){document.getElementById('idError').textContent='Numbers only — the ID from the RM Circle dApp.';return}
|
||||||
|
load(v);
|
||||||
|
});
|
||||||
|
document.getElementById('switchId').addEventListener('click',()=>{
|
||||||
|
try{localStorage.removeItem('ctb.myId')}catch(e){}
|
||||||
|
history.replaceState(null,'','/my');
|
||||||
|
dash.classList.add('hidden');prompt.classList.remove('hidden');
|
||||||
|
document.getElementById('memberId').value='';
|
||||||
|
});
|
||||||
|
const id=pathId();
|
||||||
|
if(id)load(id);
|
||||||
|
})();
|
||||||
+1
-1
@@ -15,5 +15,5 @@
|
|||||||
<div class="callout" style="margin-top:16px"><strong>What happens next:</strong> get exactly 2 directs, retire your qualified referral link, help your 2 get their 2, and upgrade with earned POL when practical. The goal is depth and duplication—not endless directs on one link.</div>
|
<div class="callout" style="margin-top:16px"><strong>What happens next:</strong> get exactly 2 directs, retire your qualified referral link, help your 2 get their 2, and upgrade with earned POL when practical. The goal is depth and duplication—not endless directs on one link.</div>
|
||||||
<div class="callout warning" style="margin-top:12px"><strong>Risk reminder:</strong> participation involves cryptocurrency and smart-contract risk. No income is guaranteed. Use only funds you can afford to lose.</div><div id="supportBox" class="notice" style="margin-top:12px"></div><div id="supportLinkWrap" class="hidden" style="margin-top:10px"><a id="supportLink" class="btn btn-secondary" target="_blank" rel="noopener noreferrer">Open Team Support ↗</a></div></section></div>
|
<div class="callout warning" style="margin-top:12px"><strong>Risk reminder:</strong> participation involves cryptocurrency and smart-contract risk. No income is guaranteed. Use only funds you can afford to lose.</div><div id="supportBox" class="notice" style="margin-top:12px"></div><div id="supportLinkWrap" class="hidden" style="margin-top:10px"><a id="supportLink" class="btn btn-secondary" target="_blank" rel="noopener noreferrer">Open Team Support ↗</a></div></section></div>
|
||||||
<figure class="roadmap-figure"><a href="/roadmap.webp" target="_blank" rel="noopener"><img src="/roadmap.webp" alt="RM Circle Premium Team Build Roadmap — core strategy, step-by-step guide, premium levels, and duplication formula" width="1149" height="1369" loading="lazy"></a><figcaption>The RM Circle is a team build project of the <strong>Crypto Team Build Network</strong>. This roadmap is the plan every member follows — tap to view full size.</figcaption></figure></div></main>
|
<figure class="roadmap-figure"><a href="/roadmap.webp" target="_blank" rel="noopener"><img src="/roadmap.webp" alt="RM Circle Premium Team Build Roadmap — core strategy, step-by-step guide, premium levels, and duplication formula" width="1149" height="1369" loading="lazy"></a><figcaption>The RM Circle is a team build project of the <strong>Crypto Team Build Network</strong>. This roadmap is the plan every member follows — tap to view full size.</figcaption></figure></div></main>
|
||||||
<footer class="wrap disclaimer">This is an independent Crypto Team Build onboarding resource, not an owner/principal page. Always confirm transaction details in your wallet before signing. Never disclose your Secret Recovery Phrase.</footer>
|
<footer class="wrap disclaimer">This is an independent Crypto Team Build onboarding resource, not an owner/principal page. Always confirm transaction details in your wallet before signing. Never disclose your Secret Recovery Phrase.<div class="footer-links"><a href="/my">Already joined? Open your Member Dashboard →</a></div></footer>
|
||||||
<script src="/track.js"></script><script src="/start.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script></body></html>
|
<script src="/track.js"></script><script src="/start.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script></body></html>
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ RULES:
|
|||||||
}
|
}
|
||||||
const SUBMISSIONS_FILE = path.join(DATA_DIR, 'submissions.json');
|
const SUBMISSIONS_FILE = path.join(DATA_DIR, 'submissions.json');
|
||||||
if (!fs.existsSync(SUBMISSIONS_FILE)) fs.writeFileSync(SUBMISSIONS_FILE, '[]');
|
if (!fs.existsSync(SUBMISSIONS_FILE)) fs.writeFileSync(SUBMISSIONS_FILE, '[]');
|
||||||
|
const memberCache = new Map();
|
||||||
|
const lookupHits = new Map();
|
||||||
|
function memberLookupLimited(ip) {
|
||||||
|
const now = Date.now(), rec = lookupHits.get(ip);
|
||||||
|
if (!rec || now > rec.reset) { lookupHits.set(ip, { count: 1, reset: now + 60000 }); return false; }
|
||||||
|
rec.count++; return rec.count > 20;
|
||||||
|
}
|
||||||
const submitHits = new Map();
|
const submitHits = new Map();
|
||||||
function submitRateLimited(ip) {
|
function submitRateLimited(ip) {
|
||||||
const now = Date.now(), rec = submitHits.get(ip);
|
const now = Date.now(), rec = submitHits.get(ip);
|
||||||
@@ -250,6 +257,20 @@ async function handleApi(req,res,pathname){
|
|||||||
if(req.method==='GET'&&pathname==='/api/public/config'){
|
if(req.method==='GET'&&pathname==='/api/public/config'){
|
||||||
const c=getConfig();return json(res,200,{siteName:c.siteName,programName:c.programName,bridgeHeadline:c.bridgeHeadline,bridgeSubheadline:c.bridgeSubheadline,premiumEntryPol:c.premiumEntryPol,telegramUrl:c.telegramUrl,supportLabel:c.supportLabel,showQueueProgress:c.showQueueProgress});
|
const c=getConfig();return json(res,200,{siteName:c.siteName,programName:c.programName,bridgeHeadline:c.bridgeHeadline,bridgeSubheadline:c.bridgeSubheadline,premiumEntryPol:c.premiumEntryPol,telegramUrl:c.telegramUrl,supportLabel:c.supportLabel,showQueueProgress:c.showQueueProgress});
|
||||||
}
|
}
|
||||||
|
if(req.method==='GET'&&pathname==='/api/public/member'){
|
||||||
|
const ip=String(req.headers['x-forwarded-for']||req.socket.remoteAddress||'').split(',')[0].trim();
|
||||||
|
if(memberLookupLimited(ip))return json(res,429,{error:'Too many lookups — give it a minute.'});
|
||||||
|
const id=Number(new URL(req.url,'http://x').searchParams.get('id')||0);
|
||||||
|
if(!Number.isInteger(id)||id<1||id>281474976710655)return json(res,400,{error:'Enter a numeric member ID.'});
|
||||||
|
const cached=memberCache.get(id);
|
||||||
|
if(cached&&Date.now()-cached.ts<120000)return json(res,200,cached.data,{'Cache-Control':'public, max-age=60'});
|
||||||
|
try{
|
||||||
|
const r=await Promise.race([chain.memberPublic(id),new Promise((_,rej)=>setTimeout(()=>rej(new Error('Blockchain lookup timed out — try again.')),20000))]);
|
||||||
|
memberCache.set(id,{data:r,ts:Date.now()});
|
||||||
|
if(memberCache.size>500)memberCache.delete(memberCache.keys().next().value);
|
||||||
|
return json(res,200,r,{'Cache-Control':'public, max-age=60'});
|
||||||
|
}catch(e){return json(res,502,{error:e.message||'Lookup failed'})}
|
||||||
|
}
|
||||||
if(req.method==='GET'&&pathname==='/api/public/payouts'){
|
if(req.method==='GET'&&pathname==='/api/public/payouts'){
|
||||||
return json(res,200,chain.getPayoutsPublic(),{'Cache-Control':'public, max-age=20'});
|
return json(res,200,chain.getPayoutsPublic(),{'Cache-Control':'public, max-age=20'});
|
||||||
}
|
}
|
||||||
@@ -330,7 +351,7 @@ const server=http.createServer(async(req,res)=>{
|
|||||||
if(pathname==='/health'||pathname.startsWith('/api/'))return await handleApi(req,res,pathname);
|
if(pathname==='/health'||pathname.startsWith('/api/'))return await handleApi(req,res,pathname);
|
||||||
if(req.method!=='GET'&&req.method!=='HEAD')return send(res,405,'Method Not Allowed',{'Content-Type':'text/plain; charset=utf-8'});
|
if(req.method!=='GET'&&req.method!=='HEAD')return send(res,405,'Method Not Allowed',{'Content-Type':'text/plain; charset=utf-8'});
|
||||||
let file;
|
let file;
|
||||||
if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else{
|
if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else{
|
||||||
const safe=path.normalize(pathname).replace(/^([.][.][/\\])+/, '').replace(/^[/\\]+/,'');file=path.join(PUBLIC_DIR,safe);if(!file.startsWith(PUBLIC_DIR))file='';
|
const safe=path.normalize(pathname).replace(/^([.][.][/\\])+/, '').replace(/^[/\\]+/,'');file=path.join(PUBLIC_DIR,safe);if(!file.startsWith(PUBLIC_DIR))file='';
|
||||||
}
|
}
|
||||||
if(file&&staticFile(req,res,file))return;return staticFile(req,res,path.join(PUBLIC_DIR,'404.html'),404);
|
if(file&&staticFile(req,res,file))return;return staticFile(req,res,path.join(PUBLIC_DIR,'404.html'),404);
|
||||||
|
|||||||
Reference in New Issue
Block a user