← 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, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
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}`);
});