← back to Goldleafwallpaper

server.js

185 lines

// Load .env if present (holds GEORGE_EXTERNAL_SEND_TOKEN for the sample/inquiry
// forms). Wrapped so the site still boots if dotenv isn't installed.
try { require('dotenv').config({ path: require('path').join(__dirname, '.env') }); } catch (e) {}

const express = require('express');
const path = require('path');
const http = require('http');

const app = express();
const PORT = process.env.PORT || 9890;
const BIND = process.env.BIND || '0.0.0.0';

app.use(express.json());

// George Gmail Agent forward — same field contract as Novasuede
const GEORGE_PORT = parseInt(process.env.GEORGE_PORT || '9850', 10);
const GEORGE_AUTH = process.env.GEORGE_AUTH ||
  ('Basic ' + Buffer.from('admin:' + (process.env.GEORGE_BASIC_AUTH_PASS || '')).toString('base64'));
// George fail-closes external email (info@goldleafwallpaper.com is external to
// George's own Gmail account) and requires the human-approval token in an
// X-Send-Approval header, else the send is blocked with a 502. Without this the
// sample-request + inquiry forms silently drop every customer submission.
const GEORGE_APPROVAL = process.env.GEORGE_EXTERNAL_SEND_TOKEN || '';
const georgeHeaders = (data) => {
  const h = {
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(data),
    'Authorization': GEORGE_AUTH,
  };
  if (GEORGE_APPROVAL) h['X-Send-Approval'] = GEORGE_APPROVAL;
  return h;
};
const SAMPLE_RECIPIENT = process.env.SAMPLE_RECIPIENT || 'info@goldleafwallpaper.com';
const INQUIRY_RECIPIENT = process.env.INQUIRY_RECIPIENT || 'info@goldleafwallpaper.com';

const escapeHtml = (s) =>
  String(s == null ? '' : s)
    .replace(/&/g, '&')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');

app.post('/api/send-sample-request', (req, res) => {
  const { name, email, company, address, city, state, zip, color, message } = req.body || {};
  if (!name || !email || !color) {
    return res.status(400).json({ error: 'Name, email, and color are required.' });
  }
  const e = {
    name: escapeHtml(name),
    email: escapeHtml(email),
    company: escapeHtml(company || ''),
    address: escapeHtml(address || ''),
    city: escapeHtml(city || ''),
    state: escapeHtml(state || ''),
    zip: escapeHtml(zip || ''),
    color: escapeHtml(color),
    message: escapeHtml((message || '').replace(/\r?\n/g, '<br>'))
  };
  const subject = `Goldleaf Sample Request — ${e.color} — ${e.name}`;
  const body = `
    <div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto">
      <h2 style="color:#222;border-bottom:2px solid #C9A14B;padding-bottom:10px">Goldleaf Wallpaper Sample Request</h2>
      <table style="width:100%;border-collapse:collapse;margin-top:15px">
        <tr style="background:#faf5e8"><td style="padding:10px;font-weight:bold;border:1px solid #ddd;width:160px">Selected Pattern</td><td style="padding:10px;border:1px solid #ddd">${e.color}</td></tr>
        <tr><td style="padding:10px;font-weight:bold;border:1px solid #ddd">Full Name</td><td style="padding:10px;border:1px solid #ddd">${e.name}</td></tr>
        <tr style="background:#faf5e8"><td style="padding:10px;font-weight:bold;border:1px solid #ddd">Email</td><td style="padding:10px;border:1px solid #ddd"><a href="mailto:${e.email}">${e.email}</a></td></tr>
        <tr><td style="padding:10px;font-weight:bold;border:1px solid #ddd">Company</td><td style="padding:10px;border:1px solid #ddd">${e.company}</td></tr>
        <tr style="background:#faf5e8"><td style="padding:10px;font-weight:bold;border:1px solid #ddd">Shipping</td><td style="padding:10px;border:1px solid #ddd">${e.address}<br>${e.city}, ${e.state} ${e.zip}</td></tr>
        <tr><td style="padding:10px;font-weight:bold;border:1px solid #ddd">Notes</td><td style="padding:10px;border:1px solid #ddd">${e.message}</td></tr>
      </table>
      <p style="margin-top:20px;color:#666;font-size:12px">From goldleafwallpaper.com — Phillipe Romano · Renaissance Metal Leaf</p>
    </div>
  `;

  // George's contract: { to, subject, body }
  const data = JSON.stringify({ to: SAMPLE_RECIPIENT, subject, body });

  const upstream = http.request({
    hostname: '127.0.0.1', port: GEORGE_PORT, path: '/api/send',
    method: 'POST', timeout: 15000,
    headers: georgeHeaders(data)
  }, r => {
    let chunks = '';
    r.on('data', c => chunks += c);
    r.on('end', () => {
      // Pass George's response through, but only mark client success if George reports success
      let parsed;
      try { parsed = JSON.parse(chunks); } catch { parsed = { raw: chunks }; }
      const ok = r.statusCode >= 200 && r.statusCode < 300 && parsed.error == null;
      res.status(ok ? 200 : 502).json(ok ? { success: true } : { error: 'Forwarding failed', upstream: parsed });
    });
  });
  upstream.on('error', err => res.status(502).json({ error: err.message }));
  upstream.on('timeout', () => { upstream.destroy(); res.status(504).json({ error: 'Upstream timeout' }); });
  upstream.write(data); upstream.end();
});

