diff --git a/backend/db.js b/backend/config/db.js similarity index 85% rename from backend/db.js rename to backend/config/db.js index 18af6fca..cf506a7b 100644 --- a/backend/db.js +++ b/backend/config/db.js @@ -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); diff --git a/backend/models/passportConfig.js b/backend/config/passportConfig.js similarity index 90% rename from backend/models/passportConfig.js rename to backend/config/passportConfig.js index 82cb533f..fa34b295 100644 --- a/backend/models/passportConfig.js +++ b/backend/config/passportConfig.js @@ -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( diff --git a/backend/controllers/certificateController.js b/backend/controllers/certificateController.js new file mode 100644 index 00000000..8a39dfab --- /dev/null +++ b/backend/controllers/certificateController.js @@ -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" }); + } + + 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 }); + }catch(err){ + console.error(err); + return res.status(500).json({message: "Internal server error"}); + } +} + +module.exports = { + createBatch, +}; diff --git a/backend/index.js b/backend/index.js index 6fa25570..b513dd25 100644 --- a/backend/index.js +++ b/backend/index.js @@ -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"); @@ -18,8 +18,8 @@ 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") { @@ -27,11 +27,13 @@ if (process.env.NODE_ENV === "production") { } 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({ @@ -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, () => { diff --git a/backend/middlewares/isAuthenticated.js b/backend/middlewares/isAuthenticated.js index f04c46ef..5879f018 100644 --- a/backend/middlewares/isAuthenticated.js +++ b/backend/middlewares/isAuthenticated.js @@ -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" }); + } +} + +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 + }); + +*/ diff --git a/backend/models/certificateSchema.js b/backend/models/certificateSchema.js new file mode 100644 index 00000000..c6da58c0 --- /dev/null +++ b/backend/models/certificateSchema.js @@ -0,0 +1,151 @@ +const mongoose = require("mongoose"); + +const certificateBatchSchema = new mongoose.Schema( + { + title: { type: String, required: true }, + unit_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "Organizational_Unit", + }, + commonData: { type: Map, of: String, required: true }, + templateId: { type: String, required: true }, + initiatedBy: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + approverIds: { + type: [ + { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, + ], + required: true, + }, + status: { + type: String, + enum: ["PendingL1", "PendingL2", "Processed", "Rejected", "Processing"], + default: "PendingL1", + }, + users: { + type: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }], + required: true, + }, + }, + { + timestamps: true, + }, +); + +const certificateSchema = new mongoose.Schema( + { + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + batchId: { + type: mongoose.Schema.Types.ObjectId, + ref: "CertificateBatch", + required: true, + }, + orgId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Organizational_Unit", + required: true, + }, + status: { + type: String, + required: true, + enum: ["Pending", "Approved", "Rejected"], + default: "Pending", + }, + rejectionReason: { + type: String, + required: function () { + return this.status === "Rejected"; + }, + }, + certificateUrl: { + type: String, + required: function () { + return this.status === "Approved"; + }, + }, + certificateId: { + type: String, + //unique: true, + required: function () { + return this.status === "Approved"; + }, + }, + }, + { + timestamps: true, + }, +); + +/** + * certificateId unique constraint will reject multiple non-approved docs. + * With unique: true, multiple pending records without certificateId can trigger duplicate-key errors. + * Use a partial unique index instead and remove unique: true from the field. + */ + +certificateSchema.index( + { certificateId: 1 }, + { + unique: true, + partialFilterExpression: { certificateId: { $exists: true } }, + }, +); + +//Indexed to serve the purpose of "Get pending batches for the logged-in approver." +/* + +_id approverIds status +1 [A, B, C] PendingL1 +2 [B, D] PendingL1 +3 [A, D] PendingL2 +4 [B] PendingL1 + +Index entries for B + +approverIds _id +B 1 +B 2 +B 4 + +*/ +certificateBatchSchema.index( + { approverIds: 1 }, + { partialFilterExpression: { status: { $in: ["PendingL1", "PendingL2"] } } }, +); + +//This is done to ensure that within each batch only 1 certificate is issued per userId. +certificateSchema.index({ batchId: 1, userId: 1 }, { unique: true }); + +//This index is for this purpose -> Get all approved certificates for the logged-in student. + +certificateSchema.index( + { userId: 1, certificateId: 1 }, + { partialFilterExpression: { certificateId: { $exists: true } } }, +); + +const CertificateBatch = mongoose.model( + "CertificateBatch", + certificateBatchSchema, +); +const Certificate = mongoose.model("Certificate", certificateSchema); + +module.exports = { + CertificateBatch, + Certificate, +}; + +/* + +if i use partialFilter when querying i have to specify its filter condition so mongodb uses that index +so here +certificateBatchSchema.index({approverIds: 1}, {partialFilterExpression: { status: {$in: ["PendingL1", "PendingL2"]}}} ) +i need to do +CertificateBatch.find({approverIds: id, status: {$in: ["PendingL1", "PendingL2"]} } ) + +*/ diff --git a/backend/models/schema.js b/backend/models/schema.js index 400bc856..0f4e57aa 100644 --- a/backend/models/schema.js +++ b/backend/models/schema.js @@ -1,89 +1,90 @@ const mongoose = require("mongoose"); -const passportLocalMongoose = require("passport-local-mongoose"); -var findOrCreate = require("mongoose-findorcreate"); -//user collection -const userSchema = new mongoose.Schema({ - user_id: { - type: String, - }, - role: { - type: String, - required: true, - }, - strategy: { - type: String, - enum: ["local", "google"], - required: true, - }, - username: { - type: String, - required: true, - unique: true, - }, - onboardingComplete: { - type: Boolean, - default: false, - }, - personal_info: { - name: { +const userSchema = new mongoose.Schema( + { + user_id: { + type: String, + }, + role: { type: String, required: true, }, - email: { + strategy: { type: String, + enum: ["local", "google"], + required: true, }, - phone: String, - date_of_birth: Date, - gender: String, - - profilePic: { + username: { type: String, - default: "https://www.gravatar.com/avatar/?d=mp", + required: true, + unique: true, }, - - cloudinaryUrl: { + password: { type: String, - default: "", + required: function () { + return this.strategy === "local"; + }, + minLength: 8, }, - }, + onboardingComplete: { + type: Boolean, + default: false, + }, + personal_info: { + name: { + type: String, + required: true, + }, + email: { + type: String, + }, + phone: String, + date_of_birth: Date, + gender: String, - academic_info: { - program: { - type: String, - //enum: ["B.Tech", "M.Tech", "PhD", "Msc","other"], + profilePic: { + type: String, + default: "https://www.gravatar.com/avatar/?d=mp", + }, + + cloudinaryUrl: { + type: String, + default: "", + }, }, - branch: String, - batch_year: String, - current_year: String, - cgpa: Number, - }, - contact_info: { - hostel: String, - room_number: String, - socialLinks: { - github: { type: String, default: "" }, - linkedin: { type: String, default: "" }, - instagram: { type: String, default: "" }, - other: { type: String, default: "" }, + academic_info: { + program: { + type: String, + //enum: ["B.Tech", "M.Tech", "PhD", "Msc","other"], + }, + branch: String, + batch_year: String, + current_year: String, + cgpa: Number, }, - }, - status: { - type: String, - enum: ["active", "inactive", "graduated"], - default: "active", - }, - created_at: { - type: Date, - default: Date.now, + contact_info: { + hostel: String, + room_number: String, + socialLinks: { + github: { type: String, default: "" }, + linkedin: { type: String, default: "" }, + instagram: { type: String, default: "" }, + other: { type: String, default: "" }, + }, + }, + + status: { + type: String, + enum: ["active", "inactive", "graduated"], + default: "active", + }, }, - updated_at: { - type: Date, - default: Date.now, + { + timestamps: true, }, -}); +); userSchema.index( { user_id: 1 }, @@ -94,138 +95,129 @@ userSchema.index( }, ); -userSchema.plugin(passportLocalMongoose); -userSchema.plugin(findOrCreate); - //organizational unit -const organizationalUnitSchema = new mongoose.Schema({ - unit_id: { - type: String, - required: true, - unique: true, - }, - name: { - type: String, - required: true, - unique: true, - }, - type: { - type: String, - enum: ["Council", "Club", "Committee", "independent_position"], - required: true, - }, - description: { - type: String, - }, - parent_unit_id: { - type: mongoose.Schema.Types.ObjectId, - ref: "Organizational_Unit", - default: null, - }, - hierarchy_level: { - type: Number, - required: true, - }, - category: { - type: String, - enum: ["cultural", "scitech", "sports", "academic", "independent"], - required: true, - }, - is_active: { - type: Boolean, - default: true, - }, - contact_info: { - email: { +const organizationalUnitSchema = new mongoose.Schema( + { + unit_id: { type: String, required: true, unique: true, }, - social_media: [ - { - platform: { - type: String, - required: true, - }, - url: { - type: String, - required: true, - }, - }, - ], - }, - budget_info: { - allocated_budget: { - type: Number, - default: 0, + name: { + type: String, + required: true, + unique: true, }, - spent_amount: { + type: { + type: String, + enum: ["Council", "Club", "Committee", "independent_position"], + required: true, + }, + description: { + type: String, + }, + parent_unit_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "Organizational_Unit", + default: null, + }, + hierarchy_level: { type: Number, - default: 0, + required: true, + }, + category: { + type: String, + enum: ["cultural", "scitech", "sports", "academic", "independent"], + required: true, + }, + is_active: { + type: Boolean, + default: true, + }, + contact_info: { + email: { + type: String, + required: true, + unique: true, + }, + social_media: [ + { + platform: { + type: String, + required: true, + }, + url: { + type: String, + required: true, + }, + }, + ], + }, + budget_info: { + allocated_budget: { + type: Number, + default: 0, + }, + spent_amount: { + type: Number, + default: 0, + }, }, }, - created_at: { - type: Date, - default: Date.now, - }, - updated_at: { - type: Date, - default: Date.now, - }, -}); + { timestamps: true }, +); //position -const positionSchema = new mongoose.Schema({ - position_id: { - type: String, - required: true, - unique: true, - }, - title: { - type: String, - required: true, - }, - unit_id: { - type: mongoose.Schema.Types.ObjectId, - ref: "Organizational_Unit", - required: true, - }, - position_type: { - type: String, - required: true, - }, - responsibilities: [ - { +const positionSchema = new mongoose.Schema( + { + position_id: { type: String, + required: true, + unique: true, }, - ], - requirements: { - min_cgpa: { - type: Number, - default: 0, + title: { + type: String, + required: true, }, - min_year: { - type: Number, - default: 1, + unit_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "Organizational_Unit", + required: true, }, - skills_required: [ + position_type: { + type: String, + required: true, + }, + responsibilities: [ { type: String, }, ], + requirements: { + min_cgpa: { + type: Number, + default: 0, + }, + min_year: { + type: Number, + default: 1, + }, + skills_required: [ + { + type: String, + }, + ], + }, + description: { + type: String, + }, + position_count: { + type: Number, + }, }, - description: { - type: String, - }, - position_count: { - type: Number, - }, - created_at: { - type: Date, - default: Date.now, - }, -}); + { timestamps: true }, +); //position holder collection; const positionHolderSchema = new mongoose.Schema({ diff --git a/backend/package-lock.json b/backend/package-lock.json index 1d66737d..de1d73ef 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,9 +10,10 @@ "license": "ISC", "dependencies": { "axios": "^1.5.1", - "body-parser": "^1.20.2", + "bcrypt": "^6.0.0", "cloudinary": "^2.6.1", "connect-mongodb-session": "^3.1.1", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.3.2", "eslint-plugin-react": "^7.33.2", @@ -25,7 +26,6 @@ "moment": "^2.30.1", "mongodb": "^6.1.0", "mongoose": "^7.6.8", - "mongoose-findorcreate": "^4.0.0", "morgan": "^1.10.0", "multer": "^2.0.1", "nodemailer": "^7.0.3", @@ -33,19 +33,16 @@ "npm": "^10.2.0", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", - "passport-local": "^1.0.0", - "passport-local-mongoose": "^8.0.0", "streamifier": "^0.1.1", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "zod": "^4.3.6" }, "devDependencies": { "cookie": "^0.5.0", - "cookie-parser": "^1.4.6", "eslint": "^8.56.0", "eslint-plugin-node": "^11.1.0", "husky": "^8.0.3", "lint-staged": "^15.2.0", - "parser": "^0.1.4", "prettier": "^3.1.1" } }, @@ -1803,6 +1800,20 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1811,29 +1822,6 @@ "node": ">=8" } }, - "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/bowser": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", @@ -2206,12 +2194,12 @@ } }, "node_modules/cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", - "dev": true, + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", "dependencies": { - "cookie": "0.4.1", + "cookie": "0.7.2", "cookie-signature": "1.0.6" }, "engines": { @@ -2219,10 +2207,10 @@ } }, "node_modules/cookie-parser/node_modules/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", - "dev": true, + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2325,12 +2313,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/disect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/disect/-/disect-1.1.1.tgz", - "integrity": "sha512-rr2Ym8FSAoqAJ1KfpUiQ/Io01HP0LZPHBuppbFsHozmSNf+YwrvyD5pm5tMTUApJFNwD7HeWJ5DGldSugScukA==", - "dev": true - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -3166,11 +3148,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/generaterr": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/generaterr/-/generaterr-1.5.0.tgz", - "integrity": "sha512-JgcGRv2yUKeboLvvNrq9Bm90P4iJBu7/vd5wSLYqMG5GJ6SxZT46LAAkMfNhQ+EK3jzC+cRBm7P8aUWYyphgcQ==" - }, "node_modules/get-east-asian-width": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", @@ -4473,11 +4450,6 @@ "url": "https://opencollective.com/mongoose" } }, - "node_modules/mongoose-findorcreate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mongoose-findorcreate/-/mongoose-findorcreate-4.0.0.tgz", - "integrity": "sha512-wi0vrTmazWBeZn8wHVdb8NEa+ZrAbnmfI8QltnFeIgvC33VlnooapvPSk21W22IEhs0vZ0cBz0MmXcc7eTTSZQ==" - }, "node_modules/mongoose/node_modules/@types/whatwg-url": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", @@ -4682,6 +4654,26 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/nodemailer": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz", @@ -7753,18 +7745,6 @@ "node": ">=6" } }, - "node_modules/parser": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/parser/-/parser-0.1.4.tgz", - "integrity": "sha512-f6EM/mBtPzmIh96MpcbePfhkBOYRmLYWuOukJqMysMlvjp4s2MQSSQnFEekd9GV4JGTnDJ2uFt3Ztcqc9wCMJg==", - "dev": true, - "dependencies": { - "tokenizer": "*" - }, - "engines": { - "node": "0.4-0.9" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -7801,30 +7781,6 @@ "node": ">= 0.4.0" } }, - "node_modules/passport-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", - "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", - "dependencies": { - "passport-strategy": "1.x.x" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/passport-local-mongoose": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/passport-local-mongoose/-/passport-local-mongoose-8.0.0.tgz", - "integrity": "sha512-jgfN/B0j11WT5f96QlL5EBvxbIwmzd+tbwPzG1Vk8hzDOF68jrch5M+NFvrHjWjb3lfAU0DkxKmNRT9BjFZysQ==", - "dependencies": { - "generaterr": "^1.5.0", - "passport-local": "^1.0.0", - "scmp": "^2.1.0" - }, - "engines": { - "node": ">= 8.0.0" - } - }, "node_modules/passport-oauth2": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", @@ -8037,20 +7993,6 @@ "node": ">= 0.6" } }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -8303,11 +8245,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "node_modules/scmp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", - "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -8756,18 +8693,6 @@ "node": ">=0.6" } }, - "node_modules/tokenizer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/tokenizer/-/tokenizer-1.1.2.tgz", - "integrity": "sha512-c/EYsBwEW/EX28q44UaSrJ9o5M2aI+N/xdJJ4Zl7dNq76OmWQHhmXH0T8DJQNjVYPc7NclV2CZQfyeUMfnEu/A==", - "dev": true, - "dependencies": { - "disect": "~1.1.0" - }, - "engines": { - "node": "0.10.x" - } - }, "node_modules/touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -9188,6 +9113,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/backend/package.json b/backend/package.json index 3814a629..5a93b741 100644 --- a/backend/package.json +++ b/backend/package.json @@ -30,9 +30,10 @@ "license": "ISC", "dependencies": { "axios": "^1.5.1", - "body-parser": "^1.20.2", + "bcrypt": "^6.0.0", "cloudinary": "^2.6.1", "connect-mongodb-session": "^3.1.1", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.3.2", "eslint-plugin-react": "^7.33.2", @@ -45,7 +46,6 @@ "moment": "^2.30.1", "mongodb": "^6.1.0", "mongoose": "^7.6.8", - "mongoose-findorcreate": "^4.0.0", "morgan": "^1.10.0", "multer": "^2.0.1", "nodemailer": "^7.0.3", @@ -53,19 +53,16 @@ "npm": "^10.2.0", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", - "passport-local": "^1.0.0", - "passport-local-mongoose": "^8.0.0", "streamifier": "^0.1.1", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "zod": "^4.3.6" }, "devDependencies": { "cookie": "^0.5.0", - "cookie-parser": "^1.4.6", "eslint": "^8.56.0", "eslint-plugin-node": "^11.1.0", "husky": "^8.0.3", "lint-staged": "^15.2.0", - "parser": "^0.1.4", "prettier": "^3.1.1" } } diff --git a/backend/routes/achievements.js b/backend/routes/achievements.js index 026bc288..1357ee0b 100644 --- a/backend/routes/achievements.js +++ b/backend/routes/achievements.js @@ -2,7 +2,7 @@ const express = require("express"); const router = express.Router(); const { Achievement } = require("../models/schema"); // Update path as needed const { v4: uuidv4 } = require("uuid"); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); const authorizeRole = require("../middlewares/authorizeRole"); const { ROLE_GROUPS } = require("../utils/roles"); diff --git a/backend/routes/analytics.js b/backend/routes/analytics.js index bf31fc49..981745aa 100644 --- a/backend/routes/analytics.js +++ b/backend/routes/analytics.js @@ -1,20 +1,40 @@ -const express = require('express'); +const express = require("express"); const router = express.Router(); -const controller = require('../controllers/analyticsController'); -const isAuthenticated = require('../middlewares/isAuthenticated'); -const authorizeRole = require('../middlewares/authorizeRole'); -const {ROLE_GROUPS} = require('../utils/roles'); +const controller = require("../controllers/analyticsController"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); +const authorizeRole = require("../middlewares/authorizeRole"); +const { ROLE_GROUPS } = require("../utils/roles"); // Route to get analytics for president -router.get('/president', isAuthenticated, authorizeRole(['PRESIDENT']), controller.getPresidentAnalytics); +router.get( + "/president", + isAuthenticated, + authorizeRole(["PRESIDENT"]), + controller.getPresidentAnalytics, +); // Route to get analytics for gensecs -router.get('/gensec', isAuthenticated,authorizeRole([...ROLE_GROUPS.GENSECS]), controller.getGensecAnalytics); +router.get( + "/gensec", + isAuthenticated, + authorizeRole([...ROLE_GROUPS.GENSECS]), + controller.getGensecAnalytics, +); // Route to get analytics for club coordinators -router.get('/club-coordinator',authorizeRole(['CLUB_COORDINATOR']), isAuthenticated, controller.getClubCoordinatorAnalytics); +router.get( + "/club-coordinator", + authorizeRole(["CLUB_COORDINATOR"]), + isAuthenticated, + controller.getClubCoordinatorAnalytics, +); // Route to get analytics for students -router.get('/student', isAuthenticated,authorizeRole(['STUDENT']), controller.getStudentAnalytics); +router.get( + "/student", + isAuthenticated, + authorizeRole(["STUDENT"]), + controller.getStudentAnalytics, +); module.exports = router; diff --git a/backend/routes/announcements.js b/backend/routes/announcements.js index c4f5ae9f..25f62154 100644 --- a/backend/routes/announcements.js +++ b/backend/routes/announcements.js @@ -7,7 +7,7 @@ const { OrganizationalUnit, Position, } = require("../models/schema"); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); const findTargetId = async (type, identifier) => { let target = null; diff --git a/backend/routes/auth.js b/backend/routes/auth.js index c2ee6f7b..7abf42e7 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,13 +1,17 @@ const express = require("express"); const router = express.Router(); const jwt = require("jsonwebtoken"); -//const secretKey = process.env.JWT_SECRET_TOKEN; -const isIITBhilaiEmail = require("../utils/isIITBhilaiEmail"); -const passport = require("../models/passportConfig"); + +//const isIITBhilaiEmail = require("../utils/isIITBhilaiEmail"); + +const { loginValidate, registerValidate } = require("../utils/authValidate"); +const passport = require("../config/passportConfig"); const rateLimit = require("express-rate-limit"); var nodemailer = require("nodemailer"); const { User } = require("../models/schema"); -const isAuthenticated= require("../middlewares/isAuthenticated"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); + +const bcrypt = require("bcrypt"); //rate limiter - for password reset try const forgotPasswordLimiter = rateLimit({ @@ -17,7 +21,7 @@ const forgotPasswordLimiter = rateLimit({ }); // Session Status -router.get("/fetchAuth",isAuthenticated, function (req, res) { +router.get("/fetchAuth", isAuthenticated, function (req, res) { if (req.isAuthenticated()) { res.json(req.user); } else { @@ -25,59 +29,89 @@ router.get("/fetchAuth",isAuthenticated, function (req, res) { } }); -// Local Authentication -router.post("/login", passport.authenticate("local"), (req, res) => { - // If authentication is successful, this function will be called - const email = req.user.username; - if (!isIITBhilaiEmail(email)) { - console.log("Access denied. Please use your IIT Bhilai email."); - return res.status(403).json({ - message: "Access denied. Please use your IIT Bhilai email.", +router.post("/login", async (req, res) => { + try { + const { username, password } = req.body; + const result = loginValidate.safeParse({ username, password }); + + if (!result.success) { + return res + .status(400) + .json({ message: result.error.message || "Invalid data sent" }); + } + + const user = await User.findOne({ username }); + if (!user) { + return res.status(401).json({ message: "Invalid user credentials" }); + } + + const isValid = await bcrypt.compare(password, user.password); + if (!isValid) { + return res.status(401).json({ message: "Invalid user credentials" }); + } + + const payload = { + id: user._id.toString(), + }; + + //console.log(payload); + + const token = jwt.sign(payload, process.env.JWT_SECRET_TOKEN, { + expiresIn: "1h", + }); + + res.cookie("token", token, { + maxAge: 60 * 60 * 1000, + httpOnly: true, + sameSite: "lax", }); + + res.json({ message: "Login Successful" }); + } catch (err) { + return res.status(500).json({ message: err.message }); } - res.status(200).json({ message: "Login successful", user: req.user }); }); router.post("/register", async (req, res) => { try { - const { name, ID, email, password } = req.body; - if (!isIITBhilaiEmail(email)) { - return res.status(400).json({ - message: "Invalid email address. Please use an IIT Bhilai email.", - }); + const { username, password, user_id, name, role } = req.body; + const result = registerValidate.safeParse({ + username, + password, + user_id, + name, + role, + }); + + if (!result.success) { + return res.status(400).json({ message: result.error.message }); } - const existingUser = await User.findOne({ username: email }); - if (existingUser) { - return res.status(400).json({ message: "User already exists." }); + + const user = await User.findOne({ username }); + if (user) { + return res + .json(401) + .json({ message: "Account with username already exists" }); } - const newUser = await User.register( - new User({ - user_id: ID, - role: "STUDENT", - strategy: "local", - username: email, - personal_info: { - name: name, - email: email, - }, - onboardingComplete: false, - }), - password, - ); + const salt = Number(process.env.SALT) || 12; + const hashedPassword = await bcrypt.hash(password, salt); - req.login(newUser, (err) => { - if (err) { - console.error(err); - return res.status(400).json({ message: "Bad request." }); - } - return res - .status(200) - .json({ message: "Registration successful", user: newUser }); + await User.create({ + user_id, + role, + strategy: "local", + username, + password: hashedPassword, + personal_info: { + name, + email: username, + }, }); - } catch (error) { - console.error("Registration error:", error); - return res.status(500).json({ message: "Internal server error" }); + + return res.json({ message: "Registered Successfully" }); + } catch (err) { + return res.status(500).json({ message: err.message }); } }); diff --git a/backend/routes/certificateRoutes.js b/backend/routes/certificateRoutes.js new file mode 100644 index 00000000..0116d033 --- /dev/null +++ b/backend/routes/certificateRoutes.js @@ -0,0 +1,8 @@ +const router = require("express").Router(); +const { createBatch } = require("../controllers/certificateController"); + +const { jwtIsAuthenticated } = require("../middlewares/isAuthenticated"); + +router.post("/", jwtIsAuthenticated, createBatch); + +module.exports = router; diff --git a/backend/routes/dashboard.js b/backend/routes/dashboard.js index 43846500..2ce8a818 100644 --- a/backend/routes/dashboard.js +++ b/backend/routes/dashboard.js @@ -1,8 +1,8 @@ -const express = require('express'); +const express = require("express"); const router = express.Router(); -const dashboardController = require('../controllers/dashboardController'); -const isAuthenticated = require('../middlewares/isAuthenticated'); +const dashboardController = require("../controllers/dashboardController"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); -router.get('/stats',isAuthenticated, dashboardController.getDashboardStats); +router.get("/stats", isAuthenticated, dashboardController.getDashboardStats); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/routes/events.js b/backend/routes/events.js index 4bf5dd92..dfe8cdb6 100644 --- a/backend/routes/events.js +++ b/backend/routes/events.js @@ -2,7 +2,7 @@ const express = require("express"); const router = express.Router(); const { Event, User, OrganizationalUnit } = require("../models/schema"); const { v4: uuidv4 } = require("uuid"); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); const isEventContact = require("../middlewares/isEventContact"); const authorizeRole = require("../middlewares/authorizeRole"); const { ROLE_GROUPS, ROLES } = require("../utils/roles"); @@ -221,7 +221,6 @@ router.post( return res.status(400).json({ message: "Registration has ended." }); } - const maxParticipants = event.registration.max_participants; if (maxParticipants) { const updatedEvent = await Event.findOneAndUpdate( @@ -255,8 +254,8 @@ router.post( event: updatedEvent, }); } catch (error) { - if (error?.name === "CastError") { - return res.status(400).json({ message: "Invalid event ID format." }); + if (error.name === "CastError") { + return res.status(400).json({ message: "Invalid event ID format." }); } console.error("Event registration error:", error); return res diff --git a/backend/routes/feedbackRoutes.js b/backend/routes/feedbackRoutes.js index d3e52386..863bc5cb 100644 --- a/backend/routes/feedbackRoutes.js +++ b/backend/routes/feedbackRoutes.js @@ -1,6 +1,6 @@ const express = require("express"); const router = express.Router(); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); const { User, Feedback, @@ -12,7 +12,7 @@ const { v4: uuidv4 } = require("uuid"); const authorizeRole = require("../middlewares/authorizeRole"); const { ROLE_GROUPS } = require("../utils/roles"); -router.post("/add",isAuthenticated, async (req, res) => { +router.post("/add", isAuthenticated, async (req, res) => { try { const { type, @@ -28,19 +28,19 @@ router.post("/add",isAuthenticated, async (req, res) => { return res.status(400).json({ message: "Missing required fields" }); } - const targetModels={ + const targetModels = { User, Event, "Club/Organization": OrganizationalUnit, POR: Position, }; - const TargetModel=targetModels[target_type]; + const TargetModel = targetModels[target_type]; - if(!TargetModel){ - return res.status(400).json({message:"Invalid target type"}); + if (!TargetModel) { + return res.status(400).json({ message: "Invalid target type" }); } - + const feedback = new Feedback({ feedback_id: uuidv4(), type, @@ -63,9 +63,12 @@ router.post("/add",isAuthenticated, async (req, res) => { } }); -router.get("/get-targetid",isAuthenticated, async (req, res) => { +router.get("/get-targetid", isAuthenticated, async (req, res) => { try { - const users = await User.find({role: "STUDENT"}, "_id user_id personal_info.name"); + const users = await User.find( + { role: "STUDENT" }, + "_id user_id personal_info.name", + ); const events = await Event.find({}, "_id title"); const organizational_units = await OrganizationalUnit.find({}, "_id name"); const positions = await Position.find({}) @@ -178,42 +181,47 @@ router.get("/view-feedback", async (req, res) => { }); // requires user middleware that attaches user info to req.user -router.put("/mark-resolved/:id",isAuthenticated,authorizeRole(ROLE_GROUPS.ADMIN), async (req, res) => { - const feedbackId = req.params.id; - const { actions_taken, resolved_by } = req.body; - console.log(req.body); - console.log("User resolving feedback:", resolved_by); - - if (!actions_taken || actions_taken.trim() === "") { - return res.status(400).json({ error: "Resolution comment is required." }); - } - - try { - const feedback = await Feedback.findById(feedbackId); - if (!feedback) { - return res.status(404).json({ error: "Feedback not found" }); +router.put( + "/mark-resolved/:id", + isAuthenticated, + authorizeRole(ROLE_GROUPS.ADMIN), + async (req, res) => { + const feedbackId = req.params.id; + const { actions_taken, resolved_by } = req.body; + console.log(req.body); + console.log("User resolving feedback:", resolved_by); + + if (!actions_taken || actions_taken.trim() === "") { + return res.status(400).json({ error: "Resolution comment is required." }); } - if (feedback.is_resolved) { - return res.status(400).json({ error: "Feedback is already resolved." }); - } + try { + const feedback = await Feedback.findById(feedbackId); + if (!feedback) { + return res.status(404).json({ error: "Feedback not found" }); + } - feedback.is_resolved = true; - feedback.resolved_at = new Date(); - feedback.actions_taken = actions_taken; - feedback.resolved_by = resolved_by; + if (feedback.is_resolved) { + return res.status(400).json({ error: "Feedback is already resolved." }); + } - await feedback.save(); + feedback.is_resolved = true; + feedback.resolved_at = new Date(); + feedback.actions_taken = actions_taken; + feedback.resolved_by = resolved_by; - res.json({ success: true, message: "Feedback marked as resolved." }); - } catch (err) { - console.error("Error updating feedback:", err); - res.status(500).json({ error: "Server error" }); - } -}); + await feedback.save(); + + res.json({ success: true, message: "Feedback marked as resolved." }); + } catch (err) { + console.error("Error updating feedback:", err); + res.status(500).json({ error: "Server error" }); + } + }, +); //get all user given feedbacks -router.get("/:userId",isAuthenticated, async (req, res) => { +router.get("/:userId", isAuthenticated, async (req, res) => { const userId = req.params.userId; try { const userFeedbacks = await Feedback.find({ feedback_by: userId }).populate( diff --git a/backend/routes/onboarding.js b/backend/routes/onboarding.js index dca690d2..9eba705c 100644 --- a/backend/routes/onboarding.js +++ b/backend/routes/onboarding.js @@ -1,10 +1,10 @@ const express = require("express"); const router = express.Router(); const { User } = require("../models/schema"); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); // Onboarding route - to be called when user logs in for the first time -router.post("/",isAuthenticated, async (req, res) => { +router.post("/", isAuthenticated, async (req, res) => { const { ID_No, add_year, Program, discipline, mobile_no } = req.body; try { diff --git a/backend/routes/orgUnit.js b/backend/routes/orgUnit.js index 2c71597b..3b19fd2a 100644 --- a/backend/routes/orgUnit.js +++ b/backend/routes/orgUnit.js @@ -12,7 +12,7 @@ const { Feedback, User, } = require("../models/schema"); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); const authorizeRole = require("../middlewares/authorizeRole"); const { ROLE_GROUPS } = require("../utils/roles"); diff --git a/backend/routes/positionRoutes.js b/backend/routes/positionRoutes.js index d25e32f4..2a5d77e3 100644 --- a/backend/routes/positionRoutes.js +++ b/backend/routes/positionRoutes.js @@ -2,7 +2,7 @@ const express = require("express"); const router = express.Router(); const { Position, PositionHolder } = require("../models/schema"); const { v4: uuidv4 } = require("uuid"); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); // POST for adding a new position router.post("/add-position", isAuthenticated, async (req, res) => { diff --git a/backend/routes/profile.js b/backend/routes/profile.js index db94cae5..fc138583 100644 --- a/backend/routes/profile.js +++ b/backend/routes/profile.js @@ -6,7 +6,7 @@ const cloudinary = require("cloudinary").v2; //const { Student } = require("../models/student"); const { User } = require("../models/schema"); const streamifier = require("streamifier"); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); // Cloudinary config cloudinary.config({ cloud_name: process.env.CLOUDINARY_CLOUD_NAME, @@ -15,15 +15,20 @@ cloudinary.config({ }); router.post( - "/photo-update",isAuthenticated, + "/photo-update", + isAuthenticated, upload.fields([{ name: "image" }]), async (req, res) => { try { const { ID_No } = req.body; - if (!ID_No) { return res.status(400).json({ error: "ID_No is required" }); } + if (!ID_No) { + return res.status(400).json({ error: "ID_No is required" }); + } const user = await User.findOne({ user_id: ID_No }); - if (!user) { return res.status(404).json({ error: "User not found" });} + if (!user) { + return res.status(404).json({ error: "User not found" }); + } if ( !req.files || @@ -46,8 +51,11 @@ router.post( let stream = cloudinary.uploader.upload_stream( { folder: "profile-photos" }, (error, result) => { - if (result) { resolve(result);} - else { reject(error); } + if (result) { + resolve(result); + } else { + reject(error); + } }, ); streamifier.createReadStream(fileBuffer).pipe(stream); @@ -69,13 +77,17 @@ router.post( ); // Delete profile photo (reset to default) -router.delete("/photo-delete",isAuthenticated, async (req, res) => { +router.delete("/photo-delete", isAuthenticated, async (req, res) => { try { const { ID_No } = req.query; // Get ID_No from frontend for DELETE - if (!ID_No) { return res.status(400).json({ error: "ID_No is required" }); } + if (!ID_No) { + return res.status(400).json({ error: "ID_No is required" }); + } const user = await user.findOne({ user_id: ID_No }); - if (!user) { return res.status(404).json({ error: "User not found" }); } + if (!user) { + return res.status(404).json({ error: "User not found" }); + } // Delete from Cloudinary if exists if (user.personal_info.cloudinaryUrl) { @@ -91,8 +103,8 @@ router.delete("/photo-delete",isAuthenticated, async (req, res) => { } }); -// API to Update Student Profile -router.put("/updateStudentProfile",isAuthenticated, async (req, res) => { +// API to Update Student Profile +router.put("/updateStudentProfile", isAuthenticated, async (req, res) => { try { const { userId, updatedDetails } = req.body; console.log("Received userId:", userId); @@ -124,13 +136,27 @@ router.put("/updateStudentProfile",isAuthenticated, async (req, res) => { cloudinaryUrl, } = updatedDetails.personal_info; - if (name) { user.personal_info.name = name; } - if (email) { user.personal_info.email = email; } - if (phone) { user.personal_info.phone = phone; } - if (gender) { user.personal_info.gender = gender; } - if (date_of_birth) { user.personal_info.date_of_birth = date_of_birth; } - if (profilePic) { user.personal_info.profilePic = profilePic; } - if (cloudinaryUrl) { user.personal_info.cloudinaryUrl = cloudinaryUrl; } + if (name) { + user.personal_info.name = name; + } + if (email) { + user.personal_info.email = email; + } + if (phone) { + user.personal_info.phone = phone; + } + if (gender) { + user.personal_info.gender = gender; + } + if (date_of_birth) { + user.personal_info.date_of_birth = date_of_birth; + } + if (profilePic) { + user.personal_info.profilePic = profilePic; + } + if (cloudinaryUrl) { + user.personal_info.cloudinaryUrl = cloudinaryUrl; + } } // ---------- ACADEMIC INFO ---------- @@ -138,19 +164,33 @@ router.put("/updateStudentProfile",isAuthenticated, async (req, res) => { const { program, branch, batch_year, current_year, cgpa } = updatedDetails.academic_info; - if (program) { user.academic_info.program = program; } - if (branch) { user.academic_info.branch = branch; } - if (batch_year) { user.academic_info.batch_year = batch_year; } - if (current_year) { user.academic_info.current_year = current_year; } - if (cgpa !== undefined) { user.academic_info.cgpa = cgpa; } + if (program) { + user.academic_info.program = program; + } + if (branch) { + user.academic_info.branch = branch; + } + if (batch_year) { + user.academic_info.batch_year = batch_year; + } + if (current_year) { + user.academic_info.current_year = current_year; + } + if (cgpa !== undefined) { + user.academic_info.cgpa = cgpa; + } } // ---------- CONTACT INFO ---------- if (updatedDetails.contact_info) { const { hostel, room_number, socialLinks } = updatedDetails.contact_info; - if (hostel) { user.contact_info.hostel = hostel; } - if (room_number) { user.contact_info.room_number = room_number; } + if (hostel) { + user.contact_info.hostel = hostel; + } + if (room_number) { + user.contact_info.room_number = room_number; + } // Social Links if (socialLinks) { diff --git a/backend/routes/skillsRoutes.js b/backend/routes/skillsRoutes.js index 04d1bb11..8a2863ac 100644 --- a/backend/routes/skillsRoutes.js +++ b/backend/routes/skillsRoutes.js @@ -2,7 +2,7 @@ const express = require("express"); const router = express.Router(); const { UserSkill, Skill } = require("../models/schema"); const { v4: uuidv4 } = require("uuid"); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); const authorizeRole = require("../middlewares/authorizeRole"); const { ROLE_GROUPS } = require("../utils/roles"); // GET unendorsed user skills for a particular skill type diff --git a/backend/seed.js b/backend/seed.js index 61a65273..aa667f4e 100644 --- a/backend/seed.js +++ b/backend/seed.js @@ -71,19 +71,6 @@ const seedOrganizationalUnits = async () => { await presidentUnit.save(); console.log("Created President Unit."); - const testPresidentUnit = new OrganizationalUnit({ - unit_id: "PRESIDENT_GYMKHANA_TEST", - name: "Test President, Student Gymkhana", - type: "independent_position", - description: "The test president for the Student Gymkhana.", - parent_unit_id: null, - hierarchy_level: 0, - category: "independent", - contact_info: { email: "test_president_gymkhana@iitbhilai.ac.in", social_media: [] }, - }); - await testPresidentUnit.save(); - console.log("Created Test President Unit."); - // 2. Create the main councils (Gensecs) and link them to the President const mainCouncilsData = [ { unit_id: "COUNCIL_CULTURAL", name: "Cultural Council", type: "Council", description: "Council for all cultural activities.", hierarchy_level: 1, category: "cultural", contact_info: { email: "gensec_cultural_gymkhana@iitbhilai.ac.in", social_media: [] }, parent_unit_id: presidentUnit._id }, @@ -109,7 +96,8 @@ const seedOrganizationalUnits = async () => { await OrganizationalUnit.insertMany(linkedUnitsData); console.log("Seeded and linked initial clubs and committees."); - // 4. Create and link the test councils and clubs + /** + * // 4. Create and link the test councils and clubs const testCouncilsData = [ { unit_id: "COUNCIL_CULTURAL_TEST", name: "Test Cultural Council", type: "Council", description: "Test council for cultural activities.", hierarchy_level: 1, category: "cultural", contact_info: { email: "test_gensec_cult@iitbhilai.ac.in", social_media: [] }, parent_unit_id: presidentUnit._id }, { unit_id: "COUNCIL_SCITECH_TEST", name: "Test SciTech Council", type: "Council", description: "Test council for scitech activities.", hierarchy_level: 1, category: "scitech", contact_info: { email: "test_gensec_scitech@iitbhilai.ac.in", social_media: [] }, parent_unit_id: presidentUnit._id }, @@ -135,7 +123,8 @@ const seedOrganizationalUnits = async () => { console.log("Seeded and linked Test Clubs."); console.log("Organizational Units seeded successfully!"); -}; + */ +} /** * Seeds the User collection based on Organizational Units and adds test students. @@ -166,7 +155,7 @@ const seedUsers = async () => { role = "STUDENT"; } - const userData = { + let userData = { username: unit.contact_info.email, role: role, onboardingComplete: true, @@ -174,15 +163,10 @@ const seedUsers = async () => { name: unit.name, email: unit.contact_info.email, }, + strategy: "google" }; - if (unit.unit_id.includes("_TEST") || unit.name.includes("Test")) { - userData.strategy = "local"; - localAuthUsers.push(userData); - } else { - userData.strategy = "google"; - googleAuthUsers.push(userData); - } + googleAuthUsers.push(userData); } // Add 10 dummy student users with local auth and correct email domain @@ -207,6 +191,7 @@ const seedUsers = async () => { localAuthUsers.push({ username: userEmail, + password: password, role: "STUDENT", strategy: "local", onboardingComplete: true, diff --git a/backend/utils/authValidate.js b/backend/utils/authValidate.js new file mode 100644 index 00000000..e8b8bd92 --- /dev/null +++ b/backend/utils/authValidate.js @@ -0,0 +1,19 @@ +const zod = require("zod"); + +const loginValidate = zod.object({ + username: zod.string().regex(/^[a-zA-Z0-9._%+-]+@iitbhilai\.ac\.in$/i), + password: zod.string().min(8), +}); + +const registerValidate = zod.object({ + username: zod.string().regex(/^[a-zA-Z0-9._%+-]+@iitbhilai\.ac\.in$/i), + password: zod.string().min(8), + user_id: zod.string().min(2), + name: zod.string().min(5), + role: zod.string().min(5), +}); + +module.exports = { + loginValidate, + registerValidate, +}; diff --git a/backend/utils/batchValidate.js b/backend/utils/batchValidate.js new file mode 100644 index 00000000..3b06a828 --- /dev/null +++ b/backend/utils/batchValidate.js @@ -0,0 +1,16 @@ +const zod = require("zod"); + +const zodObjectId = zod.string().regex(/^[a-f0-9]{24}$/, "Invalid ObjectId"); + +const validateBatchSchema = zod.object({ + title: zod.string().min(5, "Title must be atleast 5 characters"), + unit_id: zodObjectId, + commonData: zod.record(zod.string(), zod.string()), + template_id: zod.string().min(1, "Template ID is required"), + users: zod.array(zodObjectId).min(1, "Atl east 1 user must be associated."), +}); + +module.exports = { + validateBatchSchema, + zodObjectId, +};