PolHunter return loop: ?from=polhunter sets iap.return (landing + join links), /api/me carries returnTo, Overview card sends them back once a wallet is linked, wallet link answers next=/api/my/polhunter, hand-off clears the cookie
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+16
-1
@@ -1404,7 +1404,21 @@
|
|||||||
}));
|
}));
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
// came from PolHunter (iap.return cookie): one card at the top of the Overview that sends them back
|
||||||
|
async function loadReturnCard() {
|
||||||
|
const card = $('phReturn'); if (!card) return;
|
||||||
|
try {
|
||||||
|
const me = await (await fetch('/api/me')).json();
|
||||||
|
if (me.returnTo !== 'polhunter') { card.hidden = true; return; }
|
||||||
|
card.hidden = false;
|
||||||
|
card.innerHTML = '<div class="card-head"><h3>You came from PolHunter</h3></div>' + (me.address
|
||||||
|
? '<p>Your wallet is linked, so your board is ready.</p><a class="btn" href="/api/my/polhunter">Back to PolHunter</a>'
|
||||||
|
: '<p>One step left: link the wallet your POL drips will be paid to. The moment it is linked you go straight back.</p><button class="btn" type="button" id="phGoWallet">Link my wallet</button>');
|
||||||
|
const b = $('phGoWallet'); if (b) b.addEventListener('click', () => { const m = document.querySelector('.bo-menu [data-pane="wallet"]'); if (m) m.click(); });
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
async function loadCoach() {
|
async function loadCoach() {
|
||||||
|
loadReturnCard();
|
||||||
loadTank();
|
loadTank();
|
||||||
try {
|
try {
|
||||||
const r = await (await fetch('/api/my/coach')).json();
|
const r = await (await fetch('/api/my/coach')).json();
|
||||||
@@ -2383,7 +2397,8 @@
|
|||||||
}));
|
}));
|
||||||
$('linkBtn').addEventListener('click', busy($('linkBtn'), async () => {
|
$('linkBtn').addEventListener('click', busy($('linkBtn'), async () => {
|
||||||
IAP.status('Check your wallet for the free link signature…');
|
IAP.status('Check your wallet for the free link signature…');
|
||||||
await IAPWallet.signIn(); // server binds the wallet to the signed-in email account
|
const lr = await IAPWallet.signIn(); // server binds the wallet to the signed-in email account
|
||||||
|
if (lr && lr.next) { IAP.status('Wallet linked. Taking you back to PolHunter…', 'ok'); setTimeout(() => { location.href = lr.next; }, 700); return; } // they came from PolHunter: close the loop
|
||||||
IAP.status('Wallet linked. Earnings pay there from now on.', 'ok');
|
IAP.status('Wallet linked. Earnings pay there from now on.', 'ok');
|
||||||
await render();
|
await render();
|
||||||
}));
|
}));
|
||||||
|
|||||||
+2
-1
@@ -172,6 +172,7 @@
|
|||||||
<main class="bo-content">
|
<main class="bo-content">
|
||||||
|
|
||||||
<div class="pane" id="pane-overview">
|
<div class="pane" id="pane-overview">
|
||||||
|
<div class="card" id="phReturn" hidden></div>
|
||||||
<div class="card gs" id="gsCard" hidden>
|
<div class="card gs" id="gsCard" hidden>
|
||||||
<div class="card-head"><h3>Getting started</h3><span class="sub" id="gsSub"></span></div>
|
<div class="card-head"><h3>Getting started</h3><span class="sub" id="gsSub"></span></div>
|
||||||
<div class="gs-steps" id="gsSteps"></div>
|
<div class="gs-steps" id="gsSteps"></div>
|
||||||
@@ -1037,7 +1038,7 @@
|
|||||||
<script src="/assets/common.js?v=20260916a"></script>
|
<script src="/assets/common.js?v=20260916a"></script>
|
||||||
<script src="/assets/wallet.js?v=20260911a"></script>
|
<script src="/assets/wallet.js?v=20260911a"></script>
|
||||||
<script src="/assets/promo.js?v=20260911a"></script>
|
<script src="/assets/promo.js?v=20260911a"></script>
|
||||||
<script src="/assets/my.js?v=20260919b"></script>
|
<script src="/assets/my.js?v=20260920b"></script>
|
||||||
<script src="/assets/chat.js?v=20260907l"></script>
|
<script src="/assets/chat.js?v=20260907l"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1066,6 +1066,7 @@ const server = http.createServer(async (req, res) => {
|
|||||||
const cookieTail = `; Path=/; SameSite=Lax; Max-Age=${30 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`; // 30 days: whoever brings them back gets the credit
|
const cookieTail = `; Path=/; SameSite=Lax; Max-Age=${30 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`; // 30 days: whoever brings them back gets the credit
|
||||||
const set = [];
|
const set = [];
|
||||||
set.push('iap.sponsor=' + tok + cookieTail); // last touch wins
|
set.push('iap.sponsor=' + tok + cookieTail); // last touch wins
|
||||||
|
if (u.searchParams.get('from') === 'polhunter') set.push('iap.return=polhunter' + cookieTail); // came through a PolHunter share link: send them back after the wallet step
|
||||||
const promo = promos.norm(u.searchParams.get('promo')); if (promo) set.push('iap.promo=' + promo + cookieTail); // partner code, redeemed at signup
|
const promo = promos.norm(u.searchParams.get('promo')); if (promo) set.push('iap.promo=' + promo + cookieTail); // partner code, redeemed at signup
|
||||||
if (promo) { const pref = String(req.headers.referer || '').replace(/^https?:\/\//, '').split('/')[0].toLowerCase().slice(0, 80); if (pref) set.push('iap.promoref=' + encodeURIComponent(pref) + cookieTail); } // which partner page it came from
|
if (promo) { const pref = String(req.headers.referer || '').replace(/^https?:\/\//, '').split('/')[0].toLowerCase().slice(0, 80); if (pref) set.push('iap.promoref=' + encodeURIComponent(pref) + cookieTail); } // which partner page it came from
|
||||||
if (ang) set.push('iap.angle=' + angle + cookieTail);
|
if (ang) set.push('iap.angle=' + angle + cookieTail);
|
||||||
@@ -1379,7 +1380,7 @@ const server = http.createServer(async (req, res) => {
|
|||||||
const lr = await accounts.linkWallet(s.email, r.address);
|
const lr = await accounts.linkWallet(s.email, r.address);
|
||||||
if (lr.error) return json(res, 400, lr);
|
if (lr.error) return json(res, 400, lr);
|
||||||
await auth.updateSession(s.token, { address: r.address, memberId });
|
await auth.updateSession(s.token, { address: r.address, memberId });
|
||||||
return json(res, 200, { ok: true, linked: true, address: r.address, memberId });
|
return json(res, 200, { ok: true, linked: true, address: r.address, memberId, next: parseCookies(req)['iap.return'] === 'polhunter' ? '/api/my/polhunter' : null });
|
||||||
}
|
}
|
||||||
const acct = await accounts.byAddress(r.address);
|
const acct = await accounts.byAddress(r.address);
|
||||||
if (!acct && await accounts.positionOwner(r.address))
|
if (!acct && await accounts.positionOwner(r.address))
|
||||||
@@ -1414,7 +1415,7 @@ const server = http.createServer(async (req, res) => {
|
|||||||
const _defSpon = Number(siteConfig().defaultSponsorId) || 1;
|
const _defSpon = Number(siteConfig().defaultSponsorId) || 1;
|
||||||
if (!sponsorId && memberId !== _defSpon) sponsorId = _defSpon; // orphan fallback → #1 (never self-sponsor)
|
if (!sponsorId && memberId !== _defSpon) sponsorId = _defSpon; // orphan fallback → #1 (never self-sponsor)
|
||||||
if (memberId && acct && acct.memberId !== memberId) accounts.setMemberId(acct.email, memberId).catch(() => {});
|
if (memberId && acct && acct.memberId !== memberId) accounts.setMemberId(acct.email, memberId).catch(() => {});
|
||||||
const out = { signedIn: true, sponsorBlocked, sponsorRouted, sponsorName: spdMe.name || null, email: s.email || (acct && acct.email) || null,
|
const out = { signedIn: true, returnTo: parseCookies(req)['iap.return'] === 'polhunter' ? 'polhunter' : null, sponsorBlocked, sponsorRouted, sponsorName: spdMe.name || null, email: s.email || (acct && acct.email) || null,
|
||||||
address: s.address || (acct && acct.address) || null, memberId,
|
address: s.address || (acct && acct.address) || null, memberId,
|
||||||
username: (acct && acct.username) || null,
|
username: (acct && acct.username) || null,
|
||||||
refCode: (acct && acct.code) || null, sponsorId,
|
refCode: (acct && acct.code) || null, sponsorId,
|
||||||
@@ -2112,7 +2113,7 @@ const server = http.createServer(async (req, res) => {
|
|||||||
const b64u = b => Buffer.from(b).toString('base64').replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
|
const b64u = b => Buffer.from(b).toString('base64').replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
|
||||||
const payload = JSON.stringify({ iat: Date.now(), exp: Date.now() + 5 * 60000, nonce: crypto.randomBytes(12).toString('hex'), memberId: Number(memberId), email: s.email, wallet: (acct && acct.address) || s.address || null, username: (acct && acct.username) || null });
|
const payload = JSON.stringify({ iat: Date.now(), exp: Date.now() + 5 * 60000, nonce: crypto.randomBytes(12).toString('hex'), memberId: Number(memberId), email: s.email, wallet: (acct && acct.address) || s.address || null, username: (acct && acct.username) || null });
|
||||||
const tok = b64u(payload) + '.' + b64u(crypto.createHmac('sha256', secret).update(payload).digest());
|
const tok = b64u(payload) + '.' + b64u(crypto.createHmac('sha256', secret).update(payload).digest());
|
||||||
res.writeHead(302, { Location: huntUrl + '/auth?t=' + tok, 'Cache-Control': 'no-store' }); return res.end();
|
res.writeHead(302, { Location: huntUrl + '/auth?t=' + tok, 'Cache-Control': 'no-store', 'Set-Cookie': 'iap.return=; Path=/; SameSite=Lax; Max-Age=0' + (IS_PROD ? '; Secure' : '') }); return res.end(); // the loop is closed
|
||||||
}
|
}
|
||||||
if (p === '/api/my/claim' && req.method === 'POST') {
|
if (p === '/api/my/claim' && req.method === 'POST') {
|
||||||
const s = await auth.fromRequest(req);
|
const s = await auth.fromRequest(req);
|
||||||
@@ -3068,7 +3069,11 @@ const server = http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
// -- pages (HEAD answered like GET so link previewers and crawlers see 200; Node drops the body)
|
// -- pages (HEAD answered like GET so link previewers and crawlers see 200; Node drops the body)
|
||||||
if (req.method === 'GET' || req.method === 'HEAD') {
|
if (req.method === 'GET' || req.method === 'HEAD') {
|
||||||
if (p === '/') return sendFile(res, path.join(PUBLIC_DIR, 'index.html'));
|
if (p === '/') {
|
||||||
|
// arrived from PolHunter: remember it for 30 days so the dashboard can send them back once a wallet is linked
|
||||||
|
if (u.searchParams.get('from') === 'polhunter') { res.writeHead(302, { Location: '/', 'Set-Cookie': 'iap.return=polhunter; Path=/; SameSite=Lax; Max-Age=' + (30 * 24 * 3600) + (IS_PROD ? '; Secure' : ''), 'Cache-Control': 'no-store' }); return res.end(); }
|
||||||
|
return sendFile(res, path.join(PUBLIC_DIR, 'index.html'));
|
||||||
|
}
|
||||||
if (p === '/ledger') return sendFile(res, path.join(PUBLIC_DIR, 'ledger.html'));
|
if (p === '/ledger') return sendFile(res, path.join(PUBLIC_DIR, 'ledger.html'));
|
||||||
if (p === '/contract') return sendFile(res, path.join(PUBLIC_DIR, 'contract.html'));
|
if (p === '/contract') return sendFile(res, path.join(PUBLIC_DIR, 'contract.html'));
|
||||||
if (p === '/terms') return sendFile(res, path.join(PUBLIC_DIR, 'terms.html'));
|
if (p === '/terms') return sendFile(res, path.join(PUBLIC_DIR, 'terms.html'));
|
||||||
|
|||||||
Reference in New Issue
Block a user