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
60 lines
1.4 KiB
JavaScript
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,
|
|
};
|