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
10 changes: 10 additions & 0 deletions backend/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +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
147 changes: 114 additions & 33 deletions backend/config/passportConfig.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,126 @@
const passport = require("passport");
const LocalStrategy = require('passport-local').Strategy;
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} );
if (!user) {
return done(null, false, { message: 'Email is invalid '});
}

const isMatch = await user.comparePassword(password);
if (!isMatch) {
return done(null, false, { message: 'Invalid password' });
}

return done(null, {
id : user._id.toString(),
username: user.username,
email: user.email
});
} catch (err) {
return done(err);
new LocalStrategy(
{ usernameField: "email" },
async (email, password, done) => {
try {
const user = await User.findOne({ email });

if (!user) {
return done(null, false, {
message: "Invalid email or password",
});
}

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

const isMatch = await user.comparePassword(password);

if (!isMatch) {
return done(null, false, {
message: "Invalid email or password",
});
}

return done(null, {
id: user._id.toString(),
username: user.username,
email: user.email,
});
} catch (err) {
return done(err);
}
}
)
);

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"],
state: true,
},

async (accessToken, refreshToken, profile, done) => {
try {
const primaryEmail = profile.emails?.[0]?.value;
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);

const userData = {
githubId: profile.id,
username: `${loginName}_${uniqueSuffix}`,
avatar,
};

if (primaryEmail) {
userData.email = primaryEmail;
}

user = new User(userData);

} else {
user.githubId = user.githubId || profile.id;

if (primaryEmail) {
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)
// Serialize user
passport.serializeUser((user, done) => {
done(null, user.id);
done(null, user.id);
});

// Deserialize user (retrieve user from session)
// Deserialize user
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (err) {
done(err, null);
}
});
try {
const user = await User.findById(id);
done(null, user);
} catch (err) {
done(err, null);
}
});
31 changes: 25 additions & 6 deletions backend/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,46 @@ const UserSchema = new mongoose.Schema({
required: true,
unique: true,
},

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,
},
});

// ✅ FIXED: no next()
UserSchema.pre('save', async function () {
if (!this.isModified('password')) return;
// password hashing
UserSchema.pre("save", async function () {
if (!this.isModified("password")) return;

const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
});

// password comparison
// password comparison
UserSchema.methods.comparePassword = async function (enteredPassword) {
if (!this.password) return false;

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

Expand Down
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,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",
"winston": "^3.19.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 @@ -12,16 +12,25 @@ require('./config/passportConfig');
const logger = require('./logger');

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',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect backend/server.js around the referenced lines
if [ -f backend/server.js ]; then
  echo "=== backend/server.js (head) ==="
  sed -n '1,120p' backend/server.js | cat -n
  echo
  echo "=== Search for session config and SESSION_SECRET ==="
  rg -n "SESSION_SECRET|express-session|app\\.use\\(session\\(|cookie:|sameSite|trust proxy|cors\\(" backend/server.js || true
else
  echo "backend/server.js not found"
  ls -la backend || true
fi

# Check for any other session middleware usage elsewhere
echo
echo "=== Search repo for express-session usage ==="
rg -n "express-session|app\\.use\\(session\\(" . || true

# Check for CORS usage and FRONTEND_URL usage
echo
echo "=== Search repo for CORS/front-end URL config ==="
rg -n "cors\\(|FRONTEND_URL|frontendUrl" . || true

Repository: GitMetricsLab/github_tracker

Length of output: 3224


Fail fast if SESSION_SECRET is missing in production (backend/server.js:26)

express-session currently falls back to 'dev-session-secret', making session cookie signing predictable when prod env config is incomplete. (Also, with sameSite: 'lax', cross-site credentialed requests may fail if the frontend is on a different site.)

Suggested fix
 const app = express();
 const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
+const isProd = process.env.NODE_ENV === 'production';
+if (isProd && !process.env.SESSION_SECRET) {
+    throw new Error('SESSION_SECRET must be set in production');
+}
@@
 app.use(session({
-    secret: process.env.SESSION_SECRET || 'dev-session-secret',
+    secret: process.env.SESSION_SECRET || 'dev-session-secret',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/server.js` at line 26, The session middleware is using a fallback
'dev-session-secret' which should not be used in production; before calling the
express-session setup (the session({...}) call and its secret property), check
NODE_ENV === 'production' and if process.env.SESSION_SECRET is missing, throw an
error or call process.exit(1) to fail fast; update the session configuration to
use process.env.SESSION_SECRET (no default) for the secret field and ensure any
environment-dependent sameSite/secure options are set appropriately for
production deployments.

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