diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js
index f44ce39..4f55dbd 100644
--- a/backend/controllers/authController.js
+++ b/backend/controllers/authController.js
@@ -3,6 +3,7 @@ import { sendEmail } from '../utils/sendEmail.js';
import bcrypt from 'bcryptjs';
import Joi from 'joi';
import jwt from 'jsonwebtoken';
+import crypto from 'crypto';
// Validation schema for registration
const registerSchema = Joi.object({
@@ -47,6 +48,11 @@ export const register = async (req, res) => {
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);
+ // Generate email verification token
+ const emailVerificationToken = crypto.randomBytes(32).toString('hex');
+ const emailVerificationTokenExpiry = new Date();
+ emailVerificationTokenExpiry.setHours(emailVerificationTokenExpiry.getHours() + 24); // Token expires in 24 hours
+
// Create user
const user = await User.create({
firstName,
@@ -56,35 +62,78 @@ export const register = async (req, res) => {
studentStaffId,
role,
isConfirmed: false,
- status: 'pending_role'
+ status: 'pending_role',
+ isEmailVerified: false,
+ emailVerificationToken,
+ emailVerificationTokenExpiry
});
- // Send confirmation email
- try {
- await sendEmail(
- email,
- 'Registration Successful - Bit by Bit',
- `
-
Welcome to Bit by Bit!
- Dear ${firstName} ${lastName},
- Your registration was successful. Your account is currently pending role verification.
- Role: ${role}
- Student/Staff ID: ${studentStaffId}
- You will receive another email once your role has been verified by an administrator.
-
- Best regards,
Bit by Bit Team
- `
- );
- } catch (emailError) {
- console.error('Email sending failed:', emailError);
- // Don't fail registration if email fails
+ // Send verification email (only for students)
+ if (role === 'student') {
+ try {
+ // Verification link should point to backend API endpoint
+ const backendUrl = process.env.BACKEND_URL || 'http://localhost:3000';
+ const verificationUrl = `${backendUrl}/api/users/verify-email?token=${emailVerificationToken}`;
+ await sendEmail(
+ email,
+ 'Verify Your Email - Bit by Bit',
+ `
+
+
Welcome to Bit by Bit!
+
Dear ${firstName} ${lastName},
+
Thank you for registering! Please verify your email address to complete your registration.
+
Click the button below to verify your email:
+
+
Or copy and paste this link into your browser:
+
${verificationUrl}
+
This link will expire in 24 hours.
+
If you didn't create an account, please ignore this email.
+
+
Best regards,
Bit by Bit Team
+
+ `
+ );
+ } catch (emailError) {
+ console.error('Email sending failed:', emailError);
+ // Don't fail registration if email fails
+ }
+ } else {
+ // For non-students, send regular confirmation email
+ try {
+ await sendEmail(
+ email,
+ 'Registration Successful - Bit by Bit',
+ `
+ Welcome to Bit by Bit!
+ Dear ${firstName} ${lastName},
+ Your registration was successful. Your account is currently pending role verification.
+ Role: ${role}
+ Student/Staff ID: ${studentStaffId}
+ You will receive another email once your role has been verified by an administrator.
+
+ Best regards,
Bit by Bit Team
+ `
+ );
+ } catch (emailError) {
+ console.error('Email sending failed:', emailError);
+ // Don't fail registration if email fails
+ }
}
// Return user data (without password)
- const { password: _, ...userWithoutPassword } = user.toObject();
+ const { password: _, emailVerificationToken: __, ...userWithoutPassword } = user.toObject();
+
+ const message = role === 'student'
+ ? 'Registration successful. Please check your email to verify your account.'
+ : 'Registration successful. Please wait for role verification.';
return res.status(201).json({
- message: 'Registration successful. Please wait for role verification.',
+ message,
user: userWithoutPassword
});
@@ -109,6 +158,13 @@ export const login = async (req, res) => {
return res.status(401).json({ message: 'Invalid credentials' });
}
+ // Check email verification for students
+ if (user.role === 'student' && !user.isEmailVerified) {
+ return res.status(401).json({
+ message: 'Please verify your email address before logging in. Check your inbox for the verification link.'
+ });
+ }
+
// Check if user is confirmed and active
if (!user.isConfirmed || user.status !== 'active') {
return res.status(401).json({
@@ -150,6 +206,52 @@ export const login = async (req, res) => {
}
};
+// Email verification
+export const verifyEmail = async (req, res) => {
+ try {
+ const { token } = req.query;
+
+ if (!token) {
+ // Redirect to login page with error message
+ const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
+ return res.redirect(`${frontendUrl}/login?error=Invalid verification token`);
+ }
+
+ // Find user with this token
+ const user = await User.findOne({
+ emailVerificationToken: token,
+ emailVerificationTokenExpiry: { $gt: new Date() } // Token not expired
+ });
+
+ if (!user) {
+ // Token is invalid or expired
+ const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
+ return res.redirect(`${frontendUrl}/login?error=Verification token is invalid or has expired`);
+ }
+
+ // Check if already verified
+ if (user.isEmailVerified) {
+ const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
+ return res.redirect(`${frontendUrl}/login?message=Email already verified`);
+ }
+
+ // Verify the email
+ user.isEmailVerified = true;
+ user.emailVerificationToken = undefined;
+ user.emailVerificationTokenExpiry = undefined;
+ await user.save();
+
+ // Redirect to login page with success message
+ const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
+ return res.redirect(`${frontendUrl}/login?message=Email verified successfully. You can now login.`);
+
+ } catch (error) {
+ console.error('Email verification error:', error);
+ const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
+ return res.redirect(`${frontendUrl}/login?error=An error occurred during verification`);
+ }
+};
+
// User logout
export const logout = async (req, res) => {
try {
diff --git a/backend/controllers/documentController.js b/backend/controllers/documentController.js
new file mode 100644
index 0000000..9f210a6
--- /dev/null
+++ b/backend/controllers/documentController.js
@@ -0,0 +1,365 @@
+import Event, { Bazaar, BoothApplicant, BazaarApplicant } from '../models/events.js';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// Get all documents from various sources
+export const getAllDocuments = async (req, res) => {
+ try {
+ const documents = [];
+
+ // Get documents from Bazaar applicants (both embedded and separate models)
+ // First, get from embedded applicants in Event
+ const bazaars = await Event.find({ type: 'bazaar' }).populate('applicants.vendor', 'companyName');
+
+ for (const bazaar of bazaars) {
+ if (bazaar.applicants && bazaar.applicants.length > 0) {
+ for (const applicant of bazaar.applicants) {
+ if (applicant.attendees && applicant.attendees.length > 0) {
+ for (const attendee of applicant.attendees) {
+ if (attendee.idImage) {
+ documents.push({
+ id: attendee._id,
+ type: 'bazaar-attendee-id',
+ source: 'bazaar',
+ eventId: bazaar._id,
+ eventName: bazaar.name,
+ vendorId: applicant.vendor?._id,
+ vendorName: applicant.vendor?.companyName || 'Unknown',
+ attendeeName: attendee.name,
+ attendeeEmail: attendee.email,
+ documentPath: attendee.idImage,
+ uploadedAt: applicant.appliedAt,
+ status: applicant.status
+ });
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Also get from separate BazaarApplicant model
+ const bazaarApplicants = await BazaarApplicant.find().populate('vendor', 'companyName').populate('eventId', 'name');
+
+ for (const applicant of bazaarApplicants) {
+ if (applicant.attendees && applicant.attendees.length > 0) {
+ for (const attendee of applicant.attendees) {
+ if (attendee.idImage) {
+ documents.push({
+ id: attendee._id,
+ type: 'bazaar-attendee-id',
+ source: 'bazaar',
+ eventId: applicant.eventId?._id,
+ eventName: applicant.eventId?.name || 'Unknown Event',
+ vendorId: applicant.vendor?._id,
+ vendorName: applicant.vendor?.companyName || 'Unknown',
+ attendeeName: attendee.name,
+ attendeeEmail: attendee.email,
+ documentPath: attendee.idImage,
+ uploadedAt: applicant.appliedAt,
+ status: applicant.status
+ });
+ }
+ }
+ }
+ }
+
+ // Get documents from Booth applicants
+ const boothApplicants = await BoothApplicant.find().populate('vendor', 'companyName');
+
+ for (const applicant of boothApplicants) {
+ if (applicant.attendees && applicant.attendees.length > 0) {
+ for (const attendee of applicant.attendees) {
+ if (attendee.idImage) {
+ documents.push({
+ id: attendee._id,
+ type: 'booth-attendee-id',
+ source: 'booth',
+ applicantId: applicant._id,
+ vendorId: applicant.vendor?._id,
+ vendorName: applicant.vendor?.companyName || 'Unknown',
+ attendeeName: attendee.name,
+ attendeeEmail: attendee.email,
+ documentPath: attendee.idImage,
+ uploadedAt: applicant.appliedAt,
+ status: applicant.status
+ });
+ }
+ }
+ }
+ }
+
+ return res.status(200).json({
+ message: 'Documents retrieved successfully',
+ count: documents.length,
+ documents
+ });
+
+ } catch (error) {
+ console.error('Error fetching documents:', error);
+ return res.status(500).json({ message: 'Internal server error' });
+ }
+};
+
+// View/download a specific document
+export const viewDocument = async (req, res) => {
+ try {
+ const { documentId, source } = req.params;
+
+ let document = null;
+ let documentPath = null;
+ let fileName = null;
+
+ if (source === 'bazaar') {
+ // Check embedded applicants first
+ const bazaars = await Event.find({ type: 'bazaar' }).populate('applicants.vendor', 'companyName');
+
+ for (const bazaar of bazaars) {
+ if (bazaar.applicants && bazaar.applicants.length > 0) {
+ for (const applicant of bazaar.applicants) {
+ if (applicant.attendees && applicant.attendees.length > 0) {
+ for (const attendee of applicant.attendees) {
+ if (attendee._id.toString() === documentId && attendee.idImage) {
+ documentPath = attendee.idImage;
+ fileName = `${attendee.name}_${bazaar.name}_ID.jpg`;
+ document = {
+ attendeeName: attendee.name,
+ eventName: bazaar.name,
+ vendorName: applicant.vendor?.companyName
+ };
+ break;
+ }
+ }
+ }
+ if (document) break;
+ }
+ }
+ if (document) break;
+ }
+
+ // If not found, check separate BazaarApplicant model
+ if (!documentPath) {
+ const bazaarApplicants = await BazaarApplicant.find().populate('vendor', 'companyName').populate('eventId', 'name');
+
+ for (const applicant of bazaarApplicants) {
+ if (applicant.attendees && applicant.attendees.length > 0) {
+ for (const attendee of applicant.attendees) {
+ if (attendee._id.toString() === documentId && attendee.idImage) {
+ documentPath = attendee.idImage;
+ fileName = `${attendee.name}_${applicant.eventId?.name || 'Bazaar'}_ID.jpg`;
+ document = {
+ attendeeName: attendee.name,
+ eventName: applicant.eventId?.name || 'Unknown Event',
+ vendorName: applicant.vendor?.companyName
+ };
+ break;
+ }
+ }
+ }
+ if (documentPath) break;
+ }
+ }
+ } else if (source === 'booth') {
+ const boothApplicants = await BoothApplicant.find().populate('vendor', 'companyName');
+
+ for (const applicant of boothApplicants) {
+ if (applicant.attendees && applicant.attendees.length > 0) {
+ for (const attendee of applicant.attendees) {
+ if (attendee._id.toString() === documentId && attendee.idImage) {
+ documentPath = attendee.idImage;
+ fileName = `${attendee.name}_Booth_ID.jpg`;
+ document = {
+ attendeeName: attendee.name,
+ vendorName: applicant.vendor?.companyName
+ };
+ break;
+ }
+ }
+ }
+ if (document) break;
+ }
+ }
+
+ if (!documentPath) {
+ return res.status(404).json({ message: 'Document not found' });
+ }
+
+ // Check if documentPath is a file path, base64, or URL
+ if (documentPath.startsWith('data:image') || documentPath.startsWith('data:application')) {
+ // Base64 encoded document
+ const base64Data = documentPath.split(',')[1];
+ const buffer = Buffer.from(base64Data, 'base64');
+
+ // Determine content type
+ const contentType = documentPath.match(/data:([^;]+)/)?.[1] || 'application/octet-stream';
+
+ res.setHeader('Content-Type', contentType);
+ res.setHeader('Content-Disposition', `inline; filename="${fileName || 'document'}"`);
+ return res.send(buffer);
+
+ } else if (documentPath.startsWith('http://') || documentPath.startsWith('https://')) {
+ // URL - redirect to the URL
+ return res.redirect(documentPath);
+
+ } else {
+ // File path - serve from filesystem
+ const fullPath = path.isAbsolute(documentPath)
+ ? documentPath
+ : path.join(__dirname, '..', 'uploads', documentPath);
+
+ if (!fs.existsSync(fullPath)) {
+ return res.status(404).json({ message: 'Document file not found on server' });
+ }
+
+ // Determine content type based on file extension
+ const ext = path.extname(fullPath).toLowerCase();
+ const contentTypes = {
+ '.jpg': 'image/jpeg',
+ '.jpeg': 'image/jpeg',
+ '.png': 'image/png',
+ '.pdf': 'application/pdf',
+ '.doc': 'application/msword',
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+ };
+
+ const contentType = contentTypes[ext] || 'application/octet-stream';
+
+ res.setHeader('Content-Type', contentType);
+ res.setHeader('Content-Disposition', `inline; filename="${fileName || path.basename(fullPath)}"`);
+
+ return res.sendFile(fullPath);
+ }
+
+ } catch (error) {
+ console.error('Error viewing document:', error);
+ return res.status(500).json({ message: 'Internal server error' });
+ }
+};
+
+// Download a specific document (forces download instead of viewing)
+export const downloadDocument = async (req, res) => {
+ try {
+ const { documentId, source } = req.params;
+
+ let documentPath = null;
+ let fileName = null;
+
+ if (source === 'bazaar') {
+ // Check embedded applicants first
+ const bazaars = await Event.find({ type: 'bazaar' }).populate('applicants.vendor', 'companyName');
+
+ for (const bazaar of bazaars) {
+ if (bazaar.applicants && bazaar.applicants.length > 0) {
+ for (const applicant of bazaar.applicants) {
+ if (applicant.attendees && applicant.attendees.length > 0) {
+ for (const attendee of applicant.attendees) {
+ if (attendee._id.toString() === documentId && attendee.idImage) {
+ documentPath = attendee.idImage;
+ fileName = `${attendee.name}_${bazaar.name}_ID.jpg`;
+ break;
+ }
+ }
+ }
+ if (documentPath) break;
+ }
+ }
+ if (documentPath) break;
+ }
+
+ // If not found, check separate BazaarApplicant model
+ if (!documentPath) {
+ const bazaarApplicants = await BazaarApplicant.find().populate('vendor', 'companyName').populate('eventId', 'name');
+
+ for (const applicant of bazaarApplicants) {
+ if (applicant.attendees && applicant.attendees.length > 0) {
+ for (const attendee of applicant.attendees) {
+ if (attendee._id.toString() === documentId && attendee.idImage) {
+ documentPath = attendee.idImage;
+ fileName = `${attendee.name}_${applicant.eventId?.name || 'Bazaar'}_ID.jpg`;
+ break;
+ }
+ }
+ }
+ if (documentPath) break;
+ }
+ }
+ } else if (source === 'booth') {
+ const boothApplicants = await BoothApplicant.find().populate('vendor', 'companyName');
+
+ for (const applicant of boothApplicants) {
+ if (applicant.attendees && applicant.attendees.length > 0) {
+ for (const attendee of applicant.attendees) {
+ if (attendee._id.toString() === documentId && attendee.idImage) {
+ documentPath = attendee.idImage;
+ fileName = `${attendee.name}_Booth_ID.jpg`;
+ break;
+ }
+ }
+ }
+ if (documentPath) break;
+ }
+ }
+
+ if (!documentPath) {
+ return res.status(404).json({ message: 'Document not found' });
+ }
+
+ // Check if documentPath is a file path, base64, or URL
+ if (documentPath.startsWith('data:image') || documentPath.startsWith('data:application')) {
+ // Base64 encoded document
+ const base64Data = documentPath.split(',')[1];
+ const buffer = Buffer.from(base64Data, 'base64');
+
+ // Determine content type and extension
+ const contentType = documentPath.match(/data:([^;]+)/)?.[1] || 'application/octet-stream';
+ const ext = contentType.includes('pdf') ? '.pdf' :
+ contentType.includes('jpeg') || contentType.includes('jpg') ? '.jpg' :
+ contentType.includes('png') ? '.png' : '.bin';
+
+ res.setHeader('Content-Type', contentType);
+ res.setHeader('Content-Disposition', `attachment; filename="${fileName || 'document'}${ext}"`);
+ return res.send(buffer);
+
+ } else if (documentPath.startsWith('http://') || documentPath.startsWith('https://')) {
+ // URL - redirect to download
+ return res.redirect(documentPath);
+
+ } else {
+ // File path - serve from filesystem
+ const fullPath = path.isAbsolute(documentPath)
+ ? documentPath
+ : path.join(__dirname, '..', 'uploads', documentPath);
+
+ if (!fs.existsSync(fullPath)) {
+ return res.status(404).json({ message: 'Document file not found on server' });
+ }
+
+ // Determine content type based on file extension
+ const ext = path.extname(fullPath).toLowerCase();
+ const contentTypes = {
+ '.jpg': 'image/jpeg',
+ '.jpeg': 'image/jpeg',
+ '.png': 'image/png',
+ '.pdf': 'application/pdf',
+ '.doc': 'application/msword',
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+ };
+
+ const contentType = contentTypes[ext] || 'application/octet-stream';
+
+ res.setHeader('Content-Type', contentType);
+ res.setHeader('Content-Disposition', `attachment; filename="${fileName || path.basename(fullPath)}"`);
+
+ return res.download(fullPath, fileName || path.basename(fullPath));
+ }
+
+ } catch (error) {
+ console.error('Error downloading document:', error);
+ return res.status(500).json({ message: 'Internal server error' });
+ }
+};
+
diff --git a/backend/controllers/eventController.js b/backend/controllers/eventController.js
index dcdd5f5..8c7bc57 100644
--- a/backend/controllers/eventController.js
+++ b/backend/controllers/eventController.js
@@ -363,6 +363,145 @@ export const approveWorkshop = async (req, res) => {
}
};
+// ❌ Reject Workshop (for Events Office)
+export const rejectWorkshop = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { reason } = req.body; // optional rejection reason
+
+ const update = {
+ status: "rejected",
+ approvedBy: req.user?._id || null,
+ approvedAt: new Date(),
+ ...(reason && { rejectionReason: reason })
+ };
+
+ const workshop = await Workshop.findByIdAndUpdate(id, { $set: update }, { new: true });
+
+ if (!workshop)
+ return res.status(404).json({ message: "Workshop not found" });
+
+ return res.json({
+ message: "Workshop rejected successfully",
+ workshop,
+ });
+ } catch (error) {
+ console.error("Error rejecting workshop:", error);
+ res.status(500).json({ message: "Error rejecting workshop", error: error.message });
+ }
+};
+
+// 📝 Request Edits for Workshop (for Events Office)
+export const requestWorkshopEdits = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { message, fieldsToEdit } = req.body;
+
+ if (!message || message.trim() === "") {
+ return res.status(400).json({ message: "Edit request message is required" });
+ }
+
+ const workshop = await Workshop.findById(id);
+ if (!workshop) {
+ return res.status(404).json({ message: "Workshop not found" });
+ }
+
+ // Create edit request
+ const editRequest = {
+ requestedBy: req.user?._id || null,
+ requestedAt: new Date(),
+ message: message.trim(),
+ fieldsToEdit: fieldsToEdit || [],
+ resolved: false
+ };
+
+ // Add edit request to workshop and update status
+ workshop.editRequests = workshop.editRequests || [];
+ workshop.editRequests.push(editRequest);
+ workshop.status = "edits requested";
+ workshop.approvedBy = req.user?._id || null;
+ workshop.approvedAt = new Date();
+
+ await workshop.save();
+
+ return res.json({
+ message: "Edit request submitted successfully",
+ workshop,
+ editRequest
+ });
+ } catch (error) {
+ console.error("Error requesting workshop edits:", error);
+ res.status(500).json({ message: "Error requesting workshop edits", error: error.message });
+ }
+};
+
+// ✅ Resolve Edit Request (for Workshop Creator)
+export const resolveEditRequest = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { editRequestId } = req.body;
+
+ if (!editRequestId) {
+ return res.status(400).json({ message: "Edit request ID is required" });
+ }
+
+ const workshop = await Workshop.findById(id);
+ if (!workshop) {
+ return res.status(404).json({ message: "Workshop not found" });
+ }
+
+ // Find the specific edit request
+ const editRequest = workshop.editRequests.id(editRequestId);
+ if (!editRequest) {
+ return res.status(404).json({ message: "Edit request not found" });
+ }
+
+ // Mark as resolved
+ editRequest.resolved = true;
+ editRequest.resolvedAt = new Date();
+
+ // Check if all edit requests are resolved
+ const allResolved = workshop.editRequests.every(req => req.resolved);
+ if (allResolved) {
+ workshop.status = "pending"; // Reset to pending for re-review
+ }
+
+ await workshop.save();
+
+ return res.json({
+ message: "Edit request resolved successfully",
+ workshop,
+ editRequest
+ });
+ } catch (error) {
+ console.error("Error resolving edit request:", error);
+ res.status(500).json({ message: "Error resolving edit request", error: error.message });
+ }
+};
+
+// 📋 Get Workshop Edit Requests
+export const getWorkshopEditRequests = async (req, res) => {
+ try {
+ const { id } = req.params;
+
+ const workshop = await Workshop.findById(id)
+ .populate('editRequests.requestedBy', 'name email')
+ .populate('createdBy', 'name email');
+
+ if (!workshop) {
+ return res.status(404).json({ message: "Workshop not found" });
+ }
+
+ return res.json({
+ workshop,
+ editRequests: workshop.editRequests || []
+ });
+ } catch (error) {
+ console.error("Error fetching edit requests:", error);
+ res.status(500).json({ message: "Error fetching edit requests", error: error.message });
+ }
+};
+
// 🎯 Get all upcoming events with vendor details
export const getAllUpcomingEvents = async (req, res) => {
try {
diff --git a/backend/controllers/gymSessionsController.js b/backend/controllers/gymSessionsController.js
index 788150d..b69292b 100644
--- a/backend/controllers/gymSessionsController.js
+++ b/backend/controllers/gymSessionsController.js
@@ -1,4 +1,5 @@
import GymSession from "../models/gymSessions.js";
+import User from "../models/users.js";
// Create session
export const createSession = async (req, res) => {
@@ -46,3 +47,81 @@ export const getMonthlySchedule = async (req, res) => {
return res.status(500).json({ message: "Server error" });
}
};
+
+// Register for a gym session
+export const registerForSession = async (req, res) => {
+ try {
+ const { sessionId } = req.params;
+ const userId = req.user?.userId || req.user?.id;
+
+ if (!userId) {
+ return res.status(401).json({ message: "Authentication required" });
+ }
+
+ // Get user to verify role
+ const user = await User.findById(userId);
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ }
+
+ // Check if user role is allowed (student, staff, ta, professor)
+ const allowedRoles = ['student', 'staff', 'ta', 'professor'];
+ if (!allowedRoles.includes(user.role)) {
+ return res.status(403).json({
+ message: "Only students, staff, TA, and professors can register for sessions"
+ });
+ }
+
+ // Find the session
+ const session = await GymSession.findById(sessionId);
+ if (!session) {
+ return res.status(404).json({ message: "Session not found" });
+ }
+
+ // Check if session date is in the past
+ const sessionDate = new Date(session.date);
+ const now = new Date();
+ if (sessionDate < now) {
+ return res.status(400).json({
+ message: "Cannot register for past sessions"
+ });
+ }
+
+ // Check if user is already registered
+ if (session.registeredUsers && session.registeredUsers.includes(userId)) {
+ return res.status(409).json({
+ message: "You are already registered for this session"
+ });
+ }
+
+ // Check if session is full
+ const currentParticipants = session.registeredUsers ? session.registeredUsers.length : 0;
+ if (currentParticipants >= session.maxParticipants) {
+ return res.status(400).json({
+ message: "Session is full. Maximum participants reached."
+ });
+ }
+
+ // Register user for session
+ session.registeredUsers = session.registeredUsers || [];
+ session.registeredUsers.push(userId);
+ await session.save();
+
+ return res.status(200).json({
+ message: "Successfully registered for session",
+ session: {
+ id: session._id,
+ type: session.type,
+ date: session.date,
+ time: session.time,
+ duration: session.duration,
+ currentParticipants: session.registeredUsers.length,
+ maxParticipants: session.maxParticipants
+ }
+ });
+
+ } catch (error) {
+ console.error("Error registering for session:", error);
+ return res.status(500).json({ message: "Internal server error" });
+ }
+};
diff --git a/backend/controllers/userController.js b/backend/controllers/userController.js
index 7651527..b212917 100644
--- a/backend/controllers/userController.js
+++ b/backend/controllers/userController.js
@@ -9,4 +9,187 @@ export async function getAllCourts(req, res) {
catch(error){
res.status(500).json({message: error.message});
}
-};
\ No newline at end of file
+}
+
+// Get available court slots (free dates and times)
+export async function getAvailableCourtSlots(req, res) {
+ try {
+ const { courtType, date } = req.query; // Optional filters: courtType (basketball/tennis/football), date (YYYY-MM-DD)
+
+ // Build query
+ const query = {};
+ if (courtType) {
+ query.type = courtType;
+ }
+
+ const courts = await Court.find(query);
+
+ // Filter to show only available slots
+ const availableSlots = [];
+
+ for (const court of courts) {
+ if (court.availability && court.availability.length > 0) {
+ for (const availability of court.availability) {
+ // Filter by date if provided
+ if (date) {
+ const requestedDate = new Date(date);
+ const availabilityDate = new Date(availability.date);
+
+ // Compare dates (ignore time)
+ if (
+ availabilityDate.getFullYear() !== requestedDate.getFullYear() ||
+ availabilityDate.getMonth() !== requestedDate.getMonth() ||
+ availabilityDate.getDate() !== requestedDate.getDate()
+ ) {
+ continue;
+ }
+ }
+
+ // Only include future dates
+ const availabilityDate = new Date(availability.date);
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ if (availabilityDate < today) {
+ continue; // Skip past dates
+ }
+
+ if (availability.timeSlots && availability.timeSlots.length > 0) {
+ for (const slot of availability.timeSlots) {
+ if (!slot.isBooked) {
+ availableSlots.push({
+ courtId: court._id,
+ courtName: court.name,
+ courtType: court.type,
+ location: court.location,
+ date: availability.date,
+ startTime: slot.startTime,
+ endTime: slot.endTime,
+ slotId: slot._id
+ });
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Sort by date, then by start time
+ availableSlots.sort((a, b) => {
+ const dateA = new Date(a.date);
+ const dateB = new Date(b.date);
+ if (dateA.getTime() !== dateB.getTime()) {
+ return dateA - dateB;
+ }
+ return a.startTime.localeCompare(b.startTime);
+ });
+
+ return res.status(200).json({
+ message: 'Available court slots retrieved successfully',
+ count: availableSlots.length,
+ slots: availableSlots
+ });
+
+ } catch (error) {
+ console.error('Error fetching available court slots:', error);
+ return res.status(500).json({ message: 'Internal server error' });
+ }
+}
+
+// Reserve a court slot
+export async function reserveCourtSlot(req, res) {
+ try {
+ const { courtId, date, startTime } = req.body;
+ const userId = req.user?.userId || req.user?.id;
+
+ if (!courtId || !date || !startTime) {
+ return res.status(400).json({
+ message: 'courtId, date, and startTime are required'
+ });
+ }
+
+ // Get the authenticated user to auto-populate name and GUC ID
+ const user = await User.findById(userId);
+ if (!user) {
+ return res.status(404).json({ message: 'User not found' });
+ }
+
+ // Verify user is a student
+ if (user.role !== 'student') {
+ return res.status(403).json({
+ message: 'Only students can reserve courts'
+ });
+ }
+
+ // Find the court
+ const court = await Court.findById(courtId);
+ if (!court) {
+ return res.status(404).json({ message: 'Court not found' });
+ }
+
+ // Find the specific date and time slot
+ const requestedDate = new Date(date);
+ requestedDate.setHours(0, 0, 0, 0);
+
+ let foundSlot = null;
+ let foundDateIndex = -1;
+ let foundSlotIndex = -1;
+
+ for (let i = 0; i < court.availability.length; i++) {
+ const availability = court.availability[i];
+ const availabilityDate = new Date(availability.date);
+ availabilityDate.setHours(0, 0, 0, 0);
+
+ // Check if dates match
+ if (availabilityDate.getTime() === requestedDate.getTime()) {
+ if (availability.timeSlots && availability.timeSlots.length > 0) {
+ for (let j = 0; j < availability.timeSlots.length; j++) {
+ const slot = availability.timeSlots[j];
+ if (slot.startTime === startTime && !slot.isBooked) {
+ foundSlot = slot;
+ foundDateIndex = i;
+ foundSlotIndex = j;
+ break;
+ }
+ }
+ }
+ if (foundSlot) break;
+ }
+ }
+
+ if (!foundSlot) {
+ return res.status(404).json({
+ message: 'Available slot not found. The slot may already be booked or does not exist.'
+ });
+ }
+
+ // Reserve the slot
+ court.availability[foundDateIndex].timeSlots[foundSlotIndex].isBooked = true;
+ court.availability[foundDateIndex].timeSlots[foundSlotIndex].bookedBy = userId;
+ court.availability[foundDateIndex].timeSlots[foundSlotIndex].studentName = `${user.firstName} ${user.lastName}`;
+ court.availability[foundDateIndex].timeSlots[foundSlotIndex].studentGUCId = user.studentStaffId || '';
+ court.availability[foundDateIndex].timeSlots[foundSlotIndex].reservedAt = new Date();
+
+ await court.save();
+
+ return res.status(200).json({
+ message: 'Court slot reserved successfully',
+ reservation: {
+ courtId: court._id,
+ courtName: court.name,
+ courtType: court.type,
+ location: court.location,
+ date: requestedDate,
+ startTime: foundSlot.startTime,
+ endTime: foundSlot.endTime,
+ studentName: `${user.firstName} ${user.lastName}`,
+ studentGUCId: user.studentStaffId || '',
+ reservedAt: new Date()
+ }
+ });
+
+ } catch (error) {
+ console.error('Error reserving court slot:', error);
+ return res.status(500).json({ message: 'Internal server error' });
+ }
+}
\ No newline at end of file
diff --git a/backend/controllers/vendorController.js b/backend/controllers/vendorController.js
index 84d4334..3647e68 100644
--- a/backend/controllers/vendorController.js
+++ b/backend/controllers/vendorController.js
@@ -151,4 +151,26 @@ export const logoutVendor = async (req, res) => {
console.error('Vendor logout error:', error);
return res.status(500).json({ message: 'Internal server error' });
}
+};
+
+// Get all loyalty program vendors
+// Accessible to: Student, Staff, TA, Professor, Events Office, Admin
+export const getLoyaltyProgramVendors = async (req, res) => {
+ try {
+ // Get only vendors that are loyalty partners
+ const loyaltyVendors = await Vendor.find({ isLoyaltyPartner: true })
+ .select('-password -requests -upcomingEvents') // Exclude sensitive/unnecessary fields
+ .select('companyName email discountRate promoCode termsAndConditions isLoyaltyPartner createdAt updatedAt')
+ .sort({ companyName: 1 }); // Sort alphabetically by company name
+
+ return res.status(200).json({
+ message: 'Loyalty program vendors retrieved successfully',
+ count: loyaltyVendors.length,
+ vendors: loyaltyVendors
+ });
+
+ } catch (error) {
+ console.error('Error fetching loyalty program vendors:', error);
+ return res.status(500).json({ message: 'Internal server error' });
+ }
};
\ No newline at end of file
diff --git a/backend/index.js b/backend/index.js
index 8003994..d28f308 100644
--- a/backend/index.js
+++ b/backend/index.js
@@ -8,6 +8,7 @@ import adminRoutes from "./routes/adminRoutes.js";
import eventRoutes from "./routes/eventRoutes.js";
import eventRegistrationRoutes from "./routes/eventRegistrationRoutes.js";
import gymSessionRoutes from "./routes/gymSessionRoutes.js";
+import documentRoutes from "./routes/documentRoutes.js";
dotenv.config();
const app = express();
@@ -24,5 +25,6 @@ app.use("/api/admin", adminRoutes);
app.use("/api/events", eventRoutes);
app.use("/api/registrations", eventRegistrationRoutes);
app.use("/api/gym", gymSessionRoutes);
+app.use("/api/documents", documentRoutes);
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
diff --git a/backend/models/Workshop.js b/backend/models/Workshop.js
index 410e03e..4c399da 100644
--- a/backend/models/Workshop.js
+++ b/backend/models/Workshop.js
@@ -61,9 +61,20 @@ const WorkshopSchema = new mongoose.Schema({
ref: "User",
required: true
},
- status:{ type: String, enum: ["pending", "approved", "rejected"], default: "pending" },
+ status:{ type: String, enum: ["pending", "approved", "rejected", "edits requested"], default: "pending" },
approvedBy: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
approvedAt: Date,
+ rejectionReason: { type: String },
+ editRequests: [
+ {
+ requestedBy: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
+ requestedAt: { type: Date, default: Date.now },
+ message: { type: String, required: true }, // what needs to be changed
+ fieldsToEdit: [{ type: String }], // optional: list of affected fields
+ resolved: { type: Boolean, default: false }, // creator addressed it?
+ resolvedAt: { type: Date }
+ }
+ ]
},
{ timestamps: true }
);
diff --git a/backend/models/courts.js b/backend/models/courts.js
index abbe96a..4daaaa7 100644
--- a/backend/models/courts.js
+++ b/backend/models/courts.js
@@ -29,6 +29,9 @@ const courtSchema = new mongoose.Schema(
endTime: { type: String, required: true }, // e.g., "11:30"
isBooked: { type: Boolean, default: false }, // whether the slot is free or taken
bookedBy: { type: mongoose.Schema.Types.ObjectId, ref: "User", default: null },
+ studentName: { type: String }, // automatically populated from user
+ studentGUCId: { type: String }, // automatically populated from user
+ reservedAt: { type: Date }, // when the reservation was made
},
],
},
diff --git a/backend/models/gymSessions.js b/backend/models/gymSessions.js
index 10d1916..10a4299 100644
--- a/backend/models/gymSessions.js
+++ b/backend/models/gymSessions.js
@@ -11,6 +11,7 @@ const gymSessionSchema = new mongoose.Schema(
time: { type: String, required: true }, // e.g. "18:30"
duration: { type: Number, required: true }, // in minutes
maxParticipants: { type: Number, required: true, default: 20 },
+ registeredUsers: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }], // users who registered
},
{ timestamps: true }
);
diff --git a/backend/models/users.js b/backend/models/users.js
index e0aee21..4b94712 100644
--- a/backend/models/users.js
+++ b/backend/models/users.js
@@ -14,6 +14,10 @@ const userSchema = new mongoose.Schema({
isConfirmed: { type: Boolean, default: false },
status: { type: String, enum: ['active','blocked','deleted','pending_role'], default: 'pending_role' },
registeredEvents: [{ type: mongoose.Schema.Types.ObjectId, ref: "Event" }],
+ // Email verification fields
+ isEmailVerified: { type: Boolean, default: false },
+ emailVerificationToken: { type: String },
+ emailVerificationTokenExpiry: { type: Date },
}, { timestamps: true });
export default mongoose.model("User", userSchema);
diff --git a/backend/models/vendors.js b/backend/models/vendors.js
index 7a2444e..8ba1c92 100644
--- a/backend/models/vendors.js
+++ b/backend/models/vendors.js
@@ -21,6 +21,11 @@ const vendorSchema = new mongoose.Schema({
upcomingEvents: [{
event:{ type: mongoose.Schema.Types.ObjectId, ref: "Event" },
}],
+ // GUC Loyalty Program fields
+ isLoyaltyPartner: { type: Boolean, default: false },
+ discountRate: { type: Number, min: 0, max: 100 }, // Percentage discount (0-100)
+ promoCode: { type: String, trim: true },
+ termsAndConditions: { type: String },
}, { timestamps: true });
export default mongoose.model("Vendor", vendorSchema);
diff --git a/backend/routes/documentRoutes.js b/backend/routes/documentRoutes.js
new file mode 100644
index 0000000..28ac4d0
--- /dev/null
+++ b/backend/routes/documentRoutes.js
@@ -0,0 +1,32 @@
+import express from "express";
+import { getAllDocuments, viewDocument, downloadDocument } from "../controllers/documentController.js";
+import { authenticateToken, authorizeRoles } from "../middleware/authMiddleware.js";
+
+const router = express.Router();
+
+// Document routes - accessible only to Events Office and Admin
+router.get(
+ "/",
+ authenticateToken,
+ authorizeRoles('eventsOffice', 'admin'),
+ getAllDocuments
+);
+
+// View document (displays in browser)
+router.get(
+ "/view/:source/:documentId",
+ authenticateToken,
+ authorizeRoles('eventsOffice', 'admin'),
+ viewDocument
+);
+
+// Download document (forces download)
+router.get(
+ "/download/:source/:documentId",
+ authenticateToken,
+ authorizeRoles('eventsOffice', 'admin'),
+ downloadDocument
+);
+
+export default router;
+
diff --git a/backend/routes/eventRoutes.js b/backend/routes/eventRoutes.js
index 49f52d6..9230e50 100644
--- a/backend/routes/eventRoutes.js
+++ b/backend/routes/eventRoutes.js
@@ -1,5 +1,5 @@
import express from 'express';
-import { getAllBazzars, applyToBooth, applyToBazaar, approveWorkshop, createWorkshop, updateWorkshop, getWorkshops, createConference, updateConference, deleteConference, getConferences, createBazaar, updateBazaar, getAllUpcomingEvents, filterEvents, searchEvents, deleteEvent, getVendorUpcomingParticipations, getVendorPendingOrRejectedParticipations,createTrip,updateTrip,listAllApplications,
+import { getAllBazzars, applyToBooth, applyToBazaar, approveWorkshop, rejectWorkshop, requestWorkshopEdits, resolveEditRequest, getWorkshopEditRequests, createWorkshop, updateWorkshop, getWorkshops, createConference, updateConference, deleteConference, getConferences, createBazaar, updateBazaar, getAllUpcomingEvents, filterEvents, searchEvents, deleteEvent, getVendorUpcomingParticipations, getVendorPendingOrRejectedParticipations,createTrip,updateTrip,listAllApplications,
decideBazaarApplication } from '../controllers/eventController.js';
import { authenticateToken, authorizeRoles, optionalAuth } from '../middleware/authMiddleware.js';
import { authenticateVendorToken } from '../middleware/vendorAuthMiddleware.js';
@@ -30,10 +30,14 @@ router.get("/conference", getConferences);
// Events office actions on workshops
router.patch("/workshops/:id/approve", approveWorkshop);
+router.patch("/workshops/:id/reject", rejectWorkshop);
+router.post("/workshops/:id/request-edits", requestWorkshopEdits);
+router.get("/workshops/:id/edit-requests", getWorkshopEditRequests);
//professors actions on workshops
router.post("/workshops", createWorkshop);
router.patch("/workshops/:id", updateWorkshop);
+router.patch("/workshops/:id/resolve-edit", resolveEditRequest);
router.get("/workshops", getWorkshops);
// Vendor routes
diff --git a/backend/routes/gymSessionRoutes.js b/backend/routes/gymSessionRoutes.js
index 4ed40bd..5a4b75d 100644
--- a/backend/routes/gymSessionRoutes.js
+++ b/backend/routes/gymSessionRoutes.js
@@ -1,9 +1,18 @@
import express from "express";
-import { createSession, getMonthlySchedule } from "../controllers/gymSessionsController.js";
+import { createSession, getMonthlySchedule, registerForSession } from "../controllers/gymSessionsController.js";
+import { authenticateToken, authorizeRoles } from "../middleware/authMiddleware.js";
const router = express.Router();
router.post("/sessions", createSession); // create new session
router.get("/schedule", getMonthlySchedule); // view schedule by month
+// Register for a session (students, staff, TA, professors)
+router.post(
+ "/sessions/:sessionId/register",
+ authenticateToken,
+ authorizeRoles('student', 'staff', 'ta', 'professor'),
+ registerForSession
+);
+
export default router;
diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js
index 7abaa65..b70f03d 100644
--- a/backend/routes/userRoutes.js
+++ b/backend/routes/userRoutes.js
@@ -1,13 +1,31 @@
import express from "express";
-import { register, login, logout } from "../controllers/authController.js";
-import { getAllCourts } from "../controllers/userController.js";
+import { register, login, logout, verifyEmail } from "../controllers/authController.js";
+import { getAllCourts, getAvailableCourtSlots, reserveCourtSlot } from "../controllers/userController.js";
+import { authenticateToken, authorizeRoles } from "../middleware/authMiddleware.js";
const router = express.Router();
// Authentication routes
router.post("/register", register);
router.post("/login", login);
router.post("/logout", logout);
+router.get("/verify-email", verifyEmail);
+// Court routes
router.get("/courts", getAllCourts);
+// Court reservation routes (students only)
+router.get(
+ "/courts/available",
+ authenticateToken,
+ authorizeRoles('student'),
+ getAvailableCourtSlots
+);
+
+router.post(
+ "/courts/reserve",
+ authenticateToken,
+ authorizeRoles('student'),
+ reserveCourtSlot
+);
+
export default router;
\ No newline at end of file
diff --git a/backend/routes/vendorRoutes.js b/backend/routes/vendorRoutes.js
index d50dae3..9b2e255 100644
--- a/backend/routes/vendorRoutes.js
+++ b/backend/routes/vendorRoutes.js
@@ -1,5 +1,6 @@
import express from "express";
-import { registerVendor, loginVendor, logoutVendor, getAllVendors, getVendorById } from "../controllers/vendorController.js";
+import { registerVendor, loginVendor, logoutVendor, getAllVendors, getVendorById, getLoyaltyProgramVendors } from "../controllers/vendorController.js";
+import { authenticateToken, authorizeRoles } from "../middleware/authMiddleware.js";
const router = express.Router();
@@ -10,6 +11,16 @@ router.post("/logout", logoutVendor);
// Vendor management routes
router.get("/", getAllVendors);
+
+// Loyalty program vendors route (must be before /:id to avoid route conflicts)
+// Accessible to: Student, Staff, TA, Professor, Events Office, Admin
+router.get(
+ "/loyalty-program",
+ authenticateToken,
+ authorizeRoles('student', 'staff', 'ta', 'professor', 'eventsOffice', 'admin'),
+ getLoyaltyProgramVendors
+);
+
router.get("/:id", getVendorById);
export default router;