diff --git a/accounts.js b/accounts.js index 3bd17ee..cb5ce18 100644 --- a/accounts.js +++ b/accounts.js @@ -168,6 +168,10 @@ const J = { return { ok: true, account: pub(acct) }; }, async listAll(limit) { return Object.values(this.db.byEmail).sort((a, b) => (b.created || 0) - (a.created || 0)).slice(0, limit).map(pub); }, + // every account's identity, never a page of them: a lookup built from a capped list silently + // calls every older member unreachable (the admin table labelled 144 live sponsors 'dead link' + // once the site passed 500 accounts, Marty 2026-09-23) + async identities() { return Object.values(this.db.byEmail).map(a => ({ email: a.email, username: a.username || null, code: a.code || null, memberId: a.memberId || 0 })); }, async setSponsorRef(e, ref) { const acct = this.db.byEmail[e]; if (!acct) return { error: 'No such account.' }; @@ -333,6 +337,10 @@ const D = { return { ok: true, account: await this.byEmail(e) }; }, async listAll(limit) { const rows = await db.q('SELECT * FROM accounts ORDER BY created DESC LIMIT ?', [Number(limit) || 500]); return rows.map(rowPub); }, + // identity columns for EVERY account, no limit: small enough to stay cheap at any size, and a + // sponsor lookup must never be built from a page of members (see the JSON note above) + async identities() { const rows = await db.q('SELECT email, username, code, member_id FROM accounts'); + return rows.map(r => ({ email: r.email, username: r.username || null, code: r.code || null, memberId: Number(r.member_id) || 0 })); }, async setSponsorRef(e, ref) { const r = await db.q('UPDATE accounts SET sponsor_ref=? WHERE email=?', [ref, e]); if (!r.affectedRows) return { error: 'No such account.' }; @@ -445,6 +453,7 @@ async function count() { return impl().count(); } // admin: newest-first account list, and re-pointing a member's sponsor (a // username, share code, or numeric member id: the same tokens join links use) async function listAll(limit = 500) { return impl().listAll(limit); } +async function identities() { return impl().identities(); } async function setSponsorRef(email, ref) { return impl().setSponsorRef(normEmail(email), String(ref || '').trim().toLowerCase().slice(0, 40)); } // resolve a member's DIRECT sponsor account (the token they joined under) @@ -469,7 +478,7 @@ async function getChatSettings(email) { return { available: a ? a.chatAvailable !== false : true, mutes: await impl().getMutes(String(email || '').toLowerCase()) }; } -module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, byUsername, byMemberId: id => impl().byMemberId(Number(id) || 0), listAll, setSponsorRef, +module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, byUsername, byMemberId: id => impl().byMemberId(Number(id) || 0), listAll, identities, setSponsorRef, setUsername, setMemberId, namesForMembers, listByReferrer, downline, linkWallet, count, setLineBanner: (e, b, t) => impl().setLineBanner(String(e || '').toLowerCase(), b, t), setWallOffers: (e, j) => impl().setWallOffers(String(e || '').toLowerCase(), j), diff --git a/server.js b/server.js index 91c6df0..1410717 100644 --- a/server.js +++ b/server.js @@ -2998,8 +2998,10 @@ const server = http.createServer(async (req, res) => { if (!isAdmin(req)) return json(res, 401, { error: 'auth' }); const members = await accounts.listAll(500); // resolve each sponsor token (username, share code or member #) to the sponsor's name + // the lookup covers EVERY account, not the page's slice: built from `members` it called every + // sponsor who joined before the newest 500 a dead link (144 of them, Marty 2026-09-23) const byTok = {}; - for (const m of members) for (const t of [m.username, m.code, m.memberId ? String(m.memberId) : null]) if (t) byTok[String(t).toLowerCase()] = m; + for (const m of await accounts.identities()) for (const t of [m.username, m.code, m.memberId ? String(m.memberId) : null]) if (t) byTok[String(t).toLowerCase()] = m; for (const m of members) { const t = String(m.sponsorRef || '').toLowerCase(); const sp = t ? byTok[t] : null;