zustand auf server wiederhergestellt
This commit is contained in:
244
gsm-backend/routes/auth.js
Normal file
244
gsm-backend/routes/auth.js
Normal file
@@ -0,0 +1,244 @@
|
||||
import { Router } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { db, VALID_ROLES, initDiscordUsers } from '../db/init.js';
|
||||
import { authenticateToken, requireRole } from '../middleware/auth.js';
|
||||
import { getDiscordAuthUrl, exchangeCode, getDiscordUser, getGuildMember, getGuildMemberships, getUserRole, getUserRoleFromMemberships } from '../services/discord.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Initialize Discord users table
|
||||
initDiscordUsers();
|
||||
|
||||
// ===== Guest Login =====
|
||||
|
||||
// Create guest token (view-only, expires in 24h)
|
||||
router.post('/guest', (req, res) => {
|
||||
const guestId = 'guest_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9);
|
||||
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: guestId,
|
||||
username: 'Gast',
|
||||
role: 'guest',
|
||||
isGuest: true
|
||||
},
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
res.json({ token });
|
||||
});
|
||||
|
||||
// ===== Discord OAuth2 =====
|
||||
|
||||
// Start Discord OAuth2 flow
|
||||
router.get('/discord', (req, res) => {
|
||||
res.redirect(getDiscordAuthUrl());
|
||||
});
|
||||
|
||||
// Discord OAuth2 callback
|
||||
router.get('/discord/callback', async (req, res) => {
|
||||
const { code, error } = req.query;
|
||||
|
||||
// Redirect URL for frontend
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'https://gsm.dimension47.de';
|
||||
|
||||
if (error) {
|
||||
return res.redirect(`${frontendUrl}/login?error=discord_denied`);
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
return res.redirect(`${frontendUrl}/login?error=no_code`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Exchange code for access token
|
||||
const tokenData = await exchangeCode(code);
|
||||
|
||||
// Get Discord user info
|
||||
const discordUser = await getDiscordUser(tokenData.access_token);
|
||||
|
||||
// Check if user is in any of the configured guilds
|
||||
const memberships = await getGuildMemberships(discordUser.id);
|
||||
|
||||
if (!memberships) {
|
||||
return res.redirect(`${frontendUrl}/login?error=not_in_guild`);
|
||||
}
|
||||
|
||||
// Determine role based on Discord roles (highest role from all servers)
|
||||
const role = getUserRoleFromMemberships(memberships);
|
||||
|
||||
// Use first membership for display name
|
||||
const member = memberships[0].member;
|
||||
|
||||
// Get display name (nickname or username)
|
||||
const displayName = member.nick || discordUser.global_name || discordUser.username;
|
||||
|
||||
// Upsert user in database
|
||||
const existingUser = db.prepare('SELECT * FROM discord_users WHERE discord_id = ?').get(discordUser.id);
|
||||
|
||||
let userId;
|
||||
if (existingUser) {
|
||||
// Update existing user
|
||||
db.prepare(`
|
||||
UPDATE discord_users
|
||||
SET username = ?, discriminator = ?, avatar = ?, role = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE discord_id = ?
|
||||
`).run(displayName, discordUser.discriminator || '0', discordUser.avatar, role, discordUser.id);
|
||||
userId = existingUser.id;
|
||||
} else {
|
||||
// Create new user
|
||||
const result = db.prepare(`
|
||||
INSERT INTO discord_users (discord_id, username, discriminator, avatar, role)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(discordUser.id, displayName, discordUser.discriminator || '0', discordUser.avatar, role);
|
||||
userId = result.lastInsertRowid;
|
||||
}
|
||||
|
||||
// Create JWT token
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: userId,
|
||||
discordId: discordUser.id,
|
||||
username: displayName,
|
||||
role,
|
||||
avatar: discordUser.avatar
|
||||
},
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
|
||||
// Redirect to frontend with token
|
||||
res.redirect(`${frontendUrl}/auth/callback?token=${token}`);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Discord OAuth error:', err);
|
||||
res.redirect(`${frontendUrl}/login?error=oauth_failed`);
|
||||
}
|
||||
});
|
||||
|
||||
// Get current user info
|
||||
router.get('/me', authenticateToken, (req, res) => {
|
||||
// Check if it's a guest user
|
||||
if (req.user.isGuest) {
|
||||
return res.json({
|
||||
id: req.user.id,
|
||||
username: req.user.username,
|
||||
role: req.user.role,
|
||||
isGuest: true
|
||||
});
|
||||
}
|
||||
|
||||
// Check if it's a Discord user
|
||||
if (req.user.discordId) {
|
||||
const user = db.prepare('SELECT id, discord_id, username, avatar, role FROM discord_users WHERE id = ?').get(req.user.id);
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
return res.json({
|
||||
id: user.id,
|
||||
discordId: user.discord_id,
|
||||
username: user.username,
|
||||
avatar: user.avatar,
|
||||
role: user.role
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback for old users (shouldn't happen after migration)
|
||||
const user = db.prepare('SELECT id, username, role FROM users WHERE id = ?').get(req.user.id);
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
res.json({ id: user.id, username: user.username, role: user.role });
|
||||
});
|
||||
|
||||
// Refresh user role from Discord (useful if roles changed)
|
||||
router.post('/refresh-role', authenticateToken, async (req, res) => {
|
||||
if (!req.user.discordId) {
|
||||
return res.status(400).json({ error: 'Not a Discord user' });
|
||||
}
|
||||
|
||||
try {
|
||||
const memberships = await getGuildMemberships(req.user.discordId);
|
||||
|
||||
if (!memberships) {
|
||||
return res.status(403).json({ error: 'No longer in any guild' });
|
||||
}
|
||||
|
||||
const newRole = getUserRoleFromMemberships(memberships);
|
||||
|
||||
db.prepare('UPDATE discord_users SET role = ?, updated_at = CURRENT_TIMESTAMP WHERE discord_id = ?')
|
||||
.run(newRole, req.user.discordId);
|
||||
|
||||
// Generate new token with updated role
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: req.user.id,
|
||||
discordId: req.user.discordId,
|
||||
username: req.user.username,
|
||||
role: newRole,
|
||||
avatar: req.user.avatar
|
||||
},
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
|
||||
res.json({ token, role: newRole });
|
||||
} catch (err) {
|
||||
console.error('Failed to refresh role:', err);
|
||||
res.status(500).json({ error: 'Failed to refresh role' });
|
||||
}
|
||||
});
|
||||
|
||||
// ===== User Management (superadmin only) =====
|
||||
|
||||
// Get all Discord users
|
||||
router.get('/users', authenticateToken, requireRole('superadmin'), (req, res) => {
|
||||
const users = db.prepare(`
|
||||
SELECT id, discord_id, username, avatar, role, created_at, updated_at
|
||||
FROM discord_users
|
||||
ORDER BY created_at DESC
|
||||
`).all();
|
||||
res.json(users);
|
||||
});
|
||||
|
||||
// Update user role (override Discord role)
|
||||
router.patch('/users/:id/role', authenticateToken, requireRole('superadmin'), (req, res) => {
|
||||
const userId = parseInt(req.params.id);
|
||||
const { role } = req.body;
|
||||
|
||||
if (!VALID_ROLES.includes(role)) {
|
||||
return res.status(400).json({ error: 'Invalid role' });
|
||||
}
|
||||
|
||||
if (userId === req.user.id) {
|
||||
return res.status(400).json({ error: 'Cannot change your own role' });
|
||||
}
|
||||
|
||||
const user = db.prepare('SELECT id FROM discord_users WHERE id = ?').get(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
db.prepare('UPDATE discord_users SET role = ? WHERE id = ?').run(role, userId);
|
||||
res.json({ message: 'Role updated' });
|
||||
});
|
||||
|
||||
// Delete user
|
||||
router.delete('/users/:id', authenticateToken, requireRole('superadmin'), (req, res) => {
|
||||
const userId = parseInt(req.params.id);
|
||||
|
||||
if (userId === req.user.id) {
|
||||
return res.status(400).json({ error: 'Cannot delete yourself' });
|
||||
}
|
||||
|
||||
const user = db.prepare('SELECT id FROM discord_users WHERE id = ?').get(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
db.prepare('DELETE FROM discord_users WHERE id = ?').run(userId);
|
||||
res.json({ message: 'User deleted' });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -3,10 +3,10 @@ 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 } from '../services/ssh.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 } from '../db/init.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';
|
||||
|
||||
@@ -15,6 +15,7 @@ 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();
|
||||
@@ -22,6 +23,7 @@ initActivityLog();
|
||||
initFactorioTemplates();
|
||||
initFactorioWorldSettings();
|
||||
initServerDisplaySettings();
|
||||
initGuildSettings();
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -409,6 +411,73 @@ router.get("/display-settings", optionalAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ============ 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" });
|
||||
const content = await readTerrariaConfig(server);
|
||||
res.json({ content });
|
||||
} catch (error) {
|
||||
console.error("Error reading Terraria config:", error);
|
||||
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" });
|
||||
const content = await readOpenTTDConfig(server);
|
||||
res.json({ content });
|
||||
} catch (error) {
|
||||
console.error("Error reading OpenTTD config:", error);
|
||||
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();
|
||||
|
||||
679
gsm-backend/routes/servers.js.bak
Normal file
679
gsm-backend/routes/servers.js.bak
Normal file
@@ -0,0 +1,679 @@
|
||||
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 } 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" });
|
||||
|
||||
const files = await listZomboidConfigs(server);
|
||||
res.json({ files });
|
||||
} catch (err) {
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
const [status, metrics, players, playerList, processUptime] = await Promise.all([
|
||||
getServerStatus(server),
|
||||
getCurrentMetrics(server.id),
|
||||
server.rconPassword ? getPlayers(server) : { online: 0, max: null },
|
||||
server.rconPassword ? getPlayerList(server) : { 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) {
|
||||
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' });
|
||||
|
||||
try {
|
||||
const lines = parseInt(req.query.lines) || 100;
|
||||
const logs = await getConsoleLog(server, lines);
|
||||
res.json({ logs });
|
||||
} catch (err) {
|
||||
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' });
|
||||
|
||||
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) {
|
||||
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' });
|
||||
|
||||
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) {
|
||||
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' });
|
||||
|
||||
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) {
|
||||
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;
|
||||
Reference in New Issue
Block a user