diff --git a/client/src/Pages/CourseForm.jsx b/client/src/Pages/CourseForm.jsx index 06229e6..0d3ede6 100644 --- a/client/src/Pages/CourseForm.jsx +++ b/client/src/Pages/CourseForm.jsx @@ -40,6 +40,9 @@ const CourseForm = () => { const [errors, setErrors] = useState({}); + // State to check if the form is currently submitting + const [loading, setLoading] = useState(false); + useEffect(() => { const fetchOptionsAndFaculties = async () => { try { @@ -72,11 +75,11 @@ const CourseForm = () => { const handleAddFaculty = (field) => { if (!formData[field]) return; - + const selectedFaculty = options.faculties.find( (faculty) => faculty.name === formData[field] ); - + if (selectedFaculty) { setTempAssignments((prev) => ({ ...prev, @@ -85,7 +88,6 @@ const CourseForm = () => { setFormData({ ...formData, [field]: "" }); } }; - const handleRemoveFaculty = (field, index) => { setTempAssignments((prev) => { @@ -139,6 +141,10 @@ const CourseForm = () => { }); const payload = Object.values(groupedTasks); + + // Start loading so user knows it is saving + setLoading(true); + await saveAppointment(payload); // await updateCourseStatus(course?.courseId || id); @@ -152,6 +158,8 @@ const CourseForm = () => { }); } catch (error) { console.error("Failed to save appointment:", error); + // If error comes, stop the loading + setLoading(false); } } }; @@ -343,13 +351,15 @@ const CourseForm = () => { type="submit" className="courseFormButton" style={{ gridColumn: "1 / -1" }} + disabled={loading} // Disable button when loading > - Submit + {/* Change text based on loading state */} + {loading ? "Saving..." : "Submit"} - + > ); diff --git a/client/src/Pages/Login.jsx b/client/src/Pages/Login.jsx index 4a9add1..b9b9509 100644 --- a/client/src/Pages/Login.jsx +++ b/client/src/Pages/Login.jsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from "react"; import { Container, Col, Row } from "react-bootstrap"; import { FcGoogle } from "react-icons/fc"; +import { FaEye, FaEyeSlash } from "react-icons/fa"; import axios from "axios"; import md5 from "md5"; @@ -39,18 +40,26 @@ function AuthPage() { async function handleSubmit(event) { event.preventDefault(); + // check if username is there if (!formData.username.trim() && signin) { notifyError("Username cannot be empty"); return; } + // check email format const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; if (!emailRegex.test(formData.email)) { notifyError("Enter a valid email address"); return; } - // Check password length + // restrict to somaiya emails + if (formData.email.endsWith("@somaiya.edu") === false) { + notifyError("Only @somaiya.edu emails are allowed"); + return; + } + + // password validation if (formData.password.length < 8) { notifyError("Password must be at least 8 characters long"); return; @@ -164,12 +173,17 @@ function TogglerContainer(props) { } function SignUpForm(props) { + const [showPassword, setShowPassword] = useState(false); return ( <> -
> ); } function SignInForm(props) { + const [showPassword, setShowPassword] = useState(false); return ( <> - > ); } -export default AuthPage; \ No newline at end of file +export default AuthPage; diff --git a/client/src/Pages/WelcomeWithFilter.jsx b/client/src/Pages/WelcomeWithFilter.jsx index 55abb9f..d6a6b7e 100644 --- a/client/src/Pages/WelcomeWithFilter.jsx +++ b/client/src/Pages/WelcomeWithFilter.jsx @@ -28,7 +28,7 @@ const WelcomeWithFilter = () => { - + ); }; diff --git a/client/src/assets/Somaiya.jpg b/client/src/assets/Somaiya.jpg new file mode 100644 index 0000000..660b421 Binary files /dev/null and b/client/src/assets/Somaiya.jpg differ diff --git a/server/config/passport.js b/server/config/passport.js index c0a5654..0219795 100644 --- a/server/config/passport.js +++ b/server/config/passport.js @@ -13,6 +13,14 @@ passport.use( }, async (accessToken, refreshToken, profile, done) => { try { + // Security: Check if the email is from Somaiya + // We only want somaiya students/faculty to access this + if (profile.emails[0].value.endsWith("@somaiya.edu") === false) { + return done(null, false, { + message: "Only @somaiya.edu emails are allowed", + }); + } + // Check if a user with the same email already exists let user = await User.findOne({ email: profile.emails[0].value }); diff --git a/server/middleware/verifyAdmin.js b/server/middleware/verifyAdmin.js new file mode 100644 index 0000000..d20766b --- /dev/null +++ b/server/middleware/verifyAdmin.js @@ -0,0 +1,25 @@ +const jwt = require("jsonwebtoken"); + +const verifyAdmin = (req, res, next) => { + try { + const token = req.cookies.token; // Ensure you are using cookies for auth + if (!token) { + return res + .status(401) + .json({ message: "Access denied. No token provided." }); + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET); + // Check if user is admin based on the 'isAdmin' boolean in the token + if (!decoded.isAdmin) { + return res.status(403).json({ message: "Access denied. Admins only." }); + } + + req.user = decoded; // Attach user data to the request + next(); + } catch (error) { + res.status(401).json({ message: "Invalid or expired token" }); + } +}; + +module.exports = verifyAdmin; diff --git a/server/package.json b/server/package.json index ee51746..0d2658b 100644 --- a/server/package.json +++ b/server/package.json @@ -29,7 +29,7 @@ "express": "^4.19.2", "express-session": "^1.18.0", "googleapis": "^134.0.0", - "jsonwebtoken": "^9.0.2", + "jsonwebtoken": "^9.0.3", "mongodb": "^6.15.0", "mongoose": "^8.9.5", "mongoose-findorcreate": "^4.0.0", @@ -42,6 +42,8 @@ "uuid": "^11.0.3" }, "devDependencies": { - "nodemon": "^3.1.0" + "jest": "^30.2.0", + "nodemon": "^3.1.0", + "supertest": "^7.2.2" } } diff --git a/server/routes/courseRoutes.js b/server/routes/courseRoutes.js index bc432ee..e16e056 100644 --- a/server/routes/courseRoutes.js +++ b/server/routes/courseRoutes.js @@ -1,6 +1,6 @@ const express = require("express"); const Course = require("../models/Course"); -const verifyAdmin = require("../../client/src/components/verifyAdmin"); +const verifyAdmin = require("../middleware/verifyAdmin"); const router = express.Router(); @@ -21,7 +21,6 @@ router.get("/", async (req, res) => { } }); - // Get course by ID router.get("/:id", async (req, res) => { try { @@ -88,10 +87,12 @@ router.put("/:courseId", verifyAdmin, async (req, res) => { } }); -//delete +//delete router.delete("/:courseId", verifyAdmin, async (req, res) => { try { - const deletedCourse = await Course.findOneAndDelete({ courseId: req.params.courseId }); + const deletedCourse = await Course.findOneAndDelete({ + courseId: req.params.courseId, + }); if (!deletedCourse) { return res.status(404).json({ error: "Course not found" }); } @@ -105,7 +106,8 @@ router.delete("/:courseId", verifyAdmin, async (req, res) => { // add a new course router.post("/", verifyAdmin, async (req, res) => { try { - const { courseId, name, department, program, scheme, semester, status } = req.body; + const { courseId, name, department, program, scheme, semester, status } = + req.body; // Check if a course with the same courseId already exists const existingCourse = await Course.findOne({ courseId }); @@ -120,17 +122,17 @@ router.post("/", verifyAdmin, async (req, res) => { program, scheme, semester, - status + status, }); await newCourse.save(); - res.status(201).json({ message: "Course added successfully", course: newCourse }); + res + .status(201) + .json({ message: "Course added successfully", course: newCourse }); } catch (error) { console.error("Error adding course:", error); res.status(500).json({ error: "Failed to add course" }); } }); - - module.exports = router; diff --git a/server/server.js b/server/server.js index 255c464..b70e528 100644 --- a/server/server.js +++ b/server/server.js @@ -22,20 +22,32 @@ const Course = require("./models/Course"); const User = require("./models/User"); // MongoDB Connection -mongoose - .connect(process.env.mongoURI, { useNewUrlParser: true, useUnifiedTopology: true }) - .then(() => console.log("MongoDB connected")) - .catch((err) => { - console.error("MongoDB connection error:", err); - process.exit(1); // Exit the app if the database connection fails - }); +// MongoDB Connection +const connectDB = async () => { + try { + await mongoose.connect(process.env.mongoURI, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + console.log("MongoDB connected"); + } catch (err) { + console.error("MongoDB connection failed:", err.message); + } +}; + +connectDB(); // Initialize App const app = express(); const PORT = 8080; // Middleware -app.use(cors({ origin: process.env.CORS_ORIGIN || "http://localhost:3000", credentials: true })); +app.use( + cors({ + origin: process.env.CORS_ORIGIN || "http://localhost:3000", + credentials: true, + }) +); app.use(cookieparser()); app.use(express.json()); app.use(bodyParser.urlencoded({ extended: true })); @@ -74,9 +86,19 @@ app.get( "/auth/google/callback", passport.authenticate("google", { failureRedirect: "/" }), (req, res) => { - const token = jwt.sign({ userId: req.user._id }, process.env.JWT_SECRET, { expiresIn: "1h" }); + const token = jwt.sign( + { userId: req.user._id, isAdmin: req.user.isAdmin }, + process.env.JWT_SECRET, + { + expiresIn: "1h", + } + ); // Set token as a cookie or send it in the response - res.cookie("token", token, { httpOnly: true, secure: false, maxAge: 3600000 }); + res.cookie("token", token, { + httpOnly: true, + secure: false, + maxAge: 3600000, + }); res.redirect("http://localhost:3000/Welcome"); // Redirect to a frontend route after successful login } ); @@ -85,6 +107,14 @@ app.get( app.post("/api/register", async (req, res) => { try { const { username, email, password } = req.body; + + // Validation: Only allow somaiya emails + if (email.endsWith("@somaiya.edu") === false) { + return res + .status(400) + .json({ message: "Only @somaiya.edu emails are allowed" }); + } + const hashedPassword = await bcrypt.hash(password, 10); let user = await User.findOne({ email }); @@ -104,11 +134,21 @@ app.post("/api/register", async (req, res) => { await user.save(); } - // Generate a JWT token using the user's ID - const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: "1h" }); + // adding isAdmin to token so we know if user is admin + const token = jwt.sign( + { userId: user._id, isAdmin: user.isAdmin }, + process.env.JWT_SECRET, + { + expiresIn: "1h", + } + ); // Set the token as a cookie - res.cookie("token", token, { httpOnly: true, secure: false, maxAge: 3600000 }); // 1 hour expiry + res.cookie("token", token, { + httpOnly: true, + secure: false, + maxAge: 3600000, + }); // 1 hour expiry return res.status(200).json({ message: "Registered and logged in successfully", @@ -133,10 +173,20 @@ app.post("/api/login", (req, res, next) => { return res.status(500).json({ message: "Internal server error" }); } // Generate a JWT token using the user's ID - const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: "1h" }); + const token = jwt.sign( + { userId: user._id, isAdmin: user.isAdmin }, + process.env.JWT_SECRET, + { + expiresIn: "1h", + } + ); // Set the token as a cookie - res.cookie("token", token, { httpOnly: true, secure: false, maxAge: 3600000 }); // 1 hour expiry + res.cookie("token", token, { + httpOnly: true, + secure: false, + maxAge: 3600000, + }); // 1 hour expiry return res.status(200).json({ message: "Login successful", user }); }); })(req, res, next); @@ -158,7 +208,9 @@ app.get("/auth/logout", function (req, res) { console.error("Error destroying session:", err); return res.status(500).json({ message: "Error logging out" }); } - res.status(200).json({ message: "Logout successful, session destroyed" }); + res + .status(200) + .json({ message: "Logout successful, session destroyed" }); }); } else { // If no session, simply respond with success @@ -175,12 +227,18 @@ app.post("/api/refresh", (req, res) => { const refreshToken = req.cookies.token; if (!refreshToken) { - return res.status(401).json({ message: "No refresh token, authorization denied" }); + return res + .status(401) + .json({ message: "No refresh token, authorization denied" }); } try { const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET); - const newToken = jwt.sign({ userId: decoded.userId }, process.env.JWT_SECRET, { expiresIn: "1h" }); + const newToken = jwt.sign( + { userId: decoded.userId, isAdmin: decoded.isAdmin }, + process.env.JWT_SECRET, + { expiresIn: "1h" } + ); return res .cookie("token", newToken, { httpOnly: true, maxAge: 3600000 }) // Set new access token @@ -196,7 +254,6 @@ app.get("/api/auth-check", (req, res) => { const token = req.cookies.token; // Retrieve the httpOnly cookie if (!token) { - console.log("Tehehe"); return res.status(401).json({ message: "Unauthorized" }); } @@ -211,7 +268,8 @@ app.get("/api/auth-check", (req, res) => { app.get("/api/me", async (req, res) => { try { const token = req.cookies.token; // ✅ Get token from request cookies - if (!token) return res.status(401).json({ message: "Unauthorized - No Token" }); + if (!token) + return res.status(401).json({ message: "Unauthorized - No Token" }); const decoded = jwt.verify(token, process.env.JWT_SECRET); // ✅ Verify token @@ -223,16 +281,14 @@ app.get("/api/me", async (req, res) => { userId: user._id, isAdmin: user.isAdmin, // ✅ Return actual `isAdmin` value exp: decoded.exp, - iat: decoded.iat + iat: decoded.iat, }); - } catch (error) { console.error("JWT Verification Error:", error.message); res.status(401).json({ message: "Invalid token" }); } }); - // User Profile Route app.get("/api/user/profile", async (req, res) => { try { @@ -251,9 +307,6 @@ app.patch("/api/courses/:courseId", async (req, res) => { const { courseId } = req.params; const { status } = req.body; - console.log("Request params:", req.params); - console.log("Request body:", req.body); - if (!status) { console.error("Status is missing in the request body."); return res.status(400).json({ message: "Status is required" }); @@ -287,10 +340,17 @@ app.get("*", (req, res) => // Error Handling Middleware app.use((err, req, res, next) => { console.error("Error:", err.stack); - res.status(err.status || 500).json({ error: err.message || "Internal Server Error" }); + res + .status(err.status || 500) + .json({ error: err.message || "Internal Server Error" }); }); // Start Server -app.listen(PORT, () => { - console.log(`Server is running at http://localhost:8080`); -}); +// Start Server +if (require.main === module) { + app.listen(PORT, () => { + console.log(`Server is running at http://localhost:8080`); + }); +} + +module.exports = app;