costco-grocery-list/backend/middleware/rate-limit.js
Nico 77ae5be445
All checks were successful
Build & Deploy Costco Grocery List / build (push) Successful in 1m10s
Build & Deploy Costco Grocery List / verify-images (push) Successful in 3s
Build & Deploy Costco Grocery List / deploy (push) Successful in 11s
Build & Deploy Costco Grocery List / notify (push) Successful in 1s
refactor
2026-02-22 01:27:03 -08:00

60 lines
1.4 KiB
JavaScript

const { sendError } = require("../utils/http");
const buckets = new Map();
function pruneExpired(now) {
for (const [key, value] of buckets.entries()) {
if (value.resetAt <= now) {
buckets.delete(key);
}
}
}
function getClientIp(req) {
const forwardedFor = req.headers["x-forwarded-for"];
if (typeof forwardedFor === "string" && forwardedFor.trim()) {
return forwardedFor.split(",")[0].trim();
}
return req.ip || req.socket?.remoteAddress || "unknown";
}
function createRateLimit({ keyPrefix, windowMs, max, message, keyFn }) {
return (req, res, next) => {
const now = Date.now();
if (buckets.size > 5000) {
pruneExpired(now);
}
const suffix = typeof keyFn === "function" ? keyFn(req) : getClientIp(req);
const key = `${keyPrefix}:${suffix || "unknown"}`;
const existing = buckets.get(key);
const bucket =
!existing || existing.resetAt <= now
? { count: 0, resetAt: now + windowMs }
: existing;
bucket.count += 1;
buckets.set(key, bucket);
if (bucket.count > max) {
const retryAfterSeconds = Math.max(
1,
Math.ceil((bucket.resetAt - now) / 1000)
);
res.setHeader("Retry-After", String(retryAfterSeconds));
return sendError(
res,
429,
message || "Too many requests. Please try again later."
);
}
return next();
};
}
module.exports = {
createRateLimit,
};