diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..392ddd2 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,28 @@ +# Database +DATABASE_URL="postgresql://user:password@localhost:5432/travel_policy_db?schema=public" + +# JWT Secret for token generation +JWT_SECRET="your-secret-jwt-key-here-change-this-in-production" + +# Session Secret +SESSION_SECRET="your-session-secret-key-here-change-this-in-production" + +# Google OAuth Credentials +# Get these from https://console.cloud.google.com/ +GOOGLE_CLIENT_ID="your-google-client-id.apps.googleusercontent.com" +GOOGLE_CLIENT_SECRET="your-google-client-secret" +GOOGLE_CALLBACK_URL="http://localhost:5000/auth/google/callback" + +# Frontend URL (for CORS and redirects) +FRONTEND_URL="http://localhost:5173" + +# Server Configuration +PORT=5000 +NODE_ENV="development" + +# Email Configuration (if using nodemailer) +EMAIL_HOST="smtp.gmail.com" +EMAIL_PORT=587 +EMAIL_USER="your-email@example.com" +EMAIL_PASSWORD="your-email-password-or-app-password" +EMAIL_FROM="noreply@example.com" diff --git a/backend/.gitignore b/backend/.gitignore index 7af7f04..5eaf7a4 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,2 +1,5 @@ /node_modules -.env \ No newline at end of file +.env +.env.bak2 +.env.temp +.env.bak diff --git a/backend/package-lock.json b/backend/package-lock.json index 5eed52f..492e42f 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,9 +15,12 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-session": "^1.18.2", "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.16", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", "prisma": "^5.20.0" }, "devDependencies": { @@ -129,6 +132,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", @@ -515,6 +527,31 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, "node_modules/express/node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", @@ -1059,6 +1096,12 @@ "node": ">=0.10.0" } }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1091,6 +1134,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1100,12 +1152,75 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1176,6 +1291,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1468,6 +1592,24 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", diff --git a/backend/package.json b/backend/package.json index 1dc9df7..e320cdb 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,9 +6,12 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-session": "^1.18.2", "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.16", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", "prisma": "^5.20.0" }, "name": "backend", diff --git a/backend/prisma/migrations/20260101184720_add_oauth_fields/migration.sql b/backend/prisma/migrations/20260101184720_add_oauth_fields/migration.sql new file mode 100644 index 0000000..d6bf6b4 --- /dev/null +++ b/backend/prisma/migrations/20260101184720_add_oauth_fields/migration.sql @@ -0,0 +1,7 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "OAuth_AccessToken" TEXT, +ADD COLUMN "OAuth_RefreshToken" TEXT, +ADD COLUMN "auth_mode" TEXT NOT NULL DEFAULT 'password'; + +-- Update existing users to have password auth mode +UPDATE "User" SET "auth_mode" = 'password' WHERE "auth_mode" IS NULL; diff --git a/backend/prisma/migrations/20260101185000_fix_oauth_constraints/migration.sql b/backend/prisma/migrations/20260101185000_fix_oauth_constraints/migration.sql new file mode 100644 index 0000000..3dab2ab --- /dev/null +++ b/backend/prisma/migrations/20260101185000_fix_oauth_constraints/migration.sql @@ -0,0 +1,8 @@ +-- Make password optional for OAuth users +ALTER TABLE "User" ALTER COLUMN "password" DROP NOT NULL; + +-- Add UUID generation extension if not exists +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Add default UUID generation for profileId +ALTER TABLE "User" ALTER COLUMN "profileId" SET DEFAULT uuid_generate_v4(); diff --git a/backend/prisma/migrations/add_google_oauth_reference.sql b/backend/prisma/migrations/add_google_oauth_reference.sql new file mode 100644 index 0000000..630f5ee --- /dev/null +++ b/backend/prisma/migrations/add_google_oauth_reference.sql @@ -0,0 +1,30 @@ +-- Migration: Add Google OAuth Support +-- Description: Adds googleId field to User table and makes password optional for OAuth users +-- Date: 2025-01-01 + +-- Step 1: Add googleId column (nullable, unique) +ALTER TABLE "User" ADD COLUMN "googleId" TEXT; + +-- Step 2: Make password column nullable (for OAuth users who don't have passwords) +ALTER TABLE "User" ALTER COLUMN "password" DROP NOT NULL; + +-- Step 3: Add unique constraint on googleId +ALTER TABLE "User" ADD CONSTRAINT "User_googleId_key" UNIQUE ("googleId"); + +-- Step 4: Create index on googleId for faster lookups +CREATE INDEX "User_googleId_idx" ON "User"("googleId"); + +-- Step 5: Verify existing indexes (email should already be indexed) +-- CREATE INDEX "User_email_idx" ON "User"("email"); -- Should already exist + +-- Notes: +-- 1. Existing users with passwords will continue to work normally +-- 2. New OAuth users will have NULL password and a googleId +-- 3. Users can have both password and googleId if they link accounts +-- 4. Email remains unique across all users (OAuth and traditional) + +-- Rollback instructions (if needed): +-- ALTER TABLE "User" DROP CONSTRAINT "User_googleId_key"; +-- DROP INDEX "User_googleId_idx"; +-- ALTER TABLE "User" DROP COLUMN "googleId"; +-- ALTER TABLE "User" ALTER COLUMN "password" SET NOT NULL; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 30a90a6..2ca106d 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"] @@ -10,16 +9,71 @@ datasource db { relationMode = "prisma" } -enum Institute { - KJSIDS - SKSC - KJSCE - SIRC - KJSIM - SSA - KJSCEd - DLIS - MSSMPA +model Application { + applicationId String @id @default(uuid()) + applicantId String + applicant User @relation("AppliedApplications", fields: [applicantId], references: [profileId]) + institute Institute + department String + applicantName String + applicationType String + formData Json + formName String + resubmission Boolean @default(false) + facultyValidation ApplicationStatus? + hodValidation ApplicationStatus? + hoiValidation ApplicationStatus? + vcValidation ApplicationStatus? + accountsValidation ApplicationStatus? + rejectionFeedback String? + totalExpense Float @default(0) + proofOfTravel Bytes? + proofOfAccommodation Bytes? + proofOfAttendance Bytes? + expenseProof0 Bytes? + expenseProof1 Bytes? + expenseProof2 Bytes? + expenseProof3 Bytes? + expenseProof4 Bytes? + expenseProof5 Bytes? + expenseProof6 Bytes? + expenseProof7 Bytes? + expenseProof8 Bytes? + expenseProof9 Bytes? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + validators User[] @relation("ToValidateApplications") + + @@index([applicantId]) + @@index([createdAt]) +} + +model User { + profileId String @id @default(uuid()) + userName String + email String @unique @db.Text + password String? + + institute Institute? + department String? + designation Designation + + appliedApplications Application[] @relation("AppliedApplications") + toValidateApplications Application[] @relation("ToValidateApplications") + OAuth_AccessToken String? + OAuth_RefreshToken String? + auth_mode String + @@index([email]) +} + +model ToValidateApplications { + A String + B String + + @@unique([A, B], map: "_ToValidateApplications_AB_unique") + @@index([B], map: "_ToValidateApplications_B_index") + @@map("_ToValidateApplications") } enum ApplicationStatus { @@ -37,65 +91,14 @@ enum Designation { STUDENT } -model User { - profileId String @id @default(uuid()) - userName String - email String @unique @db.Text - password String - - institute Institute? - department String? - designation Designation - - appliedApplications Application[] @relation("AppliedApplications") - toValidateApplications Application[] @relation("ToValidateApplications") - - @@index([email]) -} - -model Application { - applicationId String @id @default(uuid()) - applicantId String - applicant User @relation("AppliedApplications", fields: [applicantId], references: [profileId]) - institute Institute - department String - - applicantName String - applicationType String - formData Json - - formName String - resubmission Boolean @default(false) - - facultyValidation ApplicationStatus? - hodValidation ApplicationStatus? - hoiValidation ApplicationStatus? - vcValidation ApplicationStatus? - accountsValidation ApplicationStatus? - - rejectionFeedback String? - - totalExpense Float @default(0) - - proofOfTravel Bytes? - proofOfAccommodation Bytes? - proofOfAttendance Bytes? - expenseProof0 Bytes? - expenseProof1 Bytes? - expenseProof2 Bytes? - expenseProof3 Bytes? - expenseProof4 Bytes? - expenseProof5 Bytes? - expenseProof6 Bytes? - expenseProof7 Bytes? - expenseProof8 Bytes? - expenseProof9 Bytes? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - validators User[] @relation("ToValidateApplications") - - @@index([applicantId]) - @@index([createdAt]) +enum Institute { + KJSIDS + SKSC + KJSCE + SIRC + KJSIM + SSA + KJSCEd + DLIS + MSSMPA } diff --git a/backend/src/app.js b/backend/src/app.js index f2766ed..90e9d20 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -1,29 +1,59 @@ -import express from 'express'; -import cors from 'cors'; -import cookieParser from 'cookie-parser'; -import router from './routes/auth.js'; -import applicantRoute from './routes/applicant.js'; -import validatorRoute from './routes/validator.js'; -import generalRoute from './routes/general.js'; -import { verifyApplicantToken, verifyToken, verifyValidatorToken } from './middleware/verifyJwt.js'; +import express from "express"; +import cors from "cors"; +import cookieParser from "cookie-parser"; +import session from "express-session"; +import passport, { initializePassport } from "./services/passportService.js"; +import router from "./routes/auth.js"; +import applicantRoute from "./routes/applicant.js"; +import validatorRoute from "./routes/validator.js"; +import generalRoute from "./routes/general.js"; +import { + verifyApplicantToken, + verifyToken, + verifyValidatorToken, +} from "./middleware/verifyJwt.js"; + +// Initialize passport strategies after environment variables are loaded +initializePassport(); const app = express(); // Middleware setup -app.use(cookieParser()); -app.use(express.json()); -app.use(express.urlencoded({ extended: true })); -app.use(cors({ - origin: true, - credentials: true -})); +app.use(cookieParser()); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use( + cors({ + origin: process.env.FRONTEND_URL || "http://localhost:5173", + credentials: true, + }), +); + +// Session middleware (required for Passport) +app.use( + session({ + secret: + process.env.SESSION_SECRET || "your-secret-key-change-this-in-production", + resave: false, + saveUninitialized: false, + cookie: { + secure: process.env.NODE_ENV === "production", + httpOnly: true, + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }, + }), +); + +// Initialize Passport +app.use(passport.initialize()); +app.use(passport.session()); // Route-specific middleware and routes -app.use('/applicant', verifyApplicantToken, applicantRoute); -app.use('/validator', verifyValidatorToken, validatorRoute); -app.use('/general', verifyToken, generalRoute); +app.use("/applicant", verifyApplicantToken, applicantRoute); +app.use("/validator", verifyValidatorToken, validatorRoute); +app.use("/general", verifyToken, generalRoute); // Authentication routes app.use(router); -export default app; \ No newline at end of file +export default app; diff --git a/backend/src/controllers/authControllers.js b/backend/src/controllers/authControllers.js index d0e3e58..4f47cef 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 passport from "passport"; 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, }, }); @@ -41,7 +42,13 @@ 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, { + path: "/", + sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", + secure: process.env.NODE_ENV === "production", + httpOnly: true, + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }) .status(200) .json({ message: "Login Successful", @@ -62,7 +69,7 @@ const validatorLogin = async (req, res) => { // Check if the validator profile exists let validProfile = await prisma.user.findUnique({ where: { - email + email, }, }); @@ -95,7 +102,13 @@ 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, { + path: "/", + sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", + secure: process.env.NODE_ENV === "production", + httpOnly: true, + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }) .status(200) .json({ message: "Login Successful", @@ -127,4 +140,67 @@ const logout = async (req, res) => { } }; -export { applicantLogin, validatorLogin, logout }; +//this is the controller which will handle the oauth logic +const googleAuthStart = async (req, res, next) => { + const designation = req.params.designation; + + passport.authenticate("google", { + scope: ["profile", "email"], + state: designation, + })(req, res, next); +}; + +//this is the oauth callback controller +const googleAuthCallback = async (req, res, next) => { + try { + const signUpIntent = req.query.state; + const user = req.user; + + const allowedIntents = ["validator", "applicant"]; + + if (!allowedIntents.includes(signUpIntent)) { + return res.redirect( + `${process.env.FRONTEND_URL || "http://localhost:5173"}/?error=invalid_intent`, + ); + } + + // Generate the token using correct field names from Prisma schema + const token = generateToken({ + id: user.profileId, + designation: user.designation, + department: user.department, + institute: user.institute, + role: signUpIntent, + }); + + // Set the token as a cookie for same-origin requests + const cookieOptions = { + path: "/", + sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", + secure: process.env.NODE_ENV === "production", + httpOnly: true, + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }; + + res.cookie("access_token", token, cookieOptions); + + // For OAuth callback, also pass token in URL so frontend can set it + // This is needed because cross-origin cookies don't work in development (different ports) + return res.redirect( + `${process.env.FRONTEND_URL || "http://localhost:5173"}/${signUpIntent}/dashboard?login=success&token=${token}`, + ); + } catch (error) { + console.error("OAuth callback error:", error); + return res.redirect( + `${process.env.FRONTEND_URL || "http://localhost:5173"}/?error=auth_failed`, + ); + } +}; + +export { + applicantLogin, + validatorLogin, + logout, + googleAuthStart, + googleAuthCallback, +}; diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index 3e56d6d..c5b243e 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -1,11 +1,30 @@ -import express from 'express'; -import { applicantLogin, logout, validatorLogin } from '../controllers/authControllers.js'; +import express from "express"; +import { + applicantLogin, + logout, + validatorLogin, + googleAuthStart, + googleAuthCallback, +} from "../controllers/authControllers.js"; +import passport from "../services/passportService.js"; const router = express.Router(); -router.post('/applicant-login', applicantLogin); -router.post('/validator-login', validatorLogin); +router.post("/applicant-login", applicantLogin); +//this route is for google oauth, this one route will handle both applicantLogic and validatorLo +// we will be passing the designation as a URL parameter ("validator" or "applicant") and it will be passed as state through OAuth +router.get("/auth/oauth/:designation", googleAuthStart); +//this will be the oauth callback Route +router.get( + "/auth/google/callback", + passport.authenticate("google", { + failureRedirect: "http://localhost:5173/?error=auth_failed", + }), + googleAuthCallback, +); -router.get('/logout', logout) +router.post("/validator", validatorLogin); -export default router; \ No newline at end of file +router.get("/logout", logout); + +export default router; diff --git a/backend/src/server.js b/backend/src/server.js index af5a903..7eaf4f5 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -1,9 +1,26 @@ -import app from './app.js'; -import dotenv from 'dotenv'; -dotenv.config(); +import dotenv from "dotenv"; +import path from "path"; +import { fileURLToPath } from "url"; -const port = process.env.PORT || 3000; +// Get the directory name in ES modules +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); -app.listen(port, () => { - console.log(`Server is running on port ${port}`); -}) \ No newline at end of file +// Load .env from backend directory +dotenv.config({ path: path.join(__dirname, "..", ".env") }); + +// Dynamic import to ensure dotenv loads first +const startServer = async () => { + const { default: app } = await import("./app.js"); + + const port = process.env.PORT || 3000; + + app.listen(port, () => { + console.log(`Server is running on port ${port}`); + }); +}; + +startServer().catch((error) => { + console.error("Failed to start server:", error); + process.exit(1); +}); diff --git a/backend/src/services/passportService.js b/backend/src/services/passportService.js new file mode 100644 index 0000000..9c7ab08 --- /dev/null +++ b/backend/src/services/passportService.js @@ -0,0 +1,79 @@ +import { Strategy as GoogleStrategy } from "passport-google-oauth20"; +import prisma from "../config/prismaConfig.js"; +import passport from "passport"; + +// Function to initialize passport strategies +export const initializePassport = () => { + // Validate required environment variables + if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) { + console.error( + "ERROR: Missing required Google OAuth credentials in environment variables.", + ); + console.error( + "Please ensure GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are set in your .env file", + ); + throw new Error("Missing Google OAuth credentials"); + } + + passport.use( + new GoogleStrategy( + { + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: `${process.env.BACKEND_URL || "http://localhost:3000"}/auth/google/callback`, + scope: ["profile", "email"], + }, + async (accessToken, refreshToken, profile, done) => { + //checking if theres existing user with email + try { + const existingUser = await prisma.user.findUnique({ + where: { email: profile.emails[0]?.value }, + }); + if (existingUser) { + return done(null, existingUser); + } + const newUser = await prisma.user.create({ + data: { + userName: profile.displayName, // I am storing the name , other devs can switch to display_name based on their preferences + email: profile.emails[0].value, + password: "", // OAuth users don't use password authentication + designation: "FACULTY", // Default designation, can be updated later + auth_mode: "Google", + OAuth_AccessToken: accessToken, + OAuth_RefreshToken: refreshToken, //I am saving the accessTokens and refreshTokens, which MIGHT be used later + }, + }); + console.log( + "Passport service has made a new user: ", + JSON.stringify(newUser), + ); + done(null, newUser); + } catch (err) { + console.error("Error creating user:", err); + done(err, null); + } + }, + ), + ); + + // Serialize user for session + passport.serializeUser((user, done) => { + done(null, user.profileId); + }); + + // Deserialize user from session + passport.deserializeUser(async (id, done) => { + try { + const user = await prisma.user.findUnique({ + where: { profileId: id }, + }); + done(null, user); + } catch (error) { + done(error, null); + } + }); + + return passport; +}; + +export default passport; diff --git a/docker-compose.yml b/docker-compose.yml index b4798de..6f6b14c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,8 @@ services: # PostgreSQL Database Service db: image: postgres:17-alpine + ports: + - "5432:5432" environment: - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} @@ -32,4 +34,4 @@ services: - pgdata:/var/lib/postgresql/data volumes: - pgdata: \ No newline at end of file + pgdata: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b893788..1628620 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@react-pdf/renderer": "^4.1.6", "autoprefixer": "^10.4.20", - "axios": "^1.7.5", + "axios": "^1.13.2", "bootstrap": "^5.3.3", "chart.js": "^4.4.7", "chartjs-plugin-datalabels": "^2.2.0", @@ -18,6 +18,7 @@ "framer-motion": "^11.15.0", "frontend": "file:", "hamburger-react": "^2.5.1", + "js-cookie": "^3.0.5", "pdfjs-dist": "^4.7.76", "postcss": "^8.4.40", "react": "^18.3.1", @@ -2007,13 +2008,13 @@ } }, "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -2185,6 +2186,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2641,6 +2655,20 @@ "node": ">=6.0.0" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2717,13 +2745,10 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2732,7 +2757,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -2763,10 +2787,10 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -2775,14 +2799,15 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -3355,13 +3380,15 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -3592,16 +3619,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3610,6 +3642,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-symbol-description": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", @@ -3684,12 +3729,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3752,10 +3797,10 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3767,7 +3812,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -4336,6 +4380,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4530,6 +4583,15 @@ "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-engine": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index d94201c..3e537e6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,7 @@ "dependencies": { "@react-pdf/renderer": "^4.1.6", "autoprefixer": "^10.4.20", - "axios": "^1.7.5", + "axios": "^1.13.2", "bootstrap": "^5.3.3", "chart.js": "^4.4.7", "chartjs-plugin-datalabels": "^2.2.0", @@ -20,6 +20,7 @@ "framer-motion": "^11.15.0", "frontend": "file:", "hamburger-react": "^2.5.1", + "js-cookie": "^3.0.5", "pdfjs-dist": "^4.7.76", "postcss": "^8.4.40", "react": "^18.3.1", diff --git a/frontend/src/components/OAuthCallback/OAuthCallbackHandler.jsx b/frontend/src/components/OAuthCallback/OAuthCallbackHandler.jsx new file mode 100644 index 0000000..71dfa41 --- /dev/null +++ b/frontend/src/components/OAuthCallback/OAuthCallbackHandler.jsx @@ -0,0 +1,41 @@ +import { useEffect } from 'react'; +import { useSearchParams, useNavigate } from 'react-router-dom'; +import Cookies from 'js-cookie'; + +const OAuthCallbackHandler = ({ children }) => { + const [searchParams, setSearchParams] = useSearchParams(); + const navigate = useNavigate(); + + useEffect(() => { + const token = searchParams.get('token'); + const loginSuccess = searchParams.get('login'); + + if (token && loginSuccess === 'success') { + // Store token in cookie (matching backend cookie name) + Cookies.set('access_token', token, { + expires: 1, // 1 day + path: '/', + sameSite: 'Lax', + secure: false, // set to true in production with HTTPS + }); + + // Remove token from URL for security + searchParams.delete('token'); + searchParams.delete('login'); + + // Update URL without the token parameter + const newSearch = searchParams.toString(); + const currentPath = window.location.pathname; + const newUrl = newSearch ? `${currentPath}?${newSearch}` : currentPath; + + // Replace URL without reload + window.history.replaceState({}, '', newUrl); + + console.log('OAuth token stored successfully'); + } + }, [searchParams]); + + return children; +}; + +export default OAuthCallbackHandler; diff --git a/frontend/src/pages/Dashboard/Dashboard.jsx b/frontend/src/pages/Dashboard/Dashboard.jsx index a350872..ab7a202 100644 --- a/frontend/src/pages/Dashboard/Dashboard.jsx +++ b/frontend/src/pages/Dashboard/Dashboard.jsx @@ -1,5 +1,6 @@ import React from "react"; import { useNavigate, useRouteLoaderData } from "react-router-dom"; +import OAuthCallbackHandler from "../../components/OAuthCallback/OAuthCallbackHandler"; function Dashboard() { const { role, user } = @@ -15,7 +16,8 @@ function Dashboard() { const greetingLine2 = `${designation} in ${department} Department, ${institute}`; return ( -
- Our web application simplifies the process of requesting, approving, and managing financial support for research students and associates. + Our web application simplifies the process of requesting, approving, + and managing financial support for research students and associates.
Go to Validator’s Sign in
-or use email
++ or use email +
{/* Display Error Message */} {error &&- Our web application simplifies the process of requesting, approving, and managing financial support for research students and associates. + Our web application simplifies the process of requesting, approving, + and managing financial support for research students and associates.
Go to Applicant’s Sign in
-