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>
This commit is contained in:
316
routes/config.js
Normal file
316
routes/config.js
Normal file
@@ -0,0 +1,316 @@
|
||||
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;
|
Reference in New Issue
Block a user