diff --git a/backend/routes/auth.js b/backend/routes/auth.js
index a8f603d7..4190525d 100644
--- a/backend/routes/auth.js
+++ b/backend/routes/auth.js
@@ -1,335 +1,346 @@
-// Module imports
-const express = require('express');
-const bcrypt = require('bcrypt');
-const jwt = require('jsonwebtoken');
-const { storage, cloudinary } = require('../utils/cloudinary');
-const multer = require('multer');
-const upload = multer({ storage });
-const { verifyToken } = require('../utils/verify_token.js');
-const { OAuth2Client } = require('google-auth-library');
-require('dotenv').config();
-
-// Model imports
-const User = require('../models/user');
-
-// Router instance
-const router = express.Router();
-
-// Google OAuth client instance
-const client = new OAuth2Client();
-
-/**
- * ! POST Login and/or register a User using Google
- *
- * Login and/or register a Google user
- *
- * @async
- * @returns {200} Responds with 200 status code if user is successfully registered/logged in
- * @throws {409} If the user already exists as a non-Google account
- * @throws {403} If the email is not a Monash Student email
- * @throws {500} If an error occurs whilst registering a user
- */
-router.post('/google/authenticate', async function (req, res) {
- const { idToken } = req.body;
- try {
- const ticket = await client.verifyIdToken({
- idToken: idToken,
- audience: process.env.GOOGLE_CLIENT_ID
- });
-
- const payload = ticket.getPayload();
- // sub is the unique Google ID assigned to the user
- const { email, name, picture, sub } = payload;
-
- // Regular expression to validate authcate and email
- const studentEmailRegex = /^[a-zA-Z]{4}\d{4}@student\.monash\.edu$/
- const staffEmailRegex = /^[a-zA-Z]+\.[a-zA-Z]+@monash\.edu$/;
-
- // Invalid email error case
- if (!studentEmailRegex.test(email) && !staffEmailRegex.test(email)) {
- return res.status(403).json({ error: 'Access denied: Only students with a valid Monash email can log in.' });
- }
-
- // Check if the user already exists
- let user = await User.findOne({
- $or: [
- { email: email },
- { googleID: sub }
- ]
- });
-
- // register the user if they aren't registered
- if (!user) {
- // Generate username based on email format
- let authcate;
- if (studentEmailRegex.test(email)) { authcate = email.split('@')[0]; }
- else if (staffEmailRegex.test(email)) { authcate = email.split('.')[0]; }
-
- user = new User({
- email: email,
- username: authcate,
- profileImg: picture,
- isGoogleUser: true,
- googleID: sub,
- verified: true
- });
- await user.save();
- }
-
- // if there is a user but they are NOT a Google user but same email
- // (if they signed up using traditional way but then try logging in thru Google)
- if (!user.isGoogleUser) {
- return res.status(409).json({ message: "Account already exists as non-Google account." });
- }
-
- // Create json web token
- const token = jwt.sign(
- { id: user._id, isAdmin: user.admin },
- process.env.JWT_SECRET,
- { expiresIn: '24h' }
- );
-
- // Return response as cookie with access token and user data.
- return res.cookie('access_token', token, {
- httpOnly: true,
- sameSite: 'strict'
- })
- .status(200)
- .json({ message: 'Login successful', data: user });
- }
- catch (error) {
- return res.status(500).json({ error: error.message });
- }
-});
-
-
-/**
- * ! GET Get All Users
- *
- * Gets all users from the database.
- *
- * @async
- * @returns {JSON} Responds with a list of all users in JSON format.
- * @throws {500} If an error occurs whilst fetching users from the database.
- */
-router.get('/', verifyToken, async function (req, res) {
- try {
- // Find all users
- const users = await User.find({});
-
- // Response 200 with list of users in json
- return res.status(200).json(users);
- }
- catch (error) {
- // Handle general errors
- return res.status(500).json({ error: `An error occured while getting all Users: ${error.message}` });
- }
-});
-
-
-/**
- * ! DELETE Remove a User from the database
- *
- * Deletes a User from the database. Only admins or the user themselves can
- * delete accounts.
- *
- * @async
- * @returns {JSON} Responds with a success message in JSON
- * @throws {403} If user is not authorised to delete this account
- * @throws {500} If an error occurs
- * @throws {404} User not found error
- */
-router.delete('/delete/:userId', verifyToken, async function (req, res) {
- try {
- // Get the requesting user from the token
- const requestingUser = await User.findById(req.user.id);
- if (!requestingUser) return res.status(404).json({ error: 'Requesting user not found' });
-
- // Get the target user by email
- const targetUser = await User.findById(req.params.userId);
- if (!targetUser) return res.status(404).json({ error: 'Target user not found' });
-
- // Check authorisation
- const isSameUser = requestingUser._id.toString() === targetUser._id.toString();
- const isAdminDeletingOther = requestingUser.admin && !isSameUser;
- if (!isSameUser && !isAdminDeletingOther)
- return res.status(403).json({ error: 'You are not authorised to delete this account' });
-
- // Delete the user
- await User.findOneAndDelete({ _id: targetUser._id });
-
- // Return status 200 for successful deletion of user
- return res.status(200).json({ message: "User successfully deleted" });
- }
- catch (error) {
- // Handle general errors status 500
- return res.status(500).json({ error: `Error occured while deleting user: ${error.message}` });
- }
-});
-
-/**
- * ! POST Logout a User
- *
- * Clears the token cookie to log the user out
- *
- * @async
- * @returns {JSON} Responds with a success message in JSON
- */
-router.post('/logout', verifyToken, async function (req, res) {
- try {
- // Clear the cookie
- res.clearCookie('access_token', { httpOnly: true, sameSite: 'strict' });
-
- // Respond with success message
- return res.status(200).json({ message: 'Logged out successfully' });
- }
- catch (error) {
- // Handle errors
- return res.status(500).json({ error: `An error occurred during logout: ${error.message}` });
- }
-});
-
-
-/**
- * ! PUT Update a User's details
- *
- * Updates User's username and/or password. Only admins or the user themselves
- * can update account details.
- *
- * @async
- * @param {String} userId - The ID of the user to update
- * @returns {JSON} Responds with status 200 and success message
- * @throws {404} If the Unit is not found
- * @throws {403} If the user is not authorised to update this account
- * @throws {400} If no update fields are provided
- * @throws {500} If some error occurs
- */
-router.put('/update/:userId', verifyToken, async function (req, res) {
- try {
- // Get the requesting user from the token
- const requestingUser = await User.findById(req.user.id);
- if (!requestingUser) return res.status(404).json({ error: 'Requesting user not found' });
-
- // Get the target user by userID
- const targetUser = await User.findById(req.params.userId);
- if (!targetUser) return res.status(404).json({ error: 'Target user not found' });
-
- // Check authorisation
- const isSameUser = requestingUser._id.toString() === targetUser._id.toString();
- const isAdminUpdatingOther = requestingUser.admin && !isSameUser;
- if (!isSameUser && !isAdminUpdatingOther)
- return res.status(403).json({ error: 'You are not authorised to update user details' });
-
- // Get the updated email and/or password from the request body
- const { username, password } = req.body;
-
- // Validate that either username or password is provided
- if (!username && !password)
- return res.status(400).json({ error: 'Either username or password is required to update' });
-
- // Update fields if provided
- if (username) {
- targetUser.username = username;
- }
-
- if (password) {
- const hashedPassword = await bcrypt.hash(password, 10);
- targetUser.password = hashedPassword;
- }
-
- // Save the updated user
- await targetUser.save();
-
- // Return status 200 sending success message and updated user data
- return res.status(200).json({ message: "User details successfully updated", username: targetUser.username });
- }
- catch (error) {
- // Handle general errors status 500
- return res.status(500).json({ error: `Error updating user details: ${error.message}` });
- }
-});
-
-/**
- * ! GET Validates User
- *
- * Checks if the user has the access_token in their cookies to keep session.
- * The payload also contains the user's data.
- *
- * @async
- * @returns {JSON} Responds with status 200 and json containing message and decoded user data.
- * @throws {401} If the user is not authenticated and has no access token
- * @throws {403} If the given access_token is invalid
- * @throws {404} If the user is not found
- */
-router.get('/validate', async function (req, res) {
- // Gets the access token from the user's cookies
- const token = req.cookies.access_token;
-
- // Throw error if no access token
- if (!token)
- return res.status(401).json({ message: 'Not authenticated' });
-
- try {
- // Decode the token
- const decoded = jwt.verify(token, process.env.JWT_SECRET);
-
- // Find and store the user without storing the password
- const user = await User.findById(decoded.id, 'email username isGoogleUser reviews admin profileImg likedReviews dislikedReviews notifications');
- // User not found error case
- if (!user)
- return res.status(404).json({ message: 'User not found' });
-
- // Return status 200 success with decoded user data
- return res.status(200).json({ message: 'Authenticated', data: user });
- }
- catch (error) {
- // Invalid access token error
- return res.status(403).json({ message: 'Invalid token' });
- }
-});
-
-/**
- * ! POST Upload Avatar
- *
- * Uploads the given avatar to cloudinary via middlware, and then assigns the
- * avatar as user's profileImg
- *
- * @async
- * @returns {JSON} Responds with status 200 and json containing the success message and profileImg URL
- * @throws {404} If the user is not found
- * @throws {500} Internal server errors
- */
-router.post('/upload-avatar', verifyToken, upload.single('avatar'), async function (req, res) {
- try {
- // Get the user by email
- const { email } = req.body;
- const user = await User.findOne({ email });
-
- if (!user)
- return res.status(404).json({ error: 'User not found' });
-
- // If the user already has a profile image, remove it from Cloudinary
- if (user.profileImg) {
- // Extract the public id from the existing cloudinary URL
- const urlParts = user.profileImg.split('/');
- const fileName = urlParts[urlParts.length - 1].split('.')[0]; // Extract the file name without its extension.
- const publicId = `user_avatars/${fileName}`;
-
- // Delete the old avatar
- await cloudinary.uploader.destroy(publicId);
- }
-
- // Save the Cloudinary URL as the user's avatar
- user.profileImg = req.file.path; // Cloudinary URL for the uploaded image
- await user.save();
-
- // Respond with status 200 and json containing success message and profile image
- return res.status(200).json({ message: 'Avatar uploaded successfully', profileImg: user.profileImg });
- }
- catch (error) {
- return res.status(500).json({ error: `Error uploading avatar: ${error.message}` });
- }
-});
-
-
+// Module imports
+const express = require('express');
+const bcrypt = require('bcrypt');
+const jwt = require('jsonwebtoken');
+const { storage, cloudinary } = require('../utils/cloudinary');
+const multer = require('multer');
+const upload = multer({ storage });
+const { verifyToken } = require('../utils/verify_token.js');
+const { OAuth2Client } = require('google-auth-library');
+require('dotenv').config();
+
+// Model imports
+const User = require('../models/user');
+
+// Router instance
+const router = express.Router();
+
+// Google OAuth client instance
+const client = new OAuth2Client();
+
+/**
+ * ! POST Login and/or register a User using Google
+ *
+ * Login and/or register a Google user
+ *
+ * @async
+ * @returns {200} Responds with 200 status code if user is successfully registered/logged in
+ * @throws {409} If the user already exists as a non-Google account
+ * @throws {403} If the email is not a Monash Student email
+ * @throws {500} If an error occurs whilst registering a user
+ */
+router.post('/google/authenticate', async function (req, res) {
+ const { idToken } = req.body;
+ try {
+ const ticket = await client.verifyIdToken({
+ idToken: idToken,
+ audience: process.env.GOOGLE_CLIENT_ID
+ });
+
+ const payload = ticket.getPayload();
+ // sub is the unique Google ID assigned to the user
+ const { email, name, picture, sub } = payload;
+
+ // Regular expression to validate authcate and email
+ const studentEmailRegex = /^[a-zA-Z]{4}\d{4}@student\.monash\.edu$/
+ const staffEmailRegex = /^[a-zA-Z]+\.[a-zA-Z]+@monash\.edu$/;
+
+ // Invalid email error case
+ if (!studentEmailRegex.test(email) && !staffEmailRegex.test(email)) {
+ return res.status(403).json({ error: 'Access denied: Only students with a valid Monash email can log in.' });
+ }
+
+ // Check if the user already exists
+ let user = await User.findOne({
+ $or: [
+ { email: email },
+ { googleID: sub }
+ ]
+ });
+
+ // register the user if they aren't registered
+ if (!user) {
+ // Generate username based on email format
+ let authcate;
+ if (studentEmailRegex.test(email)) { authcate = email.split('@')[0]; }
+ else if (staffEmailRegex.test(email)) { authcate = email.split('.')[0]; }
+
+ user = new User({
+ email: email,
+ username: authcate,
+ profileImg: picture,
+ isGoogleUser: true,
+ googleID: sub,
+ verified: true
+ });
+ await user.save();
+ }
+
+ // if there is a user but they are NOT a Google user but same email
+ // (if they signed up using traditional way but then try logging in thru Google)
+ if (!user.isGoogleUser) {
+ return res.status(409).json({ message: "Account already exists as non-Google account." });
+ }
+
+ // Create json web token
+ const token = jwt.sign(
+ { id: user._id, isAdmin: user.admin },
+ process.env.JWT_SECRET,
+ { expiresIn: '24h' }
+ );
+
+ // Return response as cookie with access token and user data.
+ return res.cookie('access_token', token, {
+ httpOnly: true,
+ sameSite: 'strict'
+ })
+ .status(200)
+ .json({ message: 'Login successful', data: user });
+ }
+ catch (error) {
+ return res.status(500).json({ error: error.message });
+ }
+});
+
+
+/**
+ * ! GET Get All Users
+ *
+ * Gets all users from the database.
+ *
+ * @async
+ * @returns {JSON} Responds with a list of all users in JSON format.
+ * @throws {500} If an error occurs whilst fetching users from the database.
+ */
+router.get('/', verifyToken, async function (req, res) {
+ try {
+ // Find all users
+ const users = await User.find({});
+
+ // Response 200 with list of users in json
+ return res.status(200).json(users);
+ }
+ catch (error) {
+ // Handle general errors
+ return res.status(500).json({ error: `An error occured while getting all Users: ${error.message}` });
+ }
+});
+
+
+/**
+ * ! DELETE Remove a User from the database
+ *
+ * Deletes a User from the database. Only admins or the user themselves can
+ * delete accounts.
+ *
+ * @async
+ * @returns {JSON} Responds with a success message in JSON
+ * @throws {403} If user is not authorised to delete this account
+ * @throws {500} If an error occurs
+ * @throws {404} User not found error
+ */
+router.delete('/delete/:userId', verifyToken, async function (req, res) {
+ try {
+ // Get the requesting user from the token
+ const requestingUser = await User.findById(req.user.id);
+ if (!requestingUser) return res.status(404).json({ error: 'Requesting user not found' });
+
+ // Get the target user by email
+ const targetUser = await User.findById(req.params.userId);
+ if (!targetUser) return res.status(404).json({ error: 'Target user not found' });
+
+ // Check authorisation
+ const isSameUser = requestingUser._id.toString() === targetUser._id.toString();
+ const isAdminDeletingOther = requestingUser.admin && !isSameUser;
+ if (!isSameUser && !isAdminDeletingOther)
+ return res.status(403).json({ error: 'You are not authorised to delete this account' });
+
+ // Delete the user
+ await User.findOneAndDelete({ _id: targetUser._id });
+
+ // Return status 200 for successful deletion of user
+ return res.status(200).json({ message: "User successfully deleted" });
+ }
+ catch (error) {
+ // Handle general errors status 500
+ return res.status(500).json({ error: `Error occured while deleting user: ${error.message}` });
+ }
+});
+
+/**
+ * ! POST Logout a User
+ *
+ * Clears the token cookie to log the user out
+ *
+ * @async
+ * @returns {JSON} Responds with a success message in JSON
+ */
+router.post('/logout', verifyToken, async function (req, res) {
+ try {
+ // Clear the cookie
+ res.clearCookie('access_token', { httpOnly: true, sameSite: 'strict' });
+
+ // Respond with success message
+ return res.status(200).json({ message: 'Logged out successfully' });
+ }
+ catch (error) {
+ // Handle errors
+ return res.status(500).json({ error: `An error occurred during logout: ${error.message}` });
+ }
+});
+
+
+/**
+ * ! PUT Update a User's details
+ *
+ * Updates User's username and/or password. Only admins or the user themselves
+ * can update account details.
+ *
+ * @async
+ * @param {String} userId - The ID of the user to update
+ * @returns {JSON} Responds with status 200 and success message
+ * @throws {404} If the Unit is not found
+ * @throws {403} If the user is not authorised to update this account
+ * @throws {400} If no update fields are provided
+ * @throws {500} If some error occurs
+ */
+router.put('/update/:userId', verifyToken, async function (req, res) {
+ try {
+ // Get the requesting user from the token
+ const requestingUser = await User.findById(req.user.id);
+ if (!requestingUser) return res.status(404).json({ error: 'Requesting user not found' });
+
+ // Get the target user by userID
+ const targetUser = await User.findById(req.params.userId);
+ if (!targetUser) return res.status(404).json({ error: 'Target user not found' });
+
+ // Check authorisation
+ const isSameUser = requestingUser._id.toString() === targetUser._id.toString();
+ const isAdminUpdatingOther = requestingUser.admin && !isSameUser;
+ if (!isSameUser && !isAdminUpdatingOther)
+ return res.status(403).json({ error: 'You are not authorised to update user details' });
+
+ // Get the updated email and/or password from the request body
+ const { username, password } = req.body;
+
+ // Validate that either username or password is provided
+ if (!username && !password)
+ return res.status(400).json({ error: 'Either username or password is required to update' });
+
+ // If usename is duplicated, return error
+ if (username) {
+ const existingUser = await User.findOne({
+ username: username,
+ _id: { $ne: targetUser._id } // Exclude the current user
+ });
+ if (existingUser) {
+ return res.status(400).json({ error: 'Username already exists' });
+ }
+ }
+
+ // Update fields if provided
+ if (username) {
+ targetUser.username = username;
+ }
+
+ if (password) {
+ const hashedPassword = await bcrypt.hash(password, 10);
+ targetUser.password = hashedPassword;
+ }
+
+ // Save the updated user
+ await targetUser.save();
+
+ // Return status 200 sending success message and updated user data
+ return res.status(200).json({ message: "User details successfully updated", username: targetUser.username });
+ }
+ catch (error) {
+ // Handle general errors status 500
+ return res.status(500).json({ error: `Error updating user details: ${error.message}` });
+ }
+});
+
+/**
+ * ! GET Validates User
+ *
+ * Checks if the user has the access_token in their cookies to keep session.
+ * The payload also contains the user's data.
+ *
+ * @async
+ * @returns {JSON} Responds with status 200 and json containing message and decoded user data.
+ * @throws {401} If the user is not authenticated and has no access token
+ * @throws {403} If the given access_token is invalid
+ * @throws {404} If the user is not found
+ */
+router.get('/validate', async function (req, res) {
+ // Gets the access token from the user's cookies
+ const token = req.cookies.access_token;
+
+ // Throw error if no access token
+ if (!token)
+ return res.status(401).json({ message: 'Not authenticated' });
+
+ try {
+ // Decode the token
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
+
+ // Find and store the user without storing the password
+ const user = await User.findById(decoded.id, 'email username isGoogleUser reviews admin profileImg likedReviews dislikedReviews notifications');
+ // User not found error case
+ if (!user)
+ return res.status(404).json({ message: 'User not found' });
+
+ // Return status 200 success with decoded user data
+ return res.status(200).json({ message: 'Authenticated', data: user });
+ }
+ catch (error) {
+ // Invalid access token error
+ return res.status(403).json({ message: 'Invalid token' });
+ }
+});
+
+/**
+ * ! POST Upload Avatar
+ *
+ * Uploads the given avatar to cloudinary via middlware, and then assigns the
+ * avatar as user's profileImg
+ *
+ * @async
+ * @returns {JSON} Responds with status 200 and json containing the success message and profileImg URL
+ * @throws {404} If the user is not found
+ * @throws {500} Internal server errors
+ */
+router.post('/upload-avatar', verifyToken, upload.single('avatar'), async function (req, res) {
+ try {
+ // Get the user by email
+ const { email } = req.body;
+ const user = await User.findOne({ email });
+
+ if (!user)
+ return res.status(404).json({ error: 'User not found' });
+
+ // If the user already has a profile image, remove it from Cloudinary
+ if (user.profileImg) {
+ // Extract the public id from the existing cloudinary URL
+ const urlParts = user.profileImg.split('/');
+ const fileName = urlParts[urlParts.length - 1].split('.')[0]; // Extract the file name without its extension.
+ const publicId = `user_avatars/${fileName}`;
+
+ // Delete the old avatar
+ await cloudinary.uploader.destroy(publicId);
+ }
+
+ // Save the Cloudinary URL as the user's avatar
+ user.profileImg = req.file.path; // Cloudinary URL for the uploaded image
+ await user.save();
+
+ // Respond with status 200 and json containing success message and profile image
+ return res.status(200).json({ message: 'Avatar uploaded successfully', profileImg: user.profileImg });
+ }
+ catch (error) {
+ return res.status(500).json({ error: `Error uploading avatar: ${error.message}` });
+ }
+});
+
+
module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/users.js b/backend/routes/users.js
new file mode 100644
index 00000000..0d9cefdb
--- /dev/null
+++ b/backend/routes/users.js
@@ -0,0 +1,61 @@
+// Module Imports
+const express = require('express');
+
+// Model Imports
+const User = require('../models/user');
+
+// Function Imports
+const { verifyAdmin } = require('../utils/verify_token.js');
+
+// Router instance
+const router = express.Router();
+
+/**
+ * ! GET Get User by Username
+ *
+ * Gets a user by their username
+ *
+ * @async
+ * @returns {JSON} Responds with the unit and its details in JSON format
+ * @throws {500} If an error occurs whilst getting the singular user from the database
+ */
+router.get('/:username', async function (req, res) {
+ try {
+ const user = await User.findOne({ username: req.params.username });
+
+ if (!user)
+ return res.status(404).json({ error: 'User not found'});
+
+ return res.status(200).json(user);
+ }
+ catch (error) {
+ return res.status(500).json({ error: `An error occured whilst getting the singular user: ${error.message}`});
+ }
+});
+
+/**
+ * ! GET Get All Users
+ *
+ * Gets all users from the database.
+ *
+ * @async
+ * @returns {JSON} Responds with a list of all users in JSON format.
+ * @throws {500} If an error occurs whilst fetching users from the database.
+ */
+router.get('/', async function (req, res) {
+ try {
+ // Find all the users
+ const users = await User.find({});
+
+ // Respond 200 with JSON list containing all Users.
+ return res.status(200).json(users);
+ }
+ catch (error) {
+ // Handle general errors 500
+ return res.status(500).json({ error: `An error occured while getting all Users: ${error.message}` });
+ }
+});
+
+
+// Export the router
+module.exports = router;
\ No newline at end of file
diff --git a/backend/server.js b/backend/server.js
index 3799cfc0..9f5f1c79 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -1,135 +1,135 @@
-// Load environment variables
-require("dotenv").config();
-
-// Module Imports
-const express = require("express");
-const mongoose = require("mongoose");
-const cron = require("node-cron");
-const cors = require("cors");
-const app = express();
-const cookieParser = require('cookie-parser');
-const tagManager = require('./services/tagManager.service');
-const aiOverviewService = require("./services/aiOverview.service");
-const { exec } = require('child_process');
-const path = require("path");
-
-// Router Imports
-const UnitRouter = require('./routes/units');
-const ReviewRouter = require('./routes/reviews');
-const AuthRouter = require('./routes/auth');
-const NotificationRouter = require('./routes/notifications');
-const GitHubRouter = require("./routes/github");
-const SetuRouter = require("./routes/setus");
-
-// === Environment Configuration ===
-const isDevelopment = process.env.DEVELOPMENT === 'true';
-console.log(`Running in ${isDevelopment ? 'DEVELOPMENT' : 'PRODUCTION'} mode`);
-
-// === Middleware ===
-if (isDevelopment) {
- app.use(
- cors({
- origin: "http://localhost:4200",
- credentials: true,
- })
- );
-}
-
-app.use(express.json({ limit: "50mb" })); // Increased payload limit for JSON requests.
-app.use(express.urlencoded({ limit: "50mb", extended: true })); // Increased payload limit for URL-encoded requests.
-app.use(cookieParser());
-
-// Response handler middlware
-app.use((obj, req, res, next) => {
- const statusCode = obj.status || 500;
- const message = obj.message || "Internal server error";
- return res.status(statusCode, {
- success: [200, 201, 204].some((a) => a === obj.status) ? true : false,
- status: statusCode,
- message: message,
- data: obj.data,
- });
-});
-
-// === Routes ===
-app.use('/api/v1/units', UnitRouter);
-app.use('/api/v1/reviews', ReviewRouter);
-app.use('/api/v1/auth', AuthRouter);
-app.use('/api/v1/notifications', NotificationRouter);
-app.use('/api/v1/github', GitHubRouter);
-app.use('/api/v1/setus', SetuRouter);
-
-// === Serving Static Files (Production Mode) ===
-if (!isDevelopment) {
- app.use(express.static(path.join(__dirname, '../frontend/dist/frontend/browser')));
-}
-
-// === Connect to MongoDB ===
-const url = process.env.MONGODB_CONN_STRING;
-async function connect(url) {
- await mongoose.connect(url);
-}
-connect(url)
- .then(() => {
- console.log("Connected to MongoDB Database");
- tagManager.updateMostReviewsTag(1);
- })
- .catch((error) => console.log(error));
-
-// === Services ===
-// Update the most reviews tag every hour
-cron.schedule("0 * * * *", async function () {
- await tagManager.updateMostReviewsTag(1);
-});
-
-// Generate sitemaps daily at 3:00 AM
-cron.schedule("0 3 * * *", function () {
- console.log("[Cron] Running daily sitemap generation...");
-
- // Path to the sitemap generator script
- const scriptPath = path.join(__dirname, "utils", "generate-sitemap.js");
-
- // Use Node to execute the script
- exec(`node ${scriptPath}`, (error, stdout, stderr) => {
- if (error) {
- console.error(`[Cron] Sitemap generation error: ${error.message}`);
- return;
- }
-
- if (stderr) {
- console.error(`[Cron] Sitemap stderr: ${stderr}`);
- return;
- }
-
- console.log(`[Cron] Sitemap generation complete: ${stdout}`);
- });
-});
-
-
-// Regenerate AI unit overviews ahead of each semester (Feb 1 & Jun 1 at 02:00)
-cron.schedule('0 2 1 2 *', async function () {
- console.log('[Cron] Running Semester 1 AI overview refresh');
- await aiOverviewService.generateOverviewsForAllUnits({ force: true, delayMs: 750 });
-});
-
-cron.schedule('0 2 1 6 *', async function () {
- console.log('[Cron] Running Semester 2 AI overview refresh');
- await aiOverviewService.generateOverviewsForAllUnits({ force: true, delayMs: 750 });
-});
-
-
-// === Catch all route (Production Mode) ===
-if (!isDevelopment) {
- app.get('*', (req, res) => {
- return res.sendFile(path.join(__dirname, '../frontend/dist/frontend/browser/index.html'));
- });
-}
-
-
-// === Start Server ===
-const PORT = process.env.PORT || 8080; // Default to 8080 if no port specified
-app.listen(PORT, (error) => {
- if (error) console.log(error);
-
- console.log(`Server running on port ${PORT}`);
-});
+// Load environment variables
+require("dotenv").config();
+
+// Module Imports
+const express = require("express");
+const mongoose = require("mongoose");
+const cron = require("node-cron");
+const cors = require("cors");
+const app = express();
+const cookieParser = require('cookie-parser');
+const tagManager = require('./services/tagManager.service');
+const aiOverviewService = require("./services/aiOverview.service");
+const { exec } = require('child_process');
+const path = require("path");
+
+// Router Imports
+const UnitRouter = require('./routes/units');
+const ReviewRouter = require('./routes/reviews');
+const AuthRouter = require('./routes/auth');
+const NotificationRouter = require('./routes/notifications');
+const GitHubRouter = require("./routes/github");
+const SetuRouter = require("./routes/setus");
+
+// === Environment Configuration ===
+const isDevelopment = process.env.DEVELOPMENT === 'true';
+console.log(`Running in ${isDevelopment ? 'DEVELOPMENT' : 'PRODUCTION'} mode`);
+
+// === Middleware ===
+if (isDevelopment) {
+ app.use(
+ cors({
+ origin: "http://localhost:4200",
+ credentials: true,
+ })
+ );
+}
+
+app.use(express.json({ limit: "50mb" })); // Increased payload limit for JSON requests.
+app.use(express.urlencoded({ limit: "50mb", extended: true })); // Increased payload limit for URL-encoded requests.
+app.use(cookieParser());
+
+// Response handler middlware
+app.use((obj, req, res, next) => {
+ const statusCode = obj.status || 500;
+ const message = obj.message || "Internal server error";
+ return res.status(statusCode, {
+ success: [200, 201, 204].some((a) => a === obj.status) ? true : false,
+ status: statusCode,
+ message: message,
+ data: obj.data,
+ });
+});
+
+// === Routes ===
+app.use('/api/v1/units', UnitRouter);
+app.use('/api/v1/reviews', ReviewRouter);
+app.use('/api/v1/auth', AuthRouter);
+app.use('/api/v1/notifications', NotificationRouter);
+app.use('/api/v1/github', GitHubRouter);
+app.use('/api/v1/setus', SetuRouter);
+
+// === Serving Static Files (Production Mode) ===
+if (!isDevelopment) {
+ app.use(express.static(path.join(__dirname, '../frontend/dist/frontend/browser')));
+}
+
+// === Connect to MongoDB ===
+const url = process.env.MONGODB_CONN_STRING;
+async function connect(url) {
+ await mongoose.connect(url);
+}
+connect(url)
+ .then(() => {
+ console.log("Connected to MongoDB Database");
+ tagManager.updateMostReviewsTag(1);
+ })
+ .catch((error) => console.log(error));
+
+// === Services ===
+// Update the most reviews tag every hour
+cron.schedule("0 * * * *", async function () {
+ await tagManager.updateMostReviewsTag(1);
+});
+
+// Generate sitemaps daily at 3:00 AM
+cron.schedule("0 3 * * *", function () {
+ console.log("[Cron] Running daily sitemap generation...");
+
+ // Path to the sitemap generator script
+ const scriptPath = path.join(__dirname, "utils", "generate-sitemap.js");
+
+ // Use Node to execute the script
+ exec(`node ${scriptPath}`, (error, stdout, stderr) => {
+ if (error) {
+ console.error(`[Cron] Sitemap generation error: ${error.message}`);
+ return;
+ }
+
+ if (stderr) {
+ console.error(`[Cron] Sitemap stderr: ${stderr}`);
+ return;
+ }
+
+ console.log(`[Cron] Sitemap generation complete: ${stdout}`);
+ });
+});
+
+
+// Regenerate AI unit overviews ahead of each semester (Feb 1 & Jun 1 at 02:00)
+cron.schedule('0 2 1 2 *', async function () {
+ console.log('[Cron] Running Semester 1 AI overview refresh');
+ await aiOverviewService.generateOverviewsForAllUnits({ force: true, delayMs: 750 });
+});
+
+cron.schedule('0 2 1 6 *', async function () {
+ console.log('[Cron] Running Semester 2 AI overview refresh');
+ await aiOverviewService.generateOverviewsForAllUnits({ force: true, delayMs: 750 });
+});
+
+
+// === Catch all route (Production Mode) ===
+if (!isDevelopment) {
+ app.get('*', (req, res) => {
+ return res.sendFile(path.join(__dirname, '../frontend/dist/frontend/browser/index.html'));
+ });
+}
+
+
+// === Start Server ===
+const PORT = process.env.PORT || 8080; // Default to 8080 if no port specified
+app.listen(PORT, (error) => {
+ if (error) console.log(error);
+
+ console.log(`Server running on port ${PORT}`);
+});
diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts
index fb3c6487..06616fbd 100644
--- a/frontend/src/app/app.routes.ts
+++ b/frontend/src/app/app.routes.ts
@@ -1,37 +1,40 @@
-import { Routes } from '@angular/router';
-
-// Component Imports
-import { HomeComponent } from './routes/home/home.component';
-import { UnitListComponent } from './routes/unit-list/unit-list.component';
-import { UnitOverviewComponent } from './routes/unit-overview/unit-overview.component';
-import { VerifiedComponent } from './routes/verified/verified.component';
-import { NotFoundComponent } from './routes/not-found/not-found.component';
-import { ResetPasswordComponent } from './routes/reset-password/reset-password.component';
-import { UnitMapComponent } from './routes/unit-map/unit-map.component';
-import { TermsAndCondsComponent } from './routes/terms-and-conds/terms-and-conds.component';
-import { AboutComponent } from './routes/about/about.component';
-import { SetuOverviewComponent } from './routes/setu-overview/setu-overview.component';
-
-export const routes: Routes = [
- // Homepage
- { path: '', component: HomeComponent },
- // Unit List
- { path: 'list', component: UnitListComponent },
- // Unit Overview
- { path: 'unit/:unitcode', component: UnitOverviewComponent },
- // Unit Map
- { path: 'map/:unitcode', component: UnitMapComponent },
- // SETU Data
- { path: 'setu/:unitCode', component: SetuOverviewComponent },
- // Email Verification
- { path: 'verify-email/:token', component: VerifiedComponent },
- // Reset Password
- { path: 'reset-password/:token', component: ResetPasswordComponent },
- // Terms and Conditions Page
- { path: 'terms-and-conditions', component: TermsAndCondsComponent },
- // About Page
- { path: 'about', component: AboutComponent },
-
- // 404 Not Found for all other routes
- { path: '**', component: NotFoundComponent },
-];
+import { Routes } from '@angular/router';
+
+// Component Imports
+import { HomeComponent } from './routes/home/home.component';
+import { UnitListComponent } from './routes/unit-list/unit-list.component';
+import { UnitOverviewComponent } from './routes/unit-overview/unit-overview.component';
+import { UserOverviewComponent } from './routes/user-overview/user-overview.component';
+import { VerifiedComponent } from './routes/verified/verified.component';
+import { NotFoundComponent } from './routes/not-found/not-found.component';
+import { ResetPasswordComponent } from './routes/reset-password/reset-password.component';
+import { UnitMapComponent } from './routes/unit-map/unit-map.component';
+import { TermsAndCondsComponent } from './routes/terms-and-conds/terms-and-conds.component';
+import { AboutComponent } from './routes/about/about.component';
+import { SetuOverviewComponent } from './routes/setu-overview/setu-overview.component';
+
+export const routes: Routes = [
+ // Homepage
+ { path: '', component: HomeComponent },
+ // Unit List
+ { path: 'list', component: UnitListComponent },
+ // Unit Overview
+ { path: 'unit/:unitcode', component: UnitOverviewComponent },
+ // User Overview
+ { path: 'user/:username', component: UserOverviewComponent },
+ // Unit Map
+ { path: 'map/:unitcode', component: UnitMapComponent },
+ // SETU Data
+ { path: 'setu/:unitCode', component: SetuOverviewComponent },
+ // Email Verification
+ { path: 'verify-email/:token', component: VerifiedComponent },
+ // Reset Password
+ { path: 'reset-password/:token', component: ResetPasswordComponent },
+ // Terms and Conditions Page
+ { path: 'terms-and-conditions', component: TermsAndCondsComponent },
+ // About Page
+ { path: 'about', component: AboutComponent },
+
+ // 404 Not Found for all other routes
+ { path: '**', component: NotFoundComponent },
+];
diff --git a/frontend/src/app/routes/user-overview/user-overview.component.html b/frontend/src/app/routes/user-overview/user-overview.component.html
new file mode 100644
index 00000000..19814c3a
--- /dev/null
+++ b/frontend/src/app/routes/user-overview/user-overview.component.html
@@ -0,0 +1,143 @@
+
+