forked from CSI-KJSCE/Travel-policy-
Implemented Oauth
This commit is contained in:
28
backend/.env.example
Normal file
28
backend/.env.example
Normal file
@@ -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"
|
||||
5
backend/.gitignore
vendored
5
backend/.gitignore
vendored
@@ -1,2 +1,5 @@
|
||||
/node_modules
|
||||
.env
|
||||
.env
|
||||
.env.bak2
|
||||
.env.temp
|
||||
.env.bak
|
||||
|
||||
142
backend/package-lock.json
generated
142
backend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
30
backend/prisma/migrations/add_google_oauth_reference.sql
Normal file
30
backend/prisma/migrations/add_google_oauth_reference.sql
Normal file
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
export default app;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
router.get("/logout", logout);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -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}`);
|
||||
})
|
||||
// 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);
|
||||
});
|
||||
|
||||
79
backend/src/services/passportService.js
Normal file
79
backend/src/services/passportService.js
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user