97 lines
2.8 KiB
JavaScript
97 lines
2.8 KiB
JavaScript
// src/middleware/adminRateLimit.js
|
|
// === ADMIN SECURITY VISUALIZER ===
|
|
// Light rate limiting for admin endpoints
|
|
// Prevents abuse while allowing legitimate admin access
|
|
|
|
const { getRedisClient, isRedisReady } = require('../services/redisClient');
|
|
|
|
// In-memory fallback store
|
|
const memoryStore = {};
|
|
|
|
// Clean up expired entries
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
Object.keys(memoryStore).forEach((key) => {
|
|
if (memoryStore[key].expiresAt && memoryStore[key].expiresAt < now) {
|
|
delete memoryStore[key];
|
|
}
|
|
});
|
|
}, 60000);
|
|
|
|
// Configuration: 100 requests per 15 minutes per user
|
|
const ADMIN_RATE_LIMIT = {
|
|
maxRequests: parseInt(process.env.ADMIN_RATE_LIMIT_MAX || '100', 10),
|
|
windowSeconds: parseInt(process.env.ADMIN_RATE_LIMIT_WINDOW || '900', 10), // 15 minutes
|
|
};
|
|
|
|
/**
|
|
* Admin rate limiting middleware
|
|
* Limits admin API requests per user
|
|
*/
|
|
async function adminRateLimit(req, res, next) {
|
|
try {
|
|
const userId = req.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
}
|
|
|
|
const key = `admin_rate_limit:${userId}`;
|
|
const redis = await getRedisClient();
|
|
|
|
let count;
|
|
|
|
if (isRedisReady() && redis) {
|
|
try {
|
|
count = await redis.incr(key);
|
|
if (count === 1) {
|
|
await redis.expire(key, ADMIN_RATE_LIMIT.windowSeconds);
|
|
}
|
|
} catch (err) {
|
|
console.error('Redis error in admin rate limit, falling back to memory:', err);
|
|
// Fall through to memory store
|
|
count = (memoryStore[key]?.count || 0) + 1;
|
|
memoryStore[key] = {
|
|
count,
|
|
expiresAt: Date.now() + (ADMIN_RATE_LIMIT.windowSeconds * 1000),
|
|
};
|
|
}
|
|
} else {
|
|
// Memory store fallback
|
|
const stored = memoryStore[key];
|
|
if (stored && stored.expiresAt > Date.now()) {
|
|
count = stored.count + 1;
|
|
stored.count = count;
|
|
} else {
|
|
count = 1;
|
|
memoryStore[key] = {
|
|
count: 1,
|
|
expiresAt: Date.now() + (ADMIN_RATE_LIMIT.windowSeconds * 1000),
|
|
};
|
|
}
|
|
}
|
|
|
|
// Check if limit exceeded
|
|
if (count > ADMIN_RATE_LIMIT.maxRequests) {
|
|
return res.status(429).json({
|
|
error: 'Too many requests',
|
|
message: 'Rate limit exceeded. Please try again later.',
|
|
retry_after: ADMIN_RATE_LIMIT.windowSeconds,
|
|
});
|
|
}
|
|
|
|
// Add rate limit headers
|
|
res.setHeader('X-RateLimit-Limit', ADMIN_RATE_LIMIT.maxRequests);
|
|
res.setHeader('X-RateLimit-Remaining', Math.max(0, ADMIN_RATE_LIMIT.maxRequests - count));
|
|
res.setHeader('X-RateLimit-Reset', new Date(Date.now() + (ADMIN_RATE_LIMIT.windowSeconds * 1000)).toISOString());
|
|
|
|
next();
|
|
} catch (err) {
|
|
console.error('Admin rate limit error:', err);
|
|
// On error, allow the request (fail open for admin access)
|
|
next();
|
|
}
|
|
}
|
|
|
|
module.exports = adminRateLimit;
|
|
|