Sign-up deny list beside the allow list

fraud-allow.json says "never block this person". There was nothing for the opposite case.
fraud-block.json: an address on it cannot open an account through /api/signup or the
email-code door, and the admin fraud report carries the list with a matching
/api/admin/fraud/block route. Existing accounts are untouched; Suspend covers those.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-19 05:46:04 -05:00
parent b061053ccf
commit 39572c0ff5
2 changed files with 25 additions and 2 deletions
+8 -1
View File
@@ -1214,6 +1214,7 @@ const server = http.createServer(async (req, res) => {
// out only at purchase / payout-activation time and gets linked then)
if (p === '/api/signup' && req.method === 'POST') {
const b = await readBody(req);
if (fraud.isBlocked(b.email)) return json(res, 403, { error: 'This address cannot open an account.' });
const ref = parseCookies(req)['iap.sponsor'] || ''; // last-touch attribution, locked at account creation
const r = await accounts.signup(b.email, b.password, ref);
if (r.error) return json(res, 400, r);
@@ -1239,6 +1240,7 @@ const server = http.createServer(async (req, res) => {
const e = String(b.email || '').trim().toLowerCase();
const devHdr = fraud.deviceOf(req) ? undefined : { 'Set-Cookie': fraud.deviceCookie(fraud.newDeviceId(), IS_PROD) }; // browser id for one-account-per-person checks
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(e)) return json(res, 400, { error: 'That email address does not look right.' });
if (fraud.isBlocked(e)) return json(res, 403, { error: 'This address cannot open an account.' });
const prev = emailCodes.get(e);
if (prev && Date.now() < prev.nextAt) { console.log('signup-guard cooldown', clientIp(req), e.replace(/^(.).*(@.*)$/, '$1***$2')); return json(res, 429, { error: 'Code already sent. Give it a minute, then try again.' }, devHdr); }
const guard = codeGuard(req, b); // honeypot, form age, per-IP + global limits, icon check once limited
@@ -2817,11 +2819,16 @@ const server = http.createServer(async (req, res) => {
}
if (p === '/api/admin/fraud' && req.method === 'GET') { // duplicate signals: shared browsers / IPs, flagged and suspended accounts
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, Object.assign(await fraud.report(), { allow: fraud.allowList() }));
return json(res, 200, Object.assign(await fraud.report(), { allow: fraud.allowList(), block: fraud.blockList() }));
}
// Approved exceptions: people Marty has okayed to hold more than one account. Adding an
// address here stops the duplicate checks blocking or hard-flagging them, and stops them
// being dropped from the leaderboard or barred from adopting.
if (p === '/api/admin/fraud/block' && req.method === 'POST') {
const b = await readBody(req);
const r = b.remove ? fraud.blockRemove(b.email) : fraud.blockAdd(b.email, b.note, ADMIN_EMAIL || 'admin');
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/admin/fraud/allow' && req.method === 'POST') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const b = await readBody(req);