#!/usr/bin/env bash set -e # Rocketry Service Installer # Installs the React SPA as a systemd service on the system # Preflight checks command -v node >/dev/null || { echo "Error: node not found"; exit 1; } command -v npm >/dev/null || { echo "Error: npm not found"; exit 1; } command -v sudo >/dev/null || { echo "Error: sudo not found"; exit 1; } echo "Rocketry Service Installer" echo "==========================" echo "" # Port prompt read -p "Port to run on [default: 8080]: " PORT PORT=${PORT:-8080} # Validate port if ! [[ "$PORT" =~ ^[0-9]+$ ]] || [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then echo "Error: Invalid port number. Must be between 1 and 65535." exit 1 fi echo "" echo "Building application..." npm run build # Install directory INSTALL_DIR="/opt/rocketry" echo "Installing to $INSTALL_DIR..." sudo mkdir -p "$INSTALL_DIR" sudo cp -r dist "$INSTALL_DIR/" # Write server.js to /opt/rocketry/server.js echo "Writing server.js..." sudo tee "$INSTALL_DIR/server.js" > /dev/null <<'SERVEREOF' #!/usr/bin/env node const http = require('http'); const fs = require('fs'); const path = require('path'); const PORT = process.env.PORT || 8080; const DIST_DIR = path.join(__dirname, 'dist'); const MIME_TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon', '.woff2': 'font/woff2', '.json': 'application/json', }; const server = http.createServer((req, res) => { // Normalize URL let urlPath = decodeURIComponent(req.url); if (urlPath === '/') { urlPath = '/index.html'; } // Resolve file path const filePath = path.join(DIST_DIR, urlPath); // Security check: ensure we're within DIST_DIR if (!filePath.startsWith(DIST_DIR)) { res.writeHead(403, { 'Content-Type': 'text/plain' }); res.end('Forbidden'); return; } fs.stat(filePath, (err, stats) => { if (err) { // File not found; serve index.html for SPA routing (unless it's a file with extension) const ext = path.extname(urlPath); if (ext && ext !== '.html') { // Likely a missing asset file res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); return; } // SPA fallback: serve index.html const indexPath = path.join(DIST_DIR, 'index.html'); fs.readFile(indexPath, 'utf-8', (readErr, data) => { if (readErr) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Internal Server Error'); return; } res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(data); }); return; } if (stats.isDirectory()) { // Try to serve index.html from the directory const indexPath = path.join(filePath, 'index.html'); fs.readFile(indexPath, 'utf-8', (readErr, data) => { if (readErr) { // Directory exists but no index.html; fall back to SPA index const mainIndexPath = path.join(DIST_DIR, 'index.html'); fs.readFile(mainIndexPath, 'utf-8', (mainErr, mainData) => { if (mainErr) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); return; } res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(mainData); }); return; } res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(data); }); return; } // Serve file with appropriate MIME type const ext = path.extname(filePath).toLowerCase(); const contentType = MIME_TYPES[ext] || 'application/octet-stream'; fs.readFile(filePath, (readErr, data) => { if (readErr) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Internal Server Error'); return; } res.writeHead(200, { 'Content-Type': contentType }); res.end(data); }); }); }); server.listen(PORT, () => { console.log(`Rocketry server listening on http://localhost:${PORT}`); }); server.on('error', (err) => { if (err.code === 'EACCES') { console.error(`Error: Permission denied. Port ${PORT} may require elevated privileges.`); } else if (err.code === 'EADDRINUSE') { console.error(`Error: Port ${PORT} is already in use.`); } else { console.error('Server error:', err.message); } process.exit(1); }); SERVEREOF # Make server.js executable sudo chmod +x "$INSTALL_DIR/server.js" # Write systemd unit file echo "Writing systemd service file..." sudo bash -c "cat > /etc/systemd/system/rocketry.service" <