Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
157 changes: 157 additions & 0 deletions backend/controllers/certificateController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
const {
User,
PositionHolder,
Position,
OrganizationalUnit,
} = require("../models/schema");
const { CertificateBatch } = require("../models/certificateSchema");
const { validateBatchSchema, zodObjectId } = require("../utils/batchValidate");

async function createBatch(req, res) {
//console.log(req.user);
const id = req.user.id;
const user = await User.findById(id);
if (!user) {
return res.status(404).json({ messge: "Invalid data (User not found)" });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

if (user.role && user.role !== "CLUB_COORDINATOR") {
return res
.status(403)
.json({ message: "Not authorized to perform the task" });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

//to get user club
// positionHolders({user_id: id}) -> positions({_id: position_id}) -> organizationalUnit({_id: unit_id}) -> unit_id = "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) {
return res.status(400).json({ message: "Invalid data sent" });
}

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: "You are not part of any position in a unit" });
}

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

const userUnitId = position.unit_id.toString();
if (userUnitId !== unit_id) {
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: unit 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") {
return res.status(403).json({ message: "Invalid Data: council not found" });
}

//const councilId = councilObj._id.toString();
const presidentOrgUnitId = councilObj.parent_unit_id;
const category = councilObj.category.toUpperCase();

// Resolve General Secretary and President for the council (server-side, tamper-proof)
const gensecObj = await User.findOne({ role: `GENSEC_${category}` });
console.log(gensecObj._id);

const presidentPosition = await Position.findOne({
unit_id: presidentOrgUnitId,
title: /president/i,
});
if (!presidentPosition) {
return res
.status(500)
.json({ message: "President position not found for council" });
}
console.log(presidentPosition._id);

const presidentHolder = await PositionHolder.findOne({
position_id: presidentPosition._id,
});
const presidentId = presidentHolder.user_id.toString();
console.log(presidentId);
const presidentObj = await User.findById(presidentId);

console.log(presidentObj._id);
if (!gensecObj || !presidentObj) {
return res.status(500).json({ message: "Approvers not found" });
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

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" };
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

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 });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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
Copy Markdown
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