Implemented Oauth

This commit is contained in:
2026-01-02 00:36:24 +05:30
parent 933c0741ab
commit 0130f746b0
22 changed files with 960 additions and 257 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.env

28
backend/.env.example Normal file
View 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"

3
backend/.gitignore vendored
View File

@@ -1,2 +1,5 @@
/node_modules /node_modules
.env .env
.env.bak2
.env.temp
.env.bak

View File

@@ -15,9 +15,12 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"express-session": "^1.18.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.16", "nodemailer": "^6.9.16",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"prisma": "^5.20.0" "prisma": "^5.20.0"
}, },
"devDependencies": { "devDependencies": {
@@ -129,6 +132,15 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true "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": { "node_modules/bcryptjs": {
"version": "2.4.3", "version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
@@ -515,6 +527,31 @@
"url": "https://opencollective.com/express" "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": { "node_modules/express/node_modules/cookie": {
"version": "0.7.1", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
@@ -1059,6 +1096,12 @@
"node": ">=0.10.0" "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": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -1091,6 +1134,15 @@
"node": ">= 0.8" "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": { "node_modules/parseurl": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -1100,12 +1152,75 @@
"node": ">= 0.8" "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": { "node_modules/path-to-regexp": {
"version": "0.1.12", "version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT" "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": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@@ -1176,6 +1291,15 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -1468,6 +1592,24 @@
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT" "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": { "node_modules/undefsafe": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",

View File

@@ -6,9 +6,12 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"express-session": "^1.18.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.16", "nodemailer": "^6.9.16",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"prisma": "^5.20.0" "prisma": "^5.20.0"
}, },
"name": "backend", "name": "backend",

View File

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

View File

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

View 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;

View File

@@ -1,4 +1,3 @@
// Generator to create Prisma Client
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
binaryTargets = ["native", "darwin-arm64", "linux-musl-arm64-openssl-3.0.x"] binaryTargets = ["native", "darwin-arm64", "linux-musl-arm64-openssl-3.0.x"]
@@ -10,16 +9,71 @@ datasource db {
relationMode = "prisma" relationMode = "prisma"
} }
enum Institute { model Application {
KJSIDS applicationId String @id @default(uuid())
SKSC applicantId String
KJSCE applicant User @relation("AppliedApplications", fields: [applicantId], references: [profileId])
SIRC institute Institute
KJSIM department String
SSA applicantName String
KJSCEd applicationType String
DLIS formData Json
MSSMPA 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 { enum ApplicationStatus {
@@ -37,65 +91,14 @@ enum Designation {
STUDENT STUDENT
} }
model User { enum Institute {
profileId String @id @default(uuid()) KJSIDS
userName String SKSC
email String @unique @db.Text KJSCE
password String SIRC
KJSIM
institute Institute? SSA
department String? KJSCEd
designation Designation DLIS
MSSMPA
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])
} }

View File

@@ -1,11 +1,20 @@
import express from 'express'; import express from "express";
import cors from 'cors'; import cors from "cors";
import cookieParser from 'cookie-parser'; import cookieParser from "cookie-parser";
import router from './routes/auth.js'; import session from "express-session";
import applicantRoute from './routes/applicant.js'; import passport, { initializePassport } from "./services/passportService.js";
import validatorRoute from './routes/validator.js'; import router from "./routes/auth.js";
import generalRoute from './routes/general.js'; import applicantRoute from "./routes/applicant.js";
import { verifyApplicantToken, verifyToken, verifyValidatorToken } from './middleware/verifyJwt.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(); const app = express();
@@ -13,15 +22,36 @@ const app = express();
app.use(cookieParser()); app.use(cookieParser());
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use(cors({ app.use(
origin: true, cors({
credentials: true 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 // Route-specific middleware and routes
app.use('/applicant', verifyApplicantToken, applicantRoute); app.use("/applicant", verifyApplicantToken, applicantRoute);
app.use('/validator', verifyValidatorToken, validatorRoute); app.use("/validator", verifyValidatorToken, validatorRoute);
app.use('/general', verifyToken, generalRoute); app.use("/general", verifyToken, generalRoute);
// Authentication routes // Authentication routes
app.use(router); app.use(router);

View File

@@ -1,5 +1,6 @@
import prisma from "../config/prismaConfig.js"; import prisma from "../config/prismaConfig.js";
import generateToken from "../services/generateToken.js"; import generateToken from "../services/generateToken.js";
import passport from "passport";
const applicantLogin = async (req, res) => { const applicantLogin = async (req, res) => {
try { try {
@@ -8,7 +9,7 @@ const applicantLogin = async (req, res) => {
// Check if the applicant profile exists // Check if the applicant profile exists
const validProfile = await prisma.user.findUnique({ const validProfile = await prisma.user.findUnique({
where: { where: {
email email,
}, },
}); });
@@ -41,7 +42,13 @@ const applicantLogin = async (req, res) => {
// Set the token as a cookie // Set the token as a cookie
return res 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) .status(200)
.json({ .json({
message: "Login Successful", message: "Login Successful",
@@ -62,7 +69,7 @@ const validatorLogin = async (req, res) => {
// Check if the validator profile exists // Check if the validator profile exists
let validProfile = await prisma.user.findUnique({ let validProfile = await prisma.user.findUnique({
where: { where: {
email email,
}, },
}); });
@@ -95,7 +102,13 @@ const validatorLogin = async (req, res) => {
// Set the token as a cookie // Set the token as a cookie
return res 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) .status(200)
.json({ .json({
message: "Login Successful", 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,
};

View File

@@ -1,11 +1,30 @@
import express from 'express'; import express from "express";
import { applicantLogin, logout, validatorLogin } from '../controllers/authControllers.js'; import {
applicantLogin,
logout,
validatorLogin,
googleAuthStart,
googleAuthCallback,
} from "../controllers/authControllers.js";
import passport from "../services/passportService.js";
const router = express.Router(); const router = express.Router();
router.post('/applicant-login', applicantLogin); router.post("/applicant-login", applicantLogin);
router.post('/validator-login', validatorLogin); //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);
router.get("/logout", logout);
export default router; export default router;

View File

@@ -1,9 +1,26 @@
import app from './app.js'; import dotenv from "dotenv";
import dotenv from 'dotenv'; import path from "path";
dotenv.config(); 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, () => { // 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}`); console.log(`Server is running on port ${port}`);
}) });
};
startServer().catch((error) => {
console.error("Failed to start server:", error);
process.exit(1);
});

View 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;

View File

@@ -24,6 +24,8 @@ services:
# PostgreSQL Database Service # PostgreSQL Database Service
db: db:
image: postgres:17-alpine image: postgres:17-alpine
ports:
- "5432:5432"
environment: environment:
- POSTGRES_USER=${POSTGRES_USER} - POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}

View File

@@ -10,7 +10,7 @@
"dependencies": { "dependencies": {
"@react-pdf/renderer": "^4.1.6", "@react-pdf/renderer": "^4.1.6",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"axios": "^1.7.5", "axios": "^1.13.2",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"chart.js": "^4.4.7", "chart.js": "^4.4.7",
"chartjs-plugin-datalabels": "^2.2.0", "chartjs-plugin-datalabels": "^2.2.0",
@@ -18,6 +18,7 @@
"framer-motion": "^11.15.0", "framer-motion": "^11.15.0",
"frontend": "file:", "frontend": "file:",
"hamburger-react": "^2.5.1", "hamburger-react": "^2.5.1",
"js-cookie": "^3.0.5",
"pdfjs-dist": "^4.7.76", "pdfjs-dist": "^4.7.76",
"postcss": "^8.4.40", "postcss": "^8.4.40",
"react": "^18.3.1", "react": "^18.3.1",
@@ -2007,13 +2008,13 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.9.0", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
@@ -2185,6 +2186,19 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -2641,6 +2655,20 @@
"node": ">=6.0.0" "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": { "node_modules/eastasianwidth": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -2717,13 +2745,10 @@
} }
}, },
"node_modules/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true, "license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
@@ -2732,7 +2757,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
@@ -2763,10 +2787,10 @@
} }
}, },
"node_modules/es-object-atoms": { "node_modules/es-object-atoms": {
"version": "1.0.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true, "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0" "es-errors": "^1.3.0"
}, },
@@ -2775,14 +2799,15 @@
} }
}, },
"node_modules/es-set-tostringtag": { "node_modules/es-set-tostringtag": {
"version": "2.0.3", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true, "license": "MIT",
"dependencies": { "dependencies": {
"get-intrinsic": "^1.2.4", "es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2", "has-tostringtag": "^1.0.2",
"hasown": "^2.0.1" "hasown": "^2.0.2"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -3355,13 +3380,15 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.0", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12" "mime-types": "^2.1.12"
}, },
"engines": { "engines": {
@@ -3592,16 +3619,21 @@
} }
}, },
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.2.4", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true, "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2", "function-bind": "^1.1.2",
"has-proto": "^1.0.1", "get-proto": "^1.0.1",
"has-symbols": "^1.0.3", "gopd": "^1.2.0",
"hasown": "^2.0.0" "has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -3610,6 +3642,19 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/get-symbol-description": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
@@ -3684,12 +3729,12 @@
} }
}, },
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.0.1", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true, "license": "MIT",
"dependencies": { "engines": {
"get-intrinsic": "^1.1.3" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@@ -3752,10 +3797,10 @@
} }
}, },
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.0.3", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true, "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@@ -3767,7 +3812,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"dependencies": { "dependencies": {
"has-symbols": "^1.0.3" "has-symbols": "^1.0.3"
}, },
@@ -4336,6 +4380,15 @@
"jiti": "bin/jiti.js" "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": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "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" "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": { "node_modules/media-engine": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz", "resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz",

View File

@@ -12,7 +12,7 @@
"dependencies": { "dependencies": {
"@react-pdf/renderer": "^4.1.6", "@react-pdf/renderer": "^4.1.6",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"axios": "^1.7.5", "axios": "^1.13.2",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"chart.js": "^4.4.7", "chart.js": "^4.4.7",
"chartjs-plugin-datalabels": "^2.2.0", "chartjs-plugin-datalabels": "^2.2.0",
@@ -20,6 +20,7 @@
"framer-motion": "^11.15.0", "framer-motion": "^11.15.0",
"frontend": "file:", "frontend": "file:",
"hamburger-react": "^2.5.1", "hamburger-react": "^2.5.1",
"js-cookie": "^3.0.5",
"pdfjs-dist": "^4.7.76", "pdfjs-dist": "^4.7.76",
"postcss": "^8.4.40", "postcss": "^8.4.40",
"react": "^18.3.1", "react": "^18.3.1",

View File

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

View File

@@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { useNavigate, useRouteLoaderData } from "react-router-dom"; import { useNavigate, useRouteLoaderData } from "react-router-dom";
import OAuthCallbackHandler from "../../components/OAuthCallback/OAuthCallbackHandler";
function Dashboard() { function Dashboard() {
const { role, user } = const { role, user } =
@@ -15,6 +16,7 @@ function Dashboard() {
const greetingLine2 = `${designation} in ${department} Department, ${institute}`; const greetingLine2 = `${designation} in ${department} Department, ${institute}`;
return ( return (
<OAuthCallbackHandler>
<div className="font-sans bg-white overflow-y-scroll scroll-smooth snap-y h-screen" > <div className="font-sans bg-white overflow-y-scroll scroll-smooth snap-y h-screen" >
{/* Hero Section */} {/* Hero Section */}
<section <section
@@ -161,7 +163,8 @@ function Dashboard() {
</p> </p>
</footer> </footer>
</div> </div>
); </OAuthCallbackHandler>
);
} }
export default Dashboard; export default Dashboard;

View File

@@ -1,4 +1,5 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import './Login.css'; import './Login.css';
import loginPageBg from '/images/campus_bg.jpeg'; import loginPageBg from '/images/campus_bg.jpeg';
@@ -8,6 +9,26 @@ import ValidatorLogin from './components/ValidatorLogin';
const Login = () => { const Login = () => {
const [isApplicant, setIsApplicant] = useState(true); const [isApplicant, setIsApplicant] = useState(true);
const [searchParams, setSearchParams] = useSearchParams();
const [errorMessage, setErrorMessage] = useState('');
useEffect(() => {
const error = searchParams.get('error');
if (error === 'auth_failed') {
setErrorMessage('Google authentication failed. Please try again.');
} else if (error === 'invalid_intent') {
setErrorMessage('Invalid login type. Please try again.');
}
// Clear error from URL after displaying
if (error) {
setTimeout(() => {
searchParams.delete('error');
setSearchParams(searchParams);
setErrorMessage('');
}, 5000);
}
}, [searchParams, setSearchParams]);
const toggleRole = () => { const toggleRole = () => {
setIsApplicant(!isApplicant); setIsApplicant(!isApplicant);
@@ -17,6 +38,24 @@ const Login = () => {
<div className="login-page"> <div className="login-page">
<img src={loginPageBg} className='loginPage_bg' /> <img src={loginPageBg} className='loginPage_bg' />
<div className='login'> <div className='login'>
{errorMessage && (
<div style={{
position: 'absolute',
top: '20px',
left: '50%',
transform: 'translateX(-50%)',
backgroundColor: '#f44336',
color: 'white',
padding: '12px 24px',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
zIndex: 1000,
maxWidth: '90%',
textAlign: 'center'
}}>
{errorMessage}
</div>
)}
<div className={`login-container`}> <div className={`login-container`}>
{isApplicant ? ( {isApplicant ? (
<> <>

View File

@@ -1,11 +1,14 @@
import React, { useState } from 'react'; import React, { useState } from "react";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from "react-router-dom";
import './LoginAnimation.css'; import "./LoginAnimation.css";
function ApplicantLogin({ changeRole }) { function ApplicantLogin({ changeRole }) {
const [credentials, setCredentials] = useState({ email: 'faculty.computer.kjsce@example.com', password: 'securePassword123' }); const [credentials, setCredentials] = useState({
email: "faculty.computer.kjsce@example.com",
password: "securePassword123",
});
const [animate, setAnimate] = useState(false); const [animate, setAnimate] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState("");
const [loading, setLoading] = useState(false); // Loading state const [loading, setLoading] = useState(false); // Loading state
const handleChangeRole = () => { const handleChangeRole = () => {
@@ -20,52 +23,68 @@ function ApplicantLogin({ changeRole }) {
// Basic Validation // Basic Validation
if (!credentials.email || !credentials.password) { if (!credentials.email || !credentials.password) {
setError('Please enter both email and password.'); setError("Please enter both email and password.");
return; return;
} }
if (!/\S+@\S+\.\S+/.test(credentials.email)) { if (!/\S+@\S+\.\S+/.test(credentials.email)) {
setError('Please enter a valid email address.'); setError("Please enter a valid email address.");
return; return;
} }
setLoading(true); // Show loading state setLoading(true); // Show loading state
setError(''); // Reset previous errors setError(""); // Reset previous errors
try { try {
const response = await fetch(`${import.meta.env.VITE_APP_API_URL}/applicant-login`, { const response = await fetch(
method: 'POST', `${import.meta.env.VITE_APP_API_URL}/applicant-login`,
{
method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
credentials: 'include', credentials: "include",
body: JSON.stringify(credentials), body: JSON.stringify(credentials),
}); },
);
const result = await response.json(); const result = await response.json();
if (response.ok) { if (response.ok) {
window.location.href = '/applicant/dashboard'; window.location.href = "/applicant/dashboard";
} else { } else {
setError(result.message || 'Invalid login credentials.'); setError(result.message || "Invalid login credentials.");
} }
} catch (error) { } catch (error) {
console.error('Error during login:', error); console.error("Error during login:", error);
setError('An error occurred. Please try again later.'); setError("An error occurred. Please try again later.");
} finally { } finally {
setLoading(false); // Hide loading state setLoading(false); // Hide loading state
} }
}; };
const handleGoogleLogin = () => {
//I personally would transfer all the apis to the Auth.js in APIFactory dir, but I am following the project's code style
// Google OAuth requires a full page redirect, not a fetch call
const designation = "applicant";
// Redirect to backend OAuth endpoint - it will handle the Google redirect
window.location.href = `${import.meta.env.VITE_APP_API_URL}/auth/oauth/${designation}`;
};
return ( return (
<div className="flex flex-col md:flex-row bg-red-700 shadow-lg rounded-lg overflow-hidden max-w-4xl mx-auto"> <div className="flex flex-col md:flex-row bg-red-700 shadow-lg rounded-lg overflow-hidden max-w-4xl mx-auto">
<div className={`w-full md:w-3/4 bg-red-700 p-4 flex flex-col justify-center ${animate ? 'slide-out-right' : 'fade-in-fwd'}`}> <div
<h2 className="text-white text-xl md:text-2xl lg:text-3xl font-bold mb-3 hidden md:block">Travel Policy</h2> className={`w-full md:w-3/4 bg-red-700 p-4 flex flex-col justify-center ${animate ? "slide-out-right" : "fade-in-fwd"}`}
>
<h2 className="text-white text-xl md:text-2xl lg:text-3xl font-bold mb-3 hidden md:block">
Travel Policy
</h2>
<p className="text-white text-sm md:text-base mb-6 hidden md:block"> <p className="text-white text-sm md:text-base mb-6 hidden md:block">
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.
</p> </p>
<h3 className="text-white text-lg md:text-xl font-bold">Validator?</h3> <h3 className="text-white text-lg md:text-xl font-bold">Validator?</h3>
<p className="text-white mb-3">Go to Validators Sign in</p> <p className="text-white mb-3">Go to Validators Sign in</p>
<button <button
type='button' type="button"
className="bg-white text-red-700 text-sm md:text-base px-3 py-1.5 rounded-full font-semibold shadow-md hover:bg-gray-100 transition" className="bg-white text-red-700 text-sm md:text-base px-3 py-1.5 rounded-full font-semibold shadow-md hover:bg-gray-100 transition"
onClick={handleChangeRole} onClick={handleChangeRole}
> >
@@ -73,12 +92,19 @@ function ApplicantLogin({ changeRole }) {
</button> </button>
</div> </div>
<div className={`bg-white w-full md:w-3/4 p-8 flex flex-col justify-center ${animate ? 'text-blur-out' : 'fade-in-fwd'}`}> <div
<h2 className="text-lg md:text-xl lg:text-2xl font-bold mb-3">Login for Applicants<span role="img" aria-label="wave">👋</span></h2> className={`bg-white w-full md:w-3/4 p-8 flex flex-col justify-center ${animate ? "text-blur-out" : "fade-in-fwd"}`}
>
<h2 className="text-lg md:text-xl lg:text-2xl font-bold mb-3">
Login for Applicants
<span role="img" aria-label="wave">
👋
</span>
</h2>
<button <button
type='button' type="button"
className="bg-gray-100 text-gray-700 text-sm md:text-base px-4 py-2 rounded-full font-semibold mb-3 shadow-md flex items-center justify-center hover:bg-gray-200 transition-transform transform hover:scale-105" className="bg-gray-100 text-gray-700 text-sm md:text-base px-4 py-2 rounded-full font-semibold mb-3 shadow-md flex items-center justify-center hover:bg-gray-200 transition-transform transform hover:scale-105"
onClick={handleSubmit} onClick={handleGoogleLogin}
> >
<svg <svg
className="w-6 h-6 mr-2" className="w-6 h-6 mr-2"
@@ -88,14 +114,28 @@ function ApplicantLogin({ changeRole }) {
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path d="M29.074 13.3887H28V13.3333H16V18.6667H23.5354C22.436 21.7713 19.482 24 16 24C11.582 24 8.00002 20.418 8.00002 16C8.00002 11.582 11.582 8 16 8C18.0394 8 19.8947 8.76934 21.3074 10.026L25.0787 6.25467C22.6974 4.03534 19.512 2.66667 16 2.66667C8.63669 2.66667 2.66669 8.63667 2.66669 16C2.66669 23.3633 8.63669 29.3333 16 29.3333C23.3634 29.3333 29.3334 23.3633 29.3334 16C29.3334 15.106 29.2414 14.2333 29.074 13.3887Z" fill="#FFC107"/> <path
<path d="M4.20398 9.794L8.58465 13.0067C9.76998 10.072 12.6406 8 16 8C18.0393 8 19.8946 8.76934 21.3073 10.026L25.0786 6.25467C22.6973 4.03534 19.512 2.66667 16 2.66667C10.8786 2.66667 6.43731 5.558 4.20398 9.794Z" fill="#FF3D00"/> d="M29.074 13.3887H28V13.3333H16V18.6667H23.5354C22.436 21.7713 19.482 24 16 24C11.582 24 8.00002 20.418 8.00002 16C8.00002 11.582 11.582 8 16 8C18.0394 8 19.8947 8.76934 21.3074 10.026L25.0787 6.25467C22.6974 4.03534 19.512 2.66667 16 2.66667C8.63669 2.66667 2.66669 8.63667 2.66669 16C2.66669 23.3633 8.63669 29.3333 16 29.3333C23.3634 29.3333 29.3334 23.3633 29.3334 16C29.3334 15.106 29.2414 14.2333 29.074 13.3887Z"
<path d="M16 29.3333C19.444 29.3333 22.5733 28.0153 24.9393 25.872L20.8127 22.38C19.429 23.4323 17.7383 24.0014 16 24C12.532 24 9.58734 21.7887 8.478 18.7027L4.13 22.0527C6.33667 26.3707 10.818 29.3333 16 29.3333Z" fill="#4CAF50"/> fill="#FFC107"
<path d="M29.074 13.3887H28V13.3333H16V18.6667H23.5353C23.0095 20.1443 22.0622 21.4354 20.8107 22.3807L20.8127 22.3793L24.9393 25.8713C24.6473 26.1367 29.3333 22.6667 29.3333 16C29.3333 15.106 29.2413 14.2333 29.074 13.3887Z" fill="#1976D2"/> />
<path
d="M4.20398 9.794L8.58465 13.0067C9.76998 10.072 12.6406 8 16 8C18.0393 8 19.8946 8.76934 21.3073 10.026L25.0786 6.25467C22.6973 4.03534 19.512 2.66667 16 2.66667C10.8786 2.66667 6.43731 5.558 4.20398 9.794Z"
fill="#FF3D00"
/>
<path
d="M16 29.3333C19.444 29.3333 22.5733 28.0153 24.9393 25.872L20.8127 22.38C19.429 23.4323 17.7383 24.0014 16 24C12.532 24 9.58734 21.7887 8.478 18.7027L4.13 22.0527C6.33667 26.3707 10.818 29.3333 16 29.3333Z"
fill="#4CAF50"
/>
<path
d="M29.074 13.3887H28V13.3333H16V18.6667H23.5353C23.0095 20.1443 22.0622 21.4354 20.8107 22.3807L20.8127 22.3793L24.9393 25.8713C24.6473 26.1367 29.3333 22.6667 29.3333 16C29.3333 15.106 29.2413 14.2333 29.074 13.3887Z"
fill="#1976D2"
/>
</svg> </svg>
Login With Google Login With Google
</button> </button>
<p className="text-center text-gray-500 text-xs md:text-sm mb-3">or use email</p> <p className="text-center text-gray-500 text-xs md:text-sm mb-3">
or use email
</p>
{/* Display Error Message */} {/* Display Error Message */}
{error && <div className="text-red-600 text-sm mb-3">{error}</div>} {error && <div className="text-red-600 text-sm mb-3">{error}</div>}
@@ -105,29 +145,41 @@ function ApplicantLogin({ changeRole }) {
placeholder="Email" placeholder="Email"
className="w-full mb-3 p-2 border border-gray-300 rounded-lg text-sm md:text-base focus:outline-none focus:ring-2 focus:ring-red-500" className="w-full mb-3 p-2 border border-gray-300 rounded-lg text-sm md:text-base focus:outline-none focus:ring-2 focus:ring-red-500"
value={credentials.email} value={credentials.email}
onChange={(event) => setCredentials(prev => ({ ...prev, email: event.target.value }))} onChange={(event) =>
setCredentials((prev) => ({ ...prev, email: event.target.value }))
}
/> />
<input <input
type="password" type="password"
placeholder="Password" placeholder="Password"
className="w-full mb-3 p-2 border border-gray-300 rounded-lg text-sm md:text-base focus:outline-none focus:ring-2 focus:ring-red-500" className="w-full mb-3 p-2 border border-gray-300 rounded-lg text-sm md:text-base focus:outline-none focus:ring-2 focus:ring-red-500"
autoComplete='on' autoComplete="on"
value={credentials.password} value={credentials.password}
onChange={(event) => setCredentials(prev => ({ ...prev, password: event.target.value }))} onChange={(event) =>
setCredentials((prev) => ({
...prev,
password: event.target.value,
}))
}
/> />
<div className="flex flex-col md:flex-row items-center justify-between mb-3"> <div className="flex flex-col md:flex-row items-center justify-between mb-3">
<label className="flex items-center mb-2 md:mb-0 text-sm md:text-base"> <label className="flex items-center mb-2 md:mb-0 text-sm md:text-base">
<input type="checkbox" className="mr-2" /> <input type="checkbox" className="mr-2" />
<span>Remember me</span> <span>Remember me</span>
</label> </label>
<a href="#" className="text-red-700 text-sm md:text-base hover:underline">Forgot Password?</a> <a
href="#"
className="text-red-700 text-sm md:text-base hover:underline"
>
Forgot Password?
</a>
</div> </div>
<button <button
type="submit" type="submit"
className={`bg-red-700 text-white text-sm md:text-base w-full py-2 rounded-lg font-semibold shadow-md hover:bg-red-800 transition ${loading ? 'opacity-50 cursor-not-allowed' : ''}`} className={`bg-red-700 text-white text-sm md:text-base w-full py-2 rounded-lg font-semibold shadow-md hover:bg-red-800 transition ${loading ? "opacity-50 cursor-not-allowed" : ""}`}
disabled={loading} disabled={loading}
> >
{loading ? 'Logging in...' : 'Log in'} {loading ? "Logging in..." : "Log in"}
</button> </button>
</form> </form>
</div> </div>

View File

@@ -1,11 +1,14 @@
import React, { useState } from 'react'; import React, { useState } from "react";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from "react-router-dom";
import './LoginAnimation.css'; import "./LoginAnimation.css";
function ValidatorLogin({ changeRole }) { function ValidatorLogin({ changeRole }) {
const [credentials, setCredentials] = useState({ email: 'hod.computer.kjsce@example.com', password: 'securePassword123' }); const [credentials, setCredentials] = useState({
email: "hod.computer.kjsce@example.com",
password: "securePassword123",
});
const [animate, setAnimate] = useState(false); const [animate, setAnimate] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState("");
const [loading, setLoading] = useState(false); // Loading state const [loading, setLoading] = useState(false); // Loading state
const handleChangeRole = () => { const handleChangeRole = () => {
@@ -26,52 +29,69 @@ function ValidatorLogin({ changeRole }) {
// Basic validation // Basic validation
if (!credentials.email || !credentials.password) { if (!credentials.email || !credentials.password) {
setError('Please enter both email and password.'); setError("Please enter both email and password.");
return; return;
} }
// Validate email format // Validate email format
if (!validateEmail(credentials.email)) { if (!validateEmail(credentials.email)) {
setError('Please enter a valid email address.'); setError("Please enter a valid email address.");
return; return;
} }
setLoading(true); // Show loading state setLoading(true); // Show loading state
setError(''); // Reset previous errors setError(""); // Reset previous errors
try { try {
const response = await fetch(`${import.meta.env.VITE_APP_API_URL}/validator-login`, { const response = await fetch(
method: 'POST', `${import.meta.env.VITE_APP_API_URL}/validator-login`,
{
method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
credentials: 'include', credentials: "include",
body: JSON.stringify(credentials), body: JSON.stringify(credentials),
}); },
);
const result = await response.json(); const result = await response.json();
if (response.ok) { if (response.ok) {
// Handle successful login (navigate, store tokens, etc.) // Handle successful login (navigate, store tokens, etc.)
window.location.href = '/validator/dashboard'; window.location.href = "/validator/dashboard";
} else { } else {
setError(result.message || 'Invalid login credentials.'); setError(result.message || "Invalid login credentials.");
} }
} catch (error) { } catch (error) {
console.error('Error during login:', error); console.error("Error during login:", error);
setError('An error occurred. Please try again later.'); setError("An error occurred. Please try again later.");
} finally { } finally {
setLoading(false); // Hide loading state setLoading(false); // Hide loading state
} }
}; };
const handleGoogleLogin = () => {
// Google OAuth requires a full page redirect, not a fetch call
const designation = "validator";
// Redirect to backend OAuth endpoint - it will handle the Google redirect
window.location.href = `${import.meta.env.VITE_APP_API_URL}/auth/oauth/${designation}`;
};
return ( return (
<div className="flex flex-col md:flex-row bg-red-700 shadow-lg rounded-lg overflow-hidden max-w-4xl mx-auto"> <div className="flex flex-col md:flex-row bg-red-700 shadow-lg rounded-lg overflow-hidden max-w-4xl mx-auto">
<div className={`bg-white w-full md:w-3/4 p-8 flex flex-col justify-center ${animate ? 'text-blur-out' : 'fade-in-fwd'}`}> <div
<h2 className="text-lg md:text-xl lg:text-2xl font-bold mb-3">Login for Validator<span role="img" aria-label="wave">👋</span></h2> className={`bg-white w-full md:w-3/4 p-8 flex flex-col justify-center ${animate ? "text-blur-out" : "fade-in-fwd"}`}
>
<h2 className="text-lg md:text-xl lg:text-2xl font-bold mb-3">
Login for Validator
<span role="img" aria-label="wave">
👋
</span>
</h2>
<button <button
type='button' type="button"
className="bg-gray-100 text-gray-700 text-sm md:text-base px-4 py-2 rounded-full font-semibold mb-3 shadow-md flex items-center justify-center hover:bg-gray-200 transition-transform transform hover:scale-105" className="bg-gray-100 text-gray-700 text-sm md:text-base px-4 py-2 rounded-full font-semibold mb-3 shadow-md flex items-center justify-center hover:bg-gray-200 transition-transform transform hover:scale-105"
onClick={handleSubmit} onClick={handleGoogleLogin}
> >
<svg <svg
className="w-6 h-6 mr-2" // Adjust the size of the icon if needed className="w-6 h-6 mr-2" // Adjust the size of the icon if needed
@@ -81,14 +101,28 @@ function ValidatorLogin({ changeRole }) {
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path d="M29.074 13.3887H28V13.3333H16V18.6667H23.5354C22.436 21.7713 19.482 24 16 24C11.582 24 8.00002 20.418 8.00002 16C8.00002 11.582 11.582 8 16 8C18.0394 8 19.8947 8.76934 21.3074 10.026L25.0787 6.25467C22.6974 4.03534 19.512 2.66667 16 2.66667C8.63669 2.66667 2.66669 8.63667 2.66669 16C2.66669 23.3633 8.63669 29.3333 16 29.3333C23.3634 29.3333 29.3334 23.3633 29.3334 16C29.3334 15.106 29.2414 14.2333 29.074 13.3887Z" fill="#FFC107"/> <path
<path d="M4.20398 9.794L8.58465 13.0067C9.76998 10.072 12.6406 8 16 8C18.0393 8 19.8946 8.76934 21.3073 10.026L25.0786 6.25467C22.6973 4.03534 19.512 2.66667 16 2.66667C10.8786 2.66667 6.43731 5.558 4.20398 9.794Z" fill="#FF3D00"/> d="M29.074 13.3887H28V13.3333H16V18.6667H23.5354C22.436 21.7713 19.482 24 16 24C11.582 24 8.00002 20.418 8.00002 16C8.00002 11.582 11.582 8 16 8C18.0394 8 19.8947 8.76934 21.3074 10.026L25.0787 6.25467C22.6974 4.03534 19.512 2.66667 16 2.66667C8.63669 2.66667 2.66669 8.63667 2.66669 16C2.66669 23.3633 8.63669 29.3333 16 29.3333C23.3634 29.3333 29.3334 23.3633 29.3334 16C29.3334 15.106 29.2414 14.2333 29.074 13.3887Z"
<path d="M16 29.3333C19.444 29.3333 22.5733 28.0153 24.9393 25.872L20.8127 22.38C19.429 23.4323 17.7383 24.0014 16 24C12.532 24 9.58734 21.7887 8.478 18.7027L4.13 22.0527C6.33667 26.3707 10.818 29.3333 16 29.3333Z" fill="#4CAF50"/> fill="#FFC107"
<path d="M29.074 13.3887H28V13.3333H16V18.6667H23.5353C23.0095 20.1443 22.0622 21.4354 20.8107 22.3807L20.8127 22.3793L24.9393 25.8713C24.6473 26.1367 29.3333 22.6667 29.3333 16C29.3333 15.106 29.2413 14.2333 29.074 13.3887Z" fill="#1976D2"/> />
<path
d="M4.20398 9.794L8.58465 13.0067C9.76998 10.072 12.6406 8 16 8C18.0393 8 19.8946 8.76934 21.3073 10.026L25.0786 6.25467C22.6973 4.03534 19.512 2.66667 16 2.66667C10.8786 2.66667 6.43731 5.558 4.20398 9.794Z"
fill="#FF3D00"
/>
<path
d="M16 29.3333C19.444 29.3333 22.5733 28.0153 24.9393 25.872L20.8127 22.38C19.429 23.4323 17.7383 24.0014 16 24C12.532 24 9.58734 21.7887 8.478 18.7027L4.13 22.0527C6.33667 26.3707 10.818 29.3333 16 29.3333Z"
fill="#4CAF50"
/>
<path
d="M29.074 13.3887H28V13.3333H16V18.6667H23.5353C23.0095 20.1443 22.0622 21.4354 20.8107 22.3807L20.8127 22.3793L24.9393 25.8713C24.6473 26.1367 29.3333 22.6667 29.3333 16C29.3333 15.106 29.2413 14.2333 29.074 13.3887Z"
fill="#1976D2"
/>
</svg> </svg>
Login With Google Login With Google
</button> </button>
<p className="text-center text-gray-500 text-xs md:text-sm mb-3">or use email</p> <p className="text-center text-gray-500 text-xs md:text-sm mb-3">
or use email
</p>
{/* Display Error Message */} {/* Display Error Message */}
{error && <div className="text-red-600 text-sm mb-3">{error}</div>} {error && <div className="text-red-600 text-sm mb-3">{error}</div>}
@@ -98,7 +132,9 @@ function ValidatorLogin({ changeRole }) {
placeholder="Email" placeholder="Email"
className="w-full mb-3 p-2 border border-gray-300 rounded-lg text-sm md:text-base focus:outline-none focus:ring-2 focus:ring-red-500" className="w-full mb-3 p-2 border border-gray-300 rounded-lg text-sm md:text-base focus:outline-none focus:ring-2 focus:ring-red-500"
value={credentials.email} value={credentials.email}
onChange={(event) => setCredentials(prev => ({ ...prev, email: event.target.value }))} onChange={(event) =>
setCredentials((prev) => ({ ...prev, email: event.target.value }))
}
/> />
<input <input
@@ -106,7 +142,12 @@ function ValidatorLogin({ changeRole }) {
placeholder="Password" placeholder="Password"
className="w-full mb-3 p-2 border border-gray-300 rounded-lg text-sm md:text-base focus:outline-none focus:ring-2 focus:ring-red-500" className="w-full mb-3 p-2 border border-gray-300 rounded-lg text-sm md:text-base focus:outline-none focus:ring-2 focus:ring-red-500"
value={credentials.password} value={credentials.password}
onChange={(event) => setCredentials(prev => ({ ...prev, password: event.target.value }))} onChange={(event) =>
setCredentials((prev) => ({
...prev,
password: event.target.value,
}))
}
/> />
<div className="flex flex-col md:flex-row items-center justify-between mb-3"> <div className="flex flex-col md:flex-row items-center justify-between mb-3">
@@ -114,27 +155,43 @@ function ValidatorLogin({ changeRole }) {
<input type="checkbox" className="mr-2" /> <input type="checkbox" className="mr-2" />
<span>Remember me</span> <span>Remember me</span>
</label> </label>
<a href="#" className="text-red-700 text-sm md:text-base hover:underline">Forgot Password?</a> <a
href="#"
className="text-red-700 text-sm md:text-base hover:underline"
>
Forgot Password?
</a>
</div> </div>
<button <button
type="submit" type="submit"
className={`bg-red-700 text-white text-sm md:text-base w-full py-2 rounded-lg font-semibold shadow-md hover:bg-red-800 transition ${loading ? 'opacity-50 cursor-not-allowed' : ''}`} className={`bg-red-700 text-white text-sm md:text-base w-full py-2 rounded-lg font-semibold shadow-md hover:bg-red-800 transition ${loading ? "opacity-50 cursor-not-allowed" : ""}`}
disabled={loading} disabled={loading}
> >
{loading ? 'Logging in...' : 'Log in'} {loading ? "Logging in..." : "Log in"}
</button> </button>
</form> </form>
</div> </div>
<div className={`w-full md:w-3/4 bg-red-700 p-4 flex flex-col justify-center ${animate ? 'slide-out-left' : 'fade-in-fwd'}`}> <div
<h2 className="text-white text-xl md:text-2xl lg:text-3xl font-bold mb-3 hidden md:block">Travel Policy</h2> className={`w-full md:w-3/4 bg-red-700 p-4 flex flex-col justify-center ${animate ? "slide-out-left" : "fade-in-fwd"}`}
>
<h2 className="text-white text-xl md:text-2xl lg:text-3xl font-bold mb-3 hidden md:block">
Travel Policy
</h2>
<p className="text-white text-sm md:text-base mb-6 hidden md:block"> <p className="text-white text-sm md:text-base mb-6 hidden md:block">
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.
</p> </p>
<h3 className="text-white text-lg md:text-xl font-bold">Applicant?</h3> <h3 className="text-white text-lg md:text-xl font-bold">Applicant?</h3>
<p className="text-white mb-3">Go to Applicants Sign in</p> <p className="text-white mb-3">Go to Applicants Sign in</p>
<button type='button' className="bg-white text-red-700 text-sm md:text-base px-3 py-1.5 rounded-full font-semibold shadow-md hover:bg-gray-100 transition" onClick={handleChangeRole}>Click Here</button> <button
type="button"
className="bg-white text-red-700 text-sm md:text-base px-3 py-1.5 rounded-full font-semibold shadow-md hover:bg-gray-100 transition"
onClick={handleChangeRole}
>
Click Here
</button>
</div> </div>
</div> </div>
); );