From b14dc823fcb5bba940622cc7186a2052765107e8 Mon Sep 17 00:00:00 2001 From: Yashagarwal9798 Date: Thu, 12 Mar 2026 13:45:43 +0530 Subject: [PATCH] fix: security audit - patch vulnerabilities and regressions --- backend/package-lock.json | 20 ++++ backend/package.json | 2 + backend/src/app.js | 11 +- backend/src/controllers/admin.controller.js | 19 ++-- backend/src/controllers/awards.controller.js | 16 ++- backend/src/controllers/exam.controller.js | 3 + .../higher-education.controller.js | 3 + .../src/controllers/internship.controller.js | 10 +- .../src/controllers/interview.controller.js | 7 +- .../src/controllers/profProject.controller.js | 46 +++++--- .../src/controllers/professor.controller.js | 56 ++++++++-- backend/src/controllers/user.controller.js | 90 ++++++++------- backend/src/cron-jobs/notifyMajorProf.js | 4 +- backend/src/cron-jobs/notifyProf.js | 4 +- backend/src/cron-jobs/notifyProfMinor.js | 4 +- backend/src/middlewares/auth.middleware.js | 104 +++++++++++++++++- backend/src/middlewares/multer.middleware.js | 40 ++++++- backend/src/models/otp.model.js | 7 +- backend/src/models/professor.model.js | 4 +- backend/src/routes/admin.routes.js | 7 +- backend/src/routes/backlog.routes.js | 4 +- backend/src/routes/classroom.routes.js | 2 +- backend/src/routes/professor.routes.js | 10 +- backend/src/routes/user.routes.js | 17 ++- backend/src/utils/Socket.js | 96 +++++++++++++++- frontend/src/components/Header.jsx | 1 + frontend/src/components/HeaderAdmin.jsx | 1 + frontend/src/components/HeaderFaculty.jsx | 1 + frontend/src/components/Login.jsx | 3 + frontend/src/components/LoginFaculty.jsx | 3 + frontend/src/components/Loginadmin.jsx | 3 + frontend/src/components/SideBarAdmin.jsx | 1 + frontend/src/components/SideBarFaculty.jsx | 1 + frontend/src/components/Sidebar.jsx | 1 + frontend/src/socket.js | 17 ++- 35 files changed, 499 insertions(+), 119 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index f1e8a8f..9673afe 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,7 +15,9 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.0", + "express-mongo-sanitize": "^2.2.0", "express-rate-limit": "^7.5.0", + "helmet": "^8.1.0", "http": "^0.0.1-security", "jsonwebtoken": "^9.0.2", "moment": "^2.30.1", @@ -745,6 +747,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-mongo-sanitize": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/express-mongo-sanitize/-/express-mongo-sanitize-2.2.0.tgz", + "integrity": "sha512-PZBs5nwhD6ek9ZuP+W2xmpvcrHwXZxD5GdieX2dsjPbAbH4azOkrHbycBud2QRU+YQF1CT+pki/lZGedHgo/dQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/express-rate-limit": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", @@ -1000,6 +1011,15 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/http": { "version": "0.0.1-security", "resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz", diff --git a/backend/package.json b/backend/package.json index 84dd041..3f6982c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,7 +19,9 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.0", + "express-mongo-sanitize": "^2.2.0", "express-rate-limit": "^7.5.0", + "helmet": "^8.1.0", "http": "^0.0.1-security", "jsonwebtoken": "^9.0.2", "moment": "^2.30.1", diff --git a/backend/src/app.js b/backend/src/app.js index 7323b8e..cf3609f 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -1,12 +1,15 @@ import express from "express"; import cookieParser from "cookie-parser"; import cors from "cors"; +import helmet from "helmet"; +import mongoSanitize from "express-mongo-sanitize"; const app = express(); import { User } from "./models/user.model.js"; import { Company } from "./models/company.model.js"; import { Professor } from "./models/professor.model.js"; - +// Security headers (disable CSP initially to avoid breaking frontend inline scripts/styles) +app.use(helmet({ contentSecurityPolicy: false })); app.use( cors({ @@ -22,6 +25,9 @@ app.use(express.urlencoded({ extended: true, limit: "16kb" })); app.use(express.static("public")); app.use(cookieParser()); +// Sanitize MongoDB queries to prevent NoSQL injection +app.use(mongoSanitize()); + import academicsRouter from "./routes/academic.routes.js"; app.use("/api/v1/academics", academicsRouter); @@ -96,7 +102,6 @@ import { app.use(notFoundHandler); // Global error handler - must be the last middleware -// Temporary debug test for companyInterview - +app.use(errorHandler); export { app }; diff --git a/backend/src/controllers/admin.controller.js b/backend/src/controllers/admin.controller.js index fbe4f28..e57b23f 100644 --- a/backend/src/controllers/admin.controller.js +++ b/backend/src/controllers/admin.controller.js @@ -8,6 +8,7 @@ import { Professor } from "../models/professor.model.js"; import { ApiError } from "../utils/ApiError.js"; import { ApiResponse } from "../utils/ApiResponse.js"; import { asyncHandler } from "../utils/asyncHandler.js"; +import { Otp } from "../models/otp.model.js"; const getUnverifiedUsers = asyncHandler(async (req, res) => { const admin = req.admin; @@ -193,7 +194,7 @@ const loginAdmin = asyncHandler(async (req, res) => { admin._id ); const loggedInAdmin = await Admin.findById(admin._id).select( - "-password -refeshToken" + "-password -refreshToken" ); // if (!admin.isAdmin) { // throw new ApiError(403, "You are not verified as an admin yet!"); @@ -201,7 +202,8 @@ const loginAdmin = asyncHandler(async (req, res) => { const options = { httpOnly: true, - secure: false, + secure: process.env.NODE_ENV === "production", + sameSite: "Lax", }; return res .status(200) @@ -230,8 +232,13 @@ const logoutAdmin = asyncHandler(async (req, res) => { new: true, } ); - res.clearCookie("accessToken"); - res.clearCookie("refreshToken"); + const cookieOptions = { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "Lax", + }; + res.clearCookie("accessToken", cookieOptions); + res.clearCookie("refreshToken", cookieOptions); return res .status(200) .json(new ApiResponse(200, {}, "Admin logged out successfully!")); @@ -239,13 +246,11 @@ const logoutAdmin = asyncHandler(async (req, res) => { const getCurrendAdmin = asyncHandler(async (req, res) => { const _id = req?.admin?._id; - const admin = await Admin.findById({ _id }); + const admin = await Admin.findById({ _id }).select("-password -refreshToken"); if (!admin) throw new ApiError(404, "admin not found"); res.status(200).json(new ApiResponse(200, admin, "admin fetched")); }); -import { Otp } from "../models/otp.model.js"; - export const rejectUser = asyncHandler(async (req, res) => { const { userId, reason } = req.body; if (!userId) { diff --git a/backend/src/controllers/awards.controller.js b/backend/src/controllers/awards.controller.js index 61f50d4..8b94a7d 100644 --- a/backend/src/controllers/awards.controller.js +++ b/backend/src/controllers/awards.controller.js @@ -46,6 +46,13 @@ const deleteAward = asyncHandler(async (req, res) => { const { id } = req.params; try { + const award = await Award.findById(id); + if (!award) { + throw new ApiError(404, "Award not found"); + } + if (award.student.toString() !== req.user._id.toString()) { + throw new ApiError(403, "You can only delete your own awards"); + } const deletedAward = await Award.findByIdAndDelete(id); if (!deletedAward) { @@ -78,6 +85,9 @@ const getAwardById = asyncHandler(async (req, res) => { if (!award) { throw new ApiError(404, "Award not found"); } + if (award.student.toString() !== req.user._id.toString()) { + throw new ApiError(403, "You can only view your own awards"); + } res.status(200).json({ success: true, @@ -96,7 +106,7 @@ const getAllAwards = asyncHandler(async (req, res) => { const updateAward = asyncHandler(async (req, res) => { const { id } = req.params; - const { title, description, date, student } = req.body; + const { title, description, date } = req.body; try { const award = await Award.findById(id); @@ -104,11 +114,13 @@ const updateAward = asyncHandler(async (req, res) => { if (!award) { throw new ApiError(404, "Award not found"); } + if (award.student.toString() !== req.user._id.toString()) { + throw new ApiError(403, "You can only update your own awards"); + } award.title = title; award.description = description; award.date = date; - award.student = student; const docURL = award.doc; if (docURL) { diff --git a/backend/src/controllers/exam.controller.js b/backend/src/controllers/exam.controller.js index 0028499..4e080d6 100644 --- a/backend/src/controllers/exam.controller.js +++ b/backend/src/controllers/exam.controller.js @@ -116,6 +116,9 @@ const getExamById = asyncHandler(async (req, res) => { if (!exam) { throw new ApiError(404, "Exam not found"); } + if (exam.name.toString() !== req.user._id.toString()) { + throw new ApiError(403, "You can only view your own exam records"); + } res.status(200).json({ success: true, diff --git a/backend/src/controllers/higher-education.controller.js b/backend/src/controllers/higher-education.controller.js index f821170..5107b63 100644 --- a/backend/src/controllers/higher-education.controller.js +++ b/backend/src/controllers/higher-education.controller.js @@ -104,6 +104,9 @@ const getHigherEducationById = asyncHandler(async (req, res) => { if (!higherEducation) { throw new ApiError(404, "Higher Education not found"); } + if (higherEducation.name.toString() !== req.user._id.toString()) { + throw new ApiError(403, "You can only view your own higher education records"); + } res.status(200).json({ success: true, diff --git a/backend/src/controllers/internship.controller.js b/backend/src/controllers/internship.controller.js index 5f9e858..402c1ed 100644 --- a/backend/src/controllers/internship.controller.js +++ b/backend/src/controllers/internship.controller.js @@ -34,7 +34,7 @@ const addInternship = asyncHandler(async (req, res) => { if (!uploadedDoc) { throw new ApiError(400, "Document upload failed!"); } - docUrl = uploadedDoc.url; + docUrl = uploadedDoc.secure_url; } else if (location === "outside_bit" && !req.file) { throw new ApiError( 400, @@ -97,11 +97,11 @@ const addInternDocs = asyncHandler(async (req, res) => { if (!InternDocsLocalPath) throw new ApiError(400, "Document is neccesary"); const InternDocs = await uploadOnCloudinary(InternDocsLocalPath); if (!InternDocs) { - throw new ApiError(400, "Not uploaded on Closuinary. something went wrong"); + throw new ApiError(400, "Not uploaded on Cloudinary. Something went wrong"); } const newInternRecord = await Internship.updateOne( { _id }, - { $set: { doc: InternDocs.url } } + { $set: { doc: InternDocs.secure_url } } ); res .status(200) @@ -226,12 +226,12 @@ const getAllVerifiedInternshipData = asyncHandler(async (req, res) => { }); const getInternshipDataforStudent = asyncHandler(async (req, res) => { - const { student_id } = req.body; + const student_id = req.user._id; const response = await Internship.find( { student: student_id }, { verfied: true } ) - .populate("student") + .populate("student", "-password -refreshToken") .populate("company"); res .status(200) diff --git a/backend/src/controllers/interview.controller.js b/backend/src/controllers/interview.controller.js index 73aa5b2..531b001 100644 --- a/backend/src/controllers/interview.controller.js +++ b/backend/src/controllers/interview.controller.js @@ -62,8 +62,11 @@ const getAllInterviewExps = asyncHandler(async (req, res) => { sort = "-createdAt", } = req.query; + // Whitelist allowed sort fields to prevent sort injection + const allowedSortFields = ["createdAt", "-createdAt", "company", "-company"]; + const sanitizedSort = allowedSortFields.includes(sort) ? sort : "-createdAt"; + const filter = {}; - console.log(companyId, studentId); if (companyId) { filter.company = companyId; } @@ -77,7 +80,7 @@ const getAllInterviewExps = asyncHandler(async (req, res) => { const interviewExps = await InterviewExp.find(filter) .populate("company", "companyName") .populate("student", "fullName email image branch cgpa") - .sort(sort) + .sort(sanitizedSort) .skip(skip) .limit(parseInt(limit, 10)); diff --git a/backend/src/controllers/profProject.controller.js b/backend/src/controllers/profProject.controller.js index a04bf8f..7af30ee 100644 --- a/backend/src/controllers/profProject.controller.js +++ b/backend/src/controllers/profProject.controller.js @@ -9,8 +9,10 @@ import { import { Professor } from "../models/professor.model.js"; const addNewProject = asyncHandler(async (req, res) => { - const { profId, title, desc, categories, startDate, endDate, relevantLinks } = + const { title, desc, categories, startDate, endDate, relevantLinks } = req.body; + // Derive professor ID from authenticated token, not client input + const profId = req.professor._id; const professor = await Professor.findById(profId); if (!professor) { throw new ApiError(404, "Professor not found!"); @@ -88,6 +90,11 @@ const editProject = asyncHandler(async (req, res) => { throw new ApiError(404, "Project not found"); } + // Verify ownership + if (project.professor.toString() !== req.professor._id.toString()) { + throw new ApiError(403, "Not authorized to edit this project"); + } + project.title = title || project.title; project.desc = desc || project.desc; project.categories = categories || project.categories; @@ -191,11 +198,13 @@ const applyToProject = asyncHandler(async (req, res) => { const getAllApplications = asyncHandler(async (req, res) => { const { status } = req.params; - const applications = await RequestProj.find({ status }) + // Only return applications for projects owned by this professor + const profProjects = await AdhocProject.find({ professor: req.professor._id }).select("_id"); + const projectIds = profProjects.map((p) => p._id); + const applications = await RequestProj.find({ status, projectId: { $in: projectIds } }) .populate("studentId", "fullName email rollNumber mobileNumber") - .populate("projectId", "title profName") // Added populate for projectId - .select("status applicationDate doc projectId"); // Included projectId in selected fields - console.log("applications", applications); + .populate("projectId", "title profName") + .select("status applicationDate doc projectId"); res.status(200).json({ success: true, data: applications }); }); @@ -210,16 +219,22 @@ const updateApplicationStatus = asyncHandler(async (req, res) => { ); } - const updatedApplication = await RequestProj.findByIdAndUpdate( - applicationId, - { status }, - { new: true } - ); - - if (!updatedApplication) { + const application = await RequestProj.findById(applicationId).populate("projectId"); + if (!application) { throw new ApiError(404, "Application not found"); } - res.status(200).json({ success: true, data: updatedApplication }); + if (!application.projectId) { + throw new ApiError(404, "Referenced project no longer exists"); + } + + // Verify the professor owns the project + if (application.projectId.professor.toString() !== req.professor._id.toString()) { + throw new ApiError(403, "Not authorized to update this application"); + } + + application.status = status; + await application.save(); + res.status(200).json({ success: true, data: application }); }); const getStudentApplications = asyncHandler(async (req, res) => { @@ -250,6 +265,11 @@ const closeProject = asyncHandler(async (req, res) => { throw new ApiError(404, "Project not found"); } + // Verify ownership + if (project.professor.toString() !== req.professor._id.toString()) { + throw new ApiError(403, "Not authorized to close this project"); + } + project.closed = true; await project.save(); diff --git a/backend/src/controllers/professor.controller.js b/backend/src/controllers/professor.controller.js index 409682a..6e69dee 100644 --- a/backend/src/controllers/professor.controller.js +++ b/backend/src/controllers/professor.controller.js @@ -15,7 +15,8 @@ import { Otp } from "../models/otp.model.js"; import { Review } from "../models/review.model.js"; import { Minor } from "../models/minor.model.js"; import { Major } from "../models/major.model.js"; -const url = "http://139.167.188.221:3000/faculty-login"; +const url = process.env.FACULTY_LOGIN_URL || "http://localhost:3000/faculty-login"; +const autoLoginBaseUrl = process.env.FACULTY_AUTO_LOGIN_URL || "http://localhost:3000/faculty-auto-login"; @@ -244,7 +245,7 @@ const addProf = asyncHandler(async (req, res) => { // }); const getProf = asyncHandler(async (req, res) => { - const professors = await Professor.find().select("-password"); + const professors = await Professor.find().select("-password -refreshToken"); res .status(200) .json( @@ -285,7 +286,7 @@ const loginProf = asyncHandler(async (req, res) => { professor._id ); const loggedInProfessor = await Professor.findById(professor._id).select( - "-password -refeshToken" + "-password -refreshToken" ); const reviewLog = await Review.findOne({ user: professor._id }); let review = false; @@ -294,7 +295,8 @@ const loginProf = asyncHandler(async (req, res) => { } const options = { httpOnly: true, - secure: false, + secure: process.env.NODE_ENV === "production", + sameSite: "Lax", }; return res .status(200) @@ -324,8 +326,13 @@ const logoutProf = asyncHandler(async (req, res) => { new: true, } ); - res.clearCookie("accessToken"); - res.clearCookie("refreshToken"); + const cookieOptions = { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "Lax", + }; + res.clearCookie("accessToken", cookieOptions); + res.clearCookie("refreshToken", cookieOptions); return res .status(200) .json(new ApiResponse(200, {}, "Prof logged out successfully!")); @@ -347,11 +354,11 @@ const generateAutoLoginUrl = asyncHandler(async (req, res) => { const autoLoginToken = jwt.sign( { _id: professor._id }, process.env.ACCESS_TOKEN_SECRET, - { expiresIn: "12h" } + { expiresIn: "30m" } ); // Create the auto-login URL - const autoLoginUrl = `http://139.167.188.221:3000/faculty-auto-login?token=${autoLoginToken}`; + const autoLoginUrl = `${autoLoginBaseUrl}?token=${autoLoginToken}`; return res .status(200) @@ -396,7 +403,8 @@ const autoLoginProf = asyncHandler(async (req, res) => { const options = { httpOnly: true, - secure: false, + secure: process.env.NODE_ENV === "production", + sameSite: "Lax", }; return res @@ -661,6 +669,10 @@ const denyGroup = asyncHandler(async (req, res) => { const profId = req?.professor?._id; const group = await Group.findById({ _id }); if (!group) throw new ApiError(409, "Group not exists"); + // Verify this professor is in the applied list + if (!group.summerAppliedProfs.some((p) => p.toString() === profId.toString())) { + throw new ApiError(403, "You are not in the applied professors list for this group"); + } group.summerAppliedProfs.pull(profId); const prof = await Professor.findById(profId); prof.appliedGroups.summer_training.pull(_id); @@ -713,6 +725,10 @@ const acceptGroup = asyncHandler(async (req, res) => { if (!group) { throw new ApiError(404, "Group not found"); } + // Verify this professor is in the applied list + if (!group.summerAppliedProfs.some((p) => p.toString() === profId.toString())) { + throw new ApiError(403, "You are not in the applied professors list for this group"); + } const numOfMem = group.members.length; if ( prof.currentCount.summer_training + numOfMem > @@ -920,7 +936,7 @@ const mergeGroups = asyncHandler(async (req, res) => { await newGroup.save(); // For each student in the merged groups, update their group field to the new group object id - await Student.updateMany( + await User.updateMany( { _id: { $in: uniqueMembers.map((member) => member._id) } }, { $set: { group: newGroup._id } } ); @@ -940,7 +956,7 @@ const otpForgotPassword = asyncHandler(async (req, res) => { if (!prof) { throw new ApiError(404, "Professor does not exists"); } - const otp = `${Math.floor(Math.random() * 9000 + 1000)}`; + const otp = `${crypto.randomInt(100000, 1000000)}`; await Otp.create({ email, otp }); const tOtp = await Otp.findOne({ email }); @@ -1050,7 +1066,7 @@ const changePassword = asyncHandler(async (req, res) => { const validOTP = otp === hashedOTP; // console.log(validOTP) if (!validOTP) { - throw new ApiError("Invalid code. Check your Inbox"); + throw new ApiError(400, "Invalid code. Check your Inbox"); } else { const savepass = await bcrypt.hash(newpassword, 12); const response = await Professor.updateOne( @@ -1202,6 +1218,10 @@ const denyMinorGroup = asyncHandler(async (req, res) => { const profId = req?.professor?._id; const group = await Minor.findById({ _id }); if (!group) throw new ApiError(409, "Group not exists"); + // Verify this professor is in the applied list + if (!group.minorAppliedProfs.some((p) => p.toString() === profId.toString())) { + throw new ApiError(403, "You are not in the applied professors list for this group"); + } group.minorAppliedProfs.pull(profId); const prof = await Professor.findById(profId); prof.appliedGroups.minor_project.pull(_id); @@ -1236,6 +1256,10 @@ const acceptMinorGroup = asyncHandler(async (req, res) => { if (!group) { throw new ApiError(404, "Group not found"); } + // Verify this professor is in the applied list + if (!group.minorAppliedProfs.some((p) => p.toString() === profId.toString())) { + throw new ApiError(403, "You are not in the applied professors list for this group"); + } const numOfMem = group.members.length; if ( prof.currentCount.minor_project + numOfMem > @@ -1546,6 +1570,10 @@ const denyMajorGroup = asyncHandler(async (req, res) => { const profId = req?.professor?._id; const group = await Major.findById({ _id }); if (!group) throw new ApiError(409, "Group not exists"); + // Verify this professor is in the applied list + if (!group.majorAppliedProfs.some((p) => p.toString() === profId.toString())) { + throw new ApiError(403, "You are not in the applied professors list for this group"); + } group.majorAppliedProfs.pull(profId); const prof = await Professor.findById(profId); prof.appliedGroups.major_project.pull(_id); @@ -1580,6 +1608,10 @@ const acceptMajorGroup = asyncHandler(async (req, res) => { if (!group) { throw new ApiError(404, "Group not found"); } + // Verify this professor is in the applied list + if (!group.majorAppliedProfs.some((p) => p.toString() === profId.toString())) { + throw new ApiError(403, "You are not in the applied professors list for this group"); + } const numOfMem = group.members.length; if ( prof.currentCount.major_project + numOfMem > diff --git a/backend/src/controllers/user.controller.js b/backend/src/controllers/user.controller.js index 86258c6..a259aa6 100644 --- a/backend/src/controllers/user.controller.js +++ b/backend/src/controllers/user.controller.js @@ -1,4 +1,5 @@ import bcrypt from "bcrypt"; +import crypto from "crypto"; import cron from "node-cron"; import { Otp } from "../models/otp.model.js"; import { Placement } from "../models/placement.model.js"; @@ -29,7 +30,7 @@ const generateAcessAndRefreshToken = async (userId) => { const verifyMail = asyncHandler(async (req, res) => { try { const { email } = req.body; - const otp = Math.floor(100000 + Math.random() * 900000); + const otp = crypto.randomInt(100000, 1000000); const existedUser = await User.findOne({ email }); if (existedUser) { @@ -126,15 +127,26 @@ const registerUser = asyncHandler(async (req, res) => { // throw new ApiError(500, "id card file is cannot be uploaded"); } - const user = await User.create({ - username: username.toLowerCase(), - password, - fullName, - rollNumber, - email, - idCard: idCard.url, - batch, - }); + let user; + try { + user = await User.create({ + username: username.toLowerCase(), + password, + fullName, + rollNumber, + email, + idCard: idCard.url, + batch, + }); + } catch (error) { + if (error.code === 11000) { + return res.status(409).json({ + success: false, + message: "User with this email, username, or roll number already exists", + }); + } + throw error; + } const createdUser = await User.findById(user._id).select( "-password -refreshToken" @@ -146,7 +158,6 @@ const registerUser = asyncHandler(async (req, res) => { success: false, message: "Something went wrong while registering the user", }); - // throw new ApiError(500, "Something went wrong while registering the user"); } return res @@ -183,24 +194,23 @@ const loginUser = asyncHandler(async (req, res) => { }); // throw new ApiError(401, "Invalid Credentials!"); } - const { accessToken, refreshToken } = await generateAcessAndRefreshToken( - user._id - ); - const loggedInUser = await User.findById(user._id).select( - "-password -refreshToken" - ); if (!user.isVerified) { console.log("You are not verified yet!"); return res.status(403).json({ success: false, message: "You are not verified yet!", }); - - //throw new ApiError(403, "You are not verified yet!"); } + const { accessToken, refreshToken } = await generateAcessAndRefreshToken( + user._id + ); + const loggedInUser = await User.findById(user._id).select( + "-password -refreshToken" + ); const options = { httpOnly: true, - secure: false, + secure: process.env.NODE_ENV === "production", + sameSite: "Lax", }; return res .status(200) @@ -229,7 +239,7 @@ export const otpForgotPass = asyncHandler(async (req, res) => { if (!user) { throw new ApiError(404, "User does not exists"); } - const otp = `${Math.floor(Math.random() * 9000 + 1000)}`; + const otp = `${crypto.randomInt(100000, 1000000)}`; await Otp.create({ email, otp }); // Send OTP email using utility function @@ -305,8 +315,13 @@ const logoutUser = asyncHandler(async (req, res) => { new: true, } ); - res.clearCookie("accessToken"); - res.clearCookie("refreshToken"); + const cookieOptions = { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "Lax", + }; + res.clearCookie("accessToken", cookieOptions); + res.clearCookie("refreshToken", cookieOptions); return res .status(200) .json(new ApiResponse(200, {}, "User logged out successfully!")); @@ -432,7 +447,7 @@ const updateUser1 = asyncHandler(async (req, res) => { const getCurrentUser = asyncHandler(async (req, res) => { const _id = req?.user?._id; - const user = await User.findById({ _id }).select("-marks"); + const user = await User.findById({ _id }).select("-password -refreshToken -marks"); if (!user) throw new ApiError(404, "user not found"); // console.log(user) res.status(200).json(new ApiResponse(200, user, "user fetched")); @@ -692,11 +707,22 @@ const fetchBranch = asyncHandler(async (req, res) => { }); const getUserbyRoll = asyncHandler(async (req, res) => { - const { rollNumber, isAdmin } = req.body; + const { rollNumber } = req.body; let query = User.findOne({ rollNumber: rollNumber }); - if (!isAdmin) { + // Admins get full data; regular users get limited fields + if (req.admin) { + query = query.select("-password -refreshToken"); + query = query + .populate("internShips") + .populate("placementOne") + .populate("placementTwo") + .populate("placementThree") + .populate("awards") + .populate("exams") + .populate("higherEd"); + } else { query = query.select( "-password -username -refreshToken -fatherName -fatherMobileNumber -motherName -residentialAddress -alternateEmail -alumni -awards -backlogs -codingProfiles -companyInterview -createdAt -exams -graduationYear -group -groupReq -higherEd -idCard -isSummerAllocated -isVerified -linkedin -marks -mobileNumber -peCourses -proj -resume -summerAppliedProfs -updatedAt -workExp -__v -abcId" ); @@ -705,18 +731,6 @@ const getUserbyRoll = asyncHandler(async (req, res) => { .populate("placementOne", "company role ctc date") .populate("placementTwo", "company role ctc date") .populate("placementThree", "company role ctc date"); - } else { - query = query - .populate("placementOne") - .populate("placementTwo") - .populate("placementThree") - .populate("proj") - .populate("awards") - .populate("higherEd") - .populate("internShips") - .populate("exams") - .populate("academics") - .populate("backlogs"); } const user = await query; diff --git a/backend/src/cron-jobs/notifyMajorProf.js b/backend/src/cron-jobs/notifyMajorProf.js index 12a9513..4b536bb 100644 --- a/backend/src/cron-jobs/notifyMajorProf.js +++ b/backend/src/cron-jobs/notifyMajorProf.js @@ -20,7 +20,7 @@ async function sendMajorNotificationEmail(professor) { { expiresIn: "30m" } ); - const autoLoginUrl = `http://139.167.188.221:3000/faculty-auto-login?token=${autoLoginToken}`; + const autoLoginUrl = `${process.env.FACULTY_AUTO_LOGIN_URL || "http://localhost:3000/faculty-auto-login"}?token=${autoLoginToken}`; const mailOptions = { from: process.env.AUTH_EMAIL, @@ -46,7 +46,7 @@ async function sendMajorNotificationEmail(professor) { Visit Dashboard -

