diff --git a/.husky/pre-commit b/.husky/pre-commit index 6c3b9637..d24fdfc6 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -cd frontend && npx lint-staged && cd ../backend && npx lint-staged +npx lint-staged diff --git a/backend/db.js b/backend/config/db.js similarity index 85% rename from backend/db.js rename to backend/config/db.js index 18af6fca..cf506a7b 100644 --- a/backend/db.js +++ b/backend/config/db.js @@ -5,10 +5,8 @@ dotenv.config(); const connectDB = async () => { try { const ConnectDB = process.env.MONGODB_URI; - await mongoose.connect(ConnectDB, { - useNewUrlParser: true, - useUnifiedTopology: true, - }); + //Removing the options as they are no longer needed from mongoose6+ + await mongoose.connect(ConnectDB); console.log("MongoDB Connected"); } catch (error) { console.error("MongoDB Connection Error:", error); diff --git a/backend/config/passportConfig.js b/backend/config/passportConfig.js new file mode 100644 index 00000000..1fc30ff9 --- /dev/null +++ b/backend/config/passportConfig.js @@ -0,0 +1,116 @@ +const passport = require("passport"); +const GoogleStrategy = require("passport-google-oauth20").Strategy; +const LocalStrategy = require("passport-local").Strategy; +const isIITBhilaiEmail = require("../utils/isIITBhilaiEmail"); +const User = require("../models/userSchema"); +const { loginValidate } = require("../utils/authValidate"); +const bcrypt = require("bcrypt"); +// Google OAuth Strategy +passport.use( + new GoogleStrategy( + { + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: `${process.env.BACKEND_URL}/auth/google/verify`, // Update with your callback URL + }, + async (accessToken, refreshToken, profile, done) => { + // Check if the user already exists in your database + const email = profile.emails?.[0]?.value; + if (!email) { + //console.log("No email found in Google profile"); + return done(null, false, { message: "Email not available from Google." }); + } + + if (!isIITBhilaiEmail(profile.emails[0].value)) { + console.log("Google OAuth blocked for: ", profile.emails[0].value); + return done(null, false, { + message: "Only @iitbhilai.ac.in emails are allowed.", + }); + } + try { + const user = await User.findOne({ username: email }); + //console.log("Looking for existing user with email:", email, "Found:", !!user); + + if (user) { + // If user exists, return the user + //console.log("Returning existing user:", user.username); + return done(null, user); + } + // If user doesn't exist, create a new user in your database + const newUser = await User.create({ + username: email, + role: "STUDENT", + strategy: "google", + personal_info: { + name: profile.displayName || "No Name", + email: email, + profilePic: + profile.photos && profile.photos.length > 0 + ? profile.photos[0].value + : "https://www.gravatar.com/avatar/?d=mp", + }, + onboardingComplete: false, + }); + //console.log("User is",newUser); + return done(null, newUser); + } catch (error) { + console.error("Error in Google strategy:", error); + return done(error); + } + }, + ), +); + +//Local Strategy +passport.use(new LocalStrategy(async (username, password, done) => { + + const result = loginValidate.safeParse({ username, password }); + + if (!result.success) { + let errors = result.error.issues.map((issue) => issue.message); + return done(null, false, {message: errors}); + } + + try{ + + const user = await User.findOne({ username }); + if (!user) { + return done(null, false, {message: "Invalid user credentials"}); + } + + + if (user.strategy !== "local" || !user.password) { + return done(null, false, { message: "Invalid login method" }); + } + + const isValid = await bcrypt.compare(password, user.password); + if (!isValid) { + return done(null, false, { message: "Invalid user credentials" }); + } + return done(null, user); + }catch(err){ + return done(err); + } + +})); + + +//When login succeeds this will run +// serialize basically converts user obj into a format that can be transmitted(like a string, etc...) +// here take user obj and done callback and store only userId in session +passport.serializeUser((user, done) => { + done(null, user._id.toString()); +}); + +//When a request comes in, take the stored id, fetch full user from DB, and attach it to req.user. +passport.deserializeUser(async (id, done) => { + try { + let user = await User.findById(id); + if(!user) return done(null, false); + done(null, user); + } catch (err) { + done(err, null); + } +}); + +module.exports = passport; diff --git a/backend/controllers/analyticsController.js b/backend/controllers/analyticsController.js index d7e4e982..47ca6fbc 100644 --- a/backend/controllers/analyticsController.js +++ b/backend/controllers/analyticsController.js @@ -1,7 +1,13 @@ -const {User, Achievement, UserSkill, Event, Position, PositionHolder,OrganizationalUnit}=require('../models/schema'); const mongoose = require("mongoose"); const getCurrentTenureRange = require('../utils/getTenureRange'); +const User = require("../models/userSchema"); +const Achievement = require("../models/achievementSchema"); +const Position = require("../models/positionSchema"); +const PositionHolder = require("../models/positionHolderSchema"); +const OrganizationalUnit = require("../models/organizationSchema"); +const Event = require("../models/eventSchema"); +const { UserSkill } = require("../models/schema"); exports.getPresidentAnalytics= async (req,res) => { try { diff --git a/backend/controllers/certificateController.js b/backend/controllers/certificateController.js new file mode 100644 index 00000000..cfa21a0a --- /dev/null +++ b/backend/controllers/certificateController.js @@ -0,0 +1,158 @@ + +const User = require("../models/userSchema"); +const Position = require("../models/positionSchema"); +const PositionHolder = require("../models/positionHolderSchema"); +const OrganizationalUnit = require("../models/organizationSchema"); +const { CertificateBatch } = require("../models/certificateSchema"); +const { validateBatchSchema, zodObjectId } = require("../utils/batchValidate"); + +async function createBatch(req, res) { + //console.log(req.user); + try{ + const id = req.user.id; + const user = await User.findById(id); + if (!user) { + return res.status(404).json({ messge: "Invalid data (User not found)" }); + } + + if (user.role !== "CLUB_COORDINATOR") { + return res.status(403).json({ message: "Not authorized to perform the task" }); + } + + //to get user club + // positionHolders({user_id: id}) -> positions({_id: position_id}) -> organizationalUnit({_id: unit_id}) -> unit_id = "Club name" + const { title, unit_id, commonData, template_id, users } = req.body; + const validation = validateBatchSchema.safeParse({ + title, + unit_id, + commonData, + template_id, + users, + }); + + if (!validation.success) { + let errors = validation.error.issues.map(issue => issue.message); + return res.status(400).json({ message: errors }); + } + + // Get coordinator's position and unit + const positionHolder = await PositionHolder.findOne({ user_id: id }); + if (!positionHolder) { + return res.status(403).json({ message: "You are not part of any position in a unit" }); + } + + const position = await Position.findById(positionHolder.position_id); + console.log(position._id); + if (!position) { + return res.status(403).json({ message: "Your position is invalid" }); + } + + const userUnitId = position.unit_id.toString(); + if (userUnitId !== unit_id) { + return res + .status(403) + .json({ + message: + "You are not authorized to initiate batches outside of your club", + }); + } + + //const clubId = unit_id; + // Ensure unit_id is a Club + const unitObj = await OrganizationalUnit.findById(unit_id); + if (!unitObj || unitObj.type !== "Club") { + return res + .status(403) + .json({ message: "Invalid Data: unit is not a Club" }); + } + //console.log(unitObj._id); + + // Get council (parent unit) and ensure it's a Council + if (!unitObj.parent_unit_id) { + return res + .status(403) + .json({ message: "Invalid Data: club does not belong to a council" }); + } + //console.log(unitObj.parent_unit_id); + + const councilObj = await OrganizationalUnit.findById(unitObj.parent_unit_id); + if (!councilObj || councilObj.type !== "Council") { + return res.status(403).json({ message: "Invalid Data: council not found" }); + } + + //const councilId = councilObj._id.toString(); + const presidentOrgUnitId = councilObj.parent_unit_id; + const category = councilObj.category.toUpperCase(); + + // Resolve General Secretary and President for the council (server-side, tamper-proof) + const gensecObj = await User.findOne({ role: `GENSEC_${category}` }); + if(!gensecObj){ + return res.status(500).json({ message: "General Secretary not found" }); + } + //console.log(gensecObj._id); + + const presidentPosition = await Position.findOne({ + unit_id: presidentOrgUnitId, + title: /president/i, + }); + if (!presidentPosition) { + return res + .status(500) + .json({ message: "President position not found for council" }); + } + //console.log(presidentPosition._id); + + const presidentHolder = await PositionHolder.findOne({ + position_id: presidentPosition._id, + }); + const presidentId = presidentHolder.user_id.toString(); + //console.log(presidentId); + const presidentObj = await User.findById(presidentId); + + console.log(presidentObj._id); + if (!presidentObj) { + return res.status(500).json({ message: "President not found" }); + } + + const approverIds = [gensecObj._id.toString(), presidentId]; + + const userChecks = await Promise.all( + users.map(async (uid) => { + const validation = zodObjectId.safeParse(uid); + if (!validation) { + return { uid, ok: false, reason: "Invalid ID" }; + } + + const userObj = await User.findById(uid); + if (!userObj) return { uid, ok: false, reason: "User not found" }; + + return { uid, ok: true }; + }), + ); + + const invalidData = userChecks.filter((c) => !c.ok); + if (invalidData.length > 0) { + return res + .status(400) + .json({ message: "Invalid user data sent", details: invalidData }); + } + + const newBatch = await CertificateBatch.create({ + title, + unit_id, + commonData, + templateId: template_id, + initiatedBy: id, + approverIds, + users, + }); + + res.json({ message: "New Batch created successfully", details: newBatch }); + }catch(err){ + res.status(500).json({message: err.message || "Internal server error"}); + } +} + +module.exports = { + createBatch, +}; diff --git a/backend/controllers/dashboardController.js b/backend/controllers/dashboardController.js index 193c6b26..c513aa91 100644 --- a/backend/controllers/dashboardController.js +++ b/backend/controllers/dashboardController.js @@ -1,14 +1,11 @@ // controllers/dashboardController.js -const { - Feedback, - Achievement, - UserSkill, - Skill, - Event, - PositionHolder, - Position, - OrganizationalUnit, -} = require("../models/schema"); +const Feedback = require("../models/feedbackSchema"); +const Achievement = require("../models/achievementSchema"); +const Position = require("../models/positionSchema"); +const PositionHolder = require("../models/positionHolderSchema"); +const OrganizationalUnit = require("../models/organizationSchema"); +const Event = require("../models/eventSchema"); +const { UserSkill, Skill } = require("../models/schema"); const ROLES = { PRESIDENT: "PRESIDENT", diff --git a/backend/controllers/eventControllers.js b/backend/controllers/eventControllers.js index 60832b0e..7c21d13a 100644 --- a/backend/controllers/eventControllers.js +++ b/backend/controllers/eventControllers.js @@ -1,4 +1,4 @@ -const {Event} = require('../models/schema'); +const Event = require('../models/eventSchema'); // fetch 4 most recently updated events exports.getLatestEvents = async (req, res) => { @@ -6,12 +6,15 @@ exports.getLatestEvents = async (req, res) => { const latestEvents = await Event.find({}) .sort({updated_at: -1}) .limit(4) - .select('title updated_at schedule.venue status'); + .select('title updatedAt schedule.venue status'); + if(!latestEvents){ + return res.status(404).json({message: "No events are created"}); + } const formatedEvents =latestEvents.map(event=>({ id: event._id, title: event.title, - date: event.updated_at.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), + date: event.updatedAt?.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), venue: (event.schedule && event.schedule.venue) ? event.schedule.venue : 'TBA', status: event.status || 'TBD' })) diff --git a/backend/index.js b/backend/index.js index 6fa25570..a294c54d 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,13 +1,14 @@ const express = require("express"); require("dotenv").config(); // eslint-disable-next-line node/no-unpublished-require +const { connectDB } = require("./config/db.js"); +const MongoStore = require("connect-mongo"); +const cookieParser = require("cookie-parser"); const cors = require("cors"); const routes_auth = require("./routes/auth"); const routes_general = require("./routes/route"); const session = require("express-session"); -const bodyParser = require("body-parser"); -const { connectDB } = require("./db"); -const myPassport = require("./models/passportConfig"); // Adjust the path accordingly +const myPassport = require("./config/passportConfig.js"); // Adjust the path accordingly const onboardingRoutes = require("./routes/onboarding.js"); const profileRoutes = require("./routes/profile.js"); const feedbackRoutes = require("./routes/feedbackRoutes.js"); @@ -18,8 +19,8 @@ const positionsRoutes = require("./routes/positionRoutes.js"); const organizationalUnitRoutes = require("./routes/orgUnit.js"); const announcementRoutes = require("./routes/announcements.js"); const dashboardRoutes = require("./routes/dashboard.js"); - const analyticsRoutes = require("./routes/analytics.js"); +const certificateRoutes = require("./routes/certificateRoutes.js"); const app = express(); if (process.env.NODE_ENV === "production") { @@ -28,23 +29,32 @@ if (process.env.NODE_ENV === "production") { app.use(cors({ origin: process.env.FRONTEND_URL, credentials: true })); -// Connect to MongoDB -connectDB(); -app.use(bodyParser.json()); +app.use(cookieParser()); + +//Replaced bodyParser with express.json() - the new standard +app.use(express.json()); app.use( session({ - secret: "keyboard cat", + secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === "production", // HTTPS only in prod sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", // cross-origin in prod + maxAge: 60*60*1000 }, + store: MongoStore.create({ + mongoUrl: process.env.MONGODB_URI, + ttl: 60*60*1000, + collectionName: "sessions" + }), + name: "token" }), ); +//Needed to initialize passport and all helper methods to req object app.use(myPassport.initialize()); app.use(myPassport.session()); @@ -65,10 +75,15 @@ app.use("/api/positions", positionsRoutes); app.use("/api/orgUnit", organizationalUnitRoutes); app.use("/api/announcements", announcementRoutes); app.use("/api/dashboard", dashboardRoutes); -app.use("/api/announcements", announcementRoutes); app.use("/api/analytics", analyticsRoutes); +app.use("/api/certificate-batches", certificateRoutes); // Start the server -app.listen(process.env.PORT || 8000, () => { - console.log(`connected to port ${process.env.PORT || 8000}`); -}); + +(async function(){ + // Connect to MongoDB + await connectDB(); + app.listen(process.env.PORT || 5000, () => { + console.log(`connected to port ${process.env.PORT || 5000}`); + }); +})(); diff --git a/backend/middlewares/isAuthenticated.js b/backend/middlewares/isAuthenticated.js index f04c46ef..25d1b169 100644 --- a/backend/middlewares/isAuthenticated.js +++ b/backend/middlewares/isAuthenticated.js @@ -1,7 +1,84 @@ +const jwt = require("jsonwebtoken"); + +//Passport based middleware to check whether the req are coming from authenticated users function isAuthenticated(req, res, next) { if (req.isAuthenticated && req.isAuthenticated()) { return next(); } return res.status(401).json({ message: "Unauthorized: Please login first" }); } -module.exports = isAuthenticated; + +//Token based middleware to check whether the req are coming from authenticated users or not + +function jwtIsAuthenticated(req, res, next) { + let token; + /** + * const headerData = req.headers.authorization; + if (!headerData || !headerData.startsWith("Bearer ")) { + return res.status(401).json({ message: "User not authenticated " }); + } + + token = headerData.split(" ")[1]; + */ + + token = req.cookies.token; + if(!token){ + return res.status(401).json({message: "User not authenticated"}); + } + + try { + const userData = jwt.verify(token, process.env.JWT_SECRET_TOKEN); + req.user = userData; + //console.log(userData); + next(); + } catch (err) { + res.status(401).json({ message: "Invalid or expired token sent" }); + } +} + +module.exports = { + isAuthenticated, + jwtIsAuthenticated, +}; + +/* + +const presidentObj = await User.findById(presidentId); + + console.log(presidentObj._id); + if(!gensecObj || !presidentObj) { + return res.status(500).json({ message: "Approvers not found" }); + } + + const approverIds = [gensecObj._id.toString(), presidentId]; + + const userChecks = await Promise.all( + users.map(async (uid) => { + const validation = zodObjectId.safeParse(uid); + if(!validation){ + return {uid, ok: false, reason:"Invalid ID"}; + } + + const userObj = await User.findById(uid); + if(!userObj) return {uid, ok:false, reason: "User not found"}; + + return {uid, ok: true}; + }) + ); + + const invalidData = userChecks.filter((c) => !c.ok); + if(invalidData.length > 0){ + return res.status(400).json({message: "Invalid user data sent", details: invalidData}); + } + + const newBatch = await CertificateBatch.create({ + title, + unit_id, + commonData, + template_id, + initiatedBy: id, + approverIds, + users + }); + +*/ diff --git a/backend/models/achievementSchema.js b/backend/models/achievementSchema.js new file mode 100644 index 00000000..9dbf6be8 --- /dev/null +++ b/backend/models/achievementSchema.js @@ -0,0 +1,54 @@ +const mongoose = require("mongoose"); +//achievements collection +const achievementSchema = new mongoose.Schema({ + achievement_id: { + type: String, + required: true, + unique: true, + }, + user_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + title: { + type: String, + required: true, + }, + description: String, + category: { + type: String, + required: true, + }, + type: { + type: String, + }, + level: { + type: String, + }, + date_achieved: { + type: Date, + required: true, + }, + position: { + type: String, + }, + event_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "Event", + default: null, // optional + }, + certificate_url: String, + verified: { + type: Boolean, + default: false, + }, + verified_by: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + default: null, + }, +}, { timestamps: true}); + +const Achievement = mongoose.model("Achievement", achievementSchema); +module.exports = Achievement; diff --git a/backend/models/certificateSchema.js b/backend/models/certificateSchema.js new file mode 100644 index 00000000..5cb5fcd6 --- /dev/null +++ b/backend/models/certificateSchema.js @@ -0,0 +1,126 @@ +const mongoose = require("mongoose"); + +const certificateBatchSchema = new mongoose.Schema( + { + title: { type: String, required: true }, + unit_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "Oraganizational_Unit", + }, + commonData: { type: Map, of: String, required: true }, + templateId: { type: String, required: true }, + initiatedBy: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + approverIds: [ + { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, + ], + status: { + type: String, + enum: ["PendingL1", "PendingL2", "Processed", "Rejected", "Processing"], + default: "PendingL1", + }, + users: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }], + }, + { + timestamps: true, + }, +); + +const certificateSchema = new mongoose.Schema( + { + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + batchId: { + type: mongoose.Schema.Types.ObjectId, + ref: "CertificateBatch", + required: true, + }, + status: { + type: String, + required: true, + enum: ["Pending", "Approved", "Rejected"], + default: "Pending", + }, + rejectionReason: { + type: String, + required: function () { + return this.status === "Rejected"; + }, + }, + certificateUrl: { + type: String, + required: function () { + return this.status === "Approved"; + }, + }, + certificateId: { + type: String, + unique: true, + required: function () { + return this.status === "Approved"; + }, + }, + }, + { + timestamps: true, + }, +); + +//Indexed to serve the purpose of "Get pending batches for the logged-in approver." +/* + +_id approverIds status +1 [A, B, C] PendingL1 +2 [B, D] PendingL1 +3 [A, D] PendingL2 +4 [B] PendingL1 + +Index entries for B + +approverIds _id +B 1 +B 2 +B 4 + +*/ +certificateBatchSchema.index( + { approverIds: 1 }, + { partialFilterExpression: { status: { $in: ["PendingL1", "PendingL2"] } } }, +); + +//This is done to ensure that within each batch only 1 certificate is issued per userId. +certificateSchema.index({ batchId: 1, userId: 1 }, { unique: true }); + +//This index is for this purpose -> Get all approved certificates for the logged-in student. + +certificateSchema.index( + { userId: 1, certificateId: 1 }, + { partialFilterExpression: { certificateId: { $exists: true } } }, +); + +const CertificateBatch = mongoose.model( + "CertificateBatch", + certificateBatchSchema, +); +const Certificate = mongoose.model("Certificate", certificateSchema); + +module.exports = { + CertificateBatch, + Certificate, +}; + +/* + +if i use partialFilter when querying i have to specify its filter condition so mongodb uses that index +so here +certificateBatchSchema.index({approverIds: 1}, {partialFilterExpression: { status: {$in: ["PendingL1", "PendingL2"]}}} ) +i need to do +CertificateBatch.find({approverIds: id, status: {$in: ["PendingL1", "PendingL2"]} } ) + +*/ diff --git a/backend/models/eventSchema.js b/backend/models/eventSchema.js new file mode 100644 index 00000000..c51cde37 --- /dev/null +++ b/backend/models/eventSchema.js @@ -0,0 +1,111 @@ +const mongoose = require("mongoose"); + +//events collection +const eventSchema = new mongoose.Schema({ + event_id: { + type: String, + required: true, + unique: true, + }, + title: { + type: String, + required: true, + }, + description: String, + category: { + type: String, + enum: ["cultural", "technical", "sports", "academic", "other"], + }, + type: { + type: String, + }, + organizing_unit_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "Organizational_Unit", + required: true, + }, + organizers: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + }, + ], + schedule: { + start: Date, + end: Date, + venue: String, + mode: { + type: String, + enum: ["online", "offline", "hybrid"], + }, + }, + registration: { + required: Boolean, + start: Date, + end: Date, + fees: Number, + max_participants: Number, + }, + budget: { + allocated: Number, + spent: Number, + sponsors: [ + { + type: String, + }, + ], + }, + status: { + type: String, + enum: ["planned", "ongoing", "completed", "cancelled"], + default: "planned", + }, + participants: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + }, + ], + winners: [ + { + user: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + }, + position: String, // e.g., "1st", "2nd", "Best Speaker", etc. + }, + ], + feedback_summary: { + type: Object, // You can define structure if fixed + }, + media: { + images: [String], + videos: [String], + documents: [String], + }, + room_requests: [ + { + date: { type: Date, required: true }, + time: { type: String, required: true }, + room: { type: String, required: true }, + description: { type: String }, + status: { + type: String, + enum: ["Pending", "Approved", "Rejected"], + default: "Pending", + }, + requested_at: { + type: Date, + default: Date.now, + }, + reviewed_by: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + }, + }, + ], + +}, { timestamps: true}); + +const Event = mongoose.model("Event", eventSchema); +module.exports = Event; diff --git a/backend/models/feedbackSchema.js b/backend/models/feedbackSchema.js new file mode 100644 index 00000000..29abe91f --- /dev/null +++ b/backend/models/feedbackSchema.js @@ -0,0 +1,69 @@ +const mongoose = require("mongoose"); + +//feedback collection +const feedbackSchema = new mongoose.Schema({ + feedback_id: { + type: String, + required: true, + unique: true, + }, + type: { + type: String, + required: true, + }, + target_id: { + type: mongoose.Schema.Types.ObjectId, + //required: true, + // We'll dynamically interpret this field based on target_type + }, + target_type: { + type: String, + required: true, + }, + feedback_by: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + // category: { + // type: String, + // enum: ['organization', 'communication', 'leadership'], + // required: true + // }, + rating: { + type: Number, + min: 1, + max: 5, + }, + comments: { + type: String, + }, + is_anonymous: { + type: Boolean, + default: false, + }, + is_resolved: { + type: Boolean, + default: false, + }, + actions_taken: { + type: String, + default: "", + }, + created_at: { + type: Date, + default: Date.now, + }, + resolved_at: { + type: Date, + default: null, + }, + resolved_by: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + default: null, + }, +}); + +const Feedback = mongoose.model("Feedback", feedbackSchema); +module.exports = Feedback; diff --git a/backend/models/organizationSchema.js b/backend/models/organizationSchema.js new file mode 100644 index 00000000..7bdc0523 --- /dev/null +++ b/backend/models/organizationSchema.js @@ -0,0 +1,77 @@ +const mongoose = require("mongoose"); + +//organizational unit +const organizationalUnitSchema = new mongoose.Schema({ + unit_id: { + type: String, + required: true, + unique: true, + }, + name: { + type: String, + required: true, + unique: true, + }, + type: { + type: String, + enum: ["Council", "Club", "Committee", "independent_position"], + required: true, + }, + description: { + type: String, + }, + parent_unit_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "Organizational_Unit", + default: null, + }, + hierarchy_level: { + type: Number, + required: true, + }, + category: { + type: String, + enum: ["cultural", "scitech", "sports", "academic", "independent"], + required: true, + }, + is_active: { + type: Boolean, + default: true, + }, + contact_info: { + email: { + type: String, + required: true, + unique: true, + }, + social_media: [ + { + platform: { + type: String, + required: true, + }, + url: { + type: String, + required: true, + }, + }, + ], + }, + budget_info: { + allocated_budget: { + type: Number, + default: 0, + }, + spent_amount: { + type: Number, + default: 0, + }, + }, +}, {timestamps: true}); + +const OrganizationalUnit = mongoose.model( + "Organizational_Unit", + organizationalUnitSchema, +); + +module.exports = OrganizationalUnit; diff --git a/backend/models/passportConfig.js b/backend/models/passportConfig.js index 82cb533f..07c9fe8b 100644 --- a/backend/models/passportConfig.js +++ b/backend/models/passportConfig.js @@ -1,17 +1,8 @@ const passport = require("passport"); -const LocalStrategy = require("passport-local"); +//const LocalStrategy = require("passport-local"); const GoogleStrategy = require("passport-google-oauth20"); const isIITBhilaiEmail = require("../utils/isIITBhilaiEmail"); -const { User } = require("./schema"); -// Local Strategy -passport.use( - new LocalStrategy( - { - usernameField: "email", - }, - User.authenticate(), - ), -); +const User = require("./userSchema"); // Google OAuth Strategy passport.use( diff --git a/backend/models/positionHolderSchema.js b/backend/models/positionHolderSchema.js new file mode 100644 index 00000000..65406d03 --- /dev/null +++ b/backend/models/positionHolderSchema.js @@ -0,0 +1,58 @@ +const mongoose = require("mongoose"); + +//position holder collection; +const positionHolderSchema = new mongoose.Schema( + { + por_id: { + type: String, + required: true, + unique: true, + }, + user_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + position_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "Position", + required: true, + }, + + tenure_year: { + type: String, + required: true, + }, + appointment_details: { + appointed_by: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + }, + appointment_date: { + type: Date, + }, + }, + performance_metrics: { + events_organized: { + type: Number, + default: 0, + }, + budget_utilized: { + type: Number, + default: 0, + }, + feedback: { + type: String, + }, + }, + status: { + type: String, + enum: ["active", "completed", "terminated"], + required: true, + }, + }, + { timestamps: true }, +); + +const PositionHolder = mongoose.model("Position_Holder", positionHolderSchema); +module.exports = PositionHolder; diff --git a/backend/models/positionSchema.js b/backend/models/positionSchema.js new file mode 100644 index 00000000..6aa46ec6 --- /dev/null +++ b/backend/models/positionSchema.js @@ -0,0 +1,53 @@ +const mongoose = require("mongoose"); + +//position +const positionSchema = new mongoose.Schema({ + position_id: { + type: String, + required: true, + unique: true, + }, + title: { + type: String, + required: true, + }, + unit_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "Organizational_Unit", + required: true, + }, + position_type: { + type: String, + required: true, + }, + responsibilities: [ + { + type: String, + }, + ], + requirements: { + min_cgpa: { + type: Number, + default: 0, + }, + min_year: { + type: Number, + default: 1, + }, + skills_required: [ + { + type: String, + }, + ], + }, + description: { + type: String, + }, + position_count: { + type: Number, + }, + +}, {timestamps: true}); + +const Position = mongoose.model("Position", positionSchema); +module.exports = Position; diff --git a/backend/models/schema.js b/backend/models/schema.js index 400bc856..34562771 100644 --- a/backend/models/schema.js +++ b/backend/models/schema.js @@ -1,407 +1,6 @@ const mongoose = require("mongoose"); -const passportLocalMongoose = require("passport-local-mongoose"); -var findOrCreate = require("mongoose-findorcreate"); -//user collection - -const userSchema = new mongoose.Schema({ - user_id: { - type: String, - }, - role: { - type: String, - required: true, - }, - strategy: { - type: String, - enum: ["local", "google"], - required: true, - }, - username: { - type: String, - required: true, - unique: true, - }, - onboardingComplete: { - type: Boolean, - default: false, - }, - personal_info: { - name: { - type: String, - required: true, - }, - email: { - type: String, - }, - phone: String, - date_of_birth: Date, - gender: String, - - profilePic: { - type: String, - default: "https://www.gravatar.com/avatar/?d=mp", - }, - - cloudinaryUrl: { - type: String, - default: "", - }, - }, - - academic_info: { - program: { - type: String, - //enum: ["B.Tech", "M.Tech", "PhD", "Msc","other"], - }, - branch: String, - batch_year: String, - current_year: String, - cgpa: Number, - }, - - contact_info: { - hostel: String, - room_number: String, - socialLinks: { - github: { type: String, default: "" }, - linkedin: { type: String, default: "" }, - instagram: { type: String, default: "" }, - other: { type: String, default: "" }, - }, - }, - - status: { - type: String, - enum: ["active", "inactive", "graduated"], - default: "active", - }, - created_at: { - type: Date, - default: Date.now, - }, - updated_at: { - type: Date, - default: Date.now, - }, -}); - -userSchema.index( - { user_id: 1 }, - { - unique: true, - partialFilterExpression: { user_id: { $exists: true, $type: "string" } }, - name: "user_id_partial_unique", - }, -); - -userSchema.plugin(passportLocalMongoose); -userSchema.plugin(findOrCreate); - -//organizational unit -const organizationalUnitSchema = new mongoose.Schema({ - unit_id: { - type: String, - required: true, - unique: true, - }, - name: { - type: String, - required: true, - unique: true, - }, - type: { - type: String, - enum: ["Council", "Club", "Committee", "independent_position"], - required: true, - }, - description: { - type: String, - }, - parent_unit_id: { - type: mongoose.Schema.Types.ObjectId, - ref: "Organizational_Unit", - default: null, - }, - hierarchy_level: { - type: Number, - required: true, - }, - category: { - type: String, - enum: ["cultural", "scitech", "sports", "academic", "independent"], - required: true, - }, - is_active: { - type: Boolean, - default: true, - }, - contact_info: { - email: { - type: String, - required: true, - unique: true, - }, - social_media: [ - { - platform: { - type: String, - required: true, - }, - url: { - type: String, - required: true, - }, - }, - ], - }, - budget_info: { - allocated_budget: { - type: Number, - default: 0, - }, - spent_amount: { - type: Number, - default: 0, - }, - }, - created_at: { - type: Date, - default: Date.now, - }, - updated_at: { - type: Date, - default: Date.now, - }, -}); - -//position - -const positionSchema = new mongoose.Schema({ - position_id: { - type: String, - required: true, - unique: true, - }, - title: { - type: String, - required: true, - }, - unit_id: { - type: mongoose.Schema.Types.ObjectId, - ref: "Organizational_Unit", - required: true, - }, - position_type: { - type: String, - required: true, - }, - responsibilities: [ - { - type: String, - }, - ], - requirements: { - min_cgpa: { - type: Number, - default: 0, - }, - min_year: { - type: Number, - default: 1, - }, - skills_required: [ - { - type: String, - }, - ], - }, - description: { - type: String, - }, - position_count: { - type: Number, - }, - created_at: { - type: Date, - default: Date.now, - }, -}); - -//position holder collection; -const positionHolderSchema = new mongoose.Schema({ - por_id: { - type: String, - required: true, - unique: true, - }, - user_id: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - required: true, - }, - position_id: { - type: mongoose.Schema.Types.ObjectId, - ref: "Position", - required: true, - }, - - tenure_year: { - type: String, - required: true, - }, - appointment_details: { - appointed_by: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - }, - appointment_date: { - type: Date, - }, - }, - performance_metrics: { - events_organized: { - type: Number, - default: 0, - }, - budget_utilized: { - type: Number, - default: 0, - }, - feedback: { - type: String, - }, - }, - status: { - type: String, - enum: ["active", "completed", "terminated"], - required: true, - }, - created_at: { - type: Date, - default: Date.now, - }, - updated_at: { - type: Date, - default: Date.now, - }, -}); - -//events collection -const eventSchema = new mongoose.Schema({ - event_id: { - type: String, - required: true, - unique: true, - }, - title: { - type: String, - required: true, - }, - description: String, - category: { - type: String, - enum: ["cultural", "technical", "sports", "academic", "other"], - }, - type: { - type: String, - }, - organizing_unit_id: { - type: mongoose.Schema.Types.ObjectId, - ref: "Organizational_Unit", - required: true, - }, - organizers: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - }, - ], - schedule: { - start: Date, - end: Date, - venue: String, - mode: { - type: String, - enum: ["online", "offline", "hybrid"], - }, - }, - registration: { - required: Boolean, - start: Date, - end: Date, - fees: Number, - max_participants: Number, - }, - budget: { - allocated: Number, - spent: Number, - sponsors: [ - { - type: String, - }, - ], - }, - status: { - type: String, - enum: ["planned", "ongoing", "completed", "cancelled"], - default: "planned", - }, - participants: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - }, - ], - winners: [ - { - user: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - }, - position: String, // e.g., "1st", "2nd", "Best Speaker", etc. - }, - ], - feedback_summary: { - type: Object, // You can define structure if fixed - }, - media: { - images: [String], - videos: [String], - documents: [String], - }, - room_requests: [ - { - date: { type: Date, required: true }, - time: { type: String, required: true }, - room: { type: String, required: true }, - description: { type: String }, - status: { - type: String, - enum: ["Pending", "Approved", "Rejected"], - default: "Pending", - }, - requested_at: { - type: Date, - default: Date.now, - }, - reviewed_by: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - }, - }, - ], - created_at: { - type: Date, - default: Date.now, - }, - updated_at: { - type: Date, - default: Date.now, - }, -}); //skill collection - const skillSchema = new mongoose.Schema({ skill_id: { type: String, @@ -473,126 +72,6 @@ const userSkillSchema = new mongoose.Schema({ }, }); -//achievements collection -const achievementSchema = new mongoose.Schema({ - achievement_id: { - type: String, - required: true, - unique: true, - }, - user_id: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - required: true, - }, - title: { - type: String, - required: true, - }, - description: String, - category: { - type: String, - required: true, - }, - type: { - type: String, - }, - level: { - type: String, - }, - date_achieved: { - type: Date, - required: true, - }, - position: { - type: String, - }, - event_id: { - type: mongoose.Schema.Types.ObjectId, - ref: "Event", - default: null, // optional - }, - certificate_url: String, - verified: { - type: Boolean, - default: false, - }, - verified_by: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - default: null, - }, - created_at: { - type: Date, - default: Date.now, - }, -}); - -//feedback collection -const feedbackSchema = new mongoose.Schema({ - feedback_id: { - type: String, - required: true, - unique: true, - }, - type: { - type: String, - required: true, - }, - target_id: { - type: mongoose.Schema.Types.ObjectId, - //required: true, - // We'll dynamically interpret this field based on target_type - }, - target_type: { - type: String, - required: true, - }, - feedback_by: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - required: true, - }, - // category: { - // type: String, - // enum: ['organization', 'communication', 'leadership'], - // required: true - // }, - rating: { - type: Number, - min: 1, - max: 5, - }, - comments: { - type: String, - }, - is_anonymous: { - type: Boolean, - default: false, - }, - is_resolved: { - type: Boolean, - default: false, - }, - actions_taken: { - type: String, - default: "", - }, - created_at: { - type: Date, - default: Date.now, - }, - resolved_at: { - type: Date, - default: null, - }, - resolved_by: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - default: null, - }, -}); - //announcement collection const announcementSchema = new mongoose.Schema({ title: { @@ -621,39 +100,15 @@ const announcementSchema = new mongoose.Schema({ type: Boolean, default: false, }, - createdAt: { - type: Date, - default: Date.now, - }, - updatedAt: { - type: Date, - default: Date.now, - }, -}); -const User = mongoose.model("User", userSchema); -const Feedback = mongoose.model("Feedback", feedbackSchema); -const Achievement = mongoose.model("Achievement", achievementSchema); +}, { timestamps: true}); + const UserSkill = mongoose.model("User_Skill", userSkillSchema); const Skill = mongoose.model("Skill", skillSchema); -const Event = mongoose.model("Event", eventSchema); -const PositionHolder = mongoose.model("Position_Holder", positionHolderSchema); -const Position = mongoose.model("Position", positionSchema); -const OrganizationalUnit = mongoose.model( - "Organizational_Unit", - organizationalUnitSchema, -); const Announcement = mongoose.model("Announcement", announcementSchema); module.exports = { - User, - Feedback, - Achievement, UserSkill, Skill, - Event, - PositionHolder, - Position, - OrganizationalUnit, Announcement, }; diff --git a/backend/models/userSchema.js b/backend/models/userSchema.js new file mode 100644 index 00000000..560e65bf --- /dev/null +++ b/backend/models/userSchema.js @@ -0,0 +1,108 @@ +const mongoose = require("mongoose"); +const bcrypt = require("bcrypt"); +require("dotenv").config(); + +const userSchema = new mongoose.Schema( + { + user_id: { + type: String, + }, + strategy: { + type: String, + enum: ["local", "google"], + required: true, + }, + role: { + type: String, + default: "STUDENT" + }, + username: { + type: String, + required: true, + unique: true, + }, + password: { + type: String, + required: function () { + return this.strategy === "local"; + }, + minLength: 8, + }, + onboardingComplete: { + type: Boolean, + default: false, + }, + personal_info: { + name: { + type: String, + required: true, + }, + email: { + type: String, + unique: true, + required: true, + }, + phone: String, + date_of_birth: Date, + gender: String, + + profilePic: { + type: String, + default: "https://www.gravatar.com/avatar/?d=mp", + }, + + cloudinaryUrl: { + type: String, + default: "", + }, + }, + + academic_info: { + program: { + type: String, + //enum: ["B.Tech", "M.Tech", "PhD", "Msc","other"], + }, + branch: String, + batch_year: String, + current_year: String, + cgpa: Number, + }, + + contact_info: { + hostel: String, + room_number: String, + socialLinks: { + github: { type: String, default: "" }, + linkedin: { type: String, default: "" }, + instagram: { type: String, default: "" }, + other: { type: String, default: "" }, + }, + }, + + status: { + type: String, + enum: ["active", "inactive", "graduated"], + default: "active", + }, + }, + { + timestamps: true, + }, +); + +userSchema.index( + { user_id: 1 }, + { + unique: true, + partialFilterExpression: { user_id: { $exists: true, $type: "string" } }, + name: "user_id_partial_unique", + }, +); + +userSchema.pre("save", async function () { + if (!this.isModified("password")) return; + const SALT_ROUNDS = Number(process.env.SALT) || 12 + this.password = await bcrypt.hash(this.password, SALT_ROUNDS); +}); +const User = mongoose.model("User", userSchema); +module.exports = User; diff --git a/backend/package-lock.json b/backend/package-lock.json index 1d66737d..0f32a4da 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,9 +10,11 @@ "license": "ISC", "dependencies": { "axios": "^1.5.1", - "body-parser": "^1.20.2", + "bcrypt": "^6.0.0", "cloudinary": "^2.6.1", + "connect-mongo": "^5.1.0", "connect-mongodb-session": "^3.1.1", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.3.2", "eslint-plugin-react": "^7.33.2", @@ -25,7 +27,6 @@ "moment": "^2.30.1", "mongodb": "^6.1.0", "mongoose": "^7.6.8", - "mongoose-findorcreate": "^4.0.0", "morgan": "^1.10.0", "multer": "^2.0.1", "nodemailer": "^7.0.3", @@ -34,18 +35,16 @@ "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", "passport-local": "^1.0.0", - "passport-local-mongoose": "^8.0.0", "streamifier": "^0.1.1", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "zod": "^4.3.6" }, "devDependencies": { "cookie": "^0.5.0", - "cookie-parser": "^1.4.6", "eslint": "^8.56.0", "eslint-plugin-node": "^11.1.0", "husky": "^8.0.3", "lint-staged": "^15.2.0", - "parser": "^0.1.4", "prettier": "^3.1.1" } }, @@ -1721,6 +1720,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/asynciterator.prototype": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", @@ -1803,6 +1814,20 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1811,28 +1836,11 @@ "node": ">=8" } }, - "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } + "node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" }, "node_modules/bowser": { "version": "2.11.0", @@ -2100,6 +2108,46 @@ "typedarray": "^0.0.6" } }, + "node_modules/connect-mongo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-5.1.0.tgz", + "integrity": "sha512-xT0vxQLqyqoUTxPLzlP9a/u+vir0zNkhiy9uAdHjSCcUUf7TS5b55Icw8lVyYFxfemP3Mf9gdwUOgeF3cxCAhw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.1", + "kruptein": "^3.0.0" + }, + "engines": { + "node": ">=12.9.0" + }, + "peerDependencies": { + "express-session": "^1.17.1", + "mongodb": ">= 5.1.0 < 7" + } + }, + "node_modules/connect-mongo/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/connect-mongo/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/connect-mongodb-session": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/connect-mongodb-session/-/connect-mongodb-session-3.1.1.tgz", @@ -2206,12 +2254,12 @@ } }, "node_modules/cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", - "dev": true, + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", "dependencies": { - "cookie": "0.4.1", + "cookie": "0.7.2", "cookie-signature": "1.0.6" }, "engines": { @@ -2219,10 +2267,10 @@ } }, "node_modules/cookie-parser/node_modules/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", - "dev": true, + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2325,12 +2373,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/disect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/disect/-/disect-1.1.1.tgz", - "integrity": "sha512-rr2Ym8FSAoqAJ1KfpUiQ/Io01HP0LZPHBuppbFsHozmSNf+YwrvyD5pm5tMTUApJFNwD7HeWJ5DGldSugScukA==", - "dev": true - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -3166,11 +3208,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/generaterr": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/generaterr/-/generaterr-1.5.0.tgz", - "integrity": "sha512-JgcGRv2yUKeboLvvNrq9Bm90P4iJBu7/vd5wSLYqMG5GJ6SxZT46LAAkMfNhQ+EK3jzC+cRBm7P8aUWYyphgcQ==" - }, "node_modules/get-east-asian-width": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", @@ -3989,6 +4026,18 @@ "json-buffer": "3.0.1" } }, + "node_modules/kruptein": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.2.0.tgz", + "integrity": "sha512-Bcou7bKBn3k2ZEDXyYzR/j7YWWFDIcqv0ZeabHHPWW1aYmfLn0qmJJoWPVeQvh37g6vl2x3nEO9guBSzJsmuMQ==", + "license": "MIT", + "dependencies": { + "asn1.js": "^5.4.1" + }, + "engines": { + "node": ">8" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4357,6 +4406,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4473,11 +4528,6 @@ "url": "https://opencollective.com/mongoose" } }, - "node_modules/mongoose-findorcreate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mongoose-findorcreate/-/mongoose-findorcreate-4.0.0.tgz", - "integrity": "sha512-wi0vrTmazWBeZn8wHVdb8NEa+ZrAbnmfI8QltnFeIgvC33VlnooapvPSk21W22IEhs0vZ0cBz0MmXcc7eTTSZQ==" - }, "node_modules/mongoose/node_modules/@types/whatwg-url": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", @@ -4682,6 +4732,26 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/nodemailer": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz", @@ -7753,18 +7823,6 @@ "node": ">=6" } }, - "node_modules/parser": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/parser/-/parser-0.1.4.tgz", - "integrity": "sha512-f6EM/mBtPzmIh96MpcbePfhkBOYRmLYWuOukJqMysMlvjp4s2MQSSQnFEekd9GV4JGTnDJ2uFt3Ztcqc9wCMJg==", - "dev": true, - "dependencies": { - "tokenizer": "*" - }, - "engines": { - "node": "0.4-0.9" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -7812,19 +7870,6 @@ "node": ">= 0.4.0" } }, - "node_modules/passport-local-mongoose": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/passport-local-mongoose/-/passport-local-mongoose-8.0.0.tgz", - "integrity": "sha512-jgfN/B0j11WT5f96QlL5EBvxbIwmzd+tbwPzG1Vk8hzDOF68jrch5M+NFvrHjWjb3lfAU0DkxKmNRT9BjFZysQ==", - "dependencies": { - "generaterr": "^1.5.0", - "passport-local": "^1.0.0", - "scmp": "^2.1.0" - }, - "engines": { - "node": ">= 8.0.0" - } - }, "node_modules/passport-oauth2": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", @@ -8037,20 +8082,6 @@ "node": ">= 0.6" } }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -8303,11 +8334,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "node_modules/scmp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", - "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -8756,18 +8782,6 @@ "node": ">=0.6" } }, - "node_modules/tokenizer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/tokenizer/-/tokenizer-1.1.2.tgz", - "integrity": "sha512-c/EYsBwEW/EX28q44UaSrJ9o5M2aI+N/xdJJ4Zl7dNq76OmWQHhmXH0T8DJQNjVYPc7NclV2CZQfyeUMfnEu/A==", - "dev": true, - "dependencies": { - "disect": "~1.1.0" - }, - "engines": { - "node": "0.10.x" - } - }, "node_modules/touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -9188,6 +9202,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/backend/package.json b/backend/package.json index 3814a629..41b9bf11 100644 --- a/backend/package.json +++ b/backend/package.json @@ -30,9 +30,11 @@ "license": "ISC", "dependencies": { "axios": "^1.5.1", - "body-parser": "^1.20.2", + "bcrypt": "^6.0.0", "cloudinary": "^2.6.1", + "connect-mongo": "^5.1.0", "connect-mongodb-session": "^3.1.1", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.3.2", "eslint-plugin-react": "^7.33.2", @@ -45,7 +47,6 @@ "moment": "^2.30.1", "mongodb": "^6.1.0", "mongoose": "^7.6.8", - "mongoose-findorcreate": "^4.0.0", "morgan": "^1.10.0", "multer": "^2.0.1", "nodemailer": "^7.0.3", @@ -54,18 +55,16 @@ "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", "passport-local": "^1.0.0", - "passport-local-mongoose": "^8.0.0", "streamifier": "^0.1.1", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "zod": "^4.3.6" }, "devDependencies": { "cookie": "^0.5.0", - "cookie-parser": "^1.4.6", "eslint": "^8.56.0", "eslint-plugin-node": "^11.1.0", "husky": "^8.0.3", "lint-staged": "^15.2.0", - "parser": "^0.1.4", "prettier": "^3.1.1" } } diff --git a/backend/routes/achievements.js b/backend/routes/achievements.js index 026bc288..d3b4a29b 100644 --- a/backend/routes/achievements.js +++ b/backend/routes/achievements.js @@ -1,8 +1,8 @@ const express = require("express"); const router = express.Router(); -const { Achievement } = require("../models/schema"); // Update path as needed +const Achievement = require("../models/achievementSchema"); // Update path as needed const { v4: uuidv4 } = require("uuid"); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); const authorizeRole = require("../middlewares/authorizeRole"); const { ROLE_GROUPS } = require("../utils/roles"); diff --git a/backend/routes/analytics.js b/backend/routes/analytics.js index bf31fc49..981745aa 100644 --- a/backend/routes/analytics.js +++ b/backend/routes/analytics.js @@ -1,20 +1,40 @@ -const express = require('express'); +const express = require("express"); const router = express.Router(); -const controller = require('../controllers/analyticsController'); -const isAuthenticated = require('../middlewares/isAuthenticated'); -const authorizeRole = require('../middlewares/authorizeRole'); -const {ROLE_GROUPS} = require('../utils/roles'); +const controller = require("../controllers/analyticsController"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); +const authorizeRole = require("../middlewares/authorizeRole"); +const { ROLE_GROUPS } = require("../utils/roles"); // Route to get analytics for president -router.get('/president', isAuthenticated, authorizeRole(['PRESIDENT']), controller.getPresidentAnalytics); +router.get( + "/president", + isAuthenticated, + authorizeRole(["PRESIDENT"]), + controller.getPresidentAnalytics, +); // Route to get analytics for gensecs -router.get('/gensec', isAuthenticated,authorizeRole([...ROLE_GROUPS.GENSECS]), controller.getGensecAnalytics); +router.get( + "/gensec", + isAuthenticated, + authorizeRole([...ROLE_GROUPS.GENSECS]), + controller.getGensecAnalytics, +); // Route to get analytics for club coordinators -router.get('/club-coordinator',authorizeRole(['CLUB_COORDINATOR']), isAuthenticated, controller.getClubCoordinatorAnalytics); +router.get( + "/club-coordinator", + authorizeRole(["CLUB_COORDINATOR"]), + isAuthenticated, + controller.getClubCoordinatorAnalytics, +); // Route to get analytics for students -router.get('/student', isAuthenticated,authorizeRole(['STUDENT']), controller.getStudentAnalytics); +router.get( + "/student", + isAuthenticated, + authorizeRole(["STUDENT"]), + controller.getStudentAnalytics, +); module.exports = router; diff --git a/backend/routes/announcements.js b/backend/routes/announcements.js index c4f5ae9f..25f62154 100644 --- a/backend/routes/announcements.js +++ b/backend/routes/announcements.js @@ -7,7 +7,7 @@ const { OrganizationalUnit, Position, } = require("../models/schema"); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); const findTargetId = async (type, identifier) => { let target = null; diff --git a/backend/routes/auth.js b/backend/routes/auth.js index c2ee6f7b..4059599d 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,13 +1,16 @@ const express = require("express"); const router = express.Router(); const jwt = require("jsonwebtoken"); -//const secretKey = process.env.JWT_SECRET_TOKEN; -const isIITBhilaiEmail = require("../utils/isIITBhilaiEmail"); -const passport = require("../models/passportConfig"); + +const { registerValidate } = require("../utils/authValidate"); +const passport = require("../config/passportConfig"); const rateLimit = require("express-rate-limit"); var nodemailer = require("nodemailer"); -const { User } = require("../models/schema"); -const isAuthenticated= require("../middlewares/isAuthenticated"); + +const User = require("../models/userSchema"); +const {isAuthenticated}= require("../middlewares/isAuthenticated"); + +//const bcrypt = require("bcrypt"); //rate limiter - for password reset try const forgotPasswordLimiter = rateLimit({ @@ -17,67 +20,97 @@ const forgotPasswordLimiter = rateLimit({ }); // Session Status -router.get("/fetchAuth",isAuthenticated, function (req, res) { - if (req.isAuthenticated()) { - res.json(req.user); - } else { - res.json(null); - } +router.get("/fetchAuth", isAuthenticated, function (req, res) { + const {personal_info, role, onboardingComplete, user_id, ...restData} = req.user; + res.json({message: {personal_info, role, onboardingComplete, user_id}, success: true}); }); -// Local Authentication -router.post("/login", passport.authenticate("local"), (req, res) => { - // If authentication is successful, this function will be called - const email = req.user.username; - if (!isIITBhilaiEmail(email)) { - console.log("Access denied. Please use your IIT Bhilai email."); - return res.status(403).json({ - message: "Access denied. Please use your IIT Bhilai email.", - }); +/** + * User POST /auth/login + ↓ + passport.authenticate("local") + ↓ + LocalStrategy (validate credentials) + ↓ + done(null, user) + ↓ + req.login(user) called + ↓ + serializeUser(user) → store ID in session + ↓ + Session saved → session cookie sent + */ +router.post("/login", async (req, res) => { + try { + + passport.authenticate("local", (err, user, info) => { + + if(err){ + console.error(err); + return res.status(500).json({message: "Internal server error"}); + } + + if(!user) return res.status(401).json({message: info?.message || "Login failed"}); + + // if using a custom callback like this u have to manually call req.login() else not needed + //this will seralize user, store id in session, save session and send cookie + req.login(user, (err)=>{ + if(err) return res.status(500).json({message: "Internal server error"}); + const {personal_info, role, onboardingComplete, ...restData} = user; + return res.json({message: "Login Successful", success: true, data: {personal_info, role, onboardingComplete}}); + }) + })(req,res); + + } catch (err) { + return res.status(500).json({ message: err.message }); } - res.status(200).json({ message: "Login successful", user: req.user }); }); router.post("/register", async (req, res) => { try { - const { name, ID, email, password } = req.body; - if (!isIITBhilaiEmail(email)) { - return res.status(400).json({ - message: "Invalid email address. Please use an IIT Bhilai email.", - }); + const { username, password, name } = req.body; + const role = "STUDENT"; + const result = registerValidate.safeParse({ + username, + password, + name, + role, + }); + + if (!result.success) { + const errors = result.error.issues.map(issue => issue.message); + return res.status(400).json({ message: errors, success: false}); } - const existingUser = await User.findOne({ username: email }); - if (existingUser) { - return res.status(400).json({ message: "User already exists." }); + + const user = await User.findOne({ username }); + if (user) { + return res.status(409).json({ message: "Account with username already exists", success: false}); } - const newUser = await User.register( - new User({ - user_id: ID, - role: "STUDENT", - strategy: "local", - username: email, - personal_info: { - name: name, - email: email, - }, - onboardingComplete: false, - }), + /** + * This logic is now embedded in the pre save hook + * const hashedPassword = await bcrypt.hash( password, + Number(process.env.SALT), ); + */ - req.login(newUser, (err) => { - if (err) { - console.error(err); - return res.status(400).json({ message: "Bad request." }); - } - return res - .status(200) - .json({ message: "Registration successful", user: newUser }); + const newUser = await User.create({ + strategy: "local", + username, + password, + personal_info: { + name, + email: username, + }, + role }); - } catch (error) { - console.error("Registration error:", error); - return res.status(500).json({ message: "Internal server error" }); + //console.log(newUser); + + //return res.json({ message: "Registered Successfully", user: newUser }); + return res.json({ message: "Registered Successfully", success: true }); + } catch (err) { + return res.status(500).json({ message: err.message }); } }); @@ -87,17 +120,42 @@ router.get( passport.authenticate("google", { scope: ["profile", "email"] }), ); -router.get( - "/google/verify", - passport.authenticate("google", { failureRedirect: "/" }), - (req, res) => { - if (req.user.onboardingComplete) { - res.redirect(`${process.env.FRONTEND_URL}/`); - } else { - res.redirect(`${process.env.FRONTEND_URL}/onboarding`); +router.get("/google/verify", function(req, res){ + //console.log("in verify"); + passport.authenticate("google",(err, user, info)=>{ + if(err){ + console.error(err); + return res.status(500).json({message: "Internal server error"}); } - }, -); + + if(!user) return res.status(401).json({message: info?.message || "Google Authentication failed"}); + + /** + * if(!user.onboardingComplete){ + return res.redirect(`${process.env.FRONTEND_URL}/onboarding`) + } + */ + //return res.redirect(`${process.env.FRONTEND_URL}`); + + req.login(user, (loginErr) => { + if(loginErr) { + console.error("Login error:", loginErr); + return res.status(500).json({message: "Error establishing session"}); + } + + /*console.log("User logged in successfully:", user.username); + console.log("OnboardingComplete:", user.onboardingComplete); + */ + if(!user.onboardingComplete){ + //console.log("Redirecting to onboarding"); + return res.redirect(`${process.env.FRONTEND_URL}/onboarding`); + } + + //console.log("Redirecting to home"); + return res.redirect(`${process.env.FRONTEND_URL}`); + }) + })(req, res) +}); router.post("/logout", (req, res, next) => { req.logout(function (err) { @@ -151,7 +209,7 @@ router.post("/forgot-password", forgotPasswordLimiter, async (req, res) => { .json({ message: "Password reset link sent to your email" }); } }); - console.log(link); + //console.log(link); } catch (error) { console.log(error); return res.status(500).json({ message: "Internal server error" }); @@ -160,14 +218,14 @@ router.post("/forgot-password", forgotPasswordLimiter, async (req, res) => { //route for password reset router.get("/reset-password/:id/:token", async (req, res) => { - const { id, token } = req.params; - console.log(req.params); - const user = await User.findOne({ _id: id }); - if (!user) { - return res.status(404).json({ message: "User not found" }); - } - const secret = user._id + process.env.JWT_SECRET_TOKEN; - try { + try{ + 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; jwt.verify(token, secret); return res.status(200).json({ message: "Token verified successfully" }); } catch (error) { diff --git a/backend/routes/certificateRoutes.js b/backend/routes/certificateRoutes.js new file mode 100644 index 00000000..0116d033 --- /dev/null +++ b/backend/routes/certificateRoutes.js @@ -0,0 +1,8 @@ +const router = require("express").Router(); +const { createBatch } = require("../controllers/certificateController"); + +const { jwtIsAuthenticated } = require("../middlewares/isAuthenticated"); + +router.post("/", jwtIsAuthenticated, createBatch); + +module.exports = router; diff --git a/backend/routes/dashboard.js b/backend/routes/dashboard.js index 43846500..2ce8a818 100644 --- a/backend/routes/dashboard.js +++ b/backend/routes/dashboard.js @@ -1,8 +1,8 @@ -const express = require('express'); +const express = require("express"); const router = express.Router(); -const dashboardController = require('../controllers/dashboardController'); -const isAuthenticated = require('../middlewares/isAuthenticated'); +const dashboardController = require("../controllers/dashboardController"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); -router.get('/stats',isAuthenticated, dashboardController.getDashboardStats); +router.get("/stats", isAuthenticated, dashboardController.getDashboardStats); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/routes/events.js b/backend/routes/events.js index 4bf5dd92..452ecf28 100644 --- a/backend/routes/events.js +++ b/backend/routes/events.js @@ -1,8 +1,10 @@ const express = require("express"); const router = express.Router(); -const { Event, User, OrganizationalUnit } = require("../models/schema"); +const OrganizationalUnit = require("../models/organizationSchema"); +const Event = require("../models/eventSchema"); +const User = require("../models/userSchema"); const { v4: uuidv4 } = require("uuid"); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); const isEventContact = require("../middlewares/isEventContact"); const authorizeRole = require("../middlewares/authorizeRole"); const { ROLE_GROUPS, ROLES } = require("../utils/roles"); @@ -221,7 +223,6 @@ router.post( return res.status(400).json({ message: "Registration has ended." }); } - const maxParticipants = event.registration.max_participants; if (maxParticipants) { const updatedEvent = await Event.findOneAndUpdate( @@ -255,8 +256,8 @@ router.post( event: updatedEvent, }); } catch (error) { - if (error?.name === "CastError") { - return res.status(400).json({ message: "Invalid event ID format." }); + if (error.name === "CastError") { + return res.status(400).json({ message: "Invalid event ID format." }); } console.error("Event registration error:", error); return res diff --git a/backend/routes/feedbackRoutes.js b/backend/routes/feedbackRoutes.js index d3e52386..94ca51ee 100644 --- a/backend/routes/feedbackRoutes.js +++ b/backend/routes/feedbackRoutes.js @@ -1,18 +1,16 @@ const express = require("express"); const router = express.Router(); -const isAuthenticated = require("../middlewares/isAuthenticated"); -const { - User, - Feedback, - Event, - Position, - OrganizationalUnit, -} = require("./../models/schema"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); +const OrganizationalUnit = require("../models/organizationSchema"); +const Event = require("../models/eventSchema"); +const User = require("../models/userSchema"); +const Feedback = require("../models/feedbackSchema"); +const Position = require("../models/positionSchema"); const { v4: uuidv4 } = require("uuid"); const authorizeRole = require("../middlewares/authorizeRole"); const { ROLE_GROUPS } = require("../utils/roles"); -router.post("/add",isAuthenticated, async (req, res) => { +router.post("/add", isAuthenticated, async (req, res) => { try { const { type, @@ -28,19 +26,19 @@ router.post("/add",isAuthenticated, async (req, res) => { return res.status(400).json({ message: "Missing required fields" }); } - const targetModels={ + const targetModels = { User, Event, "Club/Organization": OrganizationalUnit, POR: Position, }; - const TargetModel=targetModels[target_type]; + const TargetModel = targetModels[target_type]; - if(!TargetModel){ - return res.status(400).json({message:"Invalid target type"}); + if (!TargetModel) { + return res.status(400).json({ message: "Invalid target type" }); } - + const feedback = new Feedback({ feedback_id: uuidv4(), type, @@ -63,9 +61,12 @@ router.post("/add",isAuthenticated, async (req, res) => { } }); -router.get("/get-targetid",isAuthenticated, async (req, res) => { +router.get("/get-targetid", isAuthenticated, async (req, res) => { try { - const users = await User.find({role: "STUDENT"}, "_id user_id personal_info.name"); + const users = await User.find( + { role: "STUDENT" }, + "_id user_id personal_info.name", + ); const events = await Event.find({}, "_id title"); const organizational_units = await OrganizationalUnit.find({}, "_id name"); const positions = await Position.find({}) @@ -178,42 +179,47 @@ router.get("/view-feedback", async (req, res) => { }); // requires user middleware that attaches user info to req.user -router.put("/mark-resolved/:id",isAuthenticated,authorizeRole(ROLE_GROUPS.ADMIN), async (req, res) => { - const feedbackId = req.params.id; - const { actions_taken, resolved_by } = req.body; - console.log(req.body); - console.log("User resolving feedback:", resolved_by); - - if (!actions_taken || actions_taken.trim() === "") { - return res.status(400).json({ error: "Resolution comment is required." }); - } - - try { - const feedback = await Feedback.findById(feedbackId); - if (!feedback) { - return res.status(404).json({ error: "Feedback not found" }); +router.put( + "/mark-resolved/:id", + isAuthenticated, + authorizeRole(ROLE_GROUPS.ADMIN), + async (req, res) => { + const feedbackId = req.params.id; + const { actions_taken, resolved_by } = req.body; + console.log(req.body); + console.log("User resolving feedback:", resolved_by); + + if (!actions_taken || actions_taken.trim() === "") { + return res.status(400).json({ error: "Resolution comment is required." }); } - if (feedback.is_resolved) { - return res.status(400).json({ error: "Feedback is already resolved." }); - } + try { + const feedback = await Feedback.findById(feedbackId); + if (!feedback) { + return res.status(404).json({ error: "Feedback not found" }); + } - feedback.is_resolved = true; - feedback.resolved_at = new Date(); - feedback.actions_taken = actions_taken; - feedback.resolved_by = resolved_by; + if (feedback.is_resolved) { + return res.status(400).json({ error: "Feedback is already resolved." }); + } - await feedback.save(); + feedback.is_resolved = true; + feedback.resolved_at = new Date(); + feedback.actions_taken = actions_taken; + feedback.resolved_by = resolved_by; - res.json({ success: true, message: "Feedback marked as resolved." }); - } catch (err) { - console.error("Error updating feedback:", err); - res.status(500).json({ error: "Server error" }); - } -}); + await feedback.save(); + + res.json({ success: true, message: "Feedback marked as resolved." }); + } catch (err) { + console.error("Error updating feedback:", err); + res.status(500).json({ error: "Server error" }); + } + }, +); //get all user given feedbacks -router.get("/:userId",isAuthenticated, async (req, res) => { +router.get("/:userId", isAuthenticated, async (req, res) => { const userId = req.params.userId; try { const userFeedbacks = await Feedback.find({ feedback_by: userId }).populate( diff --git a/backend/routes/onboarding.js b/backend/routes/onboarding.js index dca690d2..14905a90 100644 --- a/backend/routes/onboarding.js +++ b/backend/routes/onboarding.js @@ -1,18 +1,17 @@ const express = require("express"); const router = express.Router(); -const { User } = require("../models/schema"); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const User = require("../models/userSchema"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); // Onboarding route - to be called when user logs in for the first time -router.post("/",isAuthenticated, async (req, res) => { - const { ID_No, add_year, Program, discipline, mobile_no } = req.body; +router.put("/", isAuthenticated, async (req, res) => { + const { add_year, Program, discipline, mobile_no } = req.body; try { - console.log(req.user); + //console.log(req.user); const updatedUser = await User.findByIdAndUpdate( req.user._id, { - user_id: ID_No, onboardingComplete: true, personal_info: Object.assign({}, req.user.personal_info, { phone: mobile_no || "", @@ -28,11 +27,11 @@ router.post("/",isAuthenticated, async (req, res) => { { new: true, runValidators: true }, ); - console.log("Onboarding completed for user:", updatedUser._id); + //console.log("Onboarding completed for user:", updatedUser._id); res.status(200).json({ message: "Onboarding completed successfully" }); } catch (error) { - console.error("Onboarding failed:", error); - res.status(500).json({ message: "Onboarding failed", error }); + console.error("Onboarding failed:", error.message); + res.status(500).json({ message: error.message || "Onboarding failed" }); } }); diff --git a/backend/routes/orgUnit.js b/backend/routes/orgUnit.js index 2c71597b..3b19fd2a 100644 --- a/backend/routes/orgUnit.js +++ b/backend/routes/orgUnit.js @@ -12,7 +12,7 @@ const { Feedback, User, } = require("../models/schema"); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); const authorizeRole = require("../middlewares/authorizeRole"); const { ROLE_GROUPS } = require("../utils/roles"); diff --git a/backend/routes/positionRoutes.js b/backend/routes/positionRoutes.js index d25e32f4..eb5602a1 100644 --- a/backend/routes/positionRoutes.js +++ b/backend/routes/positionRoutes.js @@ -1,8 +1,9 @@ const express = require("express"); const router = express.Router(); -const { Position, PositionHolder } = require("../models/schema"); +const Position = require("../models/positionSchema"); +const PositionHolder = require("../models/positionHolderSchema"); const { v4: uuidv4 } = require("uuid"); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); // POST for adding a new position router.post("/add-position", isAuthenticated, async (req, res) => { diff --git a/backend/routes/profile.js b/backend/routes/profile.js index db94cae5..9aed783e 100644 --- a/backend/routes/profile.js +++ b/backend/routes/profile.js @@ -4,9 +4,9 @@ const router = express.Router(); const upload = require("../middlewares/upload"); const cloudinary = require("cloudinary").v2; //const { Student } = require("../models/student"); -const { User } = require("../models/schema"); +const { User } = require("../models/userSchema"); const streamifier = require("streamifier"); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); // Cloudinary config cloudinary.config({ cloud_name: process.env.CLOUDINARY_CLOUD_NAME, @@ -15,15 +15,20 @@ cloudinary.config({ }); router.post( - "/photo-update",isAuthenticated, + "/photo-update", + isAuthenticated, upload.fields([{ name: "image" }]), async (req, res) => { try { const { ID_No } = req.body; - if (!ID_No) { return res.status(400).json({ error: "ID_No is required" }); } + if (!ID_No) { + return res.status(400).json({ error: "ID_No is required" }); + } const user = await User.findOne({ user_id: ID_No }); - if (!user) { return res.status(404).json({ error: "User not found" });} + if (!user) { + return res.status(404).json({ error: "User not found" }); + } if ( !req.files || @@ -46,8 +51,11 @@ router.post( let stream = cloudinary.uploader.upload_stream( { folder: "profile-photos" }, (error, result) => { - if (result) { resolve(result);} - else { reject(error); } + if (result) { + resolve(result); + } else { + reject(error); + } }, ); streamifier.createReadStream(fileBuffer).pipe(stream); @@ -69,13 +77,17 @@ router.post( ); // Delete profile photo (reset to default) -router.delete("/photo-delete",isAuthenticated, async (req, res) => { +router.delete("/photo-delete", isAuthenticated, async (req, res) => { try { const { ID_No } = req.query; // Get ID_No from frontend for DELETE - if (!ID_No) { return res.status(400).json({ error: "ID_No is required" }); } + if (!ID_No) { + return res.status(400).json({ error: "ID_No is required" }); + } - const user = await user.findOne({ user_id: ID_No }); - if (!user) { return res.status(404).json({ error: "User not found" }); } + const user = await User.findOne({ user_id: ID_No }); + if (!user) { + return res.status(404).json({ error: "User not found" }); + } // Delete from Cloudinary if exists if (user.personal_info.cloudinaryUrl) { @@ -91,8 +103,8 @@ router.delete("/photo-delete",isAuthenticated, async (req, res) => { } }); -// API to Update Student Profile -router.put("/updateStudentProfile",isAuthenticated, async (req, res) => { +// API to Update Student Profile +router.put("/updateStudentProfile", isAuthenticated, async (req, res) => { try { const { userId, updatedDetails } = req.body; console.log("Received userId:", userId); @@ -124,13 +136,27 @@ router.put("/updateStudentProfile",isAuthenticated, async (req, res) => { cloudinaryUrl, } = updatedDetails.personal_info; - if (name) { user.personal_info.name = name; } - if (email) { user.personal_info.email = email; } - if (phone) { user.personal_info.phone = phone; } - if (gender) { user.personal_info.gender = gender; } - if (date_of_birth) { user.personal_info.date_of_birth = date_of_birth; } - if (profilePic) { user.personal_info.profilePic = profilePic; } - if (cloudinaryUrl) { user.personal_info.cloudinaryUrl = cloudinaryUrl; } + if (name) { + user.personal_info.name = name; + } + if (email) { + user.personal_info.email = email; + } + if (phone) { + user.personal_info.phone = phone; + } + if (gender) { + user.personal_info.gender = gender; + } + if (date_of_birth) { + user.personal_info.date_of_birth = date_of_birth; + } + if (profilePic) { + user.personal_info.profilePic = profilePic; + } + if (cloudinaryUrl) { + user.personal_info.cloudinaryUrl = cloudinaryUrl; + } } // ---------- ACADEMIC INFO ---------- @@ -138,19 +164,33 @@ router.put("/updateStudentProfile",isAuthenticated, async (req, res) => { const { program, branch, batch_year, current_year, cgpa } = updatedDetails.academic_info; - if (program) { user.academic_info.program = program; } - if (branch) { user.academic_info.branch = branch; } - if (batch_year) { user.academic_info.batch_year = batch_year; } - if (current_year) { user.academic_info.current_year = current_year; } - if (cgpa !== undefined) { user.academic_info.cgpa = cgpa; } + if (program) { + user.academic_info.program = program; + } + if (branch) { + user.academic_info.branch = branch; + } + if (batch_year) { + user.academic_info.batch_year = batch_year; + } + if (current_year) { + user.academic_info.current_year = current_year; + } + if (cgpa !== undefined) { + user.academic_info.cgpa = cgpa; + } } // ---------- CONTACT INFO ---------- if (updatedDetails.contact_info) { const { hostel, room_number, socialLinks } = updatedDetails.contact_info; - if (hostel) { user.contact_info.hostel = hostel; } - if (room_number) { user.contact_info.room_number = room_number; } + if (hostel) { + user.contact_info.hostel = hostel; + } + if (room_number) { + user.contact_info.room_number = room_number; + } // Social Links if (socialLinks) { diff --git a/backend/routes/skillsRoutes.js b/backend/routes/skillsRoutes.js index 04d1bb11..8a2863ac 100644 --- a/backend/routes/skillsRoutes.js +++ b/backend/routes/skillsRoutes.js @@ -2,7 +2,7 @@ const express = require("express"); const router = express.Router(); const { UserSkill, Skill } = require("../models/schema"); const { v4: uuidv4 } = require("uuid"); -const isAuthenticated = require("../middlewares/isAuthenticated"); +const { isAuthenticated } = require("../middlewares/isAuthenticated"); const authorizeRole = require("../middlewares/authorizeRole"); const { ROLE_GROUPS } = require("../utils/roles"); // GET unendorsed user skills for a particular skill type diff --git a/backend/utils/authValidate.js b/backend/utils/authValidate.js new file mode 100644 index 00000000..a83f858c --- /dev/null +++ b/backend/utils/authValidate.js @@ -0,0 +1,19 @@ +const zod = require("zod"); + +const loginValidate = zod.object({ + username: zod.string().regex(/^[a-zA-Z0-9._%+-]+@iitbhilai\.ac\.in$/i), + password: zod.string().min(8), +}); + +const registerValidate = zod.object({ + username: zod.string().regex(/^[a-zA-Z0-9._%+-]+@iitbhilai\.ac\.in$/i), + password: zod.string().min(8), + //user_id: zod.string().min(2), + name: zod.string().min(5), + role: zod.string().min(5), +}); + +module.exports = { + loginValidate, + registerValidate, +}; diff --git a/backend/utils/batchValidate.js b/backend/utils/batchValidate.js new file mode 100644 index 00000000..dceb1033 --- /dev/null +++ b/backend/utils/batchValidate.js @@ -0,0 +1,16 @@ +const zod = require("zod"); + +const zodObjectId = zod.string().regex(/^[0-9a-zA-Z]{24}$/, "Invalid ObjectId"); + +const validateBatchSchema = zod.object({ + title: zod.string().min(5, "Title is required"), + unit_id: zodObjectId, + commonData: zod.record(zod.string(), zod.string()), + template_id: zod.string(), + users: zod.array(zodObjectId).min(1, "Atleast 1 user must be associated."), +}); + +module.exports = { + validateBatchSchema, + zodObjectId, +}; diff --git a/frontend/src/App.js b/frontend/src/App.jsx similarity index 98% rename from frontend/src/App.js rename to frontend/src/App.jsx index ff6e0e0c..03953e1c 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.jsx @@ -20,6 +20,7 @@ import { SidebarProvider } from "./hooks/useSidebar"; function App() { const authData = useAuth(); const { isUserLoggedIn, isOnboardingComplete, isLoading } = authData; + //console.log("User data is: ",authData); // const role = isUserLoggedIn?.role || "STUDENT"; // const navItems = NavbarConfig[role] || []; diff --git a/frontend/src/Components/Auth/Login.jsx b/frontend/src/Components/Auth/Login.jsx index 57a22f35..232fec2e 100644 --- a/frontend/src/Components/Auth/Login.jsx +++ b/frontend/src/Components/Auth/Login.jsx @@ -1,14 +1,14 @@ import React, { useState, useContext } from "react"; -import { AdminContext } from "../../context/AdminContext"; +import { useAdminContext } from "../../context/AdminContext"; import { loginUser } from "../../services/auth"; import { useNavigate } from "react-router-dom"; import GoogleIcon from "@mui/icons-material/Google"; import cosa from "../../assets/COSA.png"; import backgroundImage from "../../assets/iitbh.jpg"; import { toast } from "react-toastify"; - +import { Link } from "react-router-dom"; export default function Login() { - const { handleLogin } = useContext(AdminContext); + const { handleLogin } = useAdminContext(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [loading, setLoading] = useState(false); @@ -19,10 +19,12 @@ export default function Login() { setLoading(true); try { - const userObject = await loginUser(email, password); - if (userObject) { - handleLogin(userObject); - toast.success("Login successful! 🎉"); + const response = await loginUser(email, password); + //console.log(response); + if (response?.success) { + handleLogin(response.data); + toast.success("Login successful "); + //console.log("Onboarding now is:", isOnboardingComplete); navigate("/", { replace: true }); } else { toast.error("Login failed. Please check your credentials."); @@ -41,7 +43,7 @@ export default function Login() { style={{ backgroundImage: `url(${backgroundImage})`, backgroundSize: "cover", - backgroundPosition: "center" + backgroundPosition: "center", }} > {/* Blur Overlay */} @@ -61,7 +63,6 @@ export default function Login() { className="flex flex-wrap flex-col-reverse lg:flex-row items-center justify-center gap-12 lg:gap-16 w-full max-w-7xl relative" style={{ zIndex: 2 }} > -
{/* Google Login */} - { + window.location.href = `${process.env.REACT_APP_BACKEND_URL}/auth/google`; + }} > - - + Sign up with Google + +

Don’t have an account?{" "} - Sign Up - +

diff --git a/frontend/src/Components/Auth/Register.jsx b/frontend/src/Components/Auth/Register.jsx index 8456697a..9c45a8df 100644 --- a/frontend/src/Components/Auth/Register.jsx +++ b/frontend/src/Components/Auth/Register.jsx @@ -1,8 +1,74 @@ import GoogleIcon from "@mui/icons-material/Google"; import cosa from "../../assets/COSA.png"; import backgroundImage from "../../assets/iitbh.jpg"; +import { Link, useNavigate } from "react-router-dom"; +import { useState } from "react"; +import { registerUser } from "../../services/auth"; +import { toast } from "react-toastify"; export default function Register() { + const [form, setForm] = useState({ + username: "", + password: "", + name: "", + }); + const [loading, setLoading] = useState(false); + const navigate = useNavigate("/"); + function handleChange(e) { + const { name, value } = e.target; + setForm((prev) => ({ + ...prev, + [name]: value, + })); + } + + async function handleSubmit(e) { + e.preventDefault(); + setLoading(true); + try { + const response = await registerUser( + form.username, + form.password, + form.name, + ); + // success response is the full axios response + if ( + response && + response.status === 200 && + response.data && + response.data.success + ) { + toast.success(response.data.message || "Registration successful"); + setTimeout(() => { + navigate("/", { replace: true }); + }, 1500); + return; + } + + // handle errors returned from server + let errorMessage = ""; + const respData = response && response.data; + if (respData) { + const msg = respData.message; + if (Array.isArray(msg)) { + errorMessage = msg.join(". "); + } else if (typeof msg === "string") { + errorMessage = msg; + } else if (msg && msg.message) { + errorMessage = msg.message; + } + } else if (response && response.status) { + errorMessage = response.statusText; + } + toast.error(errorMessage); + } catch (error) { + console.error("Registration failed:", error); + toast.error("Registration failed. Please try again."); + } finally { + setLoading(false); + } + } + return (
-
-
+

Sign Up

+
+ + {/* Username */} + + + + {/* Password */} + + + + {/* Name */} + + + + {/* Register Button */} + - {/* Google Register */} - +
+ OR +
+
+ + {/* Google Register */} + {/* + + - + Sign up with Google + + + + */} + -

- Already have an account?{" "} - - Login - -

-
-
+

+ Already have an account?{" "} + + Login + +

+ {/* CoSA Logo */}
diff --git a/frontend/src/Components/Auth/RoleRedirect.jsx b/frontend/src/Components/Auth/RoleRedirect.jsx index e6970c82..5f1d93ac 100644 --- a/frontend/src/Components/Auth/RoleRedirect.jsx +++ b/frontend/src/Components/Auth/RoleRedirect.jsx @@ -1,14 +1,13 @@ import React, { useContext } from "react"; import { Navigate } from "react-router-dom"; -import { AdminContext } from "../../context/AdminContext"; +import { useAdminContext } from "../../context/AdminContext"; const RoleRedirect = () => { const { userRole, isUserLoggedIn, isOnboardingComplete, isLoading } = - useContext(AdminContext); - + useAdminContext(); if (isLoading) return
Loading...
; - if (!isUserLoggedIn) { + if (!isUserLoggedIn || Object.keys(isUserLoggedIn).length === 0) { return ; } @@ -19,7 +18,7 @@ const RoleRedirect = () => { if (!userRole) { return
Loading user role...
; // Or just return null for a blank screen } - return ; + return ; }; export default RoleRedirect; diff --git a/frontend/src/Components/Auth/UserOnboarding.jsx b/frontend/src/Components/Auth/UserOnboarding.jsx index cb4a2e01..afb57ede 100644 --- a/frontend/src/Components/Auth/UserOnboarding.jsx +++ b/frontend/src/Components/Auth/UserOnboarding.jsx @@ -1,13 +1,12 @@ import { useState, useEffect, useContext } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, Navigate } from "react-router-dom"; import { fetchCredentials, completeOnboarding } from "../../services/auth"; -import { AdminContext } from "../../context/AdminContext"; +import { useAdminContext } from "../../context/AdminContext"; import logo from "../../assets/image.png"; export default function OnboardingForm() { const navigate = useNavigate(); - const { setIsOnboardingComplete } = useContext(AdminContext); - + const { setIsOnboardingComplete, isOnboardingComplete } = useAdminContext(); const [userData, setUserData] = useState({ name: "", email: "", @@ -23,11 +22,13 @@ export default function OnboardingForm() { useEffect(() => { const fetchUser = async () => { try { - const user = await fetchCredentials(); + const response = await fetchCredentials(); + const user = response.message; + if (!user) return; setUserData((prev) => ({ ...prev, - name: user.personal_info.name, - email: user.personal_info.email, + name: user.personal_info?.name, + email: user.personal_info?.email, })); } catch (error) { console.error("Error fetching user data:", error); @@ -41,7 +42,11 @@ export default function OnboardingForm() { if (!userData.ID_No) newErrors.ID_No = "ID Number is required"; if (!/^\d{10}$/.test(userData.mobile_no)) newErrors.mobile_no = "Mobile number must be 10 digits"; - if (!userData.add_year || userData.add_year < 2016) + if ( + !userData.add_year || + userData.add_year < 2016 || + userData.add_year > new Date().getFullYear() + ) newErrors.add_year = "Invalid admission year"; if (!userData.Program) newErrors.Program = "Program is required"; if (!userData.discipline) newErrors.discipline = "Discipline is required"; @@ -55,7 +60,9 @@ export default function OnboardingForm() { const handleSubmit = async (e) => { e.preventDefault(); const validationErrors = validate(); + if (Object.keys(validationErrors).length > 0) { + //console.log("Validation errors: ", validationErrors); setErrors(validationErrors); return; } @@ -68,6 +75,9 @@ export default function OnboardingForm() { } }; + if (isOnboardingComplete) { + return ; + } return (
@@ -80,8 +90,8 @@ export default function OnboardingForm() { className="w-32 h-32 object-contain rounded-full" />
-

- Welcome to Our College +

+ Welcome to IIT Bhilai

Complete your profile to access all campus services and tools. @@ -123,7 +133,7 @@ export default function OnboardingForm() { Student ID Number { + const { isUserLoggedIn } = useAdminContext(); + const [certificates, setCertificates] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [certificateFilter, setCertificateFilter] = useState("ALL"); + const [searchTerm, setSearchTerm] = useState(""); + + useEffect(() => { + async function fetchCertificates() { + try { + setLoading(true); + // TODO: Replace with actual API endpoint when available + // const response = await api.get(`/api/certificates/${isUserLoggedIn._id}`); + // setCertificates(response.data); + + // Mock data for now - remove when API is ready + const mockCertificates = [ + { + _id: "1", + event: "Tech Fest 2024", + issuedBy: "Computer Science Club", + date: "2024-01-15", + status: "Approved", + certificateUrl: "#", + rejectionReason: undefined, + }, + { + _id: "2", + event: "Hackathon 2024", + issuedBy: "Coding Club", + date: "2024-02-20", + status: "Pending", + certificateUrl: "#", + rejectionReason: undefined, + }, + { + _id: "3", + event: "Workshop Series", + issuedBy: "Technical Society", + date: "2024-03-10", + status: "Rejected", + certificateUrl: "#", + rejectionReason: "Incomplete participation", + }, + ]; + setCertificates(mockCertificates); + } catch (err) { + console.error("Error fetching certificates:", err); + setError("Failed to fetch certificates"); + } finally { + setLoading(false); + } + } + + if (isUserLoggedIn && Object.keys(isUserLoggedIn).length > 0) { + fetchCertificates(); + } + }, [isUserLoggedIn]); + + const filteredCertificates = certificates.filter((cert) => { + // Filter by status + const matchesStatus = + certificateFilter === "ALL" || cert.status === certificateFilter; + + // Filter by search term (searches in event name and issuedBy) + const matchesSearch = + !searchTerm || + cert.event?.toLowerCase().includes(searchTerm.toLowerCase()) || + cert.issuedBy?.toLowerCase().includes(searchTerm.toLowerCase()); + + return matchesStatus && matchesSearch; + }); + + const getStatusColor = (status) => { + switch (status) { + case "Approved": + return "bg-green-100 text-green-800 border-green-300"; + case "Pending": + return "bg-yellow-100 text-yellow-800 border-yellow-300"; + case "Rejected": + return "bg-red-100 text-red-800 border-red-300"; + default: + return "bg-gray-100 text-gray-800 border-gray-300"; + } + }; + + const formatDate = (dateString) => { + if (!dateString) return "N/A"; + const date = new Date(dateString); + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); + }; + + const filterButtons = [ + { label: "ALL", value: "ALL" }, + { label: "Pending", value: "Pending" }, + { label: "Approved", value: "Approved" }, + { label: "Rejected", value: "Rejected" }, + ]; + + if (loading) { + return ( +

+
Loading certificates...
+
+ ); + } + + if (error) { + return ( +
+
{error}
+
+ ); + } + + return ( +
+ {/* Filter Buttons */} +
+ {filterButtons.map((btn) => ( + + ))} + + {/**search bar */} +
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-1.5 rounded-xl border-2 border-black bg-white text-black placeholder-gray-400 " + /> +
+
+ + {/* Certificates Grid */} +
+ {filteredCertificates.length === 0 ? ( +
+ +

+ {searchTerm + ? `No certificates found matching ${searchTerm}` + : `No ${certificateFilter === "ALL" ? "" : certificateFilter.toLowerCase()} certificates found.`} +

+
+ ) : ( +
+ {filteredCertificates.map((certificate) => ( +
+ {/* Certificate Header */} +
+ + + {certificate.status} + +
+ + {/* Certificate Details */} +
+
+
Event
+
+ {certificate.event || "N/A"} +
+
+ +
+
+ + Issued By +
+
+ {certificate.issuedBy || "N/A"} +
+
+ +
+
+ + Date +
+
+ {formatDate(certificate.date)} +
+
+ + {certificate.rejectionReason && ( +
+
+ Rejection Reason +
+
+ {certificate.rejectionReason} +
+
+ )} +
+ + {/* Action Buttons */} +
+ {certificate.status === "Approved" && ( + <> + + + + )} + {certificate.status === "Pending" && ( + + )} + {certificate.status === "Rejected" && ( + + )} +
+
+ ))} +
+ )} +
+
+ ); +}; + +export default CertificatesList; diff --git a/frontend/src/Components/Dashboard/Dashboard.jsx b/frontend/src/Components/Dashboard/Dashboard.jsx index 99f7e9a8..c5029c4b 100644 --- a/frontend/src/Components/Dashboard/Dashboard.jsx +++ b/frontend/src/Components/Dashboard/Dashboard.jsx @@ -3,22 +3,24 @@ import { NavbarConfig } from "../../config/navbarConfig"; import { DashboardComponents } from "../../config/dashboardComponents"; import api from "../../utils/api"; import LeftColumn from "./QuickStats"; -import { AdminContext } from "../../context/AdminContext"; +import { useAdminContext } from "../../context/AdminContext"; import Layout from "../common/Layout"; import { SidebarProvider, useSidebar } from "../../hooks/useSidebar"; import Home from "./Home"; function Content() { const { selected: selectedRoute } = useSidebar(); + console.log("Selected Route is: ", selectedRoute); const ActiveComponent = DashboardComponents[selectedRoute] || (() =>
Home
); return ; } // Main component that provides the sidebar context export default function RoleBasedDashboard() { - const { isUserLoggedIn } = React.useContext(AdminContext); + const { isUserLoggedIn, isOnboardingComplete } = useAdminContext(); - if (!isUserLoggedIn) { + // Show loading until we have a logged-in user object and onboarding is complete. + if (!isUserLoggedIn || !isOnboardingComplete) { return (
diff --git a/frontend/src/Components/Dashboard/Home.jsx b/frontend/src/Components/Dashboard/Home.jsx index a2cc7abf..e58f7224 100644 --- a/frontend/src/Components/Dashboard/Home.jsx +++ b/frontend/src/Components/Dashboard/Home.jsx @@ -7,7 +7,7 @@ import PresidentAnalytics from "../Analytics/presidentAnalytics"; import StudentAnalytics from "../Analytics/studentAnalytics"; import GensecAnalytics from "../Analytics/gensecAnalytics"; import ClubCoordinatorAnalytics from "../Analytics/coordinatorAnalytics"; -import { AdminContext } from "../../context/AdminContext"; +import { AdminContext, useAdminContext } from "../../context/AdminContext"; import { useAnalyticsData } from "../../hooks/useAnalyticsData"; import { @@ -36,7 +36,7 @@ ChartJS.register( ); export const Home = () => { - const { isUserLoggedIn } = React.useContext(AdminContext); + const { isUserLoggedIn } = useAdminContext(); const role = isUserLoggedIn?.role; const { data, loading, error } = useAnalyticsData(); diff --git a/frontend/src/Components/Dashboard/Sidebar.jsx b/frontend/src/Components/Dashboard/Sidebar.jsx deleted file mode 100644 index 3bbc91e9..00000000 --- a/frontend/src/Components/Dashboard/Sidebar.jsx +++ /dev/null @@ -1,53 +0,0 @@ -// Sidebar.jsx -import React from "react"; -import { LogOut } from "lucide-react"; -import { useNavigate } from "react-router-dom"; -import { sidebarConfig } from "../../config/sidebarConfig"; -import { AdminContext } from "../../context/AdminContext"; -import imgCOSA from "../../assets/COSA.png"; -const Sidebar = ({ selected, setSelected }) => { - const { isUserLoggedIn } = React.useContext(AdminContext); - const role = isUserLoggedIn?.role; - const navigate = useNavigate(); - // GENSECs share one config - const menuItems = - role === "GENSEC_SCITECH" || - role === "GENSEC_ACADEMIC" || - role === "GENSEC_CULTURAL" || - role === "GENSEC_SPORTS" - ? sidebarConfig.GENSEC_COMMON - : sidebarConfig[role] || sidebarConfig.STUDENT; - - return ( -
- {/* Logo Section */} -
- CoSA -

CoSA

-
- - {/* Menu Items */} -
    - {menuItems.map(({ key, label, icon: Icon }) => ( -
  • setSelected(key)} - className={`flex items-center gap-3 py-2.5 px-3 rounded-xl cursor-pointer transition ${ - selected === key ? "bg-white text-black font-semibold" : "hover:bg-white/20" - }`} - > - - {label} -
  • - ))} -
- - {/* Logout */} - -
- ); -}; - -export default Sidebar; diff --git a/frontend/src/Components/common/Sidebar.jsx b/frontend/src/Components/common/Sidebar.jsx index 88867d96..58768842 100644 --- a/frontend/src/Components/common/Sidebar.jsx +++ b/frontend/src/Components/common/Sidebar.jsx @@ -18,7 +18,7 @@ const Sidebar = () => { return (
{ key={item.key} onClick={() => setSelected(item.key)} className={`flex items-center gap-3 py-2 mx-1 transition-all duration-200 ${ - isCollapsed ? "px-2 justify-center" : "px-4" + isCollapsed ? "px-3 justify-center" : "px-4" } ${ selected === item.key ? "bg-white text-black font-medium !rounded-3xl" : "text-zinc-400 hover:text-white hover:bg-zinc-800 rounded-xl" }`} - title={isCollapsed ? item.label : ""} + title={isCollapsed ? "" : item.label} > {!isCollapsed && ( {item.label} )} - ) + ), )} diff --git a/frontend/src/config/dashboardComponents.js b/frontend/src/config/dashboardComponents.js index 84428c54..3438b41b 100644 --- a/frontend/src/config/dashboardComponents.js +++ b/frontend/src/config/dashboardComponents.js @@ -24,6 +24,7 @@ import EndorsementPage from "../pages/endorsementPage"; import { HomePage } from "../pages/homePage"; import ProfilePage from "../pages/profilePage"; import AnnouncementsPage from "../pages/announcementsPage"; +import CertificatesPage from "../pages/certificatesPage"; export const DashboardComponents = { dashboard: HomePage, @@ -37,6 +38,7 @@ export const DashboardComponents = { profile: ProfilePage, cosa: ViewTenure, announcements: AnnouncementsPage, + certificates: CertificatesPage, "manage-positions": CreateTenure, "view-feedback": ViewFeedback, "add-event": EventForm, diff --git a/frontend/src/config/navbarConfig.js b/frontend/src/config/navbarConfig.js index f58ff9c6..c6a54230 100644 --- a/frontend/src/config/navbarConfig.js +++ b/frontend/src/config/navbarConfig.js @@ -11,6 +11,7 @@ import { Plus, Award, Megaphone, + Dock, } from "lucide-react"; const GENSEC_COMMON_NAV = [ @@ -24,6 +25,7 @@ const GENSEC_COMMON_NAV = [ { key: "por", label: "PORs", icon: UserPlus }, { key: "profile", label: "Profile", icon: User }, { key: "organization", label: "Clubs", icon: Users }, + { key: "certificates", label: "Certificates", icon: Dock }, ]; export const NavbarConfig = { @@ -38,6 +40,7 @@ export const NavbarConfig = { // { key: "add-event", label: "Add Event", icon: Plus }, { key: "profile", label: "Profile", icon: User }, { key: "organization", label: "Clubs", icon: Users }, + { key: "certificates", label: "Certificates", icon: Dock }, ], GENSEC_SCITECH: GENSEC_COMMON_NAV, GENSEC_ACADEMIC: GENSEC_COMMON_NAV, @@ -56,6 +59,7 @@ export const NavbarConfig = { { key: "feedback", label: "Feedback", icon: ClipboardList }, { key: "profile", label: "Profile", icon: User }, { key: "endorsement", label: "Endorsements", icon: Award }, + { key: "certificates", label: "Certificates", icon: Dock }, ], STUDENT: [ @@ -71,5 +75,6 @@ export const NavbarConfig = { // { key: "view-achievements", label: "View Achievements", icon: Trophy }, { key: "skills", label: "Skills", icon: Star }, { key: "por", label: "PORs", icon: ClipboardList }, + { key: "certificates", label: "Certificates", icon: Dock }, ], }; diff --git a/frontend/src/hooks/useAuth.js b/frontend/src/hooks/useAuth.js index 53dcfb7b..82b6e7db 100644 --- a/frontend/src/hooks/useAuth.js +++ b/frontend/src/hooks/useAuth.js @@ -8,21 +8,23 @@ export const useAuth = () => { const [isOnboardingComplete, setIsOnboardingComplete] = useState(null); const handleLogin = (userData) => { + if (!userData) return; setIsUserLoggedIn(userData); - setUserRole(userData.role); - setIsOnboardingComplete(userData.onboardingComplete); + if (userData?.role) setUserRole(userData.role); + // Always set onboarding flag based on the user's value (previous logic only set it when falsy) + setIsOnboardingComplete(Boolean(userData?.onboardingComplete)); }; useEffect(() => { const initializeAuth = async () => { try { - const user = await fetchCredentials(); - if (user) { - setIsUserLoggedIn(user); - setUserRole(user.role); - setIsOnboardingComplete(user.onboardingComplete); - console.log("User role:", user.role); - console.log("Onboarding complete:", user.onboardingComplete); + const response = await fetchCredentials(); + const user = response.message; + console.log("User is:", user); + if (response?.success) { + handleLogin(user); + //console.log("User role:", user.role); + //console.log("Onboarding complete:", user.onboardingComplete); } else { setIsUserLoggedIn(false); } diff --git a/frontend/src/index.js b/frontend/src/index.jsx similarity index 96% rename from frontend/src/index.js rename to frontend/src/index.jsx index c5bab643..8bde9c47 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.jsx @@ -10,7 +10,7 @@ const root = ReactDOM.createRoot(document.getElementById("root")); root.render( - , + ); // If you want to start measuring performance in your app, pass a function diff --git a/frontend/src/pages/certificatesPage.jsx b/frontend/src/pages/certificatesPage.jsx new file mode 100644 index 00000000..fd300df8 --- /dev/null +++ b/frontend/src/pages/certificatesPage.jsx @@ -0,0 +1,32 @@ +import Layout from "../Components/common/Layout"; +import CertificatesList from "../Components/Certificates/CertificatesList"; +import { useSidebar } from "../hooks/useSidebar"; + +export default function CertificatesPage() { + const { isCollapsed } = useSidebar(); + + const components = { + CertificatesList: CertificatesList, + }; + + const gridConfig = [ + { + id: "certificates", + component: "CertificatesList", + position: { + colStart: 0, + colEnd: isCollapsed ? 26 : 20, + rowStart: 0, + rowEnd: 16, + }, + }, + ]; + + return ( + + ); +} diff --git a/frontend/src/services/auth.js b/frontend/src/services/auth.js index 0ad7a688..cf8c1fb8 100644 --- a/frontend/src/services/auth.js +++ b/frontend/src/services/auth.js @@ -1,32 +1,41 @@ -import axios from "axios"; +//import axios from "axios"; import api from "../utils/api"; + export async function fetchCredentials() { - const response = await api.get(`/auth/fetchAuth`); + const response = await api.get("/auth/fetchAuth"); return response.data; } export async function completeOnboarding(userData) { - const response = await api.post(`/onboarding`, userData); - return response.data; + try{ + const response = await api.put(`/onboarding`, userData); + return response.data; + }catch (error) { + //console.error("Error obj is:",error.response); + return error.response; + } } -export async function registerUser(name, ID, email, password) { + +export async function registerUser(username, password, name) { + try { const response = await api.post(`/auth/register`, { name, - ID, - email, + username, password, }); - return response.data.user || null; + return response; } catch (error) { - return null; + //console.error("Error obj is:",error.response); + return error.response; } } -export async function loginUser(email, password) { +export async function loginUser(username, password) { try { - const res = await api.post("/auth/login", { email, password }); - return res.data.user || null; + const res = await api.post("/auth/login", { username, password }); + //console.log("Response is: ", res); + return res.data; } catch (error) { console.error("Login failed:", error.response?.data || error.message); return null; @@ -48,6 +57,7 @@ export async function registerStudentId(id, ID_No) { return null; } } + export async function logoutUser() { try { await api.post("/auth/logout"); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index ce473d03..00000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Student_Database_COSA", - "lockfileVersion": 3, - "requires": true, - "packages": {} -}