Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion constants/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ const APPLICATION_STATUS = {
* Business requirement: Applications created after this date are considered reviewed
* and cannot be resubmitted. This date marks the start of the new application review cycle.
*/
const APPLICATION_SCORE = {
INITIAL_SCORE: 50,
NUDGE_BONUS: 10,
};

const APPLICATION_REVIEW_CYCLE_START_DATE = new Date("2026-01-01T00:00:00.000Z");

module.exports = {
Expand All @@ -57,5 +62,6 @@ module.exports = {
APPLICATION_ERROR_MESSAGES,
APPLICATION_LOG_MESSAGES,
APPLICATION_REVIEW_CYCLE_START_DATE,
APPLICATION_STATUS
APPLICATION_STATUS,
APPLICATION_SCORE
};
1 change: 1 addition & 0 deletions controllers/applications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ const nudgeApplication = async (req: CustomRequest, res: CustomResponse) => {
message: API_RESPONSE_MESSAGES.NUDGE_SUCCESS,
nudgeCount: result.nudgeCount,
lastNudgeAt: result.lastNudgeAt,
score: result.score,
});
default:
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
Expand Down
25 changes: 25 additions & 0 deletions controllers/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,30 @@ const updateSelf = async (req, res, next) => {
}
};

const handleUserPictureUpload = async (req, res) => {
if (req.body.type === "application") {
return postApplicationUserPicture(req, res);
}
return postUserPicture(req, res);
};

const postApplicationUserPicture = async (req, res) => {
const { file } = req;
const { id: userId } = req.userData;
const { coordinates } = req.body;
try {
const coordinatesObject = coordinates && JSON.parse(coordinates);
const imageData = await imageService.uploadProfilePicture({ file, userId, coordinates: coordinatesObject });
return res.status(201).json({
message: "Application picture uploaded successfully!",
image: imageData,
});
} catch (error) {
logger.error(`Error while uploading application picture: ${error}`);
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
}
};

