Add dormant public direct-join fallback (/join-now) + post-join redirect
/join-now: public self-enroll page gated behind config.dappFallbackPublic (default off -> redirects to /start, invisible until needed). Auto-assigns the current rotation sponsor via /api/public/current-sponsor, registers the user's own wallet, then redirects to /my/<newId> so new members land on their live position instead of guessing their ID. HTML lives in private/, served only via the flagged route. Reuses the proven contract engine. Admin /direct-join now also shows a 'View position ->' link to /my/<newId> after a successful join. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,7 @@
|
||||
<div style="font-size:12px;text-transform:uppercase;letter-spacing:.12em;color:var(--ok);font-weight:800">✅ Success — save this</div>
|
||||
<div id="resultId" style="font-size:46px;font-weight:900;color:var(--gold);margin:6px 0;letter-spacing:-1px">—</div>
|
||||
<div id="resultSub" style="color:var(--muted);font-size:14px"></div>
|
||||
<a id="resultLink" class="btn btn-primary btn-sm" href="/my" style="display:none;margin-top:12px">View position →</a>
|
||||
</div>
|
||||
|
||||
<div class="dj-card">
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<!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"><title>Join the Team | RM Circle</title><link rel="stylesheet" href="/styles.css">
|
||||
<style>
|
||||
.dj-wrap{width:min(720px,calc(100% - 32px));margin:22px auto 70px}
|
||||
.dj-card{background:var(--card,#0b1e30);border:1px solid var(--line);border-radius:16px;padding:20px 22px;margin:0 0 16px}
|
||||
.dj-mono{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:13px;color:var(--gold);word-break:break-all}
|
||||
.dj-kv{display:flex;justify-content:space-between;gap:12px;padding:8px 0;border-bottom:1px solid var(--line);font-size:14px}
|
||||
.dj-kv:last-child{border-bottom:0}.dj-kv b{color:var(--text)}
|
||||
.dj-log{font-size:13px;line-height:1.5;padding:8px 10px;border-left:2px solid var(--line);margin:6px 0;color:var(--muted);background:#08192880}
|
||||
.dj-log.ok{border-color:var(--ok);color:#cfeede}.dj-log.err{border-color:var(--danger);color:#f3b7ac}
|
||||
#logWrap{max-height:220px;overflow:auto}
|
||||
</style></head>
|
||||
<body>
|
||||
<header class="wrap nav"><a class="brand" href="/"><div class="brand-mark">RM</div><span><span>RM Circle</span><small>Join the Team</small></span></a><div class="nav-actions"><a class="btn btn-secondary btn-sm hide-mobile" href="/training">Training</a><a class="btn btn-secondary btn-sm" href="/">How it works</a></div></header>
|
||||
|
||||
<main class="dj-wrap">
|
||||
<div class="section-head" style="margin:6px 0 18px"><div class="eyebrow">Direct enrollment</div><h1 style="font-size:clamp(28px,5vw,40px);margin:8px 0 8px">Join the <span class="gold">RM Circle</span> team.</h1><p style="color:var(--muted);font-size:16px;line-height:1.6;margin:0">Connect your wallet and enter your Premium position on the Polygon smart contract — paid straight from your wallet, verifiable on-chain. You’ll be placed under the current team position automatically. New here? Watch the <a href="/training" style="color:var(--teal)">training</a> first.</p></div>
|
||||
|
||||
<div class="dj-card"><p id="sponsorLine" style="margin:0;color:var(--muted);font-size:15px">Loading the current team placement…</p></div>
|
||||
|
||||
<div class="dj-card">
|
||||
<h2 style="margin:0 0 10px;font-size:18px">1 · Connect your wallet</h2>
|
||||
<p style="color:var(--muted);font-size:14px;margin:0 0 14px">Use MetaMask or Trust Wallet on <strong>Polygon</strong>, funded with enough POL for your Premium entry plus a little gas.</p>
|
||||
<button id="connectBtn" class="btn btn-primary">Connect wallet</button>
|
||||
<div id="connected" style="display:none">
|
||||
<div class="dj-kv"><span>Wallet</span><b id="wallet" class="dj-mono">—</b></div>
|
||||
<div class="dj-kv"><span>Network</span><b>Polygon Mainnet ✓</b></div>
|
||||
<div class="dj-kv"><span>Balance</span><b id="bal">—</b></div>
|
||||
</div>
|
||||
<div id="already" style="display:none;margin-top:12px;color:#f0b4aa;font-size:14px"></div>
|
||||
</div>
|
||||
|
||||
<div id="joinBox" class="dj-card" style="display:none">
|
||||
<h2 style="margin:0 0 10px;font-size:18px">2 · Join</h2>
|
||||
<div class="dj-kv"><span>Tier</span><b>Premium (the tier the team builds at)</b></div>
|
||||
<div class="dj-kv"><span>You will pay</span><b id="joinCost">—</b></div>
|
||||
<button id="joinBtn" class="btn btn-primary" style="margin-top:14px;width:100%">Join the team →</button>
|
||||
<p style="color:var(--muted);font-size:12px;margin:10px 0 0">Confirm the amount in your wallet before signing. If anything is off, the contract simply rejects it and you keep your POL.</p>
|
||||
</div>
|
||||
|
||||
<div id="result" class="dj-card" style="display:none;border-color:var(--ok);background:rgba(123,224,161,.08);text-align:center">
|
||||
<div style="font-size:12px;text-transform:uppercase;letter-spacing:.12em;color:var(--ok);font-weight:800">✅ You’re in — this is your position</div>
|
||||
<div id="resultId" style="font-size:46px;font-weight:900;color:var(--gold);margin:6px 0;letter-spacing:-1px">—</div>
|
||||
<div id="resultSub" style="color:var(--muted);font-size:14px;margin-bottom:12px"></div>
|
||||
<a id="resultLink" class="btn btn-primary" href="/my" style="display:none">View your position →</a>
|
||||
</div>
|
||||
|
||||
<div class="dj-card"><h2 style="margin:0 0 8px;font-size:15px">Activity</h2><div id="logWrap"><div id="log"></div></div></div>
|
||||
|
||||
<p class="micro" style="text-align:center">No income is guaranteed. Cryptocurrency and smart-contract participation carry risk. Only use funds you can afford to lose.</p>
|
||||
</main>
|
||||
<footer class="wrap disclaimer">All figures are read live from the RM Circle smart contract on Polygon. Not an earnings guarantee or investment advice. Participation carries risk of loss.<div class="footer-links"><a href="/">Home</a><a href="/training">Training</a><a href="/how-pay-works">How You Get Paid</a><a href="/contract">Contract Security</a><a href="/disclaimer">Disclaimers</a></div></footer>
|
||||
<script src="/join-now.js"></script></body></html>
|
||||
@@ -78,7 +78,7 @@
|
||||
function receiptEventId(rc,topic0){
|
||||
var id=null; (rc.logs||[]).forEach(function(l){ if(l.address&&l.address.toLowerCase()===CONTRACT&&l.topics&&l.topics[0]===topic0) id=parseInt(l.topics[1],16); }); return id;
|
||||
}
|
||||
function showResult(big,sub){ var el=$('result'); if(!el) return; $('resultId').textContent=big; $('resultSub').textContent=sub; el.style.display='block'; try{el.scrollIntoView({behavior:'smooth',block:'center'});}catch(e){} }
|
||||
function showResult(big,sub,linkId){ var el=$('result'); if(!el) return; $('resultId').textContent=big; $('resultSub').textContent=sub; var lk=$('resultLink'); if(lk){ if(linkId){ lk.href='/my/'+linkId; lk.style.display='inline-flex'; } else lk.style.display='none'; } el.style.display='block'; try{el.scrollIntoView({behavior:'smooth',block:'center'});}catch(e){} }
|
||||
|
||||
async function connect(){
|
||||
if(!eth){ log('No wallet detected. Open this page in a browser with MetaMask (extension or the MetaMask in-app browser).','err'); return; }
|
||||
@@ -123,7 +123,7 @@
|
||||
if(rc.status==='0x0'){ log('⚠️ Reverted on-chain — no position created; your POL was returned (minus gas). Re-check the sponsor ID and amount.','err'); return; }
|
||||
refreshBalance();
|
||||
var newId=receiptEventId(rc,T_REGISTERED);
|
||||
if(newId){ showResult('#'+newId,'Your new position under sponsor #'+sponsor+' is live. Save this ID.'); log('🎉 Confirmed — your new position is <strong>#'+newId+'</strong>.','ok'); }
|
||||
if(newId){ showResult('#'+newId,'Your new position under sponsor #'+sponsor+' is live. Save this ID.',newId); log('🎉 Confirmed — your new position is <strong>#'+newId+'</strong>.','ok'); }
|
||||
else log('Confirmed on-chain, but could not read the ID from the receipt — check the tx on Polygonscan.','ok');
|
||||
}catch(e){ log('Register failed / rejected: '+(e.message||e),'err'); }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/* RM Circle — public direct-join engine (dormant fallback; served only when
|
||||
config.dappFallbackPublic is on). Talks only to window.ethereum (CSP-safe).
|
||||
Auto-assigns the current rotation sponsor, registers the member's own wallet,
|
||||
then sends them to their live position page /my/<newId>. Zero custody. */
|
||||
(function(){
|
||||
'use strict';
|
||||
var CONTRACT='0x33bdaeefd6d17d80ae53816c916dfb26c4fb2daf';
|
||||
var POLYGON_HEX='0x89';
|
||||
var SEL_REGISTER='0x30de37e4'; // register(uint48 sponsorId, uint8 tier) payable
|
||||
var SEL_GETMEMBER='0x2ada2596'; // getMember(address)
|
||||
var SEL_GETCOSTS='0x735f87b9'; // getAllCosts()
|
||||
var T_REGISTERED='0xe4a74887d749eb048f14bfef37b204477f3a5ff67055908b7c8cc62c202aef17';
|
||||
|
||||
var $=function(id){return document.getElementById(id);};
|
||||
var eth=window.ethereum;
|
||||
var account=null, costs=null, sponsorId=null;
|
||||
|
||||
function log(msg,kind){var b=$('log');if(!b)return;var d=document.createElement('div');d.className='dj-log '+(kind||'');d.innerHTML=msg;b.appendChild(d);b.scrollTop=b.scrollHeight;}
|
||||
function word(n){return BigInt(n).toString(16).padStart(64,'0');}
|
||||
function addrWord(a){return a.toLowerCase().replace(/^0x/,'').padStart(64,'0');}
|
||||
function polStr(wei){return (Number(wei)/1e18).toFixed(2);}
|
||||
function hexWei(wei){return '0x'+BigInt(wei).toString(16);}
|
||||
async function req(method,params){ if(!eth) throw new Error('No wallet found'); return await eth.request({method:method,params:params||[]}); }
|
||||
async function ethCall(data){ return await req('eth_call',[{to:CONTRACT,data:data},'latest']); }
|
||||
|
||||
async function loadCosts(){
|
||||
var r=await ethCall(SEL_GETCOSTS); var d=r.slice(2), w=[];
|
||||
for(var i=0;i<d.length;i+=64) w.push(BigInt('0x'+d.slice(i,i+64)));
|
||||
costs={ regPrem:w.slice(8,16) }; return costs;
|
||||
}
|
||||
function regValue(){ return costs.regPrem[0]*105n/100n; } // premium base * 1.05 (verified)
|
||||
|
||||
async function readPosition(addr){
|
||||
try{ var r=await ethCall(SEL_GETMEMBER+addrWord(addr)); if(!r||r==='0x') return null;
|
||||
var d=r.slice(2), level=parseInt(d.slice(5*64,6*64),16); return level?{level:level}:null; }catch(e){ return null; }
|
||||
}
|
||||
async function ensurePolygon(){
|
||||
var cid=await req('eth_chainId'); if(cid===POLYGON_HEX) return true;
|
||||
try{ await req('wallet_switchEthereumChain',[{chainId:POLYGON_HEX}]); return true; }
|
||||
catch(e){ if(e&&e.code===4902){ await req('wallet_addEthereumChain',[{chainId:POLYGON_HEX,chainName:'Polygon Mainnet',nativeCurrency:{name:'POL',symbol:'POL',decimals:18},rpcUrls:['https://polygon-rpc.com'],blockExplorerUrls:['https://polygonscan.com']}]); return true; } throw e; }
|
||||
}
|
||||
async function waitReceipt(tx){ for(var i=0;i<60;i++){ try{ var r=await req('eth_getTransactionReceipt',[tx]); if(r) return r; }catch(e){} await new Promise(function(s){setTimeout(s,3000);}); } return null; }
|
||||
function receiptEventId(rc){ var id=null; (rc.logs||[]).forEach(function(l){ if(l.address&&l.address.toLowerCase()===CONTRACT&&l.topics&&l.topics[0]===T_REGISTERED) id=parseInt(l.topics[1],16); }); return id; }
|
||||
|
||||
async function loadSponsor(){
|
||||
try{
|
||||
var r=await fetch('/api/public/current-sponsor'); var j=await r.json(); var sp=j&&j.sponsor;
|
||||
if(sp&&sp.id){ sponsorId=String(sp.id); $('sponsorLine').innerHTML='You’ll join the team under <strong>#'+sp.id+(sp.name?' ('+sp.name+')':'')+'</strong> — the current team placement.'; }
|
||||
else { $('sponsorLine').textContent='Loading the current team placement…'; }
|
||||
}catch(e){ $('sponsorLine').textContent='Could not load the current sponsor — please refresh.'; }
|
||||
}
|
||||
|
||||
async function refreshBalance(){ try{ var b=await req('eth_getBalance',[account,'latest']); $('bal').textContent=polStr(BigInt(b))+' POL'; }catch(e){} }
|
||||
|
||||
async function connect(){
|
||||
if(!eth){ log('No wallet detected. Open this page in MetaMask or Trust Wallet (extension, or the wallet app’s in-app browser).','err'); return; }
|
||||
try{
|
||||
log('Requesting wallet connection…');
|
||||
var accs=await req('eth_requestAccounts'); account=accs[0];
|
||||
await ensurePolygon(); await loadCosts();
|
||||
$('wallet').textContent=account.slice(0,6)+'…'+account.slice(-4);
|
||||
$('connected').style.display='block'; $('connectBtn').style.display='none';
|
||||
await refreshBalance();
|
||||
$('joinCost').textContent=polStr(regValue())+' POL';
|
||||
var pos=await readPosition(account);
|
||||
if(pos){ $('already').style.display='block'; $('already').innerHTML='This wallet already holds position — <a href="/my">open your dashboard</a>. To create a <em>new</em> position, connect a different wallet.'; $('joinBtn').disabled=true; }
|
||||
$('joinBox').style.display='block';
|
||||
log('Connected. Review the amount, then Join.','ok');
|
||||
}catch(e){ log('Connect failed: '+(e.message||e),'err'); }
|
||||
}
|
||||
|
||||
async function doJoin(){
|
||||
try{
|
||||
if(!sponsorId){ log('Still loading the current sponsor — one moment.','err'); await loadSponsor(); if(!sponsorId) return; }
|
||||
await ensurePolygon();
|
||||
var val=regValue();
|
||||
var data=SEL_REGISTER+word(sponsorId)+word(2);
|
||||
log('Joining under #'+sponsorId+', Premium — paying '+polStr(val)+' POL. Confirm in your wallet…');
|
||||
$('joinBtn').disabled=true;
|
||||
var tx=await req('eth_sendTransaction',[{from:account,to:CONTRACT,value:hexWei(val),data:data}]);
|
||||
log('Submitted — waiting for confirmation… <a href="https://polygonscan.com/tx/'+tx+'" target="_blank" rel="noopener">view tx</a>','ok');
|
||||
var rc=await waitReceipt(tx);
|
||||
if(!rc){ log('Still pending. Once it mines, open your dashboard at /my.',''); $('joinBtn').disabled=false; return; }
|
||||
if(rc.status==='0x0'){ log('⚠️ The transaction reverted — no position created, and your POL was returned (minus gas). Please try again.','err'); $('joinBtn').disabled=false; return; }
|
||||
var newId=receiptEventId(rc);
|
||||
if(newId){
|
||||
$('result').style.display='block';
|
||||
$('resultId').textContent='#'+newId;
|
||||
$('resultSub').innerHTML='Welcome to the team! Taking you to your position page…';
|
||||
$('resultLink').href='/my/'+newId; $('resultLink').style.display='inline-flex';
|
||||
log('🎉 Confirmed — your position is <strong>#'+newId+'</strong>. Redirecting to your dashboard…','ok');
|
||||
try{ el_scroll('result'); }catch(e){}
|
||||
setTimeout(function(){ location.href='/my/'+newId; }, 4500);
|
||||
} else {
|
||||
log('Confirmed on-chain. Open your dashboard at <a href="/my">/my</a> and enter your new ID (also shown on Polygonscan).','ok');
|
||||
$('joinBtn').disabled=false;
|
||||
}
|
||||
}catch(e){ log('Join failed / rejected: '+(e.message||e),'err'); $('joinBtn').disabled=false; }
|
||||
}
|
||||
function el_scroll(id){ var e=$(id); if(e&&e.scrollIntoView) e.scrollIntoView({behavior:'smooth',block:'center'}); }
|
||||
|
||||
document.addEventListener('DOMContentLoaded',function(){
|
||||
loadSponsor();
|
||||
$('connectBtn').addEventListener('click',connect);
|
||||
$('joinBtn').addEventListener('click',doJoin);
|
||||
if(eth&ð.on){
|
||||
eth.on('accountsChanged',function(accs){ account=(accs&&accs[0])||null; if(!account){ $('connected').style.display='none'; $('connectBtn').style.display=''; } else { $('wallet').textContent=account.slice(0,6)+'…'+account.slice(-4); refreshBalance(); } });
|
||||
eth.on('chainChanged',function(){ if(account) refreshBalance(); });
|
||||
}
|
||||
if(!eth) log('No wallet detected. Use MetaMask or Trust Wallet (extension, or the wallet app’s in-app browser).','err');
|
||||
});
|
||||
})();
|
||||
@@ -557,7 +557,7 @@ async function handleApi(req,res,pathname){
|
||||
const maxOrder=sponsors.reduce((m,s)=>Math.max(m,s.sortOrder||0),0);sponsors.push({id:String(id).trim(),name:String(name).trim(),parentId:String(parentId||'').trim(),directs:0,level,status:sponsors.some(s=>s.status==='active')?'waiting':'active',sortOrder:maxOrder+10,clicks:0,notes:String(notes||'').trim(),email:String(email||'').trim().slice(0,120)});sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,201,{sponsors});
|
||||
}
|
||||
if(req.method==='PATCH'&&pathname==='/api/admin/config'){
|
||||
const b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','telegramChatId','telegramTopicId','telegramRecruitTopicId','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail','orgRootId','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId'])if(Object.prototype.hasOwnProperty.call(b,k))next[k]=b[k];next.premiumEntryPol=Number(next.premiumEntryPol)||362;next.updatedAt=new Date().toISOString();writeJson(CONFIG_FILE,next);return json(res,200,{config:next});
|
||||
const b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','telegramChatId','telegramTopicId','telegramRecruitTopicId','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail','orgRootId','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId','dappFallbackPublic'])if(Object.prototype.hasOwnProperty.call(b,k))next[k]=b[k];next.premiumEntryPol=Number(next.premiumEntryPol)||362;next.updatedAt=new Date().toISOString();writeJson(CONFIG_FILE,next);return json(res,200,{config:next});
|
||||
}
|
||||
const m=pathname.match(/^\/api\/admin\/sponsors\/([^/]+)(?:\/(increment|activate|qualify|reset|move))?$/);
|
||||
if(m){const id=decodeURIComponent(m[1]),action=m[2]||null;let sponsors=getSponsors(),idx=sponsors.findIndex(s=>s.id===id);if(idx<0)return json(res,404,{error:'Sponsor not found.'});
|
||||
@@ -619,7 +619,7 @@ const server=http.createServer(async(req,res)=>{
|
||||
if((mj=pathname.match(/^\/join\/(\d{1,15})$/)))return serveMemberPage(req,res,path.join(PUBLIC_DIR,'join.html'),'join',mj[1]);
|
||||
}
|
||||
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==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/start'});return res.end();}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 if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(pathname==='/join-now'||pathname==='/join-now/'){if(!getConfig().dappFallbackPublic){res.writeHead(302,{Location:'/start'});return res.end();}file=path.join(ROOT,'private','join-now.html');}else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/start'});return res.end();}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