This link is valid for 30 minutes. If you prefer to login manually, click here.

+

This link is valid for 30 minutes. If you prefer to login manually, click here.

Best regards,
BITACADEMIA

diff --git a/backend/src/cron-jobs/notifyProf.js b/backend/src/cron-jobs/notifyProf.js index 48c173f..84ef26a 100644 --- a/backend/src/cron-jobs/notifyProf.js +++ b/backend/src/cron-jobs/notifyProf.js @@ -20,7 +20,7 @@ async function sendNotificationEmail(professor) { { expiresIn: "30m" } ); - const autoLoginUrl = `http://139.167.188.221:3000/faculty-auto-login?token=${autoLoginToken}`; + const autoLoginUrl = `${process.env.FACULTY_AUTO_LOGIN_URL || "http://localhost:3000/faculty-auto-login"}?token=${autoLoginToken}`; const mailOptions = { from: process.env.AUTH_EMAIL, @@ -46,7 +46,7 @@ async function sendNotificationEmail(professor) { Visit Dashboard -

This link is valid for 30 minutes. If you prefer to login manually, click here.

+

This link is valid for 30 minutes. If you prefer to login manually, click here.

Best regards,
BITACADEMIA

diff --git a/backend/src/cron-jobs/notifyProfMinor.js b/backend/src/cron-jobs/notifyProfMinor.js index c9eed26..827cea9 100644 --- a/backend/src/cron-jobs/notifyProfMinor.js +++ b/backend/src/cron-jobs/notifyProfMinor.js @@ -20,7 +20,7 @@ async function sendMinorNotificationEmail(professor) { { expiresIn: "30m" } ); - const autoLoginUrl = `http://139.167.188.221:3000/faculty-auto-login?token=${autoLoginToken}`; + const autoLoginUrl = `${process.env.FACULTY_AUTO_LOGIN_URL || "http://localhost:3000/faculty-auto-login"}?token=${autoLoginToken}`; const mailOptions = { from: process.env.AUTH_EMAIL, @@ -46,7 +46,7 @@ async function sendMinorNotificationEmail(professor) { Visit Dashboard -

This link is valid for 30 minutes. If you prefer to login manually, click here.

+

This link is valid for 30 minutes. If you prefer to login manually, click here.

Best regards,
BITACADEMIA

diff --git a/backend/src/middlewares/auth.middleware.js b/backend/src/middlewares/auth.middleware.js index 2346bdc..f9a9139 100644 --- a/backend/src/middlewares/auth.middleware.js +++ b/backend/src/middlewares/auth.middleware.js @@ -49,9 +49,9 @@ const verifyAdmin = asyncHandler(async (req, res, next) => { if (!admin) { throw new ApiError(401, "Invalid Access Token!"); } - // if (!admin.isAdmin) { - // throw new ApiError(403, "You are not verified as an admin yet!"); - // } + if (!admin.isAdmin) { + throw new ApiError(403, "You are not verified as an admin yet!"); + } req.admin = admin; next(); } catch (error) { @@ -154,8 +154,106 @@ const verifyBatchAccess = (getBatchFromReq) => next(); }); +/** + * Middleware that accepts User, Admin, or Professor tokens. + * Sets req.user, req.admin, or req.professor accordingly. + */ +const verifyAnyAuth = asyncHandler(async (req, res, next) => { + try { + const token = + req.cookies?.accessToken || + req.header("Authorization")?.replace("Bearer ", ""); + if (!token) { + throw new ApiError(401, "Unauthorized request!"); + } + const decodedToken = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET); + + // Try admin first (has isAdmin flag in token) + if (decodedToken.isAdmin) { + const admin = await Admin.findById(decodedToken?._id).select( + "-password -refreshToken" + ); + if (admin) { + req.admin = admin; + return next(); + } + } + + // Try user + const user = await User.findById(decodedToken?._id).select( + "-password -refreshToken" + ); + if (user) { + req.user = user; + return next(); + } + + // Try professor + const professor = await Professor.findById(decodedToken?._id).select( + "-password -refreshToken" + ); + if (professor) { + req.professor = professor; + return next(); + } + + throw new ApiError(401, "Invalid Access Token"); + } catch (error) { + throw new ApiError(401, error?.message || "Invalid Access Token"); + } +}); + +/** + * Optional auth middleware — attempts to authenticate but allows + * unauthenticated requests through. Sets req.admin, req.user, or + * req.professor if a valid token is present; otherwise calls next() + * with no user attached. + */ +const optionalAuth = asyncHandler(async (req, res, next) => { + const token = + req.cookies?.accessToken || + req.header("Authorization")?.replace("Bearer ", ""); + if (!token) { + return next(); + } + try { + const decodedToken = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET); + + if (decodedToken.isAdmin) { + const admin = await Admin.findById(decodedToken?._id).select( + "-password -refreshToken" + ); + if (admin) { + req.admin = admin; + return next(); + } + } + + const user = await User.findById(decodedToken?._id).select( + "-password -refreshToken" + ); + if (user) { + req.user = user; + return next(); + } + + const professor = await Professor.findById(decodedToken?._id).select( + "-password -refreshToken" + ); + if (professor) { + req.professor = professor; + return next(); + } + } catch (_err) { + // Token invalid/expired — proceed as unauthenticated + } + next(); +}); + export { + optionalAuth, verifyAdmin, + verifyAnyAuth, verifyBatchAccess, verifyJWT, verifyMasterAdmin, diff --git a/backend/src/middlewares/multer.middleware.js b/backend/src/middlewares/multer.middleware.js index a1bd8af..4b62de0 100644 --- a/backend/src/middlewares/multer.middleware.js +++ b/backend/src/middlewares/multer.middleware.js @@ -1,13 +1,51 @@ import multer from "multer"; +import crypto from "crypto"; +import path from "path"; + +const ALLOWED_MIME_TYPES = [ + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", +]; + +const ALLOWED_EXTENSIONS = [ + ".jpg", ".jpeg", ".png", ".gif", ".webp", + ".pdf", ".doc", ".docx", ".xls", ".xlsx", +]; + const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, "./public/temp"); }, filename: function (req, file, cb) { - cb(null, file.originalname); + const uniqueSuffix = crypto.randomBytes(16).toString("hex"); + const ext = path.extname(file.originalname).toLowerCase(); + cb(null, `${uniqueSuffix}${ext}`); }, }); +const fileFilter = (req, file, cb) => { + const ext = path.extname(file.originalname).toLowerCase(); + if ( + ALLOWED_MIME_TYPES.includes(file.mimetype) && + ALLOWED_EXTENSIONS.includes(ext) + ) { + cb(null, true); + } else { + cb(new Error(`File type not allowed: ${file.originalname}`), false); + } +}; + export const upload = multer({ storage, + fileFilter, + limits: { + fileSize: 5 * 1024 * 1024, // 5MB + }, }); diff --git a/backend/src/models/otp.model.js b/backend/src/models/otp.model.js index 946ab06..98115dd 100644 --- a/backend/src/models/otp.model.js +++ b/backend/src/models/otp.model.js @@ -1,10 +1,8 @@ import mongoose, { Schema } from "mongoose"; -import { User } from "./user.model.js"; const otpSchema = new Schema({ email: { type: String, - ref: "User", required: true, lowercase: true, trim: true, @@ -13,6 +11,11 @@ const otpSchema = new Schema({ type: String, required: true, }, + createdAt: { + type: Date, + default: Date.now, + expires: 600, // TTL: auto-delete after 10 minutes + }, }); export const Otp = mongoose.model("Otp", otpSchema); diff --git a/backend/src/models/professor.model.js b/backend/src/models/professor.model.js index 53d9f5c..6bcfb23 100644 --- a/backend/src/models/professor.model.js +++ b/backend/src/models/professor.model.js @@ -103,9 +103,9 @@ const professorSchema = new Schema( professorSchema.pre("save", async function (next) { if (!this.isModified("password")) { - next(); + return next(); } - this.password = await bcrypt.hash(this.password, 10); + this.password = await bcrypt.hash(this.password, 12); }); professorSchema.methods.isPasswordCorrect = async function (password) { return await bcrypt.compare(password, this.password); diff --git a/backend/src/routes/admin.routes.js b/backend/src/routes/admin.routes.js index 54a19ba..982074c 100644 --- a/backend/src/routes/admin.routes.js +++ b/backend/src/routes/admin.routes.js @@ -30,7 +30,12 @@ import { assignCompany, getAllCompanies, } from "../controllers/company.controller.js"; +import { + createRateLimiter, + requestIpMiddleware, +} from "../middlewares/ratelimiter.middleware.js"; const router = Router(); +const adminAuthLimiter = createRateLimiter({ windowMs: 15 * 60 * 1000, max: 10 }); /** * Bootstrap guard middleware - allows registration only when no admins exist. @@ -49,7 +54,7 @@ const bootstrapGuard = asyncHandler(async (req, res, next) => { // Public routes router.route("/register").post(bootstrapGuard, registerAdmin); -router.route("/login").post(loginAdmin); +router.route("/login").post(requestIpMiddleware, adminAuthLimiter, loginAdmin); // Protected routes (any admin) router.route("/unverifiedUsers").get(verifyAdmin, getUnverifiedUsers); diff --git a/backend/src/routes/backlog.routes.js b/backend/src/routes/backlog.routes.js index e6648eb..389b9c4 100644 --- a/backend/src/routes/backlog.routes.js +++ b/backend/src/routes/backlog.routes.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { verifyJWT } from "../middlewares/auth.middleware.js"; +import { verifyJWT, verifyAdmin } from "../middlewares/auth.middleware.js"; import { addBacklogbyUser, @@ -9,7 +9,7 @@ import { } from "../controllers/backlog.controller.js"; const router = Router(); -router.post("/add-subj", addbacklogSubject); +router.post("/add-subj", verifyAdmin, addbacklogSubject); router.post("/add-backlog", verifyJWT, addBacklogbyUser); router.get("/get-subj", verifyJWT, getAllBacklogSubjects); router.get("/get-backlog-user", verifyJWT, getBacklogsbyUser); diff --git a/backend/src/routes/classroom.routes.js b/backend/src/routes/classroom.routes.js index b1961e9..da57407 100644 --- a/backend/src/routes/classroom.routes.js +++ b/backend/src/routes/classroom.routes.js @@ -33,6 +33,6 @@ router.get("/bookings/approved", verifyAdmin, getApprovedBookings); // Get all rejected bookings router.get("/bookings/rejected", verifyAdmin, getRejectedBookings); -router.route("/bookedSlots").get(getDateSlots); +router.route("/bookedSlots").get(verifyJWT, getDateSlots); export default router; diff --git a/backend/src/routes/professor.routes.js b/backend/src/routes/professor.routes.js index 210f45a..6102ea8 100644 --- a/backend/src/routes/professor.routes.js +++ b/backend/src/routes/professor.routes.js @@ -5,8 +5,8 @@ import { requestIpMiddleware, } from "../middlewares/ratelimiter.middleware.js"; import { - verifyJWT, verifyAdmin, + verifyAnyAuth, verifyProfessor, } from "../middlewares/auth.middleware.js"; import { @@ -56,12 +56,14 @@ import { const router = Router(); router.route("/addprof").post(verifyAdmin, addProf); -router.route("/getProf").get(getProf); +router.route("/getProf").get(verifyAnyAuth, getProf); router.post("/save-summer-project-title", verifyProfessor, saveSummerProjectTitle); -router.route("/login").post(loginProf); +const profAuthLimiter = createRateLimiter({ windowMs: 15 * 60 * 1000, max: 10 }); +const profOtpLimiter = createRateLimiter({ windowMs: 15 * 60 * 1000, max: 5 }); +router.route("/login").post(requestIpMiddleware, profAuthLimiter, loginProf); router.route("/logout").post(verifyProfessor, logoutProf); router.route("/generate-auto-login").post(verifyAdmin, generateAutoLoginUrl); router.route("/auto-login").post(autoLoginProf); @@ -86,7 +88,7 @@ router.route("/deny-group").post(verifyProfessor, denyGroup); router.route("/accept-group").post(verifyProfessor, acceptGroup); router.route("/add-remark").post(verifyProfessor, addRemark); router.route("/meet-attend").post(verifyProfessor, groupAttendance); -router.route("/forgot-pass").post(otpForgotPassword); +router.route("/forgot-pass").post(requestIpMiddleware, profOtpLimiter, otpForgotPassword); const changePassLimiter = createRateLimiter({ windowMs: 15 * 60 * 1000, max: 5, diff --git a/backend/src/routes/user.routes.js b/backend/src/routes/user.routes.js index 91312e0..63d3dda 100644 --- a/backend/src/routes/user.routes.js +++ b/backend/src/routes/user.routes.js @@ -30,7 +30,7 @@ import { } from "../controllers/interview.controller.js"; import { getUserCompanies } from "../controllers/company.controller.js"; import { upload } from "../middlewares/multer.middleware.js"; -import { verifyJWT, verifyAdmin } from "../middlewares/auth.middleware.js"; +import { verifyJWT, verifyAdmin, verifyAnyAuth, optionalAuth } from "../middlewares/auth.middleware.js"; import { getAllBacklogSubjects } from "../controllers/backlog.controller.js"; import { @@ -39,13 +39,18 @@ import { } from "../middlewares/ratelimiter.middleware.js"; const router = Router(); -router.route("/verifyMail").post(verifyMail); + +// Rate limiters for auth routes +const authLimiter = createRateLimiter({ windowMs: 15 * 60 * 1000, max: 10 }); +const otpLimiter = createRateLimiter({ windowMs: 15 * 60 * 1000, max: 5 }); + +router.route("/verifyMail").post(requestIpMiddleware, otpLimiter, verifyMail); router .route("/register") - .post(upload.fields([{ name: "idCard", maxCount: 1 }]), registerUser); + .post(requestIpMiddleware, authLimiter, upload.fields([{ name: "idCard", maxCount: 1 }]), registerUser); -router.route("/login").post(loginUser); +router.route("/login").post(requestIpMiddleware, authLimiter, loginUser); router.route("/logout").post(verifyJWT, logoutUser); router.route("/update").patch( verifyJWT, @@ -63,7 +68,7 @@ router upload.fields([{ name: "doc", maxCount: 1 }]), updatePlacementOne ); -router.route("/getbyroll").post(getUserbyRoll); +router.route("/getbyroll").post(optionalAuth, getUserbyRoll); router .route("/ptwo") .patch( @@ -86,7 +91,7 @@ router.route("/placementTwo").get(verifyJWT, getPlacementTwo); router.route("/placementThree").get(verifyJWT, getPlacementThree); router.route("/get-all-users").get(verifyAdmin, getAllUsers); router.route("/get-backlogs").get(verifyJWT, getAllBacklogSubjects); -router.route("/get-pass-otp").post(otpForgotPass); +router.route("/get-pass-otp").post(requestIpMiddleware, otpLimiter, otpForgotPass); const changePassLimiter = createRateLimiter({ windowMs: 15 * 60 * 1000, max: 5, diff --git a/backend/src/utils/Socket.js b/backend/src/utils/Socket.js index 1ec7e3e..de3105a 100644 --- a/backend/src/utils/Socket.js +++ b/backend/src/utils/Socket.js @@ -1,35 +1,119 @@ +import jwt from "jsonwebtoken"; import { Chat } from "../models/chat.model.js"; +import { Group } from "../models/group.model.js"; +import { Minor } from "../models/minor.model.js"; +import { Major } from "../models/major.model.js"; +import { User } from "../models/user.model.js"; +import { Professor } from "../models/professor.model.js"; let io; +const findGroupByGroupId = async (groupId) => { + return ( + (await Group.findOne({ groupId })) || + (await Minor.findOne({ groupId })) || + (await Major.findOne({ groupId })) + ); +}; + +const checkGroupMembership = (group, userId) => { + const uid = userId.toString(); + return ( + (group.leader && group.leader.toString() === uid) || + (group.members && + group.members.some((m) => m.toString() === uid)) || + (group.summerAppliedProfs && + group.summerAppliedProfs.some((p) => p.toString() === uid)) || + (group.summerAllocatedProf && + group.summerAllocatedProf.toString() === uid) || + (group.minorAllocatedProf && + group.minorAllocatedProf.toString() === uid) || + (group.majorAllocatedProf && + group.majorAllocatedProf.toString() === uid) + ); +}; + export const initSocket = (ioInstance) => { io = ioInstance; + + // Authentication middleware — require valid JWT (cookie-based or token-based) + io.use(async (socket, next) => { + const token = + socket.handshake.auth?.token || + socket.handshake.headers?.authorization?.replace("Bearer ", ""); + if (!token) { + return next(new Error("Authentication required")); + } + try { + const decoded = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET); + const userId = decoded._id || decoded.id; + + // Resolve user's full name for chat display + const user = await User.findById(userId).select("fullName"); + const professor = user ? null : await Professor.findById(userId).select("fullName"); + if (!user && !professor) { + return next(new Error("User not found")); + } + + socket.userId = userId; + socket.userFullName = user?.fullName || professor?.fullName; + next(); + } catch (err) { + return next(new Error("Invalid or expired token")); + } + }); + io.on("connection", (socket) => { - socket.on("joinRoom", (roomId) => { - socket.join(roomId); + socket.on("joinRoom", async (roomId) => { + try { + const group = await findGroupByGroupId(roomId); + if (!group) { + socket.emit("roomError", "Group not found"); + return; + } + const isMember = checkGroupMembership(group, socket.userId); + if (!isMember) { + socket.emit("roomError", "Not authorized to join this room"); + return; + } + socket.join(roomId); + } catch (error) { + socket.emit("roomError", "Failed to join room"); + } }); socket.on("sendMessage", async (data) => { try { const { roomId, message } = data; - const { sender, text } = message; + const { text } = message; + + const group = await findGroupByGroupId(roomId); + if (!group) { + socket.emit("messageError", "Group not found"); + return; + } + if (!checkGroupMembership(group, socket.userId)) { + socket.emit("messageError", "Not authorized to send messages in this room"); + return; + } let chat = await Chat.findOne({ groupId: roomId }); if (!chat) { chat = new Chat({ groupId: roomId, messages: [] }); } - const newMessage = { sender: sender, text, date: new Date() }; + // Use server-derived sender name for security (prevents spoofing) + const sender = socket.userFullName; + const newMessage = { sender, text, date: new Date() }; chat.messages.push(newMessage); await chat.save(); socket.to(roomId).emit("newMessage", newMessage); } catch (error) { - console.error("Error saving message:", error); socket.emit("messageError", "Failed to send message"); } }); socket.on("disconnect", () => { - console.log("Socket disconnected:", socket.id); + // Socket disconnected }); }); }; diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index ab8ff24..706ac99 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -120,6 +120,7 @@ export default function Header() { try { const response = await axios.post('/api/v1/users/logout'); localStorage.removeItem('user'); + localStorage.removeItem('accessToken'); navigate('/'); } catch (error) { console.error(error); diff --git a/frontend/src/components/HeaderAdmin.jsx b/frontend/src/components/HeaderAdmin.jsx index e9348a8..c26b85a 100644 --- a/frontend/src/components/HeaderAdmin.jsx +++ b/frontend/src/components/HeaderAdmin.jsx @@ -181,6 +181,7 @@ export default function Header() { try { const resp = await axios.post('/api/v1/admin/logout'); localStorage.removeItem('user'); + localStorage.removeItem('accessToken'); navigate('/'); } catch (err) { console.error(err); diff --git a/frontend/src/components/HeaderFaculty.jsx b/frontend/src/components/HeaderFaculty.jsx index 3a04e9d..7eaa30c 100644 --- a/frontend/src/components/HeaderFaculty.jsx +++ b/frontend/src/components/HeaderFaculty.jsx @@ -70,6 +70,7 @@ export default function Header() { try { const resp = await axios.post('/api/v1/prof/logout'); localStorage.removeItem('faculty'); + localStorage.removeItem('accessToken'); navigate('/'); } catch (err) { console.error(err); diff --git a/frontend/src/components/Login.jsx b/frontend/src/components/Login.jsx index de219df..ad4268a 100644 --- a/frontend/src/components/Login.jsx +++ b/frontend/src/components/Login.jsx @@ -44,6 +44,9 @@ export default function Login() { }); localStorage.setItem("user", JSON.stringify(response.data.data.user)); + if (response.data.data.accessToken) { + localStorage.setItem("accessToken", response.data.data.accessToken); + } toast.success("Login successful!"); setTimeout(() => { navigate("/db"); diff --git a/frontend/src/components/LoginFaculty.jsx b/frontend/src/components/LoginFaculty.jsx index 6bab6ec..0a7dff2 100644 --- a/frontend/src/components/LoginFaculty.jsx +++ b/frontend/src/components/LoginFaculty.jsx @@ -47,6 +47,9 @@ export default function LoginFaculty() { "faculty", JSON.stringify(response.data.data.professor) ); + if (response.data.data.accessToken) { + localStorage.setItem("accessToken", response.data.data.accessToken); + } setProfessorData(response.data.data.professor); toast.success("Login Successful!"); diff --git a/frontend/src/components/Loginadmin.jsx b/frontend/src/components/Loginadmin.jsx index b1dd0d3..dd9788a 100644 --- a/frontend/src/components/Loginadmin.jsx +++ b/frontend/src/components/Loginadmin.jsx @@ -19,6 +19,9 @@ export default function SignInPage() { password, }); localStorage.setItem("user", JSON.stringify(response.data.data.admin)); + if (response.data.data.accessToken) { + localStorage.setItem("accessToken", response.data.data.accessToken); + } // console.log(response); // Show success toast diff --git a/frontend/src/components/SideBarAdmin.jsx b/frontend/src/components/SideBarAdmin.jsx index ba2a9c3..0dcd07a 100644 --- a/frontend/src/components/SideBarAdmin.jsx +++ b/frontend/src/components/SideBarAdmin.jsx @@ -207,6 +207,7 @@ export default function Sidebar() { const resp = await axios.post("/api/v1/admin/logout"); // console.log(resp); localStorage.removeItem("user"); + localStorage.removeItem("accessToken"); navigate("/"); } catch (err) { // console.log(err); diff --git a/frontend/src/components/SideBarFaculty.jsx b/frontend/src/components/SideBarFaculty.jsx index d3cff84..e50f632 100644 --- a/frontend/src/components/SideBarFaculty.jsx +++ b/frontend/src/components/SideBarFaculty.jsx @@ -53,6 +53,7 @@ export default function Sidebar() { const resp = await axios.post("/api/v1/prof/logout"); // console.log(resp); localStorage.removeItem("faculty"); + localStorage.removeItem("accessToken"); navigate("/"); } catch (err) { // console.log(err); diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 6405ad5..bc0838e 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -170,6 +170,7 @@ export default function Sidebar() { const response = await axios.post("/api/v1/users/logout"); // console.log(response); localStorage.removeItem("user"); + localStorage.removeItem("accessToken"); navigate("/"); } catch (error) { // console.log(error); diff --git a/frontend/src/socket.js b/frontend/src/socket.js index c83fb74..19c38a9 100644 --- a/frontend/src/socket.js +++ b/frontend/src/socket.js @@ -2,18 +2,29 @@ import io from "socket.io-client"; let socket = null; +const getAccessToken = () => { + // Try cookie first (primary auth method) + const match = document.cookie.match(/(?:^|;\s*)accessToken=([^;]*)/); + if (match) return match[1]; + // Fallback to localStorage (used by FacultyAutoLogin) + return localStorage.getItem("accessToken") || null; +}; + const createSocket = () => { if (!socket) { const API_URL = import.meta.env.VITE_CORS || "http://localhost:8000"; + const token = getAccessToken(); socket = io(API_URL, { withCredentials: true, - secure: true, // Force secure connection - transports: ["websocket"], // WebSocket only mode + secure: true, + transports: ["websocket"], reconnectionAttempts: 5, reconnectionDelay: 1000, timeout: 20000, agent: false, - reconnectionAttempts: 5, + auth: { + token: token, + }, }); socket.on("connect_error", (err) => { console.error("Connection error:", err.message);