// admin-tasks.js const http = require('http'); const { exec, spawn } = require('child_process'); const { URL } = require('url'); // --- Configuration --- const ADMIN_PORT = process.env.ADMIN_PORT || 9001; const ADMIN_SECRET_KEY = process.env.ADMIN_SECRET_KEY; // MUST be set via environment variable const API_DIR = '/app/api'; // Directory where 'npm run ...' commands should execute // --- Basic Authentication Middleware --- function authenticate(req, res, callback) { const providedSecret = req.headers['x-admin-secret']; if (!ADMIN_SECRET_KEY) { console.error('CRITICAL: ADMIN_SECRET_KEY is not set!'); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Server configuration error: Admin secret not set.' })); return; } if (providedSecret !== ADMIN_SECRET_KEY) { console.warn('Authentication failed: Incorrect or missing X-Admin-Secret header.'); res.writeHead(401, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Unauthorized: Missing or incorrect X-Admin-Secret header.' })); return; } callback(); // Proceed if authenticated } // --- Request Body Parser (Simple) --- function parseRequestBody(req, callback) { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { try { if (!body) { return callback(null, {}); // Handle empty body } const data = JSON.parse(body); callback(null, data); } catch (error) { callback(new Error('Invalid JSON body')); } }); req.on('error', (err) => { callback(err); }); } // --- Request Handler --- const server = http.createServer((req, res) => { const reqUrl = new URL(req.url, `http://${req.headers.host}`); const path = reqUrl.pathname; const method = req.method; console.log(`Admin task request: ${method} ${path}`); authenticate(req, res, () => { // Authenticated requests proceed here if (path === '/create-user' && method === 'POST') { handleCreateUser(req, res); } else if (path === '/delete-user' && method === 'POST') { // Using POST for simplicity, could be DELETE handleDeleteUser(req, res); } else { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Not Found' })); } }); }); // --- Handler for Create User --- function handleCreateUser(req, res) { parseRequestBody(req, (err, data) => { if (err) { res.writeHead(400, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: err.message })); } const { email, password } = data; if (!email || !password) { res.writeHead(400, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: 'Missing email or password in request body.' })); } console.log(`Attempting to create user: ${email}`); // Use spawn for interactive script const createUserProcess = spawn('npm', ['run', 'create-user'], { cwd: API_DIR, stdio: ['pipe', 'pipe', 'pipe'] // stdin, stdout, stderr }); let output = ''; let errorOutput = ''; createUserProcess.stdout.on('data', (data) => { output += data.toString(); console.log(`create-user stdout: ${data}`); // Respond to prompts based on output (this is fragile) if (output.includes('Enter email')) { console.log(`Sending email: ${email}`); createUserProcess.stdin.write(email + '\n'); } else if (output.includes('Enter password')) { console.log(`Sending password...`); createUserProcess.stdin.write(password + '\n'); } else if (output.includes('Confirm password')) { console.log(`Sending password confirmation...`); createUserProcess.stdin.write(password + '\n'); createUserProcess.stdin.end(); // Signal end of input } }); createUserProcess.stderr.on('data', (data) => { errorOutput += data.toString(); console.error(`create-user stderr: ${data}`); }); createUserProcess.on('close', (code) => { console.log(`create-user process exited with code ${code}`); if (code === 0 && !errorOutput.toLowerCase().includes('error')) { // Basic success check res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ message: `User creation process initiated for ${email}. Check container logs for success/failure.`, output: output })); } else { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: `User creation process failed with code ${code}.`, stderr: errorOutput, stdout: output })); } }); createUserProcess.on('error', (err) => { console.error('Failed to start create-user process:', err); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Failed to start user creation process.', details: err.message })); }); }); } // --- Handler for Delete User --- function handleDeleteUser(req, res) { parseRequestBody(req, (err, data) => { if (err) { res.writeHead(400, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: err.message })); } const { email } = data; if (!email) { res.writeHead(400, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: 'Missing email in request body.' })); } // Basic email format validation (optional but recommended) if (!/\S+@\S+\.\S+/.test(email)) { res.writeHead(400, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: 'Invalid email format provided.' })); } // Sanitize email to prevent command injection (basic example) const sanitizedEmail = email.replace(/[^a-zA-Z0-9@._-]/g, ''); if(sanitizedEmail !== email) { console.warn(`Potential command injection attempt detected and sanitized: ${email} -> ${sanitizedEmail}`); res.writeHead(400, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: 'Invalid characters in email.' })); } console.log(`Attempting to delete user: ${sanitizedEmail}`); const command = `npm run delete-user ${sanitizedEmail}`; exec(command, { cwd: API_DIR }, (error, stdout, stderr) => { if (error) { console.error(`exec error: ${error}`); res.writeHead(500, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: 'Failed to execute delete-user script.', details: stderr || error.message })); } if (stderr) { console.warn(`delete-user stderr: ${stderr}`); // Decide if stderr indicates a true error or just warnings // For now, we'll return success but include stderr } console.log(`delete-user stdout: ${stdout}`); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ message: `Delete command executed for user ${sanitizedEmail}.`, output: stdout, warnings: stderr })); }); }); } // --- Start Server --- server.listen(ADMIN_PORT, '0.0.0.0', () => { if (!ADMIN_SECRET_KEY) { console.error(` ################################################################## # WARNING: ADMIN_SECRET_KEY environment variable is not set! # # The admin tasks endpoint is INSECURE and will not function. # # Please set this variable before running in production. # ################################################################## `); } else { console.log(`Admin tasks server listening on port ${ADMIN_PORT}`); console.log('Ensure this port is not exposed publicly unless secured (e.g., via VPN or firewall).'); console.log(`Access requires the X-Admin-Secret header.`); } }); process.on('SIGTERM', () => { console.log('Admin tasks server received SIGTERM. Shutting down.'); server.close(() => { process.exit(0); }); });