Sign-up code guard: honeypot, form age, per-IP + global limits, progressive icon check, burst alert
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+25
-1
@@ -143,5 +143,29 @@ window.IAP = (function () {
|
|||||||
el.hidden = false;
|
el.hidden = false;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, adSlot, reportAd, $ };
|
// ── sign-up code request with the invisible guard fields (form age + honeypot)
|
||||||
|
// and the icon check the server asks for only after an IP trips a limit ──
|
||||||
|
const FORM_TS = Date.now();
|
||||||
|
function iconCheck(host, ch, note) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
host.hidden = false;
|
||||||
|
host.innerHTML = '<div class="small" style="margin:0 0 8px">' + (note ? esc(note) + ' ' : '') + 'Tap the <b>' + esc(ch.prompt) + '</b>.</div>'
|
||||||
|
+ '<div class="icon-check">' + ch.options.map(o => '<button type="button" class="ic-btn">' + esc(o) + '</button>').join('') + '</div>';
|
||||||
|
host.querySelectorAll('.ic-btn').forEach(b => b.addEventListener('click', () => { host.innerHTML = ''; host.hidden = true; resolve(b.textContent); }, { once: true }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function requestCode(email, opts) {
|
||||||
|
const o = opts || {};
|
||||||
|
let pick = null;
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const r = await (await fetch('/api/auth/email/start', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, fts: FORM_TS, website: (o.honeypot && o.honeypot.value) || '', pick }) })).json();
|
||||||
|
if (r.challenge && o.host) { pick = await iconCheck(o.host, r.challenge, r.error); continue; }
|
||||||
|
if (r.error) throw new Error(r.error);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
throw new Error('Could not verify. Refresh the page and try again.');
|
||||||
|
}
|
||||||
|
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||||||
|
return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, adSlot, reportAd, requestCode, $ };
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -43,8 +43,9 @@
|
|||||||
function busy(btn, fn) {
|
function busy(btn, fn) {
|
||||||
return async () => { if (btn.disabled) return; btn.disabled = true; err(''); try { await fn(); } catch (e) { err(e.message || 'Something went wrong.'); } finally { btn.disabled = false; } };
|
return async () => { if (btn.disabled) return; btn.disabled = true; err(''); try { await fn(); } catch (e) { err(e.message || 'Something went wrong.'); } finally { btn.disabled = false; } };
|
||||||
}
|
}
|
||||||
|
const codeOpts = () => ({ honeypot: $('jnWebsite'), host: $('jnCheck') });
|
||||||
const send = busy($('jnSend'), async () => {
|
const send = busy($('jnSend'), async () => {
|
||||||
const r = await api('/api/auth/email/start', { email: $('jnEmail').value });
|
const r = await IAP.requestCode($('jnEmail').value, codeOpts());
|
||||||
$('jnCodeRow').hidden = false; $('jnVerify').hidden = false; $('jnSend').hidden = true; $('jnResend').hidden = false;
|
$('jnCodeRow').hidden = false; $('jnVerify').hidden = false; $('jnSend').hidden = true; $('jnResend').hidden = false;
|
||||||
if (r.devCode) $('jnCode').value = r.devCode;
|
if (r.devCode) $('jnCode').value = r.devCode;
|
||||||
// the last thing they see before the account is created: who they are joining under
|
// the last thing they see before the account is created: who they are joining under
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
});
|
});
|
||||||
$('jnSend').addEventListener('click', send);
|
$('jnSend').addEventListener('click', send);
|
||||||
$('jnResend').addEventListener('click', busy($('jnResend'), async () => {
|
$('jnResend').addEventListener('click', busy($('jnResend'), async () => {
|
||||||
const r = await api('/api/auth/email/start', { email: $('jnEmail').value });
|
const r = await IAP.requestCode($('jnEmail').value, codeOpts());
|
||||||
if (r.devCode) $('jnCode').value = r.devCode;
|
if (r.devCode) $('jnCode').value = r.devCode;
|
||||||
IAP.status('Fresh code sent.', 'ok');
|
IAP.status('Fresh code sent.', 'ok');
|
||||||
}));
|
}));
|
||||||
|
|||||||
+3
-2
@@ -1610,8 +1610,9 @@
|
|||||||
if (!cfg.emailAuth) return;
|
if (!cfg.emailAuth) return;
|
||||||
$('passCards').hidden = true;
|
$('passCards').hidden = true;
|
||||||
$('magicCard').hidden = false;
|
$('magicCard').hidden = false;
|
||||||
|
const codeOpts = () => ({ honeypot: $('mcWebsite'), host: $('mcCheck') });
|
||||||
const start = busy($('mcSendBtn'), async () => {
|
const start = busy($('mcSendBtn'), async () => {
|
||||||
const r = await api('/api/auth/email/start', { email: $('mcEmail').value });
|
const r = await IAP.requestCode($('mcEmail').value, codeOpts());
|
||||||
$('mcCodeRow').hidden = false;
|
$('mcCodeRow').hidden = false;
|
||||||
$('mcVerifyBtn').hidden = false;
|
$('mcVerifyBtn').hidden = false;
|
||||||
$('mcSendBtn').hidden = true;
|
$('mcSendBtn').hidden = true;
|
||||||
@@ -1622,7 +1623,7 @@
|
|||||||
});
|
});
|
||||||
$('mcSendBtn').addEventListener('click', start);
|
$('mcSendBtn').addEventListener('click', start);
|
||||||
$('mcResend').addEventListener('click', busy($('mcResend'), async () => {
|
$('mcResend').addEventListener('click', busy($('mcResend'), async () => {
|
||||||
const r = await api('/api/auth/email/start', { email: $('mcEmail').value });
|
const r = await IAP.requestCode($('mcEmail').value, codeOpts());
|
||||||
if (r.devCode) $('mcCode').value = r.devCode;
|
if (r.devCode) $('mcCode').value = r.devCode;
|
||||||
IAP.status('Fresh code sent.', 'ok');
|
IAP.status('Fresh code sent.', 'ok');
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -628,6 +628,10 @@ img{max-width:100%}
|
|||||||
/* ── promo tools (2026-09-09) ── */
|
/* ── promo tools (2026-09-09) ── */
|
||||||
.promo-strip{display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;padding:16px 20px}
|
.promo-strip{display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;padding:16px 20px}
|
||||||
.angle-list{display:flex;flex-direction:column;gap:8px}
|
.angle-list{display:flex;flex-direction:column;gap:8px}
|
||||||
|
.hp-field{position:absolute;left:-9999px;top:auto;width:1px;height:1px;opacity:0;overflow:hidden}
|
||||||
|
.icon-check{display:flex;gap:8px;flex-wrap:wrap;margin:0 0 12px}
|
||||||
|
.icon-check .ic-btn{font-size:26px;line-height:1;padding:10px 14px;border-radius:12px;border:1px solid var(--line-strong);background:var(--panel);cursor:pointer}
|
||||||
|
.icon-check .ic-btn:hover{border-color:var(--mint)}
|
||||||
.angle-row.pb-acc>summary{padding:12px 14px;font-size:14.5px}
|
.angle-row.pb-acc>summary{padding:12px 14px;font-size:14.5px}
|
||||||
.angle-row .angle-body{display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;padding:0 14px 14px}
|
.angle-row .angle-body{display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;padding:0 14px 14px}
|
||||||
.angle-row .angle-txt{flex:1 1 320px;min-width:0}
|
.angle-row .angle-txt{flex:1 1 320px;min-width:0}
|
||||||
|
|||||||
+4
-2
@@ -111,6 +111,8 @@
|
|||||||
<h3 id="jnCapH">Join free</h3>
|
<h3 id="jnCapH">Join free</h3>
|
||||||
<p class="muted small" style="margin:0 0 12px" id="jnCapSub">Type your email and we send a 6-digit code. No password, no wallet needed today.</p>
|
<p class="muted small" style="margin:0 0 12px" id="jnCapSub">Type your email and we send a 6-digit code. No password, no wallet needed today.</p>
|
||||||
<p><input id="jnEmail" type="email" placeholder="Your email" autocomplete="email"></p>
|
<p><input id="jnEmail" type="email" placeholder="Your email" autocomplete="email"></p>
|
||||||
|
<input id="jnWebsite" class="hp-field" type="text" name="website" tabindex="-1" autocomplete="off" aria-hidden="true">
|
||||||
|
<div id="jnCheck" hidden></div>
|
||||||
<p id="jnCodeRow" hidden><input id="jnCode" inputmode="numeric" placeholder="6-digit code from your inbox"></p>
|
<p id="jnCodeRow" hidden><input id="jnCode" inputmode="numeric" placeholder="6-digit code from your inbox"></p>
|
||||||
<p id="jnUnder" class="small muted" hidden style="margin:-4px 0 12px"></p>
|
<p id="jnUnder" class="small muted" hidden style="margin:-4px 0 12px"></p>
|
||||||
<label class="small muted" style="display:flex;gap:8px;align-items:flex-start;margin:0 0 12px;cursor:pointer">
|
<label class="small muted" style="display:flex;gap:8px;align-items:flex-start;margin:0 0 12px;cursor:pointer">
|
||||||
@@ -153,7 +155,7 @@
|
|||||||
InstantAdPay · <a href="/contract">Contract</a> · <a href="/terms">Terms</a> · <a href="/privacy">Privacy</a> · <a href="/disclaimer">Disclaimer</a>
|
InstantAdPay · <a href="/contract">Contract</a> · <a href="/terms">Terms</a> · <a href="/privacy">Privacy</a> · <a href="/disclaimer">Disclaimer</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/assets/common.js?v=20260910c"></script>
|
<script src="/assets/common.js?v=20260911a"></script>
|
||||||
<script src="/assets/join.js?v=20260910d"></script>
|
<script src="/assets/join.js?v=20260911a"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+5
-3
@@ -5,7 +5,7 @@
|
|||||||
<title>Member area | InstantAdPay</title>
|
<title>Member area | InstantAdPay</title>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
<link rel="icon" type="image/png" href="/logo-icon.png">
|
<link rel="icon" type="image/png" href="/logo-icon.png">
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260911a">
|
<link rel="stylesheet" href="/assets/site.css?v=20260911b">
|
||||||
</head>
|
</head>
|
||||||
<body class="bo-body">
|
<body class="bo-body">
|
||||||
|
|
||||||
@@ -34,6 +34,8 @@
|
|||||||
<p class="muted small">Type your email and we send a 6-digit code. No password to invent,
|
<p class="muted small">Type your email and we send a 6-digit code. No password to invent,
|
||||||
no password to forget. New emails get a free account automatically.</p>
|
no password to forget. New emails get a free account automatically.</p>
|
||||||
<p><input id="mcEmail" type="email" placeholder="Email" autocomplete="email" style="width:100%"></p>
|
<p><input id="mcEmail" type="email" placeholder="Email" autocomplete="email" style="width:100%"></p>
|
||||||
|
<input id="mcWebsite" class="hp-field" type="text" name="website" tabindex="-1" autocomplete="off" aria-hidden="true">
|
||||||
|
<div id="mcCheck" hidden></div>
|
||||||
<p id="mcCodeRow" hidden><input id="mcCode" inputmode="numeric" placeholder="6-digit code" style="width:100%"></p>
|
<p id="mcCodeRow" hidden><input id="mcCode" inputmode="numeric" placeholder="6-digit code" style="width:100%"></p>
|
||||||
<button class="btn" id="mcSendBtn">Email me a code</button>
|
<button class="btn" id="mcSendBtn">Email me a code</button>
|
||||||
<button class="btn" id="mcVerifyBtn" hidden>Sign in</button>
|
<button class="btn" id="mcVerifyBtn" hidden>Sign in</button>
|
||||||
@@ -879,10 +881,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/assets/common.js?v=20260910c"></script>
|
<script src="/assets/common.js?v=20260911a"></script>
|
||||||
<script src="/assets/wallet.js?v=20260910a"></script>
|
<script src="/assets/wallet.js?v=20260910a"></script>
|
||||||
<script src="/assets/promo.js?v=20260911a"></script>
|
<script src="/assets/promo.js?v=20260911a"></script>
|
||||||
<script src="/assets/my.js?v=20260910m"></script>
|
<script src="/assets/my.js?v=20260911a"></script>
|
||||||
<script src="/assets/chat.js?v=20260907l"></script>
|
<script src="/assets/chat.js?v=20260907l"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -116,6 +116,59 @@ function chatLimited(ip) {
|
|||||||
}
|
}
|
||||||
// magic-code sign-in: emailLower -> {code, exp, tries}
|
// magic-code sign-in: emailLower -> {code, exp, tries}
|
||||||
const emailCodes = new Map();
|
const emailCodes = new Map();
|
||||||
|
// ── sign-up code guard (Marty, 2026-09-11): the email box is one field and one
|
||||||
|
// tap, so nothing visible stands in a human's way. Bots hit four invisible walls:
|
||||||
|
// a honeypot field, a minimum form age, per-IP + global send limits, and, only
|
||||||
|
// once an IP trips a limit, the same icon check the ad viewer uses.
|
||||||
|
const CODE_LIMITS = { per10m: 5, perDay: 20, globalPerMin: 60, passMs: 5 * 60 * 1000, minFormMs: 2000 };
|
||||||
|
const codeHits = new Map(); // ip -> { t: [send timestamps, 24h], passUntil, chal: { answer, exp } }
|
||||||
|
const codeGlobal = { minute: 0, n: 0 };
|
||||||
|
const codeAlert = { last: 0, trips: 0, ips: new Set() };
|
||||||
|
function clientIp(req) { return String(req.headers['x-forwarded-for'] || req.socket.remoteAddress || '').split(',')[0].trim() || 'unknown'; }
|
||||||
|
function codeChallenge(rec) {
|
||||||
|
const pick = CAPTCHA.slice().sort(() => Math.random() - 0.5).slice(0, 5);
|
||||||
|
const answer = Math.floor(Math.random() * pick.length);
|
||||||
|
rec.chal = { answer: pick[answer][0], exp: Date.now() + 5 * 60 * 1000 };
|
||||||
|
return { prompt: pick[answer][1], options: pick.map(x => x[0]) };
|
||||||
|
}
|
||||||
|
// returns null to allow the send, or { status, body } to answer with instead
|
||||||
|
function codeGuard(req, b) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (b.website) return { status: 200, body: { ok: true, sent: true } }; // honeypot: bots fill it, humans never see it
|
||||||
|
const fts = Number(b.fts) || 0;
|
||||||
|
if (!fts || now - fts < CODE_LIMITS.minFormMs || now - fts > 12 * 3600 * 1000) return { status: 400, body: { error: 'Give the page a second, then tap again.' } };
|
||||||
|
const minute = Math.floor(now / 60000);
|
||||||
|
if (codeGlobal.minute !== minute) { codeGlobal.minute = minute; codeGlobal.n = 0; }
|
||||||
|
if (codeGlobal.n >= CODE_LIMITS.globalPerMin) { codeTrip(req, 'global'); return { status: 429, body: { error: 'Busy right now. Try again in a minute.' } }; }
|
||||||
|
const ip = clientIp(req);
|
||||||
|
const rec = codeHits.get(ip) || { t: [], passUntil: 0, chal: null };
|
||||||
|
rec.t = rec.t.filter(ts => now - ts < 24 * 3600 * 1000);
|
||||||
|
const n10 = rec.t.filter(ts => now - ts < 10 * 60 * 1000).length;
|
||||||
|
const limited = n10 >= CODE_LIMITS.per10m || rec.t.length >= CODE_LIMITS.perDay;
|
||||||
|
if (limited && now >= rec.passUntil) {
|
||||||
|
const pick = String(b.pick || '');
|
||||||
|
if (pick && rec.chal && rec.chal.exp > now && pick === rec.chal.answer) { rec.passUntil = now + CODE_LIMITS.passMs; rec.chal = null; }
|
||||||
|
else {
|
||||||
|
codeTrip(req, ip);
|
||||||
|
const challenge = codeChallenge(rec); codeHits.set(ip, rec);
|
||||||
|
return { status: 429, body: { error: pick ? 'That was not it. Try once more.' : 'Quick check before we send another code.', challenge } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rec.t.push(now); codeHits.set(ip, rec); codeGlobal.n += 1;
|
||||||
|
if (codeHits.size > 5000) for (const [k, v] of codeHits) { if (!v.t.length || now - v.t[v.t.length - 1] > 24 * 3600 * 1000) codeHits.delete(k); }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// burst alert: at most one message per 10 minutes, to the admin Telegram chat if set, else the admin email
|
||||||
|
function codeTrip(req, ip) {
|
||||||
|
codeAlert.trips += 1; codeAlert.ips.add(ip);
|
||||||
|
if (Date.now() - codeAlert.last < 10 * 60 * 1000) return;
|
||||||
|
codeAlert.last = Date.now();
|
||||||
|
const text = '\u26A0\uFE0F InstantAdPay sign-up guard: ' + codeAlert.trips + ' blocked code request' + (codeAlert.trips === 1 ? '' : 's') + ' from ' + codeAlert.ips.size + ' source' + (codeAlert.ips.size === 1 ? '' : 's') + ' (' + [...codeAlert.ips].slice(0, 5).join(', ') + ') in the last window.';
|
||||||
|
codeAlert.trips = 0; codeAlert.ips = new Set();
|
||||||
|
const sc = siteConfig();
|
||||||
|
if (sc.telegramBotToken && sc.telegramAdminChatId) telegramSend(sc.telegramAdminChatId, text).catch(() => {});
|
||||||
|
else if (ADMIN_EMAIL && mailer.hasKey()) mailer.send(ADMIN_EMAIL, 'InstantAdPay: sign-up guard tripped', text).catch(() => {});
|
||||||
|
}
|
||||||
// earn-view tokens: emailLower -> {token, ts} (one live token per member)
|
// earn-view tokens: emailLower -> {token, ts} (one live token per member)
|
||||||
const earnTokens = new Map();
|
const earnTokens = new Map();
|
||||||
// human-check pairs for the view verifier: [emoji shown, word named in the prompt]
|
// human-check pairs for the view verifier: [emoji shown, word named in the prompt]
|
||||||
@@ -272,6 +325,7 @@ function siteConfig() {
|
|||||||
rehearsal: true, // shows the testnet banner; flipped off at mainnet launch
|
rehearsal: true, // shows the testnet banner; flipped off at mainnet launch
|
||||||
// payment-proof Telegram feed (blank = off) and the P&L pane's fixed monthly cost
|
// payment-proof Telegram feed (blank = off) and the P&L pane's fixed monthly cost
|
||||||
telegramBotToken: '', telegramChatId: '', telegramTopicId: '', telegramEvents: 'payouts', telegramCtaUrl: 'https://instantadpay.com/',
|
telegramBotToken: '', telegramChatId: '', telegramTopicId: '', telegramEvents: 'payouts', telegramCtaUrl: 'https://instantadpay.com/',
|
||||||
|
telegramAdminChatId: '', // private chat for admin alerts (sign-up guard bursts); falls back to ADMIN_EMAIL
|
||||||
pnlFixedMonthlyUsd: 0
|
pnlFixedMonthlyUsd: 0
|
||||||
}, saved);
|
}, saved);
|
||||||
}
|
}
|
||||||
@@ -470,7 +524,13 @@ async function telegramOnEvent(ev) {
|
|||||||
else if (ev.type === 'MemberActivated' && mode === 'all') line = '\u{1F91D} ' + who(ev.id) + ' switched on payouts';
|
else if (ev.type === 'MemberActivated' && mode === 'all') line = '\u{1F91D} ' + who(ev.id) + ' switched on payouts';
|
||||||
if (!line) return;
|
if (!line) return;
|
||||||
const text = line + ' \u00b7 <a href="' + tx + '">verify</a>' + (sc.telegramCtaUrl ? '\n<a href="' + sc.telegramCtaUrl + '">Join free</a>' : '');
|
const text = line + ' \u00b7 <a href="' + tx + '">verify</a>' + (sc.telegramCtaUrl ? '\n<a href="' + sc.telegramCtaUrl + '">Join free</a>' : '');
|
||||||
const body = JSON.stringify(Object.assign({ chat_id: sc.telegramChatId, text, parse_mode: 'HTML', disable_web_page_preview: true }, sc.telegramTopicId ? { message_thread_id: Number(sc.telegramTopicId) } : {}));
|
await telegramSend(sc.telegramChatId, text, sc.telegramTopicId);
|
||||||
|
}
|
||||||
|
// one sendMessage call; never throws, never logs the token
|
||||||
|
async function telegramSend(chatId, text, threadId) {
|
||||||
|
const sc = siteConfig();
|
||||||
|
if (!sc.telegramBotToken || !chatId) return;
|
||||||
|
const body = JSON.stringify(Object.assign({ chat_id: chatId, text, parse_mode: 'HTML', disable_web_page_preview: true }, threadId ? { message_thread_id: Number(threadId) } : {}));
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
const rq = https.request({ hostname: 'api.telegram.org', path: '/bot' + sc.telegramBotToken + '/sendMessage', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, timeout: 10000 }, r => { r.resume(); r.on('end', resolve); });
|
const rq = https.request({ hostname: 'api.telegram.org', path: '/bot' + sc.telegramBotToken + '/sendMessage', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, timeout: 10000 }, r => { r.resume(); r.on('end', resolve); });
|
||||||
rq.on('error', () => resolve()); rq.on('timeout', () => { rq.destroy(); resolve(); }); rq.end(body);
|
rq.on('error', () => resolve()); rq.on('timeout', () => { rq.destroy(); resolve(); }); rq.end(body);
|
||||||
@@ -658,6 +718,8 @@ const server = http.createServer(async (req, res) => {
|
|||||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(e)) return json(res, 400, { error: 'That email address does not look right.' });
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(e)) return json(res, 400, { error: 'That email address does not look right.' });
|
||||||
const prev = emailCodes.get(e);
|
const prev = emailCodes.get(e);
|
||||||
if (prev && Date.now() < prev.nextAt) return json(res, 429, { error: 'Code already sent. Give it a minute, then try again.' });
|
if (prev && Date.now() < prev.nextAt) return json(res, 429, { error: 'Code already sent. Give it a minute, then try again.' });
|
||||||
|
const guard = codeGuard(req, b); // honeypot, form age, per-IP + global limits, icon check once limited
|
||||||
|
if (guard) return json(res, guard.status, guard.body);
|
||||||
const code = String(Math.floor(100000 + Math.random() * 900000));
|
const code = String(Math.floor(100000 + Math.random() * 900000));
|
||||||
emailCodes.set(e, { code, exp: Date.now() + 15 * 60 * 1000, tries: 0, nextAt: Date.now() + 60 * 1000 });
|
emailCodes.set(e, { code, exp: Date.now() + 15 * 60 * 1000, tries: 0, nextAt: Date.now() + 60 * 1000 });
|
||||||
if (mailer.hasKey()) {
|
if (mailer.hasKey()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user