diff --git a/backend/.env.sample b/backend/.env.sample index 98f96881..424da19e 100644 --- a/backend/.env.sample +++ b/backend/.env.sample @@ -1,3 +1,7 @@ PORT=5000 -MONGO_URI=mongodb://localhost:27017/githubTracker +MONGO_URI=mongodb://mongo:27017/githubTracker SESSION_SECRET=your-secret-key +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= \ No newline at end of file diff --git a/backend/config/passportConfig.js b/backend/config/passportConfig.js index 842f50ca..97f629ae 100644 --- a/backend/config/passportConfig.js +++ b/backend/config/passportConfig.js @@ -1,4 +1,6 @@ const passport = require("passport"); +const GoogleStrategy = require('passport-google-oauth20').Strategy; +const GitHubStrategy = require('passport-github2').Strategy; const LocalStrategy = require('passport-local').Strategy; const User = require("../models/User"); @@ -29,6 +31,78 @@ passport.use( ) ); +passport.use( + new GoogleStrategy( + { + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: "/api/auth/google/callback", + state: true + }, + async (accessToken, refreshToken, profile, done) => { + try { + // Check if user already exists by their email + let user = await User.findOne({ email: profile.emails[0].value }); + + if (!user) { + // Create a user placeholder if they are logging in for the first time + user = new User({ + username: profile.displayName.replace(/\s+/g, "_").toLowerCase() + "_" + profile.id.substring(0, 5), + email: profile.emails[0].value, + // Create a dummy password since the schema requires it + password: "OAUTH_USER_EXTERNAL_PROVIDER" + }); + await user.save(); + } + + return done(null, { + id: user._id.toString(), + username: user.username, + email: user.email + }); + } catch (err) { + return done(err); + } + } + ) +); + +// 2. GitHub OAuth Strategy +passport.use( + new GitHubStrategy( + { + clientID: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + callbackURL: "/api/auth/github/callback", + state: true + }, + async (accessToken, refreshToken, profile, done) => { + try { + // If GitHub doesn't return a public email, fall back to a dummy identifier string + const userEmail = profile.emails?.[0]?.value || `${profile.username}@github.oauth`; + let user = await User.findOne({ email: userEmail }); + + if (!user) { + user = new User({ + username: profile.username, + email: userEmail, + password: "OAUTH_USER_EXTERNAL_PROVIDER" + }); + await user.save(); + } + + return done(null, { + id: user._id.toString(), + username: user.username, + email: user.email + }); + } catch (err) { + return done(err); + } + } + ) +); + // Serialize user (store user info in session) passport.serializeUser((user, done) => { done(null, user.id); diff --git a/backend/models/User.js b/backend/models/User.js index aeb09514..6b4eb037 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -14,11 +14,23 @@ const UserSchema = new mongoose.Schema({ }, password: { type: String, - required: true, + required: false, + }, + authProvider: { + type: String, + enum: ["local", "google", "github"], + default: "local", + }, + providerId: { + type: String, + default: null, }, }); UserSchema.pre('save', async function () { + if (this.password === "OAUTH_USER_EXTERNAL_PROVIDER") { + return; + } if (!this.isModified('password')) return; @@ -28,6 +40,9 @@ UserSchema.pre('save', async function () { // Compare passwords during login UserSchema.methods.comparePassword = async function (enteredPassword) { + if (this.password === "OAUTH_USER_EXTERNAL_PROVIDER") { + return false; + } return await bcrypt.compare(enteredPassword, this.password); }; diff --git a/backend/package.json b/backend/package.json index 38e15b8b..4e980c12 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,7 +6,6 @@ "dev": "nodemon server.js", "start": "node server.js", "test": "jasmine spec/**/*.spec.cjs" - }, "keywords": [], "author": "", @@ -15,12 +14,15 @@ "dependencies": { "bcryptjs": "^2.4.3", "body-parser": "^1.20.3", + "connect-mongo": "^6.0.0", "cors": "^2.8.5", "dotenv": "^16.4.5", - "express": "^4.21.1", + "express": "^4.22.2", "express-session": "^1.18.1", - "mongoose": "^8.8.2", + "mongoose": "^8.24.0", "passport": "^0.7.0", + "passport-github2": "^0.1.12", + "passport-google-oauth20": "^2.0.0", "passport-local": "^1.0.0", "zod": "^4.4.3" }, diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 7c2cda78..b463789f 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -47,4 +47,28 @@ router.get("/logout", (req, res) => { }); }); +router.get("/google", passport.authenticate("google", { scope: ["profile", "email"] })); + +router.get( + "/google/callback", + passport.authenticate("google", { + failureRedirect: `${process.env.FRONTEND_URL || "http://localhost:5173"}/login` + }), + (req, res) => { + res.redirect(process.env.FRONTEND_URL || "http://localhost:5173"); + } +); + +router.get("/github", passport.authenticate("github", { scope: ["user:email"] })); + +router.get( + "/github/callback", + passport.authenticate("github", { + failureRedirect: `${process.env.FRONTEND_URL || "http://localhost:5173"}/login` + }), + (req, res) => { + res.redirect(process.env.FRONTEND_URL || "http://localhost:5173"); + } +); + module.exports = router; diff --git a/backend/server.js b/backend/server.js index 3f19f00b..0c1769f7 100644 --- a/backend/server.js +++ b/backend/server.js @@ -5,7 +5,7 @@ const passport = require('passport'); const bodyParser = require('body-parser'); require('dotenv').config(); const cors = require('cors'); - +const MongoStore = require('connect-mongo'); // Passport configuration require('./config/passportConfig'); @@ -20,6 +20,16 @@ app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, + store: MongoStore.create({ + mongoUrl: process.env.MONGO_URI, + collectionName: 'sessions' + }), + cookie: { + maxAge: 1000 * 60 * 60 * 24, + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production' + } })); app.use(passport.initialize()); app.use(passport.session()); diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx index e77ee3b5..8a8f2a73 100644 --- a/src/pages/Login/Login.tsx +++ b/src/pages/Login/Login.tsx @@ -126,6 +126,20 @@ const Login: React.FC = () => { > {isLoading ? "Signing in..." : "Sign In"} +
{/* Message */} diff --git a/src/pages/Signup/Signup.tsx b/src/pages/Signup/Signup.tsx index b55df05d..20f3327c 100644 --- a/src/pages/Signup/Signup.tsx +++ b/src/pages/Signup/Signup.tsx @@ -209,6 +209,20 @@ const SignUp: React.FC = () => { > {isLoading ? "Creating account..." : "Create Account"} + {message && (