refactor
This commit is contained in:
@@ -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
60
backend/src/config.js
Normal 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)
|
||||
}
|
||||
};
|
||||
@@ -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()
|
||||
|
||||
58
backend/src/middleware/errorHandler.js
Normal file
58
backend/src/middleware/errorHandler.js
Normal 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);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user