sanket3280/code-execution
0
1// Load environment variables based on NODE_ENV2const envFile = process.env.NODE_ENV === 'production'3 ? '.env.production'4 : '.env.development';5require('dotenv').config({ path: envFile });6 7// Validate and load configuration8const { config } = require('./config/env.config');9 10const express = require('express');11const mongoose = require('mongoose');12const cors = require('cors');13const morgan = require('morgan');14const passport = require('passport');15const path = require('path');16const http = require('http');17const socketIo = require('socket.io');18const compression = require('compression');19const helmet = require('helmet');20const fs = require('fs');21const { removeServerHeader, addSecurityHeaders } = require('./middleware/security');22const { sanitizeInput } = require('./middleware/sanitize');23const { apiLimiter } = require('./middleware/rateLimiter');24const logger = require('./utils/logger');25const redisClient = require('./config/redis');26const { healthMonitor, requestMetrics, requestTracker } = require('./utils/monitoring');27 28// Import routes29const authRoutes = require('./routes/auth.routes');30const userRoutes = require('./routes/user.routes');31const profileRoutes = require('./routes/profile.routes');32const problemRoutes = require('./routes/problem.routes');33const challengeRoutes = require('./routes/challenge.routes');34const leaderboardRoutes = require('./routes/leaderboard.routes');35const messageRoutes = require('./routes/message.routes');36const notificationRoutes = require('./routes/notification.routes');37const submissionsRoutes = require('./routes/submissions.routes');38const challengeLeaderboardRoutes = require('./routes/challengeLeaderboard.routes');39const adminRoutes = require('./routes/admin.routes');40const healthRoutes = require('./routes/health.routes');41const friendsRoutes = require('./routes/friends.routes');42const formatRoutes = require('./routes/format.routes');43const feedbackRoutes = require('./routes/feedback.routes');44const spectatorRoutes = require('./routes/spectator.routes');45// const progressRoutes = require('./routes/progress.routes');46const Notification = require('./models/Notification');47const Challenge = require('./models/Challenge');48const WorldChatMessage = require('./models/WorldChatMessage');49 50// Initialize Express app51const app = express();52const server = http.createServer(app);53 54// CORS whitelist using config55const CORS_WHITELIST = [56 config.client.url,57 config.client.authCallbackUrl,58 'http://localhost:3000',59 'http://127.0.0.1:3000',60 'https://demands-gras-cold-terminology.trycloudflare.com', // Cloudflare Tunnel61 'https://codebattlefrontends.netlify.app', // New Netlify frontend62 'https://sanket3280-code-execution.hf.space', // HuggingFace Space63].filter(Boolean);64 65 66const dynamicCorsOrigin = (origin, callback) => {67 // Allow non-browser requests (like curl, health checks)68 if (!origin) return callback(null, true);69 70 // Always allow localhost during development71 if (origin && (origin.includes('localhost') || origin.includes('127.0.0.1'))) {72 return callback(null, true);73 }74 75 if (CORS_WHITELIST.includes(origin)) return callback(null, true);76 77 // Allow Netlify deploy previews (deploy-preview-*--codebattless.netlify.app)78 if (origin && origin.match(/^https:\/\/deploy-preview-\d+--codebattless\.netlify\.app$/)) {79 return callback(null, true);80 }81 82 // Allow Cloudflare Tunnel domains (*.trycloudflare.com)83 if (origin && origin.match(/^https:\/\/.*\.trycloudflare\.com$/)) {84 return callback(null, true);85 }86 87 // Allow HuggingFace Space domains (*.hf.space)88 if (origin && origin.match(/^https:\/\/.*\.hf\.space$/)) {89 return callback(null, true);90 }91 92 // Block unauthorized origins93 console.warn(`CORS: Origin not allowed - ${origin}`);94 return callback(new Error('Not allowed by CORS'));95};96 97const io = socketIo(server, {98 cors: {99 origin: dynamicCorsOrigin,100 methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],101 credentials: true102 },103 transports: ['websocket', 'polling'],104 path: '/socket.io',105 // Enable sticky sessions for load balancing106 cookie: {107 name: 'io',108 httpOnly: true,109 sameSite: config.isProduction ? 'none' : 'lax',110 secure: config.isProduction111 },112 // Connection state recovery for reconnections113 connectionStateRecovery: {114 maxDisconnectionDuration: 2 * 60 * 1000, // 2 minutes115 skipMiddlewares: true,116 }117});118 119const PORT = process.env.PORT || config.port || 5000;120 121// Middleware122 123// Respect X-Forwarded-* headers from Render/Proxies for correct IPs (rate limit)124app.set('trust proxy', 1);125 126// Security middleware127app.use(helmet({128 contentSecurityPolicy: {129 directives: {130 defaultSrc: ["'self'"],131 styleSrc: ["'self'", "'unsafe-inline'"],132 scriptSrc: ["'self'"],133 imgSrc: ["'self'", 'data:', 'https:'],134 connectSrc: ["'self'", config.client.url],135 fontSrc: ["'self'", 'data:'],136 objectSrc: ["'none'"],137 mediaSrc: ["'self'"],138 frameSrc: ["'none'"],139 },140 },141 crossOriginEmbedderPolicy: false,142}));143 144// Enable gzip compression for all responses145app.use(compression({146 level: 6, // Compression level (0-9, 6 is default)147 threshold: 1024, // Only compress responses > 1KB148 filter: (req, res) => {149 if (req.headers['x-no-compression']) {150 return false;151 }152 return compression.filter(req, res);153 }154}));155 156// Remove server identification headers157app.use(removeServerHeader);158 159// Add custom security headers160app.use(addSecurityHeaders);161 162// Request counter for load balancing tracking163let requestCount = 0;164let successCount = 0;165let errorCount = 0;166 167// Add backend server identification header for load balancing tracking168app.use((req, res, next) => {169 const serverUrl = process.env.SERVER_PUBLIC_URL || 'unknown';170 const serverName = serverUrl.split('//')[1]?.split('.')[0] || 'unknown';171 res.setHeader('x-backend-server', serverName);172 173 // Add X-SERVER-ID for load balancer testing174 const serverId = process.env.SERVER_NAME || serverName.toUpperCase();175 res.setHeader('X-SERVER-ID', serverId);176 177 // Count requests178 requestCount++;179 const currentReqNum = requestCount;180 181 // Silent request tracking in development182 if (config.isProduction) {183 console.log(`[${serverId}] REQ #${currentReqNum}: ${req.method} ${req.path}`);184 185 res.on('finish', () => {186 if (res.statusCode >= 200 && res.statusCode < 400) {187 successCount++;188 console.log(`[${serverId}] ✅ #${currentReqNum}: ${res.statusCode} | Total: ${successCount}/${requestCount}`);189 } else {190 errorCount++;191 console.log(`[${serverId}] ❌ #${currentReqNum}: ${res.statusCode} | Errors: ${errorCount}/${requestCount}`);192 }193 });194 }195 196 next();197});198 199// Input sanitization200app.use(sanitizeInput);201 202// Request tracking for monitoring203app.use(requestTracker(requestMetrics));204 205// Apply rate limiting to API routes206app.use('/api/', apiLimiter);207 208// Compression middleware - gzip responses209app.use(compression({210 filter: (req, res) => {211 if (req.headers['x-no-compression']) {212 return false;213 }214 return compression.filter(req, res);215 },216 level: 6 // Balance between speed and compression ratio217}));218 219// CORS configuration - Single origin only220const corsOptions = {221 origin: dynamicCorsOrigin,222 credentials: true,223 methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],224 allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Accept', 'Origin', 'Cache-Control', 'X-File-Name'],225 exposedHeaders: ['Content-Range', 'X-Content-Range'],226 optionsSuccessStatus: 200,227 preflightContinue: false228};229 230app.use(cors(corsOptions));231 232// Cookie parser (for JWT from cookies)233const cookieParser = require('cookie-parser');234app.use(cookieParser());235 236// Body parsers237app.use(express.json({ limit: '1mb' }));238app.use(express.urlencoded({ extended: true, limit: '1mb' }));239 240// Logging - only use detailed logging in development241if (config.isDevelopment) {242 app.use(morgan('dev'));243} else {244 app.use(morgan('combined', {245 skip: (_req, res) => res.statusCode < 400 // Only log errors in production246 }));247}248 249// Passport middleware250app.use(passport.initialize());251 252// Passport config253require('./config/passport')(passport);254 255// Make io available to routes256app.set('io', io);257 258// Routes259app.use('/api/auth', authRoutes);260app.use('/api/users', userRoutes);261app.use('/api/user', userRoutes); // Alias for singular /api/user262app.use('/api/profile', profileRoutes); // Username-based profile routes263app.use('/api/problems', problemRoutes);264app.use('/api/challenges', challengeRoutes);265app.use('/api/leaderboard', leaderboardRoutes);266app.use('/api/messages', messageRoutes);267app.use('/api/notifications', notificationRoutes);268app.use('/api/submissions', submissionsRoutes);269app.use('/api/challenge-leaderboard', challengeLeaderboardRoutes);270app.use('/api/admin', adminRoutes);271app.use('/api/health', healthRoutes);272app.use('/api/friends', friendsRoutes);273app.use('/api/format', formatRoutes);274app.use('/api/feedback', feedbackRoutes);275app.use('/api/code', require('./routes/code.routes')); // New code storage routes276app.use('/api/spectator', spectatorRoutes); // Spectator mode routes277 278// API Info endpoint279const { getApiInfo } = require('./utils/apiVersion');280const { getConnectionStats } = require('./utils/database');281 282// Load balancer stats endpoint283app.get('/api/lb-stats', (_req, res) => {284 const serverUrl = process.env.SERVER_PUBLIC_URL || 'unknown';285 const serverName = serverUrl.split('//')[1]?.split('.')[0] || 'unknown';286 const serverId = process.env.SERVER_NAME || serverName.toUpperCase();287 288 res.status(200).json({289 server: serverId,290 requests: {291 total: requestCount,292 success: successCount,293 errors: errorCount,294 successRate: requestCount > 0 ? ((successCount / requestCount) * 100).toFixed(2) + '%' : '0%'295 },296 uptime: Math.floor(process.uptime()) + 's',297 timestamp: new Date().toISOString()298 });299});300 301app.get('/', (_req, res) => {302 res.status(200).json({303 success: true,304 message: 'CodeBattle API is running',305 version: getApiInfo().currentVersion,306 environment: config.nodeEnv,307 timestamp: new Date().toISOString(),308 });309});310 311// API health and status endpoint312app.get('/api/status', (_req, res) => {313 const dbStats = getConnectionStats();314 315 res.status(200).json({316 success: true,317 status: 'operational',318 version: getApiInfo().currentVersion,319 environment: config.nodeEnv,320 database: {321 status: dbStats.state,322 host: dbStats.host,323 },324 uptime: process.uptime(),325 timestamp: new Date().toISOString(),326 });327});328 329// Initialize database connection330const { connectDB, setupEventListeners } = require('./utils/database');331const { initializeCache } = require('./utils/cache');332 333// Setup database event listeners334setupEventListeners();335 336// Connect to MongoDB with retry logic337connectDB()338 .then(async () => {339 // Initialize cache with Redis client340 initializeCache(redisClient);341 342 // Cleanup old world chat messages on server start - keep only latest 20343 try {344 const messageCount = await WorldChatMessage.countDocuments();345 346 // Only log in development347 if (config.isDevelopment) {348 logger.debug(`World Chat: ${messageCount} messages in database`);349 }350 351 if (messageCount > 20) {352 const messagesToDelete = messageCount - 20;353 const oldestMessages = await WorldChatMessage.find()354 .sort({ createdAt: 1 })355 .limit(messagesToDelete)356 .select('_id'); // Only select _id for deletion357 358 const idsToDelete = oldestMessages.map(m => m._id);359 const deleteResult = await WorldChatMessage.deleteMany({ _id: { $in: idsToDelete } });360 361 if (config.isDevelopment) {362 logger.debug(`World Chat: Cleaned up ${deleteResult.deletedCount} old messages`);363 }364 }365 } catch (error) {366 logger.error('World Chat cleanup error', error);367 }368 })369 .catch(err => {370 logger.error('MongoDB connection error', err);371 372 // Check if this is an authentication error373 if (err.message && (err.message.includes('Authentication failed') || err.message.includes('bad auth'))) {374 logger.error('MongoDB authentication failed. Please check your credentials in the .env file.');375 logger.error('Make sure your MONGODB_URI includes the correct username and password.');376 }377 });378 379// Handle MongoDB connection errors after initial connection380mongoose.connection.on('error', err => {381 logger.error('MongoDB connection error', err);382});383 384// Handle MongoDB disconnection385mongoose.connection.on('disconnected', () => {386 logger.warn('MongoDB disconnected');387});388 389// Handle process termination390process.on('SIGINT', async () => {391 await mongoose.connection.close();392 logger.info('MongoDB connection closed due to app termination');393 process.exit(0);394});395 396// Serve static assets in production with caching397if (config.isProduction) {398 // Set static folder with cache control399 const clientBuildPath = path.join(__dirname, '../client/build');400 const fallbackPath = path.join(__dirname, './client/build');401 402 // Check if client build directory exists (async check converted to sync for startup - acceptable)403 let staticPath = fs.existsSync(clientBuildPath) ? clientBuildPath : fallbackPath;404 405 // Create directory if it doesn't exist (async operations converted to sync for startup - acceptable)406 if (!fs.existsSync(staticPath)) {407 console.log(`Creating static directory: ${staticPath}`);408 fs.mkdirSync(staticPath, { recursive: true });409 // Use async write for non-critical startup file410 fs.promises.writeFile(411 path.join(staticPath, 'index.html'),412 '<html><body><h1>CodeBattle API Server</h1><p>Frontend not deployed with this instance.</p></body></html>'413 ).catch(err => console.error('Error creating index.html:', err));414 }415 416 app.use(express.static(staticPath, {417 maxAge: '1d', // Cache static assets for 1 day418 etag: true,419 lastModified: true420 }));421 422 // For all other routes, serve the React app423 app.get('*', (_req, res) => {424 if (fs.existsSync(path.join(staticPath, 'index.html'))) {425 res.sendFile(path.join(staticPath, 'index.html'));426 } else {427 res.status(200).send('<html><body><h1>CodeBattle API Server</h1><p>Frontend not deployed with this instance.</p></body></html>');428 }429 });430}431 432// Error handling middleware433const { errorHandler, notFound } = require('./utils/errorHandler');434 435// 404 handler - must be after all routes436app.use(notFound);437 438// Global error handler - must be last439app.use(errorHandler);440 441// Make io globally accessible for workers442global.io = io;443 444// Initialize spectator WebSocket handlers445const { initializeSpectatorSocket } = require('./sockets/spectator.socket');446initializeSpectatorSocket(io);447 448// Initialize challenge leaderboard WebSocket handlers449const { initializeChallengeLeaderboardSocket } = require('./sockets/challengeLeaderboard.socket');450initializeChallengeLeaderboardSocket(io);451 452// Socket.IO connection handling453io.on('connection', (socket) => {454 // Only log connections in development455 if (config.isDevelopment) {456 logger.debug('New client connected', { socketId: socket.id });457 }458 459 // Log ALL incoming events for debugging460 socket.onAny((eventName, ...args) => {461 if (eventName.startsWith('participant:')) {462 console.log(`🔵 SERVER RECEIVED EVENT: ${eventName}`, args[0]);463 }464 });465 466 // Join a user's personal room for direct messages467 socket.on('joinUserRoom', (userId) => {468 if (userId) {469 socket.join(`user_${userId}`);470 // Only log in development471 if (config.isDevelopment) {472 logger.debug(`Socket ${socket.id} joined user room: user_${userId}`);473 }474 }475 });476 477 // Join a challenge chat room478 socket.on('joinChallengeRoom', (challengeId) => {479 socket.join(`challenge:${challengeId}`);480 // Only log in development481 if (config.isDevelopment) {482 logger.debug(`Socket ${socket.id} joined challenge room: ${challengeId}`);483 }484 });485 486 // Leave a challenge chat room487 socket.on('leaveChallengeRoom', (challengeId) => {488 socket.leave(`challenge:${challengeId}`);489 // This is a placeholder for the actual implementation490 });491 492 // Handle new challenge message493 socket.on('challengeMessage', async (data) => {494 try {495 const { challengeId, message, userId, username } = data;496 497 // Broadcast the message to all users in the challenge room498 io.to(`challenge:${challengeId}`).emit('newChallengeMessage', {499 _id: message._id,500 sender: userId,501 senderName: username,502 content: message.content,503 createdAt: message.createdAt504 });505 } catch (error) {506 console.error('Error handling challenge message:', error);507 }508 });509 510 // Invite Friend to Challenge511 socket.on('invite-friend-to-challenge', async (data) => {512 const { friendId, challengeId, sentBy } = data;513 try {514 // 1. Validate challenge515 const challenge = await Challenge.findById(challengeId);516 if (!challenge) {517 return socket.emit('invite-friend-error', { message: 'Challenge not found.' });518 }519 if (challenge.status !== 'active') {520 return socket.emit('invite-friend-error', { message: 'Challenge is not active.' });521 }522 // 2. Check if friend is already a participant523 if (challenge.participants.some(p => p.user && p.user.toString() === friendId)) {524 return socket.emit('invite-friend-error', { message: 'Friend already joined.' });525 }526 // 3. Check if invite already sent527 const alreadyInvited = await Notification.findOne({528 type: 'challenge-invite',529 challengeId,530 receiver: friendId,531 status: 'pending'532 });533 if (alreadyInvited) {534 return socket.emit('invite-friend-error', { message: 'Invite already sent.' });535 }536 // 4. Save invite notification537 const notification = new Notification({538 type: 'challenge-invite',539 sender: sentBy,540 receiver: friendId,541 challengeId,542 status: 'pending',543 createdAt: new Date()544 });545 await notification.save();546 // 5. Emit real-time notification to friend (if online)547 io.to(friendId).emit('challenge-invite', {548 challengeId,549 from: sentBy,550 notificationId: notification._id551 });552 // 6. Optionally, emit success to sender553 socket.emit('invite-friend-success', { message: 'Invite sent!' });554 } catch (err) {555 console.error('Error in invite-friend-to-challenge:', err);556 socket.emit('invite-friend-error', { message: 'Server error.' });557 }558 });559 560 // Spectator join event561 socket.on('spectator-join', async ({ challengeId, userId }, callback) => {562 try {563 const challenge = await Challenge.findById(challengeId);564 if (!challenge) {565 return callback && callback({ success: false, message: 'Challenge not found.' });566 }567 if (challenge.visibility !== 'public') {568 return callback && callback({ success: false, message: 'This match is private.' });569 }570 if (challenge.status !== 'active') {571 return callback && callback({ success: false, message: 'This match is not active.' });572 }573 if (challenge.participants.some(p => p.user && p.user.toString() === userId)) {574 return callback && callback({ success: false, message: 'You are already a participant.' });575 }576 socket.join(`challenge:${challengeId}`);577 return callback && callback({ success: true });578 } catch (err) {579 console.error('Error in spectator-join:', err);580 return callback && callback({ success: false, message: 'Server error.' });581 }582 });583 584 // World Chat: join room585 socket.on('joinWorldChat', async () => {586 socket.join('world_chat');587 const messages = await WorldChatMessage.find().sort({ createdAt: 1 }).limit(20);588 socket.emit('world_chat_message', messages);589 });590 591 // World Chat: get all messages592 socket.on('getWorldChatMessages', async () => {593 const messages = await WorldChatMessage.find().sort({ createdAt: 1 }).limit(20);594 socket.emit('world_chat_message', messages);595 });596 597 // World Chat: send message598 socket.on('sendWorldChatMessage', async (msg, cb) => {599 try {600 console.log('Received world chat message:', msg);601 602 // Save new message603 const message = new WorldChatMessage({604 sender: msg.username || 'Anonymous',605 senderId: msg.userId || 'anon',606 content: msg.content,607 challenge: msg.challenge,608 createdAt: new Date()609 });610 await message.save();611 console.log('Saved message with sender:', message.sender);612 613 // Count total messages614 const messageCount = await WorldChatMessage.countDocuments();615 console.log(`Total messages in DB: ${messageCount}`);616 617 // Keep only the latest 20 messages, delete older ones618 if (messageCount > 20) {619 const messagesToDelete = messageCount - 20;620 console.log(`Need to delete ${messagesToDelete} old messages`);621 622 // Get oldest messages to delete623 const oldestMessages = await WorldChatMessage.find()624 .sort({ createdAt: 1 })625 .limit(messagesToDelete);626 627 const idsToDelete = oldestMessages.map(m => m._id);628 console.log('Deleting message IDs:', idsToDelete);629 630 const deleteResult = await WorldChatMessage.deleteMany({ _id: { $in: idsToDelete } });631 console.log(`Successfully deleted ${deleteResult.deletedCount} old messages`);632 }633 634 // Get latest 20 messages and send to all clients635 const messages = await WorldChatMessage.find().sort({ createdAt: 1 }).limit(20);636 console.log(`Sending ${messages.length} messages to clients`);637 io.to('world_chat').emit('world_chat_message', messages);638 639 if (cb) cb();640 } catch (error) {641 console.error('Error in sendWorldChatMessage:', error);642 if (cb) cb();643 }644 });645 646 // World Chat: leave room647 socket.on('leaveWorldChat', () => {648 socket.leave('world_chat');649 });650 651 // Friend System Socket Events652 socket.on('friend-request-sent', ({ toUserId, fromUser }) => {653 io.to(`user_${toUserId}`).emit('friend-request-received', { fromUser });654 });655 656 socket.on('friend-request-accepted', ({ toUserId, fromUser }) => {657 io.to(`user_${toUserId}`).emit('friend-request-accepted', { fromUser });658 io.to(`user_${fromUser._id}`).emit('friend-request-accepted', { fromUser: toUserId });659 });660 661 socket.on('friend-request-rejected', ({ toUserId, fromUser }) => {662 io.to(`user_${toUserId}`).emit('friend-request-rejected', { fromUser });663 });664 665 socket.on('friend-removed', ({ userId, removedUserId }) => {666 io.to(`user_${userId}`).emit('friend-removed', { removedUserId });667 io.to(`user_${removedUserId}`).emit('friend-removed', { removedUserId: userId });668 });669 670 socket.on('friend-online', ({ userId }) => {671 io.emit('friend-online', { userId });672 });673 674 // Handle disconnection675 socket.on('disconnect', () => {676 // Only log disconnections in development677 if (config.isDevelopment) {678 logger.debug('Client disconnected', { socketId: socket.id });679 }680 });681});682 683// Run startup checks and start server684const { runStartupChecks, displayStartupBanner } = require('./utils/startup');685 686// Silent startup in development687if (config.isDevelopment) {688 // Skip banner and checks in development689 server.listen(process.env.PORT || 7860, () => {690 console.log('\n' + '='.repeat(60));691 console.log('🚀 CODEBATTLE SERVER');692 console.log('='.repeat(60));693 console.log(`🐳 Docker: ENABLED`);694 console.log(`📡 Port: ${process.env.PORT || 7860}`);695 console.log(`🌐 Environment: ${config.nodeEnv}`);696 console.log('='.repeat(60) + '\n');697 });698} else {699 // Full startup in production700 displayStartupBanner();701 runStartupChecks()702 .then((results) => {703 server.listen(process.env.PORT || 7860, () => {704 console.log("HF backend running on:", process.env.PORT || 7860);705 logger.success(`🚀 Server running on port ${process.env.PORT || 7860}`, {706 environment: config.nodeEnv,707 port: process.env.PORT || 7860,708 });709 logger.info(`📡 API available at: ${config.server.publicUrl}`);710 logger.info(`🏥 Health check: ${config.server.publicUrl}/api/health`);711 712 if (config.isProduction) {713 healthMonitor.start();714 logger.info('📊 Health monitoring started');715 }716 });717 })718 .catch((error) => {719 logger.error('Startup checks failed', error);720 process.exit(1);721 });722}723 