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
18 changes: 18 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# ======================== REQUIRED VARIABLES ========================
# Server Configuration (REQUIRED)
PORT=5000
NODE_ENV=development

# Database (REQUIRED)
MONGO_URI=mongodb://localhost:27017/github_tracker

# Security (REQUIRED)
SESSION_SECRET=your-super-secret-random-key-change-in-production
CLIENT_URL=http://localhost:5173

# ======================== OPTIONAL VARIABLES ========================
# Cookie Configuration (Optional - uses localhost if not set)
COOKIE_DOMAIN=localhost

# Logging (Optional - defaults to 'info' in production, 'debug' in development)
LOG_LEVEL=debug
11 changes: 11 additions & 0 deletions backend/middleware/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const requireAuth = (req, res, next) => {
if (!req.isAuthenticated()) {
return res.status(401).json({
success: false,
message: 'Authentication required'
});
}
next();
};

module.exports = { requireAuth };
22 changes: 22 additions & 0 deletions backend/middleware/envValidator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const logger = require('../logger');

const validateEnv = () => {
const requiredVars = [
'MONGO_URI',
'PORT',
'SESSION_SECRET',
'CLIENT_URL',
'NODE_ENV',
];

const missingVars = requiredVars.filter(v => !process.env[v]);

if (missingVars.length > 0) {
logger.error(`Missing required environment variables: ${missingVars.join(', ')}`);
process.exit(1);
}

logger.info('Environment variables validated ✓');
};

module.exports = { validateEnv };
22 changes: 22 additions & 0 deletions backend/middleware/errorHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const logger = require('../logger');

const errorHandler = (err, req, res, next) => {
const status = err.status || err.statusCode || 500;
const message = process.env.NODE_ENV === 'production'
? 'Internal Server Error'
: err.message;

logger.error(`[${req.method} ${req.path}] Error: ${err.message}`, err);

res.status(status).json({
success: false,
message,
...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
});
};

const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};

module.exports = { errorHandler, asyncHandler };
13 changes: 13 additions & 0 deletions backend/middleware/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const morgan = require('morgan');
const logger = require('../logger');

const morganStream = {
write: (message) => logger.info(message.trim()),
};

const httpLogger = morgan(
':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" - :response-time ms',
{ stream: morganStream }
);

module.exports = httpLogger;
24 changes: 24 additions & 0 deletions backend/middleware/security.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const helmet = require('helmet');

const securityHeaders = helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'nonce-randomvalue'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https://api.github.com', 'https://avatars.githubusercontent.com'],
connectSrc: ["'self'", 'https://api.github.com'],
fontSrc: ["'self'", 'https://fonts.googleapis.com'],
},
},
hsts: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true,
},
frameguard: { action: 'deny' },
noSniff: true,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
});

module.exports = securityHeaders;
4 changes: 4 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +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-rate-limit": "^8.5.2",
"express-session": "^1.18.1",
"helmet": "^8.2.0",
"mongoose": "^8.8.2",
"morgan": "^1.10.1",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"winston": "^3.19.0",
Expand Down
92 changes: 64 additions & 28 deletions backend/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,84 @@ const passport = require("passport");
const User = require("../models/User");
const { signupSchema, loginSchema } = require("../validators/authValidator");
const { validateRequest } = require("../validators/validationRequest");
const { asyncHandler } = require("../middleware/errorHandler");
const { requireAuth } = require("../middleware/auth");
const { success, email } = require("zod");
const router = express.Router();

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

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

try {
const existingUser = await User.findOne({
$or: [{ email }, { username }],
});

if (existingUser)
return res.status(400).json({ message: 'User already exists' });
const existingUser = await User.findOne({
$or: [{ email }, { username }],
});

const newUser = new User({ username, email, password });
await newUser.save();
res.status(201).json({ message: 'User created successfully' });
} catch (err) {
if (err && err.code === 11000) {
return res.status(400).json({ message: 'User already exists' });
}
if (existingUser)
return res.status(400).json({
success: false,
message: 'User already exists'
});

res.status(500).json({ message: 'Error creating user', error: err.message });
}
});
const newUser = new User({ username, email, password });
await newUser.save();
res.status(201).json({
success: true,
message: 'User created successfully' });
}));

