← 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
A .env.exampleM .gitignoreM server.js
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 →