const express = require('express'); const session = require('express-session'); const bcrypt = require('bcryptjs'); const sqlite3 = require('sqlite3').verbose(); const bodyParser = require('body-parser'); const { spawn } = require('child_process'); const WebSocket = require('ws'); const http = require('http'); const path = require('path'); const config = require('./config'); const app = express(); const server = http.createServer(app); const wss = new WebSocket.Server({ server }); // Database setup const db = new sqlite3.Database(config.database.path); // Initialize database tables db.serialize(() => { // Users table db.run(`CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, email TEXT NOT NULL, password TEXT NOT NULL, is_admin INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`); // DevBenches table db.run(`CREATE TABLE IF NOT EXISTS devbenches ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, name TEXT NOT NULL, actual_name TEXT, status TEXT DEFAULT 'inactive', ssh_info TEXT, vnc_info TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) )`); // Create default admin user if not exists const adminPassword = bcrypt.hashSync(config.defaultAdmin.password, 10); db.run(`INSERT OR IGNORE INTO users (username, email, password, is_admin) VALUES (?, ?, ?, 1)`, [config.defaultAdmin.username, config.defaultAdmin.email, adminPassword]); }); // Middleware app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); app.use(express.static('public')); app.set('view engine', 'ejs'); app.use(session({ secret: config.session.secret, resave: false, saveUninitialized: false, cookie: { secure: false, maxAge: config.session.maxAge } })); // Authentication middleware const requireAuth = (req, res, next) => { if (req.session.userId) { next(); } else { res.redirect('/login'); } }; const requireAdmin = (req, res, next) => { if (req.session.userId && req.session.isAdmin) { next(); } else { res.status(403).send('Access denied'); } }; // WebSocket connections for real-time updates const clients = new Map(); wss.on('connection', (ws, req) => { console.log('WebSocket connection established'); ws.on('message', (message) => { try { const data = JSON.parse(message); if (data.type === 'register' && data.userId) { clients.set(data.userId, ws); console.log(`User ${data.userId} registered for WebSocket updates`); } } catch (error) { console.error('WebSocket message error:', error); } }); ws.on('close', () => { // Remove this connection from all users for (const [userId, client] of clients.entries()) { if (client === ws) { clients.delete(userId); console.log(`User ${userId} WebSocket disconnected`); break; } } }); }); // Broadcast to specific user const broadcastToUser = (userId, message) => { const client = clients.get(userId); if (client && client.readyState === WebSocket.OPEN) { try { client.send(JSON.stringify(message)); console.log(`Sent message to user ${userId}:`, message.type); } catch (error) { console.error(`Error sending message to user ${userId}:`, error); clients.delete(userId); } } else { console.log(`No active WebSocket for user ${userId}`); } }; // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), version: '1.0.0' }); }); // User info endpoint for WebSocket registration app.get('/api/user-info', requireAuth, (req, res) => { res.json({ userId: req.session.userId, username: req.session.username, isAdmin: req.session.isAdmin }); }); // Routes app.get('/', (req, res) => { if (req.session.userId) { if (req.session.isAdmin) { res.redirect('/admin'); } else { res.redirect('/dashboard'); } } else { res.redirect('/login'); } }); app.get('/login', (req, res) => { res.render('login', { error: null }); }); app.post('/login', (req, res) => { const { username, password } = req.body; db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => { if (err || !user || !bcrypt.compareSync(password, user.password)) { return res.render('login', { error: 'Invalid username or password' }); } req.session.userId = user.id; req.session.username = user.username; req.session.isAdmin = user.is_admin === 1; if (user.is_admin) { res.redirect('/admin'); } else { res.redirect('/dashboard'); } }); }); app.get('/logout', (req, res) => { req.session.destroy(); res.redirect('/login'); }); // Admin routes app.get('/admin', requireAuth, requireAdmin, (req, res) => { db.all(`SELECT u.*, COUNT(d.id) as devbench_count FROM users u LEFT JOIN devbenches d ON u.id = d.user_id WHERE u.is_admin = 0 GROUP BY u.id`, (err, users) => { if (err) { console.error(err); return res.status(500).send('Database error'); } db.all(`SELECT d.*, u.username FROM devbenches d JOIN users u ON d.user_id = u.id ORDER BY d.created_at DESC`, (err, devbenches) => { if (err) { console.error(err); return res.status(500).send('Database error'); } res.render('admin', { users, devbenches }); }); }); }); app.post('/admin/add-user', requireAuth, requireAdmin, (req, res) => { const { username, email, password } = req.body; // Validate username (no spaces, numbers, or special characters) if (!config.validation.username.test(username)) { return res.status(400).json({ error: 'Username must contain only letters' }); } const hashedPassword = bcrypt.hashSync(password, 10); db.run('INSERT INTO users (username, email, password) VALUES (?, ?, ?)', [username, email, hashedPassword], function(err) { if (err) { if (err.code === 'SQLITE_CONSTRAINT') { return res.status(400).json({ error: 'Username already exists' }); } return res.status(500).json({ error: 'Database error' }); } res.json({ success: true }); }); }); app.post('/admin/delete-user/:id', requireAuth, requireAdmin, (req, res) => { const userId = req.params.id; // Delete user's devbenches first db.run('DELETE FROM devbenches WHERE user_id = ?', [userId], (err) => { if (err) { return res.status(500).json({ error: 'Database error' }); } // Delete user db.run('DELETE FROM users WHERE id = ? AND is_admin = 0', [userId], (err) => { if (err) { return res.status(500).json({ error: 'Database error' }); } res.json({ success: true }); }); }); }); app.post('/admin/reset-password/:id', requireAuth, requireAdmin, (req, res) => { const userId = req.params.id; const { newPassword } = req.body; const hashedPassword = bcrypt.hashSync(newPassword, 10); db.run('UPDATE users SET password = ? WHERE id = ? AND is_admin = 0', [hashedPassword, userId], (err) => { if (err) { return res.status(500).json({ error: 'Database error' }); } res.json({ success: true }); }); }); // Help page app.get('/help', requireAuth, (req, res) => { res.render('help', { username: req.session.username }); }); // User dashboard app.get('/dashboard', requireAuth, (req, res) => { if (req.session.isAdmin) { return res.redirect('/admin'); } db.all('SELECT * FROM devbenches WHERE user_id = ? ORDER BY created_at DESC', [req.session.userId], (err, devbenches) => { if (err) { console.error(err); return res.status(500).send('Database error'); } res.render('dashboard', { username: req.session.username, devbenches }); }); }); // DevBench operations app.post('/create-devbench', requireAuth, (req, res) => { const { name } = req.body; // Validate devbench name (only letters, numbers, hyphens, underscores) if (!config.validation.devbenchName.test(name)) { return res.status(400).json({ error: 'DevBench name can only contain letters, numbers, hyphens, and underscores' }); } const fullName = `${req.session.username}_${name}`; // Check if devbench already exists db.get('SELECT * FROM devbenches WHERE user_id = ? AND name = ?', [req.session.userId, name], (err, existing) => { if (err) { return res.status(500).json({ error: 'Database error' }); } if (existing) { return res.status(400).json({ error: 'DevBench with this name already exists' }); } // Insert into database first db.run('INSERT INTO devbenches (user_id, name, status) VALUES (?, ?, ?)', [req.session.userId, name, 'creating'], function(err) { if (err) { console.error('Database error:', err); return res.status(500).json({ error: 'Database error' }); } const devbenchId = this.lastID; console.log(`Created devbench record with ID: ${devbenchId}, full name: ${fullName}`); // Start the provision script setTimeout(() => { executeProvisionScript('create', fullName, req.session.userId, devbenchId); }, 1000); // Small delay to ensure WebSocket is ready res.json({ success: true, devbenchId }); }); }); }); app.post('/delete-devbench/:id', requireAuth, (req, res) => { const devbenchId = req.params.id; db.get('SELECT * FROM devbenches WHERE id = ? AND user_id = ?', [devbenchId, req.session.userId], (err, devbench) => { if (err || !devbench) { return res.status(404).json({ error: 'DevBench not found' }); } if (devbench.actual_name) { executeProvisionScript('delete', devbench.actual_name, req.session.userId, devbenchId); } db.run('DELETE FROM devbenches WHERE id = ?', [devbenchId], (err) => { if (err) { return res.status(500).json({ error: 'Database error' }); } res.json({ success: true }); }); }); }); app.post('/activate-devbench/:id', requireAuth, (req, res) => { const devbenchId = req.params.id; db.get('SELECT * FROM devbenches WHERE id = ? AND user_id = ?', [devbenchId, req.session.userId], (err, devbench) => { if (err || !devbench) { return res.status(404).json({ error: 'DevBench not found' }); } if (devbench.actual_name) { executeProvisionScript('activate', devbench.actual_name, req.session.userId, devbenchId); } res.json({ success: true }); }); }); // Function to execute provision script function executeProvisionScript(command, vmName, userId, devbenchId) { console.log(`Executing provision script: ${command} ${vmName} for user ${userId}`); const scriptPath = config.provision.scriptPath; let output = ''; // Check if script exists const fs = require('fs'); if (!fs.existsSync(scriptPath)) { console.error(`Script not found: ${scriptPath}`); broadcastToUser(userId, { type: 'script_output', devbenchId, data: `Error: Script not found at ${scriptPath}\n` }); // Update database to reflect error db.run('UPDATE devbenches SET status = ? WHERE id = ?', ['error', devbenchId]); return; } // Send initial message broadcastToUser(userId, { type: 'script_output', devbenchId, data: `Starting ${command} operation for ${vmName}...\n` }); const child = spawn('bash', [scriptPath, command, vmName], { cwd: process.cwd(), env: process.env }); child.stdout.on('data', (data) => { const chunk = data.toString(); output += chunk; console.log(`Script output: ${chunk.trim()}`); // Broadcast real-time output to user broadcastToUser(userId, { type: 'script_output', devbenchId, data: chunk }); }); child.stderr.on('data', (data) => { const chunk = data.toString(); output += chunk; console.log(`Script error: ${chunk.trim()}`); broadcastToUser(userId, { type: 'script_output', devbenchId, data: `ERROR: ${chunk}` }); }); child.on('error', (error) => { console.error(`Script execution error:`, error); broadcastToUser(userId, { type: 'script_output', devbenchId, data: `Execution Error: ${error.message}\n` }); db.run('UPDATE devbenches SET status = ? WHERE id = ?', ['error', devbenchId]); }); child.on('close', (code) => { console.log(`Script finished with exit code: ${code}`); if (command === 'create') { if (code === 0) { // Parse the output to extract VM name, SSH port, and VNC port const vmNameMatch = output.match(/VM_NAME=(.+)/); const sshPortMatch = output.match(/SSH_PORT=(\d+)/); const vncPortMatch = output.match(/VNC_PORT=(\d+)/); if (vmNameMatch && sshPortMatch && vncPortMatch) { const actualName = vmNameMatch[1].trim(); const sshPort = sshPortMatch[1]; const vncPort = vncPortMatch[1]; // Generate SSH and VNC info const sshInfo = sshPort; const vncInfo = vncPort; console.log(`Updating database: ${actualName}, SSH Port: ${sshPort}, VNC Port: ${vncPort}`); db.run(`UPDATE devbenches SET actual_name = ?, status = 'active', ssh_info = ?, vnc_info = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [actualName, sshInfo, vncInfo, devbenchId], (err) => { if (err) { console.error('Database update error:', err); } else { console.log('Database updated successfully'); } }); } else { console.log('Could not parse VM info from output'); db.run('UPDATE devbenches SET status = ? WHERE id = ?', ['error', devbenchId]); } } else { console.log('Script failed, updating status to error'); db.run('UPDATE devbenches SET status = ? WHERE id = ?', ['error', devbenchId]); } } broadcastToUser(userId, { type: 'script_complete', devbenchId, exitCode: code }); }); } // Status check endpoint app.get('/check-status/:id', requireAuth, (req, res) => { const devbenchId = req.params.id; db.get('SELECT * FROM devbenches WHERE id = ? AND user_id = ?', [devbenchId, req.session.userId], (err, devbench) => { if (err || !devbench || !devbench.actual_name) { return res.json({ status: 'unknown' }); } const child = spawn('bash', [config.provision.scriptPath, 'status', devbench.actual_name]); let output = ''; child.stdout.on('data', (data) => { output += data.toString(); }); child.stderr.on('data', (data) => { output += data.toString(); }); child.on('close', (code) => { // Parse status from output - look for "active" or "inactive" in the output let status = 'inactive'; if (output.includes('active')) { status = 'active'; } console.log(`Status check for ${devbench.actual_name}: ${status} (exit code: ${code})`); db.run('UPDATE devbenches SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [status, devbenchId]); res.json({ status }); }); }); }); // Periodic status check (every minute) setInterval(() => { db.all('SELECT * FROM devbenches WHERE actual_name IS NOT NULL', (err, devbenches) => { if (err) return; devbenches.forEach(devbench => { const child = spawn('bash', [config.provision.scriptPath, 'status', devbench.actual_name]); let output = ''; child.stdout.on('data', (data) => { output += data.toString(); }); child.stderr.on('data', (data) => { output += data.toString(); }); child.on('close', (code) => { // Parse status from output - look for "active" in the output let status = 'inactive'; if (output.includes('active')) { status = 'active'; } if (status !== devbench.status) { console.log(`Status changed for ${devbench.actual_name}: ${devbench.status} -> ${status}`); db.run('UPDATE devbenches SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [status, devbench.id]); // Notify user of status change broadcastToUser(devbench.user_id, { type: 'status_update', devbenchId: devbench.id, status }); } }); }); }); }, config.provision.statusCheckInterval); const PORT = config.port; server.listen(PORT, () => { console.log(`DevBench Manager running on port ${PORT}`); console.log(`Database path: ${config.database.path}`); console.log(`Provision script path: ${config.provision.scriptPath}`); // Check if provision script exists const fs = require('fs'); if (fs.existsSync(config.provision.scriptPath)) { console.log('✓ Provision script found'); } else { console.log('✗ Provision script NOT found'); } });