← back to Robet Site
overnight loop item (f): security headers (CSP/helmet-style) + custom 404 + externalize admin script for strict CSP
44d16c072ca9640b878417f3801d93b223f12e2e · 2026-06-29 19:13:08 -0700 · Steve
Files touched
A public/404.htmlM public/admin.htmlA public/admin.jsM server.js
Diff
commit 44d16c072ca9640b878417f3801d93b223f12e2e
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jun 29 19:13:08 2026 -0700
overnight loop item (f): security headers (CSP/helmet-style) + custom 404 + externalize admin script for strict CSP
---
public/404.html | 26 ++++++++++++++++++++++++++
public/admin.html | 38 +-------------------------------------
public/admin.js | 36 ++++++++++++++++++++++++++++++++++++
server.js | 29 +++++++++++++++++++++++++++++
4 files changed, 92 insertions(+), 37 deletions(-)
diff --git a/public/404.html b/public/404.html
new file mode 100644
index 0000000..5d2622a
--- /dev/null
+++ b/public/404.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>404 · ROBERT</title>
+ <link rel="stylesheet" href="/styles.css" />
+</head>
+<body>
+ <header class="topbar">
+ <a class="logo" href="/">ROBERT</a>
+ <span class="mono" style="text-transform:uppercase">404</span>
+ </header>
+ <section style="min-height:70vh; display:flex; flex-direction:column; justify-content:center;">
+ <span class="kicker">/ dead end</span>
+ <h1 class="sec-h" style="font-size:clamp(56px,18vw,180px); line-height:.85;">404</h1>
+ <p class="mono" style="font-weight:700; font-size:20px; margin-bottom:24px;">
+ This page doesn't exist. The wall is blank here.
+ </p>
+ <div>
+ <a class="cta" href="/">← Back to start</a>
+ <a class="cta" href="/#booking" style="margin-left:12px;">Book a meeting →</a>
+ </div>
+ </section>
+</body>
+</html>
diff --git a/public/admin.html b/public/admin.html
index e207abe..833a8b8 100644
--- a/public/admin.html
+++ b/public/admin.html
@@ -32,42 +32,6 @@
<div class="keytable" id="table"></div>
</div>
- <script>
- const $ = (s) => document.querySelector(s);
- const api = (p, opt) => fetch(p, opt).then((r) => r.json());
-
- async function load() {
- const rows = await api('/api/keys');
- const t = $('#table');
- if (!rows.length) { t.innerHTML = '<p class="mono" style="font-weight:600">No keys stored yet.</p>'; return; }
- t.innerHTML =
- '<div class="keyrow head"><div>Name</div><div>Value</div><div>Action</div></div>' +
- rows.map((r) =>
- `<div class="keyrow">
- <div class="mono">${r.name}</div>
- <div class="mono">${r.masked}</div>
- <div><button class="btn warn" data-del="${r.name}">Delete</button></div>
- </div>`).join('');
- t.querySelectorAll('[data-del]').forEach((b) =>
- b.addEventListener('click', async () => {
- await api('/api/keys/' + encodeURIComponent(b.dataset.del), { method: 'DELETE' });
- load();
- }));
- }
-
- $('#saveBtn').addEventListener('click', async () => {
- const name = $('#kName').value.trim(), value = $('#kVal').value;
- const msg = $('#msg');
- if (!name || !value) { msg.textContent = '✗ name and value required'; return; }
- const j = await api('/api/keys', {
- method: 'POST', headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ name, value }),
- });
- msg.textContent = j.ok ? `✓ Saved ${j.name} (${j.masked})` : '✗ ' + (j.error || 'Failed');
- if (j.ok) { $('#kName').value = ''; $('#kVal').value = ''; load(); }
- });
-
- load();
- </script>
+ <script src="/admin.js"></script>
</body>
</html>
diff --git a/public/admin.js b/public/admin.js
new file mode 100644
index 0000000..7bd705b
--- /dev/null
+++ b/public/admin.js
@@ -0,0 +1,36 @@
+// ROBERT admin — API-key entry (externalized so CSP can stay strict, no inline script).
+const $ = (s) => document.querySelector(s);
+const api = (p, opt) => fetch(p, opt).then((r) => r.json());
+
+async function load() {
+ const rows = await api('/api/keys');
+ const t = $('#table');
+ if (!rows.length) { t.innerHTML = '<p class="mono" style="font-weight:600">No keys stored yet.</p>'; return; }
+ t.innerHTML =
+ '<div class="keyrow head"><div>Name</div><div>Value</div><div>Action</div></div>' +
+ rows.map((r) =>
+ `<div class="keyrow">
+ <div class="mono">${r.name}</div>
+ <div class="mono">${r.masked}</div>
+ <div><button class="btn warn" data-del="${r.name}">Delete</button></div>
+ </div>`).join('');
+ t.querySelectorAll('[data-del]').forEach((b) =>
+ b.addEventListener('click', async () => {
+ await api('/api/keys/' + encodeURIComponent(b.dataset.del), { method: 'DELETE' });
+ load();
+ }));
+}
+
+$('#saveBtn').addEventListener('click', async () => {
+ const name = $('#kName').value.trim(), value = $('#kVal').value;
+ const msg = $('#msg');
+ if (!name || !value) { msg.textContent = '✗ name and value required'; return; }
+ const j = await api('/api/keys', {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name, value }),
+ });
+ msg.textContent = j.ok ? `✓ Saved ${j.name} (${j.masked})` : '✗ ' + (j.error || 'Failed');
+ if (j.ok) { $('#kName').value = ''; $('#kVal').value = ''; load(); }
+});
+
+load();
diff --git a/server.js b/server.js
index c069890..7bf21d0 100644
--- a/server.js
+++ b/server.js
@@ -32,6 +32,29 @@ const app = express();
app.use(express.json({ limit: '64kb' }));
app.use(express.urlencoded({ extended: false }));
+// ── security headers (defense-in-depth; nginx also sets some on prod) ──
+const CSP = [
+ "default-src 'self'",
+ "script-src 'self' https://venturacorridor.com",
+ "style-src 'self' 'unsafe-inline'", // inline style="" attrs on the hero/admin
+ "img-src 'self' data: https://venturacorridor.com",
+ "font-src 'self'",
+ "connect-src 'self' https://venturacorridor.com",
+ "frame-src https://venturacorridor.com", // Smart Scheduling booking iframe
+ "object-src 'none'",
+ "base-uri 'self'",
+ "form-action 'self'",
+ "frame-ancestors 'self'",
+].join('; ');
+app.use((req, res, next) => {
+ res.set('Content-Security-Policy', CSP);
+ res.set('X-Content-Type-Options', 'nosniff');
+ res.set('X-Frame-Options', 'SAMEORIGIN');
+ res.set('Referrer-Policy', 'strict-origin-when-cross-origin');
+ res.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
+ next();
+});
+
// ── helpers ──
const readJson = (f, fallback) => {
try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return fallback; }
@@ -113,4 +136,10 @@ app.get('/admin', adminAuth, (_req, res) => res.sendFile(path.join(__dirname, 'p
app.use(express.static(path.join(__dirname, 'public')));
+// ── custom 404 (after static) ──
+app.use((req, res) => {
+ if (req.accepts('html')) return res.status(404).sendFile(path.join(__dirname, 'public', '404.html'));
+ res.status(404).json({ ok: false, error: 'Not found' });
+});
+
app.listen(PORT, () => console.log(`robet-site → http://127.0.0.1:${PORT}`));
← f55316d loop ledger: item (e) done + deployed
·
back to Robet Site
·
loop ledger: item (f) done + deployed 906d6de →