From 95bb696bd57f6a0e1af627d01bb6536463525bea Mon Sep 17 00:00:00 2001 From: satyam-code45 Date: Wed, 15 Oct 2025 15:06:23 +0530 Subject: [PATCH] feat: add simple rate limiter --- .env.example | 4 ++++ package-lock.json | 8 +++++++- package.json | 1 + src/app.js | 8 ++++---- src/config/rateLimiter.js | 2 ++ src/middlewares/rateLimiter.js | 21 +++++++++++++++++++++ src/models/{Role.model.js => role.model.js} | 0 src/services/authService.js | 16 ++++++++++------ 8 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 .env.example create mode 100644 src/config/rateLimiter.js create mode 100644 src/middlewares/rateLimiter.js rename src/models/{Role.model.js => role.model.js} (100%) diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1009c75 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# Example environment variables for RBAC app +PORT=5000 +MONGO_URI=mongodb://root:admin@localhost:27017/ +JWT_SECRET=your_jwt_secret_here diff --git a/package-lock.json b/package-lock.json index 817af0f..438e40e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "RBAC-1", + "name": "RBAC", "lockfileVersion": 3, "requires": true, "packages": { @@ -10,6 +10,7 @@ "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", + "express-rate-limiter": "^1.3.1", "jsonwebtoken": "^9.0.2", "mongoose": "^8.19.1", "nodemon": "^3.1.10", @@ -1730,6 +1731,11 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limiter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limiter/-/express-rate-limiter-1.3.1.tgz", + "integrity": "sha512-qLRc4ZkyCcfUCjPtVjwQOtf4OYPc7hc6ObOFemeeVYLlbam541/B7R33VvhztFsBGRUIT/wJW/oJz8n5k+fRfw==" + }, "node_modules/express/node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", diff --git a/package.json b/package.json index e9d7631..e99ea7d 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", + "express-rate-limiter": "^1.3.1", "jsonwebtoken": "^9.0.2", "mongoose": "^8.19.1", "nodemon": "^3.1.10", diff --git a/src/app.js b/src/app.js index b2d9ff5..c87bed1 100644 --- a/src/app.js +++ b/src/app.js @@ -2,19 +2,19 @@ import express from 'express'; import cors from 'cors'; import cookieparser from 'cookie-parser'; import authRoutes from './routes/authRoutes.js'; +import rateLimiter from './middlewares/rateLimiter.js'; const app = express(); -app.use(cors({ - origin: process.env.CORS_URL, - credentials: true -})); +app.use(cors()); app.use(express.json({ limit: '16kb' })); app.use(express.urlencoded({ extended: true, limit: '16kb' })); app.use(express.static('public')); app.use(cookieparser()); +app.use(rateLimiter); + // Routes app.use('/api/auth', authRoutes); diff --git a/src/config/rateLimiter.js b/src/config/rateLimiter.js new file mode 100644 index 0000000..2a572ea --- /dev/null +++ b/src/config/rateLimiter.js @@ -0,0 +1,2 @@ +export const WINDOW_MS = 60 * 1000; // 1 minute +export const MAX_REQUESTS = 10; // 10 requests per IP per minute \ No newline at end of file diff --git a/src/middlewares/rateLimiter.js b/src/middlewares/rateLimiter.js new file mode 100644 index 0000000..3386578 --- /dev/null +++ b/src/middlewares/rateLimiter.js @@ -0,0 +1,21 @@ +import { MAX_REQUESTS, WINDOW_MS } from '../config/rateLimiter.js'; + +// Basic in-memory store +const requests = {}; + +export default function rateLimiter(req, res, next) { + const now = Date.now(); + const ip = req.ip; + + if (!requests[ip]) requests[ip] = []; + + // Remove timestamps older than 1 minute + requests[ip] = requests[ip].filter(ts => now - ts < WINDOW_MS); + + if (requests[ip].length >= MAX_REQUESTS) { + return res.status(429).send('Too many requests, please try again later.'); + } + + requests[ip].push(now); + next(); +} diff --git a/src/models/Role.model.js b/src/models/role.model.js similarity index 100% rename from src/models/Role.model.js rename to src/models/role.model.js diff --git a/src/services/authService.js b/src/services/authService.js index 3f462dc..435041b 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -3,14 +3,19 @@ import Role from '../models/role.model.js'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; -const signAccessToken = (payload) => { +const signAccessToken = payload => { if (!process.env.JWT_SECRET) { throw new Error('JWT_SECRET not set in environment'); } return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1d' }); }; -export const registerUserService = async ({ username, email, fullname, password }) => { +export const registerUserService = async ({ + username, + email, + fullname, + password, +}) => { if (!username || !email || !password || !fullname) { throw new Error('All fields are required'); } @@ -26,7 +31,7 @@ export const registerUserService = async ({ username, email, fullname, password } const salt = await bcrypt.genSalt(10); - const hashedPassword = await bcrypt.hash(password,salt); + const hashedPassword = await bcrypt.hash(password, salt); const newUser = await User.create({ username, @@ -44,7 +49,6 @@ export const registerUserService = async ({ username, email, fullname, password }; }; - export const loginUserService = async ({ email, password }) => { if (!email || !password) { const err = new Error('Email and password are required'); @@ -54,7 +58,7 @@ export const loginUserService = async ({ email, password }) => { const user = await User.findOne({ email }).populate('role'); if (!user) { - const err = new Error('Invalid credentials'); + const err = new Error('Invalid credentials'); err.statusCode = 401; throw err; } @@ -84,4 +88,4 @@ export const loginUserService = async ({ email, password }) => { role: roleName, }, }; -}; \ No newline at end of file +};