diff --git a/server/index.js b/server/index.js index f5cc645..10a19cd 100644 --- a/server/index.js +++ b/server/index.js @@ -18,7 +18,14 @@ import feedbackRoutes from "./routes/feedback.js"; import pdfChatRoutes from "./routes/pdfChat.js"; import sitemapRoutes from "./routes/sitemap.js"; import { setupSocketHandlers } from "./socket/socketHandlers.js"; -import initRedis from "./utils/redis.js"; +import { + generalLimiter, + authLimiter, + uploadLimiter, + discussionLimiter, + chatLimiter, + strictAuthLimiter, +} from "./middleware/rateLimiter.js"; BigInt.prototype.toJSON = function () { return this.toString(); @@ -27,14 +34,6 @@ BigInt.prototype.toJSON = function () { // Load environment variables dotenv.config(); -// Initialize Redis (optional - will work without it) -initRedis().catch((err) => { - console.log( - "⚠️ Redis not available, continuing without cache:", - err.message - ); -}); - const app = express(); const server = createServer(app); @@ -78,6 +77,21 @@ app.use(express.urlencoded({ extended: true, limit: "50mb" })); // Initialize Passport app.use(passport.initialize()); +// Rate Limiting Middleware +app.use("/api/", generalLimiter); +app.use("/api/auth/login", authLimiter); +app.use("/api/auth/send-otp", authLimiter); +app.use("/api/auth/forgot-password", strictAuthLimiter); +app.use("/api/auth/reset-password", strictAuthLimiter); +app.use("/api/upload", uploadLimiter); +app.use("/api/discussions", (req, res, next) => { + if (["POST"].includes(req.method)) { + return discussionLimiter(req, res, next); + } + next(); +}); +app.use("/api/pdf-chat", chatLimiter); + // Debug middleware to log all requests app.use((req, res, next) => { console.log(`🔍 ${req.method} ${req.path}`); @@ -107,7 +121,7 @@ app.use("/", sitemapRoutes); // Setup Socket.IO handlers setupSocketHandlers(io); -// Health check endpoint +// Health check endpoint (not rate limited) app.get("/api/health", async (req, res) => { try { // Test database connection diff --git a/server/middleware/rateLimiter.js b/server/middleware/rateLimiter.js new file mode 100644 index 0000000..c146970 --- /dev/null +++ b/server/middleware/rateLimiter.js @@ -0,0 +1,159 @@ +import rateLimit from "express-rate-limit"; + +// General API Rate Limit: 500 requests per 15 minutes per IP +export const generalLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 500, + message: { + error: "Too many requests from this IP, please try again later.", + retryAfter: "15 minutes", + }, + standardHeaders: true, + legacyHeaders: false, + skip: (req) => req.path === "/api/health", + keyGenerator: (req) => + req.headers["x-forwarded-for"]?.split(",")[0] || + req.socket.remoteAddress || + "unknown", +}); + +// Authentication Rate Limit: 5 login attempts per 15 minutes per IP +export const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 5, + message: { + error: "Too many login attempts, please try again after 15 minutes.", + retryAfter: "15 minutes", + }, + standardHeaders: true, + legacyHeaders: false, + skip: (req) => req.path.includes("signup") && req.method === "POST", + keyGenerator: (req) => + req.headers["x-forwarded-for"]?.split(",")[0] || + req.socket.remoteAddress || + "unknown", +}); + +// File Upload Rate Limit: 10 uploads per hour per authenticated user +export const uploadLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, + max: 10, + message: { + error: "Upload limit exceeded. Maximum 10 files per hour.", + retryAfter: "1 hour", + }, + standardHeaders: true, + legacyHeaders: false, + keyGenerator: (req) => { + if (req.user && req.user.id) { + return `upload_${req.user.id}`; + } + return `upload_${ + req.headers["x-forwarded-for"]?.split(",")[0] || + req.socket.remoteAddress || + "unknown" + }`; + }, + handler: (req, res) => { + res.status(429).json({ + error: "Upload limit exceeded. Maximum 10 files per hour.", + retryAfter: "1 hour", + }); + }, +}); + +// Discussion/Comment Rate Limit: 20 posts per hour per authenticated user +export const discussionLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, + max: 20, + message: { + error: "Discussion posting limit exceeded. Maximum 20 posts per hour.", + retryAfter: "1 hour", + }, + standardHeaders: true, + legacyHeaders: false, + keyGenerator: (req) => { + if (req.user && req.user.id) { + return `discussion_${req.user.id}`; + } + return `discussion_${ + req.headers["x-forwarded-for"]?.split(",")[0] || + req.socket.remoteAddress || + "unknown" + }`; + }, + handler: (req, res) => { + res.status(429).json({ + error: "Discussion posting limit exceeded. Maximum 20 posts per hour.", + retryAfter: "1 hour", + }); + }, +}); + +// AI Chat Rate Limit: 30 requests per hour per authenticated user +export const chatLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, + max: 30, + message: { + error: "Chat limit exceeded. Maximum 30 messages per hour.", + retryAfter: "1 hour", + }, + standardHeaders: true, + legacyHeaders: false, + keyGenerator: (req) => { + if (req.user && req.user.id) { + return `chat_${req.user.id}`; + } + return `chat_${ + req.headers["x-forwarded-for"]?.split(",")[0] || + req.socket.remoteAddress || + "unknown" + }`; + }, + handler: (req, res) => { + res.status(429).json({ + error: "Chat limit exceeded. Maximum 30 messages per hour.", + retryAfter: "1 hour", + }); + }, +}); + +// Password Reset Rate Limit: 3 attempts per hour per email/IP +export const strictAuthLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, + max: 3, + message: { + error: "Too many password reset attempts. Please try again after 1 hour.", + retryAfter: "1 hour", + }, + standardHeaders: true, + legacyHeaders: false, + keyGenerator: (req) => { + const email = req.body?.email || req.query?.email; + if (email) { + return `passwordreset_${email.toLowerCase()}`; + } + return `passwordreset_${ + req.headers["x-forwarded-for"]?.split(",")[0] || + req.socket.remoteAddress || + "unknown" + }`; + }, + handler: (req, res) => { + res.status(429).json({ + error: "Too many password reset attempts. Please try again after 1 hour.", + retryAfter: "1 hour", + }); + }, +}); + +// Custom rate limiter factory for fine-grained control +export const createCustomLimiter = (options = {}) => { + const defaults = { + windowMs: 15 * 60 * 1000, + max: 500, + standardHeaders: true, + legacyHeaders: false, + }; + return rateLimit({ ...defaults, ...options }); +}; diff --git a/server/package.json b/server/package.json index df81ad1..73ebd5f 100644 --- a/server/package.json +++ b/server/package.json @@ -22,6 +22,7 @@ "cors": "^2.8.5", "dotenv": "^17.2.1", "express": "^4.18.2", + "express-rate-limit": "^7.4.0", "groq-sdk": "^0.30.0", "jsonwebtoken": "^9.0.2", "mongodb": "^6.20.0",