SCRAPE-22: Implement rate limiting for trigger endpoint (#26)
Co-authored-by: Stephen Minakian <stephenminakian@gmail.com> Co-committed-by: Stephen Minakian <stephenminakian@gmail.com>
This commit is contained in:
@ -1162,6 +1162,53 @@ const scraperConfig = require('../config/scraper');
|
||||
// Logger for scraper admin routes
|
||||
const scraperRouteLogger = createLogger('scraper-admin');
|
||||
|
||||
// Rate limiting for manual scrape trigger (per-user, in-memory)
|
||||
const RATE_LIMIT_MAX_REQUESTS = 5;
|
||||
const RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000; // 1 hour in milliseconds
|
||||
const rateLimitStore = new Map();
|
||||
|
||||
/**
|
||||
* Check rate limit for a given user ID.
|
||||
* Returns an object indicating whether the request is allowed.
|
||||
*
|
||||
* @param {string} userId - The user ID to check
|
||||
* @returns {{ allowed: boolean, retryAfterSeconds: number|null }}
|
||||
*/
|
||||
function checkRateLimit(userId) {
|
||||
const now = Date.now();
|
||||
const userKey = userId.toString();
|
||||
|
||||
if (!rateLimitStore.has(userKey)) {
|
||||
rateLimitStore.set(userKey, []);
|
||||
}
|
||||
|
||||
const timestamps = rateLimitStore.get(userKey);
|
||||
|
||||
// Remove timestamps outside the current window
|
||||
const windowStart = now - RATE_LIMIT_WINDOW_MS;
|
||||
const validTimestamps = timestamps.filter(ts => ts > windowStart);
|
||||
rateLimitStore.set(userKey, validTimestamps);
|
||||
|
||||
if (validTimestamps.length >= RATE_LIMIT_MAX_REQUESTS) {
|
||||
// Calculate when the oldest request in the window will expire
|
||||
const oldestTimestamp = validTimestamps[0];
|
||||
const retryAfterMs = (oldestTimestamp + RATE_LIMIT_WINDOW_MS) - now;
|
||||
const retryAfterSeconds = Math.ceil(retryAfterMs / 1000);
|
||||
return { allowed: false, retryAfterSeconds };
|
||||
}
|
||||
|
||||
// Record this request
|
||||
validTimestamps.push(now);
|
||||
return { allowed: true, retryAfterSeconds: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the rate limiter (for testing)
|
||||
*/
|
||||
function resetRateLimiter() {
|
||||
rateLimitStore.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/admin/scraper/run
|
||||
* Trigger a manual scrape
|
||||
@ -1175,6 +1222,15 @@ router.post('/scraper/run', async (req, res) => {
|
||||
const db = req.app.locals.db;
|
||||
const { dryRun = false, htmlContent = null } = req.body || {};
|
||||
|
||||
// Check rate limit (per-user, before mutex check)
|
||||
const rateLimitResult = checkRateLimit(req.user._id);
|
||||
if (!rateLimitResult.allowed) {
|
||||
res.setHeader('Retry-After', rateLimitResult.retryAfterSeconds.toString());
|
||||
return res.status(429).json({
|
||||
error: 'Rate limit exceeded. Maximum 5 trigger requests per hour.'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if scraper is already running
|
||||
if (isScraperRunning()) {
|
||||
return res.status(409).json({ error: 'Scrape already in progress' });
|
||||
@ -1318,3 +1374,4 @@ router.get('/scraper/history', async (req, res) => {
|
||||
|
||||
module.exports = router;
|
||||
module.exports.clearStatsCache = clearStatsCache;
|
||||
module.exports.resetRateLimiter = resetRateLimiter;
|
||||
|
||||
Reference in New Issue
Block a user