[object Object]

← back to Norma

fix(set-admin-password): read .env.local first, echo DB host, abort on multi-row (Cody gate)

7c027a1b4872e30962b86818eb6e29b4a721efd8 · 2026-08-28 00:45:05 -0700 · Steve Abrams

Files touched

Diff

commit 7c027a1b4872e30962b86818eb6e29b4a721efd8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 28 00:45:05 2026 -0700

    fix(set-admin-password): read .env.local first, echo DB host, abort on multi-row (Cody gate)
---
 scripts/set-admin-password.mjs | 21 ++++++++++++++++++---
 1 file changed, 18 insertions(+), 3 deletions(-)

diff --git a/scripts/set-admin-password.mjs b/scripts/set-admin-password.mjs
index e666085..2cf2206 100644
--- a/scripts/set-admin-password.mjs
+++ b/scripts/set-admin-password.mjs
@@ -11,13 +11,19 @@ import pg from 'pg';
 import fs from 'node:fs';
 import path from 'node:path';
 
-if (!process.env.DATABASE_URL && fs.existsSync('.env')) {
-  for (const line of fs.readFileSync('.env', 'utf8').split('\n')) {
+// PM2 loads the fleet via --env-file=.env.local (see ecosystem.config.js), and
+// DATABASE_URL lives there — so check .env.local FIRST, then .env as a fallback.
+const envFile = ['.env.local', '.env'].find((f) => fs.existsSync(f));
+if (!process.env.DATABASE_URL && envFile) {
+  for (const line of fs.readFileSync(envFile, 'utf8').split('\n')) {
     const m = line.match(/^\s*DATABASE_URL\s*=\s*(.*)$/);
     if (m) { process.env.DATABASE_URL = m[1].trim().replace(/^["']|["']$/g, ''); break; }
   }
 }
-if (!process.env.DATABASE_URL) { console.error('FATAL: DATABASE_URL not set (env or .env)'); process.exit(2); }
+if (!process.env.DATABASE_URL) {
+  console.error('FATAL: DATABASE_URL not set (checked env, .env.local, .env)');
+  process.exit(2);
+}
 
 const username = process.env.TARGET_USER || 'admin';
 const newPw = process.env.NEW_PW || 'DW2024!';
@@ -25,6 +31,8 @@ const newPw = process.env.NEW_PW || 'DW2024!';
 const { Client } = pg;
 const c = new Client({ connectionString: process.env.DATABASE_URL });
 await c.connect();
+// Audit trail: prove which server/db we hit (host + db only — never the password).
+try { const u = new URL(process.env.DATABASE_URL); console.log(`DB: ${u.hostname}:${u.port || 5432}${u.pathname}`); } catch { /* non-URL DSN */ }
 
 // 1. Capture current hash(es) for rollback
 const before = await c.query(
@@ -36,6 +44,13 @@ if (before.rows.length === 0) {
   await c.end();
   process.exit(3);
 }
+if (before.rows.length > 1) {
+  // schema.sql UNIQUEs username, but guard in case a prod migration dropped it —
+  // a single-row rollback file would silently under-capture a multi-row overwrite.
+  console.error(`ABORT: ${before.rows.length} rows for username='${username}' — refusing to overwrite ambiguously.`);
+  await c.end();
+  process.exit(4);
+}
 const stamp = new Date().toISOString().replace(/[:.]/g, '-');
 const restoreDir = path.join('scripts', 'password-restore');
 fs.mkdirSync(restoreDir, { recursive: true });

← c5e8f58 scripts: reversible set-admin-password helper (bcrypt, captu  ·  back to Norma  ·  feat(set-admin-password): NEW_PW_B64 input to survive shell db0ecc2 →