MySQL data layer: accounts, sessions, campaigns, burns (Marty: real concurrency)

Coolify MySQL (instantadpay-db) via DATABASE_URL; db.js bootstraps schema
and one-time imports the volume JSON. accounts/auth/ads are dual-mode: the
MySQL path uses guarded UPDATEs for the concurrent ad-serving hot path;
without DATABASE_URL the JSON stores remain (local dev). All data functions
async; server boots through db.init. Chain index stays a file: it is a
rebuildable cache of the blockchain, which remains the money truth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-04 14:13:37 -05:00
parent f3d2f7db88
commit 6680e154d0
7 changed files with 616 additions and 333 deletions
+45 -39
View File
@@ -23,13 +23,8 @@ const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'changeme';
const IS_PROD = process.env.NODE_ENV === 'production';
const SITE_FILE = path.join(DATA_DIR, 'site.json');
const db = require('./db');
fs.mkdirSync(DATA_DIR, { recursive: true });
chain.init({ onEvent: ev => pushFeed(ev) });
auth.init({ dataDir: DATA_DIR, chain, isProd: IS_PROD, site: 'instantadpay.com' });
accounts.init({ dataDir: DATA_DIR });
ads.init({ dataDir: DATA_DIR, chain });
mailer.init({ dataDir: DATA_DIR });
chatbot.init({ dataDir: DATA_DIR, chain });
const chatHits = new Map();
function chatLimited(ip) {
const now = Date.now(), rec = chatHits.get(ip);
@@ -39,8 +34,17 @@ function chatLimited(ip) {
}
// magic-code sign-in: emailLower -> {code, exp, tries}
const emailCodes = new Map();
setTimeout(() => ads.dailySweep(), 60 * 1000);
setInterval(() => ads.dailySweep(), 60 * 60 * 1000); // login-ad daily charges
async function boot() {
await db.init({ dataDir: DATA_DIR }); // no-op without DATABASE_URL (JSON mode)
chain.init({ onEvent: ev => pushFeed(ev) });
auth.init({ dataDir: DATA_DIR, chain, isProd: IS_PROD, site: 'instantadpay.com' });
accounts.init({ dataDir: DATA_DIR });
ads.init({ dataDir: DATA_DIR, chain });
mailer.init({ dataDir: DATA_DIR });
chatbot.init({ dataDir: DATA_DIR, chain });
setTimeout(() => ads.dailySweep().catch(e => console.error('sweep', e.message)), 60 * 1000);
setInterval(() => ads.dailySweep().catch(e => console.error('sweep', e.message)), 60 * 60 * 1000);
}
function siteConfig() {
let saved = {};
@@ -101,16 +105,16 @@ 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);
const acct = await 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) {
async function nudgeReferrer(ref) {
try {
const t = String(ref || '').trim().toLowerCase();
if (!t || /^\d+$/.test(t) || !mailer.hasKey()) return;
const owner = accounts.byCode(t);
const owner = await 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'
@@ -176,7 +180,7 @@ const server = http.createServer(async (req, res) => {
}
if (p === '/api/stats' && req.method === 'GET') {
let members = 0; try { members = await chain.memberCount(); } catch (e) {}
return json(res, 200, Object.assign({ onchainMembers: members, siteAccounts: accounts.count() }, chain.totals()));
return json(res, 200, Object.assign({ onchainMembers: members, siteAccounts: await accounts.count() }, chain.totals()));
}
// -- 24/7 assistant
@@ -193,19 +197,19 @@ const server = http.createServer(async (req, res) => {
if (p === '/api/signup' && req.method === 'POST') {
const b = await readBody(req);
const ref = parseCookies(req)['iap.sponsor'] || ''; // first-touch attribution
const r = accounts.signup(b.email, b.password, ref);
const r = await accounts.signup(b.email, b.password, ref);
if (r.error) return json(res, 400, r);
nudgeReferrer(ref);
const token = auth.mintSession({ email: r.account.email });
nudgeReferrer(ref).catch(() => {});
const token = await auth.mintSession({ email: r.account.email });
return json(res, 200, { ok: true, account: r.account }, { 'Set-Cookie': auth.sessionCookie(token) });
}
if (p === '/api/login' && req.method === 'POST') {
const b = await readBody(req);
const r = accounts.login(b.email, b.password);
const r = await accounts.login(b.email, b.password);
if (r.error) return json(res, 400, r);
let memberId = 0;
if (r.account.address) { try { memberId = await chain.memberIdByAccount(r.account.address); } catch (e) {} }
const token = auth.mintSession({ email: r.account.email, address: r.account.address, memberId });
const token = await auth.mintSession({ email: r.account.email, address: r.account.address, memberId });
return json(res, 200, { ok: true, account: r.account }, { 'Set-Cookie': auth.sessionCookie(token) });
}
@@ -238,12 +242,12 @@ const server = http.createServer(async (req, res) => {
if (String(b.code || '').trim() !== rec.code) return json(res, 400, { error: 'That code does not match.' });
emailCodes.delete(e);
const ref = parseCookies(req)['iap.sponsor'] || '';
const r = accounts.ensure(e, ref); // first touch wins; existing accounts unchanged
const r = await accounts.ensure(e, ref); // first touch wins; existing accounts unchanged
if (r.error) return json(res, 400, r);
if (r.created) nudgeReferrer(ref);
if (r.created) nudgeReferrer(ref).catch(() => {});
let memberId = 0;
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 = await auth.mintSession({ email: r.account.email, address: r.account.address, memberId });
return json(res, 200, { ok: true, account: r.account }, { 'Set-Cookie': auth.sessionCookie(token) });
}
@@ -260,27 +264,27 @@ const server = http.createServer(async (req, res) => {
if (r.error) return json(res, 400, r);
let memberId = 0;
try { memberId = await chain.memberIdByAccount(r.address); } catch (e) {}
const s = auth.fromRequest(req);
const s = await auth.fromRequest(req);
if (s && s.email) {
const lr = accounts.linkWallet(s.email, r.address);
const lr = await accounts.linkWallet(s.email, r.address);
if (lr.error) return json(res, 400, lr);
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 });
}
const acct = accounts.byAddress(r.address);
const token = auth.mintSession({ email: acct ? acct.email : null, address: r.address, memberId });
const acct = await accounts.byAddress(r.address);
const token = await auth.mintSession({ email: acct ? acct.email : null, address: r.address, memberId });
return json(res, 200, { ok: true, address: r.address, memberId },
{ 'Set-Cookie': auth.sessionCookie(token) });
}
if (p === '/api/auth/logout' && req.method === 'POST') {
auth.logout(req);
await auth.logout(req);
return json(res, 200, { ok: true }, { 'Set-Cookie': auth.clearCookie() });
}
if (p === '/api/me' && req.method === 'GET') {
const s = auth.fromRequest(req);
const s = await auth.fromRequest(req);
if (!s) return json(res, 200, { signedIn: false });
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 && await accounts.byEmail(s.email)) || (s.address && await 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,
address: s.address || (acct && acct.address) || null, memberId,
@@ -297,7 +301,7 @@ const server = http.createServer(async (req, res) => {
}
if (p === '/api/my/activity' && req.method === 'GET') {
const s = auth.fromRequest(req);
const s = await auth.fromRequest(req);
if (!s) return json(res, 401, { error: 'Sign in first.' });
const id = s.memberId || await auth.refreshMemberId(s);
if (!id) return json(res, 200, { memberId: 0, earnings: [], purchases: [], referrals: [] });
@@ -312,26 +316,26 @@ const server = http.createServer(async (req, res) => {
// -- ad engine (spec §8b v1: banners, text, login ads)
if (p === '/api/ads/slot' && req.method === 'GET') {
const ad = ads.serve(String(u.searchParams.get('type') || 'banner'));
const ad = await ads.serve(String(u.searchParams.get('type') || 'banner'));
return json(res, 200, { ad });
}
m = /^\/api\/ads\/click\/(\d+)$/.exec(p);
if (m && req.method === 'GET') {
const target = ads.click(m[1]);
const target = await ads.click(m[1]);
if (!target) { res.writeHead(404, baseHeaders()); return res.end(); }
res.writeHead(302, baseHeaders({ Location: target }));
return res.end();
}
if (p === '/api/my/campaigns' && req.method === 'GET') {
const s = auth.fromRequest(req);
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const memberId = await auth.refreshMemberId(s);
const out = { campaigns: ads.listCampaigns(s.email), rates: ads.rates() };
const out = { campaigns: await ads.listCampaigns(s.email), rates: ads.rates() };
out.availableCredits = memberId ? await ads.availableCredits(memberId) : 0;
return json(res, 200, out);
}
if (p === '/api/my/campaigns' && req.method === 'POST') {
const s = auth.fromRequest(req);
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const memberId = await auth.refreshMemberId(s);
if (!memberId) return json(res, 400, { error: 'Buy an ad package first. Campaigns spend the on-chain credits it mints.' });
@@ -341,21 +345,21 @@ const server = http.createServer(async (req, res) => {
}
m = /^\/api\/my\/campaigns\/(\d+)\/(pause|resume)$/.exec(p);
if (m && req.method === 'POST') {
const s = auth.fromRequest(req);
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const r = ads.setStatus(s.email, m[1], m[2] === 'pause' ? 'paused' : 'active');
const r = await ads.setStatus(s.email, m[1], m[2] === 'pause' ? 'paused' : 'active');
return json(res, r.error ? 400 : 200, r);
}
// -- admin (Bearer ADMIN_PASSWORD)
if (p === '/api/admin/burns' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, { pending: ads.pendingBurns() });
return json(res, 200, { pending: await ads.pendingBurns() });
}
if (p === '/api/admin/burns/mark' && req.method === 'POST') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const b = await readBody(req);
const r = ads.markBurned(b.id, b.tx);
const r = await ads.markBurned(b.id, b.tx);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/admin/rates' && req.method === 'PATCH') {
@@ -399,4 +403,6 @@ const server = http.createServer(async (req, res) => {
try { json(res, 500, { error: 'server error' }); } catch (_) {}
}
});
server.listen(PORT, () => console.log(`InstantAdPay site on :${PORT} — chain: ${chain.getConfig().chainName}`));
boot().then(() => {
server.listen(PORT, () => console.log(`InstantAdPay site on :${PORT} — chain: ${chain.getConfig().chainName} — store: ${db.enabled() ? 'MySQL' : 'volume JSON'}`));
}).catch(e => { console.error('boot failed:', e.message); process.exit(1); });