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
+142 -101
View File
@@ -1,44 +1,16 @@
// Site-side member accounts for InstantAdPay.
// The chain is the source of truth for money, credits, and qualification;
// this module holds what the chain doesn't: free members (email + password,
// the way normal people join), sponsor attribution before first purchase
// (spec §4), and the wallet link once one is connected at purchase time.
// Wiping this file = the clean reset between rehearsal and mainnet.
// Site-side member accounts. Dual-mode:
// MySQL (db.enabled) for real concurrency in production,
// JSON volume file as the no-DATABASE_URL fallback (local dev).
// All exported functions are async; both modes return identical shapes.
// The chain remains the source of truth for money/credits/qualification.
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const db = require('./db');
let DATA_DIR = null;
const FILE = () => path.join(DATA_DIR, 'accounts.json');
let db = { v: 2, byEmail: {}, byAddress: {}, joins: 0 };
function load() {
try { db = JSON.parse(fs.readFileSync(FILE(), 'utf8')); } catch (e) {}
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.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() {
try {
const tmp = FILE() + '.tmp';
fs.writeFileSync(tmp, JSON.stringify(db), { mode: 0o600 });
fs.renameSync(tmp, FILE());
} catch (e) { console.error('accounts save failed', e.message); }
}
function init(opts) { DATA_DIR = opts.dataDir; load(); }
// ---- password hashing (scrypt, no deps) ----
// ---- shared helpers ----
function hashPassword(password) {
const salt = crypto.randomBytes(16);
const hash = crypto.scryptSync(String(password), salt, 32);
@@ -51,85 +23,154 @@ function checkPassword(password, stored) {
return crypto.timingSafeEqual(hash, Buffer.from(hashHex, 'hex'));
} catch (e) { return false; }
}
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
const normEmail = e => String(e || '').trim().toLowerCase();
const normAddr = a => String(a || '').trim().toLowerCase();
function newCode(taken) {
let c;
do { c = crypto.randomBytes(5).toString('base64url').replace(/[-_]/g, '').slice(0, 7).toLowerCase(); }
while (!c || c.length < 6 || /^\d+$/.test(c) || (taken && taken(c)));
return c;
}
const pub = a => a ? { email: a.email, sponsorRef: a.sponsorRef || '', code: a.code || null,
address: a.address || null, created: a.created } : null;
// ---- email accounts (the normal join path) ----
function signup(email, password, sponsorRef) {
// ---- JSON fallback ----
const J = {
db: { v: 2, byEmail: {}, byAddress: {}, byCode: {}, joins: 0 },
FILE: () => path.join(DATA_DIR, 'accounts.json'),
load() {
try { this.db = JSON.parse(fs.readFileSync(this.FILE(), 'utf8')); } catch (e) {}
if (!this.db || !this.db.v) this.db = { v: 2, byEmail: {}, byAddress: {}, byCode: {}, joins: 0 };
if (!this.db.byCode) this.db.byCode = {};
for (const a of Object.values(this.db.byEmail)) {
if (!a.code) { a.code = newCode(c => this.db.byCode[c]); this.db.byCode[a.code] = a.email; }
else if (!this.db.byCode[a.code]) this.db.byCode[a.code] = a.email;
}
},
save() {
try {
const tmp = this.FILE() + '.tmp';
fs.writeFileSync(tmp, JSON.stringify(this.db), { mode: 0o600 });
fs.renameSync(tmp, this.FILE());
} catch (e) { console.error('accounts save failed', e.message); }
},
async signup(e, password, ref) {
if (this.db.byEmail[e]) return { error: 'That email already has an account. Log in instead.' };
const code = newCode(c => this.db.byCode[c]);
this.db.byEmail[e] = { email: e, pass: hashPassword(password), sponsorRef: ref, code, address: null, created: Date.now() };
this.db.byCode[code] = e;
this.save();
return { ok: true, created: true, account: pub(this.db.byEmail[e]) };
},
async login(e, password) {
const a = this.db.byEmail[e];
if (!a || !a.pass || !checkPassword(password, a.pass)) return { error: 'Wrong email or password.' };
return { ok: true, account: pub(a) };
},
async ensure(e, ref) {
let created = false;
if (!this.db.byEmail[e]) {
const code = newCode(c => this.db.byCode[c]);
this.db.byEmail[e] = { email: e, pass: null, sponsorRef: ref, code, address: null, created: Date.now() };
this.db.byCode[code] = e;
created = true;
this.save();
}
return { ok: true, created, account: pub(this.db.byEmail[e]) };
},
async byEmail(e) { return pub(this.db.byEmail[e]); },
async byAddress(a) { const e = this.db.byAddress[a]; return e ? pub(this.db.byEmail[e]) : null; },
async byCode(c) { const e = this.db.byCode[c]; return e ? pub(this.db.byEmail[e]) : null; },
async linkWallet(e, a) {
const acct = this.db.byEmail[e];
if (!acct) return { error: 'No such account.' };
if (acct.address && acct.address !== a) return { error: 'This account is already linked to wallet '
+ acct.address.slice(0, 6) + '…' + acct.address.slice(-4) + '. Connect that wallet instead.' };
if (this.db.byAddress[a] && this.db.byAddress[a] !== e) return { error: 'That wallet is already linked to a different account.' };
acct.address = a;
this.db.byAddress[a] = e;
this.save();
return { ok: true, account: pub(acct) };
},
async count() { return Object.keys(this.db.byEmail).length; }
};
// ---- MySQL mode ----
const rowPub = r => r ? pub({ email: r.email, sponsorRef: r.sponsor_ref, code: r.code, address: r.address, created: Number(r.created) }) : null;
const D = {
async signup(e, password, ref) {
const code = newCode();
try {
await db.q('INSERT INTO accounts (email,pass,sponsor_ref,code,address,created) VALUES (?,?,?,?,NULL,?)',
[e, hashPassword(password), ref, code, Date.now()]);
} catch (err) {
if (err.code === 'ER_DUP_ENTRY') return String(err.message).includes('code')
? this.signup(e, password, ref) // code collision: retry with a new code
: { error: 'That email already has an account. Log in instead.' };
throw err;
}
return { ok: true, created: true, account: await this.byEmail(e) };
},
async login(e, password) {
const rows = await db.q('SELECT * FROM accounts WHERE email=?', [e]);
if (!rows.length || !rows[0].pass || !checkPassword(password, rows[0].pass)) return { error: 'Wrong email or password.' };
return { ok: true, account: rowPub(rows[0]) };
},
async ensure(e, ref) {
const code = newCode();
let created = false;
try {
await db.q('INSERT INTO accounts (email,pass,sponsor_ref,code,address,created) VALUES (?,NULL,?,?,NULL,?)',
[e, ref, code, Date.now()]);
created = true;
} catch (err) {
if (err.code !== 'ER_DUP_ENTRY') throw err;
if (String(err.message).includes('code')) return this.ensure(e, ref);
}
return { ok: true, created, account: await this.byEmail(e) };
},
async byEmail(e) { const r = await db.q('SELECT * FROM accounts WHERE email=?', [e]); return rowPub(r[0]); },
async byAddress(a) { const r = await db.q('SELECT * FROM accounts WHERE address=?', [a]); return rowPub(r[0]); },
async byCode(c) { const r = await db.q('SELECT * FROM accounts WHERE code=?', [c]); return rowPub(r[0]); },
async linkWallet(e, a) {
const cur = await this.byEmail(e);
if (!cur) return { error: 'No such account.' };
if (cur.address && cur.address !== a) return { error: 'This account is already linked to wallet '
+ cur.address.slice(0, 6) + '…' + cur.address.slice(-4) + '. Connect that wallet instead.' };
try { await db.q('UPDATE accounts SET address=? WHERE email=?', [a, e]); }
catch (err) {
if (err.code === 'ER_DUP_ENTRY') return { error: 'That wallet is already linked to a different account.' };
throw err;
}
return { ok: true, account: await this.byEmail(e) };
},
async count() { const r = await db.q('SELECT COUNT(*) n FROM accounts'); return Number(r[0].n); }
};
const impl = () => db.enabled() ? D : J;
function init(opts) { DATA_DIR = opts.dataDir; J.load(); }
async function signup(email, password, sponsorRef) {
const e = normEmail(email);
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 (db.byEmail[e]) return { error: 'That email already has an account. Log in instead.' };
const code = genCode();
db.byEmail[e] = {
email: e,
pass: hashPassword(password),
sponsorRef: String(sponsorRef || ''), // first touch; resolved to a chain id at buy time
code,
address: null,
created: Date.now()
};
db.byCode[code] = e;
db.joins += 1;
save();
return { ok: true, created: true, account: publicView(db.byEmail[e]) };
return impl().signup(e, String(password), String(sponsorRef || ''));
}
function login(email, password) {
const e = normEmail(email);
const acct = db.byEmail[e];
if (!acct || !checkPassword(password, acct.pass)) return { error: 'Wrong email or password.' };
acct.lastSeen = Date.now(); save();
return { ok: true, account: publicView(acct) };
}
// Passwordless path: a verified email code proves ownership, so the account
// may exist with no password at all.
function ensure(email, sponsorRef) {
async function login(email, password) { return impl().login(normEmail(email), String(password || '')); }
async function ensure(email, sponsorRef) {
const e = normEmail(email);
if (!EMAIL_RE.test(e)) return { error: 'That email address does not look right.' };
let created = false;
if (!db.byEmail[e]) {
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;
created = true;
save();
}
return { ok: true, created, account: publicView(db.byEmail[e]) };
return impl().ensure(e, String(sponsorRef || ''));
}
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 byAddress(address) {
const e = db.byAddress[normAddr(address)];
return e ? publicView(db.byEmail[e]) : null;
}
// ---- wallet link (happens at purchase / payout activation time) ----
// First link wins and is permanent for the account; one wallet, one account.
function linkWallet(email, address) {
const e = normEmail(email);
async function byEmail(email) { return impl().byEmail(normEmail(email)); }
async function byAddress(address) { return impl().byAddress(normAddr(address)); }
async function byCode(code) { return impl().byCode(String(code || '').toLowerCase()); }
async function linkWallet(email, address) {
const a = normAddr(address);
const acct = db.byEmail[e];
if (!acct) return { error: 'No such account.' };
if (!/^0x[0-9a-f]{40}$/.test(a)) return { error: 'Bad wallet address.' };
if (acct.address && acct.address !== a) return { error: 'This account is already linked to wallet '
+ acct.address.slice(0, 6) + '…' + acct.address.slice(-4) + '. Earnings pay to that wallet. Connect it instead.' };
if (db.byAddress[a] && db.byAddress[a] !== e) return { error: 'That wallet is already linked to a different account.' };
acct.address = a;
db.byAddress[a] = e;
save();
return { ok: true, account: publicView(acct) };
return impl().linkWallet(normEmail(email), a);
}
function publicView(a) {
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; }
async function count() { return impl().count(); }
module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, linkWallet, count };