Skip to content
Open
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
6 changes: 2 additions & 4 deletions backend/db.js → backend/config/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ dotenv.config();
const connectDB = async () => {
try {
const ConnectDB = process.env.MONGODB_URI;
await mongoose.connect(ConnectDB, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
//Removing the options as they are no longer needed from mongoose6+
await mongoose.connect(ConnectDB);
console.log("MongoDB Connected");
} catch (error) {
console.error("MongoDB Connection Error:", error);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
const passport = require("passport");
const LocalStrategy = require("passport-local");
const GoogleStrategy = require("passport-google-oauth20");
const isIITBhilaiEmail = require("../utils/isIITBhilaiEmail");
const { User } = require("./schema");
// Local Strategy
passport.use(
new LocalStrategy(
{
usernameField: "email",
},
User.authenticate(),
),
);
const { User } = require("../models/schema");

// Google OAuth Strategy
passport.use(
Expand Down
187 changes: 187 additions & 0 deletions backend/controllers/certificateController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
const {
User,
PositionHolder,
Position,
OrganizationalUnit,
} = require("../models/schema");
const { CertificateBatch } = require("../models/certificateSchema");
const { validateBatchSchema, zodObjectId } = require("../utils/batchValidate");

async function createBatch(req, res) {
try{
//console.log(req.user);

/*
Get the id of user trying to initiate the request
and ensure if the person is of right authority
*/
const id = req.user.id;
const user = await User.findById(id);
if (!user) {
return res.status(404).json({ message: "User not found" });
}
if (!user.role || user.role.toUpperCase() !== "CLUB_COORDINATOR") {
return res
.status(403)
.json({ message: "Not authorized to perform the task" });
}

//to get user club
/*

positionHolders({user_id: id})
-> positions({_id: position_id})
-> organizationalUnit({_id: unit_id})
-> {type === "Club" name}
*/
const { title, unit_id, commonData, template_id, users } = req.body;
const validation = validateBatchSchema.safeParse({
title,
unit_id,
commonData,
template_id,
users,
});

if (!validation.success) {
let errors = validation.error.issues.map((issue) => issue.message);
return res.status(400).json({ message: errors });
}

//console.log(id);
// Get coordinator's position and unit
const positionHolder = await PositionHolder.findOne({ user_id: id });
//console.log(positionHolder._id);
if (!positionHolder) {
return res
.status(403)
.json({
message:
"Unauthorized to do the task as user doesn't hold any position in any unit",
});
}

const position = await Position.findById(positionHolder.position_id);
//console.log(position._id);
if (!position) {
return res.status(403).json({ message: "Invalid user position" });
}

/*
Check if the organization obtained by fetching related docs accross various collections
is same as the input OrgId received
*/
const userOrgId = position.unit_id.toString();
if (unit_id !== userOrgId) {
return res.status(403).json({
message:
"You are not authorized to initiate batches outside of your club",
});
}

//const clubId = unit_id;
// Ensure unit_id is a Club
const unitObj = await OrganizationalUnit.findById(unit_id);
if (!unitObj || unitObj.type !== "Club") {
return res
.status(403)
.json({ message: "Invalid Data: Organization is not a Club" });
}
//console.log(unitObj._id);

// Get council (parent unit) and ensure it's a Council
if (!unitObj.parent_unit_id) {
return res
.status(403)
.json({ message: "Invalid Data: club does not belong to a council" });
}
//console.log(unitObj.parent_unit_id);

const councilObj = await OrganizationalUnit.findById(unitObj.parent_unit_id);
if (
!councilObj ||
councilObj.type !== "Council" ||
!councilObj.parent_unit_id
) {
return res
.status(403)
.json({
message:
"Invalid Data: Organization is not a council or it's parent organization not found",
});
}

//const councilId = councilObj._id.toString();
const presidentOrgId = councilObj.parent_unit_id;

const presidentPosition = await Position.findOne({
unit_id: presidentOrgId,
title: "President",
});

if (!presidentPosition) {
return res.status(500).json({ message: "President position not found" });
}

const presidentHolder = await PositionHolder.findOne({
position_id: presidentPosition._id,
});

if (!presidentHolder) {
return res
.status(500)
.json({ message: "President position holder not found" });
}

const presidentObj = await User.findById(presidentHolder.user_id);

//console.log(presidentPosition._id);
const category = councilObj.category.toUpperCase();
const gensecObj = await User.findOne({ role: `GENSEC_${category}` });
if (!gensecObj || !presidentObj) {
return res.status(500).json({ message: "Approvers not found" });
}
Comment on lines +139 to +143
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against councilObj.category being null/undefined.

If category is missing on the council document, councilObj.category.toUpperCase() throws a TypeError. The outer try-catch catches it, but the client gets a generic 500 instead of a useful error. Add an explicit check.

Proposed fix
+    if (!councilObj.category) {
+      return res
+        .status(500)
+        .json({ message: "Council category is not defined" });
+    }
     const category = councilObj.category.toUpperCase();
     const gensecObj = await User.findOne({ role: `GENSEC_${category}` });
🤖 Prompt for AI Agents
In `@backend/controllers/certificateController.js` around lines 139 - 143, The
code calls councilObj.category.toUpperCase() which throws if category is
null/undefined; before calling toUpperCase (in the block that defines category
and looks up gensecObj via User.findOne), add an explicit guard that checks
councilObj.category (or the derived category variable) and return a
400/validation error response with a clear message if it's missing, otherwise
call toUpperCase on the confirmed string (use a new variable like categoryUpper)
and use that for the User.findOne(`GENSEC_${categoryUpper}`) lookup; ensure the
check is performed before computing gensecObj and before the existing
approver-not-found 500 branch so clients get a useful 4xx error when category is
absent.


const approverIds = [gensecObj._id.toString(), presidentObj._id.toString()];

const userChecks = await Promise.all(
users.map(async (uid) => {
const validation = zodObjectId.safeParse(uid);
if (!validation.success) {
return { uid, ok: false, reason: "Invalid ID" };
}

const userObj = await User.findById(uid);
if (!userObj) return { uid, ok: false, reason: "User not found" };

return { uid, ok: true };
}),
);

const invalidData = userChecks.filter((c) => !c.ok);
if (invalidData.length > 0) {
return res
.status(400)
.json({ message: "Invalid user data sent", details: invalidData });
}

const newBatch = await CertificateBatch.create({
title,
unit_id,
commonData,
templateId: template_id,
initiatedBy: id,
approverIds,
users,
});

res.json({ message: "New Batch created successfully", details: newBatch });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Return 201 for resource creation.

res.json(...) sends a 200 by default. REST convention for successful resource creation is 201 Created.

Proposed fix
-    res.json({ message: "New Batch created successfully", details: newBatch });
+    res.status(201).json({ message: "New Batch created successfully", details: newBatch });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
res.json({ message: "New Batch created successfully", details: newBatch });
res.status(201).json({ message: "New Batch created successfully", details: newBatch });
🤖 Prompt for AI Agents
In `@backend/controllers/certificateController.js` at line 178, The response for
creating the new batch currently uses res.json(...) which returns HTTP 200;
change the controller to return HTTP 201 for resource creation by replacing the
call with res.status(201).json({ message: "New Batch created successfully",
details: newBatch }); — update the response where newBatch is returned (the
controller action that calls res.json) to use res.status(201).json(...) instead
of res.json(...).

}catch(err){
console.error(err);
return res.status(500).json({message: "Internal server error"});
}
}

module.exports = {
createBatch,
};
15 changes: 9 additions & 6 deletions backend/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
const express = require("express");
require("dotenv").config();
// eslint-disable-next-line node/no-unpublished-require
const { connectDB } = require("./config/db.js");
const cookieParser = require("cookie-parser");
const cors = require("cors");
const routes_auth = require("./routes/auth");
const routes_general = require("./routes/route");
const session = require("express-session");
const bodyParser = require("body-parser");
const { connectDB } = require("./db");
const myPassport = require("./models/passportConfig"); // Adjust the path accordingly
const myPassport = require("./config/passportConfig.js"); // Adjust the path accordingly
const onboardingRoutes = require("./routes/onboarding.js");
const profileRoutes = require("./routes/profile.js");
const feedbackRoutes = require("./routes/feedbackRoutes.js");
Expand All @@ -18,20 +18,22 @@ const positionsRoutes = require("./routes/positionRoutes.js");
const organizationalUnitRoutes = require("./routes/orgUnit.js");
const announcementRoutes = require("./routes/announcements.js");
const dashboardRoutes = require("./routes/dashboard.js");

const analyticsRoutes = require("./routes/analytics.js");
const certificateRoutes = require("./routes/certificateRoutes.js");
const app = express();

if (process.env.NODE_ENV === "production") {
app.set("trust proxy", 1);
}

app.use(cors({ origin: process.env.FRONTEND_URL, credentials: true }));

// Connect to MongoDB
connectDB();

app.use(bodyParser.json());
app.use(cookieParser());

//Replaced bodyParser with express.json() - the new standard
app.use(express.json());

app.use(
session({
Expand Down Expand Up @@ -67,6 +69,7 @@ app.use("/api/announcements", announcementRoutes);
app.use("/api/dashboard", dashboardRoutes);
app.use("/api/announcements", announcementRoutes);
app.use("/api/analytics", analyticsRoutes);
app.use("/api/certificate-batches", certificateRoutes);

// Start the server
app.listen(process.env.PORT || 8000, () => {
Expand Down
71 changes: 70 additions & 1 deletion backend/middlewares/isAuthenticated.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,76 @@
const jwt = require("jsonwebtoken");

//Passport based middleware to check whether the req are coming from authenticated users
function isAuthenticated(req, res, next) {
if (req.isAuthenticated && req.isAuthenticated()) {
return next();
}
return res.status(401).json({ message: "Unauthorized: Please login first" });
}
module.exports = isAuthenticated;

//Token based middleware to check whether the req are coming from authenticated users or not

function jwtIsAuthenticated(req, res, next) {
let token;
const headerData = req.headers.authorization;
if (!headerData || !headerData.startsWith("Bearer ")) {
return res.status(401).json({ message: "User not authenticated " });
}

token = headerData.split(" ")[1];
try {
const userData = jwt.verify(token, process.env.JWT_SECRET_TOKEN);
req.user = userData;
//console.log(userData);
next();
} catch (err) {
res.status(401).json({ message: "Invalid or expired token sent" });
}
Comment on lines +13 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat package.json | grep -A 5 -B 5 jsonwebtoken

Repository: OpenLake/Student_Database_COSA

Length of output: 118


🌐 Web query:

jsonwebtoken npm library algorithms option security best practices

💡 Result:

jsonwebtoken (npm i jsonwebtoken) — algorithms option security best practices

  1. Pin/upgrade to v9+ and keep it updated. Versions <= 8.5.1 had a verification-bypass issue when verify() was called without an algorithms allow-list and with a falsy key, causing a dangerous default to none (fixed in 9.0.0). [1][2]
  2. Always pass an explicit algorithms allow-list to jwt.verify() (don’t rely on defaults). Example: allow only what you actually issue. [3]
  3. Never allow none in production. In v9, accepting unsigned tokens requires explicitly allowing none via options.algorithms—don’t do this unless you have a very specific, safe use case. [4]
  4. Don’t mix symmetric and asymmetric algorithms (e.g., don’t accept both HS256 and RS256 for the same verifier). This helps avoid “algorithm confusion/downgrade” classes of mistakes. [5]
  5. Prefer asymmetric signing (e.g., RS256/ES256) for most real systems, especially when multiple services verify tokens or key rotation matters. Auth0 (and many deployments) recommend RS256 over HS256 because verifiers only need a public key, and rotation is easier. [6]
  6. Validate claims, not just the signature: set issuer and audience (and use expiration). This reduces token replay/misuse across services. [5]
  7. Use verify(), not decode(), for untrusted tokens. decode() does not validate signatures/claims; treat decoded payload as untrusted input unless verified. [7]

Minimal safe verification pattern

import jwt from "jsonwebtoken";

const payload = jwt.verify(token, publicKeyPem, {
  algorithms: ["RS256"],   // strict allow-list
  issuer: "https://issuer.example",
  audience: "my-api",
  // optionally: maxAge: "15m"
});

Sources: GitHub Security Advisory + NVD for CVE-2022-23540 [1][2]; jsonwebtoken npm docs on algorithms option/defaults [3]; v8→v9 migration note on explicitly allowing none [4]; express-jwt guidance on required algorithms and not mixing HS/RS + issuer/audience [5]; Auth0 guidance recommending RS256 [6]; Auth0 JWT validation note to use verify() (not decode()) and disallow none [7].


🏁 Script executed:

find . -name "package.json" -type f | head -20

Repository: OpenLake/Student_Database_COSA

Length of output: 120


🏁 Script executed:

rg "jwt\." --type js -A 2 -B 2 | head -100

Repository: OpenLake/Student_Database_COSA

Length of output: 1563


🏁 Script executed:

rg "JWT_SECRET_TOKEN" --type js -A 3 -B 3

Repository: OpenLake/Student_Database_COSA

Length of output: 2072


🏁 Script executed:

cat backend/package.json | jq '.dependencies.jsonwebtoken, .devDependencies.jsonwebtoken'

Repository: OpenLake/Student_Database_COSA

Length of output: 87


🏁 Script executed:

cat -n backend/middlewares/isAuthenticated.js

Repository: OpenLake/Student_Database_COSA

Length of output: 2711


🏁 Script executed:

cat -n backend/routes/auth.js | grep -A 10 -B 5 "jwt.sign\|jwt.verify"

Repository: OpenLake/Student_Database_COSA

Length of output: 2473


Explicitly restrict JWT algorithms and validate secret configuration.

jwt.verify() in jsonwebtoken v9+ requires passing an explicit algorithms allow-list; the current code relies on defaults, which bypasses algorithm-restriction security. Additionally, missing JWT_SECRET_TOKEN should return 500 (server misconfiguration) rather than 401 (auth failure).

Proposed fix
 function jwtIsAuthenticated(req, res, next) {
   let token;
   const headerData = req.headers.authorization;
   if (!headerData || !headerData.startsWith("Bearer ")) {
     return res.status(401).json({ message: "User not authenticated " });
   }

   token = headerData.split(" ")[1];
   try {
-    const userData = jwt.verify(token, process.env.JWT_SECRET_TOKEN);
+    const secret = process.env.JWT_SECRET_TOKEN;
+    if (!secret) {
+      return res.status(500).json({ message: "JWT secret not configured" });
+    }
+    const userData = jwt.verify(token, secret, {
+      algorithms: ["HS256"],
+    });
     req.user = userData;
     //console.log(userData);
     next();
   } catch (err) {
     res.status(401).json({ message: "Invalid or expired token sent" });
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function jwtIsAuthenticated(req, res, next) {
let token;
const headerData = req.headers.authorization;
if (!headerData || !headerData.startsWith("Bearer ")) {
return res.status(401).json({ message: "User not authenticated " });
}
token = headerData.split(" ")[1];
try {
const userData = jwt.verify(token, process.env.JWT_SECRET_TOKEN);
req.user = userData;
//console.log(userData);
next();
} catch (err) {
res.status(401).json({ message: "Invalid or expired token sent" });
}
function jwtIsAuthenticated(req, res, next) {
let token;
const headerData = req.headers.authorization;
if (!headerData || !headerData.startsWith("Bearer ")) {
return res.status(401).json({ message: "User not authenticated " });
}
token = headerData.split(" ")[1];
try {
const secret = process.env.JWT_SECRET_TOKEN;
if (!secret) {
return res.status(500).json({ message: "JWT secret not configured" });
}
const userData = jwt.verify(token, secret, {
algorithms: ["HS256"],
});
req.user = userData;
//console.log(userData);
next();
} catch (err) {
res.status(401).json({ message: "Invalid or expired token sent" });
}
}
🤖 Prompt for AI Agents
In `@backend/middlewares/isAuthenticated.js` around lines 13 - 28, In
jwtIsAuthenticated, ensure process.env.JWT_SECRET_TOKEN is present and return
res.status(500).json({ message: "Server misconfiguration: JWT secret missing" })
if not; when calling jwt.verify(token, process.env.JWT_SECRET_TOKEN) pass an
explicit algorithms allow-list (e.g. algorithms: ['HS256']) to prevent algorithm
downgrade attacks, assign the verified payload to req.user as before, and keep
the existing catch to return 401 for invalid/expired tokens.

}

module.exports = {
isAuthenticated,
jwtIsAuthenticated,
};

/*

const presidentObj = await User.findById(presidentId);

console.log(presidentObj._id);
if(!gensecObj || !presidentObj) {
return res.status(500).json({ message: "Approvers not found" });
}

const approverIds = [gensecObj._id.toString(), presidentId];

const userChecks = await Promise.all(
users.map(async (uid) => {
const validation = zodObjectId.safeParse(uid);
if(!validation){
return {uid, ok: false, reason:"Invalid ID"};
}

const userObj = await User.findById(uid);
if(!userObj) return {uid, ok:false, reason: "User not found"};

return {uid, ok: true};
})
);

const invalidData = userChecks.filter((c) => !c.ok);
if(invalidData.length > 0){
return res.status(400).json({message: "Invalid user data sent", details: invalidData});
}

const newBatch = await CertificateBatch.create({
title,
unit_id,
commonData,
template_id,
initiatedBy: id,
approverIds,
users
});

*/
Loading