const express = require('express'); const http = require('http'); const cors = require('cors'); const helmet = require('helmet'); const compression = require('compression'); const rateLimit = require('rate-limiter-flexible'); require('dotenv').config(); const logger = require('./utils/logger'); const connectDB = require('./config/database'); const authRoutes = require('./routes/auth'); const serverRoutes = require('./routes/servers'); const { errorHandler, notFound } = require('./middleware/errorHandler'); const GameServer = require('./models/GameServer'); // Override console.error to properly log error objects const originalConsoleError = console.error; console.error = (...args) => { args.forEach(arg => { if (arg instanceof Error) { logger.error('Console Error:', { message: arg.message, stack: arg.stack, name: arg.name }); } else if (typeof arg === 'object' && arg !== null) { logger.error('Console Error Object:', arg); } else { logger.error('Console Error:', arg); } }); }; const app = express(); const server = http.createServer(app); // Middleware app.use(helmet()); app.use(compression()); app.use(cors({ origin: ["https://galaxystrike.online", "https://api.korvarix.com", "http://api.korvarix.com:3001", "https://dev.gameserver.galaxystrike.online"], credentials: true })); app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true })); // Static file serving app.use(express.static('../Website/dist')); // Rate limiting const { RateLimiterMemory } = require('rate-limiter-flexible'); const limiter = new RateLimiterMemory({ keyGenerator: (req) => req.ip, points: 100, // limit each IP to 100 requests per windowMs duration: 900, // 15 minutes blockDuration: 900, // Block for 15 minutes }); app.use('/api/', async (req, res, next) => { try { const resLimiter = await limiter.consume(req.ip); if (!resLimiter.remainingPoints) { return res.status(429).json({ error: 'Too many requests, please try again later.' }); } next(); } catch (error) { next(); } }); // Routes - API Server Only (Auth + Server Browser) app.use('/api/auth', authRoutes); app.use('/api/servers', serverRoutes); // Manual cleanup endpoint (for testing) app.post('/api/admin/cleanup-dead-servers', async (req, res) => { try { await cleanupDeadServers(); res.json({ success: true, message: 'Dead server cleanup completed' }); } catch (error) { logger.error('Manual cleanup error:', error); res.status(500).json({ success: false, error: 'Cleanup failed' }); } }); // Health check app.get('/health', (req, res) => { res.status(200).json({ status: 'API Server OK', service: 'galaxystrikeonline-api', timestamp: new Date().toISOString(), uptime: process.uptime() }); }); // API version endpoint app.get('/api/ssc/version', (req, res) => { res.status(200).json({ version: '1.0.0', service: 'galaxystrikeonline-api', timestamp: new Date().toISOString() }); }); // Fallback route for SPA - only serve index.html for non-API routes app.get('*', (req, res) => { // Don't try to serve index.html for API routes if (req.path.startsWith('/api/')) { return res.status(404).json({ error: 'API endpoint not found' }); } // Try dist first (for built files), fallback to public (for development) const distPath = require('path').resolve(__dirname, '../dist/index.html'); const publicPath = require('path').resolve(__dirname, '../public/index.html'); const fs = require('fs'); if (fs.existsSync(distPath)) { res.sendFile(distPath); } else if (fs.existsSync(publicPath)) { res.sendFile(publicPath); } else { res.status(404).json({ error: 'Frontend not found' }); } }); // Error handling app.use(notFound); app.use(errorHandler); // Clean up dead servers async function cleanupDeadServers() { try { logger.info('[API SERVER] Starting dead server cleanup...'); // Find servers that haven't been updated in the last 5 minutes const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); const deadServers = await GameServer.find({ lastActivity: { $lt: fiveMinutesAgo } }); if (deadServers.length > 0) { logger.info(`[API SERVER] Found ${deadServers.length} potentially dead servers, checking...`); for (const server of deadServers) { const isAlive = await checkServerHealth(server.gameServerUrl); if (!isAlive) { logger.info(`[API SERVER] Removing dead server: ${server.name} (${server.serverId})`); await GameServer.deleteOne({ _id: server._id }); } else { logger.info(`[API SERVER] Server ${server.name} is still alive, updating lastActivity`); server.lastActivity = new Date(); await server.save(); } } } else { logger.info('[API SERVER] No dead servers found'); } } catch (error) { logger.error('[API SERVER] Error during dead server cleanup:', error); } } // Check if a server is healthy async function checkServerHealth(serverUrl) { try { if (!serverUrl) { return false; } // Add /health endpoint to the URL const healthUrl = serverUrl.endsWith('/') ? `${serverUrl}health` : `${serverUrl}/health`; const response = await fetch(healthUrl, { method: 'GET', timeout: 5000 // 5 second timeout }); return response.ok; } catch (error) { logger.warn(`[API SERVER] Health check failed for ${serverUrl}:`, error.message); return false; } } // Initialize database only (no game systems for API server) async function startServer() { try { // Connect to database await connectDB(); logger.info('Database connected successfully'); // Start API server const PORT = process.env.PORT || 3000; server.listen(PORT, () => { logger.info(`API Server running on port ${PORT}`); logger.info('API Server handles: Authentication, Server Browser, User Data'); // Start dead server cleanup (every 2 minutes) setInterval(cleanupDeadServers, 120000); }); } catch (error) { logger.error('Failed to start API server:', error); process.exit(1); } } // Handle uncaught errors process.on('uncaughtException', (error) => { logger.error('Uncaught Exception:', error); }); process.on('unhandledRejection', (reason, promise) => { logger.error('Unhandled Rejection at:', promise, 'reason:', reason); }); // Handle HTTP server errors server.on('error', (error) => { logger.error('HTTP Server error:', error); }); startServer(); module.exports = { app, server };