mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
refactor(api): dedupe in-memory IP rate limiter (#1740)
This commit is contained in:
20
api/_ip-rate-limit.js
Normal file
20
api/_ip-rate-limit.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export function createIpRateLimiter({ limit, windowMs }) {
|
||||
const rateLimitMap = new Map();
|
||||
|
||||
function getEntry(ip) {
|
||||
return rateLimitMap.get(ip) || null;
|
||||
}
|
||||
|
||||
function isRateLimited(ip) {
|
||||
const now = Date.now();
|
||||
const entry = getEntry(ip);
|
||||
if (!entry || now - entry.windowStart > windowMs) {
|
||||
rateLimitMap.set(ip, { windowStart: now, count: 1 });
|
||||
return false;
|
||||
}
|
||||
entry.count += 1;
|
||||
return entry.count > limit;
|
||||
}
|
||||
|
||||
return { isRateLimited, getEntry };
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { ConvexHttpClient } from 'convex/browser';
|
||||
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
|
||||
import { getClientIp, verifyTurnstile } from './_turnstile.js';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
import { createIpRateLimiter } from './_ip-rate-limit.js';
|
||||
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const PHONE_RE = /^[+(]?\d[\d\s()./-]{4,23}\d$/;
|
||||
@@ -23,20 +24,10 @@ const FREE_EMAIL_DOMAINS = new Set([
|
||||
't-online.de', 'libero.it', 'virgilio.it',
|
||||
]);
|
||||
|
||||
const rateLimitMap = new Map();
|
||||
const RATE_LIMIT = 3;
|
||||
const RATE_WINDOW_MS = 60 * 60 * 1000;
|
||||
|
||||
function isRateLimited(ip) {
|
||||
const now = Date.now();
|
||||
const entry = rateLimitMap.get(ip);
|
||||
if (!entry || now - entry.windowStart > RATE_WINDOW_MS) {
|
||||
rateLimitMap.set(ip, { windowStart: now, count: 1 });
|
||||
return false;
|
||||
}
|
||||
entry.count += 1;
|
||||
return entry.count > RATE_LIMIT;
|
||||
}
|
||||
const rateLimiter = createIpRateLimiter({ limit: RATE_LIMIT, windowMs: RATE_WINDOW_MS });
|
||||
|
||||
async function sendNotificationEmail(name, email, organization, phone, message) {
|
||||
const resendKey = process.env.RESEND_API_KEY;
|
||||
@@ -113,7 +104,7 @@ export default async function handler(req) {
|
||||
|
||||
const ip = getClientIp(req);
|
||||
|
||||
if (isRateLimited(ip)) {
|
||||
if (rateLimiter.isRateLimited(ip)) {
|
||||
return jsonResponse({ error: 'Too many requests' }, 429, cors);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,25 +4,16 @@ import { ConvexHttpClient } from 'convex/browser';
|
||||
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
|
||||
import { getClientIp, verifyTurnstile } from './_turnstile.js';
|
||||
import { jsonResponse } from './_json-response.js';
|
||||
import { createIpRateLimiter } from './_ip-rate-limit.js';
|
||||
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const MAX_EMAIL_LENGTH = 320;
|
||||
const MAX_META_LENGTH = 100;
|
||||
|
||||
const rateLimitMap = new Map();
|
||||
const RATE_LIMIT = 5;
|
||||
const RATE_WINDOW_MS = 60 * 60 * 1000;
|
||||
|
||||
function isRateLimited(ip) {
|
||||
const now = Date.now();
|
||||
const entry = rateLimitMap.get(ip);
|
||||
if (!entry || now - entry.windowStart > RATE_WINDOW_MS) {
|
||||
rateLimitMap.set(ip, { windowStart: now, count: 1 });
|
||||
return false;
|
||||
}
|
||||
entry.count += 1;
|
||||
return entry.count > RATE_LIMIT;
|
||||
}
|
||||
const rateLimiter = createIpRateLimiter({ limit: RATE_LIMIT, windowMs: RATE_WINDOW_MS });
|
||||
|
||||
async function sendConfirmationEmail(email, referralCode) {
|
||||
const referralLink = `https://worldmonitor.app/pro?ref=${referralCode}`;
|
||||
@@ -193,7 +184,7 @@ export default async function handler(req) {
|
||||
}
|
||||
|
||||
const ip = getClientIp(req);
|
||||
if (isRateLimited(ip)) {
|
||||
if (rateLimiter.isRateLimited(ip)) {
|
||||
return jsonResponse({ error: 'Too many requests' }, 429, cors);
|
||||
}
|
||||
|
||||
@@ -214,7 +205,7 @@ export default async function handler(req) {
|
||||
const DESKTOP_SOURCES = new Set(['desktop-settings']);
|
||||
const isDesktopSource = typeof body.source === 'string' && DESKTOP_SOURCES.has(body.source);
|
||||
if (isDesktopSource) {
|
||||
const entry = rateLimitMap.get(ip);
|
||||
const entry = rateLimiter.getEntry(ip);
|
||||
if (entry && entry.count > 2) {
|
||||
return jsonResponse({ error: 'Rate limit exceeded' }, 429, cors);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user