Files
LuHost/routes/config.js
Nathan Schneider 3aed09b60f Initial commit: LuHost - Luanti Server Management Web Interface
A modern web interface for Luanti (Minetest) server management with ContentDB integration.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-23 17:32:37 -06:00

316 lines
8.7 KiB
JavaScript

const express = require('express');
const fs = require('fs').promises;
const paths = require('../utils/paths');
const ConfigParser = require('../utils/config-parser');
const appConfig = require('../utils/app-config');
const router = express.Router();
// Configuration schema
const configSchema = {
system: {
data_directory: {
type: 'string',
default: '',
description: 'Luanti data directory path (leave empty for auto-detection)',
section: 'System Settings'
}
},
server: {
port: {
type: 'number',
default: 30000,
description: 'Port for server to listen on'
},
server_name: {
type: 'string',
default: 'Luanti Server',
description: 'Name of the server'
},
server_description: {
type: 'string',
default: 'A Luanti server',
description: 'Server description'
},
server_address: {
type: 'string',
default: '',
description: 'IP address to bind to (empty for all interfaces)'
},
server_announce: {
type: 'boolean',
default: false,
description: 'Announce server to server list'
},
server_dedicated: {
type: 'boolean',
default: false,
description: 'Run as dedicated server'
},
max_users: {
type: 'number',
default: 20,
description: 'Maximum number of users'
}
},
gameplay: {
creative_mode: {
type: 'boolean',
default: false,
description: 'Enable creative mode by default'
},
enable_damage: {
type: 'boolean',
default: true,
description: 'Enable player damage by default'
},
enable_pvp: {
type: 'boolean',
default: true,
description: 'Enable player vs player combat by default'
},
default_game: {
type: 'string',
default: 'minetest_game',
description: 'Default game to use for new worlds'
},
time_speed: {
type: 'number',
default: 72,
description: 'Time speed (72 = normal, higher = faster)'
}
},
security: {
disallow_empty_password: {
type: 'boolean',
default: false,
description: 'Disallow empty passwords'
},
'secure.enable_security': {
type: 'boolean',
default: true,
description: 'Enable security features'
},
strict_protocol_version_checking: {
type: 'boolean',
default: false,
description: 'Strict protocol version checking'
}
},
performance: {
dedicated_server_step: {
type: 'number',
default: 0.1,
description: 'Server step time in seconds'
},
num_emerge_threads: {
type: 'number',
default: 1,
description: 'Number of emerge threads'
},
server_map_save_interval: {
type: 'number',
default: 15.3,
description: 'Map save interval in seconds'
},
max_block_send_distance: {
type: 'number',
default: 12,
description: 'Maximum block send distance'
},
max_simultaneous_block_sends_per_client: {
type: 'number',
default: 40,
description: 'Maximum simultaneous block sends per client'
}
}
};
// Configuration page
router.get('/', async (req, res) => {
try {
paths.ensureDirectories();
// Load both Luanti config and app config
const luantiConfig = await ConfigParser.parseConfig(paths.configFile);
await appConfig.load();
// Combine configs for display
const combinedConfig = {
...luantiConfig,
data_directory: appConfig.getDataDirectory()
};
res.render('config/index', {
title: 'Server Configuration',
config: combinedConfig,
schema: configSchema,
currentPage: 'config',
currentDataDirectory: appConfig.getDataDirectory(),
defaultDataDirectory: appConfig.getDefaultDataDirectory()
});
} catch (error) {
console.error('Error getting config:', error);
res.status(500).render('error', {
error: 'Failed to load configuration',
message: error.message
});
}
});
// Update configuration
router.post('/update', async (req, res) => {
try {
const updates = req.body;
// Handle data directory change separately
if (updates.data_directory !== undefined) {
const newDataDir = updates.data_directory.trim();
if (newDataDir && newDataDir !== appConfig.getDataDirectory()) {
try {
await appConfig.setDataDirectory(newDataDir);
// Update paths to use new directory
await paths.initialize();
} catch (error) {
throw new Error(`Failed to update data directory: ${error.message}`);
}
}
delete updates.data_directory; // Remove from Luanti config updates
}
// Read current Luanti config
const currentConfig = await ConfigParser.parseConfig(paths.configFile);
// Process form data and convert types for Luanti config
const processedUpdates = {};
for (const [key, value] of Object.entries(updates)) {
if (key === '_csrf' || key === 'returnUrl') continue; // Skip CSRF and utility fields
// Find the field in schema to determine type
let fieldType = 'string';
let fieldFound = false;
for (const section of Object.values(configSchema)) {
if (section[key]) {
fieldType = section[key].type;
fieldFound = true;
break;
}
}
// Convert value based on type
if (fieldType === 'boolean') {
processedUpdates[key] = value === 'on' || value === 'true';
} else if (fieldType === 'number') {
const numValue = parseFloat(value);
if (!isNaN(numValue)) {
processedUpdates[key] = numValue;
}
} else {
// String or unknown type
if (value !== '') {
processedUpdates[key] = value;
}
}
}
// Merge with current Luanti config
const updatedConfig = { ...currentConfig, ...processedUpdates };
// Write updated Luanti config
await ConfigParser.writeConfig(paths.configFile, updatedConfig);
const returnUrl = req.body.returnUrl || '/config';
res.redirect(`${returnUrl}?updated=true`);
} catch (error) {
console.error('Error updating config:', error);
const returnUrl = req.body.returnUrl || '/config';
res.redirect(`${returnUrl}?error=${encodeURIComponent(error.message)}`);
}
});
// Reset configuration to defaults
router.post('/reset', async (req, res) => {
try {
const defaultConfig = {};
// Build default configuration from schema
for (const [sectionName, section] of Object.entries(configSchema)) {
for (const [key, field] of Object.entries(section)) {
if (field.default !== undefined) {
defaultConfig[key] = field.default;
}
}
}
await ConfigParser.writeConfig(paths.configFile, defaultConfig);
res.redirect('/config?reset=true');
} catch (error) {
console.error('Error resetting config:', error);
res.redirect(`/config?error=${encodeURIComponent(error.message)}`);
}
});
// Export current configuration as file
router.get('/export', async (req, res) => {
try {
const config = await ConfigParser.parseConfig(paths.configFile);
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', 'attachment; filename=minetest.conf');
const configLines = [];
for (const [key, value] of Object.entries(config)) {
if (value !== undefined && value !== null) {
configLines.push(`${key} = ${value}`);
}
}
res.send(configLines.join('\n'));
} catch (error) {
console.error('Error exporting config:', error);
res.status(500).render('error', {
error: 'Failed to export configuration',
message: error.message
});
}
});
// Get configuration schema (API)
router.get('/api/schema', (req, res) => {
res.json(configSchema);
});
// Get current configuration (API)
router.get('/api/current', async (req, res) => {
try {
const config = await ConfigParser.parseConfig(paths.configFile);
res.json(config);
} catch (error) {
console.error('Error getting config:', error);
res.status(500).json({ error: 'Failed to get configuration' });
}
});
// Update configuration (API)
router.put('/api/update', async (req, res) => {
try {
const updates = req.body;
const currentConfig = await ConfigParser.parseConfig(paths.configFile);
const updatedConfig = { ...currentConfig, ...updates };
await ConfigParser.writeConfig(paths.configFile, updatedConfig);
res.json({ message: 'Configuration updated successfully' });
} catch (error) {
console.error('Error updating config:', error);
res.status(500).json({ error: 'Failed to update configuration' });
}
});
module.exports = router;