app.post('/api/send-inquiry', (req, res) => {
  const { name, email, phone, company, projectName, projectScope, message } = req.body || {};
  if (!name || !email || !projectName || !projectScope) {
    return res.status(400).json({ error: 'Name, email, project name, and project scope are required.' });
  }
  const e = {
    name: escapeHtml(name),
    email: escapeHtml(email),
    phone: escapeHtml(phone || ''),
    company: escapeHtml(company || ''),
    projectName: escapeHtml(projectName),
    projectScope: escapeHtml(projectScope.replace(/\r?\n/g, '<br>')),
    message: escapeHtml((message || '').replace(/\r?\n/g, '<br>'))
  };
  const subject = `Goldleaf Project Inquiry — ${e.projectName} — ${e.name}`;
  const body = `
    <div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto">
      <h2 style="color:#222;border-bottom:2px solid #C9A14B;padding-bottom:10px">Goldleaf Wallpaper Project Inquiry</h2>
      <table style="width:100%;border-collapse:collapse;margin-top:15px">
        <tr style="background:#faf5e8"><td style="padding:10px;font-weight:bold;border:1px solid #ddd;width:160px">Project Name</td><td style="padding:10px;border:1px solid #ddd">${e.projectName}</td></tr>
        <tr><td style="padding:10px;font-weight:bold;border:1px solid #ddd">Project Scope</td><td style="padding:10px;border:1px solid #ddd">${e.projectScope}</td></tr>
        <tr style="background:#faf5e8"><td style="padding:10px;font-weight:bold;border:1px solid #ddd">Full Name</td><td style="padding:10px;border:1px solid #ddd">${e.name}</td></tr>
        <tr><td style="padding:10px;font-weight:bold;border:1px solid #ddd">Email</td><td style="padding:10px;border:1px solid #ddd"><a href="mailto:${e.email}">${e.email}</a></td></tr>
        <tr style="background:#faf5e8"><td style="padding:10px;font-weight:bold;border:1px solid #ddd">Phone</td><td style="padding:10px;border:1px solid #ddd">${e.phone || '—'}</td></tr>
        <tr><td style="padding:10px;font-weight:bold;border:1px solid #ddd">Company / Trade</td><td style="padding:10px;border:1px solid #ddd">${e.company || '—'}</td></tr>
        <tr style="background:#faf5e8"><td style="padding:10px;font-weight:bold;border:1px solid #ddd">Additional Info</td><td style="padding:10px;border:1px solid #ddd">${e.message || '—'}</td></tr>
      </table>
      <p style="margin-top:20px;color:#666;font-size:12px">From goldleafwallpaper.com — Phillipe Romano · Renaissance Metal Leaf</p>
    </div>
  `;

  const data = JSON.stringify({ to: INQUIRY_RECIPIENT, subject, body });

  const upstream = http.request({
    hostname: '127.0.0.1', port: GEORGE_PORT, path: '/api/send',
    method: 'POST', timeout: 15000,
    headers: georgeHeaders(data)
  }, r => {
    let chunks = '';
    r.on('data', c => chunks += c);
    r.on('end', () => {
      let parsed;
      try { parsed = JSON.parse(chunks); } catch { parsed = { raw: chunks }; }
      const ok = r.statusCode >= 200 && r.statusCode < 300 && parsed.error == null;
      res.status(ok ? 200 : 502).json(ok ? { success: true } : { error: 'Forwarding failed', upstream: parsed });
    });
  });
  upstream.on('error', err => res.status(502).json({ error: err.message }));
  upstream.on('timeout', () => { upstream.destroy(); res.status(504).json({ error: 'Upstream timeout' }); });
  upstream.write(data); upstream.end();
});

// Health endpoint
app.get('/health', (req, res) => res.json({ ok: true, service: 'goldleafwallpaper', port: PORT }));

// Never serve editor/snapshot files (.bak/.pre-*/.orig/.save/.old/.backup.json leak source
// history) or server source/manifest files (server.js, package*.json, *.conf, *.sh expose
// internals + dependency versions). express.static(__dirname) would otherwise serve them.
app.use((req, res, next) => {
  const base = path.basename(req.path);
  if (/(\.bak(\.|$)|\.pre-|\.orig$|\.save$|\.old$|\.backup\.json$|\.conf$|\.sh$)/i.test(req.path) ||
      /^(server\.js|package\.json|package-lock\.json)$/i.test(base)) {
    return res.status(404).send('Not found');
  }
  next();
});

// Static files with sane caching for assets (HTML stays fresh)
app.use(express.static(__dirname, {
  maxAge: '7d',
  setHeaders: (res, p) => {
    if (p.endsWith('.html')) {
      res.setHeader('Cache-Control', 'public, max-age=300');
    }
    if (p.endsWith('.json')) {
      res.setHeader('Cache-Control', 'public, max-age=3600');
    }
  }
}));

// SPA fallback (homepage for any other route)
app.get('*', (req, res) => res.sendFile(path.join(__dirname, 'index.html')));

app.listen(PORT, BIND, () => {
  console.log(`Goldleaf Wallpaper site listening on ${BIND}:${PORT}`);
});