import { Router } from 'express'; import { readFileSync } from 'fs'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; import { authenticateToken, optionalAuth, requireRole } from '../middleware/auth.js'; import { getServerStatus, startServer, stopServer, restartServer, getConsoleLog, getProcessUptime, listFactorioSaves, createFactorioWorld, deleteFactorioSave, getFactorioCurrentSave, isHostFailed, listZomboidConfigs, readZomboidConfig, writeZomboidConfig, listPalworldConfigs, readPalworldConfig, writePalworldConfig, readTerrariaConfig, writeTerrariaConfig, readOpenTTDConfig, writeOpenTTDConfig } from '../services/ssh.js'; import { sendRconCommand, getPlayers, getPlayerList } from '../services/rcon.js'; import { getServerMetricsHistory, getCurrentMetrics } from '../services/prometheus.js'; import { initWhitelistCache, getCachedWhitelist, setCachedWhitelist, initFactorioTemplates, getFactorioTemplates, createFactorioTemplate, deleteFactorioTemplate, initFactorioWorldSettings, getFactorioWorldSettings, saveFactorioWorldSettings, deleteFactorioWorldSettings, initAutoShutdownSettings, getAutoShutdownSettings, setAutoShutdownSettings, initActivityLog, logActivity, getActivityLog, initServerDisplaySettings, getServerDisplaySettings, getAllServerDisplaySettings, setServerDisplaySettings, initGuildSettings } from '../db/init.js'; import { getEmptySince } from '../services/autoshutdown.js'; import { getDefaultMapGenSettings, getPresetNames, getPreset } from '../services/factorio.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); function loadConfig() { return JSON.parse(readFileSync(join(__dirname, '..', 'config.json'), 'utf-8')); } // RAM Budget Checkasync function checkRamBudget(serverToStart) { const config = loadConfig(); const ramBudget = config.ramBudget || 30; let usedRam = 0; for (const server of config.servers) { if (server.id === serverToStart.id) continue; try { const status = await getServerStatus(server); if (status === "online") usedRam += server.maxRam || 0; } catch (err) {} } const serverRam = serverToStart.maxRam || 0; const availableRam = ramBudget - usedRam; return { canStart: availableRam >= serverRam, usedRam, serverRam, availableRam, ramBudget };} // Initialize tables initWhitelistCache(); initActivityLog(); initFactorioTemplates(); initFactorioWorldSettings(); initServerDisplaySettings(); initGuildSettings(); const router = Router(); function formatBytes(bytes, forceUnit = null) { if (bytes === 0) return { value: 0, unit: forceUnit || "B" }; const gb = bytes / (1024 * 1024 * 1024); const mb = bytes / (1024 * 1024); if (forceUnit === "GB") return { value: gb, unit: "GB" }; if (forceUnit === "MB") return { value: mb, unit: "MB" }; if (gb >= 1) return { value: gb, unit: "GB" }; return { value: mb, unit: "MB" }; } // ============ FACTORIO ROUTES ============ // Factorio: List saves router.get("/factorio/saves", authenticateToken, requireRole("moderator"), async (req, res) => { try { const config = loadConfig(); const server = config.servers.find(s => s.type === "factorio"); if (!server) return res.status(404).json({ error: "Factorio server not configured" }); const saves = await listFactorioSaves(server); res.json({ saves }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Factorio: Get presets and default settings router.get("/factorio/presets", authenticateToken, requireRole("moderator"), async (req, res) => { try { const presets = getPresetNames(); const defaultSettings = getDefaultMapGenSettings(); res.json({ presets, defaultSettings }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Factorio: Get preset by name router.get("/factorio/presets/:name", authenticateToken, requireRole("moderator"), async (req, res) => { try { const preset = getPreset(req.params.name); res.json({ settings: preset }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Factorio: List templates router.get("/factorio/templates", authenticateToken, requireRole("moderator"), async (req, res) => { try { const templates = getFactorioTemplates(); res.json({ templates: templates.map(t => ({ ...t, settings: JSON.parse(t.settings) })) }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Factorio: Create template router.post("/factorio/templates", authenticateToken, requireRole("moderator"), async (req, res) => { try { const { name, settings } = req.body; if (!name || !settings) { return res.status(400).json({ error: "Name and settings required" }); } const id = createFactorioTemplate(name, settings, req.user.id); res.json({ id, message: "Template created" }); } catch (err) { if (err.message.includes("UNIQUE constraint")) { return res.status(400).json({ error: "Template name already exists" }); } res.status(500).json({ error: err.message }); } }); // Factorio: Delete template router.delete("/factorio/templates/:id", authenticateToken, requireRole("moderator"), async (req, res) => { try { const result = deleteFactorioTemplate(parseInt(req.params.id)); if (result.changes === 0) { return res.status(404).json({ error: "Template not found" }); } res.json({ message: "Template deleted" }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Factorio: Create new world router.post("/factorio/create-world", authenticateToken, requireRole("moderator"), async (req, res) => { try { const { saveName, settings } = req.body; if (!saveName) { return res.status(400).json({ error: "Save name required" }); } const config = loadConfig(); const server = config.servers.find(s => s.type === "factorio"); if (!server) return res.status(404).json({ error: "Factorio server not configured" }); const finalSettings = settings || getDefaultMapGenSettings(); await createFactorioWorld(server, saveName, finalSettings); // Save settings to database for later reference saveFactorioWorldSettings(saveName, finalSettings, req.user.id); logActivity(req.user.id, req.user.username, 'factorio_world_create', 'factorio', saveName, req.user.discordId, req.user.avatar); res.json({ message: "World created", saveName }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Factorio: Delete save router.delete("/factorio/saves/:name", authenticateToken, requireRole("moderator"), async (req, res) => { try { const config = loadConfig(); const server = config.servers.find(s => s.type === "factorio"); if (!server) return res.status(404).json({ error: "Factorio server not configured" }); await deleteFactorioSave(server, req.params.name); // Also delete stored settings if they exist deleteFactorioWorldSettings(req.params.name); logActivity(req.user.id, req.user.username, 'factorio_world_delete', 'factorio', req.params.name, req.user.discordId, req.user.avatar); res.json({ message: "Save deleted" }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Factorio: Get world settings router.get("/factorio/saves/:name/settings", authenticateToken, requireRole("moderator"), async (req, res) => { try { const settings = getFactorioWorldSettings(req.params.name); if (!settings) { return res.json({ legacy: true, message: "This is a legacy world created before settings tracking was implemented" }); } res.json({ legacy: false, settings: JSON.parse(settings.settings), createdBy: settings.created_by, createdAt: settings.created_at }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Factorio: Get current/default save router.get("/factorio/current-save", authenticateToken, async (req, res) => { try { const config = loadConfig(); const server = config.servers.find(s => s.type === "factorio"); if (!server) return res.status(404).json({ error: "Factorio server not configured" }); const result = await getFactorioCurrentSave(server); res.json(result); } catch (err) { res.status(500).json({ error: err.message }); } }); // ============ ZOMBOID CONFIG ROUTES ============ // Zomboid: List config files router.get("/zomboid/config", authenticateToken, requireRole("moderator"), async (req, res) => { try { const config = loadConfig(); const server = config.servers.find(s => s.type === "zomboid"); if (!server) return res.status(404).json({ error: "Zomboid server not configured" }); if (isHostFailed(server.host, server.sshUser)) { return res.status(503).json({ error: "Server host is unreachable", unreachable: true }); } const files = await listZomboidConfigs(server); res.json({ files }); } catch (err) { if (err.message.includes('unreachable') || err.message.includes('ECONNREFUSED') || err.message.includes('ETIMEDOUT')) { return res.status(503).json({ error: 'Server host is unreachable', unreachable: true }); } res.status(500).json({ error: err.message }); } }); // Zomboid: Read config file router.get("/zomboid/config/:filename", authenticateToken, requireRole("moderator"), async (req, res) => { try { const config = loadConfig(); const server = config.servers.find(s => s.type === "zomboid"); if (!server) return res.status(404).json({ error: "Zomboid server not configured" }); const content = await readZomboidConfig(server, req.params.filename); res.json({ filename: req.params.filename, content }); } catch (err) { if (err.message === "File not allowed") { return res.status(403).json({ error: err.message }); } res.status(500).json({ error: err.message }); } }); // Zomboid: Write config file router.put("/zomboid/config/:filename", authenticateToken, requireRole("moderator"), async (req, res) => { try { const config = loadConfig(); const server = config.servers.find(s => s.type === "zomboid"); if (!server) return res.status(404).json({ error: "Zomboid server not configured" }); const { content } = req.body; if (content === undefined) { return res.status(400).json({ error: "Content required" }); } await writeZomboidConfig(server, req.params.filename, content); logActivity(req.user.id, req.user.username, 'zomboid_config', 'zomboid', req.params.filename, req.user.discordId, req.user.avatar); res.json({ message: "Config saved", filename: req.params.filename }); } catch (err) { if (err.message === "File not allowed") { return res.status(403).json({ error: err.message }); } res.status(500).json({ error: err.message }); } }); // Palworld: List config files router.get("/palworld/config", authenticateToken, requireRole("moderator"), async (req, res) => { try { const config = loadConfig(); const server = config.servers.find(s => s.type === "palworld"); if (!server) return res.status(404).json({ error: "Palworld server not configured" }); const files = await listPalworldConfigs(server); res.json({ files }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Palworld: Read config file router.get("/palworld/config/:filename", authenticateToken, requireRole("moderator"), async (req, res) => { try { const config = loadConfig(); const server = config.servers.find(s => s.type === "palworld"); if (!server) return res.status(404).json({ error: "Palworld server not configured" }); const content = await readPalworldConfig(server, req.params.filename); res.json({ filename: req.params.filename, content }); } catch (err) { if (err.message === "File not allowed") { return res.status(403).json({ error: err.message }); } res.status(500).json({ error: err.message }); } }); // Palworld: Write config file router.put("/palworld/config/:filename", authenticateToken, requireRole("moderator"), async (req, res) => { try { const config = loadConfig(); const server = config.servers.find(s => s.type === "palworld"); if (!server) return res.status(404).json({ error: "Palworld server not configured" }); const { content } = req.body; if (content === undefined) { return res.status(400).json({ error: "Content required" }); } await writePalworldConfig(server, req.params.filename, content); logActivity(req.user.id, req.user.username, "palworld_config", "palworld", req.params.filename, req.user.discordId, req.user.avatar); res.json({ message: "Config saved", filename: req.params.filename }); } catch (err) { if (err.message === "File not allowed") { return res.status(403).json({ error: err.message }); } res.status(500).json({ error: err.message }); } }); // ============ GENERAL ROUTES ============ // Get all servers with status router.get('/', optionalAuth, async (req, res) => { try { const config = loadConfig(); const servers = await Promise.all(config.servers.map(async (server) => { // Quick check if host is unreachable - skip expensive operations const hostUnreachable = isHostFailed(server.host, server.sshUser); // If host is unreachable, return immediately with minimal data if (hostUnreachable) { const metrics = await getCurrentMetrics(server.id).catch(() => ({ cpu: 0, cpuCores: 1, memory: 0, memoryUsed: 0, memoryTotal: 0, uptime: 0 })); const memTotal = formatBytes(metrics.memoryTotal); const memUsed = formatBytes(metrics.memoryUsed, memTotal.unit); return { id: server.id, name: server.name, type: server.type, status: "unreachable", running: false, metrics: { cpu: metrics.cpu, cpuCores: metrics.cpuCores, memory: metrics.memory, memoryUsed: memUsed.value, memoryTotal: memTotal.value, memoryUnit: memTotal.unit, uptime: 0 }, players: { online: 0, max: null, list: [] }, hasRcon: !!server.rconPassword }; } const [status, metrics, players, playerList, processUptime] = await Promise.all([ getServerStatus(server), getCurrentMetrics(server.id).catch(() => ({ cpu: 0, cpuCores: 1, memory: 0, memoryUsed: 0, memoryTotal: 0, uptime: 0 })), server.rconPassword ? getPlayers(server).catch(() => ({ online: 0, max: null })) : { online: 0, max: null }, server.rconPassword ? getPlayerList(server).catch(() => ({ players: [] })) : { players: [] }, getProcessUptime(server).catch(() => 0) ]); const memTotal = formatBytes(metrics.memoryTotal); const memUsed = formatBytes(metrics.memoryUsed, memTotal.unit); // Get auto-shutdown info const shutdownSettings = getAutoShutdownSettings(server.id); const emptySince = getEmptySince(server.id); return { id: server.id, name: server.name, type: server.type, status, running: status === 'online', metrics: { cpu: metrics.cpu, cpuCores: metrics.cpuCores, memory: metrics.memory, memoryUsed: memUsed.value, memoryTotal: memTotal.value, memoryUnit: memTotal.unit, uptime: processUptime }, players: { ...players, list: playerList.players }, hasRcon: !!server.rconPassword, autoShutdown: { enabled: shutdownSettings?.enabled === 1 || false, timeoutMinutes: shutdownSettings?.timeout_minutes || 15, emptySinceMinutes: emptySince } }; })); res.json(servers); } catch (err) { res.status(500).json({ error: err.message }); } }); // Activity Log (superadmin only) router.get('/activity-log', authenticateToken, requireRole('superadmin'), (req, res) => { try { const limit = parseInt(req.query.limit) || 100; const logs = getActivityLog(limit); res.json(logs); } catch (err) { res.status(500).json({ error: err.message }); } }); // Get all server display settings (for ServerCard) router.get("/display-settings", optionalAuth, async (req, res) => { try { const settings = getAllServerDisplaySettings(); const result = {}; settings.forEach(s => { result[s.server_id] = { address: s.address, hint: s.hint }; }); res.json(result); } catch (err) { res.status(500).json({ error: err.message }); } }); // ============ TERRARIA ROUTES ============ // Get Terraria config router.get("/terraria/config", authenticateToken, requireRole("moderator"), async (req, res) => { try { const config = loadConfig(); const server = config.servers.find(s => s.id === "terraria"); if (!server) return res.status(404).json({ error: "Server not found" }); if (isHostFailed(server.host, server.sshUser)) { return res.status(503).json({ error: "Server host is unreachable", unreachable: true }); } const content = await readTerrariaConfig(server); res.json({ content }); } catch (error) { console.error("Error reading Terraria config:", error); if (error.message.includes('unreachable') || error.message.includes('ECONNREFUSED') || error.message.includes('ETIMEDOUT')) { return res.status(503).json({ error: 'Server host is unreachable', unreachable: true }); } res.status(500).json({ error: error.message }); } }); // Save Terraria config router.put("/terraria/config", authenticateToken, requireRole("moderator"), async (req, res) => { try { const config = loadConfig(); const server = config.servers.find(s => s.id === "terraria"); if (!server) return res.status(404).json({ error: "Server not found" }); const { content } = req.body; if (!content) return res.status(400).json({ error: "Content required" }); await writeTerrariaConfig(server, content); logActivity(req.user.id, req.user.username, "terraria_config", "terraria", "serverconfig.txt", req.user.discordId, req.user.avatar); res.json({ success: true }); } catch (error) { console.error("Error writing Terraria config:", error); res.status(500).json({ error: error.message }); } }); // ============ OPENTTD ROUTES ============ // Get OpenTTD config router.get("/openttd/config", authenticateToken, requireRole("moderator"), async (req, res) => { try { const config = loadConfig(); const server = config.servers.find(s => s.id === "openttd"); if (!server) return res.status(404).json({ error: "Server not found" }); if (isHostFailed(server.host, server.sshUser)) { return res.status(503).json({ error: "Server host is unreachable", unreachable: true }); } const content = await readOpenTTDConfig(server); res.json({ content }); } catch (error) { console.error("Error reading OpenTTD config:", error); if (error.message.includes('unreachable') || error.message.includes('ECONNREFUSED') || error.message.includes('ETIMEDOUT')) { return res.status(503).json({ error: 'Server host is unreachable', unreachable: true }); } res.status(500).json({ error: error.message }); } }); // Save OpenTTD config router.put("/openttd/config", authenticateToken, requireRole("moderator"), async (req, res) => { try { const config = loadConfig(); const server = config.servers.find(s => s.id === "openttd"); if (!server) return res.status(404).json({ error: "Server not found" }); const { content } = req.body; if (!content) return res.status(400).json({ error: "Content required" }); await writeOpenTTDConfig(server, content); logActivity(req.user.id, req.user.username, "openttd_config", "openttd", "openttd.cfg", req.user.discordId, req.user.avatar); res.json({ success: true }); } catch (error) { console.error("Error writing OpenTTD config:", error); res.status(500).json({ error: error.message }); } }); // Get single server router.get('/:id', optionalAuth, async (req, res) => { const config = loadConfig(); const server = config.servers.find(s => s.id === req.params.id); if (!server) { return res.status(404).json({ error: 'Server not found' }); } try { // Check if host is unreachable const hostUnreachable = isHostFailed(server.host, server.sshUser); if (hostUnreachable) { const metrics = await getCurrentMetrics(server.id).catch(() => ({ cpu: 0, cpuCores: 1, memory: 0, memoryUsed: 0, memoryTotal: 0, uptime: 0 })); const memTotal = formatBytes(metrics.memoryTotal); const memUsed = formatBytes(metrics.memoryUsed, memTotal.unit); return res.json({ id: server.id, name: server.name, type: server.type, status: "unreachable", running: false, metrics: { cpu: metrics.cpu, cpuCores: metrics.cpuCores, memory: metrics.memory, memoryUsed: memUsed.value, memoryTotal: memTotal.value, memoryUnit: memTotal.unit, uptime: 0 }, players: { online: 0, max: null, list: [] }, hasRcon: !!server.rconPassword }); } const [status, metrics, players, playerList, processUptime] = await Promise.all([ getServerStatus(server), getCurrentMetrics(server.id).catch(() => ({ cpu: 0, cpuCores: 1, memory: 0, memoryUsed: 0, memoryTotal: 0, uptime: 0 })), server.rconPassword ? getPlayers(server).catch(() => ({ online: 0, max: null })) : { online: 0, max: null }, server.rconPassword ? getPlayerList(server).catch(() => ({ players: [] })) : { players: [] }, getProcessUptime(server).catch(() => 0) ]); const memTotal = formatBytes(metrics.memoryTotal); const memUsed = formatBytes(metrics.memoryUsed, memTotal.unit); res.json({ id: server.id, name: server.name, type: server.type, status, running: status === 'online', metrics: { cpu: metrics.cpu, cpuCores: metrics.cpuCores, memory: metrics.memory, memoryUsed: memUsed.value, memoryTotal: memTotal.value, memoryUnit: memTotal.unit, uptime: processUptime }, players: { ...players, list: playerList.players }, hasRcon: !!server.rconPassword }); } catch (err) { console.error(`Error fetching server ${req.params.id}:`, err.message); res.status(500).json({ error: err.message }); } }); // Get metrics history from Prometheus router.get('/:id/metrics/history', optionalAuth, async (req, res) => { const config = loadConfig(); const server = config.servers.find(s => s.id === req.params.id); if (!server) return res.status(404).json({ error: 'Server not found' }); const range = req.query.range || '1h'; const validRanges = ['15m', '1h', '6h', '24h']; if (!validRanges.includes(range)) { return res.status(400).json({ error: 'Invalid range. Valid: 15m, 1h, 6h, 24h' }); } try { const history = await getServerMetricsHistory(server.id, range); res.json(history); } catch (err) { res.status(500).json({ error: err.message }); } }); // Get player list router.get('/:id/players', authenticateToken, async (req, res) => { const config = loadConfig(); const server = config.servers.find(s => s.id === req.params.id); if (!server) return res.status(404).json({ error: 'Server not found' }); if (!server.rconPassword) { return res.status(400).json({ error: 'RCON not configured for this server' }); } try { const [count, list] = await Promise.all([ getPlayers(server), getPlayerList(server) ]); res.json({ ...count, list: list.players }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Get console logs (moderator+) router.get('/:id/logs', authenticateToken, requireRole('moderator'), async (req, res) => { const config = loadConfig(); const server = config.servers.find(s => s.id === req.params.id); if (!server) return res.status(404).json({ error: 'Server not found' }); if (isHostFailed(server.host, server.sshUser)) { return res.status(503).json({ error: 'Server host is unreachable', unreachable: true }); } try { const lines = parseInt(req.query.lines) || 100; const logs = await getConsoleLog(server, lines); res.json({ logs }); } catch (err) { if (err.message.includes('unreachable') || err.message.includes('ECONNREFUSED') || err.message.includes('ETIMEDOUT')) { return res.status(503).json({ error: 'Server host is unreachable', unreachable: true }); } res.status(500).json({ error: err.message }); } }); // Power actions (moderator+) router.post('/:id/start', authenticateToken, requireRole('moderator'), async (req, res) => { const config = loadConfig(); const server = config.servers.find(s => s.id === req.params.id); if (!server) return res.status(404).json({ error: 'Server not found' }); if (isHostFailed(server.host, server.sshUser)) { return res.status(503).json({ error: 'Server host is unreachable', unreachable: true }); } try { const { save } = req.body || {}; await startServer(server, { save }); logActivity(req.user.id, req.user.username, 'server_start', server.id, save ? 'Save: ' + save : null, req.user.discordId, req.user.avatar); res.json({ message: 'Server starting' }); } catch (err) { if (err.message.includes('unreachable') || err.message.includes('ECONNREFUSED') || err.message.includes('ETIMEDOUT')) { return res.status(503).json({ error: 'Server host is unreachable', unreachable: true }); } res.status(500).json({ error: err.message }); } }); router.post('/:id/stop', authenticateToken, requireRole('moderator'), async (req, res) => { const config = loadConfig(); const server = config.servers.find(s => s.id === req.params.id); if (!server) return res.status(404).json({ error: 'Server not found' }); if (isHostFailed(server.host, server.sshUser)) { return res.status(503).json({ error: 'Server host is unreachable', unreachable: true }); } try { await stopServer(server); logActivity(req.user.id, req.user.username, 'server_stop', server.id, null, req.user.discordId, req.user.avatar); res.json({ message: 'Server stopping' }); } catch (err) { if (err.message.includes('unreachable') || err.message.includes('ECONNREFUSED') || err.message.includes('ETIMEDOUT')) { return res.status(503).json({ error: 'Server host is unreachable', unreachable: true }); } res.status(500).json({ error: err.message }); } }); router.post('/:id/restart', authenticateToken, requireRole('moderator'), async (req, res) => { const config = loadConfig(); const server = config.servers.find(s => s.id === req.params.id); if (!server) return res.status(404).json({ error: 'Server not found' }); if (isHostFailed(server.host, server.sshUser)) { return res.status(503).json({ error: 'Server host is unreachable', unreachable: true }); } try { await restartServer(server); logActivity(req.user.id, req.user.username, 'server_restart', server.id, null, req.user.discordId, req.user.avatar); res.json({ message: 'Server restarting' }); } catch (err) { if (err.message.includes('unreachable') || err.message.includes('ECONNREFUSED') || err.message.includes('ETIMEDOUT')) { return res.status(503).json({ error: 'Server host is unreachable', unreachable: true }); } res.status(500).json({ error: err.message }); } }); // Get whitelist (with server-side caching) router.get('/:id/whitelist', optionalAuth, async (req, res) => { const config = loadConfig(); const server = config.servers.find(s => s.id === req.params.id); if (!server) return res.status(404).json({ error: 'Server not found' }); if (!server.rconPassword) { return res.json({ players: [], cached: false }); } try { const response = await sendRconCommand(server, 'whitelist list'); const match = response.trim().match(/:\s*(.+)$/); let players = []; if (match && match[1]) { players = match[1].split(',').map(p => p.trim()).filter(p => p.length > 0); } setCachedWhitelist(server.id, players); res.json({ players, cached: false }); } catch (err) { const players = getCachedWhitelist(server.id); res.json({ players, cached: true }); } }); // RCON command (moderator+) router.post('/:id/rcon', authenticateToken, requireRole('moderator'), async (req, res) => { const config = loadConfig(); const server = config.servers.find(s => s.id === req.params.id); if (!server) return res.status(404).json({ error: 'Server not found' }); if (!server.rconPassword) { return res.status(400).json({ error: 'RCON not configured for this server' }); } const { command } = req.body; if (!command) { return res.status(400).json({ error: 'Command required' }); } try { const response = await sendRconCommand(server, command); logActivity(req.user.id, req.user.username, 'rcon_command', server.id, command, req.user.discordId, req.user.avatar); if (command.startsWith("whitelist ")) { try { const listResponse = await sendRconCommand(server, "whitelist list"); const match = listResponse.trim().match(/:\s*(.+)$/); let players = []; if (match && match[1]) { players = match[1].split(",").map(p => p.trim()).filter(p => p.length > 0); } setCachedWhitelist(server.id, players); } catch (e) { /* ignore */ } } res.json({ response }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Initialize auto-shutdown settings table initAutoShutdownSettings(); // ============ AUTO-SHUTDOWN ROUTES ============ // Get auto-shutdown settings for a server router.get('/:id/autoshutdown', authenticateToken, requireRole('moderator'), async (req, res) => { const config = loadConfig(); const server = config.servers.find(s => s.id === req.params.id); if (!server) return res.status(404).json({ error: 'Server not found' }); const settings = getAutoShutdownSettings(req.params.id); const emptySince = getEmptySince(req.params.id); res.json({ enabled: settings?.enabled === 1 || false, timeoutMinutes: settings?.timeout_minutes || 15, emptySinceMinutes: emptySince }); }); // Update auto-shutdown settings for a server router.put('/:id/autoshutdown', authenticateToken, requireRole('moderator'), async (req, res) => { const config = loadConfig(); const server = config.servers.find(s => s.id === req.params.id); if (!server) return res.status(404).json({ error: 'Server not found' }); const { enabled, timeoutMinutes } = req.body; const timeout = Math.max(1, Math.min(1440, timeoutMinutes || 15)); setAutoShutdownSettings(req.params.id, enabled, timeout); logActivity(req.user.id, req.user.username, 'autoshutdown_config', req.params.id, 'Enabled: ' + enabled + ', Timeout: ' + timeout + ' min', req.user.discordId, req.user.avatar); console.log('[AutoShutdown] Settings updated for ' + req.params.id + ': enabled=' + enabled + ', timeout=' + timeout + 'min'); res.json({ message: 'Auto-shutdown settings updated', enabled, timeoutMinutes: timeout }); }); // Get display settings for a specific server router.get("/:id/display-settings", authenticateToken, requireRole("superadmin"), async (req, res) => { try { const settings = getServerDisplaySettings(req.params.id); res.json(settings || { server_id: req.params.id, address: "", hint: "" }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Update display settings for a server (superadmin only) router.put("/:id/display-settings", authenticateToken, requireRole("superadmin"), async (req, res) => { const { address, hint } = req.body; try { setServerDisplaySettings(req.params.id, address || "", hint || ""); res.json({ message: "Display settings updated", address, hint }); } catch (err) { res.status(500).json({ error: err.message }); } }); export default router;