This commit is contained in:
2026-01-29 01:49:52 -05:00
parent 31c37d9bdd
commit 3e3f37a570
13 changed files with 365 additions and 57 deletions

View File

@@ -14,6 +14,7 @@
"bcryptjs": "^2.4.3",
"dotenv": "^16.4.5",
"cors": "^2.8.5",
"node-fetch": "^3.3.2"
"node-fetch": "^3.3.2",
"express-rate-limit": "^7.1.5"
}
}

60
backend/src/config.js Normal file
View File

@@ -0,0 +1,60 @@
/**
* Environment configuration and validation
*/
const requiredEnvVars = [
'JWT_SECRET',
'DB_HOST',
'DB_USER',
'DB_PASSWORD',
'DB_NAME',
'TMDB_API_KEY'
];
export function validateConfig() {
const missing = [];
for (const varName of requiredEnvVars) {
if (!process.env[varName]) {
missing.push(varName);
}
}
if (missing.length > 0) {
console.error('❌ Missing required environment variables:');
missing.forEach(varName => {
console.error(` - ${varName}`);
});
console.error('\nPlease set these in your .env file or environment.');
process.exit(1);
}
// Validate JWT_SECRET is strong enough
if (process.env.JWT_SECRET.length < 32) {
console.error('❌ JWT_SECRET must be at least 32 characters long for security.');
process.exit(1);
}
console.log('✅ Environment configuration validated');
}
export const config = {
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: '7d'
},
db: {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
port: parseInt(process.env.DB_PORT || '3306', 10)
},
tmdb: {
apiKey: process.env.TMDB_API_KEY,
baseUrl: 'https://api.themoviedb.org/3'
},
server: {
port: parseInt(process.env.PORT || '3000', 10)
}
};

View File

@@ -4,7 +4,10 @@ import dotenv from 'dotenv';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import rateLimit from 'express-rate-limit';
import { initDB } from './db/index.js';
import { validateConfig, config } from './config.js';
import { errorHandler } from './middleware/errorHandler.js';
import authRoutes from './routes/auth.js';
import challengeRoutes from './routes/challenges.js';
import predictionRoutes from './routes/predictions.js';
@@ -14,22 +17,50 @@ import leaderboardRoutes from './routes/leaderboard.js';
dotenv.config();
// Validate environment configuration
validateConfig();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
// Rate limiting
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 requests per window
message: { error: 'Too many authentication attempts, please try again later.' },
standardHeaders: true,
legacyHeaders: false,
});
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: { error: 'Too many requests, please try again later.' },
standardHeaders: true,
legacyHeaders: false,
});
const tmdbLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 20, // 20 requests per minute
message: { error: 'Too many search requests, please slow down.' },
standardHeaders: true,
legacyHeaders: false,
});
// Middleware
app.use(cors());
app.use(express.json());
// API Routes
app.use('/api/auth', authRoutes);
app.use('/api/challenges', challengeRoutes);
app.use('/api/predictions', predictionRoutes);
app.use('/api/friends', friendRoutes);
app.use('/api/tmdb', tmdbRoutes);
app.use('/api/leaderboard', leaderboardRoutes);
app.use('/api/auth', authLimiter, authRoutes);
app.use('/api/challenges', apiLimiter, challengeRoutes);
app.use('/api/predictions', apiLimiter, predictionRoutes);
app.use('/api/friends', apiLimiter, friendRoutes);
app.use('/api/tmdb', tmdbLimiter, tmdbRoutes);
app.use('/api/leaderboard', apiLimiter, leaderboardRoutes);
// Health check
app.get('/api/health', (req, res) => {
@@ -57,7 +88,10 @@ if (frontendExists) {
});
}
const PORT = process.env.PORT || 4000;
// Error handling middleware (must be last)
app.use(errorHandler);
const PORT = config.server.port;
// Initialize database and start server
initDB()

View File

@@ -0,0 +1,58 @@
/**
* Centralized error handling middleware
*/
export class AppError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
export function errorHandler(err, req, res, next) {
let { statusCode = 500, message } = err;
// Log error for debugging
if (process.env.NODE_ENV !== 'production') {
console.error('Error:', {
message: err.message,
stack: err.stack,
statusCode,
path: req.path,
method: req.method
});
} else {
console.error('Error:', err.message);
}
// Handle specific error types
if (err.code === 'ER_DUP_ENTRY') {
statusCode = 409;
message = 'A record with this information already exists';
}
if (err.name === 'JsonWebTokenError') {
statusCode = 401;
message = 'Invalid token';
}
if (err.name === 'TokenExpiredError') {
statusCode = 401;
message = 'Token expired';
}
// Send error response
res.status(statusCode).json({
error: message,
...(process.env.NODE_ENV !== 'production' && { stack: err.stack })
});
}
// Async handler wrapper to catch errors in async routes
export function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}