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:

+
+ + Verify Email Address + +
+

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;