diff --git a/backend/package-lock.json b/backend/package-lock.json index 5eed52f..a2342e8 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,7 +15,9 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", - "jsonwebtoken": "^9.0.2", + "jsonwebtoken": "^9.0.3", + "jwa": "^2.0.1", + "jws": "^4.0.1", "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.16", "prisma": "^5.20.0" @@ -196,7 +198,8 @@ "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" }, "node_modules/buffer-from": { "version": "1.1.2", @@ -414,6 +417,7 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" } @@ -789,12 +793,12 @@ "license": "MIT" }, "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "license": "MIT", "dependencies": { - "jws": "^3.2.2", + "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", @@ -816,21 +820,23 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -1124,6 +1130,7 @@ "integrity": "sha512-6obb3ucKgAnsGS9x9gLOe8qa51XxvJ3vLQtmyf52CTey1Qcez3A6W6ROH5HIz5Q5bW+0VpmZb8WBohieMFGpig==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/engines": "5.20.0" }, diff --git a/backend/package.json b/backend/package.json index 1dc9df7..fa8dbfb 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,7 +6,9 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", - "jsonwebtoken": "^9.0.2", + "jsonwebtoken": "^9.0.3", + "jwa": "^2.0.1", + "jws": "^4.0.1", "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.16", "prisma": "^5.20.0" diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 30a90a6..ad59f92 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -1,4 +1,3 @@ -// Generator to create Prisma Client generator client { provider = "prisma-client-js" binaryTargets = ["native", "darwin-arm64", "linux-musl-arm64-openssl-3.0.x"] @@ -40,7 +39,7 @@ enum Designation { model User { profileId String @id @default(uuid()) userName String - email String @unique @db.Text + email String @unique password String institute Institute? @@ -54,9 +53,9 @@ model User { } model Application { - applicationId String @id @default(uuid()) + applicationId String @id @default(uuid()) applicantId String - applicant User @relation("AppliedApplications", fields: [applicantId], references: [profileId]) + applicant User @relation("AppliedApplications", fields: [applicantId], references: [profileId]) institute Institute department String diff --git a/backend/prisma/seed.js b/backend/prisma/seed.js index 1dfc3fc..eba06db 100644 --- a/backend/prisma/seed.js +++ b/backend/prisma/seed.js @@ -1,10 +1,16 @@ -import prisma from "../src/config/prismaConfig.js"; +import { PrismaClient } from "@prisma/client"; +import bcrypt from "bcryptjs"; + +const prisma = new PrismaClient(); async function main() { - // Common password for all users + // Use a common password for all development users to make testing easier const commonPassword = "securePassword123"; - // Applicant and Validator data + // Secure the password with hashing even for seed data! + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(commonPassword, salt); + const institutes = [ "KJSIDS", "SKSC", @@ -28,96 +34,116 @@ async function main() { "Library", ]; - // Create VC (single, no department or institute) - console.log("Seeding VC..."); - await prisma.user.create({ - data: { - userName: "Validator_VC", - email: "vc@example.com", - password: commonPassword, + console.log("Seeding started..."); + + // VC is usually global + // We use 'upsert' here. It means "Update if exists, Insert if not". + // This prevents errors if we run the seed script multiple times. + await prisma.user.upsert({ + where: { email: "vc@somaiya.edu" }, + update: {}, + create: { + userName: "Vice Chancellor", + email: "vc@somaiya.edu", + password: hashedPassword, + institute: "KJSCE", + department: "Management", designation: "VC", }, }); for (const institute of institutes) { - // Create HOI for each institute + // 1. Create HOI for this institute console.log(`Seeding HOI for ${institute}...`); - const hoiEmail = `hoi.${institute.toLowerCase()}@example.com`; - - await prisma.user.create({ - data: { + const hoiEmail = `hoi@${institute.toLowerCase()}.edu`; + await prisma.user.upsert({ + where: { email: hoiEmail }, + update: {}, + create: { userName: `HOI_${institute}`, email: hoiEmail, - password: commonPassword, + password: hashedPassword, institute, + department: "Administration", designation: "HOI", }, }); - // Create Accounts for each institute + // 2. Create Accounts for this institute console.log(`Seeding Accounts for ${institute}...`); - await prisma.user.create({ - data: { + const accountsEmail = `accounts.${institute.toLowerCase()}@example.com`; + await prisma.user.upsert({ + where: { email: accountsEmail }, + update: {}, + create: { userName: `Validator_Accounts_${institute}`, - email: `accounts.${institute.toLowerCase()}@example.com`, - password: commonPassword, + email: accountsEmail, + password: hashedPassword, institute, designation: "ACCOUNTS", }, }); for (const department of departments) { - // Create HOD for each department of each institute + // 3. Create HOD for each department of each institute console.log(`Seeding HOD for ${department} in ${institute}...`); const hodEmail = `hod.${department.toLowerCase()}.${institute.toLowerCase()}@example.com`; - - await prisma.user.create({ - data: { + await prisma.user.upsert({ + where: { email: hodEmail }, + update: {}, + create: { userName: `HOD_${department}_${institute}`, email: hodEmail, - password: commonPassword, + password: hashedPassword, institute, department, designation: "HOD", }, }); - // Create Faculty for each department of each institute + // 4. Create Faculty for each department of each institute console.log(`Seeding Faculty for ${department} in ${institute}...`); const facultyEmail = `faculty.${department.toLowerCase()}.${institute.toLowerCase()}@example.com`; - - await prisma.user.create({ - data: { + await prisma.user.upsert({ + where: { email: facultyEmail }, + update: {}, + create: { userName: `Faculty_${department}_${institute}`, email: facultyEmail, - password: commonPassword, + password: hashedPassword, institute, department, designation: "FACULTY", }, }); - // Create Student for each department of each institute + + // 5. Create Student for each department of each institute console.log(`Seeding Student for ${department} in ${institute}...`); const studentEmail = `student.${department.toLowerCase()}.${institute.toLowerCase()}@example.com`; - - await prisma.user.create({ - data: { + await prisma.user.upsert({ + where: { email: studentEmail }, + update: {}, + create: { userName: `Student_${department}_${institute}`, email: studentEmail, - password: commonPassword, + password: hashedPassword, institute, department, designation: "STUDENT", }, }); } - console.log("Seeding completed!"); } + + console.log("Seeding completed successfully."); } -main().catch((e) => { - console.error(e); - process.exit(1); -}).finally(async () => { - await prisma.$disconnect(); -}); \ No newline at end of file +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/backend/src/controllers/applicantControllers.js b/backend/src/controllers/applicantControllers.js index 253964e..27f4d94 100644 --- a/backend/src/controllers/applicantControllers.js +++ b/backend/src/controllers/applicantControllers.js @@ -55,11 +55,16 @@ const createApplication = async (req, res) => { }); if (!intimationApplication) { - return res.status(404).send({ message: "Intimation Application not found" }); + return res + .status(404) + .send({ message: "Intimation Application not found" }); } - if ( intimationApplication["formName"] !== "Travel Intimation Form") { - return res.status(400).send({ message: "Intimation Application ID is not of a Travel Intimation Form" }); + if (intimationApplication["formName"] !== "Travel Intimation Form") { + return res.status(400).send({ + message: + "Intimation Application ID is not of a Travel Intimation Form", + }); } const validationFields = [ @@ -111,7 +116,9 @@ const createApplication = async (req, res) => { }, }); if (!primarySupervisor) { - return res.status(404).send({ message: "Faculty not found (Incorrect Primary Supervisor Email)" }); + return res.status(404).send({ + message: "Faculty not found (Incorrect Primary Supervisor Email)", + }); } anotherSupervisor = await prisma.user.findUnique({ where: { @@ -161,7 +168,10 @@ const createApplication = async (req, res) => { if (applicant.designation === "STUDENT") { // Validate that primary supervisor email is provided for student applications if (!formData.primarySupervisorEmail) { - return res.status(400).send({ message: "Primary supervisor email is required for student applications" }); + return res.status(400).send({ + message: + "Primary supervisor email is required for student applications", + }); } } @@ -215,7 +225,7 @@ const createApplication = async (req, res) => { department, institute, totalExpense, - formData, + formData: JSON.stringify(formData), proofOfTravel: proofOfTravelBuffer, proofOfAccommodation: proofOfAccommodationBuffer, proofOfAttendance: proofOfAttendanceBuffer, @@ -312,7 +322,7 @@ const updateApplication = async (req, res) => { return res.status(404).send({ message: "Application not found" }); } - const originalFormData = originalApplication.formData; + const originalFormData = JSON.parse(originalApplication.formData); // Verify that disabled fields haven't been changed // Only expenses can be edited, everything else should remain the same regardless of resubmission status @@ -400,7 +410,7 @@ const updateApplication = async (req, res) => { const updatedData = { totalExpense, - formData, + formData: JSON.stringify(formData), proofOfTravel: proofOfTravelBuffer, proofOfAccommodation: proofOfAccommodationBuffer, proofOfAttendance: proofOfAttendanceBuffer, diff --git a/backend/src/controllers/authControllers.js b/backend/src/controllers/authControllers.js index d0e3e58..0c3d0db 100644 --- a/backend/src/controllers/authControllers.js +++ b/backend/src/controllers/authControllers.js @@ -1,5 +1,6 @@ import prisma from "../config/prismaConfig.js"; import generateToken from "../services/generateToken.js"; +import bcrypt from "bcryptjs"; const applicantLogin = async (req, res) => { try { @@ -8,7 +9,7 @@ const applicantLogin = async (req, res) => { // Check if the applicant profile exists const validProfile = await prisma.user.findUnique({ where: { - email + email, }, }); @@ -19,14 +20,51 @@ const applicantLogin = async (req, res) => { }); } - // Check if the password is correct - if (validProfile.password !== password) { + let isPasswordCorrect = false; + let needsMigration = false; + + // Step 1: Try checking if it's a hashed password + try { + isPasswordCorrect = await bcrypt.compare(password, validProfile.password); + } catch (err) { + isPasswordCorrect = false; + } + + // Step 2: If the bcrypt check failed, maybe they have an old plain-text password? + // This is a "Fallback" mechanism to prevent old users from getting locked out after we added hashing. + if (!isPasswordCorrect) { + if (validProfile.password === password) { + isPasswordCorrect = true; + needsMigration = true; // Flag them for migration to secure hash + } + } + + if (!isPasswordCorrect) { return res.status(404).json({ message: "Wrong Password", data: null, }); } + // Step 3: If they were using a plain-text password, let's secure it now! + // We automatically update their password to a hash so next time they log in safely. + if (needsMigration) { + try { + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + await prisma.user.update({ + where: { profileId: validProfile.profileId }, + data: { password: hashedPassword }, + }); + console.log( + `[Security] Automatically migrated password for user ${email}` + ); + } catch (migrationError) { + console.log("Could not update password", migrationError); + // It's checked, so we just log the error and continue + } + } + // Create token object const tokenObject = { id: validProfile.profileId, @@ -41,7 +79,11 @@ const applicantLogin = async (req, res) => { // Set the token as a cookie return res - .cookie("access_token", token, { sameSite: 'None', secure: true, httpOnly: true }) + .cookie("access_token", token, { + sameSite: "Lax", + secure: false, + httpOnly: true, + }) .status(200) .json({ message: "Login Successful", @@ -62,7 +104,7 @@ const validatorLogin = async (req, res) => { // Check if the validator profile exists let validProfile = await prisma.user.findUnique({ where: { - email + email, }, }); @@ -73,14 +115,48 @@ const validatorLogin = async (req, res) => { }); } - // Check if the password is correct - if (validProfile.password !== password) { + let isPasswordCorrect = false; + let needsMigration = false; + + // Step 1: Try checking if it's a hashed password + try { + isPasswordCorrect = await bcrypt.compare(password, validProfile.password); + } catch (err) { + isPasswordCorrect = false; + } + + // Step 2: If the bcrypt check failed, maybe they have an old plain-text password? + // We check this so old users don't get locked out. + if (!isPasswordCorrect) { + if (validProfile.password === password) { + isPasswordCorrect = true; + needsMigration = true; + } + } + + if (!isPasswordCorrect) { return res.status(404).json({ message: "Wrong Password", data: null, }); } + // Step 3: If they had a plain-text password, let's secure it now! + // We update it to a hash so next time it is safe. + if (needsMigration) { + try { + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + await prisma.user.update({ + where: { profileId: validProfile.profileId }, + data: { password: hashedPassword }, + }); + console.log(`Updated password for validator ${email}`); + } catch (migrationError) { + console.log("Could not update password", migrationError); + } + } + // Create token object const tokenObject = { id: validProfile.profileId, @@ -95,7 +171,11 @@ const validatorLogin = async (req, res) => { // Set the token as a cookie return res - .cookie("access_token", token, { sameSite: 'None', secure: true, httpOnly: true }) + .cookie("access_token", token, { + sameSite: "Lax", + secure: false, + httpOnly: true, + }) .status(200) .json({ message: "Login Successful", diff --git a/backend/src/controllers/generalControllers.js b/backend/src/controllers/generalControllers.js index 64b7a84..6e81e82 100644 --- a/backend/src/controllers/generalControllers.js +++ b/backend/src/controllers/generalControllers.js @@ -4,6 +4,7 @@ import { applicantDesignations, validatorDesignations, } from "../config/designations.js"; +import bcrypt from "bcryptjs"; const dataRoot = async (req, res) => { try { @@ -98,7 +99,12 @@ const getApplicationsByStatus = async (req, res) => { }), ...(status === "ACCEPTED" && { AND: [ - { OR: [{ facultyValidation: "ACCEPTED" }, { facultyValidation: null }] }, + { + OR: [ + { facultyValidation: "ACCEPTED" }, + { facultyValidation: null }, + ], + }, { OR: [{ hodValidation: "ACCEPTED" }, { hodValidation: null }] }, { OR: [{ hoiValidation: "ACCEPTED" }, { hoiValidation: null }] }, { OR: [{ vcValidation: "ACCEPTED" }, { vcValidation: null }] }, @@ -185,15 +191,18 @@ const getApplicationsByStatus = async (req, res) => { } // Format response with selected fields - const responseApplications = applications.map((application) => ({ - applicationId: application.applicationId, - applicantName: application.applicantName, - formData: { - eventName: application.formData.eventName, - applicantDepartment: application.formData.applicantDepartment, - }, - createdAt: application.createdAt, - })); + const responseApplications = applications.map((application) => { + const parsedFormData = JSON.parse(application.formData); + return { + applicationId: application.applicationId, + applicantName: application.applicantName, + formData: { + eventName: parsedFormData.eventName, + applicantDepartment: parsedFormData.applicantDepartment, + }, + createdAt: application.createdAt, + }; + }); return res.status(200).json({ message: `${status} Applications Fetched Successfully`, @@ -308,9 +317,14 @@ const getApplicationData = async (req, res) => { } // Respond with the full application data and current status + const parsedApplicationFull = { + ...applicationFull, + formData: JSON.parse(applicationFull.formData), + }; + return res.status(200).json({ message: "Application data retrieved successfully", - data: { ...applicationFull, currentStatus }, + data: { ...parsedApplicationFull, currentStatus }, }); } catch (error) { console.error("Error retrieving application data:", error); @@ -436,4 +450,62 @@ const getFile = async (req, res) => { } }; -export { getApplicationData, getFile, dataRoot, getApplicationsByStatus }; +export { + getApplicationData, + getFile, + dataRoot, + getApplicationsByStatus, + changePassword, +}; + +const changePassword = async (req, res) => { + try { + const user = req.user; + const { oldPassword, newPassword } = req.body; + + if (!user || !user.id) { + return res.status(401).json({ message: "Unauthorized" }); + } + + // Get the current user from DB to check password + const dbUser = await prisma.user.findUnique({ + where: { profileId: user.id }, + }); + + if (!dbUser) { + return res.status(404).json({ message: "User not found" }); + } + + let isPasswordCorrect = false; + + // 1. Try bcrypt + try { + isPasswordCorrect = await bcrypt.compare(oldPassword, dbUser.password); + } catch (err) { + isPasswordCorrect = false; + } + + // 2. Try plaintext (fallback) + if (!isPasswordCorrect && dbUser.password === oldPassword) { + isPasswordCorrect = true; + } + + if (!isPasswordCorrect) { + return res.status(400).json({ message: "Incorrect old password" }); + } + + // Hash the new password + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(newPassword, salt); + + await prisma.user.update({ + where: { profileId: user.id }, + data: { password: hashedPassword }, + }); + + return res.status(200).json({ message: "Password updated successfully" }); + } catch (error) { + console.error(error); + return res.status(500).json({ message: "Internal Server Error" }); + } +}; diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index 3e56d6d..00ad9b0 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -1,5 +1,9 @@ -import express from 'express'; -import { applicantLogin, logout, validatorLogin } from '../controllers/authControllers.js'; +import express from "express"; +import { + applicantLogin, + logout, + validatorLogin, +} from "../controllers/authControllers.js"; const router = express.Router(); @@ -8,4 +12,8 @@ router.post('/validator-login', validatorLogin); router.get('/logout', logout) -export default router; \ No newline at end of file +router.get("/", (req, res) => { + res.send("Travel Policy Backend is Running!"); +}); + +export default router; diff --git a/backend/src/routes/general.js b/backend/src/routes/general.js index d33580a..8d19f80 100644 --- a/backend/src/routes/general.js +++ b/backend/src/routes/general.js @@ -1,11 +1,19 @@ -import express from 'express'; -import { dataRoot, getApplicationData, getApplicationsByStatus, getFile } from '../controllers/generalControllers.js'; +import express from "express"; +import { + dataRoot, + getApplicationData, + getApplicationsByStatus, + getFile, + changePassword, +} from "../controllers/generalControllers.js"; const router = express.Router(); router.get("/dataRoot", dataRoot ); -router.get('/getApplications/:status', getApplicationsByStatus); +router.post("/changePassword", changePassword); + +router.get("/getApplications/:status", getApplicationsByStatus); router.get("/getApplicationData/:applicationId", getApplicationData); diff --git a/backend/src/services/sendMail.js b/backend/src/services/sendMail.js index b423e47..d057fb3 100644 --- a/backend/src/services/sendMail.js +++ b/backend/src/services/sendMail.js @@ -1,9 +1,17 @@ import nodeMailer from 'nodemailer'; -export default async function sendMail({ emailId, link, type, status, designation }) { - if (!process.env.TravelPolicyEmail || !process.env.TravelPolicyEmailPass) { - return; - } +export default async function sendMail({ + emailId, + link, + type, + status, + designation, +}) { + // Check if we have email password in env + if (!process.env.TravelPolicyEmail || !process.env.TravelPolicyEmailPass) { + console.log("No email password found in .env, so I can't send emails."); + return; + } console.log("parametrs", emailId, link, type, status, designation); @@ -48,20 +56,20 @@ export default async function sendMail({ emailId, link, type, status, designatio View Application

