← back to Dw Boardroom Governance

frontend/serve.cjs

94 lines

// Static server with Basic Auth + API Proxy for Governance UI
const http = require('http');
const fs = require('fs');
const path = require('path');

const PORT = 4030;
const API_PORT = 4020;
const USER = 'admin';
const PASS = 'DWSecure2024!';
const DIST = path.join(__dirname, 'dist');

const MIME = {
  '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css',
  '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
  '.svg': 'image/svg+xml', '.woff2': 'font/woff2', '.woff': 'font/woff',
};

const server = http.createServer((req, res) => {
  const auth = req.headers.authorization;
  if (!auth || !auth.startsWith('Basic ')) {
    res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="DW Governance"' });
    return res.end('Unauthorized');
  }
  const [u, p] = Buffer.from(auth.slice(6), 'base64').toString().split(':');
  if (u !== USER || p !== PASS) {
    res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="DW Governance"' });
    return res.end('Unauthorized');
  }

  // ── API Proxy: Forward /api/* requests to backend on port 4020 ──
  if (req.url.startsWith('/api/')) {
    const proxyOpts = {
      hostname: '127.0.0.1',
      port: API_PORT,
      path: req.url,
      method: req.method,
      headers: { ...req.headers, host: `127.0.0.1:${API_PORT}` },
    };

    const proxyReq = http.request(proxyOpts, (proxyRes) => {
      res.writeHead(proxyRes.statusCode, proxyRes.headers);
      proxyRes.pipe(res);
    });

    proxyReq.on('error', (err) => {
      console.error('[Proxy] Error forwarding to API:', err.message);
      res.writeHead(502, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'API backend unavailable', detail: err.message }));
    });

    // Forward request body for POST/PUT/PATCH
    req.pipe(proxyReq);
    return;
  }

  // ── WebSocket upgrade: Forward /ws to backend ──
  // (handled by 'upgrade' event below)

  // ── Static file serving ──
  let filePath = path.join(DIST, req.url === '/' ? 'index.html' : req.url);
  if (!fs.existsSync(filePath)) filePath = path.join(DIST, 'index.html');
  const ext = path.extname(filePath);
  const mime = MIME[ext] || 'application/octet-stream';
  try {
    const content = fs.readFileSync(filePath);
    res.writeHead(200, { 'Content-Type': mime });
    res.end(content);
  } catch {
    res.writeHead(404);
    res.end('Not found');
  }
});

// ── WebSocket Proxy: Forward upgrade requests to backend ──
const net = require('net');
server.on('upgrade', (req, socket, head) => {
  if (!req.url.startsWith('/ws')) return socket.destroy();

  const proxy = net.connect(API_PORT, '127.0.0.1', () => {
    proxy.write(
      `${req.method} ${req.url} HTTP/1.1\r\n` +
      Object.entries(req.headers).map(([k, v]) => `${k}: ${v}`).join('\r\n') +
      '\r\n\r\n'
    );
    if (head && head.length) proxy.write(head);
    socket.pipe(proxy).pipe(socket);
  });

  proxy.on('error', () => socket.destroy());
  socket.on('error', () => proxy.destroy());
});

server.listen(PORT, '0.0.0.0', () => console.log(`[Governance UI] Serving on port ${PORT} with auth + API proxy to ${API_PORT}`));