From 775a687ddae6a8c651768edbaf3271d6d45a8ecf Mon Sep 17 00:00:00 2001 From: indar suthar Date: Thu, 23 Oct 2025 23:53:44 +0530 Subject: [PATCH] feat: adding single sign-in option(google & github) --- backend/.env.example | 11 ++ backend/package-lock.json | 166 ++++++++++++++++++++++ backend/package.json | 8 +- backend/src/app.ts | 12 +- backend/src/controllers/authController.ts | 63 ++++++-- backend/src/models/userModel.ts | 29 ++-- backend/src/routes/authRoutes.ts | 31 +++- backend/src/utils/passport.ts | 126 ++++++++++++++++ backend/tsconfig.tsbuildinfo | 2 +- frontend/.env.example | 2 + frontend/package-lock.json | 1 + frontend/src/App.tsx | 14 +- frontend/src/components/SocialLogin.tsx | 41 ++++++ frontend/src/pages/OAuthSuccess.tsx | 41 ++++++ frontend/src/pages/Signin.tsx | 21 ++- 15 files changed, 522 insertions(+), 46 deletions(-) create mode 100644 backend/.env.example create mode 100644 backend/src/utils/passport.ts create mode 100644 frontend/.env.example create mode 100644 frontend/src/components/SocialLogin.tsx create mode 100644 frontend/src/pages/OAuthSuccess.tsx diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..9eec154 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,11 @@ +MONGO_URI = mongodb://localhost:27017/PeerCall +PORT = 3000 +JWT_SECRET = secret12peercall +FRONTEND_URL = http://localhost:5173 + +GITHUB_CLIENT_ID = put yours +GITHUB_CLIENT_SECRET put yours +GOOGLE_CLIENT_ID = put yours +GOOGLE_CLIENT_SECRET = put yours +GOOGLE_CALLBACK_URL = http://localhost:3000/api/auth/google/callback +GITHUB_CALLBACK_URL = http://localhost:3000/api/auth/github/callback \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index a5555f2..05e6086 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,6 +16,9 @@ "jsonwebtoken": "^9.0.2", "mongodb": "^6.20.0", "mongoose": "^8.19.2", + "passport": "^0.7.0", + "passport-github2": "^0.1.12", + "passport-google-oauth20": "^2.0.0", "socket.io": "^4.8.1", "zod": "^4.1.12" }, @@ -24,6 +27,9 @@ "@types/express": "^5.0.3", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.9.1", + "@types/passport": "^1.0.17", + "@types/passport-github2": "^1.2.9", + "@types/passport-google-oauth20": "^2.0.16", "@types/socket.io": "^3.0.1", "nodemon": "^3.1.10", "ts-node": "^10.9.2", @@ -219,6 +225,62 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/oauth": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.6.tgz", + "integrity": "sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-github2": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@types/passport-github2/-/passport-github2-1.2.9.tgz", + "integrity": "sha512-/nMfiPK2E6GKttwBzwj0Wjaot8eHrM57hnWxu52o6becr5/kXlH/4yE2v2rh234WGvSgEEzIII02Nc5oC5xEHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-oauth2": "*" + } + }, + "node_modules/@types/passport-google-oauth20": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.16.tgz", + "integrity": "sha512-ayXK2CJ7uVieqhYOc6k/pIr5pcQxOLB6kBev+QUGS7oEZeTgIs1odDobXRqgfBPvXzl0wXCQHftV5220czZCPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-oauth2": "*" + } + }, + "node_modules/@types/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-6//z+4orIOy/g3zx17HyQ71GSRK4bs7Sb+zFasRoc2xzlv7ZCJ+vkDBYFci8U6HY+or6Zy7ajf4mz4rK7nsWJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/oauth": "*", + "@types/passport": "*" + } + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -381,6 +443,15 @@ "node": "^4.5.0 || >= 5.9" } }, + "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": "3.0.2", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", @@ -1553,6 +1624,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", @@ -1604,6 +1681,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-github2": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz", + "integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -1631,6 +1777,11 @@ "url": "https://opencollective.com/express" } }, + "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", @@ -2359,6 +2510,12 @@ "node": ">=14.17" } }, + "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", @@ -2381,6 +2538,15 @@ "node": ">= 0.8" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index 5fce7eb..9ecd185 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,6 +19,9 @@ "jsonwebtoken": "^9.0.2", "mongodb": "^6.20.0", "mongoose": "^8.19.2", + "passport": "^0.7.0", + "passport-github2": "^0.1.12", + "passport-google-oauth20": "^2.0.0", "socket.io": "^4.8.1", "zod": "^4.1.12" }, @@ -27,10 +30,13 @@ "@types/express": "^5.0.3", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.9.1", + "@types/passport": "^1.0.17", + "@types/passport-github2": "^1.2.9", + "@types/passport-google-oauth20": "^2.0.16", "@types/socket.io": "^3.0.1", "nodemon": "^3.1.10", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", "typescript": "^5.9.3" } -} \ No newline at end of file +} diff --git a/backend/src/app.ts b/backend/src/app.ts index f69831f..390de29 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -4,12 +4,22 @@ import authRoutes from "./routes/authRoutes.js"; import healthRoutes from "./routes/healthRoutes.js"; import { errorHandler } from "./middleware/errorHandler.js"; import roomRoutes from "./routes/roomRoutes.js"; - +import passport from "passport"; +import "./utils/passport.js" +import cors from "cors"; dotenv.config(); const app = express(); app.use(express.json()); +app.use( + cors({ + origin: process.env.FRONTEND_URL || "http://localhost:5173", + credentials: true, + }) +); +//initialize passport +app.use(passport.initialize()); // Routes app.use("/api/auth", authRoutes); app.use("/api/health", healthRoutes); diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/authController.ts index 749d20d..bb61a2b 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/authController.ts @@ -1,60 +1,97 @@ import type { Request, Response, NextFunction } from "express"; import bcrypt from "bcryptjs"; -import User from "../models/userModel.js"; +import User, { type IUser } from "../models/userModel.js"; import { generateToken } from "../utils/generateToken.js"; import { userSchema, loginSchema } from "../utils/validateInputs.js"; +import dotenv from "dotenv"; +dotenv.config(); +const asTypedUser = (user: any): IUser & { _id: string } => user as IUser & { _id: string }; + +//signup controller export const registerUser = async (req: Request, res: Response, next: NextFunction) => { try { const parseResult = userSchema.safeParse(req.body); - if (!parseResult.success) { - return res.status(400).json({ success: false, message: parseResult.error.issues[0]?.message }); + return res.status(400).json({ + success: false, + message: parseResult.error.issues[0]?.message, + }); } const { name, email, password } = parseResult.data; - const existingUser = await User.findOne({ email }); if (existingUser) return res.status(400).json({ success: false, message: "Email already registered" }); const hashedPassword = await bcrypt.hash(password, 10); - const user = await User.create({ name, email, password: hashedPassword }); + const newUser = await User.create({ name, email, password: hashedPassword }); + const typedUser = asTypedUser(newUser); res.status(201).json({ success: true, message: "User registered successfully", - token: generateToken(user._id.toString()), + token: generateToken(typedUser._id.toString()), }); } catch (err) { next(err); } }; +//sign in controller export const loginUser = async (req: Request, res: Response, next: NextFunction) => { try { const parseResult = loginSchema.safeParse(req.body); - if (!parseResult.success) { - return res.status(400).json({ success: false, message: parseResult.error.issues[0]?.message || "Validation error" }); + return res.status(400).json({ + success: false, + message: parseResult.error.issues[0]?.message || "Validation error", + }); } const { email, password } = parseResult.data; - - const user = await User.findOne({ email }); - if (!user) + const foundUser = await User.findOne({ email }); + if (!foundUser) return res.status(400).json({ success: false, message: "Invalid credentials" }); + if (!foundUser.password || foundUser.password === "") { + return res.status(400).json({ + success: false, + message: "This account was registered via SSO. Please sign in with Google or GitHub.", + }); + } - const isMatch = await bcrypt.compare(password, user.password); + const isMatch = await bcrypt.compare(password, foundUser.password); if (!isMatch) return res.status(400).json({ success: false, message: "Invalid credentials" }); + const typedUser = asTypedUser(foundUser); + res.json({ success: true, message: "Login successful", - token: generateToken(user._id.toString()), + token: generateToken(typedUser._id.toString()), }); } catch (err) { next(err); } }; + +//OAuth callback handler +export const oauthCallback = (req: Request & { user?: any }, res: Response) => { + try { + const frontendUrl = process.env.FRONTEND_URL || "http://localhost:5173"; + const user = req.user as IUser & { _id: string } | undefined; + + if (!user) { + return res.redirect(`${frontendUrl}/signin?error=oauth_failed`); + } + + const token = generateToken(user._id.toString()); + const redirectUrl = `${frontendUrl}/oauth-success#token=${token}`; + + return res.redirect(redirectUrl); + } catch (err) { + const frontendUrl = process.env.FRONTEND_URL || "http://localhost:5173"; + return res.redirect(`${frontendUrl}/signin?error=server_error`); + } +}; diff --git a/backend/src/models/userModel.ts b/backend/src/models/userModel.ts index 1c17d5c..80fba91 100644 --- a/backend/src/models/userModel.ts +++ b/backend/src/models/userModel.ts @@ -1,12 +1,23 @@ -import mongoose from "mongoose"; +import mongoose, { Schema, Document } from "mongoose"; -const userSchema = new mongoose.Schema( - { - name: { type: String, required: true }, - email: { type: String, required: true, unique: true }, - password: { type: String, required: true }, - }, - { timestamps: true } +export interface IUser extends Document { + name: string; + email: string; + password?: string; // optional for SSO users + ssoProvider?: string; // "google" or "github" + ssoId?: string; + createdAt: Date; +} + +const userSchema: Schema = new Schema( + { + name: { type: String, required: true }, + email: { type: String, required: true, unique: true, index: true }, + password: { type: String, default: "" }, // empty for SSO accounts + ssoProvider: { type: String, enum: ["google", "github"], default: null }, + ssoId: { type: String, default: null }, + }, + { timestamps: true } ); -export default mongoose.model("User", userSchema); +export default mongoose.model("User", userSchema); \ No newline at end of file diff --git a/backend/src/routes/authRoutes.ts b/backend/src/routes/authRoutes.ts index 9930d18..5fb34c0 100644 --- a/backend/src/routes/authRoutes.ts +++ b/backend/src/routes/authRoutes.ts @@ -1,9 +1,32 @@ import express from "express"; -import { registerUser, loginUser } from "../controllers/authController.js"; +import { registerUser, loginUser, oauthCallback } from "../controllers/authController.js"; +import passport from "passport"; const router = express.Router(); -router.post("/register", registerUser); -router.post("/login", loginUser); +router.post("/signup", registerUser); +router.post("/signin", loginUser); -export default router; +// Google OAuth +router.get( + "/google", + passport.authenticate("google", { scope: ["profile", "email"], session: false }) +); +router.get( + "/google/callback", + passport.authenticate("google", { session: false, failureRedirect: process.env.FRONTEND_URL ? `${process.env.FRONTEND_URL}/signin` : "/signin" }), + oauthCallback +); + +// GitHub OAuth +router.get( + "/github", + passport.authenticate("github", { scope: ["user:email"], session: false }) +); +router.get( + "/github/callback", + passport.authenticate("github", { session: false, failureRedirect: process.env.FRONTEND_URL ? `${process.env.FRONTEND_URL}/signin` : "/signin" }), + oauthCallback +); + +export default router; \ No newline at end of file diff --git a/backend/src/utils/passport.ts b/backend/src/utils/passport.ts new file mode 100644 index 0000000..d1d9bef --- /dev/null +++ b/backend/src/utils/passport.ts @@ -0,0 +1,126 @@ +import passport from "passport"; +import { Strategy as GoogleStrategy } from "passport-google-oauth20"; +import { Strategy as GitHubStrategy } from "passport-github2"; +import dotenv from "dotenv"; +import User, { type IUser } from "../models/userModel.js"; +import type { Document } from "mongoose"; + +dotenv.config(); + +const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID!; +const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET!; +const GOOGLE_CALLBACK_URL = process.env.GOOGLE_CALLBACK_URL!; + +const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID!; +const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET!; +const GITHUB_CALLBACK_URL = process.env.GITHUB_CALLBACK_URL!; + +//google strategy +passport.use( + new GoogleStrategy( + { + clientID: GOOGLE_CLIENT_ID, + clientSecret: GOOGLE_CLIENT_SECRET, + callbackURL: GOOGLE_CALLBACK_URL, + }, + async (accessToken, refreshToken, profile, done) => { + try { + const email = profile.emails?.[0]?.value; + const name = profile.displayName || profile.name?.givenName || "Google User"; + const providerId = profile.id; + + if (!email) { + return done(new Error("No email found in Google profile"), undefined); + } + + let user = await User.findOne({ email }); + + if (user) { + if (!user.ssoProvider || !user.ssoId) { + user.ssoProvider = "google"; + user.ssoId = providerId; + await user.save(); + } + } else { + user = await User.create({ + name, + email, + password: "", + ssoProvider: "google", + ssoId: providerId, + }); + } + + done(null, user as IUser & { _id: string }); + } catch (err) { + done(err as Error, undefined); + } + } + ) +); +//github strategy +passport.use( + new GitHubStrategy( + { + clientID: GITHUB_CLIENT_ID, + clientSecret: GITHUB_CLIENT_SECRET, + callbackURL: GITHUB_CALLBACK_URL, + scope: ["user:email"], + }, + async ( + _accessToken: string, + _refreshToken: string, + profile: { emails?: { value: string }[]; username?: string; displayName?: string; id: string }, + done: (err: Error | null, user?: IUser & { _id: string }) => void + ) => { + try { + const emailFromProfile = profile.emails?.[0]?.value; + const fallbackEmail = profile.username ? `${profile.username}@users.noreply.github.com` : undefined; + const email = emailFromProfile || fallbackEmail; + const name = profile.displayName || profile.username || "GitHub User"; + const providerId = profile.id; + + if (!email) { + return done(new Error("No email found in GitHub profile"), undefined); + } + + let user = await User.findOne({ email }); + + if (user) { + if (!user.ssoProvider || !user.ssoId) { + user.ssoProvider = "github"; + user.ssoId = providerId; + await user.save(); + } + } else { + user = await User.create({ + name, + email, + password: "", + ssoProvider: "github", + ssoId: providerId, + }); + } + + done(null, user as IUser & { _id: string }); + } catch (err) { + done(err as Error, undefined); + } + } + ) +); +// Serialize and deserialize user +passport.serializeUser((user: any, done) => { + done(null, user?._id?.toString() || undefined); +}); + +passport.deserializeUser(async (id: string, done) => { + try { + const user = await User.findById(id); + done(null, user as (IUser & { _id: string }) | undefined); + } catch (err) { + done(err as Error, undefined); + } +}); + +export default passport; diff --git a/backend/tsconfig.tsbuildinfo b/backend/tsconfig.tsbuildinfo index 40067f3..6ad5b5b 100644 --- a/backend/tsconfig.tsbuildinfo +++ b/backend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.ts","./src/server.ts","./src/controllers/authcontroller.ts","./src/controllers/healthcontroller.ts","./src/controllers/roomcontroller.ts","./src/middleware/authmiddleware.ts","./src/middleware/errorhandler.ts","./src/models/chatmessagemodel.ts","./src/models/roommodel.ts","./src/models/usermodel.ts","./src/routes/authroutes.ts","./src/routes/healthroutes.ts","./src/routes/roomroutes.ts","./src/utils/generatetoken.ts","./src/utils/validateinputs.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/app.ts","./src/server.ts","./src/controllers/authcontroller.ts","./src/controllers/healthcontroller.ts","./src/controllers/roomcontroller.ts","./src/middleware/authmiddleware.ts","./src/middleware/errorhandler.ts","./src/models/chatmessagemodel.ts","./src/models/roommodel.ts","./src/models/usermodel.ts","./src/routes/authroutes.ts","./src/routes/healthroutes.ts","./src/routes/roomroutes.ts","./src/utils/generatetoken.ts","./src/utils/passport.ts","./src/utils/validateinputs.ts"],"version":"5.9.3"} \ No newline at end of file diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..846b079 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +VITE_API_URL=http://localhost:3000 +VITE_FRONTEND_URL=http://localhost:5173 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5fa4f38..af8281c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -6954,6 +6954,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 18c0e4f..08ef271 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,11 +1,12 @@ -import { Toaster } from "./components/ui/toaster"; -import { Toaster as Sonner } from "./components/ui/sonner"; -import { TooltipProvider } from "./components/ui/tooltip"; +import { Toaster } from "./components/ui/toaster.js"; +import { Toaster as Sonner } from "./components/ui/sonner.js"; +import { TooltipProvider } from "./components/ui/tooltip.js"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter, Routes, Route } from "react-router-dom"; -import Index from "./pages/Index"; -import SignUp from "./pages/Signup"; -import SignIn from "./pages/Signin"; +import Index from "./pages/Index.js"; +import SignUp from "./pages/Signup.js"; +import SignIn from "./pages/Signin.js"; +import OAuthSuccess from "./pages/OAuthSuccess.js"; import "./index.css" const queryClient = new QueryClient(); @@ -19,6 +20,7 @@ const App = () => ( } /> } /> } /> + } /> diff --git a/frontend/src/components/SocialLogin.tsx b/frontend/src/components/SocialLogin.tsx new file mode 100644 index 0000000..fb96c6c --- /dev/null +++ b/frontend/src/components/SocialLogin.tsx @@ -0,0 +1,41 @@ +import React from "react"; + +const apiBase = import.meta.env.VITE_API_URL || ""; + +const SocialLogin: React.FC = () => { + const startGoogleSignIn = () => { + window.location.href = `${apiBase}/api/auth/google`; + }; + + const startGithubSignIn = () => { + window.location.href = `${apiBase}/api/auth/github`; + }; + + return ( +
+ + +
+ ); +}; + +export default SocialLogin; diff --git a/frontend/src/pages/OAuthSuccess.tsx b/frontend/src/pages/OAuthSuccess.tsx new file mode 100644 index 0000000..13f2c4f --- /dev/null +++ b/frontend/src/pages/OAuthSuccess.tsx @@ -0,0 +1,41 @@ +import React, { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +const OAuthSuccess: React.FC = () => { + const navigate = useNavigate(); + const [message, setMessage] = useState("Finalizing sign in..."); + + useEffect(() => { + const hash = window.location.hash || ""; + const fragment = hash.startsWith("#") ? hash.slice(1) : hash; + const params = new URLSearchParams(fragment); + const token = params.get("token"); + + if (token) { + try { + localStorage.setItem("token", token); + setMessage("Sign in successful — redirecting..."); + setTimeout(() => { + navigate("/", { replace: true }); + }, 700); + } catch (err) { + console.error("Failed to save token", err); + setMessage("Sign in succeeded but we couldn't save the session locally."); + setTimeout(() => navigate("/signin"), 1500); + } + } else { + setMessage("No token found in redirect. Please try signing in again."); + setTimeout(() => navigate("/signin"), 1300); + } + }, [navigate]); + + return ( +
+
+

{message}

+
+
+ ); +}; + +export default OAuthSuccess; \ No newline at end of file diff --git a/frontend/src/pages/Signin.tsx b/frontend/src/pages/Signin.tsx index 8760e41..dc17796 100644 --- a/frontend/src/pages/Signin.tsx +++ b/frontend/src/pages/Signin.tsx @@ -1,12 +1,13 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; import { Link } from "react-router-dom"; -import { Button } from "../components/ui/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../components/ui/card"; -import { InputField } from "../components/InputField"; -import { toast } from "../hooks/use-toast"; -import { mockLogin, SignInData } from "../lib/api"; +import { Button } from "../components/ui/button.js" +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../components/ui/card.js"; +import { InputField } from "../components/InputField.js"; +import { toast } from "../hooks/use-toast.js"; +import { mockLogin, SignInData } from "../lib/api.js"; import { Loader2 } from "lucide-react"; +import SocialLogin from "../components/SocialLogin.js"; const SignIn = () => { const [isLoading, setIsLoading] = useState(false); @@ -26,7 +27,7 @@ const SignIn = () => { description: response.message, }); reset(); - // Navigate to dashboard or home page here + // Navigate home page } catch (error: any) { toast({ title: "Error", @@ -97,11 +98,9 @@ const SignIn = () => { )} - -
-

- Demo credentials: demo@peercall.com / password123 -

+
+
Or continue with
+