From a1b2f904126bf1441dfd58486a72b9550157698b Mon Sep 17 00:00:00 2001 From: kibuikaCodes Date: Mon, 27 Mar 2023 17:33:40 +0300 Subject: [PATCH 1/2] revert stupid code --- src/v1/routes/pastPaperRoutes.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/v1/routes/pastPaperRoutes.js b/src/v1/routes/pastPaperRoutes.js index 384d2b3..b5551c6 100644 --- a/src/v1/routes/pastPaperRoutes.js +++ b/src/v1/routes/pastPaperRoutes.js @@ -7,9 +7,9 @@ const { uploadImages } = require('../../middleware/imageUpload'); router.get('/', pastPaperController.fetchAllPapers); router.get('/:pastPaperId', getPastPaper, pastPaperController.fetchSinglePastPaper); -router.post(`/${process.env.ACCESS_CODE}/`, uploadImages.array("images", 10), pastPaperController.addPastPaper); -router.patch(`/${process.env.ACCESS_CODE}/:pastPaperId`, getPastPaper, pastPaperController.updatePastPaper); -router.delete(`/${process.env.ACCESS_CODE}/:pastPaperId`, getPastPaper, pastPaperController.deletePastPaper); -router.post("/gpt", chatGPTPrompt.getAnswers); +router.post(`/`, uploadImages.array("images", 10), pastPaperController.addPastPaper); +router.patch(`/:pastPaperId`, getPastPaper, pastPaperController.updatePastPaper); +router.delete(`/:pastPaperId`, getPastPaper, pastPaperController.deletePastPaper); +router.post(`/gpt`, chatGPTPrompt.getAnswers); module.exports = router; \ No newline at end of file From f1582d580037436ede091e50bc676cae9f626066 Mon Sep 17 00:00:00 2001 From: kibuikaCodes Date: Mon, 27 Mar 2023 18:18:35 +0300 Subject: [PATCH 2/2] :sparkles: add rate limiter --- package-lock.json | 109 ++++++++++++++++++++++++++++++- package.json | 2 + src/app/index.js | 5 +- src/middleware/rateLimiter.js | 67 +++++++++++++++++++ src/v1/routes/pastPaperRoutes.js | 8 +-- 5 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 src/middleware/rateLimiter.js diff --git a/package-lock.json b/package-lock.json index d2aad27..eb072b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,15 +13,17 @@ "cloudinary": "^1.34.0", "cors": "^2.8.5", "dotenv": "^16.0.3", - "env-cmd": "^10.1.0", "express": "^4.18.2", + "moment": "^2.29.4", "mongoose": "^6.7.5", "multer": "^1.4.5-lts.1", "openai": "^3.2.1", + "redis": "^4.6.5", "uuid": "^9.0.0" }, "devDependencies": { "cross-env": "^7.0.3", + "env-cmd": "^10.1.0", "eslint": "^8.35.0", "eslint-config-prettier": "^8.6.0", "nodemon": "^2.0.20", @@ -1247,6 +1249,64 @@ "node": ">= 8" } }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.6.tgz", + "integrity": "sha512-dFD1S6je+A47Lj22jN/upVU2fj4huR7S9APd7/ziUXsIXDL+11GPYti4Suv5y8FuXaN+0ZG4JF+y1houEJ7ToA==", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/@redis/graph": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", + "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz", + "integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.2.tgz", + "integrity": "sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz", + "integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -1705,6 +1765,14 @@ "lodash": ">=4.0" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1738,6 +1806,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, "engines": { "node": ">= 6" } @@ -1843,6 +1912,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1952,6 +2022,7 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-10.1.0.tgz", "integrity": "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==", + "dev": true, "dependencies": { "commander": "^4.0.0", "cross-spawn": "^7.0.0" @@ -2643,6 +2714,14 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "engines": { + "node": ">= 4" + } + }, "node_modules/get-intrinsic": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", @@ -3031,7 +3110,8 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "node_modules/js-sdsl": { "version": "4.3.0", @@ -3220,6 +3300,14 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, "node_modules/mongodb": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.13.0.tgz", @@ -3621,6 +3709,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "engines": { "node": ">=8" } @@ -3841,6 +3930,19 @@ "node": ">=8.10.0" } }, + "node_modules/redis": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.5.tgz", + "integrity": "sha512-O0OWA36gDQbswOdUuAhRL6mTZpHFN525HlgZgDaVNgCJIAZR3ya06NTESb0R+TUZ+BFaDpz6NnnVvoMx9meUFg==", + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.5.6", + "@redis/graph": "1.1.0", + "@redis/json": "1.0.4", + "@redis/search": "1.1.2", + "@redis/time-series": "1.0.4" + } + }, "node_modules/regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", @@ -4006,6 +4108,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -4017,6 +4120,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "engines": { "node": ">=8" } @@ -4405,6 +4509,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, diff --git a/package.json b/package.json index a523e9e..c1e74ab 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,11 @@ "cors": "^2.8.5", "dotenv": "^16.0.3", "express": "^4.18.2", + "moment": "^2.29.4", "mongoose": "^6.7.5", "multer": "^1.4.5-lts.1", "openai": "^3.2.1", + "redis": "^4.6.5", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/src/app/index.js b/src/app/index.js index d286efc..1897c31 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -6,9 +6,10 @@ const mongoose = require("mongoose"); const bodyParser = require("body-parser") const connectDB = require('../config/dbConnection'); -const corsOptions = require('../config/corsOptions'); +// const corsOptions = require('../config/corsOptions'); const checkOrigins = require('../middleware/checkOrigins'); const v1PastPaperRoutes = require('../v1/routes/pastPaperRoutes'); +const { redisRateLimiter } = require('../middleware/rateLimiter'); // Connect to MongoDB connectDB(); @@ -24,6 +25,8 @@ app.use(checkOrigins); // Cross Origin Resource Sharing app.use(cors({ origin: "*" })); +app.use(redisRateLimiter) + // middleware for json app.use(express.json()); diff --git a/src/middleware/rateLimiter.js b/src/middleware/rateLimiter.js new file mode 100644 index 0000000..0365a9a --- /dev/null +++ b/src/middleware/rateLimiter.js @@ -0,0 +1,67 @@ +import moment from "moment"; +import redis from "redis"; + +const redisClient = redis.createClient(); +redisClient.on("error", (err) => console.log("Redis error: ", err)); + +const WINDOW_SIZE_IN_HOURS = 24; +const MAX_WINDOW_REQUEST_COUNT = 100; +const WINDOW_LOG_INTERVAL_IN_HOURS = 1; + +export const redisRateLimiter = async ( req, res, next ) => { + await redisClient.connect(); + try { + // check that redis client exists + if (!redisClient) { + throw new Error("Redis client does not exist!"); + } + const record = await redisClient.get(req.ip); + const currentRequestTime = moment(); + console.log("Current time: ", currentRequestTime.format()); + // if no record exists, create a new record for user + if (record == null) { + let newRecord = []; + let requestLog = { + requestTimeStamp: currentRequestTime.unix(), + requestCount: 1, + } + newRecord.push(requestLog); + await redisClient.set(req.ip, JSON.stringify(newRecord)); + next(); + } + // if record exists, parse data and determine whether user has reached max requests per window + let data = JSON.parse(record); + let windowStartTimestamp = moment().subtract(WINDOW_SIZE_IN_HOURS, "hours").unix(); + let requestsWithinWindow = data.filter(entry => { + return entry.requestTimeStamp > windowStartTimestamp; + }) + console.log("Request data within window: ", requestsWithinWindow); + let totalWindowRequestsCount = requestsWithinWindow.reduce((accumulator, entry) => { + return accumulator + entry.requestCount; + }, 0); + + if (totalWindowRequestsCount >= MAX_WINDOW_REQUEST_COUNT) { + return res.status(429).send("You have exceeded the " + MAX_WINDOW_REQUEST_COUNT + " requests in " + WINDOW_SIZE_IN_HOURS + " hrs limit!"); + } + else { + // if user has not reached max requests per window, increment counter + let lastRequestLog = data[data.length - 1]; + let potentialCurrentWindowIntervalStartTimeStamp = currentRequestTime.subtract(WINDOW_LOG_INTERVAL_IN_HOURS, "hours").unix(); + if (lastRequestLog.requestTimeStamp < potentialCurrentWindowIntervalStartTimeStamp) { + let requestLog = { + requestTimeStamp: currentRequestTime.unix(), + requestCount: 1, + } + data.push(requestLog); + } + else { + lastRequestLog.requestCount++; + } + await redisClient.set(req.ip, JSON.stringify(data)); + next(); + } + + } catch(err) { + next(err) + } +} \ No newline at end of file diff --git a/src/v1/routes/pastPaperRoutes.js b/src/v1/routes/pastPaperRoutes.js index b5551c6..1ebb4af 100644 --- a/src/v1/routes/pastPaperRoutes.js +++ b/src/v1/routes/pastPaperRoutes.js @@ -7,9 +7,9 @@ const { uploadImages } = require('../../middleware/imageUpload'); router.get('/', pastPaperController.fetchAllPapers); router.get('/:pastPaperId', getPastPaper, pastPaperController.fetchSinglePastPaper); -router.post(`/`, uploadImages.array("images", 10), pastPaperController.addPastPaper); -router.patch(`/:pastPaperId`, getPastPaper, pastPaperController.updatePastPaper); -router.delete(`/:pastPaperId`, getPastPaper, pastPaperController.deletePastPaper); -router.post(`/gpt`, chatGPTPrompt.getAnswers); +router.post("/", uploadImages.array("images", 10), pastPaperController.addPastPaper); +router.patch("/:pastPaperId", getPastPaper, pastPaperController.updatePastPaper); +router.delete("/:pastPaperId", getPastPaper, pastPaperController.deletePastPaper); +router.post("/gpt", chatGPTPrompt.getAnswers); module.exports = router; \ No newline at end of file