// Login route
router.post("/login", validateRequest(loginSchema), passport.authenticate('local'), (req, res) => {
res.status(200).json( { message: 'Login successful', user: req.user } );
router.post("/login", validateRequest(loginSchema), (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) return next(err);
if (!user) return res.status(401).json({
success: false,
message: 'Invalid email or password'
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

req.session.regenerate((regenerateErr) => {
if (regenerateErr) return next(regenerateErr);

req.logIn(user, (loginErr) => {
if (loginErr) return next(loginErr);
res.status(200).json({
success: true,
message: 'Login successful',
user: { id: user.id, username: user.username, email: user.email }
});
});
});
})(req, res, next);
});

// Logout route
router.get("/logout", (req, res) => {
// Logout route with authentication check
router.get("/logout", requireAuth, asyncHandler(async (req, res) => {

req.logout((err) => {
if (err) {
return res.status(500).json({
success: false,
message: 'Logout failed'
});
}
res.status(200).json({
success: true,
message: 'Logged out successfully'
});
});
}));

if (err)
return res.status(500).json({ message: 'Logout failed', error: err.message });
else
res.status(200).json({ message: 'Logged out successfully' });
// Get current user route with authentication check
router.get("/me", requireAuth, asyncHandler(async (req, res) => {
res.status(200).json({
success: true,
user: {
id: req.user.id,
username: req.user.username,
email: req.user.email,
},
});
});
}));

module.exports = router;
92 changes: 86 additions & 6 deletions backend/server.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
const express = require('express');
const mongoose = require('mongoose');
const session = require('express-session');
const MongoStore = require('connect-mongo').default;
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

🧩 Analysis chain

🌐 Web query:

connect-mongo version 6.0.0 import pattern CommonJS require

💡 Result:

For connect-mongo v6.0.0, the CommonJS require pattern is: const session = require('express-session'); const { MongoStore } = require('connect-mongo'); app.use(session({ secret: 'foo', store: MongoStore.create(options) })); This is the documented CJS example for connect-mongo (including v6.x) showing destructuring { MongoStore } from 'connect-mongo' and using MongoStore.create(options) for express-session’s store. [1][2] If you’re specifically asking about the older factory style (require('connect-mongo')(session)), that’s from older versions and does not match the current documented v6 pattern. [3] Note: connect-mongo v6.0.0 also supports an ESM import style (for contrast) like: import session from 'express-session' import MongoStore from 'connect-mongo' app.use(session({ secret: 'foo', store: MongoStore.create(options) })); [1][2]

Citations:


Fix connect-mongo v6 CommonJS MongoStore import

  • backend/server.js line 4: const MongoStore = require('connect-mongo').default; should be const { MongoStore } = require('connect-mongo'); for connect-mongo v6’s documented CommonJS usage (the existing MongoStore.create(...) call matches that pattern).
  • touchAfter: 24 * 3600 controls how often connect-mongo “touches”/writes the session in MongoDB; confirm the intended interval and units match the option’s behavior.
🤖 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 4, Replace the incorrect default import for
connect-mongo by using the named MongoStore import (change the require from
using .default to destructuring { MongoStore }) so the existing
MongoStore.create(...) call works with connect-mongo v6; then verify the session
store options passed to MongoStore.create (specifically the touchAfter option on
the store configuration) use the correct units/interval you intend (touchAfter
expects seconds) and adjust the numeric value if you meant a different interval.

const passport = require('passport');
const bodyParser = require('body-parser');
const rateLimit = require('express-rate-limit');
require('dotenv').config();
const cors = require('cors');

// Passport configuration
require('./config/passportConfig');

const logger = require('./logger');
const securityHeaders = require('./middleware/security');
const { validateEnv } = require('./middleware/envValidator');
const httpLogger = require('./middleware/logger');
const { errorHandler } = require('./middleware/errorHandler');
const { requireAuth } = require('./middleware/auth');

// Validate environment variables
validateEnv();

const app = express();

app.use(securityHeaders);
app.use(httpLogger);

// CORS configuration
const allowedOrigins = ['http://localhost:5173', 'https://github-spy.etlify.app'];
app.use(cors({
Expand All @@ -27,27 +40,94 @@ app.use(cors({
}));

// Middleware
app.use(bodyParser.json());
app.use(bodyParser.json({ limit: '10mb' }));
app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }));
Comment on lines +43 to +44
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

10MB body size limit may enable DoS attacks.

The request body limit of 10mb is 100x larger than body-parser's default (100kb). Large request bodies can:

  • Consume excessive server memory
  • Slow down request processing
  • Enable denial-of-service attacks by exhausting resources

Unless there's a specific requirement for large payloads (e.g., data imports), consider:

  1. Reducing the limit to 1mb or less for typical API operations
  2. Using a separate endpoint with multipart/form-data for file uploads
  3. Implementing streaming for large data transfers
  4. Documenting why 10MB is necessary if it's required
🔒 Recommended limit for typical APIs
 // Middleware
-app.use(bodyParser.json({ limit: '10mb' }));
-app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }));
+app.use(bodyParser.json({ limit: '1mb' }));  // Adjust based on actual requirements
+app.use(bodyParser.urlencoded({ limit: '1mb', extended: true }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.use(bodyParser.json({ limit: '10mb' }));
app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }));
app.use(bodyParser.json({ limit: '1mb' })); // Adjust based on actual requirements
app.use(bodyParser.urlencoded({ limit: '1mb', extended: true }));
🤖 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` around lines 43 - 44, The current app.use calls set
bodyParser.json and bodyParser.urlencoded limits to '10mb', which is risky;
change these to a safer default (e.g., '1mb') or make the limit configurable
(ENV variable) and only allow larger sizes for dedicated upload endpoints;
update the app.use(bodyParser.json(...)) and app.use(bodyParser.urlencoded(...))
usages to use the reduced/configurable limit, and if large payloads are required
implement/route them to a multipart/form-data or streaming handler instead of
increasing the global body-parser limit.


app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: process.env.MONGO_URI,
touchAfter: 24 * 3600,
}),
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 1000 * 60 * 30,
domain: process.env.COOKIE_DOMAIN || undefined,
}
}));

app.use(passport.initialize());
app.use(passport.session());

// Rate Limiting
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
standardHeaders: true,
legacyHeaders: false,
message: { message: 'Too many attempts, please try again after 15 minutes.' },
skipSuccessfulRequests: true,
// keyGenerator: (req) => req.ip, // Rate limit by IP address
});

// General API: 100 requests per 15 minutes
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
skip: (req) => req.isAuthenticated(), // Skip rate limiting for authenticated users
})

app.use('/api/auth', authLimiter);
app.use('/api', generalLimiter);
Comment on lines +86 to +87
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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Test rate limiting behavior on auth endpoints

echo "Testing auth rate limit with 12 failed login attempts..."

for i in {1..12}; do
  echo "Request $i:"
  curl -X POST http://localhost:5000/api/auth/login \
    -H "Content-Type: application/json" \
    -d '{"email":"test@example.com","password":"wrong"}' \
    -w "\nHTTP Status: %{http_code}\n" \
    -s | jq -c '.message' 2>/dev/null || echo "(parse error)"
  
  if [ $i -eq 10 ]; then
    echo -e "\n--- Reached authLimiter max (10), next requests should be blocked ---\n"
  fi
  
  sleep 1
done

echo -e "\n=== Check which limiter blocked the requests ==="

Repository: GitMetricsLab/github_tracker

Length of output: 554


Avoid stacking rate limiters on /api/auth
Requests under /api/auth/* will match both middleware mounts:

app.use('/api/auth', authLimiter);
app.use('/api', generalLimiter);

So unauthenticated auth calls are limited by both authLimiter (10/15m, skipSuccessfulRequests: true) and generalLimiter (100/15m, skipped only when req.isAuthenticated()), which can over-restrict failed logins unless this cumulative behavior is intentional (either document it or exclude /api/auth from generalLimiter).

The proposed curl/jq verification didn’t show limiter outcomes because jq failed to parse the response ((parse error)); adjust the test to capture HTTP status and any rate-limit headers/body so you can see which limiter triggered.

🤖 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` around lines 86 - 87, Your current mounts stack rate
limiters so requests to /api/auth hit both authLimiter and generalLimiter;
change generalLimiter so it excludes auth routes (e.g., wrap or configure it to
skip when req.path.startsWith('/auth') or when req.originalUrl startsWith
'/api/auth'), or mount generalLimiter on a narrower path (e.g., '/api' but with
a skip middleware that bypasses it for '/api/auth'), ensuring authLimiter alone
governs auth endpoints; also update verification scripts to capture HTTP status
and rate-limit headers/body (e.g., print response headers and status) to
determine which limiter triggered.


