formies/src/middleware/redisRateLimiter.js
Mohamad.Elsena 2927013a6d Update environment configuration, add API documentation, and implement user authentication system
- Updated `.env` and added `.env.test` for environment variables.
- Introduced API documentation in `API_DOCUMENTATION.md`.
- Added authentication setup guide in `AUTHENTICATION_SETUP.md`.
- Implemented user authentication with JWT and email verification.
- Created new routes for user management and form submissions.
- Added middleware for API key authentication and error handling.
- Set up Redis for rate limiting and notifications.
- Removed obsolete files and configurations related to the previous Rust implementation.
2025-05-28 11:18:35 +02:00

147 lines
3.9 KiB
JavaScript

const rateLimit = require("express-rate-limit");
const RedisStore = require("rate-limit-redis").default;
const { getRedisClient, isRedisConnected } = require("../config/redis");
// Track if we've already logged the fallback warning
let fallbackWarningLogged = false;
// Simple in-memory store as fallback when Redis is not available
class MemoryStore {
constructor() {
this.hits = new Map();
this.resetTime = new Map();
// Clean up old entries periodically to prevent memory leaks
this.cleanupInterval = setInterval(
() => {
const now = Date.now();
for (const [key, resetTime] of this.resetTime.entries()) {
if (now > resetTime) {
this.hits.delete(key);
this.resetTime.delete(key);
}
}
},
5 * 60 * 1000
); // Clean up every 5 minutes
}
async increment(key, windowMs) {
const now = Date.now();
const resetTime = this.resetTime.get(key);
if (!resetTime || now > resetTime) {
this.hits.set(key, 1);
this.resetTime.set(key, now + windowMs);
return { totalHits: 1, timeToExpire: windowMs };
}
const hits = (this.hits.get(key) || 0) + 1;
this.hits.set(key, hits);
return { totalHits: hits, timeToExpire: resetTime - now };
}
async decrement(key) {
const hits = this.hits.get(key) || 0;
if (hits > 0) {
this.hits.set(key, hits - 1);
}
}
async resetKey(key) {
this.hits.delete(key);
this.resetTime.delete(key);
}
}
// Create store based on Redis availability
const createStore = () => {
try {
if (isRedisConnected()) {
const redisClient = getRedisClient();
return new RedisStore({
sendCommand: (...args) => redisClient.sendCommand(args),
});
} else {
throw new Error("Redis not connected");
}
} catch (error) {
// Only log the warning once to avoid spam
if (!fallbackWarningLogged) {
console.warn("Rate limiting: Using in-memory store (Redis unavailable)");
fallbackWarningLogged = true;
}
return new MemoryStore();
}
};
// Create rate limiter for form submissions
const createSubmissionRateLimiter = () => {
return rateLimit({
store: createStore(),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // Limit each IP to 10 requests per windowMs for any form
message: {
error:
"Too many form submissions from this IP address. Please try again later.",
},
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
keyGenerator: (req) => {
// Generate unique key per IP
return `submit_ip:${req.ip}`;
},
skip: (req) => {
// Skip rate limiting for specific conditions if needed
return false;
},
});
};
// Create more restrictive rate limiter for specific form+IP combinations
const createFormSpecificRateLimiter = () => {
return rateLimit({
store: createStore(),
windowMs: 5 * 60 * 1000, // 5 minutes
max: 3, // Limit each IP to 3 requests per 5 minutes per specific form
message: {
error:
"Too many submissions for this form from your IP address. Please try again later.",
},
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => {
// Generate unique key per form+IP combination
const formUuid = req.params.formUuid;
return `submit_form:${formUuid}:${req.ip}`;
},
skip: (req) => {
// Skip rate limiting for specific conditions if needed
return false;
},
});
};
// Create a more aggressive rate limiter for potential abuse
const createStrictRateLimiter = () => {
return rateLimit({
store: createStore(),
windowMs: 60 * 60 * 1000, // 1 hour
max: 50, // Limit each IP to 50 requests per hour across all forms
message: {
error: "Too many requests from this IP address. Please try again later.",
},
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => {
return `strict_ip:${req.ip}`;
},
});
};
module.exports = {
createSubmissionRateLimiter,
createFormSpecificRateLimiter,
createStrictRateLimiter,
};