forked from CSI-KJSCE/Travel-policy-
Implemented Oauth
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.env
|
||||
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;
|
||||
@@ -24,6 +24,8 @@ services:
|
||||
# PostgreSQL Database Service
|
||||
db:
|
||||
image: postgres:17-alpine
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_USER=${POSTGRES_USER}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
@@ -32,4 +34,4 @@ services:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
pgdata:
|
||||
|
||||
150
frontend/package-lock.json
generated
150
frontend/package-lock.json
generated
@@ -10,7 +10,7 @@
|
||||
"dependencies": {
|
||||
"@react-pdf/renderer": "^4.1.6",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^1.7.5",
|
||||
"axios": "^1.13.2",
|
||||
"bootstrap": "^5.3.3",
|
||||
"chart.js": "^4.4.7",
|
||||
"chartjs-plugin-datalabels": "^2.2.0",
|
||||
@@ -18,6 +18,7 @@
|
||||
"framer-motion": "^11.15.0",
|
||||
"frontend": "file:",
|
||||
"hamburger-react": "^2.5.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"pdfjs-dist": "^4.7.76",
|
||||
"postcss": "^8.4.40",
|
||||
"react": "^18.3.1",
|
||||
@@ -2007,13 +2008,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
@@ -2185,6 +2186,19 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -2641,6 +2655,20 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@@ -2717,13 +2745,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.4"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -2732,7 +2757,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -2763,10 +2787,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
|
||||
"integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
|
||||
"dev": true,
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
@@ -2775,14 +2799,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
|
||||
"integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
|
||||
"dev": true,
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.1"
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -3355,13 +3380,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3592,16 +3619,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
||||
"dev": true,
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"has-proto": "^1.0.1",
|
||||
"has-symbols": "^1.0.3",
|
||||
"hasown": "^2.0.0"
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -3610,6 +3642,19 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-symbol-description": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
|
||||
@@ -3684,12 +3729,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.1.3"
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -3752,10 +3797,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"dev": true,
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -3767,7 +3812,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
@@ -4336,6 +4380,15 @@
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
},
|
||||
"node_modules/js-cookie": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -4530,6 +4583,15 @@
|
||||
"url": "https://github.com/wojtekmaj/make-event-props?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-engine": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"dependencies": {
|
||||
"@react-pdf/renderer": "^4.1.6",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^1.7.5",
|
||||
"axios": "^1.13.2",
|
||||
"bootstrap": "^5.3.3",
|
||||
"chart.js": "^4.4.7",
|
||||
"chartjs-plugin-datalabels": "^2.2.0",
|
||||
@@ -20,6 +20,7 @@
|
||||
"framer-motion": "^11.15.0",
|
||||
"frontend": "file:",
|
||||
"hamburger-react": "^2.5.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"pdfjs-dist": "^4.7.76",
|
||||
"postcss": "^8.4.40",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -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;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { useNavigate, useRouteLoaderData } from "react-router-dom";
|
||||
import OAuthCallbackHandler from "../../components/OAuthCallback/OAuthCallbackHandler";
|
||||
|
||||
function Dashboard() {
|
||||
const { role, user } =
|
||||
@@ -15,7 +16,8 @@ function Dashboard() {
|
||||
const greetingLine2 = `${designation} in ${department} Department, ${institute}`;
|
||||
|
||||
return (
|
||||
<div className="font-sans bg-white overflow-y-scroll scroll-smooth snap-y h-screen" >
|
||||
<OAuthCallbackHandler>
|
||||
<div className="font-sans bg-white overflow-y-scroll scroll-smooth snap-y h-screen" >
|
||||
{/* Hero Section */}
|
||||
<section
|
||||
className="relative w-full h-screen flex items-center justify-center text-white overflow-hidden bg-cover bg-center snap-start"
|
||||
@@ -161,7 +163,8 @@ function Dashboard() {
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
</OAuthCallbackHandler>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
|
||||
@@ -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 loginPageBg from '/images/campus_bg.jpeg';
|
||||
@@ -8,6 +9,26 @@ import ValidatorLogin from './components/ValidatorLogin';
|
||||
|
||||
const Login = () => {
|
||||
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 = () => {
|
||||
setIsApplicant(!isApplicant);
|
||||
@@ -17,6 +38,24 @@ const Login = () => {
|
||||
<div className="login-page">
|
||||
<img src={loginPageBg} className='loginPage_bg' />
|
||||
<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`}>
|
||||
{isApplicant ? (
|
||||
<>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import './LoginAnimation.css';
|
||||
import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import "./LoginAnimation.css";
|
||||
|
||||
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 [error, setError] = useState('');
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false); // Loading state
|
||||
|
||||
const handleChangeRole = () => {
|
||||
@@ -17,68 +20,91 @@ function ApplicantLogin({ changeRole }) {
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
// Basic Validation
|
||||
if (!credentials.email || !credentials.password) {
|
||||
setError('Please enter both email and password.');
|
||||
setError("Please enter both email and password.");
|
||||
return;
|
||||
}
|
||||
if (!/\S+@\S+\.\S+/.test(credentials.email)) {
|
||||
setError('Please enter a valid email address.');
|
||||
setError("Please enter a valid email address.");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true); // Show loading state
|
||||
setError(''); // Reset previous errors
|
||||
setError(""); // Reset previous errors
|
||||
|
||||
try {
|
||||
const response = await fetch(`${import.meta.env.VITE_APP_API_URL}/applicant-login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_APP_API_URL}/applicant-login`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(credentials),
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
window.location.href = '/applicant/dashboard';
|
||||
window.location.href = "/applicant/dashboard";
|
||||
} else {
|
||||
setError(result.message || 'Invalid login credentials.');
|
||||
setError(result.message || "Invalid login credentials.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during login:', error);
|
||||
setError('An error occurred. Please try again later.');
|
||||
console.error("Error during login:", error);
|
||||
setError("An error occurred. Please try again later.");
|
||||
} finally {
|
||||
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 (
|
||||
<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'}`}>
|
||||
<h2 className="text-white text-xl md:text-2xl lg:text-3xl font-bold mb-3 hidden md:block">Travel Policy</h2>
|
||||
<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"}`}
|
||||
>
|
||||
<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">
|
||||
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>
|
||||
<h3 className="text-white text-lg md:text-xl font-bold">Validator?</h3>
|
||||
<p className="text-white mb-3">Go to Validator’s 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"
|
||||
<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 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
|
||||
type='button'
|
||||
<div
|
||||
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
|
||||
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"
|
||||
onClick={handleSubmit}
|
||||
onClick={handleGoogleLogin}
|
||||
>
|
||||
<svg
|
||||
className="w-6 h-6 mr-2"
|
||||
@@ -88,46 +114,72 @@ function ApplicantLogin({ changeRole }) {
|
||||
fill="none"
|
||||
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 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"/>
|
||||
<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
|
||||
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>
|
||||
Login With Google
|
||||
</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 */}
|
||||
{error && <div className="text-red-600 text-sm mb-3">{error}</div>}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
placeholder="Email"
|
||||
<input
|
||||
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"
|
||||
value={credentials.email}
|
||||
onChange={(event) => setCredentials(prev => ({ ...prev, email: event.target.value }))}
|
||||
value={credentials.email}
|
||||
onChange={(event) =>
|
||||
setCredentials((prev) => ({ ...prev, email: event.target.value }))
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
<input
|
||||
type="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"
|
||||
autoComplete='on'
|
||||
value={credentials.password}
|
||||
onChange={(event) => setCredentials(prev => ({ ...prev, password: event.target.value }))}
|
||||
autoComplete="on"
|
||||
value={credentials.password}
|
||||
onChange={(event) =>
|
||||
setCredentials((prev) => ({
|
||||
...prev,
|
||||
password: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<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">
|
||||
<input type="checkbox" className="mr-2" />
|
||||
<span>Remember me</span>
|
||||
</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>
|
||||
<button
|
||||
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' : ''}`}
|
||||
<button
|
||||
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" : ""}`}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Logging in...' : 'Log in'}
|
||||
{loading ? "Logging in..." : "Log in"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import './LoginAnimation.css';
|
||||
import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import "./LoginAnimation.css";
|
||||
|
||||
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 [error, setError] = useState('');
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false); // Loading state
|
||||
|
||||
const handleChangeRole = () => {
|
||||
@@ -26,52 +29,69 @@ function ValidatorLogin({ changeRole }) {
|
||||
|
||||
// Basic validation
|
||||
if (!credentials.email || !credentials.password) {
|
||||
setError('Please enter both email and password.');
|
||||
setError("Please enter both email and password.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
if (!validateEmail(credentials.email)) {
|
||||
setError('Please enter a valid email address.');
|
||||
setError("Please enter a valid email address.");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true); // Show loading state
|
||||
setError(''); // Reset previous errors
|
||||
setError(""); // Reset previous errors
|
||||
|
||||
try {
|
||||
const response = await fetch(`${import.meta.env.VITE_APP_API_URL}/validator-login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_APP_API_URL}/validator-login`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(credentials),
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
// Handle successful login (navigate, store tokens, etc.)
|
||||
window.location.href = '/validator/dashboard';
|
||||
window.location.href = "/validator/dashboard";
|
||||
} else {
|
||||
setError(result.message || 'Invalid login credentials.');
|
||||
setError(result.message || "Invalid login credentials.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during login:', error);
|
||||
setError('An error occurred. Please try again later.');
|
||||
console.error("Error during login:", error);
|
||||
setError("An error occurred. Please try again later.");
|
||||
} finally {
|
||||
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 (
|
||||
<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'}`}>
|
||||
<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
|
||||
type='button'
|
||||
<div
|
||||
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
|
||||
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"
|
||||
onClick={handleSubmit}
|
||||
onClick={handleGoogleLogin}
|
||||
>
|
||||
<svg
|
||||
className="w-6 h-6 mr-2" // Adjust the size of the icon if needed
|
||||
@@ -81,32 +101,53 @@ function ValidatorLogin({ changeRole }) {
|
||||
fill="none"
|
||||
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 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"/>
|
||||
<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
|
||||
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>
|
||||
Login With Google
|
||||
</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 */}
|
||||
{error && <div className="text-red-600 text-sm mb-3">{error}</div>}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
placeholder="Email"
|
||||
<input
|
||||
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"
|
||||
value={credentials.email}
|
||||
onChange={(event) => setCredentials(prev => ({ ...prev, email: event.target.value }))}
|
||||
value={credentials.email}
|
||||
onChange={(event) =>
|
||||
setCredentials((prev) => ({ ...prev, email: event.target.value }))
|
||||
}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
<input
|
||||
type="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"
|
||||
value={credentials.password}
|
||||
onChange={(event) => setCredentials(prev => ({ ...prev, password: event.target.value }))}
|
||||
value={credentials.password}
|
||||
onChange={(event) =>
|
||||
setCredentials((prev) => ({
|
||||
...prev,
|
||||
password: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
|
||||
<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" />
|
||||
<span>Remember me</span>
|
||||
</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>
|
||||
|
||||
<button
|
||||
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' : ''}`}
|
||||
<button
|
||||
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" : ""}`}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Logging in...' : 'Log in'}
|
||||
{loading ? "Logging in..." : "Log in"}
|
||||
</button>
|
||||
</form>
|
||||
</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'}`}>
|
||||
<h2 className="text-white text-xl md:text-2xl lg:text-3xl font-bold mb-3 hidden md:block">Travel Policy</h2>
|
||||
<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"}`}
|
||||
>
|
||||
<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">
|
||||
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>
|
||||
<h3 className="text-white text-lg md:text-xl font-bold">Applicant?</h3>
|
||||
<p className="text-white mb-3">Go to Applicant’s 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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user