fix(scripts): convert reset-db.js to ESM + require ADMIN_INITIAL_PASSWORD #25
2 changed files with 161 additions and 18 deletions
124
.convoys/fix-reset-db-script.md
Normal file
124
.convoys/fix-reset-db-script.md
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# fix-reset-db-script (P2 hygiene — fold of two queued follow-ups)
|
||||
|
||||
**Status:** in-progress
|
||||
**Priority:** P2 hygiene (not a security blocker; `npm run reset-db` is dev-only
|
||||
and currently broken on Node 22, so blast radius is low — but the bug
|
||||
pattern is the same as the P0-grade weak-creds shape that
|
||||
`drop-public-setup` already fixed once)
|
||||
**Convoy owner:** parent (no architect — fold of two well-scoped
|
||||
follow-ups; single-file fix following an established proven pattern)
|
||||
**Opened:** 2026-05-25
|
||||
|
||||
## Problem (3 bugs in 1 file)
|
||||
|
||||
`scripts/reset-db.js` carries three known bugs surfaced by the
|
||||
`pick-a-name` architect audit (2026-05-24) and ratified for fix in
|
||||
this session:
|
||||
|
||||
1. **CJS-in-ESM environment** (lines 10, 12, 142): `require()` calls in
|
||||
an ESM file (`package.json "type": "module"` since `bump-next-js`).
|
||||
`npm run reset-db` throws `ReferenceError: require is not defined`
|
||||
on Node 22.x. **Same bug pattern that hit `setup-neon-db.js`
|
||||
pre-`drop-public-setup` Brief 2.** Fix is the same fix.
|
||||
2. **Hardcoded weak admin password** (line 143: `bcrypt.hash('admin123', 12)`):
|
||||
identical anti-pattern to the one `drop-public-setup` Brief 1
|
||||
removed from `setup-neon-db.js`. Should require `ADMIN_INITIAL_PASSWORD`
|
||||
env var, fail loud if unset.
|
||||
3. **Password echoed to stdout** (line 156:
|
||||
`console.log(' Admin Password: admin123')`): explicit security
|
||||
anti-pattern. `setup-neon-db.js` post-`drop-public-setup` does NOT
|
||||
echo the password — same convention applies here.
|
||||
|
||||
## Fix (mirror `setup-neon-db.js` exactly)
|
||||
|
||||
The fix shape is fully derived from the post-`drop-public-setup`
|
||||
`scripts/setup-neon-db.js` file shipped at `b63b509`. Verbatim mirror:
|
||||
|
||||
- Replace CJS `require('dotenv').config()` with ESM
|
||||
`import dotenv from 'dotenv'; dotenv.config({ path: '.env.local' })`
|
||||
- Replace CJS `const { neon } = require('@neondatabase/serverless')`
|
||||
with ESM `import { neon } from '@neondatabase/serverless'`
|
||||
- Replace inline CJS `const bcrypt = require('bcryptjs')` (line 142)
|
||||
with top-level ESM `import bcrypt from 'bcryptjs'`
|
||||
- Add the same fail-loud `ADMIN_INITIAL_PASSWORD` env-var check at the
|
||||
top of the `async function resetDatabase()` body, with the same
|
||||
helpful error message that points to README.md
|
||||
- Replace `bcrypt.hash('admin123', 12)` with `bcrypt.hash(adminPassword, 12)`
|
||||
- Update the admin email to the post-`pick-a-name` canonical
|
||||
(`admin@deckhearth.com` — already correct in the file at line 147,
|
||||
by the B2 sweep)
|
||||
- Replace the `Admin Password: admin123` log line with `Admin user
|
||||
ready (email: admin@deckhearth.com)` (matching `setup-neon-db.js`
|
||||
line 157)
|
||||
- Add `ON CONFLICT (email) DO NOTHING` to the INSERT (matching
|
||||
`setup-neon-db.js` line 149 — defensive against double-run)
|
||||
|
||||
## Scope
|
||||
|
||||
- **In scope:** `scripts/reset-db.js` only.
|
||||
- **Out of scope:** any other `scripts/*.js` files (none have the same
|
||||
bugs — `setup-neon-db.js` already fixed, migration script already
|
||||
ESM, the rest don't ship admin creds).
|
||||
|
||||
## Why no architect
|
||||
|
||||
This is a **proven-pattern fold** — both `convert-reset-db-to-esm` and
|
||||
`purge-weak-creds-from-helpers` were architect-recommended in
|
||||
`pick-a-name` for "may fold if more such bugs accumulate in helper
|
||||
scripts." All 3 bugs are in 1 file; the fix shape is verbatim-mirror of
|
||||
the post-`drop-public-setup` `setup-neon-db.js`. No new decisions; no
|
||||
new precedents; no surface for an architect to add value. Parent
|
||||
applies the fix, runs the bounded checks, opens the PR. If anything
|
||||
surprising surfaces (a 4th bug, a different bcrypt API, etc.), the
|
||||
parent stops and dispatches an architect mid-execution.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `node --check scripts/reset-db.js` exit 0
|
||||
- `npm run lint` exit 1 with 128 problems (baseline preserved; no
|
||||
regression)
|
||||
- `npm run test:run` 21/21 pass (no test surface touched; verification
|
||||
only)
|
||||
- Grep: 0 occurrences of `require(` in `scripts/reset-db.js`
|
||||
- Grep: 0 occurrences of `admin123` in `scripts/reset-db.js`
|
||||
- Grep: 0 occurrences of `Admin Password` in `scripts/reset-db.js`
|
||||
- Grep: `ADMIN_INITIAL_PASSWORD` referenced (1 hit)
|
||||
|
||||
## Operator action required pre-merge
|
||||
|
||||
- **None pre-merge** (no schema change, no env-var addition).
|
||||
- **Optional post-merge:** if the operator wants to verify the fix
|
||||
works end-to-end, they can run `npm run reset-db` against a
|
||||
**non-prod** Neon branch (the script drops all tables — DO NOT run
|
||||
against prod). The script will refuse to run if `ADMIN_INITIAL_PASSWORD`
|
||||
is not set in `.env.local`, with the same helpful error message
|
||||
`setup-neon-db.js` already uses.
|
||||
|
||||
## Known constraints
|
||||
|
||||
- The script is **destructive** (drops all tables). We do NOT live-test
|
||||
it in this convoy. Boot-the-brief is syntax + lint + vitest only.
|
||||
Live verification is the operator's optional post-merge action.
|
||||
- This convoy does NOT add ESLint `no-restricted-syntax` rule against
|
||||
CJS `require()` in `scripts/**`. That would be a separate
|
||||
`lint-against-cjs-in-esm-scripts` convoy. Surfaced here so future
|
||||
helper scripts don't re-introduce the bug.
|
||||
|
||||
## Out of scope (queued follow-ups)
|
||||
|
||||
- `lint-against-cjs-in-esm-scripts` (NEW, P3 polish): add ESLint rule
|
||||
to prevent any future `require()` in `scripts/**` once `package.json`
|
||||
has `"type": "module"`.
|
||||
- `add-neon-return-shape-rule` (P3 polish, surfaced 2026-05-25 by
|
||||
PR #24): codify the `neon()` vs `@vercel/postgres` return-shape
|
||||
difference as a rule. **May fold into `single-sql-client`** which
|
||||
would eliminate the dual-client problem entirely.
|
||||
|
||||
## Owns
|
||||
|
||||
Parent (single-file proven-pattern fix; no architect or implementer
|
||||
subagent required).
|
||||
|
||||
## As-shipped
|
||||
|
||||
(To be appended post-merge.)
|
||||
|
|
@ -2,24 +2,47 @@
|
|||
|
||||
/**
|
||||
* Reset Database Script
|
||||
*
|
||||
* This script drops and recreates all tables in your Neon database.
|
||||
*
|
||||
* This script drops and recreates all tables in your Neon database, then
|
||||
* seeds an admin user. DESTRUCTIVE — never run against production.
|
||||
*
|
||||
* Required env (in .env.local):
|
||||
* POSTGRES_URL — Neon connection string
|
||||
* ADMIN_INITIAL_PASSWORD — strong password for the seeded admin user
|
||||
* (generate with `openssl rand -base64 24`)
|
||||
*
|
||||
* Mirrors the post-`drop-public-setup` shape of `setup-neon-db.js`
|
||||
* (commit b63b509) — same ESM imports, same fail-loud env-var check,
|
||||
* same no-password-echo convention. Convoy: `fix-reset-db-script`
|
||||
* (2026-05-25).
|
||||
*/
|
||||
|
||||
// Load environment variables from .env.local
|
||||
require('dotenv').config({ path: '.env.local' });
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config({ path: '.env.local' });
|
||||
|
||||
const { neon } = require('@neondatabase/serverless');
|
||||
import { neon } from '@neondatabase/serverless';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
async function resetDatabase() {
|
||||
const adminPassword = process.env.ADMIN_INITIAL_PASSWORD;
|
||||
if (!adminPassword || !adminPassword.trim()) {
|
||||
console.error(
|
||||
'❌ ADMIN_INITIAL_PASSWORD environment variable is not set.\n' +
|
||||
'\n' +
|
||||
' Set it in .env.local for local dev, or as a CI secret if you run reset from CI.\n' +
|
||||
' Generate a strong password with: openssl rand -base64 24\n' +
|
||||
' See README.md → "First-time admin setup" for the full flow.\n'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const sql = neon(process.env.POSTGRES_URL);
|
||||
|
||||
try {
|
||||
console.log('✅ Connecting to Neon database...');
|
||||
|
||||
// Drop all tables in correct order (due to foreign key constraints)
|
||||
console.log('🗑️ Dropping existing tables...');
|
||||
|
||||
|
||||
await sql`DROP TABLE IF EXISTS deck_cards CASCADE`;
|
||||
await sql`DROP TABLE IF EXISTS decks CASCADE`;
|
||||
await sql`DROP TABLE IF EXISTS collection_cards CASCADE`;
|
||||
|
|
@ -27,10 +50,9 @@ async function resetDatabase() {
|
|||
await sql`DROP TABLE IF EXISTS user_cards CASCADE`;
|
||||
await sql`DROP TABLE IF EXISTS cards CASCADE`;
|
||||
await sql`DROP TABLE IF EXISTS users CASCADE`;
|
||||
|
||||
|
||||
console.log('✅ Dropped all tables');
|
||||
|
||||
// Create tables
|
||||
await sql`
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
|
@ -138,13 +160,12 @@ async function resetDatabase() {
|
|||
`;
|
||||
console.log('✅ Created deck_cards table');
|
||||
|
||||
// Create admin user
|
||||
const bcrypt = require('bcryptjs');
|
||||
const hashedPassword = await bcrypt.hash('admin123', 12);
|
||||
|
||||
const hashedPassword = await bcrypt.hash(adminPassword, 12);
|
||||
|
||||
await sql`
|
||||
INSERT INTO users (email, password, role)
|
||||
INSERT INTO users (email, password, role)
|
||||
VALUES (${'admin@deckhearth.com'}, ${hashedPassword}, ${'admin'})
|
||||
ON CONFLICT (email) DO NOTHING
|
||||
`;
|
||||
console.log('✅ Created admin user');
|
||||
|
||||
|
|
@ -152,13 +173,11 @@ async function resetDatabase() {
|
|||
console.log('');
|
||||
console.log('📋 Database Details:');
|
||||
console.log(' Database: Neon PostgreSQL');
|
||||
console.log(' Admin User: admin@deckhearth.com');
|
||||
console.log(' Admin Password: admin123');
|
||||
|
||||
console.log(' Admin user ready (email: admin@deckhearth.com)');
|
||||
} catch (error) {
|
||||
console.error('❌ Database reset failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
resetDatabase();
|
||||
resetDatabase();
|
||||
|
|
|
|||
Loading…
Reference in a new issue