[object Object]

← back to New Items Dashboard

secure: basic-auth gate on all routes (server was public on 0.0.0.0:7201)

b447df17b17f7aa70d0033eb84ebe3092fc3c0fb · 2026-05-19 22:12:50 -0700 · Steve Abrams

Mounts an Express middleware before any route declaration so /api/new-wines,
/api/new-handbags, /api/stats and the HTML dashboard all require credentials.
Credentials come from env BASIC_AUTH (user:pass form); placeholder is left in
code with a SECURITY: rotate-before-deploy comment. In dev (NODE_ENV !=
production) with no BASIC_AUTH set, the gate fails open for local convenience.
Uses crypto.timingSafeEqual to dodge length-leak side channels.

.env.example documents BASIC_AUTH + NODE_ENV. .gitignore exempts .env.example
so the template can be tracked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit b447df17b17f7aa70d0033eb84ebe3092fc3c0fb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 19 22:12:50 2026 -0700

    secure: basic-auth gate on all routes (server was public on 0.0.0.0:7201)
    
    Mounts an Express middleware before any route declaration so /api/new-wines,
    /api/new-handbags, /api/stats and the HTML dashboard all require credentials.
    Credentials come from env BASIC_AUTH (user:pass form); placeholder is left in
    code with a SECURITY: rotate-before-deploy comment. In dev (NODE_ENV !=
    production) with no BASIC_AUTH set, the gate fails open for local convenience.
    Uses crypto.timingSafeEqual to dodge length-leak side channels.
    
    .env.example documents BASIC_AUTH + NODE_ENV. .gitignore exempts .env.example
    so the template can be tracked.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 .env.example | 13 +++++++++++++
 .gitignore   |  1 +
 server.js    | 28 ++++++++++++++++++++++++++++
 3 files changed, 42 insertions(+)

diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..0b3a946
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,13 @@
+# new-items-dashboard env
+
+# Port (default 7201)
+# PORT=7201
+
+# Basic-auth credentials in "user:password" form.
+# SECURITY: rotate before deploying to prod. Without this var set in production,
+# the dashboard will fall back to the in-code placeholder — DO NOT ship that.
+BASIC_AUTH=admin:CHANGE-ME
+
+# Set NODE_ENV=production in prod so the gate fails CLOSED even if BASIC_AUTH
+# is somehow unset.
+# NODE_ENV=production
diff --git a/.gitignore b/.gitignore
index d1b3d72..bae4166 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ tmp/
 
 .env.*.local
 .env.*
+!.env.example
 
 # Snapshot / editor scratch files — never commit
 *.bak
diff --git a/server.js b/server.js
index 6b5e94f..770b346 100644
--- a/server.js
+++ b/server.js
@@ -2,6 +2,7 @@ const express = require('express');
 const helmet = require('helmet');
 const path = require('path');
 const fs = require('fs').promises;
+const crypto = require('crypto');
 const sqlite3 = require('sqlite3').verbose();
 
 const app = express();
@@ -18,6 +19,33 @@ app.use((req, res, next) => {
   next();
 });
 
+// ---------------------------------------------------------------------------
+// Basic-auth gate (added 2026-05-19 — was publicly exposing vendor + price data)
+// SECURITY: rotate before next deploy — set BASIC_AUTH in prod .env to override.
+const BASIC_AUTH = process.env.BASIC_AUTH || 'admin:DW2025Secure!';
+function safeEqual(a, b) {
+  const ab = Buffer.from(String(a));
+  const bb = Buffer.from(String(b));
+  if (ab.length !== bb.length) return false;
+  return crypto.timingSafeEqual(ab, bb);
+}
+app.use((req, res, next) => {
+  // In dev (NODE_ENV !== 'production') with no explicit BASIC_AUTH set, fail OPEN.
+  if (process.env.NODE_ENV !== 'production' && !process.env.BASIC_AUTH) return next();
+  const hdr = req.headers.authorization || '';
+  if (!hdr.startsWith('Basic ')) {
+    res.set('WWW-Authenticate', 'Basic realm="new-items-dashboard"');
+    return res.status(401).send('Authentication required');
+  }
+  const supplied = Buffer.from(hdr.slice(6), 'base64').toString();
+  if (!safeEqual(supplied, BASIC_AUTH)) {
+    res.set('WWW-Authenticate', 'Basic realm="new-items-dashboard"');
+    return res.status(401).send('Invalid credentials');
+  }
+  next();
+});
+// ---------------------------------------------------------------------------
+
 // Serve static files
 app.use(express.static('public'));
 

← 7d0b816 feat: sort + density controls on wines & handbags grids  ·  back to New Items Dashboard  ·  test: auth gate smoke test (boots server, asserts 401/200 ma ed3ed84 →