249 lines
7.4 KiB
JavaScript
249 lines
7.4 KiB
JavaScript
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());
|
|
const allowedOrigins = [
|
|
"https://galaxystrike.online",
|
|
"https://api.korvarix.com",
|
|
"http://api.korvarix.com:3001",
|
|
"https://dev.gameserver.galaxystrike.online",
|
|
"http://localhost:3000",
|
|
"http://localhost:3001",
|
|
"http://localhost:3002",
|
|
...(process.env.CLIENT_URL ? [process.env.CLIENT_URL] : []),
|
|
];
|
|
app.use(cors({
|
|
origin: (origin, callback) => {
|
|
// Allow no-origin (Electron, mobile, curl) + whitelisted origins
|
|
if (!origin || allowedOrigins.includes(origin)) return callback(null, true);
|
|
return callback(null, false);
|
|
},
|
|
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 (more lenient for development)
|
|
const { RateLimiterMemory } = require('rate-limiter-flexible');
|
|
const limiter = new RateLimiterMemory({
|
|
keyGenerator: (req) => req.ip,
|
|
points: 1000, // limit each IP to 1000 requests per windowMs (increased from 100)
|
|
duration: 60, // 1 minute window (reduced from 15 minutes)
|
|
blockDuration: 60, // Block for 1 minute (reduced from 15 minutes)
|
|
});
|
|
|
|
app.use('/api/', async (req, res, next) => {
|
|
try {
|
|
// Skip rate limiting for localhost in development
|
|
const isLocalhost = req.ip === '127.0.0.1' || req.ip === '::1' || req.hostname === 'localhost';
|
|
|
|
if (!isLocalhost) {
|
|
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 (rejRes) {
|
|
// Handle rate limit exceeded
|
|
const secs = Math.round(rejRes.msBeforeNext / 1000) || 1;
|
|
res.set('Retry-After', String(secs));
|
|
res.status(429).json({ error: 'Too many requests, please try again later.' });
|
|
}
|
|
});
|
|
|
|
// 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 };
|