Files
LuHost/routes/auth.js
Nathan Schneider 2d3b1166fe Fix server management issues and improve overall stability
Major server management fixes:
- Replace Flatpak-specific pkill with universal process tree termination using pstree + process.kill()
- Fix signal format errors (SIGTERM/SIGKILL instead of TERM/KILL strings)
- Add 5-second cooldown after server stop to prevent race conditions with external detection
- Enable Stop Server button for external servers in UI
- Implement proper timeout handling with process tree killing

ContentDB improvements:
- Fix download retry logic and "closed" error by preventing concurrent zip extraction
- Implement smart root directory detection and stripping during package extraction
- Add game-specific timeout handling (8s for VoxeLibre vs 3s for simple games)

World creation fixes:
- Make world creation asynchronous to prevent browser hangs
- Add WebSocket notifications for world creation completion status

Other improvements:
- Remove excessive debug logging
- Improve error handling and user feedback throughout the application
- Clean up temporary files and unnecessary logging

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-24 19:17:38 -06:00

317 lines
9.4 KiB
JavaScript

const express = require('express');
const AuthManager = require('../utils/auth');
const { redirectIfAuthenticated } = require('../middleware/auth');
const securityLogger = require('../utils/security-logger');
const paths = require('../utils/paths');
const appConfig = require('../utils/app-config');
const router = express.Router();
const authManager = new AuthManager();
// Initialize auth manager
authManager.initialize().catch(console.error);
// Login page
router.get('/login', redirectIfAuthenticated, async (req, res) => {
try {
const isFirstUser = await authManager.isFirstUser();
if (isFirstUser) {
// No users exist yet - redirect to registration
return res.redirect('/register');
}
const redirectUrl = req.query.redirect || '/';
res.render('auth/login', {
title: 'Login',
redirectUrl: redirectUrl,
currentPage: 'login'
});
} catch (error) {
console.error('Error checking first user on login:', error);
const redirectUrl = req.query.redirect || '/';
res.render('auth/login', {
title: 'Login',
redirectUrl: redirectUrl,
currentPage: 'login'
});
}
});
// Register page (only for first user)
router.get('/register', redirectIfAuthenticated, async (req, res) => {
try {
const isFirstUser = await authManager.isFirstUser();
if (!isFirstUser) {
return res.status(403).render('error', {
error: 'Registration Not Available',
message: 'New accounts can only be created by existing administrators. Please contact an admin to create your account.'
});
}
// Detect available Luanti data directories
const detectedDirectories = paths.detectLuantiDataDirectories();
res.render('auth/register', {
title: 'Setup Administrator Account',
isFirstUser: isFirstUser,
currentPage: 'register',
detectedDirectories: detectedDirectories,
defaultDataDir: paths.getDefaultDataDirectory()
});
} catch (error) {
console.error('Error checking first user:', error);
res.status(500).render('error', {
error: 'Failed to load registration page',
message: error.message
});
}
});
// Process login
router.post('/login', redirectIfAuthenticated, async (req, res) => {
try {
const { username, password, redirect } = req.body;
if (!username || !password) {
return res.render('auth/login', {
title: 'Login',
error: 'Username and password are required',
redirectUrl: redirect || '/',
currentPage: 'login',
formData: { username }
});
}
const user = await authManager.authenticateUser(username, password);
// Log successful authentication
await securityLogger.logAuthSuccess(req, username);
// Create session
req.session.user = user;
// Redirect to intended page or dashboard
const redirectUrl = redirect && redirect !== '/login' ? redirect : '/';
res.redirect(redirectUrl);
} catch (error) {
console.error('Login error:', error);
// Log failed authentication
await securityLogger.logAuthFailure(req, username, error.message);
res.render('auth/login', {
title: 'Login',
error: error.message,
redirectUrl: req.body.redirect || '/',
currentPage: 'login',
formData: { username: req.body.username }
});
}
});
// Process registration (only for first user)
router.post('/register', redirectIfAuthenticated, async (req, res) => {
try {
const isFirstUser = await authManager.isFirstUser();
if (!isFirstUser) {
return res.status(403).render('error', {
error: 'Registration Not Available',
message: 'New accounts can only be created by existing administrators.'
});
}
const { username, password, confirmPassword, dataDirectory, customDataDirectory } = req.body;
// Validate inputs
if (!username || !password || !confirmPassword) {
const detectedDirectories = paths.detectLuantiDataDirectories();
return res.render('auth/register', {
title: 'Setup Administrator Account',
error: 'All fields are required',
isFirstUser: true,
currentPage: 'register',
formData: { username },
detectedDirectories: detectedDirectories,
defaultDataDir: paths.getDefaultDataDirectory()
});
}
if (password !== confirmPassword) {
const detectedDirectories = paths.detectLuantiDataDirectories();
return res.render('auth/register', {
title: 'Setup Administrator Account',
error: 'Passwords do not match',
isFirstUser: true,
currentPage: 'register',
formData: { username },
detectedDirectories: detectedDirectories,
defaultDataDir: paths.getDefaultDataDirectory()
});
}
// Handle data directory selection
let selectedDataDir = dataDirectory;
if (dataDirectory === 'custom' && customDataDirectory) {
selectedDataDir = customDataDirectory;
}
// Validate the selected data directory
if (selectedDataDir && selectedDataDir !== 'custom') {
try {
// Ensure the directory exists or can be created
const fs = require('fs');
if (!fs.existsSync(selectedDataDir)) {
await fs.promises.mkdir(selectedDataDir, { recursive: true });
}
// Save the data directory to app config
await appConfig.load();
appConfig.setDataDirectory(selectedDataDir);
await appConfig.save();
// Update paths to use the new directory
paths.setDataDirectory(selectedDataDir);
console.log('Data directory set to:', selectedDataDir);
} catch (error) {
console.error('Error setting data directory:', error);
const detectedDirectories = paths.detectLuantiDataDirectories();
return res.render('auth/register', {
title: 'Setup Administrator Account',
error: `Invalid data directory: ${error.message}`,
isFirstUser: true,
currentPage: 'register',
formData: { username },
detectedDirectories: detectedDirectories,
defaultDataDir: paths.getDefaultDataDirectory()
});
}
}
const user = await authManager.createUser(username, password);
// Create session for new user
req.session.user = {
id: user.id,
username: user.username,
created_at: user.created_at
};
// Redirect to dashboard
res.redirect('/?registered=true');
} catch (error) {
console.error('Registration error:', error);
const detectedDirectories = paths.detectLuantiDataDirectories();
res.render('auth/register', {
title: 'Register',
error: error.message,
isFirstUser: await authManager.isFirstUser(),
currentPage: 'register',
formData: {
username: req.body.username
},
detectedDirectories: detectedDirectories,
defaultDataDir: paths.getDefaultDataDirectory()
});
}
});
// Logout
router.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
console.error('Logout error:', err);
return res.status(500).json({ error: 'Failed to logout' });
}
if (req.headers.accept && req.headers.accept.includes('application/json')) {
res.json({ message: 'Logged out successfully' });
} else {
res.redirect('/login?message=You have been logged out');
}
});
});
// Get logout (for convenience)
router.get('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
console.error('Logout error:', err);
}
res.redirect('/login?message=You have been logged out');
});
});
// User profile page
router.get('/profile', async (req, res) => {
if (!req.session || !req.session.user) {
return res.redirect('/login');
}
try {
const user = await authManager.getUserById(req.session.user.id);
if (!user) {
req.session.destroy();
return res.redirect('/login?error=User not found');
}
res.render('auth/profile', {
title: 'Profile',
user: user,
currentPage: 'profile'
});
} catch (error) {
console.error('Profile error:', error);
res.status(500).render('error', {
error: 'Failed to load profile',
message: error.message
});
}
});
// Change password
router.post('/change-password', async (req, res) => {
if (!req.session || !req.session.user) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const { currentPassword, newPassword, confirmPassword } = req.body;
if (!currentPassword || !newPassword || !confirmPassword) {
throw new Error('All fields are required');
}
if (newPassword !== confirmPassword) {
throw new Error('New passwords do not match');
}
await authManager.changePassword(req.session.user.id, currentPassword, newPassword);
if (req.headers.accept && req.headers.accept.includes('application/json')) {
res.json({ message: 'Password changed successfully' });
} else {
res.redirect('/profile?success=Password changed successfully');
}
} catch (error) {
console.error('Change password error:', error);
if (req.headers.accept && req.headers.accept.includes('application/json')) {
res.status(400).json({ error: error.message });
} else {
res.redirect('/profile?error=' + encodeURIComponent(error.message));
}
}
});
module.exports = router;