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:
@@ -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); });
|
||||
|
||||
Reference in New Issue
Block a user