/**
* Post user profile picture
*
Expand Down Expand Up @@ -1186,4 +1210,5 @@ module.exports = {
getIdentityStats,
updateUsernames,
updateProfile,
handleUserPictureUpload,
};
8 changes: 8 additions & 0 deletions middlewares/pictureRouteMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const skipWhenApplicationType = (middleware) => {
return (req, res, next) => {
if (req.body.type === "application") return next();
return middleware(req, res, next);
};
};

module.exports = skipWhenApplicationType;
11 changes: 10 additions & 1 deletion middlewares/validators/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const validateApplicationData = async (req: CustomRequest, res: CustomResponse,
userId: joi.string().optional(),
firstName: joi.string().min(1).required(),
lastName: joi.string().min(1).required(),
college: joi.string().min(1).required(),
institution: joi.string().min(1).required(),
skills: joi.string().min(5).required(),
city: joi.string().min(1).required(),
state: joi.string().min(1).required(),
Expand Down Expand Up @@ -130,6 +130,15 @@ const validateApplicationUpdateData = async (req: CustomRequest, res: CustomResp
.strict()
.min(1)
.keys({
institution: joi.string().min(1).optional(),
skills: joi.string().min(5).optional(),
city: joi.string().min(1).optional(),
state: joi.string().min(1).optional(),
country: joi.string().min(1).optional(),
role: joi
.string()
.valid(...Object.values(APPLICATION_ROLES))
.optional(),
imageUrl: joi.string().uri().optional(),
foundFrom: joi.string().min(1).optional(),
introduction: joi.string().min(1).optional(),
Expand Down
11 changes: 7 additions & 4 deletions models/applications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const { logType } = require("../constants/logs");
const firestore = require("../utils/firestore");
const logger = require("../utils/logger");
const ApplicationsModel = firestore.collection("applicants");
const { APPLICATION_STATUS_TYPES, APPLICATION_STATUS } = require("../constants/application");
const { APPLICATION_STATUS_TYPES, APPLICATION_STATUS, APPLICATION_SCORE } = require("../constants/application");
const { convertDaysToMilliseconds } = require("../utils/time");

const getAllApplications = async (limit: number, lastDocId?: string) => {
Expand All @@ -16,7 +16,7 @@ const getAllApplications = async (limit: number, lastDocId?: string) => {
lastDoc = await ApplicationsModel.doc(lastDocId).get();
}

let dbQuery = ApplicationsModel.orderBy("createdAt", "desc");
let dbQuery = ApplicationsModel.where("isNew", "==", true).orderBy("createdAt", "desc");

if (lastDoc) {
dbQuery = dbQuery.startAfter(lastDoc);
Expand Down Expand Up @@ -59,7 +59,7 @@ const getApplicationsBasedOnStatus = async (status: string, limit: number, lastD
try {
let lastDoc = null;
const applications = [];
let dbQuery = ApplicationsModel.where("status", "==", status);
let dbQuery = ApplicationsModel.where("isNew", "==", true).where("status", "==", status);

if (userId) {
dbQuery = dbQuery.where("userId", "==", userId);
Expand All @@ -86,7 +86,7 @@ const getApplicationsBasedOnStatus = async (status: string, limit: number, lastD
});
});

let countQuery = ApplicationsModel.where("status", "==", status);
let countQuery = ApplicationsModel.where("isNew", "==", true).where("status", "==", status);

const totalApplications = await countQuery.get();
const totalCount = totalApplications.size;
Expand Down Expand Up @@ -219,16 +219,19 @@ const nudgeApplication = async ({ applicationId, userId }: { applicationId: stri
const currentNudgeCount = application.nudgeCount || 0;
const updatedNudgeCount = currentNudgeCount + 1;
const newLastNudgeAt = new Date(currentTime).toISOString();
const updatedScore = (application.score || 0) + APPLICATION_SCORE.NUDGE_BONUS;

transaction.update(applicationRef, {
nudgeCount: updatedNudgeCount,
lastNudgeAt: newLastNudgeAt,
score: updatedScore,
});

return {
status: APPLICATION_STATUS.success,
nudgeCount: updatedNudgeCount,
lastNudgeAt: newLastNudgeAt,
score: updatedScore,
};
});

Expand Down
1 change: 1 addition & 0 deletions models/discordactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ const updateIdleUsersOnDiscord = async (dev) => {
let allUsersHavingGroupIdle = [];
let groupIdleRole;
let groupIdleRoleId;
const allMavens = [];

try {
groupIdleRole = await getGroupRole("group-idle");
Expand Down
9 changes: 8 additions & 1 deletion routes/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
const { devFlagMiddleware } = require("../middlewares/devFlag");
const { userAuthorization } = require("../middlewares/userAuthorization");
const conditionalMiddleware = require("../middlewares/conditionalMiddleware");
const skipWhenApplicationType = require("../middlewares/pictureRouteMiddleware");

router.post("/", authorizeAndAuthenticate([ROLES.SUPERUSER], [Services.CRON_JOB_HANDLER]), users.markUnverified);
router.post("/update-in-discord", authenticate, authorizeRoles([SUPERUSER]), users.setInDiscordScript);
Expand Down Expand Up @@ -65,7 +66,13 @@
);

// upload.single('profile') -> multer inmemory storage of file for type multipart/form-data
router.post("/picture", authenticate, checkIsVerifiedDiscord, upload.single("profile"), users.postUserPicture);
router.post(
"/picture",
authenticate,

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI 5 days ago

In general, to fix missing rate limiting on an Express route that performs authentication/authorization and potentially expensive work, you should add a rate-limiting middleware (e.g., via express-rate-limit) before the handler. This middleware should be configured to allow a reasonable number of requests per user/IP in a fixed time window, returning HTTP 429 when exceeded. Applying it specifically to the sensitive route avoids impacting unrelated traffic.

For this code, the least intrusive and clearest fix is to introduce an express-rate-limit middleware in routes/users.js and apply it only to the /picture POST route. That keeps existing functionality unchanged except for enforcing a maximum number of profile picture upload attempts per IP (or per whatever key the rate limiter uses). Concretely:

  • Add const rateLimit = require("express-rate-limit"); near the other require statements.
  • Define a const pictureUploadLimiter = rateLimit({...}) with a sensible windowMs and max value (e.g., smallish cap such as 10–20 attempts per 15 minutes, which is generous for profile pictures but mitigates DoS).
  • Insert pictureUploadLimiter into the middleware chain for router.post("/picture", ...), right after authenticate (so only authenticated users are counted, but before actual upload/processing to minimize resource usage on abusive requests).

This addresses all three alert variants, is localized to the shown file, and avoids changing any external behavior other than adding rate limiting.

Suggested changeset 2
routes/users.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/routes/users.js b/routes/users.js
--- a/routes/users.js
+++ b/routes/users.js
@@ -16,7 +16,13 @@
 const { userAuthorization } = require("../middlewares/userAuthorization");
 const conditionalMiddleware = require("../middlewares/conditionalMiddleware");
 const skipWhenApplicationType = require("../middlewares/pictureRouteMiddleware");
+const rateLimit = require("express-rate-limit");
 
+const pictureUploadLimiter = rateLimit({
+  windowMs: 15 * 60 * 1000, // 15 minutes
+  max: 20, // limit each IP to 20 picture upload requests per windowMs
+});
+
 router.post("/", authorizeAndAuthenticate([ROLES.SUPERUSER], [Services.CRON_JOB_HANDLER]), users.markUnverified);
 router.post("/update-in-discord", authenticate, authorizeRoles([SUPERUSER]), users.setInDiscordScript);
 router.post("/verify", authenticate, users.verifyUser);
@@ -69,6 +74,7 @@
 router.post(
   "/picture",
   authenticate,
+  pictureUploadLimiter,
   upload.single("profile"),
   skipWhenApplicationType(checkIsVerifiedDiscord),
   users.handleUserPictureUpload
EOF
@@ -16,7 +16,13 @@
const { userAuthorization } = require("../middlewares/userAuthorization");
const conditionalMiddleware = require("../middlewares/conditionalMiddleware");
const skipWhenApplicationType = require("../middlewares/pictureRouteMiddleware");
const rateLimit = require("express-rate-limit");

const pictureUploadLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 20, // limit each IP to 20 picture upload requests per windowMs
});

router.post("/", authorizeAndAuthenticate([ROLES.SUPERUSER], [Services.CRON_JOB_HANDLER]), users.markUnverified);
router.post("/update-in-discord", authenticate, authorizeRoles([SUPERUSER]), users.setInDiscordScript);
router.post("/verify", authenticate, users.verifyUser);
@@ -69,6 +74,7 @@
router.post(
"/picture",
authenticate,
pictureUploadLimiter,
upload.single("profile"),
skipWhenApplicationType(checkIsVerifiedDiscord),
users.handleUserPictureUpload
package.json
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -42,7 +42,8 @@
     "passport-github2": "0.1.12",
     "passport-google-oauth20": "^2.0.0",
     "rate-limiter-flexible": "5.0.3",
-    "winston": "3.13.0"
+    "winston": "3.13.0",
+    "express-rate-limit": "^8.2.1"
   },
   "devDependencies": {
     "@types/chai": "4.3.16",
EOF
@@ -42,7 +42,8 @@
"passport-github2": "0.1.12",
"passport-google-oauth20": "^2.0.0",
"rate-limiter-flexible": "5.0.3",
"winston": "3.13.0"
"winston": "3.13.0",
"express-rate-limit": "^8.2.1"
},
"devDependencies": {
"@types/chai": "4.3.16",
This fix introduces these dependencies
Package Version Security advisories
express-rate-limit (npm) 8.2.1 None
Copilot is powered by AI and may make mistakes. Always verify output.
upload.single("profile"),
skipWhenApplicationType(checkIsVerifiedDiscord),
users.handleUserPictureUpload
);
router.patch(
"/picture/verify/:id",
authenticate,
Expand Down
5 changes: 3 additions & 2 deletions services/applicationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const {
APPLICATION_STATUS_TYPES,
APPLICATION_ERROR_MESSAGES,
APPLICATION_REVIEW_CYCLE_START_DATE,
APPLICATION_SCORE,
} = require("../constants/application");
const logger = require("../utils/logger");

Expand All @@ -31,7 +32,7 @@ const transformPayloadToApplication = (payload: applicationPayload, userId: stri
country: payload.country,
},
professional: {
institution: payload.college,
institution: payload.institution,
skills: payload.skills,
},
intro: {
Expand Down Expand Up @@ -86,7 +87,7 @@ export const createApplicationService = async (

const applicationData: application = {
...transformPayloadToApplication(payload, userId),
score: 0,
score: APPLICATION_SCORE.INITIAL_SCORE,
status: APPLICATION_STATUS_TYPES.PENDING,
createdAt,
isNew: true,
Expand Down
19 changes: 12 additions & 7 deletions test/fixtures/applications/applications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module.exports = () => {
{
firstName: "vinayak",
lastName: "triveid",
college: "Christ Church college",
institution: "Christ Church institution",
skills: "React, Ember, Node js",
city: "Kanpur",
state: "Uttar Pradesh",
Expand All @@ -21,12 +21,13 @@ module.exports = () => {
},
createdAt: null,
role: "developer",
isNew: true,
},
{
userId: "xyajkdfsfsd",
firstName: "Ritik",
lastName: "Jaiwal",
college: "Tata Consultancy services",
institution: "Tata Consultancy services",
skills: "React, Ember, Node js",
city: "Bangalore",
state: "Karnataka",
Expand All @@ -44,9 +45,10 @@ module.exports = () => {
},
createdAt: null,
role: "developer",
isNew: true,
},
{
college: "Groww",
institution: "Groww",
state: "Karnataka",
firstName: "Vaibhav",
lastName: "Desai",
Expand All @@ -68,9 +70,10 @@ module.exports = () => {
status: "rejected",
createdAt: null,
role: "developer",
isNew: true,
},
{
college: "Groww",
institution: "Groww",
state: "Karnataka",
firstName: "Vaibhav",
lastName: "Desai",
Expand All @@ -92,9 +95,10 @@ module.exports = () => {
status: "rejected",
createdAt: null,
role: "developer",
isNew: true,
},
{
college: "Groww",
institution: "Groww",
state: "Karnataka",
firstName: "Vaibhav",
lastName: "Desai",
Expand All @@ -116,9 +120,10 @@ module.exports = () => {
status: "rejected",
createdAt: null,
role: "developer",
isNew: true,
},
{
college: "Groww",
institution: "Groww",
state: "Karnataka",
firstName: "Vaibhav",
lastName: "Desai",
Expand All @@ -145,7 +150,7 @@ module.exports = () => {
city: "Kanpur",
state: "UP",
country: "India",
college: "Christ Church College",
institution: "Christ Church College",
skills: "React, NodeJs, Ember",
introduction:
"mattis aliquam faucibus purus in massa tempor nec feugiat nisl pretium fusce id velit ut tortor pretium viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed viverra tellus in hac habitasse platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus at augue eget arcu dictum varius duis at consectetur lorem donec massa sapien faucibus et molestie ac feugiat sed lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare massa eget egestas purus viverra accumsan in nisl nisi scelerisque eu ultrices vitae auctor eu augue ut lectus arcu bibendum at",
Expand Down
Loading
Loading