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>
316 lines
8.7 KiB
JavaScript
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; |