diff --git a/backend/controllers/achievementController.js b/backend/controllers/achievementController.js new file mode 100644 index 00000000..9d97d53f --- /dev/null +++ b/backend/controllers/achievementController.js @@ -0,0 +1,128 @@ +const { Achievement } = require("../models/schema"); +const { v4: uuidv4 } = require("uuid"); + +// GET unverified achievements by type +exports.getUnendorsedAchievements = async (req, res) => { + const { type } = req.params; + + try { + const unverifiedAchievements = await Achievement.find({ + type, + verified: false, + }) + .populate("user_id", "personal_info.name username user_id") + .populate("event_id", "title description "); + + res.json(unverifiedAchievements); + } catch (err) { + console.error(err); + res + .status(500) + .json({ message: "Failed to fetch unverified achievements." }); + } +}; + +// PATCH verify achievement by ID +exports.verifyAchievement = async (req, res) => { + const { id } = req.params; + const { verified_by } = req.body; + try { + const achievement = await Achievement.findById(id); + + if (!achievement) { + return res.status(404).json({ message: "Achievement not found." }); + } + + achievement.verified = true; + achievement.verified_by = verified_by; + await achievement.save(); + + res.json({ message: "Achievement verified successfully.", achievement }); + } catch (err) { + console.error(err); + res.status(500).json({ message: "Failed to verify achievement." }); + } +}; + +// REJECT (delete) achievement by ID +exports.rejectAchievement = async (req, res) => { + const { id } = req.params; + + try { + const deletedAchievement = await Achievement.findByIdAndDelete(id); + + if (!deletedAchievement) { + return res.status(404).json({ + message: "Achievement not found.", + }); + } + + res.json({ + message: "Achievement rejected and deleted successfully.", + }); + } catch (err) { + console.error("Failed to reject achievement:", err); + res.status(500).json({ + message: "Failed to reject achievement.", + }); + } +}; + +// Add achievement +exports.addAchievement = async (req, res) => { + try { + const { + title, + description, + category, + type, + level, + date_achieved, + position, + certificate_url, + event_id, + user_id, + } = req.body; + + if (!title || !category || !date_achieved || !user_id) { + return res.status(400).json({ message: "Missing required fields" }); + } + + const achievement = new Achievement({ + achievement_id: uuidv4(), + user_id, + title, + description, + category, + type, + level, + date_achieved, + position, + certificate_url, + event_id: event_id || null, + }); + + await achievement.save(); + + return res + .status(201) + .json({ message: "Achievement saved successfully", achievement }); + } catch (error) { + console.error("Error saving achievement:", error); + return res.status(500).json({ message: "Server error" }); + } +}; + +// Get all user achievements (endorsed + unendorsed) +exports.getUserAchievements = async (req, res) => { + const userId = req.params.userId; + try { + const userAchievements = await Achievement.find({ user_id: userId }) + .populate("event_id", "title description") + .populate("verified_by", "personal_info.name username user_id"); + res.json(userAchievements); + } catch (err) { + console.error("Failed to get user Achievements:", err); + res.status(500).json({ message: "Failed to get user Achievements." }); + } +}; diff --git a/backend/controllers/announcementController.js b/backend/controllers/announcementController.js new file mode 100644 index 00000000..2f8d80aa --- /dev/null +++ b/backend/controllers/announcementController.js @@ -0,0 +1,265 @@ +const mongoose = require("mongoose"); +const { + Announcement, + Event, + OrganizationalUnit, + Position, +} = require("../models/schema"); + +// Helper function +const findTargetId = async (type, identifier) => { + let target = null; + + const isObjectId = mongoose.Types.ObjectId.isValid(identifier); + const objectId = isObjectId ? new mongoose.Types.ObjectId(identifier) : null; + if (type === "Event") { + target = await Event.findOne({ + $or: [ + ...(objectId ? [{ _id: identifier }] : []), + { event_id: identifier }, + ], + }); + } else if (type === "Organizational_Unit") { + target = await OrganizationalUnit.findOne({ + $or: [ + ...(objectId ? [{ _id: identifier }] : []), + { unit_id: identifier }, + ], + }); + } else if (type === "Position") { + target = await Position.findOne({ + $or: [ + ...(objectId ? [{ _id: identifier }] : []), + { position_id: identifier }, + ], + }); + } + + return target ? target._id : null; +}; + +exports.createAnnouncement = async (req, res) => { + try { + const { + title, + content, + type = "General", + isPinned, + targetIdentifier, + } = req.body; + let targetId = null; + + if (type != "General" && targetIdentifier) { + targetId = await findTargetId(type, targetIdentifier); + if (!targetId) { + return res + .status(404) + .json({ error: `No ${type} found with that identifier` }); + } + } + + const newAnnouncement = new Announcement({ + author: req.user._id, + content, + is_pinned: isPinned || false, + title, + type: type || "General", + target_id: targetId, + }); + await newAnnouncement.save(); + res.status(201).json(newAnnouncement); + } catch (error) { + console.error("Error creating announcement:", error); + res.status(500).json({ error: "Failed to create announcement" }); + } +}; + +exports.getAnnouncements = async (req, res) => { + try { + const { + type, + author, + isPinned, + search, + page = 1, + limit = 10, + sortBy = "createdAt", + sortOrder = "desc", + } = req.query; + + const filter = {}; + + if (type && type != "All") filter.type = type; + if (author) filter.author = author; + if (typeof isPinned !== "undefined") { + // accept true/false or 1/0 + const val = `${isPinned}`.toLowerCase(); + filter.is_pinned = val === "true" || val === "1"; + } + + if (search) { + const regex = new RegExp(search, "i"); + filter.$or = [{ title: regex }, { content: regex }]; + } + + const pageNum = Math.max(parseInt(page, 10) || 1, 1); + const limNum = Math.max(parseInt(limit, 10) || 10, 1); + + const sortDirection = sortOrder === "asc" ? 1 : -1; + const sort = { [sortBy]: sortDirection }; + + const total = await Announcement.countDocuments(filter); + const query = Announcement.find(filter) + .sort(sort) + .skip((pageNum - 1) * limNum) + .limit(limNum) + .populate("author", "username personal_info.email personal_info.name"); + + if (filter.type && filter.type !== "General") { + query.populate("target_id"); + } + const announcements = await query; + + res.json({ + total, + page: pageNum, + limit: limNum, + totalPages: Math.ceil(total / limNum) || 0, + announcements, + }); + // console.log(announcements); + } catch (error) { + console.error("Error fetching announcements:", error); + res.status(500).json({ error: "Failed to fetch announcements" }); + } +}; + +exports.getAnnouncementById = async (req, res) => { + try { + const { id } = req.params; + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: "Invalid announcement id" }); + } + + const announcement = await Announcement.findById(id) + .populate("author", "username personal_info.email personal_info.name") + .populate("target_id"); + + if (!announcement) { + return res.status(404).json({ error: "Announcement not found" }); + } + + res.json(announcement); + } catch (error) { + console.error("Error fetching announcement by id:", error); + res.status(500).json({ error: "Failed to fetch announcement" }); + } +}; + +exports.updateAnnouncement = async (req, res) => { + try { + const { id } = req.params; + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: "Invalid announcement id" }); + } + + const announcement = await Announcement.findById(id); + if (!announcement) { + return res.status(404).json({ error: "Announcement not found" }); + } + + // Only the author + const isAuthor = + announcement.author && + announcement.author.toString() === req.user._id.toString(); + if (!isAuthor) { + return res + .status(403) + .json({ error: "Forbidden: cannot edit this announcement" }); + } + + const { title, content, type, targetIdentifier, isPinned } = req.body; + if (title !== undefined) announcement.title = title; + if (content !== undefined) announcement.content = content; + if (isPinned !== undefined) announcement.is_pinned = Boolean(isPinned); + + if (type || targetIdentifier) { + const newType = type || announcement.type; + + const newIdentifier = + targetIdentifier || + (announcement.target_id ? announcement.target_id.toString() : null); + + if (newType === "General") { + announcement.type = "General"; + announcement.target_id = null; + } else { + if (!newIdentifier) { + return res.status(400).json({ + error: + "targetIdentifier is required when setting a non-General type", + }); + } + const newTargetId = await findTargetId(newType, newIdentifier); + if (!newTargetId) { + return res.status(404).json({ + error: `Target ${newType} not found with identifier ${newIdentifier}`, + }); + } + announcement.target_id = newTargetId; + announcement.type = newType; + } + } + + announcement.updatedAt = Date.now(); + await announcement.save(); + + const populated = await announcement.populate([ + { + path: "author", + select: "username personal_info.email personal_info.name", + }, + ]); + if (announcement.type !== "General") { + await populated.populate("target_id"); + } + res.json(populated); + } catch (error) { + console.error("Error updating announcement:", error); + res.status(500).json({ error: "Failed to update announcement" }); + } +}; + +exports.deleteAnnouncement = async (req, res) => { + try { + const { id } = req.params; + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: "Invalid announcement id" }); + } + + const announcement = await Announcement.findById(id); + if (!announcement) { + return res.status(404).json({ error: "Announcement not found" }); + } + + // Only the author + const isAuthor = + announcement.author && + announcement.author.toString() === req.user._id.toString(); + if (!isAuthor) { + return res + .status(403) + .json({ error: "Forbidden: cannot delete this announcement" }); + } + + await Announcement.deleteOne({ _id: id }); + + res.json({ message: "Announcement deleted", id }); + } catch (error) { + console.error("Error deleting announcement:", error); + res.status(500).json({ error: "Failed to delete announcement" }); + } +}; diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js new file mode 100644 index 00000000..5bc9f6c3 --- /dev/null +++ b/backend/controllers/authController.js @@ -0,0 +1,175 @@ +const jwt = require("jsonwebtoken"); +const isIITBhilaiEmail = require("../utils/isIITBhilaiEmail"); +const { User } = require("../models/schema"); +var nodemailer = require("nodemailer"); + +exports.fetchAuth = (req, res) => { + if (req.isAuthenticated()) { + res.json(req.user); + } else { + res.json(null); + } +}; + +exports.login = (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.", + }); + } + res.status(200).json({ message: "Login successful", user: req.user }); +}; + +exports.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 existingUser = await User.findOne({ username: email }); + if (existingUser) { + return res.status(400).json({ message: "User already exists." }); + } + + const newUser = await User.register( + new User({ + user_id: ID, + role: "STUDENT", + strategy: "local", + username: email, + personal_info: { + name: name, + email: email, + }, + onboardingComplete: false, + }), + password, + ); + + 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 }); + }); + } catch (error) { + console.error("Registration error:", error); + return res.status(500).json({ message: "Internal server error" }); + } +}; + +exports.googleCallback = (req, res) => { + if (req.user.onboardingComplete) { + res.redirect(`${process.env.FRONTEND_URL}/`); + } else { + res.redirect(`${process.env.FRONTEND_URL}/onboarding`); + } +}; + +exports.logout = (req, res, next) => { + req.logout(function (err) { + if (err) { + return next(err); + } + res.send("Logout Successful"); + }); +}; + +exports.forgotPassword = async (req, res) => { + try { + const { email } = req.body; + const user = await User.findOne({ username: email }); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + if (user.strategy === "google") { + return res.status(400).json({ + message: + "This email is linked with Google Login. Please use 'Sign in with Google' instead.", + }); + } + const secret = user._id + process.env.JWT_SECRET_TOKEN; + const token = jwt.sign({ email: email, id: user._id }, secret, { + expiresIn: "10m", + }); + const link = `${process.env.FRONTEND_URL}/reset-password/${user._id}/${token}`; + var transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, + }); + var mailOptions = { + from: process.env.EMAIL_USER, + to: email, + subject: "Password-Reset Request", + text: `To reset your password, click here: ${link}`, + }; + transporter.sendMail(mailOptions, function (error, info) { + if (error) { + console.log(error); + return res.status(500).json({ message: "Error sending email" }); + } else { + console.log("Email sent:", info.response); + return res + .status(200) + .json({ message: "Password reset link sent to your email" }); + } + }); + console.log(link); + } catch (error) { + console.log(error); + return res.status(500).json({ message: "Internal server error" }); + } +}; + +exports.verifyResetToken = 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); + return res.status(200).json({ message: "Token verified successfully" }); + } catch (error) { + console.log(error); + return res.status(400).json({ message: "Invalid or expired token" }); + } +}; + +exports.resetPassword = async (req, res) => { + const { id, token } = req.params; + const { password } = req.body; + 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); + user.setPassword(password, async (error) => { + if (error) { + return res.status(500).json({ message: "Error resetting password" }); + } + await user.save(); + return res + .status(200) + .json({ message: "Password has been reset successfully" }); + }); + } catch (error) { + console.log(error); + return res.status(400).json({ message: "Invalid or expired token" }); + } +}; diff --git a/backend/controllers/eventController.js b/backend/controllers/eventController.js new file mode 100644 index 00000000..a9187f45 --- /dev/null +++ b/backend/controllers/eventController.js @@ -0,0 +1,476 @@ +const { Event, User, OrganizationalUnit } = require("../models/schema"); +const { v4: uuidv4 } = require("uuid"); +const { ROLES } = require("../utils/roles"); + +// Fetch 4 most recently updated events +exports.getLatestEvents = async (req, res) => { + try { + const latestEvents = await Event.find({}) + .sort({ updated_at: -1 }) + .limit(4) + .select("title updated_at schedule.venue status"); + + const formatedEvents = latestEvents.map((event) => ({ + id: event._id, + title: event.title, + date: event.updated_at.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }), + venue: + event.schedule && event.schedule.venue ? event.schedule.venue : "TBA", + status: event.status || "TBD", + })); + res.status(200).json(formatedEvents); + } catch (error) { + console.error("Error fetching latest events:", error); + res.status(500).json({ message: "Server error" }); + } +}; + +// Create a new event (new events can be created by admins only) +exports.createEvent = async (req, res) => { + try { + const { + title, + description, + category, + type, + organizing_unit_id, + organizers, + schedule, + registration, + budget, + } = req.body; + + // Validate organizing unit + const orgUnit = await OrganizationalUnit.findById(organizing_unit_id); + if (!orgUnit) { + return res.status(400).json({ message: "Invalid organizational unit." }); + } + + // Optional: Validate organizer IDs + if (organizers && organizers.length > 0) { + const validUsers = await User.find({ _id: { $in: organizers } }); + if (validUsers.length !== organizers.length) { + return res + .status(400) + .json({ message: "One or more organizers are invalid." }); + } + } + + const newEvent = new Event({ + event_id: uuidv4(), + title, + description, + category, + type, + organizing_unit_id, + organizers, + schedule, + registration, + budget, + }); + + await newEvent.save(); + res + .status(201) + .json({ message: "Event created successfully", event: newEvent }); + console.log("Event created:", newEvent); + } catch (err) { + console.error(err); + res.status(500).json({ message: "Server error while creating event." }); + } +}; + +// GET all events (for all users: logged in or not logged in) +exports.getAllEvents = async (req, res) => { + try { + const events = await Event.find().populate("organizing_unit_id", "name"); + res.json(events); + } catch (err) { + console.error(err); + res.status(500).json({ message: "Error fetching events." }); + } +}; + +exports.getOrganizationalUnits = async (req, res) => { + try { + const role = (req.user && req.user.role) || ""; + const userEmail = String( + (req.user && + (req.user.username || + (req.user.personal_info && req.user.personal_info.email))) || + "", + ) + .trim() + .toLowerCase(); + + const categoryForRole = { + [ROLES.GENSEC_SCITECH]: "scitech", + [ROLES.GENSEC_ACADEMIC]: "academic", + [ROLES.GENSEC_CULTURAL]: "cultural", + [ROLES.GENSEC_SPORTS]: "sports", + }; + + let units = []; + + if (role === ROLES.PRESIDENT) { + // President sees all units + units = await OrganizationalUnit.find(); + } else if (categoryForRole[role]) { + // GenSecs see units by category + units = await OrganizationalUnit.find({ + category: categoryForRole[role], + }); + } else if (role === ROLES.CLUB_COORDINATOR) { + // Club Coordinator sees only their own unit (matched by contact email) + const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const coordUnit = await OrganizationalUnit.findOne({ + "contact_info.email": new RegExp(`^${escapeRegex(userEmail)}$`, "i"), + }); + units = coordUnit ? [coordUnit] : []; + } else { + // Default: return all units (keeps previous behavior for non-admins if needed) + units = await OrganizationalUnit.find(); + } + + res.json(units); + } catch (err) { + console.error(err); + res.status(500).json({ message: "Error fetching organizational units." }); + } +}; + +exports.getUsers = async (req, res) => { + try { + const users = await User.find({ role: "STUDENT" }); + res.json(users); + } catch (err) { + console.error(err); + res.status(500).json({ message: "Error fetching users." }); + } +}; + +/** + * Returns { isContact: true|false } for the logged-in user. + */ +exports.isEventContact = async (req, res) => { + try { + const { eventId } = req.params; + const event = await Event.findById(eventId).populate("organizing_unit_id"); + if (!event) return res.status(404).json({ message: "Event not found" }); + + // Defensive checks instead of optional chaining for lint/parse compatibility + const unit = event.organizing_unit_id; + const unitEmail = String( + (unit && unit.contact_info && unit.contact_info.email) || "", + ) + .trim() + .toLowerCase(); + + const userEmail = String( + (req.user && + (req.user.username || + (req.user.personal_info && req.user.personal_info.email))) || + "", + ) + .trim() + .toLowerCase(); + + const isContact = !!(unitEmail && userEmail && unitEmail === userEmail); + return res.json({ isContact }); + } catch (err) { + console.error("is-contact error:", err); + return res.status(500).json({ message: "Server error" }); + } +}; + +// Register student for an event +exports.registerForEvent = async (req, res) => { + try { + const { eventId } = req.params; + const userId = req.user._id; + + const event = await Event.findById(eventId); + if (!event) { + return res.status(404).json({ message: "Event not found." }); + } + + if (event.status === "completed") { + return res + .status(400) + .json({ message: "Registration is closed for this event." }); + } + + if (event.participants.some((p) => p.equals(userId))) { + return res + .status(409) + .json({ message: "You are already registered for this event." }); + } + + if (event.registration && event.registration.required) { + const now = new Date(); + + if ( + event.registration.start && + now < new Date(event.registration.start) + ) { + return res + .status(400) + .json({ message: "Registration has not started yet." }); + } + + if (event.registration.end && now > new Date(event.registration.end)) { + return res.status(400).json({ message: "Registration has ended." }); + } + + const maxParticipants = event.registration.max_participants; + if (maxParticipants) { + const updatedEvent = await Event.findOneAndUpdate( + { + _id: eventId, + $expr: { $lt: [{ $size: "$participants" }, maxParticipants] }, + }, + { $addToSet: { participants: userId } }, + { new: true }, + ); + + if (!updatedEvent) { + return res.status(400).json({ message: "Registration is full." }); + } + + return res.status(200).json({ + message: "Successfully registered for the event.", + event: updatedEvent, + }); + } + } + + const updatedEvent = await Event.findByIdAndUpdate( + eventId, + { $addToSet: { participants: userId } }, + { new: true }, + ); + + return res.status(200).json({ + message: "Successfully registered for the event.", + event: updatedEvent, + }); + } catch (error) { + if (error?.name === "CastError") { + return res.status(400).json({ message: "Invalid event ID format." }); + } + console.error("Event registration error:", error); + return res + .status(500) + .json({ message: "Server error during registration." }); + } +}; + +// GET event by ID +exports.getEventById = async (req, res) => { + try { + const event = await Event.findById(req.params.id) + .populate("organizing_unit_id", "name") + .populate("organizers", "personal_info.name"); + res.json(event); + } catch (err) { + console.error(err); + res.status(500).json({ message: "Error fetching event." }); + } +}; + +exports.getEventsByRole = async (req, res) => { + const userRole = req.params.userRole; + try { + let query = {}; + + // Build the query based on the user's role + switch (userRole) { + case "STUDENT": + query = { status: { $in: ["planned", "ongoing"] } }; + break; + + case "CLUB_COORDINATOR": { + const username = req.query.username; + if (!username) { + return res + .status(400) + .json({ message: "Username is required for Club Coordinator." }); + } + // 1. Find the organizational unit where the contact email matches the username + const orgUnit = await OrganizationalUnit.findOne({ + "contact_info.email": username, + }); + + if (!orgUnit) { + return res.status(404).json({ + message: "No organizational unit found for this coordinator.", + }); + } + // 2. Set the query to filter events by the unit's _id + query = { organizing_unit_id: orgUnit._id }; + break; + } + case "GENSEC_SCITECH": + query = { category: "technical" }; + break; + + case "GENSEC_ACADEMIC": + query = { category: "academic" }; + break; + + case "GENSEC_CULTURAL": + query = { category: "cultural" }; + break; + + case "GENSEC_SPORTS": + query = { category: "sports" }; + break; + + case "PRESIDENT": + query = {}; + break; + + default: + query = { status: { $in: ["planned", "ongoing"] } }; + break; + } + + const events = await Event.find(query) + .sort({ "schedule.start": 1 }) + .populate("organizing_unit_id") + .populate("organizers") + .populate("participants") + .populate("winners.user") + .populate("room_requests.reviewed_by"); + + res.status(200).json(events); + } catch (error) { + console.error("Error fetching events:", error); + res.status(500).json({ message: "Server error while fetching events" }); + } +}; + +//room request +exports.addRoomRequest = async (req, res) => { + try { + const { eventId } = req.params; + const { date, time, room, description } = req.body; + if (!date || !time || !room) { + return res + .status(400) + .json({ message: "Date, time, and room are required fields." }); + } + const event = await Event.findById(eventId); + + if (!event) { + return res.status(404).json({ message: "Event not found." }); + } + const newRoomRequest = { + date, + time, + room, + description: description || "", + }; + + event.room_requests.push(newRoomRequest); + const updatedEvent = await event.save(); + res.status(201).json(updatedEvent); + } catch (error) { + console.error("Error adding room request:", error); + if (error.name === "CastError") { + return res.status(400).json({ message: "Invalid event ID format." }); + } + res + .status(500) + .json({ message: "Server error while adding room request." }); + } +}; + +exports.updateRoomRequestStatus = async (req, res) => { + const { requestId } = req.params; + const { status, reviewed_by } = req.body; + if (!status || !["Approved", "Rejected"].includes(status)) { + return res.status(400).json({ + message: 'A valid status ("Approved" or "Rejected") is required.', + }); + } + try { + const event = await Event.findOne({ "room_requests._id": requestId }); + if (!event) { + return res + .status(404) + .json({ message: "Request or associated event not found." }); + } + const request = event.room_requests.id(requestId); + if (request) { + request.status = status; + request.requested_at = new Date(); + request.reviewed_by = reviewed_by; + } + + const updatedEvent = await event.save(); + res.status(200).json(updatedEvent); + } catch (error) { + console.error(`Error updating request status to ${status}:`, error); + res + .status(500) + .json({ message: "Server error while updating request status." }); + } +}; + +// Update an event (only unit contact) +exports.updateEvent = async (req, res) => { + try { + const { eventId } = req.params; + const updates = req.body; + + // 🔍 DEBUG LOGS - START + console.log("\n=== 📝 UPDATE EVENT DEBUG ==="); + console.log("Event ID:", eventId); + console.log( + "Updates received (full body):", + JSON.stringify(updates, null, 2), + ); + console.log("Number of fields to update:", Object.keys(updates).length); + console.log("Fields being updated:", Object.keys(updates)); + console.log("========================\n"); + + // Fetch the event BEFORE update to compare + const eventBefore = await Event.findById(eventId); + console.log("Event BEFORE update:", JSON.stringify(eventBefore, null, 2)); + + const event = await Event.findByIdAndUpdate(eventId, updates, { + new: true, + runValidators: true, // Added this to ensure validation runs + }); + + console.log("\n=== ✅ UPDATE RESULT ==="); + console.log("Event AFTER update:", JSON.stringify(event, null, 2)); + console.log("Update successful:", !!event); + console.log("========================\n"); + + if (!event) return res.status(404).json({ message: "Event not found" }); + return res.json({ message: "Event updated", event }); + } catch (err) { + console.error("❌ update event error:", err); + return res + .status(500) + .json({ message: "Server error", error: err.message }); + } +}; + +// Delete an event (only unit contact) +exports.deleteEvent = async (req, res) => { + try { + const { eventId } = req.params; + const deleted = await Event.findByIdAndDelete(eventId); + if (!deleted) return res.status(404).json({ message: "Event not found" }); + return res.json({ message: "Event deleted" }); + } catch (err) { + console.error("delete event error:", err); + return res.status(500).json({ message: "Server error" }); + } +}; diff --git a/backend/controllers/eventControllers.js b/backend/controllers/eventControllers.js deleted file mode 100644 index 60832b0e..00000000 --- a/backend/controllers/eventControllers.js +++ /dev/null @@ -1,23 +0,0 @@ -const {Event} = require('../models/schema'); - -// fetch 4 most recently updated events -exports.getLatestEvents = async (req, res) => { - try{ - const latestEvents = await Event.find({}) - .sort({updated_at: -1}) - .limit(4) - .select('title updated_at schedule.venue status'); - - const formatedEvents =latestEvents.map(event=>({ - id: event._id, - title: event.title, - date: event.updated_at.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), - venue: (event.schedule && event.schedule.venue) ? event.schedule.venue : 'TBA', - status: event.status || 'TBD' - })) - res.status(200).json(formatedEvents); - } catch (error) { - console.error('Error fetching latest events:', error); - res.status(500).json({ message: 'Server error' }); - } -}; \ No newline at end of file diff --git a/backend/controllers/feedbackController.js b/backend/controllers/feedbackController.js new file mode 100644 index 00000000..68393971 --- /dev/null +++ b/backend/controllers/feedbackController.js @@ -0,0 +1,224 @@ +const { + User, + Feedback, + Event, + Position, + OrganizationalUnit, +} = require("./../models/schema"); +const { v4: uuidv4 } = require("uuid"); + +exports.addFeedback = async (req, res) => { + try { + const { + type, + target_type, + target_id, + feedback_by, + rating, + comments, + is_anonymous, + } = req.body; + + if (!type || !target_type || !target_id || !feedback_by) { + return res.status(400).json({ message: "Missing required fields" }); + } + + const targetModels = { + User, + Event, + "Club/Organization": OrganizationalUnit, + POR: Position, + }; + + const TargetModel = targetModels[target_type]; + + if (!TargetModel) { + return res.status(400).json({ message: "Invalid target type" }); + } + + const feedback = new Feedback({ + feedback_id: uuidv4(), + type, + target_type, + target_id, + feedback_by, + rating, + comments, + is_anonymous: is_anonymous === "ture" || is_anonymous === true, // Typo preserved from original code "ture" + }); + + await feedback.save(); + console.log("Feedback added successfully:", feedback); + res + .status(201) + .json({ message: "Feedback submitted successfully", feedback }); + } catch (error) { + console.error("Error adding feedback:", error); + res.status(500).json({ message: "Failed to submit feedback" }); + } +}; + +exports.getTargetIds = async (req, res) => { + try { + 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({}) + .populate("unit_id", "name") + .select("_id title unit_id"); + + const formattedUsers = users.map((user) => ({ + _id: user._id, + name: user.personal_info.name, + user_id: user.user_id, + })); + + const formattedPositions = positions.map((position) => ({ + _id: position._id, + title: position.title, + unit: position.unit_id ? position.unit_id.name : "N/A", + })); + + res.json({ + users: formattedUsers, + events, + organizational_units, + positions: formattedPositions, + }); + } catch (error) { + console.error("Error fetching target ID:", error); + res.status(500).json({ message: "Failed to fetch target IDs" }); + } +}; + +exports.viewFeedback = async (req, res) => { + try { + const feedback = await Feedback.find() + .populate("feedback_by", "personal_info.name username") + .sort({ created_at: -1 }); + + const populatedFeedback = await Promise.all( + feedback.map(async (fb) => { + let targetData = null; + + switch (fb.target_type) { + case "User": + targetData = await User.findById(fb.target_id).select( + "personal_info.name user_id username", + ); + break; + + case "Event": { + const event = await Event.findById(fb.target_id).select( + "title organizing_unit_id", + ); + if (event) { + const orgUnit = await OrganizationalUnit.findById( + event.organizing_unit_id, + ).select("name"); + targetData = { + title: event.title, + organizing_unit: orgUnit ? orgUnit.name : "N/A", + }; + } + break; + } + + case "Club/Organization": { + const org = await OrganizationalUnit.findById(fb.target_id).select( + "name parent_unit_id", + ); + if (org) { + const parent = await OrganizationalUnit.findById( + org.parent_unit_id, + ).select("name"); + targetData = { + name: org.name, + parent: parent ? parent.name : "N/A", + }; + } + break; + } + + case "POR": { + const position = await Position.findById(fb.target_id).select( + "title unit_id", + ); + if (position) { + const unit = await OrganizationalUnit.findById( + position.unit_id, + ).select("name"); + targetData = { + title: position.title, + unit: unit ? unit.name : "N/A", + }; + } + break; + } + + default: + targetData = null; + break; + } + const fbObj = fb.toObject(); + fbObj.target_data = targetData; + return fbObj; + }), + ); + res.status(200).json(populatedFeedback); + } catch (error) { + console.error("Error viewing feedback:", error); + res.status(500).json({ message: "Failed to retrieve feedback" }); + } +}; + +exports.markFeedbackResolved = 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" }); + } + + if (feedback.is_resolved) { + return res.status(400).json({ error: "Feedback is already resolved." }); + } + + feedback.is_resolved = true; + feedback.resolved_at = new Date(); + feedback.actions_taken = actions_taken; + feedback.resolved_by = resolved_by; + + 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" }); + } +}; + +exports.getUserFeedbacks = async (req, res) => { + const userId = req.params.userId; + try { + const userFeedbacks = await Feedback.find({ feedback_by: userId }).populate( + "resolved_by", + "personal_info.name username user_id", + ); + res.json(userFeedbacks); + } catch (err) { + console.error("Failed to get user Feedbacks:", err); + res.status(500).json({ message: "Failed to get user Feedbacks." }); + } +}; diff --git a/backend/controllers/onboardingController.js b/backend/controllers/onboardingController.js new file mode 100644 index 00000000..5fda8eeb --- /dev/null +++ b/backend/controllers/onboardingController.js @@ -0,0 +1,33 @@ +const { User } = require("../models/schema"); + +exports.completeOnboarding = async (req, res) => { + const { ID_No, add_year, Program, discipline, mobile_no } = req.body; + + try { + 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 || "", + }), + academic_info: { + program: Program.trim(), + branch: discipline, + batch_year: add_year, + }, + role: req.user.role, // required field + strategy: req.user.strategy, // required field + }, + { new: true, runValidators: true }, + ); + + 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 }); + } +}; diff --git a/backend/controllers/orgUnitController.js b/backend/controllers/orgUnitController.js new file mode 100644 index 00000000..76dc5d6f --- /dev/null +++ b/backend/controllers/orgUnitController.js @@ -0,0 +1,195 @@ +const mongoose = require("mongoose"); +const { v4: uuidv4 } = require("uuid"); +const { + OrganizationalUnit, + Event, + Position, + PositionHolder, + Achievement, + Feedback, + User, +} = require("../models/schema"); + +exports.getClubData = async (req, res) => { + try { + const email = req.params.email; + if (!email || email.trim() === "") { + return res.status(400).json({ error: "Missing email" }); + } + + const unit = await OrganizationalUnit.findOne({ + "contact_info.email": email, + }).populate("parent_unit_id"); + if (!unit) { + return res.status(404).json({ error: "Organizational Unit not found" }); + } + + const events = await Event.find({ organizing_unit_id: unit._id }) + .populate("participants") + .populate("winners.user"); + const eventIds = events.map((e) => e._id); + + const achievements = await Achievement.find({ event_id: { $in: eventIds } }) + .populate("user_id") + .populate("event_id") + .populate("verified_by"); + + const positions = await Position.find({ unit_id: unit._id }).populate( + "unit_id", + ); + const positionIds = positions.map((pos) => pos._id); + + const positionHolders = await PositionHolder.find({ + position_id: { $in: positionIds }, + }) + .populate("user_id") + .populate("appointment_details.appointed_by") + .populate({ + path: "position_id", + populate: { path: "unit_id" }, + }); + + const feedbacks = await Feedback.find({ + target_type: "Club/Organization", + target_id: unit._id, + }) + .populate("feedback_by") + .populate("resolved_by"); + + res.json({ + unit, + events, + positions, + positionHolders, + achievements, + feedbacks, + }); + } catch (e) { + console.error(e); + res.status(500).json({ error: "Server error" }); + } +}; + +exports.getAllOrganizationalUnits = async (req, res) => { + try { + const { category, type } = req.query; + + const filter = {}; + + if (category) { + filter.category = category; + } + if (type) { + filter.type = type; + } + const units = await OrganizationalUnit.find(filter).sort({ + name: 1, + }); + res.status(200).json(units); + } catch (error) { + res.status(500).json({ + message: "Server error while fetching organizational units", + error, + }); + } +}; + +exports.createOrganizationalUnit = async (req, res) => { + const session = await mongoose.startSession(); + session.startTransaction(); + + try { + const { + name, + type, + description, + parent_unit_id, + hierarchy_level, + category, + is_active, + contact_info, + budget_info, + } = req.body; + + if ( + !name || + !type || + !category || + !hierarchy_level || + !contact_info.email + ) { + return res.status(400).json({ + message: + "Validation failed: name, type, category, and hierarchy_level are required.", + }); + } + + const newUnitData = { + unit_id: `org-${uuidv4()}`, + name, + type, + description, + parent_unit_id: parent_unit_id || null, + hierarchy_level, + category, + is_active, + contact_info, + budget_info, + }; + + const newUnit = new OrganizationalUnit(newUnitData); + await newUnit.save(session); + + const existingUser = await User.findOne({ + username: contact_info.email, + }).session(session); + if (existingUser) { + // If user exists, we must abort the transaction to avoid an inconsistent state + await session.abortTransaction(); + session.endSession(); + return res.status(409).json({ + message: + "A user with this email already exists. The organizational unit was not created.", + }); + } + + const newUser = new User({ + username: contact_info.email, + role: "CLUB_COORDINATOR", // Assign the specified role + strategy: "google", // Set strategy to google + onboardingComplete: true, // Set onboarding to true + personal_info: { + name: name, // Use the organization's name for the user's name + email: contact_info.email, + }, + }); + await newUser.save({ session }); // Pass the session to the save command + + // --- 5. If both operations were successful, commit the transaction --- + await session.commitTransaction(); + session.endSession(); + + res.status(201).json(newUnit); + } catch (error) { + await session.abortTransaction(); + session.endSession(); + console.error("Error creating organizational unit and user: ", error); + + if (error.name === "ValidationError" || error.name === "CastError") { + return res + .status(400) + .json({ message: `Invalid data provided: ${error.message}` }); + } + + if (error.code === 11000) { + return res.status(409).json({ + message: + "Conflict: An organizational unit with this name or ID already exists.", + }); + } + + res + .status(500) + .json({ message: "Server error while creating organizational unit." }); + } +}; diff --git a/backend/controllers/positionController.js b/backend/controllers/positionController.js new file mode 100644 index 00000000..3e8e6638 --- /dev/null +++ b/backend/controllers/positionController.js @@ -0,0 +1,170 @@ +const { Position, PositionHolder } = require("../models/schema"); +const { v4: uuidv4 } = require("uuid"); + +// POST for adding a new position +exports.addPosition = async (req, res) => { + try { + const { + title, + unit_id, + position_type, + responsibilities, + description, + position_count, + requirements, + } = req.body; + + // Validation + if (!title || !unit_id || !position_type) { + return res.status(400).json({ error: "Missing required fields" }); + } + + const newPosition = new Position({ + position_id: uuidv4(), + title, + unit_id, + position_type, + responsibilities, + description, + position_count, + requirements, + }); + + await newPosition.save(); + res.status(201).json(newPosition); + } catch (error) { + console.error("Error adding position:", error); + res.status(500).json({ error: "Server error" }); + } +}; + +// for getting all the position +exports.getAllPositions = async (req, res) => { + try { + const positions = await Position.find().populate( + "unit_id", + "name category contact_info.email", + ); + res.json(positions); + } catch (err) { + console.error(err); + res.status(500).json({ message: "Error fetching positions." }); + } +}; + +//add position holder +exports.addPositionHolder = async (req, res) => { + try { + const { + user_id, + position_id, + tenure_year, + appointment_details, + performance_metrics, + status, + } = req.body; + if (!user_id || !position_id || !tenure_year) { + return res.status(400).json({ message: "Missing required fields." }); + } + + // Step 1: Fetch the position to get the position_count + const position = await Position.findById(position_id); + if (!position) { + return res.status(404).json({ message: "Position not found." }); + } + + // Step 2: Count how many holders already exist for this position and tenure + const existingCount = await PositionHolder.countDocuments({ + position_id, + tenure_year, + }); + + if (existingCount >= position.position_count) { + return res.status(400).json({ + message: `Maximum position holders (${position.position_count}) already appointed for the year ${tenure_year}.`, + }); + } + + console.log(req.body); + const newPH = new PositionHolder({ + por_id: uuidv4(), + user_id, + position_id, + tenure_year, + appointment_details: + appointment_details && + (appointment_details.appointed_by || + appointment_details.appointment_date) + ? appointment_details + : undefined, + performance_metrics: { + events_organized: + performance_metrics && + performance_metrics.events_organized !== undefined + ? performance_metrics.events_organized + : 0, + budget_utilized: + performance_metrics && + performance_metrics.budget_utilized !== undefined + ? performance_metrics.budget_utilized + : 0, + feedback: + performance_metrics && performance_metrics.feedback + ? performance_metrics.feedback.trim() + : undefined, + }, + status, + }); + + const saved = await newPH.save(); + res.status(201).json(saved); + } catch (err) { + console.error("Error adding position holder:", err); + res.status(500).json({ message: "Internal server error" }); + } +}; + +// Get all position holders +exports.getAllPositionHolders = async (req, res) => { + try { + const positionHolders = await PositionHolder.find() + .populate("user_id", "personal_info.name user_id username") + .populate({ + path: "position_id", + select: "title unit_id position_type", + populate: { + path: "unit_id", + select: "name", + }, + }) + .populate( + "appointment_details.appointed_by", + "personal_info.name username user_id", + ); + + res.json(positionHolders); + } catch (err) { + console.error(err); + res.status(500).json({ message: "Error fetching position holders." }); + } +}; + +// Get positions by id +exports.getPositionHoldersByUserId = async (req, res) => { + const userId = req.params.userId; + try { + const positionHolder = await PositionHolder.find({ user_id: userId }) + .populate({ + path: "position_id", + populate: { + path: "unit_id", + model: "Organizational_Unit", + }, + }) + .populate("appointment_details.appointed_by"); + res.json(positionHolder); + } catch (err) { + console.error(err); + res.status(500).json({ message: "Error fetching position holder." }); + } +}; diff --git a/backend/controllers/profileController.js b/backend/controllers/profileController.js new file mode 100644 index 00000000..4e32b9f1 --- /dev/null +++ b/backend/controllers/profileController.js @@ -0,0 +1,227 @@ +const cloudinary = require("cloudinary").v2; +const { User } = require("../models/schema"); +const streamifier = require("streamifier"); + +// Cloudinary config +cloudinary.config({ + cloud_name: process.env.CLOUDINARY_CLOUD_NAME, + api_key: process.env.CLOUDINARY_API_KEY, + api_secret: process.env.CLOUDINARY_API_SECRET, +}); + +exports.updateProfilePhoto = async (req, res) => { + try { + const { ID_No } = req.body; + 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 ( + !req.files || + !req.files["image"] || + req.files["image"].length === 0 + ) { + return res.status(400).json({ error: "Image file is required" }); + } + + const file = req.files["image"][0]; + + // Delete old image if exists + if (user.personal_info.cloudinaryUrl) { + await cloudinary.uploader.destroy(user.personal_info.cloudinaryUrl); + } + + // Upload new image using upload_stream + const streamUpload = (fileBuffer) => { + return new Promise((resolve, reject) => { + let stream = cloudinary.uploader.upload_stream( + { folder: "profile-photos" }, + (error, result) => { + if (result) { + resolve(result); + } else { + reject(error); + } + }, + ); + streamifier.createReadStream(fileBuffer).pipe(stream); + }); + }; + + const result = await streamUpload(file.buffer); + + user.personal_info.profilePic = result.secure_url; + user.personal_info.cloudinaryUrl = result.public_id; + await user.save(); + + res.json({ profilePic: user.personal_info.profilePic }); + } catch (err) { + console.error("Upload error:", err); + res.status(500).json({ error: "Upload failed" }); + } +}; + +// Delete profile photo (reset to default) +exports.deleteProfilePhoto = 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" }); + } + + const user = await User.findOne({ user_id: ID_No }); // Capital User + if (!user) { + return res.status(404).json({ error: "User not found" }); + } + + // Delete from Cloudinary if exists + if (user.personal_info.cloudinaryUrl) { + await cloudinary.uploader.destroy(user.personal_info.cloudinaryUrl); + user.personal_info.profilePic = "https://www.gravatar.com/avatar/?d=mp"; // Default photo + user.personal_info.cloudinaryUrl = ""; + await user.save(); + } + + res.json({ profilePic: user.personal_info.profilePic }); + } catch (err) { + res.status(500).json({ error: "Delete failed" }); + } +}; + +// API to Update Student Profile +exports.updateStudentProfile = async (req, res) => { + try { + const { userId, updatedDetails } = req.body; + console.log("Received userId:", userId); + console.log("Received updatedDetails:", updatedDetails); + + if (!userId || !updatedDetails) { + return res + .status(400) + .json({ success: false, message: "Missing required fields" }); + } + + const user = await User.findOne({ user_id: userId }); // <-- updated from ID_No + + if (!user) { + return res + .status(404) + .json({ success: false, message: "Student not found" }); + } + + // ---------- PERSONAL INFO ---------- + if (updatedDetails.personal_info) { + const { + name, + email, + phone, + gender, + date_of_birth, + profilePic, + 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; + } + } + + // ---------- ACADEMIC INFO ---------- + if (updatedDetails.academic_info) { + 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; + } + } + + // ---------- 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; + } + + // Social Links + if (socialLinks) { + user.contact_info.socialLinks.github = + socialLinks.github != null + ? socialLinks.github + : user.contact_info.socialLinks.github; + + user.contact_info.socialLinks.linkedin = + socialLinks.linkedin != null + ? socialLinks.linkedin + : user.contact_info.socialLinks.linkedin; + + user.contact_info.socialLinks.instagram = + socialLinks.instagram != null + ? socialLinks.instagram + : user.contact_info.socialLinks.instagram; + + user.contact_info.socialLinks.other = + socialLinks.other != null + ? socialLinks.other + : user.contact_info.socialLinks.other; + } + } + + // Update the updated_at timestamp + user.updated_at = new Date(); + + // Save changes + await user.save(); + + return res.status(200).json({ + success: true, + message: "Student profile updated successfully", + updatedStudent: user, + }); + } catch (error) { + console.error("Error updating student profile:", error); + return res.status(500).json({ + success: false, + message: "Internal server error", + }); + } +}; diff --git a/backend/controllers/skillController.js b/backend/controllers/skillController.js new file mode 100644 index 00000000..8b349da4 --- /dev/null +++ b/backend/controllers/skillController.js @@ -0,0 +1,226 @@ +const { UserSkill, Skill } = require("../models/schema"); +const { v4: uuidv4 } = require("uuid"); + +// GET unendorsed user skills for a particular skill type +exports.getUnendorsedUserSkills = async (req, res) => { + const skillType = req.params.type; // e.g. "cultural", "sports" + + try { + const skills = await UserSkill.find({ is_endorsed: false }) + .populate({ + path: "skill_id", + match: { type: skillType }, + }) + .populate("user_id", "personal_info.name username user_id") // optionally fetch user info + .populate("position_id", "title"); + + // Filter out null populated skills (i.e., skill type didn't match) + const filtered = skills.filter((us) => us.skill_id !== null); + + res.json(filtered); + } catch (err) { + console.error(err); + res.status(500).json({ message: "Error fetching unendorsed skills." }); + } +}; + +exports.endorseUserSkill = async (req, res) => { + const skillId = req.params.id; + try { + const userSkill = await UserSkill.findById(skillId); + if (!userSkill) { + return res.status(404).json({ message: "User skill not found" }); + } + userSkill.is_endorsed = true; + await userSkill.save(); + res.json({ message: "User skill endorsed successfully" }); + } catch (err) { + console.error(err); + res.status(500).json({ message: "Error endorsing user skill" }); + } +}; + +// REJECT (delete) a user skill +exports.rejectUserSkill = async (req, res) => { + const skillId = req.params.id; + + try { + const deletedSkill = await UserSkill.findByIdAndDelete(skillId); + + if (!deletedSkill) { + return res.status(404).json({ + message: "User skill not found", + }); + } + + res.json({ + message: "User skill rejected and deleted successfully", + }); + } catch (err) { + console.error("Error rejecting user skill:", err); + res.status(500).json({ + message: "Error rejecting user skill", + }); + } +}; + +// GET all unendorsed skills by type +exports.getUnendorsedSkills = async (req, res) => { + const skillType = req.params.type; + + try { + const skills = await Skill.find({ type: skillType, is_endorsed: false }); + res.json(skills); + } catch (err) { + console.error(err); + res.status(500).json({ message: "Error fetching unendorsed skills." }); + } +}; + +// POST endorse a skill +exports.endorseSkill = async (req, res) => { + const skillId = req.params.id; + + try { + const skill = await Skill.findById(skillId); + + if (!skill) { + return res.status(404).json({ message: "Skill not found" }); + } + + skill.is_endorsed = true; + await skill.save(); + + res.json({ message: "Skill endorsed successfully", skill }); + } catch (err) { + console.error(err); + res.status(500).json({ message: "Failed to endorse skill." }); + } +}; + +// REJECT (delete) a skill +exports.rejectSkill = async (req, res) => { + const skillId = req.params.id; + + try { + const deletedSkill = await Skill.findByIdAndDelete(skillId); + + if (!deletedSkill) { + return res.status(404).json({ + message: "Skill not found", + }); + } + + res.json({ + message: "Skill rejected and deleted successfully", + }); + } catch (err) { + console.error("Error rejecting skill:", err); + res.status(500).json({ + message: "Failed to reject skill", + }); + } +}; + +//get all endorsed skills +exports.getAllSkills = async (req, res) => { + try { + const skills = await Skill.find({ is_endorsed: true }); + res.json(skills); + } catch (err) { + console.error(err); + res.status(500).json({ message: "Failed to get endorsed skills." }); + } +}; + +//get all user skills (endorsed + unendorsed) +exports.getUserSkills = async (req, res) => { + const userId = req.params.userId; + try { + const userSkills = await UserSkill.find({ user_id: userId }) + .populate("skill_id") + .populate({ + path: "position_id", + populate: { + path: "unit_id", + select: "name", + }, + }); + res.json(userSkills); + } catch (err) { + console.error("Failed to get user skills:", err); + res.status(500).json({ message: "Failed to get user skills." }); + } +}; + +//create a new skill +exports.createSkill = async (req, res) => { + try { + const { name, category, type, description } = req.body; + const skill = new Skill({ + skill_id: uuidv4(), + name, + category, + type, + description, + }); + await skill.save(); + res.status(201).json(skill); + } catch (err) { + res.status(500).json({ error: "Failed to add skill" }); + } +}; + +//create new user skill +exports.createUserSkill = async (req, res) => { + try { + const { user_id, skill_id, proficiency_level, position_id } = req.body; + + const newUserSkill = new UserSkill({ + user_id, + skill_id, + proficiency_level, + position_id: position_id || null, + }); + + await newUserSkill.save(); + res.status(201).json(newUserSkill); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to add user skill" }); + } +}; + +// GET top 5 most popular skills campus-wide +exports.getTopSkills = async (req, res) => { + try { + const topSkills = await UserSkill.aggregate([ + { $match: { is_endorsed: true } }, + { $group: { _id: "$skill_id", totalUsers: { $sum: 1 } } }, + { $sort: { totalUsers: -1 } }, + { $limit: 5 }, + { + $lookup: { + from: "skills", + localField: "_id", + foreignField: "_id", + as: "skillDetails", + }, + }, + { $unwind: "$skillDetails" }, + { + $project: { + _id: 0, + skillName: "$skillDetails.name", + type: "$skillDetails.type", + totalUsers: 1, + }, + }, + ]); + + res.json(topSkills); + } catch (err) { + console.error(err); + res.status(500).json({ message: "Error fetching top skills." }); + } +}; diff --git a/backend/index.js b/backend/index.js index 6fa25570..e0314750 100644 --- a/backend/index.js +++ b/backend/index.js @@ -65,7 +65,6 @@ 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); // Start the server diff --git a/backend/routes/achievements.js b/backend/routes/achievements.js index 026bc288..cd78d89b 100644 --- a/backend/routes/achievements.js +++ b/backend/routes/achievements.js @@ -1,35 +1,16 @@ const express = require("express"); const router = express.Router(); -const { Achievement } = require("../models/schema"); // Update path as needed -const { v4: uuidv4 } = require("uuid"); const isAuthenticated = require("../middlewares/isAuthenticated"); const authorizeRole = require("../middlewares/authorizeRole"); const { ROLE_GROUPS } = require("../utils/roles"); +const achievementController = require("../controllers/achievementController"); // GET unverified achievements by type (achievements which are pending to verify fetched by admins only) router.get( "/unendorsed/:type", isAuthenticated, authorizeRole(ROLE_GROUPS.ADMIN), - async (req, res) => { - const { type } = req.params; - - try { - const unverifiedAchievements = await Achievement.find({ - type, - verified: false, - }) - .populate("user_id", "personal_info.name username user_id") - .populate("event_id", "title description "); - - res.json(unverifiedAchievements); - } catch (err) { - console.error(err); - res - .status(500) - .json({ message: "Failed to fetch unverified achievements." }); - } - }, + achievementController.getUnendorsedAchievements ); // PATCH verify achievement by ID (achievements can be verified by admins only) @@ -37,26 +18,7 @@ router.patch( "/verify/:id", isAuthenticated, authorizeRole(ROLE_GROUPS.ADMIN), - async (req, res) => { - const { id } = req.params; - const { verified_by } = req.body; // Assuming you send the verifier's ID in the request body - try { - const achievement = await Achievement.findById(id); - - if (!achievement) { - return res.status(404).json({ message: "Achievement not found." }); - } - - achievement.verified = true; - achievement.verified_by = verified_by; - await achievement.save(); - - res.json({ message: "Achievement verified successfully.", achievement }); - } catch (err) { - console.error(err); - res.status(500).json({ message: "Failed to verify achievement." }); - } - }, + achievementController.verifyAchievement ); // REJECT (delete) achievement by ID @@ -64,87 +26,13 @@ router.post( "/reject/:id", isAuthenticated, authorizeRole(ROLE_GROUPS.ADMIN), - async (req, res) => { - const { id } = req.params; - - try { - const deletedAchievement = await Achievement.findByIdAndDelete(id); - - if (!deletedAchievement) { - return res.status(404).json({ - message: "Achievement not found.", - }); - } - - res.json({ - message: "Achievement rejected and deleted successfully.", - }); - } catch (err) { - console.error("Failed to reject achievement:", err); - res.status(500).json({ - message: "Failed to reject achievement.", - }); - } - }, + achievementController.rejectAchievement ); //add achievement (achievements can be added by all the users (students -> their own ahieve, admins -> council achieve + student achieve)) -router.post("/add", isAuthenticated, async (req, res) => { - try { - const { - title, - description, - category, - type, - level, - date_achieved, - position, - certificate_url, - event_id, - user_id, - } = req.body; - - if (!title || !category || !date_achieved || !user_id) { - return res.status(400).json({ message: "Missing required fields" }); - } - - const achievement = new Achievement({ - achievement_id: uuidv4(), - user_id, - title, - description, - category, - type, - level, - date_achieved, - position, - certificate_url, - event_id: event_id || null, - }); - - await achievement.save(); - - return res - .status(201) - .json({ message: "Achievement saved successfully", achievement }); - } catch (error) { - console.error("Error saving achievement:", error); - return res.status(500).json({ message: "Server error" }); - } -}); +router.post("/add", isAuthenticated, achievementController.addAchievement); //get all user achievements (endorsed + unendorsed) (must be accessible by the user themselves and admins, so all users) -router.get("/:userId", isAuthenticated, async (req, res) => { - const userId = req.params.userId; - try { - const userAchievements = await Achievement.find({ user_id: userId }) - .populate("event_id", "title description") - .populate("verified_by", "personal_info.name username user_id"); - res.json(userAchievements); - } catch (err) { - console.error("Failed to get user Achievements:", err); - res.status(500).json({ message: "Failed to get user Achievements." }); - } -}); +router.get("/:userId", isAuthenticated, achievementController.getUserAchievements); module.exports = router; diff --git a/backend/routes/announcements.js b/backend/routes/announcements.js index c4f5ae9f..b8aab135 100644 --- a/backend/routes/announcements.js +++ b/backend/routes/announcements.js @@ -1,165 +1,15 @@ const express = require("express"); const router = express.Router(); -const mongoose = require("mongoose"); -const { - Announcement, - Event, - OrganizationalUnit, - Position, -} = require("../models/schema"); const isAuthenticated = require("../middlewares/isAuthenticated"); +const announcementController = require("../controllers/announcementController"); -const findTargetId = async (type, identifier) => { - let target = null; - - const isObjectId = mongoose.Types.ObjectId.isValid(identifier); - const objectId = isObjectId ? new mongoose.Types.ObjectId(identifier) : null; - if (type === "Event") { - target = await Event.findOne({ - $or: [ - ...(objectId ? [{ _id: identifier }] : []), - { event_id: identifier }, - ], - }); - } else if (type === "Organizational_Unit") { - target = await OrganizationalUnit.findOne({ - $or: [ - ...(objectId ? [{ _id: identifier }] : []), - { unit_id: identifier }, - ], // FIXED - }); - } else if (type === "Position") { - target = await Position.findOne({ - $or: [ - ...(objectId ? [{ _id: identifier }] : []), - { position_id: identifier }, - ], // FIXED - }); - } - - return target ? target._id : null; -}; - -router.post("/", isAuthenticated, async (req, res) => { - try { - const { - title, - content, - type = "General", - isPinned, - targetIdentifier, - } = req.body; - let targetId = null; - - if (type != "General" && targetIdentifier) { - targetId = await findTargetId(type, targetIdentifier); - if (!targetId) { - return res - .status(404) - .json({ error: `No ${type} found with that identifier` }); - } - } - - const newAnnouncement = new Announcement({ - author: req.user._id, - content, - is_pinned: isPinned || false, - title, - type: type || "General", - target_id: targetId, - }); - await newAnnouncement.save(); - res.status(201).json(newAnnouncement); - } catch (error) { - console.error("Error creating announcement:", error); - res.status(500).json({ error: "Failed to create announcement" }); - } -}); +router.post("/", isAuthenticated, announcementController.createAnnouncement); // GET / - list announcements with filtering, search, pagination and sorting -router.get("/", async (req, res) => { - try { - const { - type, - author, - isPinned, - search, - page = 1, - limit = 10, - sortBy = "createdAt", - sortOrder = "desc", - } = req.query; - - const filter = {}; - - if (type && type != "All") filter.type = type; - if (author) filter.author = author; - if (typeof isPinned !== "undefined") { - // accept true/false or 1/0 - const val = `${isPinned}`.toLowerCase(); - filter.is_pinned = val === "true" || val === "1"; - } - - if (search) { - const regex = new RegExp(search, "i"); - filter.$or = [{ title: regex }, { content: regex }]; - } - - const pageNum = Math.max(parseInt(page, 10) || 1, 1); - const limNum = Math.max(parseInt(limit, 10) || 10, 1); - - const sortDirection = sortOrder === "asc" ? 1 : -1; - const sort = { [sortBy]: sortDirection }; - - const total = await Announcement.countDocuments(filter); - const query = Announcement.find(filter) - .sort(sort) - .skip((pageNum - 1) * limNum) - .limit(limNum) - .populate("author", "username personal_info.email personal_info.name"); - - if (filter.type && filter.type !== "General") { - query.populate("target_id"); - } - const announcements = await query; - - res.json({ - total, - page: pageNum, - limit: limNum, - totalPages: Math.ceil(total / limNum) || 0, - announcements, - }); - // console.log(announcements); - } catch (error) { - console.error("Error fetching announcements:", error); - res.status(500).json({ error: "Failed to fetch announcements" }); - } -}); +router.get("/", announcementController.getAnnouncements); // GET /:id - fetch a single announcement by id -router.get("/:id", async (req, res) => { - try { - const { id } = req.params; - - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ error: "Invalid announcement id" }); - } - - const announcement = await Announcement.findById(id) - .populate("author", "username personal_info.email personal_info.name") - .populate("target_id"); - - if (!announcement) { - return res.status(404).json({ error: "Announcement not found" }); - } - - res.json(announcement); - } catch (error) { - console.error("Error fetching announcement by id:", error); - res.status(500).json({ error: "Failed to fetch announcement" }); - } -}); +router.get("/:id", announcementController.getAnnouncementById); // PUT /:id - update an announcement by id router.put( @@ -167,113 +17,10 @@ router.put( isAuthenticated, // allow authors, admins and gensec/president roles to update announcements // authorizeRole(["admin", "gen_sec", "president", "gensec"]), - async (req, res) => { - try { - const { id } = req.params; - - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ error: "Invalid announcement id" }); - } - - const announcement = await Announcement.findById(id); - if (!announcement) { - return res.status(404).json({ error: "Announcement not found" }); - } - - // Only the author - const isAuthor = - announcement.author && - announcement.author.toString() === req.user._id.toString(); - if (!isAuthor) { - return res - .status(403) - .json({ error: "Forbidden: cannot edit this announcement" }); - } - - const { title, content, type, targetIdentifier, isPinned } = req.body; - if (title !== undefined) announcement.title = title; - if (content !== undefined) announcement.content = content; - if (isPinned !== undefined) announcement.is_pinned = Boolean(isPinned); - - if (type || targetIdentifier) { - const newType = type || announcement.type; - - const newIdentifier = - targetIdentifier || - (announcement.target_id ? announcement.target_id.toString() : null); - - if (newType === "General") { - announcement.type = "General"; - announcement.target_id = null; - } else { - if (!newIdentifier) { - return res.status(400).json({ - error: - "targetIdentifier is required when setting a non-General type", - }); - } - const newTargetId = await findTargetId(newType, newIdentifier); - if (!newTargetId) { - return res.status(404).json({ - error: `Target ${newType} not found with identifier ${newIdentifier}`, - }); - } - announcement.target_id = newTargetId; - announcement.type = newType; - } - } - - announcement.updatedAt = Date.now(); - await announcement.save(); - - const populated = await announcement.populate([ - { - path: "author", - select: "username personal_info.email personal_info.name", - }, - ]); - if (announcement.type !== "General") { - await populated.populate("target_id"); - } - res.json(populated); - } catch (error) { - console.error("Error updating announcement:", error); - res.status(500).json({ error: "Failed to update announcement" }); - } - }, + announcementController.updateAnnouncement ); // DELETE /:id - delete an announcement by id -router.delete("/:id", isAuthenticated, async (req, res) => { - try { - const { id } = req.params; - - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ error: "Invalid announcement id" }); - } - - const announcement = await Announcement.findById(id); - if (!announcement) { - return res.status(404).json({ error: "Announcement not found" }); - } - - // Only the author - const isAuthor = - announcement.author && - announcement.author.toString() === req.user._id.toString(); - if (!isAuthor) { - return res - .status(403) - .json({ error: "Forbidden: cannot delete this announcement" }); - } - - await Announcement.deleteOne({ _id: id }); - - res.json({ message: "Announcement deleted", id }); - } catch (error) { - console.error("Error deleting announcement:", error); - res.status(500).json({ error: "Failed to delete announcement" }); - } -}); +router.delete("/:id", isAuthenticated, announcementController.deleteAnnouncement); module.exports = router; diff --git a/backend/routes/auth.js b/backend/routes/auth.js index c2ee6f7b..394d4be3 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,13 +1,9 @@ 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 rateLimit = require("express-rate-limit"); -var nodemailer = require("nodemailer"); -const { User } = require("../models/schema"); -const isAuthenticated= require("../middlewares/isAuthenticated"); +const isAuthenticated = require("../middlewares/isAuthenticated"); +const authController = require("../controllers/authController"); //rate limiter - for password reset try const forgotPasswordLimiter = rateLimit({ @@ -17,188 +13,37 @@ 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, authController.fetchAuth); // 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.", - }); - } - 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 existingUser = await User.findOne({ username: email }); - if (existingUser) { - return res.status(400).json({ message: "User already exists." }); - } +router.post("/login", passport.authenticate("local"), authController.login); - const newUser = await User.register( - new User({ - user_id: ID, - role: "STUDENT", - strategy: "local", - username: email, - personal_info: { - name: name, - email: email, - }, - onboardingComplete: false, - }), - password, - ); - - 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 }); - }); - } catch (error) { - console.error("Registration error:", error); - return res.status(500).json({ message: "Internal server error" }); - } -}); +router.post("/register", authController.register); // Google OAuth Authentication router.get( "/google", - passport.authenticate("google", { scope: ["profile", "email"] }), + 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`); - } - }, + authController.googleCallback ); -router.post("/logout", (req, res, next) => { - req.logout(function (err) { - if (err) { - return next(err); - } - res.send("Logout Successful"); - }); -}); +router.post("/logout", authController.logout); //routes for forgot-password -router.post("/forgot-password", forgotPasswordLimiter, async (req, res) => { - try { - const { email } = req.body; - const user = await User.findOne({ username: email }); - if (!user) { - return res.status(404).json({ message: "User not found" }); - } - if (user.strategy === "google") { - return res.status(400).json({ - message: - "This email is linked with Google Login. Please use 'Sign in with Google' instead.", - }); - } - const secret = user._id + process.env.JWT_SECRET_TOKEN; - const token = jwt.sign({ email: email, id: user._id }, secret, { - expiresIn: "10m", - }); - const link = `${process.env.FRONTEND_URL}/reset-password/${user._id}/${token}`; - var transporter = nodemailer.createTransport({ - service: "gmail", - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASS, - }, - }); - var mailOptions = { - from: process.env.EMAIL_USER, - to: email, - subject: "Password-Reset Request", - text: `To reset your password, click here: ${link}`, - }; - transporter.sendMail(mailOptions, function (error, info) { - if (error) { - console.log(error); - return res.status(500).json({ message: "Error sending email" }); - } else { - console.log("Email sent:", info.response); - return res - .status(200) - .json({ message: "Password reset link sent to your email" }); - } - }); - console.log(link); - } catch (error) { - console.log(error); - return res.status(500).json({ message: "Internal server error" }); - } -}); +router.post( + "/forgot-password", + forgotPasswordLimiter, + authController.forgotPassword +); //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); - return res.status(200).json({ message: "Token verified successfully" }); - } catch (error) { - console.log(error); - return res.status(400).json({ message: "Invalid or expired token" }); - } -}); +router.get("/reset-password/:id/:token", authController.verifyResetToken); -router.post("/reset-password/:id/:token", async (req, res) => { - const { id, token } = req.params; - const { password } = req.body; - 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); - user.setPassword(password, async (error) => { - if (error) { - return res.status(500).json({ message: "Error resetting password" }); - } - await user.save(); - return res - .status(200) - .json({ message: "Password has been reset successfully" }); - }); - } catch (error) { - console.log(error); - return res.status(400).json({ message: "Invalid or expired token" }); - } -}); +router.post("/reset-password/:id/:token", authController.resetPassword); module.exports = router; diff --git a/backend/routes/events.js b/backend/routes/events.js index 4bf5dd92..ed776f5a 100644 --- a/backend/routes/events.js +++ b/backend/routes/events.js @@ -1,145 +1,27 @@ const express = require("express"); const router = express.Router(); -const { Event, User, OrganizationalUnit } = require("../models/schema"); -const { v4: uuidv4 } = require("uuid"); const isAuthenticated = require("../middlewares/isAuthenticated"); const isEventContact = require("../middlewares/isEventContact"); const authorizeRole = require("../middlewares/authorizeRole"); const { ROLE_GROUPS, ROLES } = require("../utils/roles"); -const eventsController = require("../controllers/eventControllers"); +const eventController = require("../controllers/eventController"); -router.get("/latest", eventsController.getLatestEvents); +router.get("/latest", eventController.getLatestEvents); // Create a new event (new events can be created by admins only) router.post( "/create", isAuthenticated, authorizeRole(ROLE_GROUPS.ADMIN), - async (req, res) => { - try { - const { - title, - description, - category, - type, - organizing_unit_id, - organizers, - schedule, - registration, - budget, - } = req.body; - - // Validate organizing unit - const orgUnit = await OrganizationalUnit.findById(organizing_unit_id); - if (!orgUnit) { - return res - .status(400) - .json({ message: "Invalid organizational unit." }); - } - - // Optional: Validate organizer IDs - if (organizers && organizers.length > 0) { - const validUsers = await User.find({ _id: { $in: organizers } }); - if (validUsers.length !== organizers.length) { - return res - .status(400) - .json({ message: "One or more organizers are invalid." }); - } - } - - const newEvent = new Event({ - event_id: uuidv4(), - title, - description, - category, - type, - organizing_unit_id, - organizers, - schedule, - registration, - budget, - }); - - await newEvent.save(); - res - .status(201) - .json({ message: "Event created successfully", event: newEvent }); - console.log("Event created:", newEvent); - } catch (err) { - console.error(err); - res.status(500).json({ message: "Server error while creating event." }); - } - }, + eventController.createEvent ); // GET all events (for all users: logged in or not logged in) -router.get("/events", async (req, res) => { - try { - const events = await Event.find().populate("organizing_unit_id", "name"); - res.json(events); - } catch (err) { - console.error(err); - res.status(500).json({ message: "Error fetching events." }); - } -}); - -router.get("/units", isAuthenticated, async (req, res) => { - try { - const role = (req.user && req.user.role) || ""; - const userEmail = String( - (req.user && - (req.user.username || - (req.user.personal_info && req.user.personal_info.email))) || - "", - ) - .trim() - .toLowerCase(); - - const categoryForRole = { - [ROLES.GENSEC_SCITECH]: "scitech", - [ROLES.GENSEC_ACADEMIC]: "academic", - [ROLES.GENSEC_CULTURAL]: "cultural", - [ROLES.GENSEC_SPORTS]: "sports", - }; +router.get("/events", eventController.getAllEvents); - let units = []; +router.get("/units", isAuthenticated, eventController.getOrganizationalUnits); - if (role === ROLES.PRESIDENT) { - // President sees all units - units = await OrganizationalUnit.find(); - } else if (categoryForRole[role]) { - // GenSecs see units by category - units = await OrganizationalUnit.find({ - category: categoryForRole[role], - }); - } else if (role === ROLES.CLUB_COORDINATOR) { - // Club Coordinator sees only their own unit (matched by contact email) - const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const coordUnit = await OrganizationalUnit.findOne({ - "contact_info.email": new RegExp(`^${escapeRegex(userEmail)}$`, "i"), - }); - units = coordUnit ? [coordUnit] : []; - } else { - // Default: return all units (keeps previous behavior for non-admins if needed) - units = await OrganizationalUnit.find(); - } - - res.json(units); - } catch (err) { - console.error(err); - res.status(500).json({ message: "Error fetching organizational units." }); - } -}); - -router.get("/users", isAuthenticated, async (req, res) => { - try { - const users = await User.find({ role: "STUDENT" }); - res.json(users); - } catch (err) { - console.error(err); - res.status(500).json({ message: "Error fetching users." }); - } -}); +router.get("/users", isAuthenticated, eventController.getUsers); /** * NEW: Endpoint used by frontend to decide conditional rendering. @@ -147,286 +29,34 @@ router.get("/users", isAuthenticated, async (req, res) => { * * Placed before the "/:id" route to avoid route collision. */ -router.get("/:eventId/is-contact", isAuthenticated, async (req, res) => { - try { - const { eventId } = req.params; - const event = await Event.findById(eventId).populate("organizing_unit_id"); - if (!event) return res.status(404).json({ message: "Event not found" }); - - // Defensive checks instead of optional chaining for lint/parse compatibility - const unit = event.organizing_unit_id; - const unitEmail = String( - (unit && unit.contact_info && unit.contact_info.email) || "", - ) - .trim() - .toLowerCase(); - - const userEmail = String( - (req.user && - (req.user.username || - (req.user.personal_info && req.user.personal_info.email))) || - "", - ) - .trim() - .toLowerCase(); - - const isContact = !!(unitEmail && userEmail && unitEmail === userEmail); - return res.json({ isContact }); - } catch (err) { - console.error("is-contact error:", err); - return res.status(500).json({ message: "Server error" }); - } -}); +router.get("/:eventId/is-contact", isAuthenticated, eventController.isEventContact); // Register student for an event router.post( "/:eventId/register", isAuthenticated, authorizeRole(ROLES.STUDENT), - async (req, res) => { - try { - const { eventId } = req.params; - const userId = req.user._id; - - const event = await Event.findById(eventId); - if (!event) { - return res.status(404).json({ message: "Event not found." }); - } - - if (event.status === "completed") { - return res - .status(400) - .json({ message: "Registration is closed for this event." }); - } - - if (event.participants.some((p) => p.equals(userId))) { - return res - .status(409) - .json({ message: "You are already registered for this event." }); - } - - if (event.registration && event.registration.required) { - const now = new Date(); - - if ( - event.registration.start && - now < new Date(event.registration.start) - ) { - return res - .status(400) - .json({ message: "Registration has not started yet." }); - } - - if (event.registration.end && now > new Date(event.registration.end)) { - return res.status(400).json({ message: "Registration has ended." }); - } - - - const maxParticipants = event.registration.max_participants; - if (maxParticipants) { - const updatedEvent = await Event.findOneAndUpdate( - { - _id: eventId, - $expr: { $lt: [{ $size: "$participants" }, maxParticipants] }, - }, - { $addToSet: { participants: userId } }, - { new: true }, - ); - - if (!updatedEvent) { - return res.status(400).json({ message: "Registration is full." }); - } - - return res.status(200).json({ - message: "Successfully registered for the event.", - event: updatedEvent, - }); - } - } - - const updatedEvent = await Event.findByIdAndUpdate( - eventId, - { $addToSet: { participants: userId } }, - { new: true }, - ); - - return res.status(200).json({ - message: "Successfully registered for the event.", - event: updatedEvent, - }); - } catch (error) { - if (error?.name === "CastError") { - return res.status(400).json({ message: "Invalid event ID format." }); - } - console.error("Event registration error:", error); - return res - .status(500) - .json({ message: "Server error during registration." }); - } - }, + eventController.registerForEvent ); // GET event by ID -router.get("/:id", async (req, res) => { - try { - const event = await Event.findById(req.params.id) - .populate("organizing_unit_id", "name") - .populate("organizers", "personal_info.name"); - res.json(event); - } catch (err) { - console.error(err); - res.status(500).json({ message: "Error fetching event." }); - } -}); - -router.get("/by-role/:userRole", isAuthenticated, async (req, res) => { - const userRole = req.params.userRole; - try { - let query = {}; - - // Build the query based on the user's role - switch (userRole) { - case "STUDENT": - query = { status: { $in: ["planned", "ongoing"] } }; - break; - - case "CLUB_COORDINATOR": { - const username = req.query.username; - if (!username) { - return res - .status(400) - .json({ message: "Username is required for Club Coordinator." }); - } - // 1. Find the organizational unit where the contact email matches the username - const orgUnit = await OrganizationalUnit.findOne({ - "contact_info.email": username, - }); - - if (!orgUnit) { - return res.status(404).json({ - message: "No organizational unit found for this coordinator.", - }); - } - // 2. Set the query to filter events by the unit's _id - query = { organizing_unit_id: orgUnit._id }; - break; - } - case "GENSEC_SCITECH": - query = { category: "technical" }; - break; - - case "GENSEC_ACADEMIC": - query = { category: "academic" }; - break; - - case "GENSEC_CULTURAL": - query = { category: "cultural" }; - break; - - case "GENSEC_SPORTS": - query = { category: "sports" }; - break; - - case "PRESIDENT": - query = {}; - break; +router.get("/:id", eventController.getEventById); - default: - query = { status: { $in: ["planned", "ongoing"] } }; - break; - } - - const events = await Event.find(query) - .sort({ "schedule.start": 1 }) - .populate("organizing_unit_id") - .populate("organizers") - .populate("participants") - .populate("winners.user") - .populate("room_requests.reviewed_by"); - - res.status(200).json(events); - } catch (error) { - console.error("Error fetching events:", error); - res.status(500).json({ message: "Server error while fetching events" }); - } -}); +router.get("/by-role/:userRole", isAuthenticated, eventController.getEventsByRole); //room request router.post( "/:eventId/room-requests", isAuthenticated, authorizeRole([...ROLE_GROUPS.GENSECS, ...ROLE_GROUPS.COORDINATORS]), - async (req, res) => { - try { - const { eventId } = req.params; - const { date, time, room, description } = req.body; - if (!date || !time || !room) { - return res - .status(400) - .json({ message: "Date, time, and room are required fields." }); - } - const event = await Event.findById(eventId); - - if (!event) { - return res.status(404).json({ message: "Event not found." }); - } - const newRoomRequest = { - date, - time, - room, - description: description || "", - }; - - event.room_requests.push(newRoomRequest); - const updatedEvent = await event.save(); - res.status(201).json(updatedEvent); - } catch (error) { - console.error("Error adding room request:", error); - if (error.name === "CastError") { - return res.status(400).json({ message: "Invalid event ID format." }); - } - res - .status(500) - .json({ message: "Server error while adding room request." }); - } - }, + eventController.addRoomRequest ); router.patch( "/room-requests/:requestId/status", isAuthenticated, authorizeRole("PRESIDENT"), - async (req, res) => { - const { requestId } = req.params; - const { status, reviewed_by } = req.body; - if (!status || !["Approved", "Rejected"].includes(status)) { - return res.status(400).json({ - message: 'A valid status ("Approved" or "Rejected") is required.', - }); - } - try { - const event = await Event.findOne({ "room_requests._id": requestId }); - if (!event) { - return res - .status(404) - .json({ message: "Request or associated event not found." }); - } - const request = event.room_requests.id(requestId); - if (request) { - request.status = status; - request.requested_at = new Date(); - request.reviewed_by = reviewed_by; - } - - const updatedEvent = await event.save(); - res.status(200).json(updatedEvent); - } catch (error) { - console.error(`Error updating request status to ${status}:`, error); - res - .status(500) - .json({ message: "Server error while updating request status." }); - } - }, + eventController.updateRoomRequestStatus ); /** @@ -435,62 +65,14 @@ router.patch( */ // Update an event (only unit contact) -router.put("/:eventId", isAuthenticated, isEventContact, async (req, res) => { - try { - const { eventId } = req.params; - const updates = req.body; - - // 🔍 DEBUG LOGS - START - console.log("\n=== 📝 UPDATE EVENT DEBUG ==="); - console.log("Event ID:", eventId); - console.log( - "Updates received (full body):", - JSON.stringify(updates, null, 2), - ); - console.log("Number of fields to update:", Object.keys(updates).length); - console.log("Fields being updated:", Object.keys(updates)); - console.log("========================\n"); - - // Fetch the event BEFORE update to compare - const eventBefore = await Event.findById(eventId); - console.log("Event BEFORE update:", JSON.stringify(eventBefore, null, 2)); - - const event = await Event.findByIdAndUpdate(eventId, updates, { - new: true, - runValidators: true, // Added this to ensure validation runs - }); - - console.log("\n=== ✅ UPDATE RESULT ==="); - console.log("Event AFTER update:", JSON.stringify(event, null, 2)); - console.log("Update successful:", !!event); - console.log("========================\n"); - - if (!event) return res.status(404).json({ message: "Event not found" }); - return res.json({ message: "Event updated", event }); - } catch (err) { - console.error("❌ update event error:", err); - return res - .status(500) - .json({ message: "Server error", error: err.message }); - } -}); +router.put("/:eventId", isAuthenticated, isEventContact, eventController.updateEvent); // Delete an event (only unit contact) router.delete( "/:eventId", isAuthenticated, isEventContact, - async (req, res) => { - try { - const { eventId } = req.params; - const deleted = await Event.findByIdAndDelete(eventId); - if (!deleted) return res.status(404).json({ message: "Event not found" }); - return res.json({ message: "Event deleted" }); - } catch (err) { - console.error("delete event error:", err); - return res.status(500).json({ message: "Server error" }); - } - }, + eventController.deleteEvent ); module.exports = router; diff --git a/backend/routes/feedbackRoutes.js b/backend/routes/feedbackRoutes.js index d3e52386..e13cebae 100644 --- a/backend/routes/feedbackRoutes.js +++ b/backend/routes/feedbackRoutes.js @@ -1,229 +1,25 @@ const express = require("express"); const router = express.Router(); const isAuthenticated = require("../middlewares/isAuthenticated"); -const { - User, - Feedback, - Event, - Position, - OrganizationalUnit, -} = require("./../models/schema"); -const { v4: uuidv4 } = require("uuid"); const authorizeRole = require("../middlewares/authorizeRole"); const { ROLE_GROUPS } = require("../utils/roles"); +const feedbackController = require("../controllers/feedbackController"); -router.post("/add",isAuthenticated, async (req, res) => { - try { - const { - type, - target_type, - target_id, - feedback_by, - rating, - comments, - is_anonymous, - } = req.body; +router.post("/add", isAuthenticated, feedbackController.addFeedback); - if (!type || !target_type || !target_id || !feedback_by) { - return res.status(400).json({ message: "Missing required fields" }); - } +router.get("/get-targetid", isAuthenticated, feedbackController.getTargetIds); - const targetModels={ - User, - Event, - "Club/Organization": OrganizationalUnit, - POR: Position, - }; - - const TargetModel=targetModels[target_type]; - - if(!TargetModel){ - return res.status(400).json({message:"Invalid target type"}); - } - - const feedback = new Feedback({ - feedback_id: uuidv4(), - type, - target_type, - target_id, - feedback_by, - rating, - comments, - is_anonymous: is_anonymous === "ture" || is_anonymous === true, - }); - - await feedback.save(); - console.log("Feedback added successfully:", feedback); - res - .status(201) - .json({ message: "Feedback submitted successfully", feedback }); - } catch (error) { - console.error("Error adding feedback:", error); - res.status(500).json({ message: "Failed to submit feedback" }); - } -}); - -router.get("/get-targetid",isAuthenticated, async (req, res) => { - try { - 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({}) - .populate("unit_id", "name") - .select("_id title unit_id"); - - const formattedUsers = users.map((user) => ({ - _id: user._id, - name: user.personal_info.name, - user_id: user.user_id, - })); - - const formattedPositions = positions.map((position) => ({ - _id: position._id, - title: position.title, - unit: position.unit_id ? position.unit_id.name : "N/A", - })); - - res.json({ - users: formattedUsers, - events, - organizational_units, - positions: formattedPositions, - }); - } catch (error) { - console.error("Error fetching target ID:", error); - res.status(500).json({ message: "Failed to fetch target IDs" }); - } -}); - -router.get("/view-feedback", async (req, res) => { - try { - const feedback = await Feedback.find() - .populate("feedback_by", "personal_info.name username") - .sort({ created_at: -1 }); - - const populatedFeedback = await Promise.all( - feedback.map(async (fb) => { - let targetData = null; - - switch (fb.target_type) { - case "User": - targetData = await User.findById(fb.target_id).select( - "personal_info.name user_id username", - ); - break; - - case "Event": { - const event = await Event.findById(fb.target_id).select( - "title organizing_unit_id", - ); - if (event) { - const orgUnit = await OrganizationalUnit.findById( - event.organizing_unit_id, - ).select("name"); - targetData = { - title: event.title, - organizing_unit: orgUnit ? orgUnit.name : "N/A", - }; - } - break; - } - - case "Club/Organization": { - const org = await OrganizationalUnit.findById(fb.target_id).select( - "name parent_unit_id", - ); - if (org) { - const parent = await OrganizationalUnit.findById( - org.parent_unit_id, - ).select("name"); - targetData = { - name: org.name, - parent: parent ? parent.name : "N/A", - }; - } - break; - } - - case "POR": { - const position = await Position.findById(fb.target_id).select( - "title unit_id", - ); - if (position) { - const unit = await OrganizationalUnit.findById( - position.unit_id, - ).select("name"); - targetData = { - title: position.title, - unit: unit ? unit.name : "N/A", - }; - } - break; - } - - default: - targetData = null; - break; - } - const fbObj = fb.toObject(); - fbObj.target_data = targetData; - return fbObj; - }), - ); - res.status(200).json(populatedFeedback); - } catch (error) { - console.error("Error viewing feedback:", error); - res.status(500).json({ message: "Failed to retrieve feedback" }); - } -}); +router.get("/view-feedback", feedbackController.viewFeedback); // 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" }); - } - - if (feedback.is_resolved) { - return res.status(400).json({ error: "Feedback is already resolved." }); - } - - feedback.is_resolved = true; - feedback.resolved_at = new Date(); - feedback.actions_taken = actions_taken; - feedback.resolved_by = resolved_by; - - 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" }); - } -}); +router.put( + "/mark-resolved/:id", + isAuthenticated, + authorizeRole(ROLE_GROUPS.ADMIN), + feedbackController.markFeedbackResolved +); //get all user given feedbacks -router.get("/:userId",isAuthenticated, async (req, res) => { - const userId = req.params.userId; - try { - const userFeedbacks = await Feedback.find({ feedback_by: userId }).populate( - "resolved_by", - "personal_info.name username user_id", - ); - res.json(userFeedbacks); - } catch (err) { - console.error("Failed to get user Feedbacks:", err); - res.status(500).json({ message: "Failed to get user Feedbacks." }); - } -}); +router.get("/:userId", isAuthenticated, feedbackController.getUserFeedbacks); + module.exports = router; diff --git a/backend/routes/onboarding.js b/backend/routes/onboarding.js index dca690d2..298bdaef 100644 --- a/backend/routes/onboarding.js +++ b/backend/routes/onboarding.js @@ -1,39 +1,9 @@ const express = require("express"); const router = express.Router(); -const { User } = require("../models/schema"); const isAuthenticated = require("../middlewares/isAuthenticated"); +const onboardingController = require("../controllers/onboardingController"); // 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; - - try { - 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 || "", - }), - academic_info: { - program: Program.trim(), - branch: discipline, - batch_year: add_year, - }, - role: req.user.role, // required field - strategy: req.user.strategy, // required field - }, - { new: true, runValidators: true }, - ); - - 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 }); - } -}); +router.post("/", isAuthenticated, onboardingController.completeOnboarding); module.exports = router; diff --git a/backend/routes/orgUnit.js b/backend/routes/orgUnit.js index 2c71597b..fe24e1c0 100644 --- a/backend/routes/orgUnit.js +++ b/backend/routes/orgUnit.js @@ -1,209 +1,21 @@ // routes/club.js const express = require("express"); const router = express.Router(); -const mongoose = require("mongoose"); -const { v4: uuidv4 } = require("uuid"); -const { - OrganizationalUnit, - Event, - Position, - PositionHolder, - Achievement, - Feedback, - User, -} = require("../models/schema"); const isAuthenticated = require("../middlewares/isAuthenticated"); const authorizeRole = require("../middlewares/authorizeRole"); const { ROLE_GROUPS } = require("../utils/roles"); +const orgUnitController = require("../controllers/orgUnitController"); -router.get("/clubData/:email", isAuthenticated, async (req, res) => { - try { - const email = req.params.email; - if (!email || email.trim() === "") { - return res.status(400).json({ error: "Missing email" }); - } - - const unit = await OrganizationalUnit.findOne({ - "contact_info.email": email, - }).populate("parent_unit_id"); - if (!unit) { - return res.status(404).json({ error: "Organizational Unit not found" }); - } - - const events = await Event.find({ organizing_unit_id: unit._id }) - .populate("participants") - .populate("winners.user"); - const eventIds = events.map((e) => e._id); - - const achievements = await Achievement.find({ event_id: { $in: eventIds } }) - .populate("user_id") - .populate("event_id") - .populate("verified_by"); - - const positions = await Position.find({ unit_id: unit._id }).populate( - "unit_id", - ); - const positionIds = positions.map((pos) => pos._id); - - const positionHolders = await PositionHolder.find({ - position_id: { $in: positionIds }, - }) - .populate("user_id") - .populate("appointment_details.appointed_by") - .populate({ - path: "position_id", - populate: { path: "unit_id" }, - }); - - const feedbacks = await Feedback.find({ - target_type: "Club/Organization", - target_id: unit._id, - }) - .populate("feedback_by") - .populate("resolved_by"); - - res.json({ - unit, - events, - positions, - positionHolders, - achievements, - feedbacks, - }); - } catch (e) { - console.error(e); - res.status(500).json({ error: "Server error" }); - } -}); +router.get("/clubData/:email", isAuthenticated, orgUnitController.getClubData); // Fetches all units, or filters by category if provided in the query. -router.get("/organizational-units", isAuthenticated, async (req, res) => { - try { - const { category, type } = req.query; - - const filter = {}; - - if (category) { - filter.category = category; - } - if (type) { - filter.type = type; - } - const units = await OrganizationalUnit.find(filter).sort({ - name: 1, - }); - res.status(200).json(units); - } catch (error) { - res.status(500).json({ - message: "Server error while fetching organizational units", - error, - }); - } -}); +router.get("/organizational-units", isAuthenticated, orgUnitController.getAllOrganizationalUnits); // Create a new organizational unit router.post( "/create", isAuthenticated, authorizeRole([...ROLE_GROUPS.GENSECS, "PRESIDENT"]), - async (req, res) => { - const session = await mongoose.startSession(); - session.startTransaction(); - - try { - const { - name, - type, - description, - parent_unit_id, - hierarchy_level, - category, - is_active, - contact_info, - budget_info, - } = req.body; - - if ( - !name || - !type || - !category || - !hierarchy_level || - !contact_info.email - ) { - return res.status(400).json({ - message: - "Validation failed: name, type, category, and hierarchy_level are required.", - }); - } - - const newUnitData = { - unit_id: `org-${uuidv4()}`, - name, - type, - description, - parent_unit_id: parent_unit_id || null, - hierarchy_level, - category, - is_active, - contact_info, - budget_info, - }; - - const newUnit = new OrganizationalUnit(newUnitData); - await newUnit.save(session); - - const existingUser = await User.findOne({ - username: contact_info.email, - }).session(session); - if (existingUser) { - // If user exists, we must abort the transaction to avoid an inconsistent state - await session.abortTransaction(); - session.endSession(); - return res.status(409).json({ - message: - "A user with this email already exists. The organizational unit was not created.", - }); - } - - const newUser = new User({ - username: contact_info.email, - role: "CLUB_COORDINATOR", // Assign the specified role - strategy: "google", // Set strategy to google - onboardingComplete: true, // Set onboarding to true - personal_info: { - name: name, // Use the organization's name for the user's name - email: contact_info.email, - }, - }); - await newUser.save({ session }); // Pass the session to the save command - - // --- 5. If both operations were successful, commit the transaction --- - await session.commitTransaction(); - session.endSession(); - - res.status(201).json(newUnit); - } catch (error) { - await session.abortTransaction(); - session.endSession(); - console.error("Error creating organizational unit and user: ", error); - - if (error.name === "ValidationError" || error.name === "CastError") { - return res - .status(400) - .json({ message: `Invalid data provided: ${error.message}` }); - } - - if (error.code === 11000) { - return res.status(409).json({ - message: - "Conflict: An organizational unit with this name or ID already exists.", - }); - } - - res - .status(500) - .json({ message: "Server error while creating organizational unit." }); - } - }, + orgUnitController.createOrganizationalUnit ); module.exports = router; diff --git a/backend/routes/positionRoutes.js b/backend/routes/positionRoutes.js index d25e32f4..0c83853b 100644 --- a/backend/routes/positionRoutes.js +++ b/backend/routes/positionRoutes.js @@ -1,176 +1,30 @@ const express = require("express"); const router = express.Router(); -const { Position, PositionHolder } = require("../models/schema"); -const { v4: uuidv4 } = require("uuid"); const isAuthenticated = require("../middlewares/isAuthenticated"); +const positionController = require("../controllers/positionController"); // POST for adding a new position -router.post("/add-position", isAuthenticated, async (req, res) => { - try { - const { - title, - unit_id, - position_type, - responsibilities, - description, - position_count, - requirements, - } = req.body; - - // Validation - if (!title || !unit_id || !position_type) { - return res.status(400).json({ error: "Missing required fields" }); - } - - const newPosition = new Position({ - position_id: uuidv4(), - title, - unit_id, - position_type, - responsibilities, - description, - position_count, - requirements, - }); - - await newPosition.save(); - res.status(201).json(newPosition); - } catch (error) { - console.error("Error adding position:", error); - res.status(500).json({ error: "Server error" }); - } -}); +router.post("/add-position", isAuthenticated, positionController.addPosition); // for getting all the position -router.get("/get-all", isAuthenticated, async (req, res) => { - try { - const positions = await Position.find().populate( - "unit_id", - "name category contact_info.email", - ); - res.json(positions); - } catch (err) { - console.error(err); - res.status(500).json({ message: "Error fetching positions." }); - } -}); +router.get("/get-all", isAuthenticated, positionController.getAllPositions); //add position holder -router.post("/add-position-holder", isAuthenticated, async (req, res) => { - try { - const { - user_id, - position_id, - tenure_year, - appointment_details, - performance_metrics, - status, - } = req.body; - if (!user_id || !position_id || !tenure_year) { - return res.status(400).json({ message: "Missing required fields." }); - } - - // Step 1: Fetch the position to get the position_count - const position = await Position.findById(position_id); - if (!position) { - return res.status(404).json({ message: "Position not found." }); - } - - // Step 2: Count how many holders already exist for this position and tenure - const existingCount = await PositionHolder.countDocuments({ - position_id, - tenure_year, - }); - - if (existingCount >= position.position_count) { - return res.status(400).json({ - message: `Maximum position holders (${position.position_count}) already appointed for the year ${tenure_year}.`, - }); - } - - console.log(req.body); - const newPH = new PositionHolder({ - por_id: uuidv4(), - user_id, - position_id, - tenure_year, - appointment_details: - appointment_details && - (appointment_details.appointed_by || - appointment_details.appointment_date) - ? appointment_details - : undefined, - performance_metrics: { - events_organized: - performance_metrics && - performance_metrics.events_organized !== undefined - ? performance_metrics.events_organized - : 0, - budget_utilized: - performance_metrics && - performance_metrics.budget_utilized !== undefined - ? performance_metrics.budget_utilized - : 0, - feedback: - performance_metrics && performance_metrics.feedback - ? performance_metrics.feedback.trim() - : undefined, - }, - status, - }); - - const saved = await newPH.save(); - res.status(201).json(saved); - } catch (err) { - console.error("Error adding position holder:", err); - res.status(500).json({ message: "Internal server error" }); - } -}); +router.post( + "/add-position-holder", + isAuthenticated, + positionController.addPositionHolder +); // Get all position holders -router.get("/get-all-position-holder", isAuthenticated, async (req, res) => { - try { - const positionHolders = await PositionHolder.find() - .populate("user_id", "personal_info.name user_id username") - .populate({ - path: "position_id", - select: "title unit_id position_type", - populate: { - path: "unit_id", - select: "name", - }, - }) - .populate( - "appointment_details.appointed_by", - "personal_info.name username user_id", - ); - - res.json(positionHolders); - } catch (err) { - console.error(err); - res.status(500).json({ message: "Error fetching position holders." }); - } -}); +router.get( + "/get-all-position-holder", + isAuthenticated, + positionController.getAllPositionHolders +); // Get positions by id -router.get("/:userId", isAuthenticated, async (req, res) => { - const userId = req.params.userId; - try { - const positionHolder = await PositionHolder.find({ user_id: userId }) - .populate({ - path: "position_id", - populate: { - path: "unit_id", - model: "Organizational_Unit", - }, - }) - .populate("appointment_details.appointed_by"); - res.json(positionHolder); - } catch (err) { - console.error(err); - res.status(500).json({ message: "Error fetching position holder." }); - } -}); +router.get("/:userId", isAuthenticated, positionController.getPositionHoldersByUserId); module.exports = router; diff --git a/backend/routes/profile.js b/backend/routes/profile.js index db94cae5..a4ff6355 100644 --- a/backend/routes/profile.js +++ b/backend/routes/profile.js @@ -2,198 +2,20 @@ const express = require("express"); 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 streamifier = require("streamifier"); const isAuthenticated = require("../middlewares/isAuthenticated"); -// Cloudinary config -cloudinary.config({ - cloud_name: process.env.CLOUDINARY_CLOUD_NAME, - api_key: process.env.CLOUDINARY_API_KEY, - api_secret: process.env.CLOUDINARY_API_SECRET, -}); +const profileController = require("../controllers/profileController"); 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" }); } - - const user = await User.findOne({ user_id: ID_No }); - if (!user) { return res.status(404).json({ error: "User not found" });} - - if ( - !req.files || - !req.files["image"] || - req.files["image"].length === 0 - ) { - return res.status(400).json({ error: "Image file is required" }); - } - - const file = req.files["image"][0]; - - // Delete old image if exists - if (user.personal_info.cloudinaryUrl) { - await cloudinary.uploader.destroy(user.personal_info.cloudinaryUrl); - } - - // Upload new image using upload_stream - const streamUpload = (fileBuffer) => { - return new Promise((resolve, reject) => { - let stream = cloudinary.uploader.upload_stream( - { folder: "profile-photos" }, - (error, result) => { - if (result) { resolve(result);} - else { reject(error); } - }, - ); - streamifier.createReadStream(fileBuffer).pipe(stream); - }); - }; - - const result = await streamUpload(file.buffer); - - user.personal_info.profilePic = result.secure_url; - user.personal_info.cloudinaryUrl = result.public_id; - await user.save(); - - res.json({ profilePic: user.personal_info.profilePic }); - } catch (err) { - console.error("Upload error:", err); - res.status(500).json({ error: "Upload failed" }); - } - }, + profileController.updateProfilePhoto ); // Delete profile photo (reset to default) -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" }); } - - 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) { - await cloudinary.uploader.destroy(user.personal_info.cloudinaryUrl); - user.personal_info.profilePic = "https://www.gravatar.com/avatar/?d=mp"; // Default photo - user.personal_info.cloudinaryUrl = ""; - await user.save(); - } - - res.json({ profilePic: user.personal_info.profilePic }); - } catch (err) { - res.status(500).json({ error: "Delete failed" }); - } -}); - -// API to Update Student Profile -router.put("/updateStudentProfile",isAuthenticated, async (req, res) => { - try { - const { userId, updatedDetails } = req.body; - console.log("Received userId:", userId); - console.log("Received updatedDetails:", updatedDetails); - - if (!userId || !updatedDetails) { - return res - .status(400) - .json({ success: false, message: "Missing required fields" }); - } - - const user = await User.findOne({ user_id: userId }); // <-- updated from ID_No - - if (!user) { - return res - .status(404) - .json({ success: false, message: "Student not found" }); - } - - // ---------- PERSONAL INFO ---------- - if (updatedDetails.personal_info) { - const { - name, - email, - phone, - gender, - date_of_birth, - profilePic, - 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; } - } - - // ---------- ACADEMIC INFO ---------- - if (updatedDetails.academic_info) { - 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; } - } - - // ---------- 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; } - - // Social Links - if (socialLinks) { - user.contact_info.socialLinks.github = - socialLinks.github != null - ? socialLinks.github - : user.contact_info.socialLinks.github; - - user.contact_info.socialLinks.linkedin = - socialLinks.linkedin != null - ? socialLinks.linkedin - : user.contact_info.socialLinks.linkedin; - - user.contact_info.socialLinks.instagram = - socialLinks.instagram != null - ? socialLinks.instagram - : user.contact_info.socialLinks.instagram; - - user.contact_info.socialLinks.other = - socialLinks.other != null - ? socialLinks.other - : user.contact_info.socialLinks.other; - } - } - - // Update the updated_at timestamp - user.updated_at = new Date(); - - // Save changes - await user.save(); +router.delete("/photo-delete", isAuthenticated, profileController.deleteProfilePhoto); - return res.status(200).json({ - success: true, - message: "Student profile updated successfully", - updatedStudent: user, - }); - } catch (error) { - console.error("Error updating student profile:", error); - return res.status(500).json({ - success: false, - message: "Internal server error", - }); - } -}); +// API to Update Student Profile +router.put("/updateStudentProfile", isAuthenticated, profileController.updateStudentProfile); module.exports = router; diff --git a/backend/routes/skillsRoutes.js b/backend/routes/skillsRoutes.js index 04d1bb11..36577b1c 100644 --- a/backend/routes/skillsRoutes.js +++ b/backend/routes/skillsRoutes.js @@ -1,56 +1,22 @@ 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 authorizeRole = require("../middlewares/authorizeRole"); const { ROLE_GROUPS } = require("../utils/roles"); +const skillController = require("../controllers/skillController"); + // GET unendorsed user skills for a particular skill type router.get( "/user-skills/unendorsed/:type", isAuthenticated, - async (req, res) => { - const skillType = req.params.type; // e.g. "cultural", "sports" - - try { - const skills = await UserSkill.find({ is_endorsed: false }) - .populate({ - path: "skill_id", - match: { type: skillType }, - }) - .populate("user_id", "personal_info.name username user_id") // optionally fetch user info - .populate("position_id", "title"); - - // Filter out null populated skills (i.e., skill type didn't match) - const filtered = skills.filter((us) => us.skill_id !== null); - - res.json(filtered); - } catch (err) { - console.error(err); - res.status(500).json({ message: "Error fetching unendorsed skills." }); - } - }, + skillController.getUnendorsedUserSkills ); router.post( "/user-skills/endorse/:id", isAuthenticated, authorizeRole(ROLE_GROUPS.ADMIN), - async (req, res) => { - const skillId = req.params.id; - try { - const userSkill = await UserSkill.findById(skillId); - if (!userSkill) { - return res.status(404).json({ message: "User skill not found" }); - } - userSkill.is_endorsed = true; - await userSkill.save(); - res.json({ message: "User skill endorsed successfully" }); - } catch (err) { - console.error(err); - res.status(500).json({ message: "Error endorsing user skill" }); - } - }, + skillController.endorseUserSkill ); // REJECT (delete) a user skill @@ -58,67 +24,18 @@ router.post( "/user-skills/reject/:id", isAuthenticated, authorizeRole(ROLE_GROUPS.ADMIN), - async (req, res) => { - const skillId = req.params.id; - - try { - const deletedSkill = await UserSkill.findByIdAndDelete(skillId); - - if (!deletedSkill) { - return res.status(404).json({ - message: "User skill not found", - }); - } - - res.json({ - message: "User skill rejected and deleted successfully", - }); - } catch (err) { - console.error("Error rejecting user skill:", err); - res.status(500).json({ - message: "Error rejecting user skill", - }); - } - }, + skillController.rejectUserSkill ); // GET all unendorsed skills by type -router.get("/unendorsed/:type", isAuthenticated, async (req, res) => { - const skillType = req.params.type; - - try { - const skills = await Skill.find({ type: skillType, is_endorsed: false }); - res.json(skills); - } catch (err) { - console.error(err); - res.status(500).json({ message: "Error fetching unendorsed skills." }); - } -}); +router.get("/unendorsed/:type", isAuthenticated, skillController.getUnendorsedSkills); // POST endorse a skill router.post( "/endorse/:id", isAuthenticated, authorizeRole(ROLE_GROUPS.ADMIN), - async (req, res) => { - const skillId = req.params.id; - - try { - const skill = await Skill.findById(skillId); - - if (!skill) { - return res.status(404).json({ message: "Skill not found" }); - } - - skill.is_endorsed = true; - await skill.save(); - - res.json({ message: "Skill endorsed successfully", skill }); - } catch (err) { - console.error(err); - res.status(500).json({ message: "Failed to endorse skill." }); - } - }, + skillController.endorseSkill ); // REJECT (delete) a skill @@ -126,131 +43,22 @@ router.post( "/reject/:id", isAuthenticated, authorizeRole(ROLE_GROUPS.ADMIN), - async (req, res) => { - const skillId = req.params.id; - - try { - const deletedSkill = await Skill.findByIdAndDelete(skillId); - - if (!deletedSkill) { - return res.status(404).json({ - message: "Skill not found", - }); - } - - res.json({ - message: "Skill rejected and deleted successfully", - }); - } catch (err) { - console.error("Error rejecting skill:", err); - res.status(500).json({ - message: "Failed to reject skill", - }); - } - }, + skillController.rejectSkill ); //get all endorsed skills -router.get("/get-skills", isAuthenticated, async (req, res) => { - try { - const skills = await Skill.find({ is_endorsed: true }); - res.json(skills); - } catch (err) { - console.error(err); - res.status(500).json({ message: "Failed to get endorsed skills." }); - } -}); +router.get("/get-skills", isAuthenticated, skillController.getAllSkills); //get all user skills (endorsed + unendorsed) -router.get("/user-skills/:userId", isAuthenticated, async (req, res) => { - const userId = req.params.userId; - try { - const userSkills = await UserSkill.find({ user_id: userId }) - .populate("skill_id") - .populate({ - path: "position_id", - populate: { - path: "unit_id", - select: "name", - }, - }); - res.json(userSkills); - } catch (err) { - console.error("Failed to get user skills:", err); - res.status(500).json({ message: "Failed to get user skills." }); - } -}); +router.get("/user-skills/:userId", isAuthenticated, skillController.getUserSkills); //create a new skill -router.post("/create-skill", isAuthenticated, async (req, res) => { - try { - const { name, category, type, description } = req.body; - const skill = new Skill({ - skill_id: uuidv4(), - name, - category, - type, - description, - }); - await skill.save(); - res.status(201).json(skill); - } catch (err) { - res.status(500).json({ error: "Failed to add skill" }); - } -}); +router.post("/create-skill", isAuthenticated, skillController.createSkill); //create new user skill -router.post("/create-user-skill", isAuthenticated, async (req, res) => { - try { - const { user_id, skill_id, proficiency_level, position_id } = req.body; - - const newUserSkill = new UserSkill({ - user_id, - skill_id, - proficiency_level, - position_id: position_id || null, - }); - - await newUserSkill.save(); - res.status(201).json(newUserSkill); - } catch (err) { - console.error(err); - res.status(500).json({ error: "Failed to add user skill" }); - } -}); +router.post("/create-user-skill", isAuthenticated, skillController.createUserSkill); // GET top 5 most popular skills campus-wide -router.get("/top-skills", isAuthenticated, async (req, res) => { - try { - const topSkills = await UserSkill.aggregate([ - { $match: { is_endorsed: true } }, - { $group: { _id: "$skill_id", totalUsers: { $sum: 1 } } }, - { $sort: { totalUsers: -1 } }, - { $limit: 5 }, - { - $lookup: { - from: "skills", - localField: "_id", - foreignField: "_id", - as: "skillDetails", - }, - }, - { $unwind: "$skillDetails" }, - { - $project: { - _id: 0, - skillName: "$skillDetails.name", - type: "$skillDetails.type", - totalUsers: 1, - }, - }, - ]); - - res.json(topSkills); - } catch (err) { - console.error(err); - res.status(500).json({ message: "Error fetching top skills." }); - } -}); +router.get("/top-skills", isAuthenticated, skillController.getTopSkills); module.exports = router;