Add Dark Mode, Password Hashing for better security , Settings Page, Policy PDF in Policy section,UI Changes

This commit is contained in:
arav
2026-01-10 19:39:40 +05:30
parent 933c0741ab
commit 9b605279e6
35 changed files with 1344 additions and 659 deletions

View File

@@ -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"
},

View File

@@ -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"

View File

@@ -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

View File

@@ -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();
});
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});

View File

@@ -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,

View File

@@ -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",

View File

@@ -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" });
}
};

View File

@@ -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;
router.get("/", (req, res) => {
res.send("Travel Policy Backend is Running!");
});
export default router;

View File

@@ -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);

View File

@@ -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
<a href=${link}>View Application</a>
<p>Thank you.</p>
`,
};
break;
case 'accounts':
mailOptions = {
from: process.env.TravelPolicyEmail,
to: emailId,
subject: 'Transfer money to the applicant',
html: `
<p>Please transfer the travel policy amount to the applicant's account. Click on the link below to view the application:</p>`
}
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: `
<p>Please transfer the travel policy amount to the applicant's account. Click on the link below to view the application:</p>`,
};
break;
default:
throw new Error("Invalid email type");
}
try {
await transporter.sendMail(mailOptions);