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:
Nathan Schneider
2025-08-23 17:32:37 -06:00
commit 3aed09b60f
47 changed files with 12878 additions and 0 deletions

61
middleware/auth.js Normal file
View File

@@ -0,0 +1,61 @@
// Authentication middleware
const AuthManager = require('../utils/auth');
const authManager = new AuthManager();
// Initialize auth manager
authManager.initialize().catch(console.error);
async function requireAuth(req, res, next) {
if (req.session && req.session.user) {
// User is authenticated
return next();
} else {
// User is not authenticated - check if this is first user setup
try {
const isFirstUser = await authManager.isFirstUser();
if (isFirstUser) {
// No users exist yet - redirect to registration
if (req.headers.accept && req.headers.accept.includes('application/json')) {
return res.status(401).json({ error: 'No users configured. Please complete setup.' });
} else {
return res.redirect('/register');
}
} else {
// Users exist but this person isn't authenticated
if (req.headers.accept && req.headers.accept.includes('application/json')) {
return res.status(401).json({ error: 'Authentication required' });
} else {
return res.redirect('/login?redirect=' + encodeURIComponent(req.originalUrl));
}
}
} catch (error) {
console.error('Error checking first user in auth middleware:', error);
// Fallback to login on error
return res.redirect('/login?redirect=' + encodeURIComponent(req.originalUrl));
}
}
}
function redirectIfAuthenticated(req, res, next) {
if (req.session && req.session.user) {
// User is already authenticated, redirect to dashboard
return res.redirect('/');
} else {
// User is not authenticated, continue to login/register
return next();
}
}
function attachUser(req, res, next) {
// Make user available to templates
res.locals.user = req.session ? req.session.user : null;
res.locals.isAuthenticated = !!(req.session && req.session.user);
next();
}
module.exports = {
requireAuth,
redirectIfAuthenticated,
attachUser
};

185
middleware/security.js Normal file
View File

@@ -0,0 +1,185 @@
// Security middleware for input validation and CSRF protection
/**
* Input validation middleware
* Validates common input patterns and sanitizes data
*/
function validateInput(req, res, next) {
// Sanitize query parameters
for (const key in req.query) {
if (typeof req.query[key] === 'string') {
// Remove control characters
req.query[key] = req.query[key].replace(/[\x00-\x1F\x7F]/g, '');
// Limit length
if (req.query[key].length > 1000) {
req.query[key] = req.query[key].substring(0, 1000);
}
}
}
// Sanitize body data for non-JSON requests
if (req.body && typeof req.body === 'object' && !Array.isArray(req.body)) {
for (const key in req.body) {
if (typeof req.body[key] === 'string') {
// Remove control characters but preserve newlines for textareas
if (key.includes('description') || key.includes('content') || key.includes('motd')) {
req.body[key] = req.body[key].replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
} else {
req.body[key] = req.body[key].replace(/[\x00-\x1F\x7F]/g, '');
}
// Limit length based on field type
const maxLength = getMaxLengthForField(key);
if (req.body[key].length > maxLength) {
req.body[key] = req.body[key].substring(0, maxLength);
}
}
}
}
next();
}
/**
* Get maximum allowed length for different field types
*/
function getMaxLengthForField(fieldName) {
const fieldLimits = {
// User authentication fields
'username': 50,
'password': 200,
'confirmPassword': 200,
'currentPassword': 200,
'newPassword': 200,
// Server/world names
'name': 100,
'worldName': 100,
'serverName': 200,
// Text content
'description': 2000,
'motd': 500,
'content': 5000,
// Commands and paths
'command': 500,
'path': 500,
// Network settings
'bind': 100,
'serverlist_url': 500,
// Default
'default': 200
};
return fieldLimits[fieldName] || fieldLimits['default'];
}
/**
* XSS protection middleware
* Escapes HTML in user input for specific fields
*/
function xssProtection(req, res, next) {
if (req.body && typeof req.body === 'object') {
// Fields that should be HTML escaped
const fieldsToEscape = ['username', 'name', 'worldName', 'serverName', 'motd'];
for (const field of fieldsToEscape) {
if (req.body[field] && typeof req.body[field] === 'string') {
req.body[field] = escapeHtml(req.body[field]);
}
}
}
next();
}
/**
* Basic HTML escape function
*/
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
/**
* Security headers middleware (additional to helmet)
*/
function additionalSecurityHeaders(req, res, next) {
// Prevent MIME type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// XSS protection
res.setHeader('X-XSS-Protection', '1; mode=block');
// Referrer policy
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
}
/**
* Request size validation
*/
function validateRequestSize(req, res, next) {
// Check for unusually large requests that might indicate an attack
const contentLength = req.headers['content-length'];
if (contentLength && parseInt(contentLength) > 50 * 1024 * 1024) { // 50MB limit
return res.status(413).json({ error: 'Request too large' });
}
next();
}
/**
* Path traversal protection
*/
function pathTraversalProtection(req, res, next) {
// Check for path traversal attempts in various parameters
const suspiciousPatterns = ['../', '..\\', '%2e%2e%2f', '%2e%2e%5c'];
function checkForTraversal(obj, path = '') {
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'string') {
const lowerValue = value.toLowerCase();
for (const pattern of suspiciousPatterns) {
if (lowerValue.includes(pattern)) {
console.warn(`Path traversal attempt detected: ${path}${key} = ${value}`);
return true;
}
}
} else if (typeof value === 'object' && value !== null) {
if (checkForTraversal(value, `${path}${key}.`)) {
return true;
}
}
}
return false;
}
if ((req.query && checkForTraversal(req.query)) ||
(req.body && checkForTraversal(req.body))) {
return res.status(400).json({ error: 'Invalid request parameters' });
}
next();
}
module.exports = {
validateInput,
xssProtection,
additionalSecurityHeaders,
validateRequestSize,
pathTraversalProtection,
escapeHtml
};