Thank you.

`, - }; - break; - case 'accounts': - mailOptions = { - from: process.env.TravelPolicyEmail, - to: emailId, - subject: 'Transfer money to the applicant', - html: ` -

Please transfer the travel policy amount to the applicant's account. Click on the link below to view the application:

` - } - break; - default: - throw new Error('Invalid email type'); - } + }; + break; + case "accounts": + mailOptions = { + from: process.env.TravelPolicyEmail, + to: emailId, + subject: "Transfer money to the applicant", + html: ` +

Please transfer the travel policy amount to the applicant's account. Click on the link below to view the application:

`, + }; + break; + default: + throw new Error("Invalid email type"); + } try { await transporter.sendMail(mailOptions); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b893788..81974bb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -92,6 +92,7 @@ "version": "7.24.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -405,6 +406,7 @@ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", "license": "MIT", + "peer": true, "dependencies": { "@emotion/memoize": "^0.9.0" } @@ -1716,6 +1718,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2153,6 +2156,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001646", "electron-to-chromium": "^1.5.4", @@ -2252,6 +2256,7 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz", "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==", "license": "MIT", + "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -2868,6 +2873,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5138,6 +5144,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -5355,6 +5362,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5376,6 +5384,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5502,6 +5511,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -6169,6 +6179,7 @@ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-module-imports": "^7.0.0", "@babel/traverse": "^7.4.5", @@ -6458,6 +6469,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6791,6 +6803,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b8c5d06..121a22b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,6 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import React, { Suspense } from "react"; +import { ThemeProvider } from "./context/ThemeContext"; import "./App.css"; import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; @@ -15,7 +16,10 @@ const Applications = React.lazy(() => import("./pages/Applications/Applications" const Report = React.lazy(() => import("./pages/Report/Report")); const LoginRoot = React.lazy(() => import("./components/LoginRoot/LoginRoot")); const ContactUs = React.lazy(() => import("./pages/ContactUs/ContactUs")); -const ApplicationView = React.lazy(() => import("./pages/ApplicationView/ApplicationView")); +const ApplicationView = React.lazy(() => + import("./pages/ApplicationView/ApplicationView") +); +const Settings = React.lazy(() => import("./pages/Settings/Settings")); import userDataLoader from "./services/userDataLoader"; import { upsertApplicationAction } from "./services/upsertApplicationAction"; @@ -41,9 +45,14 @@ const router = createBrowserRouter([ children: [ { path: "dashboard", element: }, { path: "dashboard/:status", element: }, - { path: "dashboard/:status/:applicationId", element: , action: upsertApplicationAction }, + { + path: "dashboard/:status/:applicationId", + element: , + action: upsertApplicationAction, + }, { path: "form", element:
, action: upsertApplicationAction }, { path: "contact-us", element: }, + { path: "settings", element: }, { path: "policy", element: }, ], }, @@ -55,8 +64,13 @@ const router = createBrowserRouter([ children: [ { path: "dashboard", element: }, { path: "dashboard/:status", element: }, - { path: "dashboard/:status/:applicationId", element: , action: applicationStatusAction }, + { + path: "dashboard/:status/:applicationId", + element: , + action: applicationStatusAction, + }, { path: "report", element: }, + { path: "settings", element: }, { path: "policy", element: }, ], }, @@ -64,12 +78,12 @@ const router = createBrowserRouter([ function App() { return ( - <> + }> - + ); } diff --git a/frontend/src/components/DashboardRoot/Root.jsx b/frontend/src/components/DashboardRoot/Root.jsx index 311e1d8..95b5ef8 100644 --- a/frontend/src/components/DashboardRoot/Root.jsx +++ b/frontend/src/components/DashboardRoot/Root.jsx @@ -32,9 +32,17 @@ const Root = () => { return (
- -
- {sidebarIsVisible && !(urlPath.split("/").at(-1).includes("dashboard")) && } + +
+ {sidebarIsVisible && + !urlPath.split("/").at(-1).includes("dashboard") && ( + + )}
diff --git a/frontend/src/components/DashboardRoot/Sidebar.jsx b/frontend/src/components/DashboardRoot/Sidebar.jsx index 44f770f..34ee3c8 100644 --- a/frontend/src/components/DashboardRoot/Sidebar.jsx +++ b/frontend/src/components/DashboardRoot/Sidebar.jsx @@ -1,258 +1,388 @@ -import React from "react"; -import { FaSignOutAlt } from "react-icons/fa"; +import { FaSignOutAlt, FaMoon, FaSun } from "react-icons/fa"; +import { useTheme } from "../../context/ThemeContext"; import { Link, NavLink } from "react-router-dom"; +import { toast } from "react-toastify"; -export const handleLogout = async () => { - let res = await fetch(`${import.meta.env.VITE_APP_API_URL}/logout`, { - method: "GET", - credentials: "include", - }); +const handleLogout = async () => { + // Step 1: Call the backend logout route + // We need to do this to clear the httpOnly cookie from the browser + try { + let res = await fetch(`${import.meta.env.VITE_APP_API_URL}/logout`, { + method: "GET", + credentials: "include", // Important: This ensures the cookie is sent/cleared + }); - return res; + // Step 2: If successful, we can redirect or show a message + if (res.ok) { + toast.info("Logged out successfully"); + } + return res; + } catch (error) { + // console.log("Logout failed", error); + toast.error("Logout failed, please check your connection."); + } }; -const Sidebar = ({ role }) => ( -
-
-
-

- {`${role} Portal`} -

-

- Travel Policy SVU -

+const Sidebar = ({ role }) => { + // Using our custom ThemeContext to switch between Light and Dark mode + const { theme, toggleTheme } = useTheme(); + + return ( +
+
+
+

+ {`${role} Portal`} +

+

+ Travel Policy SVU +

+
+
+ + + {/* Spacer to push logout to the bottom */} +
+ {/* Theme Toggle */} + + + + + Logout +
- - - {/* Spacer to push logout to the bottom */} -
- - - Logout - -
-
-); + ); +}; export default Sidebar; diff --git a/frontend/src/components/LoginRoot/LoginRoot.jsx b/frontend/src/components/LoginRoot/LoginRoot.jsx index 0bd7ad1..82ce248 100644 --- a/frontend/src/components/LoginRoot/LoginRoot.jsx +++ b/frontend/src/components/LoginRoot/LoginRoot.jsx @@ -40,7 +40,7 @@ const LoginRoot = () => {
- ©2024 KJSCE, All Rights Reserved. + ©{new Date().getFullYear()} KJSCE, All Rights Reserved.
diff --git a/frontend/src/components/Modal/Modal.jsx b/frontend/src/components/Modal/Modal.jsx index 8396b61..3afcae1 100644 --- a/frontend/src/components/Modal/Modal.jsx +++ b/frontend/src/components/Modal/Modal.jsx @@ -7,13 +7,13 @@ const Modal = ({ onClose, children }) => { onClick={onClose} >
e.stopPropagation()} >
+
- {/* Mission & Vision */} -
+ {/* + Our Approach Section: + Explains the philosophy behind the travel policy. + */} +
-

Our Approach

-

+

Our Approach

+

Our travel policy for research students and associates ensures a structured process for approvals and financial support, fostering - efficiency and alignment with academic and research objectives. + efficiency and alignment with academic and research objectives. We + believe in transparency and speed, enabling you to focus on your + work.

Travel Policy
{/* Achievements & History */} -
-

Policy Highlights

-
- {[ - { - title: "Travel Request Form", - description: - "Submit detailed forms including purpose, destination, budget, and supporting documentation.", - icon: "📄", - }, - { - title: "Approval Process", - description: - "Supervisor, department head, and Office of Research must approve for major travels.", - icon: "✔️", - }, - { - title: "Financial Support", - description: - "Eligibility depends on travel relevance, available funds, and research alignment.", - icon: "💰", - }, - { - title: "Documentation", - description: - "Attach conference invitations or research collaboration letters for approval.", - icon: "📜", - }, - { - title: "International Travel", - description: - "Managed through the Office of Research for additional oversight and funding.", - icon: "✈️", - }, - { - title: "Funding Sources", - description: - "Includes department funds, institutional grants, or scholarships.", - icon: "🏛️", - }, - ].map((item, index) => ( -
-
{item.icon}
-

{item.title}

-

{item.description}

-
- ))} +
+
+

+ Policy Highlights +

+
+ {[ + { + title: "Travel Request Form", + description: + "Submit detailed forms including purpose, destination, budget, and supporting documentation.", + icon: "📄", + }, + { + title: "Approval Process", + description: + "Supervisor, department head, and Office of Research must approve for major travels.", + icon: "✔️", + }, + { + title: "Financial Support", + description: + "Eligibility depends on travel relevance, available funds, and research alignment.", + icon: "💰", + }, + { + title: "Documentation", + description: + "Attach conference invitations or research collaboration letters for approval.", + icon: "📜", + }, + { + title: "International Travel", + description: + "Managed through the Office of Research for additional oversight and funding.", + icon: "✈️", + }, + { + title: "Funding Sources", + description: + "Includes department funds, institutional grants, or scholarships.", + icon: "🏛️", + }, + ].map((item, index) => ( +
+
{item.icon}
+

+ {item.title} +

+

+ {item.description} +

+
+ ))} +
{/* Why Choose Us */} -
-

Why Our Policy Stands Out

+
+

+ Why Our Policy Stands Out +

{[ { @@ -112,7 +144,8 @@ const About = () => { }, { title: "Global Outlook", - description: "Facilitates international collaborations and exchanges.", + description: + "Facilitates international collaborations and exchanges.", }, { title: "Comprehensive", @@ -120,24 +153,25 @@ const About = () => { }, { title: "Transparent", - description: "Approval criteria and funding sources clearly defined.", + description: + "Approval criteria and funding sources clearly defined.", }, ].map((item, index) => (
-

{item.title}

-

{item.description}

+

+ {item.title} +

+

{item.description}

))}
- {/* Back to Top */} -
- -
+ {/* Footer / Back to Top */} +
); }; diff --git a/frontend/src/pages/ApplicationForm/Input.jsx b/frontend/src/pages/ApplicationForm/Input.jsx index fcbe749..f647714 100644 --- a/frontend/src/pages/ApplicationForm/Input.jsx +++ b/frontend/src/pages/ApplicationForm/Input.jsx @@ -39,9 +39,11 @@ function Input({ return (
-

{section.label}

+

+ {section.label} +

@@ -90,7 +92,7 @@ function Input({ onChange={handleChange} onBlur={handleBlur} value={values[formFeild.name] || ""} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-700 transition duration-300 ease-in-out" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-red-700 transition duration-300 ease-in-out dark:bg-[#303134] dark:text-white" disabled={formFeild?.disabled} >