CoolFace
Apppublic

sanket3280/code-execution

sourceHugging Faceupdated 10mo agoView on Hugging Face
0likes
server.js723 linesDownload Raw Back to server
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