GUA-AI / admin-tasks.js
WasabiDrop's picture
Create admin-tasks.js
0501295 verified
// 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);
});
});