
- 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.
147 lines
3.9 KiB
JavaScript
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,
|
|
};
|