[object Object]

← back to Domain Landings

app: admin inquiries viewer (Basic Auth, created date+time) + env-configurable George email auth

82ea4eb2cd0c5ce8988182164656eac786375029 · 2026-08-17 14:03:47 -0700 · Steve Abrams

Files touched

Diff

commit 82ea4eb2cd0c5ce8988182164656eac786375029
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 17 14:03:47 2026 -0700

    app: admin inquiries viewer (Basic Auth, created date+time) + env-configurable George email auth
---
 server.js | 42 +++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 41 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index 7bbcdfa..6562943 100644
--- a/server.js
+++ b/server.js
@@ -9,6 +9,10 @@ const path = require('path');
 const PORT = Number(process.env.PORT || readPort() || 9788);
 const OFFER_TO = process.env.OFFER_TO || 'steve@designerwallcoverings.com';
 const GEORGE = process.env.GEORGE_URL || 'http://127.0.0.1:9850/api/send';
+const GEORGE_TOKEN = process.env.GEORGE_TOKEN || '';               // set to enable email
+const GEORGE_AUTH_SCHEME = process.env.GEORGE_AUTH_SCHEME || 'Bearer'; // Bearer | Basic | raw
+const GEORGE_AUTH_HEADER = process.env.GEORGE_AUTH_HEADER || 'Authorization';
+const BASIC_AUTH = process.env.BASIC_AUTH || 'admin:DW2024!';      // admin viewer gate
 const CFG = JSON.parse(fs.readFileSync(path.join(__dirname, 'data/domains.json'), 'utf8'));
 const INQ = path.join(__dirname, 'data/inquiries.jsonl');
 
@@ -111,8 +115,12 @@ async function forwardGeorge(payload) {
     try {
       const data = JSON.stringify(payload);
       const u = new URL(GEORGE);
+      const headers = { 'content-type': 'application/json', 'content-length': Buffer.byteLength(data) };
+      if (GEORGE_TOKEN) {
+        headers[GEORGE_AUTH_HEADER] = GEORGE_AUTH_SCHEME === 'raw' ? GEORGE_TOKEN : `${GEORGE_AUTH_SCHEME} ${GEORGE_TOKEN}`;
+      }
       const req = http.request({ hostname: u.hostname, port: u.port, path: u.pathname, method: 'POST',
-        headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(data) }, timeout: 8000 },
+        headers, timeout: 8000 },
         r => { let b = ''; r.on('data', c => b += c); r.on('end', () => resolve({ ok: r.statusCode < 400, code: r.statusCode, body: b.slice(0, 200) })); });
       req.on('error', e => resolve({ ok: false, err: String(e.message) }));
       req.on('timeout', () => { req.destroy(); resolve({ ok: false, err: 'timeout' }); });
@@ -121,8 +129,40 @@ async function forwardGeorge(payload) {
   });
 }
 
+function adminInquiries(req, res) {
+  const hdr = req.headers.authorization || '';
+  const want = 'Basic ' + Buffer.from(BASIC_AUTH).toString('base64');
+  if (hdr !== want) {
+    res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="offers"' }).end('auth required');
+    return;
+  }
+  let rows = [];
+  try {
+    rows = fs.readFileSync(INQ, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l)).reverse();
+  } catch {}
+  const fmt = ts => { try { return new Date(ts).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); } catch { return ts; } };
+  const cards = rows.map(r => `<div class="card">
+    <div class="dom">${esc(r.domain)}</div>
+    <div class="when" title="${esc(r.ts)}">🕓 ${esc(fmt(r.ts))}</div>
+    <div class="who"><b>${esc(r.name)}</b> · <a href="mailto:${esc(r.email)}">${esc(r.email)}</a></div>
+    <div class="msg">${esc(r.message) || '<i>(no message)</i>'}</div></div>`).join('');
+  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
+  res.end(`<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1">
+<title>Domain offers (${rows.length})</title>
+<style>body{font:15px/1.5 -apple-system,system-ui,sans-serif;background:#0f1216;color:#e6edf3;margin:0;padding:24px}
+h1{font-size:20px;margin:0 0 18px}.card{background:#161b22;border:1px solid #2a3038;border-radius:12px;padding:16px 18px;margin:0 0 12px;max-width:720px}
+.dom{font-weight:700;color:#58a6ff;font-size:16px}.when{color:#8b949e;font-size:12px;margin:2px 0 8px}
+.who{margin-bottom:6px}.who a{color:#58a6ff}.msg{color:#c9d1d9;white-space:pre-wrap}
+.empty{color:#8b949e}</style>
+<h1>💰 Domain offers — ${rows.length}</h1>
+${cards || '<p class="empty">No offers yet.</p>'}`);
+}
+
 const server = http.createServer((req, res) => {
   const host = hostOf(req);
+  if (req.method === 'GET' && req.url.split('?')[0] === '/admin/inquiries') {
+    return adminInquiries(req, res);
+  }
   if (req.method === 'POST' && req.url.split('?')[0] === '/api/inquire') {
     let body = '';
     req.on('data', c => { body += c; if (body.length > 1e5) req.destroy(); });

← ee9f1e5 batch rollout tooling: dns_point + batch_nginx (batch 1: 18  ·  back to Domain Landings  ·  app: Basic-auth base64 for George + .env loader (secrets sta 8a2e5c4 →