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/config/passportConfig.js b/backend/config/passportConfig.js new file mode 100644 index 00000000..1fc30ff9 --- /dev/null +++ b/backend/config/passportConfig.js @@ -0,0 +1,116 @@ +const passport = require("passport"); +const GoogleStrategy = require("passport-google-oauth20").Strategy; +const LocalStrategy = require("passport-local").Strategy; +const isIITBhilaiEmail = require("../utils/isIITBhilaiEmail"); +const User = require("../models/userSchema"); +const { loginValidate } = require("../utils/authValidate"); +const bcrypt = require("bcrypt"); +// Google OAuth Strategy +passport.use( + new GoogleStrategy( + { + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: `${process.env.BACKEND_URL}/auth/google/verify`, // Update with your callback URL + }, + async (accessToken, refreshToken, profile, done) => { + // Check if the user already exists in your database + const email = profile.emails?.[0]?.value; + if (!email) { + //console.log("No email found in Google profile"); + return done(null, false, { message: "Email not available from Google." }); + } + + if (!isIITBhilaiEmail(profile.emails[0].value)) { + console.log("Google OAuth blocked for: ", profile.emails[0].value); + return done(null, false, { + message: "Only @iitbhilai.ac.in emails are allowed.", + }); + } + try { + const user = await User.findOne({ username: email }); + //console.log("Looking for existing user with email:", email, "Found:", !!user); + + if (user) { + // If user exists, return the user + //console.log("Returning existing user:", user.username); + return done(null, user); + } + // If user doesn't exist, create a new user in your database + const newUser = await User.create({ + username: email, + role: "STUDENT", + strategy: "google", + personal_info: { + name: profile.displayName || "No Name", + email: email, + profilePic: + profile.photos && profile.photos.length > 0 + ? profile.photos[0].value + : "https://www.gravatar.com/avatar/?d=mp", + }, + onboardingComplete: false, + }); + //console.log("User is",newUser); + return done(null, newUser); + } catch (error) { + console.error("Error in Google strategy:", error); + return done(error); + } + }, + ), +); + +//Local Strategy +passport.use(new LocalStrategy(async (username, password, done) => { + + const result = loginValidate.safeParse({ username, password }); + + if (!result.success) { + let errors = result.error.issues.map((issue) => issue.message); + return done(null, false, {message: errors}); + } + + try{ + + const user = await User.findOne({ username }); + if (!user) { + return done(null, false, {message: "Invalid user credentials"}); + } + + + if (user.strategy !== "local" || !user.password) { + return done(null, false, { message: "Invalid login method" }); + } + + const isValid = await bcrypt.compare(password, user.password); + if (!isValid) { + return done(null, false, { message: "Invalid user credentials" }); + } + return done(null, user); + }catch(err){ + return done(err); + } + +})); + + +//When login succeeds this will run +// serialize basically converts user obj into a format that can be transmitted(like a string, etc...) +// here take user obj and done callback and store only userId in session +passport.serializeUser((user, done) => { + done(null, user._id.toString()); +}); + +//When a request comes in, take the stored id, fetch full user from DB, and attach it to req.user. +passport.deserializeUser(async (id, done) => { + try { + let user = await User.findById(id); + if(!user) return done(null, false); + done(null, user); + } catch (err) { + done(err, null); + } +}); + +module.exports = passport; diff --git a/backend/controllers/analyticsController.js b/backend/controllers/analyticsController.js index d7e4e982..47ca6fbc 100644 --- a/backend/controllers/analyticsController.js +++ b/backend/controllers/analyticsController.js @@ -1,7 +1,13 @@ -const {User, Achievement, UserSkill, Event, Position, PositionHolder,OrganizationalUnit}=require('../models/schema'); const mongoose = require("mongoose"); const getCurrentTenureRange = require('../utils/getTenureRange'); +const User = require("../models/userSchema"); +const Achievement = require("../models/achievementSchema"); +const Position = require("../models/positionSchema"); +const PositionHolder = require("../models/positionHolderSchema"); +const OrganizationalUnit = require("../models/organizationSchema"); +const Event = require("../models/eventSchema"); +const { UserSkill } = require("../models/schema"); exports.getPresidentAnalytics= async (req,res) => { try { diff --git a/backend/controllers/dashboardController.js b/backend/controllers/dashboardController.js index 193c6b26..c513aa91 100644 --- a/backend/controllers/dashboardController.js +++ b/backend/controllers/dashboardController.js @@ -1,14 +1,11 @@ // controllers/dashboardController.js -const { - Feedback, - Achievement, - UserSkill, - Skill, - Event, - PositionHolder, - Position, - OrganizationalUnit, -} = require("../models/schema"); +const Feedback = require("../models/feedbackSchema"); +const Achievement = require("../models/achievementSchema"); +const Position = require("../models/positionSchema"); +const PositionHolder = require("../models/positionHolderSchema"); +const OrganizationalUnit = require("../models/organizationSchema"); +const Event = require("../models/eventSchema"); +const { UserSkill, Skill } = require("../models/schema"); const ROLES = { PRESIDENT: "PRESIDENT", diff --git a/backend/controllers/eventControllers.js b/backend/controllers/eventControllers.js index 60832b0e..83573585 100644 --- a/backend/controllers/eventControllers.js +++ b/backend/controllers/eventControllers.js @@ -1,17 +1,20 @@ -const {Event} = require('../models/schema'); +const Event = require('../models/eventSchema'); // fetch 4 most recently updated events exports.getLatestEvents = async (req, res) => { try{ const latestEvents = await Event.find({}) - .sort({updated_at: -1}) + .sort({updatedAt: -1}) .limit(4) - .select('title updated_at schedule.venue status'); + .select('title updatedAt schedule.venue status'); + if(!latestEvents){ + return res.status(404).json({message: "No events are created"}); + } const formatedEvents =latestEvents.map(event=>({ id: event._id, title: event.title, - date: event.updated_at.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), + date: event.updatedAt?.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), venue: (event.schedule && event.schedule.venue) ? event.schedule.venue : 'TBA', status: event.status || 'TBD' })) diff --git a/backend/controllers/taskController.js b/backend/controllers/taskController.js new file mode 100644 index 00000000..6ced00c0 --- /dev/null +++ b/backend/controllers/taskController.js @@ -0,0 +1,232 @@ +const Task = require("../models/taskSchema"); +const User = require("../models/userSchema"); +const PositionHolder = require("../models/positionHolderSchema"); +const OrganizationalUnit = require("../models/organizationSchema"); +const Position = require("../models/positionSchema"); + +const { taskValidate } = require("../utils/taskValidate"); + +async function getTasks(req, res) { + try { + //console.log(req.user); + const id = req.user._id; + const user = await User.findById(id); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + const tasks = await Task.find({ + $or: [{ assignees: user._id }, { assigned_by: user._id }], + }).populate([ + { + path: "assigned_by", + select: "personal_info", + }, + { + path: "assignees", + select: "personal_info.name", + }, + ]); + + if (!tasks || tasks.length === 0) { + return res.status(404).json({ message: "No tasks found" }); + } + res.json({ message: tasks }); + } catch (error) { + console.error(error); + res.status(500).json({ message: error.message }); + } +} + +async function updateTask(req, res) { + try { + const taskId = req.params.taskId; + const _id = req.user._id; + + const { status, submission_note, admin_notes } = req.body; + + if (!["pending", "in-progress", "under-review", "completed"].includes(status)) { + return res.status(400).json({ message: "Invalid status" }); + } + + const task = await Task.findById(taskId); + if (!task) { + return res.status(404).json({ message: "Task not found" }); + } + + const isAssigner = task.assigned_by.toString() === _id.toString(); + const isAssignee = task.assignees.some((a) => a.toString() === _id.toString()); + + if(!isAssigner && !isAssignee){ + return res.status(403).json({message: "You are not authorized to perform the update"}) + } + + if (isAssignee) { + const validAssigneeMove = + (task.status === "pending" && status === "in-progress") || + (task.status === "in-progress" && status === "under-review"); + if (!validAssigneeMove) { + return res.status(403).json({ message: "Invalid assignee transition" }); + } + if (status === "under-review" && !submission_note) { + return res.status(400).json({ message: "submission_note is required" }); + } + } + + if (isAssigner) { + const validAssignerMove = + task.status === "under-review" && + (status === "completed" || status === "in-progress"); + if (!validAssignerMove) { + return res.status(403).json({ message: "Invalid assigner transition" }); + } + } + + + task.status = status; + if (status) task.status = status; + if (submission_note) task.submission_note = submission_note; + if (admin_notes) task.admin_notes = admin_notes; + + await task.save(); + res.json({ message: "Task updated successfully" }); + } catch (err) { + console.error(err); + res.status(500).json({ message: err.message }); + } +} + +async function createTask(req, res) { + try { + const userId = req.user._id; + const { title, description, deadline, priority, assignees } = req.body; + const validation = taskValidate.safeParse({ + title, + description, + deadline, + priority, + assignees, + }); + + if (!validation.success) { + let errors = validation.error.issues.map((issue) => issue.message); + return res.status(400).json({ message: errors }); + } + + const user = await User.findById(userId); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + const unit = await PositionHolder.findOne({ user_id: userId }).populate({ + path: "position_id", + select: "unit_id", + }); + if (!unit) { + return res.status(404).json({ message: "Unit not found" }); + } + console.log("Unit:", unit); + + const unitId = unit?.position_id?.unit_id; + if (!unitId) { + return res.status(404).json({ message: "Unit Id not found" }); + } + console.log("Unit Id:", unitId); + + const existingUserIds = await User.find({ _id: { $in: assignees } }) + .select("_id") + .lean(); + const existingSet = new Set(existingUserIds.map((u) => u._id)); + if (existingSet.size !== assignees.length) { + return res.status(400).json({ message: "Invalid assignees data" }); + } + + await Task.create({ + title, + description, + deadline, + priority, + status: "pending", + progress: 0, + assigned_by: userId, + assignees, + unit_id: unitId, + }); + return res.json({ message: "Task created successfully" }); + } catch (err) { + console.error(err); + res.status(500).json({ message: err.message }); + } +} + +async function getTaskUsers(req, res) { + try { + const id = req.user._id; + const user = await User.findById(id); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + const role = user.role?.toUpperCase(); + if (role === "PRESIDENT") { + const userIds = (await PositionHolder.find().select("user_id")).map((u) => u.user_id); + const users = await User.find({_id: {$in: userIds}}).select("personal_info role"); + return res.json({ message: users }); + } + + const unit = await PositionHolder.findOne({ user_id: id }).populate({ + path: "position_id", + select: "unit_id", + }); + + if (!unit) { + return res.status(404).json({ message: "Unit not found" }); + } + const unitId = unit?.position_id?.unit_id; + + if (!unitId) { + return res.status(404).json({ message: "Unit Id not found" }); + } + + const categoryDoc = await (OrganizationalUnit.findById(unitId).select("category")); + const category = categoryDoc?.category; + if (!category) { + return res.status(404).json({ message: "Category not found" }); + } + + if (role.startsWith("GENSEC")) { + const categoryOrgsIds = ( await OrganizationalUnit.find({ category: category }).select("_id")).map((org) => org._id); + + const positionsIds = (await Position.find({ unit_id: { $in: categoryOrgsIds } }).select("_id")).map((p) => p._id); + + const userIds = (await PositionHolder.find({position_id: { $in: positionsIds }}).select("user_id")).map((u) => u._id); + + const users = await User.find({ _id: { $in: userIds } }).select("personal_info, role"); + return res.json({ message: users }); + } + + if (role === "CLUB_COORDINATOR") { + const positions = ( + await Position.find({ unit_id: unitId }).select("_id") + ).map((p) => p._id); + + const userIds = ( + await PositionHolder.find({ position_id: { $in: positions } }).select( + "user_id", + ) + ).map((u) => u.user_id); + + const users = await User.find({ _id: { $in: userIds } }).select( + "personal_info role", + ); + + return res.json({ message: users }); + } + return res.json({ message: [] }); + } catch (err) { + console.error(err); + res.status(500).json({ message: err.message || "Internal server error" }); + } +} + +module.exports = { getTasks, updateTask, createTask, getTaskUsers }; diff --git a/backend/index.js b/backend/index.js index b1cc49c1..51ffe567 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,13 +1,14 @@ const express = require("express"); require("dotenv").config(); // eslint-disable-next-line node/no-unpublished-require +const { connectDB } = require("./config/db.js"); +const MongoStore = require("connect-mongo"); +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 +19,9 @@ 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 taskRoutes = require("./routes/task.routes.js"); + const porRoutes = require("./routes/por.js"); const roomBookingRoutes = require("./routes/roomBooking.js"); const app = express(); @@ -30,23 +32,31 @@ if (process.env.NODE_ENV === "production") { app.use(cors({ origin: process.env.FRONTEND_URL, credentials: true })); -// Connect to MongoDB -connectDB(); +app.use(cookieParser()); -app.use(bodyParser.json()); +//Replaced bodyParser with express.json() - the new standard +app.use(express.json()); app.use( session({ - secret: "keyboard cat", + secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === "production", // HTTPS only in prod - sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", // cross-origin in prod, + sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", // cross-origin in prod + maxAge: 60 * 60 * 1000, }, + store: MongoStore.create({ + mongoUrl: process.env.MONGODB_URI, + ttl: 60 * 60, + collectionName: "sessions", + }), + name: "token", }), ); +//Needed to initialize passport and all helper methods to req object app.use(myPassport.initialize()); app.use(myPassport.session()); @@ -67,12 +77,17 @@ app.use("/api/positions", positionsRoutes); app.use("/api/orgUnit", organizationalUnitRoutes); app.use("/api/announcements", announcementRoutes); app.use("/api/dashboard", dashboardRoutes); -app.use("/api/announcements", announcementRoutes); app.use("/api/analytics", analyticsRoutes); -app.use("/api/rooms", roomBookingRoutes); +app.use("/api/tasks", taskRoutes); app.use("/api/por", porRoutes); +app.use("/api/rooms", roomBookingRoutes); // Start the server -app.listen(process.env.PORT || 8000, () => { - console.log(`connected to port ${process.env.PORT || 8000}`); -}); + +(async function () { + // Connect to MongoDB + await connectDB(); + app.listen(process.env.PORT || 8000, () => { + console.log(`connected to port ${process.env.PORT || 8000}`); + }); +})(); diff --git a/backend/middlewares/isAuthenticated.js b/backend/middlewares/isAuthenticated.js index f04c46ef..25d1b169 100644 --- a/backend/middlewares/isAuthenticated.js +++ b/backend/middlewares/isAuthenticated.js @@ -1,7 +1,84 @@ +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]; + */ + + token = req.cookies.token; + if(!token){ + return res.status(401).json({message: "User not authenticated"}); + } + + 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/achievementSchema.js b/backend/models/achievementSchema.js new file mode 100644 index 00000000..9dbf6be8 --- /dev/null +++ b/backend/models/achievementSchema.js @@ -0,0 +1,54 @@ +const mongoose = require("mongoose"); +//achievements collection +const achievementSchema = new mongoose.Schema({ + achievement_id: { + type: String, + required: true, + unique: true, + }, + user_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + title: { + type: String, + required: true, + }, + description: String, + category: { + type: String, + required: true, + }, + type: { + type: String, + }, + level: { + type: String, + }, + date_achieved: { + type: Date, + required: true, + }, + position: { + type: String, + }, + event_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "Event", + default: null, // optional + }, + certificate_url: String, + verified: { + type: Boolean, + default: false, + }, + verified_by: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + default: null, + }, +}, { timestamps: true}); + +const Achievement = mongoose.model("Achievement", achievementSchema); +module.exports = Achievement; diff --git a/backend/models/certificateSchema.js b/backend/models/certificateSchema.js new file mode 100644 index 00000000..5cb5fcd6 --- /dev/null +++ b/backend/models/certificateSchema.js @@ -0,0 +1,126 @@ +const mongoose = require("mongoose"); + +const certificateBatchSchema = new mongoose.Schema( + { + title: { type: String, required: true }, + unit_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "Oraganizational_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: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, + ], + status: { + type: String, + enum: ["PendingL1", "PendingL2", "Processed", "Rejected", "Processing"], + default: "PendingL1", + }, + users: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }], + }, + { + 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, + }, + 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, + }, +); + +//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/eventSchema.js b/backend/models/eventSchema.js new file mode 100644 index 00000000..c51cde37 --- /dev/null +++ b/backend/models/eventSchema.js @@ -0,0 +1,111 @@ +const mongoose = require("mongoose"); + +//events collection +const eventSchema = new mongoose.Schema({ + event_id: { + type: String, + required: true, + unique: true, + }, + title: { + type: String, + required: true, + }, + description: String, + category: { + type: String, + enum: ["cultural", "technical", "sports", "academic", "other"], + }, + type: { + type: String, + }, + organizing_unit_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "Organizational_Unit", + required: true, + }, + organizers: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + }, + ], + schedule: { + start: Date, + end: Date, + venue: String, + mode: { + type: String, + enum: ["online", "offline", "hybrid"], + }, + }, + registration: { + required: Boolean, + start: Date, + end: Date, + fees: Number, + max_participants: Number, + }, + budget: { + allocated: Number, + spent: Number, + sponsors: [ + { + type: String, + }, + ], + }, + status: { + type: String, + enum: ["planned", "ongoing", "completed", "cancelled"], + default: "planned", + }, + participants: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + }, + ], + winners: [ + { + user: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + }, + position: String, // e.g., "1st", "2nd", "Best Speaker", etc. + }, + ], + feedback_summary: { + type: Object, // You can define structure if fixed + }, + media: { + images: [String], + videos: [String], + documents: [String], + }, + room_requests: [ + { + date: { type: Date, required: true }, + time: { type: String, required: true }, + room: { type: String, required: true }, + description: { type: String }, + status: { + type: String, + enum: ["Pending", "Approved", "Rejected"], + default: "Pending", + }, + requested_at: { + type: Date, + default: Date.now, + }, + reviewed_by: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + }, + }, + ], + +}, { timestamps: true}); + +const Event = mongoose.model("Event", eventSchema); +module.exports = Event; diff --git a/backend/models/feedbackSchema.js b/backend/models/feedbackSchema.js new file mode 100644 index 00000000..29abe91f --- /dev/null +++ b/backend/models/feedbackSchema.js @@ -0,0 +1,69 @@ +const mongoose = require("mongoose"); + +//feedback collection +const feedbackSchema = new mongoose.Schema({ + feedback_id: { + type: String, + required: true, + unique: true, + }, + type: { + type: String, + required: true, + }, + target_id: { + type: mongoose.Schema.Types.ObjectId, + //required: true, + // We'll dynamically interpret this field based on target_type + }, + target_type: { + type: String, + required: true, + }, + feedback_by: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + // category: { + // type: String, + // enum: ['organization', 'communication', 'leadership'], + // required: true + // }, + rating: { + type: Number, + min: 1, + max: 5, + }, + comments: { + type: String, + }, + is_anonymous: { + type: Boolean, + default: false, + }, + is_resolved: { + type: Boolean, + default: false, + }, + actions_taken: { + type: String, + default: "", + }, + created_at: { + type: Date, + default: Date.now, + }, + resolved_at: { + type: Date, + default: null, + }, + resolved_by: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + default: null, + }, +}); + +const Feedback = mongoose.model("Feedback", feedbackSchema); +module.exports = Feedback; diff --git a/backend/models/organizationSchema.js b/backend/models/organizationSchema.js new file mode 100644 index 00000000..7bdc0523 --- /dev/null +++ b/backend/models/organizationSchema.js @@ -0,0 +1,77 @@ +const mongoose = require("mongoose"); + +//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: { + 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, + }, + }, +}, {timestamps: true}); + +const OrganizationalUnit = mongoose.model( + "Organizational_Unit", + organizationalUnitSchema, +); + +module.exports = OrganizationalUnit; diff --git a/backend/models/passportConfig.js b/backend/models/passportConfig.js index 82cb533f..07c9fe8b 100644 --- a/backend/models/passportConfig.js +++ b/backend/models/passportConfig.js @@ -1,17 +1,8 @@ const passport = require("passport"); -const LocalStrategy = require("passport-local"); +//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("./userSchema"); // Google OAuth Strategy passport.use( diff --git a/backend/models/positionHolderSchema.js b/backend/models/positionHolderSchema.js new file mode 100644 index 00000000..65406d03 --- /dev/null +++ b/backend/models/positionHolderSchema.js @@ -0,0 +1,58 @@ +const mongoose = require("mongoose"); + +//position holder collection; +const positionHolderSchema = new mongoose.Schema( + { + por_id: { + type: String, + required: true, + unique: true, + }, + user_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + position_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "Position", + required: true, + }, + + tenure_year: { + type: String, + required: true, + }, + appointment_details: { + appointed_by: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + }, + appointment_date: { + type: Date, + }, + }, + performance_metrics: { + events_organized: { + type: Number, + default: 0, + }, + budget_utilized: { + type: Number, + default: 0, + }, + feedback: { + type: String, + }, + }, + status: { + type: String, + enum: ["active", "completed", "terminated"], + required: true, + }, + }, + { timestamps: true }, +); + +const PositionHolder = mongoose.model("Position_Holder", positionHolderSchema); +module.exports = PositionHolder; diff --git a/backend/models/positionSchema.js b/backend/models/positionSchema.js new file mode 100644 index 00000000..6aa46ec6 --- /dev/null +++ b/backend/models/positionSchema.js @@ -0,0 +1,53 @@ +const mongoose = require("mongoose"); + +//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: [ + { + 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, + }, + +}, {timestamps: true}); + +const Position = mongoose.model("Position", positionSchema); +module.exports = Position; diff --git a/backend/models/schema.js b/backend/models/schema.js index 05ebd00b..b4abbfc7 100644 --- a/backend/models/schema.js +++ b/backend/models/schema.js @@ -1,408 +1,6 @@ const mongoose = require("mongoose"); -const passportLocalMongoose = require("passport-local-mongoose"); -var findOrCreate = require("mongoose-findorcreate"); -const { v4: uuidv4 } = require("uuid"); -//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: { - type: String, - required: true, - }, - email: { - type: String, - }, - phone: String, - date_of_birth: Date, - gender: String, - - profilePic: { - type: String, - default: "https://www.gravatar.com/avatar/?d=mp", - }, - - cloudinaryUrl: { - 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, - }, - - 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", - }, - created_at: { - type: Date, - default: Date.now, - }, - updated_at: { - type: Date, - default: Date.now, - }, -}); - -userSchema.index( - { user_id: 1 }, - { - unique: true, - partialFilterExpression: { user_id: { $exists: true, $type: "string" } }, - name: "user_id_partial_unique", - }, -); - -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: { - 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, - }, -}); - -//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: [ - { - 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, - }, - created_at: { - type: Date, - default: Date.now, - }, -}); - -//position holder collection; -const positionHolderSchema = new mongoose.Schema({ - por_id: { - type: String, - required: true, - unique: true, - }, - user_id: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - required: true, - }, - position_id: { - type: mongoose.Schema.Types.ObjectId, - ref: "Position", - required: true, - }, - - tenure_year: { - type: String, - required: true, - }, - appointment_details: { - appointed_by: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - }, - appointment_date: { - type: Date, - }, - }, - performance_metrics: { - events_organized: { - type: Number, - default: 0, - }, - budget_utilized: { - type: Number, - default: 0, - }, - feedback: { - type: String, - }, - }, - status: { - type: String, - enum: ["active", "completed", "terminated"], - required: true, - }, - created_at: { - type: Date, - default: Date.now, - }, - updated_at: { - type: Date, - default: Date.now, - }, -}); - -//events collection -const eventSchema = new mongoose.Schema({ - event_id: { - type: String, - required: true, - unique: true, - }, - title: { - type: String, - required: true, - }, - description: String, - category: { - type: String, - enum: ["cultural", "technical", "sports", "academic", "other"], - }, - type: { - type: String, - }, - organizing_unit_id: { - type: mongoose.Schema.Types.ObjectId, - ref: "Organizational_Unit", - required: true, - }, - organizers: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - }, - ], - schedule: { - start: Date, - end: Date, - venue: String, - mode: { - type: String, - enum: ["online", "offline", "hybrid"], - }, - }, - registration: { - required: Boolean, - start: Date, - end: Date, - fees: Number, - max_participants: Number, - }, - budget: { - allocated: Number, - spent: Number, - sponsors: [ - { - type: String, - }, - ], - }, - status: { - type: String, - enum: ["planned", "ongoing", "completed", "cancelled"], - default: "planned", - }, - participants: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - }, - ], - winners: [ - { - user: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - }, - position: String, // e.g., "1st", "2nd", "Best Speaker", etc. - }, - ], - feedback_summary: { - type: Object, // You can define structure if fixed - }, - media: { - images: [String], - videos: [String], - documents: [String], - }, - room_requests: [ - { - date: { type: Date, required: true }, - time: { type: String, required: true }, - room: { type: String, required: true }, - description: { type: String }, - status: { - type: String, - enum: ["Pending", "Approved", "Rejected"], - default: "Pending", - }, - requested_at: { - type: Date, - default: Date.now, - }, - reviewed_by: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - }, - }, - ], - created_at: { - type: Date, - default: Date.now, - }, - updated_at: { - type: Date, - default: Date.now, - }, -}); //skill collection - const skillSchema = new mongoose.Schema({ skill_id: { type: String, @@ -474,126 +72,6 @@ const userSkillSchema = new mongoose.Schema({ }, }); -//achievements collection -const achievementSchema = new mongoose.Schema({ - achievement_id: { - type: String, - required: true, - unique: true, - }, - user_id: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - required: true, - }, - title: { - type: String, - required: true, - }, - description: String, - category: { - type: String, - required: true, - }, - type: { - type: String, - }, - level: { - type: String, - }, - date_achieved: { - type: Date, - required: true, - }, - position: { - type: String, - }, - event_id: { - type: mongoose.Schema.Types.ObjectId, - ref: "Event", - default: null, // optional - }, - certificate_url: String, - verified: { - type: Boolean, - default: false, - }, - verified_by: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - default: null, - }, - created_at: { - type: Date, - default: Date.now, - }, -}); - -//feedback collection -const feedbackSchema = new mongoose.Schema({ - feedback_id: { - type: String, - required: true, - unique: true, - }, - type: { - type: String, - required: true, - }, - target_id: { - type: mongoose.Schema.Types.ObjectId, - //required: true, - // We'll dynamically interpret this field based on target_type - }, - target_type: { - type: String, - required: true, - }, - feedback_by: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - required: true, - }, - // category: { - // type: String, - // enum: ['organization', 'communication', 'leadership'], - // required: true - // }, - rating: { - type: Number, - min: 1, - max: 5, - }, - comments: { - type: String, - }, - is_anonymous: { - type: Boolean, - default: false, - }, - is_resolved: { - type: Boolean, - default: false, - }, - actions_taken: { - type: String, - default: "", - }, - created_at: { - type: Date, - default: Date.now, - }, - resolved_at: { - type: Date, - default: null, - }, - resolved_by: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - default: null, - }, -}); - //announcement collection const announcementSchema = new mongoose.Schema({ title: { @@ -622,28 +100,11 @@ const announcementSchema = new mongoose.Schema({ type: Boolean, default: false, }, - createdAt: { - type: Date, - default: Date.now, - }, - updatedAt: { - type: Date, - default: Date.now, - }, -}); -const User = mongoose.model("User", userSchema); -const Feedback = mongoose.model("Feedback", feedbackSchema); -const Achievement = mongoose.model("Achievement", achievementSchema); +}, { timestamps: true}); + const UserSkill = mongoose.model("User_Skill", userSkillSchema); const Skill = mongoose.model("Skill", skillSchema); -const Event = mongoose.model("Event", eventSchema); -const PositionHolder = mongoose.model("Position_Holder", positionHolderSchema); -const Position = mongoose.model("Position", positionSchema); -const OrganizationalUnit = mongoose.model( - "Organizational_Unit", - organizationalUnitSchema, -); const Announcement = mongoose.model("Announcement", announcementSchema); const roomSchema = new mongoose.Schema({ @@ -774,15 +235,8 @@ roomBookingSchema.index({ room: 1, date: 1, startTime: 1, endTime: 1 }); const RoomBooking = mongoose.model("RoomBooking", roomBookingSchema); module.exports = { - User, - Feedback, - Achievement, UserSkill, Skill, - Event, - PositionHolder, - Position, - OrganizationalUnit, Announcement, Room, RoomBooking, diff --git a/backend/models/taskSchema.js b/backend/models/taskSchema.js new file mode 100644 index 00000000..24cdcaad --- /dev/null +++ b/backend/models/taskSchema.js @@ -0,0 +1,44 @@ +const mongoose = require("mongoose"); + +const taskSchema = new mongoose.Schema( + { + title: { type: String, required: true }, + description: { type: String, required: true }, + assigned_by: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + assignees: { + type: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + }, + ], + required: true, + }, + unit_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "Organizational_Unit", + required: true, + }, + deadline: { type: Date, required: true }, + priority: { + type: String, + enum: ["low", "medium", "high"], + default: "medium", + }, + status: { + type: String, + enum: ["pending", "in-progress", "under-review", "completed"], + default: "pending", + }, + progress: { type: Number, default: 0, max: 100 }, + submission_note: { type: String, default: "" }, // Link to work (Drive/Doc) or description + admin_notes: { type: String, default: "" }, // Feedback from the assigner + }, + { timestamps: true }, +); + +module.exports = mongoose.model("Task", taskSchema); diff --git a/backend/models/userSchema.js b/backend/models/userSchema.js new file mode 100644 index 00000000..560e65bf --- /dev/null +++ b/backend/models/userSchema.js @@ -0,0 +1,108 @@ +const mongoose = require("mongoose"); +const bcrypt = require("bcrypt"); +require("dotenv").config(); + +const userSchema = new mongoose.Schema( + { + user_id: { + type: String, + }, + strategy: { + type: String, + enum: ["local", "google"], + required: true, + }, + role: { + type: String, + default: "STUDENT" + }, + username: { + type: String, + required: true, + unique: true, + }, + password: { + type: String, + required: function () { + return this.strategy === "local"; + }, + minLength: 8, + }, + onboardingComplete: { + type: Boolean, + default: false, + }, + personal_info: { + name: { + type: String, + required: true, + }, + email: { + type: String, + unique: true, + required: true, + }, + phone: String, + date_of_birth: Date, + gender: String, + + profilePic: { + type: String, + default: "https://www.gravatar.com/avatar/?d=mp", + }, + + cloudinaryUrl: { + 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, + }, + + 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", + }, + }, + { + timestamps: true, + }, +); + +userSchema.index( + { user_id: 1 }, + { + unique: true, + partialFilterExpression: { user_id: { $exists: true, $type: "string" } }, + name: "user_id_partial_unique", + }, +); + +userSchema.pre("save", async function () { + if (!this.isModified("password")) return; + const SALT_ROUNDS = Number(process.env.SALT) || 12 + this.password = await bcrypt.hash(this.password, SALT_ROUNDS); +}); +const User = mongoose.model("User", userSchema); +module.exports = User; diff --git a/backend/package-lock.json b/backend/package-lock.json index dec1d123..65d26bd5 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,9 +10,11 @@ "license": "ISC", "dependencies": { "axios": "^1.5.1", - "body-parser": "^1.20.2", + "bcrypt": "^6.0.0", "cloudinary": "^2.6.1", + "connect-mongo": "^5.1.0", "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 +27,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", @@ -34,18 +35,16 @@ "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" } }, @@ -1885,7 +1884,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/async-function": { + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/asynciterator.prototype": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", @@ -1976,8 +1987,21 @@ "node_modules/basic-auth/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" + "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.3.0", @@ -1991,29 +2015,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "license": "MIT", - "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.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } + "node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" }, "node_modules/bowser": { "version": "2.14.1", @@ -2330,6 +2336,46 @@ "typedarray": "^0.0.6" } }, + "node_modules/connect-mongo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-5.1.0.tgz", + "integrity": "sha512-xT0vxQLqyqoUTxPLzlP9a/u+vir0zNkhiy9uAdHjSCcUUf7TS5b55Icw8lVyYFxfemP3Mf9gdwUOgeF3cxCAhw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.1", + "kruptein": "^3.0.0" + }, + "engines": { + "node": ">=12.9.0" + }, + "peerDependencies": { + "express-session": "^1.17.1", + "mongodb": ">= 5.1.0 < 7" + } + }, + "node_modules/connect-mongo/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/connect-mongo/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/connect-mongodb-session": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/connect-mongodb-session/-/connect-mongodb-session-3.1.1.tgz", @@ -2449,9 +2495,9 @@ "version": "1.4.7", "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", - "dev": true, "license": "MIT", "dependencies": { + "cookie": "0.7.2", "cookie": "0.7.2", "cookie-signature": "1.0.6" }, @@ -2463,7 +2509,6 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -3608,22 +3653,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==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "ISC" - }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/get-east-asian-width": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", @@ -4619,6 +4648,18 @@ "json-buffer": "3.0.1" } }, + "node_modules/kruptein": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.2.0.tgz", + "integrity": "sha512-Bcou7bKBn3k2ZEDXyYzR/j7YWWFDIcqv0ZeabHHPWW1aYmfLn0qmJJoWPVeQvh37g6vl2x3nEO9guBSzJsmuMQ==", + "license": "MIT", + "dependencies": { + "asn1.js": "^5.4.1" + }, + "engines": { + "node": ">8" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5014,12 +5055,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { "node": ">=18" }, @@ -5126,12 +5174,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==", - "license": "MIT" - }, "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", @@ -5354,22 +5396,24 @@ "node": ">= 0.6" } }, - "node_modules/node-exports-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", - "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "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", - "dependencies": { - "array.prototype.flatmap": "^1.3.3", - "es-errors": "^1.3.0", - "object.entries": "^1.1.9", - "semver": "^6.3.1" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "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": { @@ -8093,18 +8137,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", @@ -8155,20 +8187,6 @@ "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==", - "license": "MIT", - "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.8.0", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", @@ -8398,21 +8416,6 @@ "node": ">= 0.6" } }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "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", @@ -8697,13 +8700,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "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==", - "deprecated": "Just use Node.js's crypto.timingSafeEqual()", - "license": "BSD-3-Clause" - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -9308,15 +9304,6 @@ "node": ">=0.6" } }, - "node_modules/tokenizer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/tokenizer/-/tokenizer-0.1.0.tgz", - "integrity": "sha512-AdGT3w9DLrBa767o+gXgNuEHeRUcCuKnjLv6nlkM60TAzdT0HqZuDMVBGtqIybeOA62YLLV7/7g2+DHRr1fvjQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/touch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", @@ -9789,6 +9776,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..41b9bf11 100644 --- a/backend/package.json +++ b/backend/package.json @@ -30,9 +30,11 @@ "license": "ISC", "dependencies": { "axios": "^1.5.1", - "body-parser": "^1.20.2", + "bcrypt": "^6.0.0", "cloudinary": "^2.6.1", + "connect-mongo": "^5.1.0", "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 +47,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", @@ -54,18 +55,16 @@ "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..d3b4a29b 100644 --- a/backend/routes/achievements.js +++ b/backend/routes/achievements.js @@ -1,8 +1,8 @@ const express = require("express"); const router = express.Router(); -const { Achievement } = require("../models/schema"); // Update path as needed +const Achievement = require("../models/achievementSchema"); // 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..40127816 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", + isAuthenticated, + authorizeRole(["CLUB_COORDINATOR"]), + 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..6bfba2df 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,13 +1,16 @@ 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 { 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 User = require("../models/userSchema"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); + +//const bcrypt = require("bcrypt"); //rate limiter - for password reset try const forgotPasswordLimiter = rateLimit({ @@ -17,67 +20,111 @@ const forgotPasswordLimiter = rateLimit({ }); // Session Status -router.get("/fetchAuth",isAuthenticated, function (req, res) { - if (req.isAuthenticated()) { - res.json(req.user); - } else { - res.json(null); - } +router.get("/fetchAuth", isAuthenticated, function (req, res) { + const { personal_info, role, onboardingComplete, _id, ...restData } = + req.user; + res.json({ + message: { personal_info, role, onboardingComplete, _id }, + success: true, + }); }); -// 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.", - }); +/** + * User POST /auth/login + ↓ + passport.authenticate("local") + ↓ + LocalStrategy (validate credentials) + ↓ + done(null, user) + ↓ + req.login(user) called + ↓ + serializeUser(user) → store ID in session + ↓ + Session saved → session cookie sent + */ +router.post("/login", async (req, res) => { + try { + passport.authenticate("local", (err, user, info) => { + if (err) { + console.error(err); + return res.status(500).json({ message: "Internal server error" }); + } + + if (!user) + return res + .status(401) + .json({ message: info?.message || "Login failed" }); + + // if using a custom callback like this u have to manually call req.login() else not needed + //this will seralize user, store id in session, save session and send cookie + req.login(user, (err) => { + if (err) + return res.status(500).json({ message: "Internal server error" }); + const { personal_info, role, onboardingComplete, ...restData } = user; + return res.json({ + message: "Login Successful", + success: true, + data: { personal_info, role, onboardingComplete }, + }); + }); + })(req, res); + } 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, name } = req.body; + const role = "STUDENT"; + const result = registerValidate.safeParse({ + username, + password, + name, + role, + }); + + if (!result.success) { + const errors = result.error.issues.map((issue) => issue.message); + return res.status(400).json({ message: errors, success: false }); } - 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 + .status(409) + .json({ + message: "Account with username already exists", + success: false, + }); } - const newUser = await User.register( - new User({ - user_id: ID, - role: "STUDENT", - strategy: "local", - username: email, - personal_info: { - name: name, - email: email, - }, - onboardingComplete: false, - }), + /** + * This logic is now embedded in the pre save hook + * const hashedPassword = await bcrypt.hash( password, + Number(process.env.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 }); + const newUser = await User.create({ + strategy: "local", + username, + password, + personal_info: { + name, + email: username, + }, + role, }); - } catch (error) { - console.error("Registration error:", error); - return res.status(500).json({ message: "Internal server error" }); + //console.log(newUser); + + //return res.json({ message: "Registered Successfully", user: newUser }); + return res.json({ message: "Registered Successfully", success: true }); + } catch (err) { + return res.status(500).json({ message: err.message }); } }); @@ -87,17 +134,45 @@ router.get( passport.authenticate("google", { scope: ["profile", "email"] }), ); -router.get( - "/google/verify", - passport.authenticate("google", { failureRedirect: "/" }), - (req, res) => { - if (req.user.onboardingComplete) { - res.redirect(`${process.env.FRONTEND_URL}/`); - } else { - res.redirect(`${process.env.FRONTEND_URL}/onboarding`); +router.get("/google/verify", function (req, res) { + //console.log("in verify"); + passport.authenticate("google", (err, user, info) => { + if (err) { + console.error(err); + return res.status(500).json({ message: "Internal server error" }); } - }, -); + + if (!user) + return res + .status(401) + .json({ message: info?.message || "Google Authentication failed" }); + + /** + * if(!user.onboardingComplete){ + return res.redirect(`${process.env.FRONTEND_URL}/onboarding`) + } + */ + //return res.redirect(`${process.env.FRONTEND_URL}`); + + req.login(user, (loginErr) => { + if (loginErr) { + console.error("Login error:", loginErr); + return res.status(500).json({ message: "Error establishing session" }); + } + + /*console.log("User logged in successfully:", user.username); + console.log("OnboardingComplete:", user.onboardingComplete); + */ + if (!user.onboardingComplete) { + //console.log("Redirecting to onboarding"); + return res.redirect(`${process.env.FRONTEND_URL}/onboarding`); + } + + //console.log("Redirecting to home"); + return res.redirect(`${process.env.FRONTEND_URL}`); + }); + })(req, res); +}); router.post("/logout", (req, res, next) => { req.logout(function (err) { @@ -122,7 +197,7 @@ router.post("/forgot-password", forgotPasswordLimiter, async (req, res) => { "This email is linked with Google Login. Please use 'Sign in with Google' instead.", }); } - const secret = user._id + process.env.JWT_SECRET_TOKEN; + const secret = process.env.JWT_SECRET_TOKEN; const token = jwt.sign({ email: email, id: user._id }, secret, { expiresIn: "10m", }); @@ -151,7 +226,7 @@ router.post("/forgot-password", forgotPasswordLimiter, async (req, res) => { .json({ message: "Password reset link sent to your email" }); } }); - console.log(link); + //console.log(link); } catch (error) { console.log(error); return res.status(500).json({ message: "Internal server error" }); @@ -160,15 +235,18 @@ router.post("/forgot-password", forgotPasswordLimiter, async (req, res) => { //route for password reset router.get("/reset-password/:id/:token", async (req, res) => { - const { id, token } = req.params; - console.log(req.params); - const user = await User.findOne({ _id: id }); - if (!user) { - return res.status(404).json({ message: "User not found" }); - } - const secret = user._id + process.env.JWT_SECRET_TOKEN; try { - jwt.verify(token, secret); + const { id, token } = req.params; + const user = await User.findOne({ _id: id }); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + const secret = process.env.JWT_SECRET_TOKEN; + const decoded = jwt.verify(token, secret); + if (decoded.id !== user._id.toString()) { + return res.status(400).json({ message: "Invalid or expired token" }); + } + return res.status(200).json({ message: "Token verified successfully" }); } catch (error) { console.log(error); @@ -183,7 +261,7 @@ router.post("/reset-password/:id/:token", async (req, res) => { if (!user) { return res.status(404).json({ message: "User not found" }); } - const secret = user._id + process.env.JWT_SECRET_TOKEN; + const secret = process.env.JWT_SECRET_TOKEN; try { jwt.verify(token, secret); user.setPassword(password, async (error) => { 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..452ecf28 100644 --- a/backend/routes/events.js +++ b/backend/routes/events.js @@ -1,8 +1,10 @@ const express = require("express"); const router = express.Router(); -const { Event, User, OrganizationalUnit } = require("../models/schema"); +const OrganizationalUnit = require("../models/organizationSchema"); +const Event = require("../models/eventSchema"); +const User = require("../models/userSchema"); 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 +223,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 +256,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..94ca51ee 100644 --- a/backend/routes/feedbackRoutes.js +++ b/backend/routes/feedbackRoutes.js @@ -1,18 +1,16 @@ const express = require("express"); const router = express.Router(); -const isAuthenticated = require("../middlewares/isAuthenticated"); -const { - User, - Feedback, - Event, - Position, - OrganizationalUnit, -} = require("./../models/schema"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); +const OrganizationalUnit = require("../models/organizationSchema"); +const Event = require("../models/eventSchema"); +const User = require("../models/userSchema"); +const Feedback = require("../models/feedbackSchema"); +const Position = require("../models/positionSchema"); 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 +26,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 +61,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 +179,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..14905a90 100644 --- a/backend/routes/onboarding.js +++ b/backend/routes/onboarding.js @@ -1,18 +1,17 @@ const express = require("express"); const router = express.Router(); -const { User } = require("../models/schema"); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const User = require("../models/userSchema"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); // Onboarding route - to be called when user logs in for the first time -router.post("/",isAuthenticated, async (req, res) => { - const { ID_No, add_year, Program, discipline, mobile_no } = req.body; +router.put("/", isAuthenticated, async (req, res) => { + const { add_year, Program, discipline, mobile_no } = req.body; try { - console.log(req.user); + //console.log(req.user); const updatedUser = await User.findByIdAndUpdate( req.user._id, { - user_id: ID_No, onboardingComplete: true, personal_info: Object.assign({}, req.user.personal_info, { phone: mobile_no || "", @@ -28,11 +27,11 @@ router.post("/",isAuthenticated, async (req, res) => { { new: true, runValidators: true }, ); - console.log("Onboarding completed for user:", updatedUser._id); + //console.log("Onboarding completed for user:", updatedUser._id); res.status(200).json({ message: "Onboarding completed successfully" }); } catch (error) { - console.error("Onboarding failed:", error); - res.status(500).json({ message: "Onboarding failed", error }); + console.error("Onboarding failed:", error.message); + res.status(500).json({ message: error.message || "Onboarding failed" }); } }); 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..eb5602a1 100644 --- a/backend/routes/positionRoutes.js +++ b/backend/routes/positionRoutes.js @@ -1,8 +1,9 @@ const express = require("express"); const router = express.Router(); -const { Position, PositionHolder } = require("../models/schema"); +const Position = require("../models/positionSchema"); +const PositionHolder = require("../models/positionHolderSchema"); 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..25ad74bf 100644 --- a/backend/routes/profile.js +++ b/backend/routes/profile.js @@ -4,9 +4,9 @@ const router = express.Router(); const upload = require("../middlewares/upload"); const cloudinary = require("cloudinary").v2; //const { Student } = require("../models/student"); -const { User } = require("../models/schema"); +const User = require("../models/userSchema"); 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" }); } + const user = await User.findOne({ user_id: ID_No }); + 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/routes/task.routes.js b/backend/routes/task.routes.js new file mode 100644 index 00000000..5df34ec6 --- /dev/null +++ b/backend/routes/task.routes.js @@ -0,0 +1,15 @@ +const router = require("express").Router(); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); +const { + getTasks, + updateTask, + createTask, + getTaskUsers, +} = require("../controllers/taskController"); + +router.get("/", isAuthenticated, getTasks); +router.patch("/:taskId", isAuthenticated, updateTask); +router.post("/create-task", isAuthenticated, createTask); +router.get("/get-users", isAuthenticated, getTaskUsers); + +module.exports = router; diff --git a/backend/seed.js b/backend/seed.js deleted file mode 100644 index 2faff674..00000000 --- a/backend/seed.js +++ /dev/null @@ -1,1160 +0,0 @@ -require("dotenv").config(); -const mongoose = require("mongoose"); - -const { - User, - Feedback, - Achievement, - UserSkill, - Skill, - Event, - RoomBooking, - PositionHolder, - Position, - OrganizationalUnit, - Room, -} = require("./models/schema"); - -// Sample Rooms for Seeding -const sampleRooms = [ - { - name: "LH-101", - capacity: 60, - location: "Academic Block 1, Ground Floor", - amenities: ["Projector", "AC", "Whiteboard"], - }, - { - name: "LH-102", - capacity: 60, - location: "Academic Block 1, Ground Floor", - amenities: ["Projector", "AC"], - }, - { - name: "Seminar Hall", - capacity: 120, - location: "Admin Block, 1st Floor", - amenities: ["Projector", "Sound System", "AC"], - }, -]; - -// Seeds sample rooms for testing room booking features. - -const seedRooms = async () => { - console.log("Seeding sample rooms..."); - await Room.deleteMany({}); - await Room.insertMany(sampleRooms); - console.log("Sample rooms seeded!"); -}; - -// --- Data for Seeding --- - -// Original club/committee data. -const initialUnitsData = [ - { - unit_id: "CLUB_OPENLAKE", - name: "OpenLake", - type: "Club", - description: "Open Source Club of IIT Bhilai", - hierarchy_level: 2, - category: "scitech", - contact_info: { email: "openlake@iitbhilai.ac.in", social_media: [] }, - }, - { - unit_id: "CLUB_RENAISSANCE", - name: "Renaissance", - type: "Club", - description: "Fine Arts Club under Cultural Council.", - hierarchy_level: 2, - category: "cultural", - contact_info: { - email: "renaissance@iitbhilai.ac.in", - social_media: [ - { - platform: "Instagram", - url: "https://www.instagram.com/renaissance_iitbh?igsh=dzRqNmV5bncxZWp1", - }, - { - platform: "LinkedIn", - url: "https://www.linkedin.com/in/renaissance-club-a76430331", - }, - ], - }, - }, - { - unit_id: "CLUB_GOALS", - name: "GOALS", - type: "Club", - description: - "General Oratory and Literary Society handling Literature and Oration.", - hierarchy_level: 2, - category: "independent", - contact_info: { - email: "goals@iitbhilai.ac.in", - social_media: [ - { - platform: "Instagram", - url: "https://www.instagram.com/goals_iitbhilai?igsh=ejF6NzVmM3lxMmky", - }, - { - platform: "LinkedIn", - url: "https://www.linkedin.com/company/general-oratory-and-literary-society-goals/", - }, - ], - }, - }, - { - unit_id: "CLUB_BEATHACKERS", - name: "Beathackers", - type: "Club", - description: "The Dance Club of IIT Bhilai.", - hierarchy_level: 2, - category: "cultural", - contact_info: { - email: "beathackers@iitbhilai.ac.in", - social_media: [ - { - platform: "Instagram", - url: "https://www.instagram.com/beathackers_iitbhilai?igsh=YnVmbGozZ2V3dWE=", - }, - { - platform: "YouTube", - url: "https://youtube.com/@beathackersiitbhilai8247", - }, - ], - }, - }, - { - unit_id: "CLUB_EPSILON", - name: "The Epsilon Club", - type: "Club", - description: "Robotics Club of IIT Bhilai", - hierarchy_level: 2, - category: "scitech", - contact_info: { - email: "epsilon@iitbhilai.ac.in", - social_media: [ - { - platform: "Instagram", - url: "https://www.instagram.com/roboticsclub_iitbhilai", - }, - { - platform: "LinkedIn", - url: "https://www.linkedin.com/company/the-epsilon-club-iit-bhilai-robotics-club/", - }, - ], - }, - }, - { - unit_id: "CLUB_INGENUITY", - name: "Ingenuity", - type: "Club", - description: "Competitive programming club fostering problem-solving.", - hierarchy_level: 2, - category: "scitech", - contact_info: { - email: "ingenuity@iitbhilai.ac.in", - social_media: [ - { - platform: "Instagram", - url: "https://www.instagram.com/ingenuity_iit_bh/", - }, - { - platform: "LinkedIn", - url: "https://www.linkedin.com/company/74349589/admin/dashboard/", - }, - ], - }, - }, - { - unit_id: "CLUB_DESIGNX", - name: "DesignX", - type: "Club", - description: "Digital Arts club of IIT Bhilai.", - hierarchy_level: 2, - category: "cultural", - contact_info: { - email: "designx@iitbhilai.ac.in", - social_media: [ - { - platform: "Instagram", - url: "https://www.instagram.com/designx_iitbhilai?igsh=NTc4MTIwNjQ2YQ==", - }, - { - platform: "LinkedIn", - url: "https://www.linkedin.com/in/designx-iit-bhilai-612a7a371", - }, - ], - }, - }, - { - unit_id: "CLUB_SPECTRE", - name: "Spectre", - type: "Club", - description: "Cybersecurity Club of IIT Bhilai.", - hierarchy_level: 2, - category: "scitech", - contact_info: { - email: "spectre@iitbhilai.ac.in", - social_media: [ - { - platform: "Instagram", - url: "https://www.instagram.com/spectre_iitbhilai?igsh=ZDlyaDlqeXllYjNk", - }, - { - platform: "LinkedIn", - url: "https://www.linkedin.com/company/spectre-iit-bhilai/", - }, - ], - }, - }, - { - unit_id: "COMMITTEE_EXTERNAL", - name: "External Affairs", - type: "Committee", - description: "Handles sponsorship and PR opportunities of IIT Bhilai.", - hierarchy_level: 1, - category: "independent", - contact_info: { - email: "Outreach_cosa@iitbhilai.ac.in", - social_media: [ - { - platform: "LinkedIn", - url: "https://www.linkedin.com/in/external-affairs-iit-bhilai-8246a737b", - }, - ], - }, - }, - { - unit_id: "CLUB_YOGA", - name: "Yoga Club", - type: "Club", - description: "Promotes physical and mental well-being through yoga.", - hierarchy_level: 2, - category: "sports", - contact_info: { email: "sports_yoga@iitbhilai.ac.in", social_media: [] }, - }, - { - unit_id: "CLUB_MOTORSPORTS", - name: "Motorsports", - type: "Club", - description: "Promotes automotive culture in the institute.", - hierarchy_level: 2, - category: "scitech", - contact_info: { - email: "baja@iitbhilai.ac.in", - social_media: [ - { - platform: "Instagram", - url: "https://www.instagram.com/iitbhilaimotorsports", - }, - { - platform: "LinkedIn", - url: "https://www.linkedin.com/company/iit-bhilai-motorsports/", - }, - ], - }, - }, - { - unit_id: "CLUB_FPS", - name: "Film Production Society", - type: "Club", - description: "Film-making society of IIT Bhilai.", - hierarchy_level: 2, - category: "cultural", - contact_info: { - email: "fps@iitbhilai.ac.in", - social_media: [ - { platform: "Instagram", url: "https://www.instagram.com/fps_iitbh" }, - { platform: "YouTube", url: "http://youtube.com/@fps-iitbhilai9282" }, - ], - }, - }, - { - unit_id: "CLUB_SWARA", - name: "Swara", - type: "Club", - description: "Music Club of IIT Bhilai.", - hierarchy_level: 2, - category: "cultural", - contact_info: { - email: "swara@iitbhilai.ac.in", - social_media: [ - { platform: "Instagram", url: "https://www.instagram.com/swara_iitbh" }, - { platform: "YouTube", url: "https://youtube.com/@swaraiitbhilai" }, - ], - }, - }, -]; - -/** - * Clears all data from the relevant collections. - */ -const clearData = async () => { - console.log("Clearing existing data..."); - await RoomBooking.deleteMany({}); - await Room.deleteMany({}); - await OrganizationalUnit.deleteMany({}); - await Position.deleteMany({}); - await User.deleteMany({}); - await PositionHolder.deleteMany({}); - await Event.deleteMany({}); - await Skill.deleteMany({}); - await UserSkill.deleteMany({}); - await Achievement.deleteMany({}); - await Feedback.deleteMany({}); - console.log("All collections cleared successfully!"); -}; - -/** - * Seeds the Organizational Units with a proper hierarchy. - */ -const seedOrganizationalUnits = async () => { - console.log("Seeding Organizational Units..."); - - // 1. Create the top-level President and Test President units - const presidentUnit = new OrganizationalUnit({ - unit_id: "PRESIDENT_GYMKHANA", - name: "President, Student Gymkhana", - type: "independent_position", - description: - "The highest student representative body in the Student Gymkhana.", - parent_unit_id: null, - hierarchy_level: 0, - category: "independent", - contact_info: { - email: "president_gymkhana@iitbhilai.ac.in", - social_media: [], - }, - }); - 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, - }, - { - unit_id: "COUNCIL_SCITECH", - name: "Science and Technology Council", - type: "Council", - description: "Council for all science and technology activities.", - hierarchy_level: 1, - category: "scitech", - contact_info: { - email: "gensec_scitech_gymkhana@iitbhilai.ac.in", - social_media: [], - }, - parent_unit_id: presidentUnit._id, - }, - { - unit_id: "COUNCIL_SPORTS", - name: "Sports Council", - type: "Council", - description: "Council for all sports activities.", - hierarchy_level: 1, - category: "sports", - contact_info: { - email: "gensec_sports_gymkhana@iitbhilai.ac.in", - social_media: [], - }, - parent_unit_id: presidentUnit._id, - }, - { - unit_id: "COUNCIL_ACADEMIC", - name: "Academic Affairs Council", - type: "Council", - description: "Council for all academic affairs.", - hierarchy_level: 1, - category: "academic", - contact_info: { - email: "gensec_academic_gymkhana@iitbhilai.ac.in", - social_media: [], - }, - parent_unit_id: presidentUnit._id, - }, - ]; - await OrganizationalUnit.insertMany(mainCouncilsData); - console.log("Created Main Councils (Gensecs)."); - - // 3. Link initial clubs and committees to their respective parent councils - const councils = await OrganizationalUnit.find({ - type: "Council", - unit_id: { $not: /_TEST/ }, - }); - const councilMap = councils.reduce((map, council) => { - map[council.category] = council._id; - return map; - }, {}); - - const linkedUnitsData = initialUnitsData.map((unit) => { - return Object.assign({}, unit, { - parent_unit_id: councilMap[unit.category] || presidentUnit._id, - }); - }); - await OrganizationalUnit.insertMany(linkedUnitsData); - console.log("Seeded and linked initial clubs and committees."); - - // 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, - }, - { - unit_id: "COUNCIL_SPORTS_TEST", - name: "Test Sports Council", - type: "Council", - description: "Test council for sports activities.", - hierarchy_level: 1, - category: "sports", - contact_info: { - email: "test_gensec_sports@iitbhilai.ac.in", - social_media: [], - }, - parent_unit_id: presidentUnit._id, - }, - { - unit_id: "COUNCIL_ACAD_TEST", - name: "Test Academic Council", - type: "Council", - description: "Test council for academic activities.", - hierarchy_level: 1, - category: "academic", - contact_info: { - email: "test_gensec_acad@iitbhilai.ac.in", - social_media: [], - }, - parent_unit_id: presidentUnit._id, - }, - ]; - await OrganizationalUnit.insertMany(testCouncilsData); - console.log("Created Test Councils."); - - const testCouncils = await OrganizationalUnit.find({ name: /Test/ }); - const testCouncilMap = testCouncils.reduce((map, council) => { - map[council.category] = council._id; - return map; - }, {}); - - const testClubsData = [ - { - unit_id: "CLUB_CULTURAL_TEST", - name: "Test Cultural Club", - type: "Club", - description: "A test club for cultural events.", - hierarchy_level: 2, - category: "cultural", - contact_info: { - email: "test_cultural_club@iitbhilai.ac.in", - social_media: [], - }, - parent_unit_id: testCouncilMap.cultural, - }, - { - unit_id: "CLUB_SCITECH_TEST", - name: "Test SciTech Club", - type: "Club", - description: "A test club for scitech events.", - hierarchy_level: 2, - category: "scitech", - contact_info: { - email: "test_scitech_club@iitbhilai.ac.in", - social_media: [], - }, - parent_unit_id: testCouncilMap.scitech, - }, - { - unit_id: "CLUB_SPORTS_TEST", - name: "Test Sports Club", - type: "Club", - description: "A test club for sports events.", - hierarchy_level: 2, - category: "sports", - contact_info: { - email: "test_sports_club@iitbhilai.ac.in", - social_media: [], - }, - parent_unit_id: testCouncilMap.sports, - }, - { - unit_id: "CLUB_ACAD_TEST", - name: "Test Academic Club", - type: "Club", - description: "A test club for academic events.", - hierarchy_level: 2, - category: "academic", - contact_info: { - email: "test_acad_club@iitbhilai.ac.in", - social_media: [], - }, - parent_unit_id: testCouncilMap.academic, - }, - ]; - await OrganizationalUnit.insertMany(testClubsData); - 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. - */ -const seedUsers = async () => { - console.log("Seeding Users..."); - const units = await OrganizationalUnit.find({}); - const localAuthUsers = []; - const googleAuthUsers = []; - const password = "password"; - - for (const unit of units) { - let role; - - if (unit.unit_id.includes("PRESIDENT_GYMKHANA")) { - role = "PRESIDENT"; - } else if (unit.unit_id.includes("COUNCIL_CULTURAL")) { - role = "GENSEC_CULTURAL"; - } else if (unit.unit_id.includes("COUNCIL_SCITECH")) { - role = "GENSEC_SCITECH"; - } else if (unit.unit_id.includes("COUNCIL_SPORTS")) { - role = "GENSEC_SPORTS"; - } else if ( - unit.unit_id.includes("COUNCIL_ACADEMIC") || - unit.unit_id.includes("COUNCIL_ACAD") - ) { - role = "GENSEC_ACADEMIC"; - } else if (unit.type === "Club" || unit.type === "Committee") { - role = "CLUB_COORDINATOR"; - } else { - role = "STUDENT"; - } - - const userData = { - username: unit.contact_info.email, - role: role, - onboardingComplete: true, - personal_info: { - name: unit.name, - email: unit.contact_info.email, - }, - }; - - if (unit.unit_id.includes("_TEST") || unit.name.includes("Test")) { - userData.strategy = "local"; - localAuthUsers.push(userData); - } else { - userData.strategy = "google"; - googleAuthUsers.push(userData); - } - } - - // Add 10 dummy student users with local auth and correct email domain - for (let i = 1; i <= 10; i++) { - const userEmail = `student${i}@iitbhilai.ac.in`; - - // --- Add dummy academic info --- - const branches = ["CSE", "EE"]; - const batchYears = ["2026", "2027"]; - - const batch_year = - batchYears[Math.floor(Math.random() * batchYears.length)]; - // Assuming Sep 2025 as current time: 2026 grad year -> 4th year, 2027 grad year -> 3rd year - const current_year = batch_year === "2026" ? "4" : "3"; - - const academic_info = { - program: "B.Tech", - branch: branches[Math.floor(Math.random() * branches.length)], - batch_year: batch_year, - current_year: current_year, - cgpa: parseFloat((Math.random() * (10.0 - 6.0) + 6.0).toFixed(2)), - }; - - localAuthUsers.push({ - username: userEmail, - role: "STUDENT", - strategy: "local", - onboardingComplete: true, - personal_info: { name: `Demo Student ${i}`, email: userEmail }, - academic_info: academic_info, // Add academic info to user data - }); - } - - // Create Google auth users (no password needed) - if (googleAuthUsers.length > 0) { - await User.insertMany(googleAuthUsers); - console.log(`Created ${googleAuthUsers.length} Google auth users.`); - } - - // Create Local auth users (requires password hashing) - for (const userData of localAuthUsers) { - const user = new User(userData); - await User.register(user, password); - } - console.log( - `Created and registered ${localAuthUsers.length} local auth users.`, - ); - - console.log("Users seeded successfully!"); -}; - -/** - * Seeds the Position collection for all test units. - */ -const seedPositions = async () => { - console.log("Seeding Positions for test units..."); - const testUnits = await OrganizationalUnit.find({ - $or: [{ unit_id: /_TEST/ }, { name: /Test/ }], - }); - - const positionsToCreate = []; - - for (const unit of testUnits) { - const positions = [ - { title: "Coordinator", count: 1, type: "Leadership" }, - { title: "Core Member", count: 5, type: "CoreTeam" }, - { title: "Member", count: 10, type: "General" }, - ]; - - for (const pos of positions) { - const positionData = { - position_id: `${unit.unit_id}_${pos.title.toUpperCase().replace(" ", "_")}`, - title: pos.title, - unit_id: unit._id, - position_type: pos.type, - description: `The ${pos.title} position for ${unit.name}.`, - position_count: pos.count, - responsibilities: [`Fulfill the duties of a ${pos.title}.`], - requirements: { - min_cgpa: 6.0, - min_year: 1, - skills_required: ["Teamwork", "Communication"], - }, - }; - positionsToCreate.push(positionData); - } - } - - if (positionsToCreate.length > 0) { - await Position.insertMany(positionsToCreate); - console.log( - `Created ${positionsToCreate.length} positions for test units.`, - ); - } else { - console.log("No test units found to create positions for."); - } - - console.log("Positions seeded successfully!"); -}; - -/** - * Seeds the PositionHolder collection by assigning test students to test positions. - */ -const seedPositionHolders = async () => { - console.log("Seeding Position Holders for test units..."); - - const students = await User.find({ role: "STUDENT", strategy: "local" }); - const testClubs = await OrganizationalUnit.find({ - type: "Club", - name: /Test/, - }); - const testPositions = await Position.find({}).populate("unit_id"); - - if (students.length === 0) { - console.log("No student users found to assign positions to."); - return; - } - - const positionHoldersToCreate = []; - let studentIndex = 0; - - for (const club of testClubs) { - if (studentIndex >= students.length) { - break; - } - - const clubPositions = testPositions.filter((p) => - p.unit_id._id.equals(club._id), - ); - const coordinatorPos = clubPositions.find((p) => p.title === "Coordinator"); - const coreMemberPos = clubPositions.find((p) => p.title === "Core Member"); - - if (coordinatorPos && studentIndex < students.length) { - const student = students[studentIndex++]; - positionHoldersToCreate.push({ - por_id: `POR_${student._id}_${coordinatorPos._id}`, - user_id: student._id, - position_id: coordinatorPos._id, - tenure_year: "2024-2025", - status: "active", - }); - } - - if (coreMemberPos && studentIndex < students.length) { - const coreMemberCount = Math.floor(Math.random() * 2) + 1; - for ( - let i = 0; - i < coreMemberCount && studentIndex < students.length; - i++ - ) { - const student = students[studentIndex++]; - positionHoldersToCreate.push({ - por_id: `POR_${student._id}_${coreMemberPos._id}_${i}`, - user_id: student._id, - position_id: coreMemberPos._id, - tenure_year: "2024-2025", - status: "active", - }); - } - } - } - - if (positionHoldersToCreate.length > 0) { - await PositionHolder.insertMany(positionHoldersToCreate); - console.log(`Created ${positionHoldersToCreate.length} position holders.`); - } else { - console.log("Could not create any position holders."); - } - - console.log("Position Holders seeded successfully!"); -}; - -/** - * Seeds the Skill collection with a predefined list of skills. - */ -const seedSkills = async () => { - console.log("Seeding Skills..."); - const skillsData = [ - { - skill_id: "SKL_JS", - name: "JavaScript", - category: "Programming", - type: "technical", - }, - { - skill_id: "SKL_PY", - name: "Python", - category: "Programming", - type: "technical", - }, - { - skill_id: "SKL_REACT", - name: "React", - category: "Web Development", - type: "technical", - }, - { - skill_id: "SKL_NODE", - name: "Node.js", - category: "Web Development", - type: "technical", - }, - { - skill_id: "SKL_MONGO", - name: "MongoDB", - category: "Database", - type: "technical", - }, - { - skill_id: "SKL_CYBER", - name: "Cybersecurity", - category: "Security", - type: "technical", - }, - { - skill_id: "SKL_ROBO", - name: "Robotics", - category: "Hardware", - type: "technical", - }, - { - skill_id: "SKL_DANCE", - name: "Dancing", - category: "Performing Arts", - type: "cultural", - }, - { - skill_id: "SKL_SING", - name: "Singing", - category: "Performing Arts", - type: "cultural", - }, - { - skill_id: "SKL_PAINT", - name: "Painting", - category: "Fine Arts", - type: "cultural", - }, - { - skill_id: "SKL_DART", - name: "Digital Art", - category: "Fine Arts", - type: "cultural", - }, - { - skill_id: "SKL_SPEAK", - name: "Public Speaking", - category: "Literary", - type: "cultural", - }, - { - skill_id: "SKL_FILM", - name: "Film Making", - category: "Media", - type: "cultural", - }, - { - skill_id: "SKL_CRIC", - name: "Cricket", - category: "Team Sport", - type: "sports", - }, - { - skill_id: "SKL_FOOT", - name: "Football", - category: "Team Sport", - type: "sports", - }, - { skill_id: "SKL_YOGA", name: "Yoga", category: "Fitness", type: "sports" }, - { - skill_id: "SKL_BASK", - name: "Basketball", - category: "Team Sport", - type: "sports", - }, - ]; - await Skill.insertMany(skillsData); - console.log(`Created ${skillsData.length} skills.`); -}; - -/** - * Assigns a random set of skills to each dummy student. - */ -const seedUserSkills = async () => { - console.log("Assigning skills to users..."); - const skills = await Skill.find({}); - const students = await User.find({ role: "STUDENT", strategy: "local" }); - - if (skills.length === 0 || students.length === 0) { - console.log("No skills or students found to create user-skill links."); - return; - } - - const userSkillsToCreate = []; - const proficiencyLevels = ["beginner", "intermediate", "advanced", "expert"]; - - for (const student of students) { - const skillsToAssignCount = Math.floor(Math.random() * 3) + 2; - const shuffledSkills = [...skills].sort(() => 0.5 - Math.random()); - const selectedSkills = shuffledSkills.slice(0, skillsToAssignCount); - - for (const skill of selectedSkills) { - userSkillsToCreate.push({ - user_id: student._id, - skill_id: skill._id, - proficiency_level: - proficiencyLevels[ - Math.floor(Math.random() * proficiencyLevels.length) - ], - is_endorsed: false, - }); - } - } - - if (userSkillsToCreate.length > 0) { - await UserSkill.insertMany(userSkillsToCreate); - console.log( - `Assigned ${userSkillsToCreate.length} skills across ${students.length} students.`, - ); - } - console.log("User skills seeded successfully!"); -}; - -/** - * Seeds the Event collection with dummy events for test clubs. - */ -const seedEvents = async () => { - console.log("Seeding Events for test clubs..."); - - const testClubs = await OrganizationalUnit.find({ - type: "Club", - name: /Test/, - }); - const students = await User.find({ role: "STUDENT", strategy: "local" }); - - if (testClubs.length === 0 || students.length === 0) { - console.log("No test clubs or students found to create events for."); - return; - } - - const eventsToCreate = []; - const now = new Date(); - - for (const club of testClubs) { - const eventCategory = - club.category === "scitech" ? "technical" : club.category; - - // --- Completed Event --- - const completedEventParticipants = [...students] - .sort(() => 0.5 - Math.random()) - .slice(0, 5); - if (completedEventParticipants.length === 0) { - continue; - } - const completedEvent = { - event_id: `EVENT_${club.unit_id}_COMPLETED`, - title: `Annual ${club.category} Gala`, - description: `A look back at the amazing ${club.category} events of the past year.`, - category: eventCategory, - type: "Gala", - organizing_unit_id: club._id, - schedule: { - start: new Date(now.getFullYear(), now.getMonth() - 1, 15), - end: new Date(now.getFullYear(), now.getMonth() - 1, 15), - venue: "Main Auditorium", - mode: "offline", - }, - status: "completed", - participants: completedEventParticipants.map((p) => p._id), - winners: [ - { user: completedEventParticipants[0]._id, position: "1st Place" }, - ], - }; - eventsToCreate.push(completedEvent); - - // --- Planned Event --- - const plannedEvent = { - event_id: `EVENT_${club.unit_id}_PLANNED`, - title: `Introductory Workshop on ${club.category}`, - description: `Join us for a fun and interactive workshop!`, - category: eventCategory, - type: "Workshop", - organizing_unit_id: club._id, - schedule: { - start: new Date(now.getFullYear(), now.getMonth() + 1, 10), - end: new Date(now.getFullYear(), now.getMonth() + 1, 10), - venue: "Room C101", - mode: "offline", - }, - registration: { - required: true, - start: new Date(now.getFullYear(), now.getMonth(), 1), - end: new Date(now.getFullYear(), now.getMonth() + 1, 5), - fees: 0, - max_participants: 50, - }, - status: "planned", - }; - eventsToCreate.push(plannedEvent); - } - - if (eventsToCreate.length > 0) { - await Event.insertMany(eventsToCreate); - console.log(`Created ${eventsToCreate.length} dummy events.`); - } - - console.log("Events seeded successfully!"); -}; - -/** - * Seeds the Achievement collection based on winners of completed events. - */ -const seedAchievements = async () => { - console.log("Seeding Achievements from event winners..."); - - const completedEventsWithWinners = await Event.find({ - status: "completed", - winners: { $exists: true, $not: { $size: 0 } }, - }).populate("organizing_unit_id"); - - if (completedEventsWithWinners.length === 0) { - console.log( - "No completed events with winners found to create achievements from.", - ); - return; - } - - const achievementsToCreate = []; - for (const event of completedEventsWithWinners) { - for (const winner of event.winners) { - const achievementCategory = - event.category === "technical" ? "scitech" : event.category; - const achievementData = { - achievement_id: `ACH_${event.event_id}_${winner.user}`, - user_id: winner.user, - title: `Winner of ${event.title}`, - description: `Achieved ${winner.position} in the ${event.title} event organized by ${event.organizing_unit_id.name}.`, - category: achievementCategory, - type: "Competition", - level: "Institute", - date_achieved: event.schedule.end, - position: winner.position, - event_id: event._id, - verified: true, - }; - achievementsToCreate.push(achievementData); - } - } - - if (achievementsToCreate.length > 0) { - await Achievement.insertMany(achievementsToCreate); - console.log( - `Created ${achievementsToCreate.length} achievements for event winners.`, - ); - } -}; - -/** - * Seeds the Feedback collection for events and organizational units. - */ -const seedFeedbacks = async () => { - console.log("Seeding Feedback..."); - - const students = await User.find({ role: "STUDENT", strategy: "local" }); - const completedEvents = await Event.find({ status: "completed" }); - const testClubs = await OrganizationalUnit.find({ - type: "Club", - name: /Test/, - }); - - if ( - students.length < 2 || - completedEvents.length === 0 || - testClubs.length === 0 - ) { - console.log("Not enough data to create meaningful feedback."); - return; - } - - const feedbacksToCreate = []; - - // 1. Create feedback for an event - const eventToReview = completedEvents[0]; - const studentReviewer1 = students[0]; - const studentReviewer2 = students[1]; - - feedbacksToCreate.push({ - feedback_id: `FDB_EVT_${eventToReview._id}_${studentReviewer1._id}`, - type: "Event Feedback", - target_id: eventToReview._id, - target_type: "Event", - feedback_by: studentReviewer1._id, - rating: 5, - comments: "This was an amazing event! Well organized and very engaging.", - is_anonymous: false, - }); - - feedbacksToCreate.push({ - feedback_id: `FDB_EVT_${eventToReview._id}_${studentReviewer2._id}`, - type: "Event Feedback", - target_id: eventToReview._id, - target_type: "Event", - feedback_by: studentReviewer2._id, - rating: 4, - comments: "Good event, but the timings could have been better.", - is_anonymous: true, - }); - - // 2. Create feedback for an organizational unit - const clubToReview = testClubs[0]; - feedbacksToCreate.push({ - feedback_id: `FDB_OU_${clubToReview._id}_${studentReviewer1._id}`, - type: "Unit Feedback", - target_id: clubToReview._id, - target_type: "Organizational_Unit", - feedback_by: studentReviewer1._id, - rating: 4, - comments: `The ${clubToReview.name} is doing a great job this semester.`, - is_anonymous: false, - }); - - if (feedbacksToCreate.length > 0) { - await Feedback.insertMany(feedbacksToCreate); - console.log(`Created ${feedbacksToCreate.length} feedback entries.`); - } -}; - -/** - * Main function to run the entire seeding process. - */ -async function seedDB() { - try { - console.log("Connecting to the database..."); - await mongoose.connect(process.env.MONGODB_URI, { - useNewUrlParser: true, - useUnifiedTopology: true, - }); - console.log("Database connected successfully."); - - await clearData(); - await seedRooms(); - await seedOrganizationalUnits(); - await seedUsers(); - await seedPositions(); - await seedPositionHolders(); - await seedSkills(); - await seedUserSkills(); - await seedEvents(); - await seedAchievements(); - await seedFeedbacks(); - - console.log("\n✅ Seeding completed successfully!"); - } catch (error) { - console.error("\n❌ An error occurred during the seeding process:", error); - } finally { - if (mongoose.connection.readyState === 1) { - await mongoose.connection.close(); - console.log("Database connection closed."); - } - } -} - -// Run the seeding function -seedDB(); diff --git a/backend/seedTasks.js b/backend/seedTasks.js new file mode 100644 index 00000000..f466debb --- /dev/null +++ b/backend/seedTasks.js @@ -0,0 +1,303 @@ +require("dotenv").config(); +const mongoose = require("mongoose"); +const Task = require("./models/taskSchema"); +const User = require("./models/userSchema"); +const PositionHolder = require("./models/positionHolderSchema"); +const Position = require("./models/positionSchema"); +const OrganizationalUnit = require("./models/organizationSchema"); + +// Helpers + +const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)]; + +const randomSubset = (arr, min, max) => { + const count = Math.min( + arr.length, + Math.floor(Math.random() * (max - min + 1)) + min, + ); + return [...arr].sort(() => 0.5 - Math.random()).slice(0, count); +}; + +const futureDate = (daysAhead) => { + const d = new Date(); + d.setDate(d.getDate() + daysAhead); + return d; +}; + +const PRIORITIES = ["low", "medium", "high"]; +const STATUSES = ["pending", "in-progress", "under-review", "completed"]; + +const SAMPLE_TASKS = [ + { + title: "Organise Annual Tech Fest", + description: + "Plan and execute the annual tech fest including logistics and sponsorships.", + }, + { + title: "Club Budget Report Submission", + description: "Compile and submit the quarterly budget utilisation report.", + }, + { + title: "Social Media Campaign", + description: "Run a two-week social media campaign for the upcoming event.", + }, + { + title: "Sponsor Outreach", + description: + "Reach out to at least 10 potential sponsors and collect proposals.", + }, + { + title: "Workshop Coordination", + description: + "Coordinate with external speakers and manage workshop registrations.", + }, + { + title: "Internal Newsletter", + description: + "Draft and publish the monthly internal newsletter for club members.", + }, + { + title: "Venue Booking", + description: "Identify and book a suitable venue for the seminar.", + }, + { + title: "Volunteer Management", + description: "Recruit and brief volunteers for the upcoming event.", + }, + { + title: "Post-Event Survey", + description: + "Design and circulate a feedback form after the event and compile results.", + }, + { + title: "Equipment Procurement", + description: + "Assess equipment needs and raise purchase requests with proper documentation.", + }, +]; + +// Returns the unit ObjectId associated with an assigner by tracing PositionHolder -> Position -> unit_id. +async function getAssignerUnitId(user) { + const holder = await PositionHolder.findOne({ + user_id: user._id, + status: "active", + }); + if (!holder) return null; + const position = await Position.findById(holder.position_id).select( + "unit_id", + ); + return position ? position.unit_id : null; +} + +// Builds a deduplicated set of assignable user ObjectIds for the given assigner, +// following the role-based rules: +// PRESIDENT -> any GENSEC_* or CLUB_COORDINATOR user, plus all active PositionHolders +// GENSEC_* -> CLUB_COORDINATORs and active PositionHolders in units of the same category +// CLUB_COORDINATOR -> active PositionHolders in their own unit only +async function resolveAssignees(assignerUser, assignerUnitId) { + const role = assignerUser.role; + const assignerId = assignerUser._id.toString(); + + if (role === "PRESIDENT") { + const roleUsers = await User.find({ + _id: { $ne: assignerUser._id }, + $or: [{ role: /^GENSEC_/ }, { role: "CLUB_COORDINATOR" }], + }).select("_id"); + + const activeHolders = await PositionHolder.find({ + status: "active", + }).select("user_id"); + + const allIds = new Set([ + ...roleUsers.map((u) => u._id.toString()), + ...activeHolders.map((h) => h.user_id.toString()), + ]); + allIds.delete(assignerId); + + return [...allIds].map((id) => new mongoose.Types.ObjectId(id)); + } + + if (role && role.startsWith("GENSEC_")) { + if (!assignerUnitId) return []; + + const unit = + await OrganizationalUnit.findById(assignerUnitId).select("category"); + if (!unit) return []; + + const category = unit.category; + const categoryUnits = await OrganizationalUnit.find({ category }).select( + "_id", + ); + const categoryUnitIds = categoryUnits.map((u) => u._id); + + const categoryPositions = await Position.find({ + unit_id: { $in: categoryUnitIds }, + }).select("_id"); + const categoryPositionIds = categoryPositions.map((p) => p._id); + + const holders = await PositionHolder.find({ + position_id: { $in: categoryPositionIds }, + status: "active", + }).select("user_id"); + + const holderUserIds = holders.map((h) => h.user_id.toString()); + + // Also pull in CLUB_COORDINATOR users whose position maps to the same category + const allCoords = await User.find({ role: "CLUB_COORDINATOR" }).select( + "_id", + ); + const coordsInCategory = []; + for (const coord of allCoords) { + const coordUnitId = await getAssignerUnitId(coord); + if (!coordUnitId) continue; + const coordUnit = + await OrganizationalUnit.findById(coordUnitId).select("category"); + if (coordUnit && coordUnit.category === category) { + coordsInCategory.push(coord._id.toString()); + } + } + + const allIds = new Set([...holderUserIds, ...coordsInCategory]); + allIds.delete(assignerId); + + return [...allIds].map((id) => new mongoose.Types.ObjectId(id)); + } + + if (role === "CLUB_COORDINATOR") { + if (!assignerUnitId) return []; + + const unitPositions = await Position.find({ + unit_id: assignerUnitId, + }).select("_id"); + const unitPositionIds = unitPositions.map((p) => p._id); + + const holders = await PositionHolder.find({ + position_id: { $in: unitPositionIds }, + status: "active", + }).select("user_id"); + + return holders + .map((h) => h.user_id) + .filter((id) => id.toString() !== assignerId); + } + + return []; +} + +async function seedTasks(options) { + const tasksPerAssigner = (options && options.tasksPerAssigner) || 3; + const clearExisting = (options && options.clearExisting) || false; + + await mongoose.connect(process.env.MONGODB_URI || process.env.MONGO_URI); + console.log("MongoDB Connected"); + + if (clearExisting) { + await Task.deleteMany({}); + console.log("Cleared existing tasks"); + } + + const assigners = await User.find({ + $or: [ + { role: "PRESIDENT" }, + { role: /^GENSEC_/ }, + { role: "CLUB_COORDINATOR" }, + ], + status: "active", + }); + + if (!assigners.length) { + console.log("No assigner users found. Seed users first."); + await mongoose.disconnect(); + return; + } + + const tasksToInsert = []; + + for (const assigner of assigners) { + const unitId = await getAssignerUnitId(assigner); + const assigneeIds = await resolveAssignees(assigner, unitId); + const name = + (assigner.personal_info && assigner.personal_info.name) || + assigner.username; + + if (!assigneeIds.length) { + console.log( + "No valid assignees for " + + assigner.role + + " (" + + name + + "). Skipping.", + ); + continue; + } + + // Use the assigner's unit, or fall back to any active unit + const taskUnitId = + unitId || + ( + (await OrganizationalUnit.findOne({ is_active: true }).select("_id")) || + {} + )._id; + + if (!taskUnitId) { + console.log( + "No active organizational unit found for " + name + ". Skipping.", + ); + continue; + } + + for (let i = 0; i < tasksPerAssigner; i++) { + const template = randomItem(SAMPLE_TASKS); + const assignees = randomSubset( + assigneeIds, + 1, + Math.min(3, assigneeIds.length), + ); + + tasksToInsert.push({ + title: template.title, + description: template.description, + assigned_by: assigner._id, + assignees, + unit_id: taskUnitId, + deadline: futureDate(Math.floor(Math.random() * 60) + 7), + priority: randomItem(PRIORITIES), + status: randomItem(STATUSES), + submission_note: "", + admin_notes: "", + }); + } + + console.log( + "Queued " + + tasksPerAssigner + + " task(s) for " + + assigner.role + + " - " + + name, + ); + } + + if (!tasksToInsert.length) { + console.log( + "No tasks to insert. Check that assigners have valid position holders and units.", + ); + await mongoose.disconnect(); + return; + } + + const inserted = await Task.insertMany(tasksToInsert); + console.log("Seeded " + inserted.length + " task(s) successfully."); + + await mongoose.disconnect(); + console.log("Disconnected from MongoDB"); +} + +seedTasks({ + tasksPerAssigner: 3, + clearExisting: true, +}).catch((err) => { + console.error("Seeding failed:", err); + mongoose.disconnect(); + process.exit(1); +}); diff --git a/backend/utils/authValidate.js b/backend/utils/authValidate.js new file mode 100644 index 00000000..a83f858c --- /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..dceb1033 --- /dev/null +++ b/backend/utils/batchValidate.js @@ -0,0 +1,16 @@ +const zod = require("zod"); + +const zodObjectId = zod.string().regex(/^[0-9a-zA-Z]{24}$/, "Invalid ObjectId"); + +const validateBatchSchema = zod.object({ + title: zod.string().min(5, "Title is required"), + unit_id: zodObjectId, + commonData: zod.record(zod.string(), zod.string()), + template_id: zod.string(), + users: zod.array(zodObjectId).min(1, "Atleast 1 user must be associated."), +}); + +module.exports = { + validateBatchSchema, + zodObjectId, +}; diff --git a/backend/utils/taskValidate.js b/backend/utils/taskValidate.js new file mode 100644 index 00000000..f026346f --- /dev/null +++ b/backend/utils/taskValidate.js @@ -0,0 +1,17 @@ +const zod = require("zod"); +const zodObjectId = zod.string().regex(/^[0-9a-fA-F]{24}$/, "Invalid ObjectId"); + +const taskValidate = zod.object({ + title: zod.string().min(5, "Title is required"), + description: zod.string().min(5, "Description is required"), + priority: zod.enum(["low", "medium", "high"]), + deadline: zod.coerce.date(), + assignees: zod + .array(zodObjectId) + .min(1, "Atleast 1 user must be assigned to the task."), +}); + +module.exports = { + taskValidate, + zodObjectId, +}; diff --git a/frontend/src/App.js b/frontend/src/App.jsx similarity index 98% rename from frontend/src/App.js rename to frontend/src/App.jsx index 5f7688ed..c8a0b1fa 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.jsx @@ -15,11 +15,10 @@ import Unauthorised from "./Components/Unauthorised"; import "react-toastify/dist/ReactToastify.css"; import { ToastContainer } from "react-toastify"; - - function App() { const authData = useAuth(); const { isUserLoggedIn, isOnboardingComplete, isLoading } = authData; + //console.log("User data is: ",authData); // const role = isUserLoggedIn?.role || "STUDENT"; // const navItems = NavbarConfig[role] || []; diff --git a/frontend/src/Components/AddPositionHolder.jsx b/frontend/src/Components/AddPositionHolder.jsx index 650b2bc5..cecba3c6 100644 --- a/frontend/src/Components/AddPositionHolder.jsx +++ b/frontend/src/Components/AddPositionHolder.jsx @@ -25,12 +25,12 @@ const AddPositionHolder = ({ onClose }) => { const [positions, setPositions] = useState([]); const [appointingUsers, setAppointingUsers] = useState([]); - const [positionSearchTerm, setPositionSearchTerm] = useState(""); const [appointedBySearchTerm, setAppointedBySearchTerm] = useState(""); const [isPositionDropdownOpen, setIsPositionDropdownOpen] = useState(false); - const [isAppointedByDropdownOpen, setIsAppointedByDropdownOpen] = useState(false); + const [isAppointedByDropdownOpen, setIsAppointedByDropdownOpen] = + useState(false); const [errors, setErrors] = useState({}); @@ -65,23 +65,33 @@ const AddPositionHolder = ({ onClose }) => { const filteredPositions = positions.filter( (position) => position.title.toLowerCase().includes(positionSearchTerm.toLowerCase()) || - position.unit_id.name.toLowerCase().includes(positionSearchTerm.toLowerCase()) + position.unit_id.name + .toLowerCase() + .includes(positionSearchTerm.toLowerCase()), ); const filteredAppointingUsers = appointingUsers.filter( (user) => - user.personal_info.name.toLowerCase().includes(appointedBySearchTerm.toLowerCase()) || - user.username.toLowerCase().includes(appointedBySearchTerm.toLowerCase()) + user.personal_info.name + .toLowerCase() + .includes(appointedBySearchTerm.toLowerCase()) || + user.username.toLowerCase().includes(appointedBySearchTerm.toLowerCase()), ); const validateForm = () => { const newErrors = {}; if (!formData.user_id) newErrors.user_id = "User selection is required"; - if (!formData.position_id) newErrors.position_id = "Position selection is required"; - if (!formData.tenure_year) newErrors.tenure_year = "Tenure year is required"; + if (!formData.position_id) + newErrors.position_id = "Position selection is required"; + if (!formData.tenure_year) + newErrors.tenure_year = "Tenure year is required"; if (!formData.status) newErrors.status = "Status is required"; - if (formData.appointment_details.appointment_date && !formData.appointment_details.appointed_by) { - newErrors.appointed_by = "Appointed by is required when appointment date is provided"; + if ( + formData.appointment_details.appointment_date && + !formData.appointment_details.appointed_by + ) { + newErrors.appointed_by = + "Appointed by is required when appointment date is provided"; } if (formData.performance_metrics.events_organized < 0) { newErrors.events_organized = "Events organized cannot be negative"; @@ -112,13 +122,20 @@ const AddPositionHolder = ({ onClose }) => { const cleanedData = { ...formData, appointment_details: - formData.appointment_details.appointed_by || formData.appointment_details.appointment_date + formData.appointment_details.appointed_by || + formData.appointment_details.appointment_date ? formData.appointment_details : undefined, performance_metrics: { ...formData.performance_metrics, - events_organized: formData.performance_metrics.events_organized === "" ? 0 : parseInt(formData.performance_metrics.events_organized, 10), - budget_utilized: formData.performance_metrics.budget_utilized === "" ? 0 : parseFloat(formData.performance_metrics.budget_utilized), + events_organized: + formData.performance_metrics.events_organized === "" + ? 0 + : parseInt(formData.performance_metrics.events_organized, 10), + budget_utilized: + formData.performance_metrics.budget_utilized === "" + ? 0 + : parseFloat(formData.performance_metrics.budget_utilized), feedback: formData.performance_metrics.feedback.trim() || undefined, }, }; @@ -132,8 +149,12 @@ const AddPositionHolder = ({ onClose }) => { } }; - const selectedPosition = positions.find((p) => p._id === formData.position_id); - const selectedAppointedBy = appointingUsers.find((u) => u._id === formData.appointment_details.appointed_by); + const selectedPosition = positions.find( + (p) => p._id === formData.position_id, + ); + const selectedAppointedBy = appointingUsers.find( + (u) => u._id === formData.appointment_details.appointed_by, + ); return (