← back to Marketing Command Center

modules/reels/index.js

79 lines

// Reels — embeds the DW New Arrivals Reels console (the standalone
// dw-marketing-reels app) as an MCC panel via a same-origin reverse proxy.
// The reels app keeps running as its own pm2 process (:9848); this module
// proxies /api/reels/ui/* → REELS_BASE, injecting the upstream Basic auth
// server-side so the operator logs into the MCC once. Streams pass through
// untouched (Range headers forwarded → video scrubbing works in the iframe).
// If the upstream is down the panel degrades to a clear banner, never a hang.
const http = require('http');
const https = require('https');
const { URL } = require('url');

const BASE = process.env.REELS_BASE || 'http://127.0.0.1:9848';
const UP_USER = process.env.REELS_USER || 'admin';
const UP_PASS = process.env.REELS_PASS || 'DW2024!';
const UP_AUTH = 'Basic ' + Buffer.from(`${UP_USER}:${UP_PASS}`).toString('base64');

function upstream(pathname, req, res) {
  let u;
  try { u = new URL(pathname, BASE.endsWith('/') ? BASE : BASE + '/'); }
  catch (e) { return res.status(502).json({ error: `bad REELS_BASE: ${e.message}` }); }
  const lib = u.protocol === 'https:' ? https : http;
  const headers = {
    Authorization: UP_AUTH,
    Accept: req.headers.accept || '*/*',
  };
  // Range passthrough is what makes <video> seek work through the proxy.
  if (req.headers.range) headers.Range = req.headers.range;
  if (req.headers['content-type']) headers['Content-Type'] = req.headers['content-type'];

  const up = lib.request({
    method: req.method,
    hostname: u.hostname,
    port: u.port || (u.protocol === 'https:' ? 443 : 80),
    path: u.pathname + u.search,
    headers,
    timeout: 15000,
  }, (r) => {
    const h = { ...r.headers };
    delete h['www-authenticate']; // never leak the upstream auth challenge to the browser
    res.writeHead(r.statusCode || 502, h);
    r.pipe(res);
  });
  up.on('timeout', () => { up.destroy(new Error('upstream timeout')); });
  up.on('error', (e) => {
    if (!res.headersSent) res.status(502).json({ error: `reels app unreachable: ${e.message}`, base: BASE });
    else res.end();
  });
  req.pipe(up);
}

module.exports = {
  id: 'reels',
  title: 'Reels',
  icon: '🎬',
  mount(router) {
    // Fast reachability probe for the panel banner.
    router.get('/status', (_req, res) => {
      const u = new URL('api/reels', BASE.endsWith('/') ? BASE : BASE + '/');
      const lib = u.protocol === 'https:' ? https : http;
      const p = lib.get({
        hostname: u.hostname, port: u.port || 80, path: u.pathname,
        headers: { Authorization: UP_AUTH }, timeout: 4000,
      }, (r) => {
        let body = '';
        r.on('data', (d) => { body += d; if (body.length > 1e6) r.destroy(); });
        r.on('end', () => {
          let count = null;
          try { count = JSON.parse(body).length; } catch { /* non-JSON = still up */ }
          res.json({ ok: r.statusCode === 200, status: r.statusCode, reels: count, base: BASE });
        });
      });
      p.on('timeout', () => p.destroy(new Error('timeout')));
      p.on('error', (e) => res.json({ ok: false, error: e.message, base: BASE }));
    });
    // Everything under /api/reels/ui/* is the embedded console itself.
    router.use('/ui', (req, res) => upstream(req.url.replace(/^\//, ''), req, res));
  },
};