// Health Check
app.get('/api/health', (req,res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});

// Routes
const authRoutes = require('./routes/auth');

app.use('/api/auth', authRoutes);


// 404 Handler
app.use((req, res) => {
res.status(404).json({ success: false, message: 'Route not found' });
});

// Global Error Handler
app.use(errorHandler);

module.exports = app;

// Connect to MongoDB
mongoose.connect(process.env.MONGO_URI, {}).then(() => {
mongoose.connect(process.env.MONGO_URI, {
maxPoolSize: 10,
minPoolSize: 5,
waitQueueTimeoutMS: 10000,
}).then(() => {
logger.info('Connected to MongoDB');

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
app.listen(process.env.PORT, () => {
logger.info(`Server running on port ${process.env.PORT}`);
logger.info(`✓ Environment: ${process.env.NODE_ENV}`);
});
}).catch((err) => {
logger.error('MongoDB connection error', err);
process.exit(1);
});

// Graceful shutdown
process.on('SIGTERM', () => {
logger.info('SIGTERM received, shutting down gracefully');
mongoose.connection.close(false, () => {
logger.info('MongoDB connection closed');
process.exit(0);
})
})
Comment on lines +127 to +133
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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Graceful shutdown should also close the HTTP server.

The SIGTERM handler closes the MongoDB connection but doesn't close the Express server. This means new HTTP requests may still be accepted during shutdown, which could lead to:

  • Incomplete request processing
  • Database connection errors for in-flight requests
  • Improper resource cleanup
🔄 Recommended graceful shutdown sequence
+let server;
+
 // Connect to MongoDB
 mongoose.connect(process.env.MONGO_URI, {
     maxPoolSize: 10,
     minPoolSize: 5,
     waitQueueTimeoutMS: 10000,
 }).then(() => {
     logger.info('Connected to MongoDB');
-    app.listen(process.env.PORT, () => {
+    server = app.listen(process.env.PORT, () => {
         logger.info(`Server running on port ${process.env.PORT}`);
         logger.info(`✓ Environment: ${process.env.NODE_ENV}`);
     });
 }).catch((err) => {
     logger.error('MongoDB connection error', err);
     process.exit(1);
 });

 // Graceful shutdown
 process.on('SIGTERM', () => {
     logger.info('SIGTERM received, shutting down gracefully');
-    mongoose.connection.close(false, () => {
-        logger.info('MongoDB connection closed');
-        process.exit(0);
-    })
+    server.close(() => {
+        logger.info('HTTP server closed');
+        mongoose.connection.close(false, () => {
+            logger.info('MongoDB connection closed');
+            process.exit(0);
+        });
+    });
 })
🤖 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` around lines 127 - 133, The SIGTERM handler currently only
calls mongoose.connection.close and must also stop accepting new HTTP requests
by closing the Express HTTP server; update the process.on('SIGTERM') block to
call server.close (the variable that holds the result of app.listen) and wait
for its callback or error before calling mongoose.connection.close, logging via
logger at each step and ensuring process.exit(0/1) is invoked after DB close or
on server close error; reference the process.on('SIGTERM') handler, the
server.close callback, mongoose.connection.close, and logger for where to add
the sequencing.

Loading