Free members refer from day one: share codes with late chain binding
Every account gets a share code at signup; /join/<code> attributes first-touch site-side and resolves to the referrer's CURRENT on-chain id at the referral's buy time, so activating any time before your people buy locks the line to you. Joining through a code emails the referrer an activate-payouts nudge. Buy flow re-resolves the sponsor at click time. Copy updated across home and members; assets bumped to v=20260904g. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+32
-8
@@ -16,6 +16,18 @@ function load() {
|
|||||||
try { db = JSON.parse(fs.readFileSync(FILE(), 'utf8')); } catch (e) {}
|
try { db = JSON.parse(fs.readFileSync(FILE(), 'utf8')); } catch (e) {}
|
||||||
if (!db || !db.v) db = { v: 2, byEmail: {}, byAddress: {}, joins: 0 };
|
if (!db || !db.v) db = { v: 2, byEmail: {}, byAddress: {}, joins: 0 };
|
||||||
if (db.v === 1) { db.v = 2; db.byEmail = db.byEmail || {}; } // early rehearsal file
|
if (db.v === 1) { db.v = 2; db.byEmail = db.byEmail || {}; } // early rehearsal file
|
||||||
|
if (!db.byCode) db.byCode = {};
|
||||||
|
// every account carries a share code from day one (backfill older records)
|
||||||
|
for (const a of Object.values(db.byEmail)) {
|
||||||
|
if (!a.code) { a.code = genCode(); db.byCode[a.code] = a.email; }
|
||||||
|
else if (!db.byCode[a.code]) db.byCode[a.code] = a.email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function genCode() {
|
||||||
|
let c;
|
||||||
|
do { c = crypto.randomBytes(5).toString('base64url').replace(/[-_]/g, '').slice(0, 7).toLowerCase(); }
|
||||||
|
while (!c || c.length < 6 || (db.byCode && db.byCode[c]) || /^\d+$/.test(c));
|
||||||
|
return c;
|
||||||
}
|
}
|
||||||
function save() {
|
function save() {
|
||||||
try {
|
try {
|
||||||
@@ -45,21 +57,24 @@ const normEmail = e => String(e || '').trim().toLowerCase();
|
|||||||
const normAddr = a => String(a || '').trim().toLowerCase();
|
const normAddr = a => String(a || '').trim().toLowerCase();
|
||||||
|
|
||||||
// ---- email accounts (the normal join path) ----
|
// ---- email accounts (the normal join path) ----
|
||||||
function signup(email, password, sponsorId) {
|
function signup(email, password, sponsorRef) {
|
||||||
const e = normEmail(email);
|
const e = normEmail(email);
|
||||||
if (!EMAIL_RE.test(e)) return { error: 'That email address does not look right.' };
|
if (!EMAIL_RE.test(e)) return { error: 'That email address does not look right.' };
|
||||||
if (String(password || '').length < 8) return { error: 'Password needs at least 8 characters.' };
|
if (String(password || '').length < 8) return { error: 'Password needs at least 8 characters.' };
|
||||||
if (db.byEmail[e]) return { error: 'That email already has an account. Log in instead.' };
|
if (db.byEmail[e]) return { error: 'That email already has an account. Log in instead.' };
|
||||||
|
const code = genCode();
|
||||||
db.byEmail[e] = {
|
db.byEmail[e] = {
|
||||||
email: e,
|
email: e,
|
||||||
pass: hashPassword(password),
|
pass: hashPassword(password),
|
||||||
sponsorId: Number(sponsorId) || 0, // first touch, written on-chain at first purchase
|
sponsorRef: String(sponsorRef || ''), // first touch; resolved to a chain id at buy time
|
||||||
|
code,
|
||||||
address: null,
|
address: null,
|
||||||
created: Date.now()
|
created: Date.now()
|
||||||
};
|
};
|
||||||
|
db.byCode[code] = e;
|
||||||
db.joins += 1;
|
db.joins += 1;
|
||||||
save();
|
save();
|
||||||
return { ok: true, account: publicView(db.byEmail[e]) };
|
return { ok: true, created: true, account: publicView(db.byEmail[e]) };
|
||||||
}
|
}
|
||||||
function login(email, password) {
|
function login(email, password) {
|
||||||
const e = normEmail(email);
|
const e = normEmail(email);
|
||||||
@@ -70,15 +85,23 @@ function login(email, password) {
|
|||||||
}
|
}
|
||||||
// Passwordless path: a verified email code proves ownership, so the account
|
// Passwordless path: a verified email code proves ownership, so the account
|
||||||
// may exist with no password at all.
|
// may exist with no password at all.
|
||||||
function ensure(email, sponsorId) {
|
function ensure(email, sponsorRef) {
|
||||||
const e = normEmail(email);
|
const e = normEmail(email);
|
||||||
if (!EMAIL_RE.test(e)) return { error: 'That email address does not look right.' };
|
if (!EMAIL_RE.test(e)) return { error: 'That email address does not look right.' };
|
||||||
|
let created = false;
|
||||||
if (!db.byEmail[e]) {
|
if (!db.byEmail[e]) {
|
||||||
db.byEmail[e] = { email: e, pass: null, sponsorId: Number(sponsorId) || 0, address: null, created: Date.now() };
|
const code = genCode();
|
||||||
|
db.byEmail[e] = { email: e, pass: null, sponsorRef: String(sponsorRef || ''), code, address: null, created: Date.now() };
|
||||||
|
db.byCode[code] = e;
|
||||||
db.joins += 1;
|
db.joins += 1;
|
||||||
|
created = true;
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
return { ok: true, account: publicView(db.byEmail[e]) };
|
return { ok: true, created, account: publicView(db.byEmail[e]) };
|
||||||
|
}
|
||||||
|
function byCode(code) {
|
||||||
|
const e = db.byCode[String(code || '').toLowerCase()];
|
||||||
|
return e ? publicView(db.byEmail[e]) : null;
|
||||||
}
|
}
|
||||||
function byEmail(email) { const a = db.byEmail[normEmail(email)]; return a ? publicView(a) : null; }
|
function byEmail(email) { const a = db.byEmail[normEmail(email)]; return a ? publicView(a) : null; }
|
||||||
function byAddress(address) {
|
function byAddress(address) {
|
||||||
@@ -104,8 +127,9 @@ function linkWallet(email, address) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function publicView(a) {
|
function publicView(a) {
|
||||||
return { email: a.email, sponsorId: a.sponsorId || 0, address: a.address || null, created: a.created };
|
return { email: a.email, sponsorRef: a.sponsorRef || String(a.sponsorId || '') || '',
|
||||||
|
code: a.code || null, address: a.address || null, created: a.created };
|
||||||
}
|
}
|
||||||
function count() { return Object.keys(db.byEmail).length; }
|
function count() { return Object.keys(db.byEmail).length; }
|
||||||
|
|
||||||
module.exports = { init, signup, login, ensure, byEmail, byAddress, linkWallet, count };
|
module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, linkWallet, count };
|
||||||
|
|||||||
@@ -5,10 +5,11 @@
|
|||||||
IAP.$('contractLink').href = c.explorer + '/address/' + c.contract;
|
IAP.$('contractLink').href = c.explorer + '/address/' + c.contract;
|
||||||
|
|
||||||
const sp = await (await fetch('/api/sponsor')).json();
|
const sp = await (await fetch('/api/sponsor')).json();
|
||||||
if (sp.sponsorId) {
|
if (sp.invited) {
|
||||||
const el = IAP.$('sponsorLine');
|
const el = IAP.$('sponsorLine');
|
||||||
el.hidden = false;
|
el.hidden = false;
|
||||||
el.textContent = 'You were invited by member #' + sp.sponsorId + '. Your purchases pay their team, and your own link will do the same for you.';
|
el.textContent = (sp.sponsorId ? 'You were invited by member #' + sp.sponsorId + '.' : 'You arrived through a member’s invite.')
|
||||||
|
+ ' Your purchases pay their team, and your own link will do the same for you.';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadLadder() {
|
async function loadLadder() {
|
||||||
@@ -41,7 +42,10 @@
|
|||||||
await IAPWallet.signIn();
|
await IAPWallet.signIn();
|
||||||
}
|
}
|
||||||
IAP.status('Confirm the purchase in your wallet…');
|
IAP.status('Confirm the purchase in your wallet…');
|
||||||
const r = await IAPWallet.buy(Number(btn.dataset.id), sp.sponsorId || 0, btn.dataset.cost);
|
// resolve the sponsor at buy time: a code referrer who activated since
|
||||||
|
// page load still gets locked in
|
||||||
|
const spNow = await (await fetch('/api/sponsor')).json();
|
||||||
|
const r = await IAPWallet.buy(Number(btn.dataset.id), spNow.sponsorId || 0, btn.dataset.cost);
|
||||||
if (r.receipt.status !== '0x1') throw new Error('Transaction reverted. Check the explorer.');
|
if (r.receipt.status !== '0x1') throw new Error('Transaction reverted. Check the explorer.');
|
||||||
IAP.status('Purchase settled on-chain. Credits are yours, payouts delivered. Watch it on the ledger.', 'ok');
|
IAP.status('Purchase settled on-chain. Credits are yours, payouts delivered. Watch it on the ledger.', 'ok');
|
||||||
IAP.refreshNavWallet();
|
IAP.refreshNavWallet();
|
||||||
|
|||||||
+11
-7
@@ -34,21 +34,25 @@
|
|||||||
$('campaignCard').hidden = !me.memberId;
|
$('campaignCard').hidden = !me.memberId;
|
||||||
if (me.memberId) loadCampaigns();
|
if (me.memberId) loadCampaigns();
|
||||||
|
|
||||||
|
// the share link works from day one; codes resolve to your chain id later
|
||||||
|
if (me.refCode || me.memberId) {
|
||||||
|
$('inviteLine').textContent = location.origin + '/join/' + (me.refCode || me.memberId);
|
||||||
|
$('copyInvite').hidden = false;
|
||||||
|
} else {
|
||||||
|
$('inviteLine').textContent = 'Sign in with your email to get your link.';
|
||||||
|
$('copyInvite').hidden = true;
|
||||||
|
}
|
||||||
if (me.memberId) {
|
if (me.memberId) {
|
||||||
const bc = me.buyerCount || 0;
|
const bc = me.buyerCount || 0;
|
||||||
$('qualLine').innerHTML = '<b>' + bc + '</b> qualifying buyer(s) referred<br>'
|
$('qualLine').innerHTML = '<b>' + bc + '</b> qualifying buyer(s) referred<br>'
|
||||||
+ (bc >= 5 ? '<span class="badge">Level 3 unlocked: full three-level earnings</span>'
|
+ (bc >= 5 ? '<span class="badge">Level 3 unlocked: full three-level earnings</span>'
|
||||||
: bc >= 2 ? '<span class="badge">Level 2 unlocked</span> · ' + (5 - bc) + ' more for level 3'
|
: bc >= 2 ? '<span class="badge">Level 2 unlocked</span> · ' + (5 - bc) + ' more for level 3'
|
||||||
: (2 - bc) + ' more buyer(s) of $20+ unlock level 2');
|
: (2 - bc) + ' more buyer(s) of $20+ unlock level 2');
|
||||||
$('inviteLine').textContent = location.origin + '/join/' + me.memberId;
|
|
||||||
$('copyInvite').hidden = false;
|
|
||||||
loadActivity();
|
loadActivity();
|
||||||
} else {
|
} else {
|
||||||
$('qualLine').textContent = 'Level 1 pays the moment you are on-chain. Referrals who buy packages of $20 or more unlock levels 2 and 3.';
|
$('qualLine').innerHTML = 'Share your link now. Then switch on payouts (free, above) '
|
||||||
$('inviteLine').textContent = me.address
|
+ '<b>before your people start buying</b>: the contract locks each buyer to their sponsor at '
|
||||||
? 'Activate payouts above and your invite link appears here.'
|
+ 'their first purchase, and payments only route to wallets that are switched on.';
|
||||||
: 'Link a wallet and switch on payouts to get your invite link.';
|
|
||||||
$('copyInvite').hidden = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<title>The contract | InstantAdPay</title>
|
<title>The contract | InstantAdPay</title>
|
||||||
<meta name="description" content="Plain-language review of the InstantAdPay settlement contract: what it does, what nobody can change, what the operator can and cannot touch, and how to verify all of it yourself.">
|
<meta name="description" content="Plain-language review of the InstantAdPay settlement contract: what it does, what nobody can change, what the operator can and cannot touch, and how to verify all of it yourself.">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260904f">
|
<link rel="stylesheet" href="/assets/site.css?v=20260904g">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
<div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.</div>
|
<div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script src="/assets/common.js?v=20260904f"></script>
|
<script src="/assets/common.js?v=20260904g"></script>
|
||||||
<script src="/assets/contract.js?v=20260904f"></script>
|
<script src="/assets/contract.js?v=20260904g"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+5
-5
@@ -5,7 +5,7 @@
|
|||||||
<title>InstantAdPay: advertise and earn, locked in code</title>
|
<title>InstantAdPay: advertise and earn, locked in code</title>
|
||||||
<meta name="description" content="Real ad packages with instant on-chain settlement. Every purchase pays the sponsor line in the same transaction, verifiable by anyone on the live ledger.">
|
<meta name="description" content="Real ad packages with instant on-chain settlement. Every purchase pays the sponsor line in the same transaction, verifiable by anyone on the live ledger.">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260904f">
|
<link rel="stylesheet" href="/assets/site.css?v=20260904g">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -255,7 +255,7 @@
|
|||||||
<h3>Free membership includes</h3>
|
<h3>Free membership includes</h3>
|
||||||
<ul class="checks">
|
<ul class="checks">
|
||||||
<li>A member account and the live ledger</li>
|
<li>A member account and the live ledger</li>
|
||||||
<li>Your personal referral link, once payouts are switched on</li>
|
<li>Your personal referral link, working from day one</li>
|
||||||
<li>Earnings from day one on your referrals' package purchases</li>
|
<li>Earnings from day one on your referrals' package purchases</li>
|
||||||
<li>Access to the member dashboard</li>
|
<li>Access to the member dashboard</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -345,8 +345,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script src="/assets/common.js?v=20260904f"></script>
|
<script src="/assets/common.js?v=20260904g"></script>
|
||||||
<script src="/assets/wallet.js?v=20260904f"></script>
|
<script src="/assets/wallet.js?v=20260904g"></script>
|
||||||
<script src="/assets/home.js?v=20260904f"></script>
|
<script src="/assets/home.js?v=20260904g"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+3
-3
@@ -5,7 +5,7 @@
|
|||||||
<title>Live ledger | InstantAdPay</title>
|
<title>Live ledger | InstantAdPay</title>
|
||||||
<meta name="description" content="Every purchase, payout, and pass-up on InstantAdPay, streamed straight from the blockchain with a verify link on every line.">
|
<meta name="description" content="Every purchase, payout, and pass-up on InstantAdPay, streamed straight from the blockchain with a verify link on every line.">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260904f">
|
<link rel="stylesheet" href="/assets/site.css?v=20260904g">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<div>InstantAdPay · <a href="/">how it works</a> · <a id="contractLink" href="#" target="_blank" rel="noopener">contract source ↗</a></div>
|
<div>InstantAdPay · <a href="/">how it works</a> · <a id="contractLink" href="#" target="_blank" rel="noopener">contract source ↗</a></div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script src="/assets/common.js?v=20260904f"></script>
|
<script src="/assets/common.js?v=20260904g"></script>
|
||||||
<script src="/assets/ledger.js?v=20260904f"></script>
|
<script src="/assets/ledger.js?v=20260904g"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+8
-7
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>My account | InstantAdPay</title>
|
<title>My account | InstantAdPay</title>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260904f">
|
<link rel="stylesheet" href="/assets/site.css?v=20260904g">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
@@ -71,9 +71,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>Your invite link</h3>
|
<h3>Your invite link, live from day one</h3>
|
||||||
<p class="muted small">Share it anywhere. Everyone who joins through it becomes part of your line.
|
<p class="muted small">Share it anywhere, starting now. Everyone who joins through it becomes part of
|
||||||
You earn 50 percent of every ad package they ever buy, plus levels 2 and 3 of their teams as you qualify.</p>
|
your line, and you earn 50 percent of every ad package they ever buy, plus levels 2 and 3 as you
|
||||||
|
qualify. Just switch on payouts before your people start buying so every payment locks to you.</p>
|
||||||
<p class="mono" id="inviteLine">…</p>
|
<p class="mono" id="inviteLine">…</p>
|
||||||
<button class="btn small sec" id="copyInvite" hidden>Copy link</button>
|
<button class="btn small sec" id="copyInvite" hidden>Copy link</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,8 +131,8 @@
|
|||||||
|
|
||||||
<footer><div>InstantAdPay · <a href="/ledger">live ledger</a></div></footer>
|
<footer><div>InstantAdPay · <a href="/ledger">live ledger</a></div></footer>
|
||||||
</div>
|
</div>
|
||||||
<script src="/assets/common.js?v=20260904f"></script>
|
<script src="/assets/common.js?v=20260904g"></script>
|
||||||
<script src="/assets/wallet.js?v=20260904f"></script>
|
<script src="/assets/wallet.js?v=20260904g"></script>
|
||||||
<script src="/assets/my.js?v=20260904f"></script>
|
<script src="/assets/my.js?v=20260904g"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -85,6 +85,32 @@ function isAdmin(req) {
|
|||||||
const h = req.headers.authorization || '';
|
const h = req.headers.authorization || '';
|
||||||
return h === 'Bearer ' + ADMIN_PASSWORD;
|
return h === 'Bearer ' + ADMIN_PASSWORD;
|
||||||
}
|
}
|
||||||
|
// A sponsor token is a numeric chain id or a site share code. Codes resolve
|
||||||
|
// to the referrer's CURRENT chain id, so activation any time before the
|
||||||
|
// referral's first purchase still locks the line to them.
|
||||||
|
async function resolveSponsorToken(tok) {
|
||||||
|
const t = String(tok || '').trim().toLowerCase();
|
||||||
|
if (!t) return 0;
|
||||||
|
if (/^\d+$/.test(t)) return Number(t);
|
||||||
|
const acct = accounts.byCode(t);
|
||||||
|
if (!acct || !acct.address) return 0;
|
||||||
|
try { return await chain.memberIdByAccount(acct.address); } catch (e) { return 0; }
|
||||||
|
}
|
||||||
|
// The moment someone joins through a code, nudge its owner to activate.
|
||||||
|
function nudgeReferrer(ref) {
|
||||||
|
try {
|
||||||
|
const t = String(ref || '').trim().toLowerCase();
|
||||||
|
if (!t || /^\d+$/.test(t) || !mailer.hasKey()) return;
|
||||||
|
const owner = accounts.byCode(t);
|
||||||
|
if (!owner || !owner.email || owner.address) return; // already activated-ready
|
||||||
|
mailer.send(owner.email, 'Someone just joined through your InstantAdPay link',
|
||||||
|
'Good news: a new member just signed up through your share link.\n\n'
|
||||||
|
+ 'One thing to do so you never miss a payment: sign in and switch on payouts '
|
||||||
|
+ '(one free wallet step). The contract locks each buyer to their sponsor at their '
|
||||||
|
+ 'first purchase, so have payouts on before your people start buying.\n\n'
|
||||||
|
+ 'https://instantadpay.com/my\n\nInstantAdPay').catch(e => console.error('nudge failed', e.message));
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- live feed (SSE) ----
|
// ---- live feed (SSE) ----
|
||||||
const feedClients = new Set();
|
const feedClients = new Set();
|
||||||
@@ -99,14 +125,16 @@ const server = http.createServer(async (req, res) => {
|
|||||||
const u = new URL(req.url, 'http://x');
|
const u = new URL(req.url, 'http://x');
|
||||||
const p = u.pathname;
|
const p = u.pathname;
|
||||||
|
|
||||||
// -- join links: /join/<sponsorId> — first-touch attribution cookie
|
// -- join links: /join/<memberId or share code> — first-touch cookie.
|
||||||
let m = /^\/join\/(\d{1,9})$/.exec(p);
|
// Codes resolve LATE (at buy time) to whatever chain id the referrer
|
||||||
|
// has by then, so free members refer from day one.
|
||||||
|
let m = /^\/join\/([A-Za-z0-9]{1,16})$/.exec(p);
|
||||||
if (m && req.method === 'GET') {
|
if (m && req.method === 'GET') {
|
||||||
const sid = Number(m[1]);
|
const tok = m[1].toLowerCase();
|
||||||
const cookies = parseCookies(req);
|
const cookies = parseCookies(req);
|
||||||
const headers = { Location: '/' };
|
const headers = { Location: '/' };
|
||||||
if (!cookies['iap.sponsor']) {
|
if (!cookies['iap.sponsor']) {
|
||||||
headers['Set-Cookie'] = `iap.sponsor=${sid}; Path=/; SameSite=Lax; Max-Age=${180 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`;
|
headers['Set-Cookie'] = `iap.sponsor=${tok}; Path=/; SameSite=Lax; Max-Age=${180 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`;
|
||||||
}
|
}
|
||||||
res.writeHead(302, baseHeaders(headers));
|
res.writeHead(302, baseHeaders(headers));
|
||||||
return res.end();
|
return res.end();
|
||||||
@@ -133,10 +161,9 @@ const server = http.createServer(async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (p === '/api/sponsor' && req.method === 'GET') {
|
if (p === '/api/sponsor' && req.method === 'GET') {
|
||||||
const sid = Number(parseCookies(req)['iap.sponsor']) || 0;
|
const tok = parseCookies(req)['iap.sponsor'] || '';
|
||||||
let sponsor = null;
|
const sponsorId = await resolveSponsorToken(tok);
|
||||||
if (sid) { try { const mm = await chain.member(sid); if (mm.account !== '0x' + '0'.repeat(40)) sponsor = { id: sid }; } catch (e) {} }
|
return json(res, 200, { ref: tok, sponsorId, invited: !!tok });
|
||||||
return json(res, 200, { sponsorId: sponsor ? sid : 0 });
|
|
||||||
}
|
}
|
||||||
if (p === '/api/stats' && req.method === 'GET') {
|
if (p === '/api/stats' && req.method === 'GET') {
|
||||||
let members = 0; try { members = await chain.memberCount(); } catch (e) {}
|
let members = 0; try { members = await chain.memberCount(); } catch (e) {}
|
||||||
@@ -147,9 +174,10 @@ const server = http.createServer(async (req, res) => {
|
|||||||
// out only at purchase / payout-activation time and gets linked then)
|
// out only at purchase / payout-activation time and gets linked then)
|
||||||
if (p === '/api/signup' && req.method === 'POST') {
|
if (p === '/api/signup' && req.method === 'POST') {
|
||||||
const b = await readBody(req);
|
const b = await readBody(req);
|
||||||
const sid = Number(parseCookies(req)['iap.sponsor']) || 0; // first-touch attribution
|
const ref = parseCookies(req)['iap.sponsor'] || ''; // first-touch attribution
|
||||||
const r = accounts.signup(b.email, b.password, sid);
|
const r = accounts.signup(b.email, b.password, ref);
|
||||||
if (r.error) return json(res, 400, r);
|
if (r.error) return json(res, 400, r);
|
||||||
|
nudgeReferrer(ref);
|
||||||
const token = auth.mintSession({ email: r.account.email });
|
const token = auth.mintSession({ email: r.account.email });
|
||||||
return json(res, 200, { ok: true, account: r.account }, { 'Set-Cookie': auth.sessionCookie(token) });
|
return json(res, 200, { ok: true, account: r.account }, { 'Set-Cookie': auth.sessionCookie(token) });
|
||||||
}
|
}
|
||||||
@@ -191,9 +219,10 @@ const server = http.createServer(async (req, res) => {
|
|||||||
if (rec.tries > 6) { emailCodes.delete(e); return json(res, 400, { error: 'Too many tries. Request a fresh code.' }); }
|
if (rec.tries > 6) { emailCodes.delete(e); return json(res, 400, { error: 'Too many tries. Request a fresh code.' }); }
|
||||||
if (String(b.code || '').trim() !== rec.code) return json(res, 400, { error: 'That code does not match.' });
|
if (String(b.code || '').trim() !== rec.code) return json(res, 400, { error: 'That code does not match.' });
|
||||||
emailCodes.delete(e);
|
emailCodes.delete(e);
|
||||||
const sid = Number(parseCookies(req)['iap.sponsor']) || 0;
|
const ref = parseCookies(req)['iap.sponsor'] || '';
|
||||||
const r = accounts.ensure(e, sid); // first touch wins; existing accounts unchanged
|
const r = accounts.ensure(e, ref); // first touch wins; existing accounts unchanged
|
||||||
if (r.error) return json(res, 400, r);
|
if (r.error) return json(res, 400, r);
|
||||||
|
if (r.created) nudgeReferrer(ref);
|
||||||
let memberId = 0;
|
let memberId = 0;
|
||||||
if (r.account.address) { try { memberId = await chain.memberIdByAccount(r.account.address); } catch (err) {} }
|
if (r.account.address) { try { memberId = await chain.memberIdByAccount(r.account.address); } catch (err) {} }
|
||||||
const token = auth.mintSession({ email: r.account.email, address: r.account.address, memberId });
|
const token = auth.mintSession({ email: r.account.email, address: r.account.address, memberId });
|
||||||
@@ -234,9 +263,10 @@ const server = http.createServer(async (req, res) => {
|
|||||||
if (!s) return json(res, 200, { signedIn: false });
|
if (!s) return json(res, 200, { signedIn: false });
|
||||||
const memberId = await auth.refreshMemberId(s);
|
const memberId = await auth.refreshMemberId(s);
|
||||||
const acct = (s.email && accounts.byEmail(s.email)) || (s.address && accounts.byAddress(s.address)) || null;
|
const acct = (s.email && accounts.byEmail(s.email)) || (s.address && accounts.byAddress(s.address)) || null;
|
||||||
|
const sponsorId = await resolveSponsorToken((acct && acct.sponsorRef) || parseCookies(req)['iap.sponsor']);
|
||||||
const out = { signedIn: true, email: s.email || (acct && acct.email) || null,
|
const out = { signedIn: true, email: s.email || (acct && acct.email) || null,
|
||||||
address: s.address || (acct && acct.address) || null, memberId,
|
address: s.address || (acct && acct.address) || null, memberId,
|
||||||
sponsorId: (acct && acct.sponsorId) || Number(parseCookies(req)['iap.sponsor']) || 0 };
|
refCode: (acct && acct.code) || null, sponsorId };
|
||||||
if (memberId) {
|
if (memberId) {
|
||||||
try {
|
try {
|
||||||
const mm = await chain.member(memberId);
|
const mm = await chain.member(memberId);
|
||||||
|
|||||||
Reference in New Issue
Block a user