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 PackageRegistry = require('../utils/package-registry'); 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); // Force reload paths to use new directory await paths.forceReload(); // Reinitialize package registry to use new directory const packageRegistry = new PackageRegistry(); await packageRegistry.reinitialize(); } 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) // Change data directory only router.post('/change-data-directory', async (req, res) => { try { const { newDataDirectory } = req.body; if (!newDataDirectory || !newDataDirectory.trim()) { return res.status(400).json({ success: false, error: 'Data directory path is required' }); } const newDataDir = newDataDirectory.trim(); const currentDataDir = appConfig.getDataDirectory(); if (newDataDir === currentDataDir) { return res.json({ success: true, message: 'Data directory is already set to this path', dataDirectory: currentDataDir }); } // Validate and set new data directory await appConfig.load(); await appConfig.setDataDirectory(newDataDir); await appConfig.save(); // Update paths to use new directory await paths.initialize(); res.json({ success: true, message: 'Data directory updated successfully. Please restart the application for all changes to take effect.', dataDirectory: newDataDir, previousDirectory: currentDataDir }); } catch (error) { console.error('Error changing data directory:', error); res.status(500).json({ success: false, error: `Failed to change data directory: ${error.message}` }); } }); 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;