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:
@@ -50,6 +50,13 @@ RULES:
|
||||
}
|
||||
const SUBMISSIONS_FILE = path.join(DATA_DIR, 'submissions.json');
|
||||
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();
|
||||
function submitRateLimited(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'){
|
||||
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'){
|
||||
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(req.method!=='GET'&&req.method!=='HEAD')return send(res,405,'Method Not Allowed',{'Content-Type':'text/plain; charset=utf-8'});
|
||||
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='';
|
||||
}
|
||||
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