Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions backend/.env.sample
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
PORT=5000
MONGO_URI=mongodb://localhost:27017/githubTracker
SESSION_SECRET=your-secret-key

# GitHub OAuth
GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_secret
GITHUB_CALLBACK_URL=http://localhost:5000/api/auth/github/callback

FRONTEND_URL=http://localhost:5173
62 changes: 59 additions & 3 deletions backend/config/passportConfig.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
const passport = require("passport");
const LocalStrategy = require('passport-local').Strategy;
const GitHubStrategy = require('passport-github2').Strategy;
const User = require("../models/User");

passport.use(
new LocalStrategy(
{ usernameField: "email" },
async (email, password, done) => {
try {
const user = await User.findOne( {email} );
const user = await User.findOne({ email });
if (!user) {
return done(null, false, { message: 'Email is invalid '});
return done(null, false, { message: 'Email is invalid ' });
}

if (!user.password) {
return done(null, false, { message: 'Use GitHub sign in for this account' });
}

const isMatch = await user.comparePassword(password);
Expand All @@ -18,7 +23,7 @@ passport.use(
}

return done(null, {
id : user._id.toString(),
id: user._id.toString(),
username: user.username,
email: user.email
});
Expand All @@ -29,6 +34,57 @@ passport.use(
)
);

if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) {
passport.use(
new GitHubStrategy(
{
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: process.env.GITHUB_CALLBACK_URL,
scope: ['user:email'],
},
async (accessToken, refreshToken, profile, done) => {
try {
const primaryEmail = profile.emails?.[0]?.value || null;
const avatar = profile.photos?.[0]?.value || "";

let user = await User.findOne({ githubId: profile.id });
if (!user && primaryEmail) {
user = await User.findOne({ email: primaryEmail });
}

if (!user) {
const loginName = profile.username || `github_${profile.id}`;
const uniqueSuffix = Math.random().toString(36).slice(2, 7);

user = new User({
githubId: profile.id,
username: `${loginName}_${uniqueSuffix}`,
email: primaryEmail,
avatar,
});
} else {
user.githubId = user.githubId || profile.id;
user.email = user.email || primaryEmail;
user.avatar = user.avatar || avatar;
}

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);
Expand Down
20 changes: 18 additions & 2 deletions backend/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,26 @@ const UserSchema = new mongoose.Schema({
},
email: {
type: String,
required: true,
required: function requiredEmail() {
return !this.githubId;
},
unique: true,
sparse: true,
},
password: {
type: String,
required: true,
required: function requiredPassword() {
return !this.githubId;
},
},
githubId: {
type: String,
unique: true,
sparse: true,
},
avatar: {
type: String,
},
});

UserSchema.pre('save', async function () {
Expand All @@ -28,6 +41,9 @@ UserSchema.pre('save', async function () {

// Compare passwords during login
UserSchema.methods.comparePassword = async function (enteredPassword) {
if (!this.password)
return false;

return await bcrypt.compare(enteredPassword, this.password);
};

Expand Down
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"dev": "nodemon server.js",
"start": "node server.js",
"test": "jasmine spec/**/*.spec.cjs"

},
"keywords": [],
"author": "",
Expand All @@ -21,6 +20,7 @@
"express-session": "^1.18.1",
"mongoose": "^8.8.2",
"passport": "^0.7.0",
"passport-github2": "^0.1.12",
"passport-local": "^1.0.0",
"zod": "^4.4.3"
},
Expand Down
33 changes: 32 additions & 1 deletion backend/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,41 @@ const { signupSchema, loginSchema } = require("../validators/authValidator");
const { validateRequest } = require("../validators/validationRequest");
const router = express.Router();

const frontendUrl = process.env.FRONTEND_URL || "http://localhost:5173";
const isGitHubConfigured = Boolean(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET && process.env.GITHUB_CALLBACK_URL);

// GitHub OAuth start route
router.get("/github", (req, res, next) => {
if (!isGitHubConfigured) {
return res.redirect(`${frontendUrl}/login?githubAuth=not_configured`);
}

return passport.authenticate("github", { scope: ["user:email"] })(req, res, next);
});

// GitHub OAuth callback route
router.get(
"/github/callback",
(req, res, next) => {
if (!isGitHubConfigured) {
return res.redirect(`${frontendUrl}/login?githubAuth=not_configured`);
}

return next();
},
passport.authenticate("github", {
failureRedirect: `${frontendUrl}/login?githubAuth=failed`,
session: true,
}),
(_req, res) => {
return res.redirect(`${frontendUrl}/login?githubAuth=success`);
}
);

// Signup route
router.post("/signup", validateRequest(signupSchema), async (req, res) => {

const { username, email, password } = req.body;
const { username, email, password } = req.body;

try {
const existingUser = await User.findOne({
Expand Down
13 changes: 11 additions & 2 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,25 @@ const cors = require('cors');
require('./config/passportConfig');

const app = express();
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';

// CORS configuration
app.use(cors('*'));
app.use(cors({
origin: frontendUrl,
credentials: true,
}));

// Middleware
app.use(bodyParser.json());
app.use(session({
secret: process.env.SESSION_SECRET,
secret: process.env.SESSION_SECRET || 'dev-session-secret',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'lax',
},
}));
app.use(passport.initialize());
app.use(passport.session());
Expand Down
Loading
Loading