From 8e3d7dc5e3749143c40d4416a73668b25d1d0ee2 Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Thu, 17 Jul 2025 16:46:08 -0400 Subject: [PATCH 01/15] add express-rate-limit dependency --- package-lock.json | 28 ++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 29 insertions(+) diff --git a/package-lock.json b/package-lock.json index eec3cd5..49107e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.21.2", + "express-rate-limit": "^8.0.1", "helmet": "^7.0.0", "joi": "^17.9.2", "jsonwebtoken": "^9.0.2", @@ -2528,6 +2529,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.0.1.tgz", + "integrity": "sha512-aZVCnybn7TVmxO4BtlmnvX+nuz8qHW124KKJ8dumsBsmv5ZLxE0pYu7S2nwyRBGHHCAzdmnGyrc5U/rksSPO7Q==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3098,6 +3117,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/package.json b/package.json index 9f6c1ff..c42b534 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.21.2", + "express-rate-limit": "^8.0.1", "helmet": "^7.0.0", "joi": "^17.9.2", "jsonwebtoken": "^9.0.2", From 46b890cd4bbe08529f06dbcba906d173865b4826 Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Thu, 17 Jul 2025 16:47:47 -0400 Subject: [PATCH 02/15] fix #10 --- routes/user.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/routes/user.js b/routes/user.js index 462c321..151064a 100644 --- a/routes/user.js +++ b/routes/user.js @@ -17,6 +17,7 @@ const multer = require('multer'); const path = require('path'); const fs = require('fs'); const { v4: uuidv4 } = require('uuid'); +const rateLimit = require('express-rate-limit'); const { createUser, getUserByEmail, @@ -187,6 +188,21 @@ const formatUserResponse = (user) => { }; }; +// ======================================== +// RATE LIMITERS +// ======================================== + +// Rate limiter for sensitive endpoints (e.g., password change) +const passwordChangeLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, // limit each IP to 5 requests per windowMs + message: { + success: false, + error: 'Too Many Requests', + message: 'Too many password change attempts from this IP, please try again later.' + } +}); + // ======================================== // ROUTES // ======================================== @@ -565,7 +581,7 @@ router.put('/stats', authenticateToken, async (req, res) => { * * Changes the authenticated user's password */ -router.put('/password', authenticateToken, async (req, res) => { +router.put('/password', authenticateToken, passwordChangeLimiter, async (req, res) => { console.log(TAG, 'PUT /api/user/password - Password change requested'); try { // Validate request body From 5b3f46b21b0ea86ac7973a9f188366a7b005961a Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Thu, 17 Jul 2025 16:57:10 -0400 Subject: [PATCH 03/15] fix #13 #12 #11 #9 #8 #6 --- routes/user.js | 78 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/routes/user.js b/routes/user.js index 151064a..4831e51 100644 --- a/routes/user.js +++ b/routes/user.js @@ -203,6 +203,61 @@ const passwordChangeLimiter = rateLimit({ } }); +// Rate limiter for profile endpoint +const profileLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 30, // limit each IP to 30 requests per windowMs + message: { + success: false, + error: 'Too Many Requests', + message: 'Too many profile requests from this IP, please try again later.' + } +}); + +// Rate limiter for profile update endpoint +const profileUpdateLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 10, // limit each IP to 10 requests per windowMs + message: { + success: false, + error: 'Too Many Requests', + message: 'Too many profile update attempts from this IP, please try again later.' + } +}); + +// Rate limiter for stats update endpoint +const statsUpdateLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 20, // limit each IP to 20 requests per windowMs + message: { + success: false, + error: 'Too Many Requests', + message: 'Too many stats update attempts from this IP, please try again later.' + } +}); + +// Rate limiter for profile image upload endpoint +const profileImageLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, // limit each IP to 5 requests per windowMs + message: { + success: false, + error: 'Too Many Requests', + message: 'Too many profile image upload attempts from this IP, please try again later.' + } +}); + +// Rate limiter for status endpoint +const statusLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: { + success: false, + error: 'Too Many Requests', + message: 'Too many status requests from this IP, please try again later.' + } +}); + // ======================================== // ROUTES // ======================================== @@ -405,7 +460,7 @@ router.post('/login', async (req, res) => { * * Gets current user's profile */ -router.get('/profile', authenticateToken, async (req, res) => { +router.get('/profile', authenticateToken, profileLimiter, async (req, res) => { console.log(TAG, 'GET /api/user/profile - Profile requested'); try { @@ -459,7 +514,7 @@ router.get('/profile', authenticateToken, async (req, res) => { * * Updates user profile */ -router.put('/profile', authenticateToken, async (req, res) => { +router.put('/profile', authenticateToken, profileUpdateLimiter, async (req, res) => { console.log(TAG, 'PUT /api/user/profile - Profile update requested'); try { @@ -520,7 +575,7 @@ router.put('/profile', authenticateToken, async (req, res) => { * * Updates user statistics */ -router.put('/stats', authenticateToken, async (req, res) => { +router.put('/stats', authenticateToken, statsUpdateLimiter, async (req, res) => { console.log(TAG, 'PUT /api/user/stats - Stats update requested'); try { @@ -645,7 +700,7 @@ router.put('/password', authenticateToken, passwordChangeLimiter, async (req, re * * Uploads a profile image for the authenticated user */ -router.post('/profile-image', authenticateToken, upload.single('image'), async (req, res) => { +router.post('/profile-image', authenticateToken, profileImageLimiter, upload.single('image'), async (req, res) => { console.log(TAG, 'POST /api/user/profile-image - Profile image upload requested'); try { @@ -685,9 +740,16 @@ router.post('/profile-image', authenticateToken, upload.single('image'), async ( // Clean up uploaded file if there was an error if (req.file) { - fs.unlink(req.file.path, (err) => { - if (err) console.error(TAG, 'Error deleting uploaded file:', err); - }); + // Sanitize and validate the file path before deleting + const uploadsRoot = path.resolve(__dirname, '../uploads'); + const filePath = path.resolve(uploadsRoot, path.basename(req.file.filename)); + if (filePath.startsWith(uploadsRoot)) { + fs.unlink(filePath, (err) => { + if (err) console.error(TAG, 'Error deleting uploaded file:', err); + }); + } else { + console.error(TAG, 'Attempted to delete file outside uploads directory:', filePath); + } } res.status(500).json({ @@ -702,7 +764,7 @@ router.post('/profile-image', authenticateToken, upload.single('image'), async ( * * Returns the status of the user service */ -router.get('/status', async (req, res) => { +router.get('/status', statusLimiter, async (req, res) => { console.log(TAG, 'GET /api/user/status - Status check requested'); try { From 261fae51e451148e75ca59e77e9a6397618b8b81 Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Thu, 17 Jul 2025 21:58:48 -0400 Subject: [PATCH 04/15] address copilot comments --- routes/user.js | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/routes/user.js b/routes/user.js index 4831e51..f285ebf 100644 --- a/routes/user.js +++ b/routes/user.js @@ -258,6 +258,28 @@ const statusLimiter = rateLimit({ } }); +// Rate limiter for signup endpoint +const signupLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, // limit each IP to 5 signup attempts per windowMs + message: { + success: false, + error: 'Too Many Requests', + message: 'Too many signup attempts from this IP, please try again later.' + } +}); + +// Rate limiter for login endpoint +const loginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 10, // limit each IP to 10 login attempts per windowMs + message: { + success: false, + error: 'Too Many Requests', + message: 'Too many login attempts from this IP, please try again later.' + } +}); + // ======================================== // ROUTES // ======================================== @@ -267,7 +289,7 @@ const statusLimiter = rateLimit({ * * Creates a new user account */ -router.post('/signup', async (req, res) => { +router.post('/signup', signupLimiter, async (req, res) => { console.log(TAG, 'POST /api/user/signup - User signup requested'); try { @@ -364,7 +386,7 @@ router.post('/signup', async (req, res) => { * * Authenticates user and returns token */ -router.post('/login', async (req, res) => { +router.post('/login', loginLimiter, async (req, res) => { console.log(TAG, 'POST /api/user/login - User login requested'); try { From 683e06882ace4d60adb54cd79ac55a16584d4ce7 Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Thu, 17 Jul 2025 22:10:58 -0400 Subject: [PATCH 05/15] address Copilot comments and correct ordering of rate limiter and add limits to all endpoints --- routes/chat.js | 70 ++++++++++++++++++++++++++++++++++++++++++++++---- routes/user.js | 10 ++++---- 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/routes/chat.js b/routes/chat.js index 018bea8..f846ed9 100644 --- a/routes/chat.js +++ b/routes/chat.js @@ -13,6 +13,7 @@ const express = require('express'); const Joi = require('joi'); const { v4: uuidv4 } = require('uuid'); +const rateLimit = require('express-rate-limit'); const { addImagesToTrips } = require('../services/unsplash'); const { getTrips, @@ -34,6 +35,65 @@ const router = express.Router(); // ======================================== const TAG = "[ChatRoutes]"; +// ======================================== +// RATE LIMITERS +// ======================================== + +// Rate limiter for trip listing endpoint +const tripListLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 50, // limit each IP to 50 requests per windowMs + message: { + success: false, + error: 'Too Many Requests', + message: 'Too many trip list requests from this IP, please try again later.' + } +}); + +// Rate limiter for individual trip retrieval endpoint +const tripGetLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: { + success: false, + error: 'Too Many Requests', + message: 'Too many trip retrieval requests from this IP, please try again later.' + } +}); + +// Rate limiter for trip deletion endpoint +const tripDeleteLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 20, // limit each IP to 20 deletion requests per windowMs + message: { + success: false, + error: 'Too Many Requests', + message: 'Too many trip deletion requests from this IP, please try again later.' + } +}); + +// Rate limiter for audit logs endpoint +const auditLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 20, // limit each IP to 20 audit requests per windowMs + message: { + success: false, + error: 'Too Many Requests', + message: 'Too many audit log requests from this IP, please try again later.' + } +}); + +// Rate limiter for status endpoint +const statusLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 status requests per windowMs + message: { + success: false, + error: 'Too Many Requests', + message: 'Too many status requests from this IP, please try again later.' + } +}); + // ======================================== // VALIDATION SCHEMAS // ======================================== @@ -128,7 +188,7 @@ const logRequestDetails = (req, action) => { * Retrieves trip history with optional filtering, sorting, and pagination. * Uses optional authentication - shows user's trips if authenticated, public trips if not. */ -router.get('/', optionalAuth, async (req, res) => { +router.get('/', optionalAuth, tripListLimiter, async (req, res) => { console.log(TAG, 'GET /api/chat - Trip history requested'); try { @@ -214,7 +274,7 @@ router.get('/', optionalAuth, async (req, res) => { * * Returns audit logs for chat operations (admin access recommended) */ -router.get('/audit', authenticateToken, async (req, res) => { +router.get('/audit', auditLimiter, authenticateToken, async (req, res) => { try { console.log(TAG, 'GET /api/chat/audit - Audit logs requested by user:', req.userId); @@ -318,7 +378,7 @@ router.get('/audit', authenticateToken, async (req, res) => { * * Returns the status of the chat service */ -router.get('/status', async (req, res) => { +router.get('/status', statusLimiter, async (req, res) => { console.log(TAG, 'GET /api/chat/status - Status check requested'); try { @@ -370,7 +430,7 @@ router.get('/status', async (req, res) => { * Retrieves a specific trip planning conversation by ID. * Uses optional authentication to verify ownership of private trips. */ -router.get('/:chatId', optionalAuth, async (req, res) => { +router.get('/:chatId', optionalAuth, tripGetLimiter, async (req, res) => { console.log(TAG, 'GET /api/chat/:chatId - Specific chat requested'); try { @@ -487,7 +547,7 @@ router.get('/:chatId', optionalAuth, async (req, res) => { * Deletes a specific chat conversation. * Uses optional authentication to verify ownership before deletion. */ -router.delete('/:chatId', optionalAuth, async (req, res) => { +router.delete('/:chatId', optionalAuth, tripDeleteLimiter, async (req, res) => { console.log(TAG, 'DELETE /api/chat/:chatId - Chat deletion requested'); try { diff --git a/routes/user.js b/routes/user.js index f285ebf..be04225 100644 --- a/routes/user.js +++ b/routes/user.js @@ -482,7 +482,7 @@ router.post('/login', loginLimiter, async (req, res) => { * * Gets current user's profile */ -router.get('/profile', authenticateToken, profileLimiter, async (req, res) => { +router.get('/profile', profileLimiter, authenticateToken, async (req, res) => { console.log(TAG, 'GET /api/user/profile - Profile requested'); try { @@ -536,7 +536,7 @@ router.get('/profile', authenticateToken, profileLimiter, async (req, res) => { * * Updates user profile */ -router.put('/profile', authenticateToken, profileUpdateLimiter, async (req, res) => { +router.put('/profile', profileUpdateLimiter, authenticateToken, async (req, res) => { console.log(TAG, 'PUT /api/user/profile - Profile update requested'); try { @@ -597,7 +597,7 @@ router.put('/profile', authenticateToken, profileUpdateLimiter, async (req, res) * * Updates user statistics */ -router.put('/stats', authenticateToken, statsUpdateLimiter, async (req, res) => { +router.put('/stats', statsUpdateLimiter, authenticateToken, async (req, res) => { console.log(TAG, 'PUT /api/user/stats - Stats update requested'); try { @@ -658,7 +658,7 @@ router.put('/stats', authenticateToken, statsUpdateLimiter, async (req, res) => * * Changes the authenticated user's password */ -router.put('/password', authenticateToken, passwordChangeLimiter, async (req, res) => { +router.put('/password', passwordChangeLimiter, authenticateToken, async (req, res) => { console.log(TAG, 'PUT /api/user/password - Password change requested'); try { // Validate request body @@ -722,7 +722,7 @@ router.put('/password', authenticateToken, passwordChangeLimiter, async (req, re * * Uploads a profile image for the authenticated user */ -router.post('/profile-image', authenticateToken, profileImageLimiter, upload.single('image'), async (req, res) => { +router.post('/profile-image', profileImageLimiter, authenticateToken, upload.single('image'), async (req, res) => { console.log(TAG, 'POST /api/user/profile-image - Profile image upload requested'); try { From fd86c2dbd822ddc3658d185fa3957e6a7982410a Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Thu, 17 Jul 2025 22:13:50 -0400 Subject: [PATCH 06/15] correct limiter ordering for optionalAuth --- routes/chat.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/routes/chat.js b/routes/chat.js index f846ed9..c62f29c 100644 --- a/routes/chat.js +++ b/routes/chat.js @@ -188,7 +188,7 @@ const logRequestDetails = (req, action) => { * Retrieves trip history with optional filtering, sorting, and pagination. * Uses optional authentication - shows user's trips if authenticated, public trips if not. */ -router.get('/', optionalAuth, tripListLimiter, async (req, res) => { +router.get('/', tripListLimiter, optionalAuth, async (req, res) => { console.log(TAG, 'GET /api/chat - Trip history requested'); try { @@ -430,7 +430,7 @@ router.get('/status', statusLimiter, async (req, res) => { * Retrieves a specific trip planning conversation by ID. * Uses optional authentication to verify ownership of private trips. */ -router.get('/:chatId', optionalAuth, tripGetLimiter, async (req, res) => { +router.get('/:chatId', tripGetLimiter, optionalAuth, async (req, res) => { console.log(TAG, 'GET /api/chat/:chatId - Specific chat requested'); try { @@ -547,7 +547,7 @@ router.get('/:chatId', optionalAuth, tripGetLimiter, async (req, res) => { * Deletes a specific chat conversation. * Uses optional authentication to verify ownership before deletion. */ -router.delete('/:chatId', optionalAuth, tripDeleteLimiter, async (req, res) => { +router.delete('/:chatId', tripDeleteLimiter, optionalAuth, async (req, res) => { console.log(TAG, 'DELETE /api/chat/:chatId - Chat deletion requested'); try { From 940790f66bb6d646292dcaeff4f0370403686989 Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Thu, 17 Jul 2025 22:21:33 -0400 Subject: [PATCH 07/15] address more Copilot comments --- routes/chat.js | 9 +++------ routes/user.js | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/routes/chat.js b/routes/chat.js index c62f29c..b38d54e 100644 --- a/routes/chat.js +++ b/routes/chat.js @@ -12,14 +12,11 @@ // ======================================== const express = require('express'); const Joi = require('joi'); -const { v4: uuidv4 } = require('uuid'); const rateLimit = require('express-rate-limit'); const { addImagesToTrips } = require('../services/unsplash'); const { getTrips, getTripById, - createTrip, - updateTrip, deleteTrip, pool } = require('../services/database'); @@ -188,7 +185,7 @@ const logRequestDetails = (req, action) => { * Retrieves trip history with optional filtering, sorting, and pagination. * Uses optional authentication - shows user's trips if authenticated, public trips if not. */ -router.get('/', tripListLimiter, optionalAuth, async (req, res) => { +router.get('/', optionalAuth, tripListLimiter, async (req, res) => { console.log(TAG, 'GET /api/chat - Trip history requested'); try { @@ -430,7 +427,7 @@ router.get('/status', statusLimiter, async (req, res) => { * Retrieves a specific trip planning conversation by ID. * Uses optional authentication to verify ownership of private trips. */ -router.get('/:chatId', tripGetLimiter, optionalAuth, async (req, res) => { +router.get('/:chatId', optionalAuth, tripGetLimiter, async (req, res) => { console.log(TAG, 'GET /api/chat/:chatId - Specific chat requested'); try { @@ -547,7 +544,7 @@ router.get('/:chatId', tripGetLimiter, optionalAuth, async (req, res) => { * Deletes a specific chat conversation. * Uses optional authentication to verify ownership before deletion. */ -router.delete('/:chatId', tripDeleteLimiter, optionalAuth, async (req, res) => { +router.delete('/:chatId', optionalAuth, tripDeleteLimiter, async (req, res) => { console.log(TAG, 'DELETE /api/chat/:chatId - Chat deletion requested'); try { diff --git a/routes/user.js b/routes/user.js index be04225..5e3c385 100644 --- a/routes/user.js +++ b/routes/user.js @@ -764,7 +764,7 @@ router.post('/profile-image', profileImageLimiter, authenticateToken, upload.sin if (req.file) { // Sanitize and validate the file path before deleting const uploadsRoot = path.resolve(__dirname, '../uploads'); - const filePath = path.resolve(uploadsRoot, path.basename(req.file.filename)); + const filePath = path.resolve(uploadsRoot, path.basename(req.file.path)); if (filePath.startsWith(uploadsRoot)) { fs.unlink(filePath, (err) => { if (err) console.error(TAG, 'Error deleting uploaded file:', err); From 370d10d4a066eeabe10f267f03ac7f3341773440 Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Fri, 18 Jul 2025 01:05:55 -0400 Subject: [PATCH 08/15] add testing scripts for rate limits --- testing/README.md | 94 +++++++++++++++++++++++ testing/get-token.ps1 | 65 ++++++++++++++++ testing/test-rate-limits.ps1 | 141 +++++++++++++++++++++++++++++++++++ 3 files changed, 300 insertions(+) create mode 100644 testing/README.md create mode 100644 testing/get-token.ps1 create mode 100644 testing/test-rate-limits.ps1 diff --git a/testing/README.md b/testing/README.md new file mode 100644 index 0000000..5e59c9c --- /dev/null +++ b/testing/README.md @@ -0,0 +1,94 @@ +# Testing Script Steps +This quick guide is to walk you through how to use the Powershell (.ps1) and Bash Script (.sh) testing scripts to test the rate limits. + +## Prerequisites +- Your API server running on `http://localhost:3000` (see [this README](https://github.com/Rongbin99/PlanIT-API/blob/main/README.md)) +- Valid user account credentials +- PowerShell (Windows) or Bash (macOS/Linux) + +## Obtain your JWT +Quickly enter your email and password to obtain the JWT associated with your USER_ID + +Windows (PowerShell): + +```powershell +.\get-token.ps1 +``` + +macOS/Linux (Bash): + +```bash +./get-token.sh +``` + +## Execute the Rate Limit Test Scripts +Using your JWT from above, run the rate limit testing scripts + +Windows (PowerShell): + +```powershell +.\test-rate-limits.ps1 -Token "YOUR_JWT_TOKEN_HERE" +``` + +macOS/Linux (Bash): + +```bash +./test-rate-limits.sh -t "YOUR_JWT_TOKEN_HERE" +``` + +## Expected Results + +### Successful Rate Limiting +Upon success, you should see: +- `200 OK` status for requests within the limit +- `429 Too Many Requests` once the rate limit has been hit + +![NOTE] +> `Expected failure (401)` is expected. If you want to test for a login request with legitamite credentials, modify the `test-rate-limits` script email and password field. + +### Rate Limit Thresholds +- **Profile Endpoint**: 30 requests per 15 minutes +- **Login Endpoint**: 10 requests per 15 minutes +- **Signup Endpoint**: 5 requests per 15 minutes +- **Password Change**: 5 requests per 15 minutes +- **Profile Image Upload**: 5 requests per 15 minutes +- **Trip Listing**: 50 requests per 15 minutes +- **Trip Retrieval**: 100 requests per 15 minutes +- **Trip Deletion**: 20 requests per 15 minutes +- **Audit Logs**: 20 requests per 15 minutes +- **Status Endpoints**: 100 requests per 15 minutes + +## Manual Testing + +### Quick Test with curl +```bash +# Test single request +curl -H "Authorization: Bearer [YOUR_JWT_TOKEN]" http://localhost:3000/api/user/profile + +# Spam requests to hit limit +for i in {1..40}; do + curl -H "Authorization: Bearer [YOUR_JWT_TOKEN]" http://localhost:3000/api/user/profile + echo "Request $i" +done +``` + +## Troubleshooting + +### Common Issues +1. **"Connection refused"**: Make sure your API server is running +2. **"Invalid token"**: Get a fresh JWT token using the get-token script +3. **"Rate limit not working"**: Check server logs for rate limit messages +4. **"Script permission denied"**: Make scripts executable with `chmod +x *.sh` (macOS/Linux) + +### Server Logs +Monitor your server console for rate limit messages: +``` +[UserRoutes] Rate limit exceeded for IP: XXX.X.X.X +[ChatRoutes] Rate limit exceeded for IP: XXX.X.X.X +``` + +### Reset Rate Limits +Rate limits reset after 15 minutes. To test immediately: +- Restart your API server +- Wait 15 minutes for automatic reset +- Use a different IP address diff --git a/testing/get-token.ps1 b/testing/get-token.ps1 new file mode 100644 index 0000000..bf328a5 --- /dev/null +++ b/testing/get-token.ps1 @@ -0,0 +1,65 @@ +# Get JWT Token Script for Windows +# Usage: .\get-token.ps1 + +param( + [string]$BaseUrl = "http://localhost:3000", + [string]$Email = "", + [string]$Password = "" +) + +Write-Host "Getting JWT Token" +Write-Host "Base URL: $BaseUrl" +Write-Host "" + +# Prompt for credentials if not provided +if (-not $Email) { + $Email = Read-Host "Enter your email" +} + +if (-not $Password) { + $SecurePassword = Read-Host "Enter your password" -AsSecureString + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword) + $Password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) +} + +Write-Host "Attempting login..." + +try { + $body = @{ + email = $Email + password = $Password + } | ConvertTo-Json + + $response = Invoke-RestMethod -Uri "$BaseUrl/api/user/login" -Body $body -ContentType "application/json" -Method POST -TimeoutSec 10 + + if ($response.success) { + Write-Host "Login successful!" + Write-Host "" + Write-Host "Your JWT Token:" + Write-Host $response.token + Write-Host "" + Write-Host "Copy this token and use it in your rate limit tests:" + Write-Host ".\test-rate-limits.ps1 -Token `"$($response.token)`"" + Write-Host "" + Write-Host "User Info:" + Write-Host "Name: $($response.user.name)" + Write-Host "Email: $($response.user.email)" + Write-Host "User ID: $($response.user.id)" + } else { + Write-Host "Login failed: $($response.message)" + } +} +catch { + Write-Host "Error during login: $($_.Exception.Message)" + + if ($_.Exception.Response) { + $statusCode = $_.Exception.Response.StatusCode + Write-Host "Status Code: $statusCode" + + if ($statusCode -eq 401) { + Write-Host "Invalid email or password" + } elseif ($statusCode -eq 429) { + Write-Host "Rate limited - too many login attempts" + } + } +} diff --git a/testing/test-rate-limits.ps1 b/testing/test-rate-limits.ps1 new file mode 100644 index 0000000..675eb3b --- /dev/null +++ b/testing/test-rate-limits.ps1 @@ -0,0 +1,141 @@ +# Rate Limit Testing Script for Windows +# Usage: .\test-rate-limits.ps1 + +param( + [string]$BaseUrl = "http://localhost:3000", + [string]$Token = "", + [int]$TestCount = 60 +) + +Write-Host "Testing Rate Limits" +Write-Host "Base URL: $BaseUrl" +Write-Host "Test Count: $TestCount" +Write-Host "" + +# Check if token is provided +if (-not $Token) { + Write-Host "JWT token is required. Use -Token parameter." + Write-Host "Example: .\test-rate-limits.ps1 -Token `"your_jwt_token_here`"" + exit 1 +} + +# Test 1: Profile endpoint (should hit rate limit) +Write-Host "Testing Profile Endpoint..." +$successCount = 0 +$rateLimitedCount = 0 + +for ($i = 1; $i -le $TestCount; $i++) { + try { + $headers = @{ + "Authorization" = "Bearer $Token" + } + + $response = Invoke-RestMethod -Uri "$BaseUrl/api/user/profile" -Headers $headers -Method GET -TimeoutSec 5 + + if ($response.success) { + $successCount++ + Write-Host "Request $i - Success" + } + } + catch { + if ($_.Exception.Response.StatusCode -eq 429) { + $rateLimitedCount++ + Write-Host "Request $i - Rate Limited" + } else { + Write-Host "Request $i - Error: $($_.Exception.Message)" + } + } + + # Small delay to see the progression + Start-Sleep -Milliseconds 100 +} + +Write-Host "" +Write-Host "Profile Endpoint Results:" +Write-Host "Successful: $successCount" +Write-Host "Rate Limited: $rateLimitedCount" +Write-Host "" + +# Test 2: Login endpoint (should hit rate limit) +Write-Host "Testing Login Endpoint..." +$loginSuccessCount = 0 +$loginRateLimitedCount = 0 + +for ($i = 1; $i -le 15; $i++) { + try { + $body = @{ + email = "test@example.com" # change to valid email to test real login request + password = "wrongpassword" # change to valid password to test real login request + } | ConvertTo-Json + + $response = Invoke-RestMethod -Uri "$BaseUrl/api/user/login" -Body $body -ContentType "application/json" -Method POST -TimeoutSec 5 + + if ($response.success -eq $false -and $response.error -eq "Invalid Credentials") { + $loginSuccessCount++ + Write-Host "Login Request $i - Expected failure" + } + } + catch { + if ($_.Exception.Response.StatusCode -eq 429) { + $loginRateLimitedCount++ + Write-Host "Login Request $i - Rate Limited" + } elseif ($_.Exception.Response.StatusCode -eq 401) { + $loginSuccessCount++ + Write-Host "Login Request $i - Expected failure (401)" + } else { + Write-Host "Login Request $i - Other error: $($_.Exception.Response.StatusCode)" + } + } + + Start-Sleep -Milliseconds 100 +} + +Write-Host "" +Write-Host "Login Endpoint Results:" +Write-Host "Successful (expected failures): $loginSuccessCount" +Write-Host "Rate Limited: $loginRateLimitedCount" +Write-Host "" + +# Test 3: Trip listing endpoint (optional auth) +Write-Host "Testing Trip Listing Endpoint..." +$tripSuccessCount = 0 +$tripRateLimitedCount = 0 + +for ($i = 1; $i -le 60; $i++) { + try { + $response = Invoke-RestMethod -Uri "$BaseUrl/api/chat" -Method GET -TimeoutSec 5 + + if ($response.success) { + $tripSuccessCount++ + Write-Host "Trip Request $i - Success" + } + } + catch { + if ($_.Exception.Response.StatusCode -eq 429) { + $tripRateLimitedCount++ + Write-Host "Trip Request $i - Rate Limited" + } else { + Write-Host "Trip Request $i - Error: $($_.Exception.Message)" + } + } + + Start-Sleep -Milliseconds 100 +} + +Write-Host "" +Write-Host "Trip Listing Endpoint Results:" +Write-Host "Successful: $tripSuccessCount" +Write-Host "Rate Limited: $tripRateLimitedCount" +Write-Host "" + +Write-Host "Rate Limit Testing Complete!" +Write-Host "" +Write-Host "Summary:" +Write-Host "Profile Endpoint: $successCount success, $rateLimitedCount rate limited" +Write-Host "Login Endpoint: $loginSuccessCount success, $loginRateLimitedCount rate limited" +Write-Host "Trip Listing: $tripSuccessCount success, $tripRateLimitedCount rate limited" +Write-Host "" +Write-Host "Expected Rate Limits:" +Write-Host "Profile: 30 requests/15min" +Write-Host "Login: 10 requests/15min" +Write-Host "Trip Listing: 50 requests/15min" From 60b0829ca43e57302ca840de6979c4b6d2b51c3d Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Fri, 18 Jul 2025 01:10:28 -0400 Subject: [PATCH 09/15] update testing README --- testing/README.md | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/testing/README.md b/testing/README.md index 5e59c9c..6c322eb 100644 --- a/testing/README.md +++ b/testing/README.md @@ -24,7 +24,7 @@ macOS/Linux (Bash): ## Execute the Rate Limit Test Scripts Using your JWT from above, run the rate limit testing scripts -Windows (PowerShell): +Windows (PowerShell): ```powershell .\test-rate-limits.ps1 -Token "YOUR_JWT_TOKEN_HERE" @@ -43,7 +43,7 @@ Upon success, you should see: - `200 OK` status for requests within the limit - `429 Too Many Requests` once the rate limit has been hit -![NOTE] +> [!INFO] > `Expected failure (401)` is expected. If you want to test for a login request with legitamite credentials, modify the `test-rate-limits` script email and password field. ### Rate Limit Thresholds @@ -75,17 +75,10 @@ done ## Troubleshooting ### Common Issues -1. **"Connection refused"**: Make sure your API server is running -2. **"Invalid token"**: Get a fresh JWT token using the get-token script -3. **"Rate limit not working"**: Check server logs for rate limit messages -4. **"Script permission denied"**: Make scripts executable with `chmod +x *.sh` (macOS/Linux) - -### Server Logs -Monitor your server console for rate limit messages: -``` -[UserRoutes] Rate limit exceeded for IP: XXX.X.X.X -[ChatRoutes] Rate limit exceeded for IP: XXX.X.X.X -``` +1. **Connection refused**: Make sure your API server is running +2. **Invalid token**: Get a fresh JWT token using the get-token script +3. **Rate limit not working**: Check server logs for rate limit messages +4. **Script permission denied**: Make scripts executable with `chmod +x *.sh` (macOS/Linux) ### Reset Rate Limits Rate limits reset after 15 minutes. To test immediately: From 14014a445a4287edbde707845dc4a2999ebd930b Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Fri, 18 Jul 2025 01:14:17 -0400 Subject: [PATCH 10/15] update testing README with macOS specifics --- testing/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/testing/README.md b/testing/README.md index 6c322eb..e6e6718 100644 --- a/testing/README.md +++ b/testing/README.md @@ -6,6 +6,9 @@ This quick guide is to walk you through how to use the Powershell (.ps1) and Bas - Valid user account credentials - PowerShell (Windows) or Bash (macOS/Linux) +> [!TIP] +> For macOS, run `chmod +x *.sh` before proceeding! + ## Obtain your JWT Quickly enter your email and password to obtain the JWT associated with your USER_ID From 6cc80439744fcb4852eb97d7bd1ef286c6176361 Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Fri, 18 Jul 2025 01:15:03 -0400 Subject: [PATCH 11/15] macOS rate limiting testing scripts --- testing/get-token.sh | 153 ++++++++++++++++++++++++++ testing/test-rate-limits.sh | 214 ++++++++++++++++++++++++++++++++++++ 2 files changed, 367 insertions(+) create mode 100644 testing/get-token.sh create mode 100644 testing/test-rate-limits.sh diff --git a/testing/get-token.sh b/testing/get-token.sh new file mode 100644 index 0000000..fe029dd --- /dev/null +++ b/testing/get-token.sh @@ -0,0 +1,153 @@ +#!/bin/bash + +# Get JWT Token Script for macOS/Linux +# Usage: ./get-token.sh + +# Default values +BASE_URL="http://localhost:3000" +EMAIL="" +PASSWORD="" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +WHITE='\033[1;37m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${GREEN}Getting JWT Token${NC}" + echo -e "${YELLOW}Base URL: $BASE_URL${NC}" + echo "" +} + +print_success() { + echo -e "${GREEN}✅ $1${NC}" +} + +print_error() { + echo -e "${RED}❌ $1${NC}" +} + +print_info() { + echo -e "${CYAN}📋 $1${NC}" +} + +print_user_info() { + echo -e "${YELLOW}User Info:${NC}" + echo -e "${WHITE}Name: $1${NC}" + echo -e "${WHITE}Email: $2${NC}" + echo -e "${WHITE}User ID: $3${NC}" +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -u|--url) + BASE_URL="$2" + shift 2 + ;; + -e|--email) + EMAIL="$2" + shift 2 + ;; + -p|--password) + PASSWORD="$2" + shift 2 + ;; + -h|--help) + echo "Usage: $0 [OPTIONS]" + echo "Options:" + echo " -u, --url URL Base URL (default: http://localhost:3000)" + echo " -e, --email EMAIL Email address" + echo " -p, --password PASS Password" + echo " -h, --help Show this help message" + exit 0 + ;; + *) + print_error "Unknown option: $1" + exit 1 + ;; + esac +done + +print_status + +# Prompt for credentials if not provided +if [ -z "$EMAIL" ]; then + echo -n "Enter your email: " + read -r EMAIL +fi + +if [ -z "$PASSWORD" ]; then + echo -n "Enter your password: " + read -rs PASSWORD + echo "" +fi + +echo -e "${CYAN}Attempting login...${NC}" + +# Create temporary JSON file for request body +TEMP_FILE=$(mktemp) +cat > "$TEMP_FILE" << EOF +{ + "email": "$EMAIL", + "password": "$PASSWORD" +} +EOF + +# Make the login request +RESPONSE=$(curl -s -w "\n%{http_code}" \ + -X POST \ + -H "Content-Type: application/json" \ + -d @"$TEMP_FILE" \ + "$BASE_URL/api/user/login") + +# Clean up temporary file +rm "$TEMP_FILE" + +# Extract response body and status code +RESPONSE_BODY=$(echo "$RESPONSE" | head -n -1) +HTTP_STATUS=$(echo "$RESPONSE" | tail -n 1) + +if [ "$HTTP_STATUS" -eq 200 ]; then + # Parse JSON response + SUCCESS=$(echo "$RESPONSE_BODY" | grep -o '"success":[^,]*' | cut -d':' -f2 | tr -d ' ') + TOKEN=$(echo "$RESPONSE_BODY" | grep -o '"token":"[^"]*"' | cut -d'"' -f4) + USER_NAME=$(echo "$RESPONSE_BODY" | grep -o '"name":"[^"]*"' | cut -d'"' -f4) + USER_EMAIL=$(echo "$RESPONSE_BODY" | grep -o '"email":"[^"]*"' | cut -d'"' -f4) + USER_ID=$(echo "$RESPONSE_BODY" | grep -o '"id":"[^"]*"' | cut -d'"' -f4) + + if [ "$SUCCESS" = "true" ] && [ -n "$TOKEN" ]; then + print_success "Login successful!" + echo "" + echo -e "${YELLOW}Your JWT Token:${NC}" + echo -e "${WHITE}$TOKEN${NC}" + echo "" + print_info "Copy this token and use it in your rate limit tests:" + echo -e "${WHITE}./test-rate-limits.sh -t \"$TOKEN\"${NC}" + echo "" + print_user_info "$USER_NAME" "$USER_EMAIL" "$USER_ID" + else + print_error "Login failed: Invalid response format" + echo "Response: $RESPONSE_BODY" + fi +else + case $HTTP_STATUS in + 401) + print_error "Invalid email or password" + ;; + 429) + print_error "Rate limited - too many login attempts" + ;; + 500) + print_error "Server error - check if API is running" + ;; + *) + print_error "Login failed with status code: $HTTP_STATUS" + echo "Response: $RESPONSE_BODY" + ;; + esac +fi diff --git a/testing/test-rate-limits.sh b/testing/test-rate-limits.sh new file mode 100644 index 0000000..9b51da7 --- /dev/null +++ b/testing/test-rate-limits.sh @@ -0,0 +1,214 @@ +#!/bin/bash + +# Rate Limit Testing Script for macOS/Linux +# Usage: ./test-rate-limits.sh + +# Default values +BASE_URL="http://localhost:3000" +TOKEN="" +TEST_COUNT=60 + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' +WHITE='\033[1;37m' +NC='\033[0m' # No Color + +# Function to print colored output +print_header() { + echo -e "${GREEN}Testing Rate Limits${NC}" + echo -e "${YELLOW}Base URL: $BASE_URL${NC}" + echo -e "${YELLOW}Test Count: $TEST_COUNT${NC}" + echo "" +} + +print_success() { + echo -e "${GREEN}✅ $1${NC}" +} + +print_error() { + echo -e "${RED}❌ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +print_info() { + echo -e "${CYAN}📊 $1${NC}" +} + +print_results() { + echo -e "${MAGENTA}📈 $1${NC}" +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -u|--url) + BASE_URL="$2" + shift 2 + ;; + -t|--token) + TOKEN="$2" + shift 2 + ;; + -c|--count) + TEST_COUNT="$2" + shift 2 + ;; + -h|--help) + echo "Usage: $0 [OPTIONS]" + echo "Options:" + echo " -u, --url URL Base URL (default: http://localhost:3000)" + echo " -t, --token TOKEN JWT token for authentication" + echo " -c, --count COUNT Number of requests to test (default: 60)" + echo " -h, --help Show this help message" + exit 0 + ;; + *) + print_error "Unknown option: $1" + exit 1 + ;; + esac +done + +print_header + +# Check if token is provided +if [ -z "$TOKEN" ]; then + print_error "JWT token is required. Use -t or --token option." + echo "Example: $0 -t \"your_jwt_token_here\"" + exit 1 +fi + +# Test 1: Profile endpoint (should hit rate limit) +print_info "Testing Profile Endpoint..." +success_count=0 +rate_limited_count=0 + +for ((i=1; i<=TEST_COUNT; i++)); do + # Make the request + RESPONSE=$(curl -s -w "\n%{http_code}" \ + -H "Authorization: Bearer $TOKEN" \ + "$BASE_URL/api/user/profile") + + # Extract response body and status code + RESPONSE_BODY=$(echo "$RESPONSE" | head -n -1) + HTTP_STATUS=$(echo "$RESPONSE" | tail -n 1) + + if [ "$HTTP_STATUS" -eq 200 ]; then + ((success_count++)) + print_success "Request $i - Success" + elif [ "$HTTP_STATUS" -eq 429 ]; then + ((rate_limited_count++)) + print_error "Request $i - Rate Limited" + else + print_warning "Request $i - HTTP $HTTP_STATUS" + fi + + # Small delay to see the progression + sleep 0.1 +done + +echo "" +print_results "Profile Endpoint Results:" +echo -e "${WHITE}Successful: $success_count${NC}" +echo -e "${WHITE}Rate Limited: $rate_limited_count${NC}" +echo "" + +# Test 2: Login endpoint (should hit rate limit) +print_info "Testing Login Endpoint..." +login_success_count=0 +login_rate_limited_count=0 + +# Create temporary JSON file for login request +TEMP_FILE=$(mktemp) +cat > "$TEMP_FILE" << EOF +{ + "email": "test@example.com", + "password": "wrongpassword" +} +EOF + +for ((i=1; i<=15; i++)); do + # Make the login request + RESPONSE=$(curl -s -w "\n%{http_code}" \ + -X POST \ + -H "Content-Type: application/json" \ + -d @"$TEMP_FILE" \ + "$BASE_URL/api/user/login") + + # Extract response body and status code + RESPONSE_BODY=$(echo "$RESPONSE" | head -n -1) + HTTP_STATUS=$(echo "$RESPONSE" | tail -n 1) + + if [ "$HTTP_STATUS" -eq 401 ]; then + ((login_success_count++)) + print_success "Login Request $i - Expected failure (401)" + elif [ "$HTTP_STATUS" -eq 429 ]; then + ((login_rate_limited_count++)) + print_error "Login Request $i - Rate Limited" + else + print_warning "Login Request $i - Other error: $HTTP_STATUS" + fi + + sleep 0.1 +done + +# Clean up temporary file +rm "$TEMP_FILE" + +echo "" +print_results "Login Endpoint Results:" +echo -e "${WHITE}Successful (expected failures): $login_success_count${NC}" +echo -e "${WHITE}Rate Limited: $login_rate_limited_count${NC}" +echo "" + +# Test 3: Trip listing endpoint (optional auth) +print_info "Testing Trip Listing Endpoint..." +trip_success_count=0 +trip_rate_limited_count=0 + +for ((i=1; i<=60; i++)); do + # Make the request (no auth required) + RESPONSE=$(curl -s -w "\n%{http_code}" \ + "$BASE_URL/api/chat") + + # Extract response body and status code + RESPONSE_BODY=$(echo "$RESPONSE" | head -n -1) + HTTP_STATUS=$(echo "$RESPONSE" | tail -n 1) + + if [ "$HTTP_STATUS" -eq 200 ]; then + ((trip_success_count++)) + print_success "Trip Request $i - Success" + elif [ "$HTTP_STATUS" -eq 429 ]; then + ((trip_rate_limited_count++)) + print_error "Trip Request $i - Rate Limited" + else + print_warning "Trip Request $i - HTTP $HTTP_STATUS" + fi + + sleep 0.1 +done + +echo "" +print_results "Trip Listing Endpoint Results:" +echo -e "${WHITE}Successful: $trip_success_count${NC}" +echo -e "${WHITE}Rate Limited: $trip_rate_limited_count${NC}" +echo "" + +print_success "Rate Limit Testing Complete!" +echo "" +echo -e "${CYAN}Summary:${NC}" +echo -e "${WHITE}Profile Endpoint: $success_count success, $rate_limited_count rate limited${NC}" +echo -e "${WHITE}Login Endpoint: $login_success_count success, $login_rate_limited_count rate limited${NC}" +echo -e "${WHITE}Trip Listing: $trip_success_count success, $trip_rate_limited_count rate limited${NC}" +echo "" +echo -e "${YELLOW}Expected Rate Limits:${NC}" +echo -e "${WHITE}Profile: 30 requests/15min${NC}" +echo -e "${WHITE}Login: 10 requests/15min${NC}" +echo -e "${WHITE}Trip Listing: 50 requests/15min${NC}" From 4794f580e283ccaf1e8e8b7a81cd10aad151ddaa Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Fri, 18 Jul 2025 01:16:25 -0400 Subject: [PATCH 12/15] last README fix smh --- testing/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/README.md b/testing/README.md index e6e6718..d3d8749 100644 --- a/testing/README.md +++ b/testing/README.md @@ -46,7 +46,7 @@ Upon success, you should see: - `200 OK` status for requests within the limit - `429 Too Many Requests` once the rate limit has been hit -> [!INFO] +> [!NOTE] > `Expected failure (401)` is expected. If you want to test for a login request with legitamite credentials, modify the `test-rate-limits` script email and password field. ### Rate Limit Thresholds From 355c06e29ffa60d83db62b9334a95ae22f7919c0 Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Fri, 18 Jul 2025 01:22:10 -0400 Subject: [PATCH 13/15] fix spelling fml --- testing/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/README.md b/testing/README.md index d3d8749..4816b90 100644 --- a/testing/README.md +++ b/testing/README.md @@ -47,7 +47,7 @@ Upon success, you should see: - `429 Too Many Requests` once the rate limit has been hit > [!NOTE] -> `Expected failure (401)` is expected. If you want to test for a login request with legitamite credentials, modify the `test-rate-limits` script email and password field. +> `Expected failure (401)` is expected. If you want to test for a login request with legitimate credentials, modify the `test-rate-limits` script email and password field. ### Rate Limit Thresholds - **Profile Endpoint**: 30 requests per 15 minutes From c2e693cccb70a24177493e1be90d3e54f740792f Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Fri, 18 Jul 2025 16:56:42 -0400 Subject: [PATCH 14/15] make scripts executable --- testing/get-token.sh | 0 testing/test-rate-limits.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 testing/get-token.sh mode change 100644 => 100755 testing/test-rate-limits.sh diff --git a/testing/get-token.sh b/testing/get-token.sh old mode 100644 new mode 100755 diff --git a/testing/test-rate-limits.sh b/testing/test-rate-limits.sh old mode 100644 new mode 100755 From 284b7047169be2e9fc74bb28e236f9aff8a675fa Mon Sep 17 00:00:00 2001 From: Rongbin99 Date: Fri, 18 Jul 2025 16:56:48 -0400 Subject: [PATCH 15/15] npm audit fix --- package-lock.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 49107e2..983265d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1977,16 +1977,16 @@ } }, "node_modules/compression": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", - "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "license": "MIT", "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", - "on-headers": "~1.0.2", + "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" }, @@ -4387,16 +4387,16 @@ } }, "node_modules/morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", - "on-headers": "~1.0.2" + "on-headers": "~1.1.0" }, "engines": { "node": ">= 0.8.0" @@ -4712,9 +4712,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", "engines": { "node": ">= 0.8"