--- convoy: drop-public-setup brief_number: 1 depends_on: [] files: - scripts/setup-neon-db.js - README.md --- # Brief 1: Require `ADMIN_INITIAL_PASSWORD`; strip credentials from README ## Goal (1 sentence) Gate admin-user creation in `scripts/setup-neon-db.js` behind a required `ADMIN_INITIAL_PASSWORD` env var (fail-loud with an actionable message and exit code 1 if unset), remove the hardcoded `'admin123'` literal and the `console.log` lines that echo credentials to stdout, and replace the README's "Default Admin Account" section with a "First-time admin setup" paragraph that documents the new env var. ## Files in scope (do not edit anything else) - `scripts/setup-neon-db.js` — modified - `README.md` — modified ## Conventions to follow - **No-go-zones reading.** `.cursor/rules/no-go-zones.mdc` prohibits editing `scripts/setup-neon-db.js` for **schema** changes (DDL, columns, idempotency-of-DDL). This brief's change is **operational** — env-var gate plus two `console.log` removals, with zero changes to `CREATE TABLE` blocks, table shape, or the `ON CONFLICT` clause. The Architect explicitly recorded this distinction in `.convoys/drop-public-setup.md` § Decisions B. **Do not** treat the no-go-zones rule as banning operational hardening of this file. - **Match existing file style.** `scripts/setup-neon-db.js` uses `#!/usr/bin/env node` shebang and CommonJS `require('dotenv')` / `require('@neondatabase/serverless')` / `require('bcryptjs')`. The repo's `package.json` is `"type": "module"`, but converting CJS → ESM is **out of scope** for this brief (flagged for a separate follow-up convoy in `.convoys/drop-public-setup.md` § Anything flagged but not acted on). Keep `require()` everywhere; do not introduce `import` statements. - **Fail-loud-message shape, modeled on Brief 1 of `fix-auth-bypass`.** That brief's `lib/auth-secret.js::JWT_SECRET` throw is the template for the actionable-message shape (names the env var, points at `.env.local`, suggests a generation tip, mentions CI as an alternative). See `.convoys/fix-auth-bypass/brief-1-central-jwt-secret-helper.md` § "Acceptance criteria → `lib/auth-secret.js` (new)" for the verbatim message form. **Difference:** that brief throws at module load; this brief calls `console.error(...)` + `process.exit(1)` at function entry, because the script is a CLI tool, not an imported module — exiting with a non-zero code is the canonical CLI "fail loudly" signal. - **No new dependencies.** `bcryptjs` and `dotenv` are already in `dependencies`. No `package.json` change. - **No edits to** any file outside `files:` above. In particular: no `lib/`, no `pages/`, no `components/`, no `scripts/reset-db.js`, no `scripts/create-test-users.js`, no `TESTING_GUIDE.md`. Those three sibling files still reference `admin@tcgvault.com` / `admin123` but are explicitly out of scope per the convoy spec and the architect's flagged follow-ups. If you find yourself wanting to touch them, **stop and flag in PR description**; do not expand the diff. ## Acceptance criteria ### `scripts/setup-neon-db.js` - [ ] **Add the env-var check at the top of `setupNeonDatabase()`**, **before** the `const sql = neon(process.env.POSTGRES_URL);` line. The check must: - Read `process.env.ADMIN_INITIAL_PASSWORD` into a local `const adminPassword`. - If `adminPassword` is `undefined`, `null`, empty string, or only whitespace, write a clear error message to `console.error` and call `process.exit(1)`. **Do not** call `process.exit(1)` directly without first logging. - The error message body MUST name the env var, point at `.env.local`, give a generation suggestion, and mention the CI-secret alternative. Verbatim shape (the message body MAY be reworded for tone, but every claim MUST be present): ```js 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 setup 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); } ``` - [ ] **The check sits BEFORE the `neon(...)` call** so the script does not open a DB connection (and does not run any DDL) when the env var is missing. This is the documented contract — operators see the error and fix their env BEFORE touching the database. - [ ] **Replace `bcrypt.hash('admin123', 12)`** (currently line 131) with `bcrypt.hash(adminPassword, 12)`. The `12` cost factor is unchanged. - [ ] **Do not change `ON CONFLICT (email) DO NOTHING`** on the `INSERT INTO users` statement (line 136). This is intentional — re-running `setup-db` on an env that already has the admin row is a no-op for the password. Rotation is out of scope (Decision A — see convoy file). - [ ] **Delete the two credential-echo `console.log` lines** in the success summary (currently lines 144-145): ```js // before: console.log(' Admin User: admin@tcgvault.com'); console.log(' Admin Password: admin123'); // after: replace BOTH lines with a single line that does not echo credentials: console.log(' Admin user ready (email: admin@tcgvault.com)'); ``` Do **not** interpolate `adminPassword` into the log — that would put the chosen password into stdout (and into CI logs if setup ever runs there). The chosen password lives in the operator's env-var source only. - [ ] The script's existing CommonJS `require()` calls, transaction structure (none — each `sql\`\`` is its own connection per Neon HTTP semantics), and error handling (`try/catch` with `process.exit(1)` on failure) are **unchanged**. - [ ] No new `require()` imports. `bcryptjs` is already required inline at line 130 (move it to the top of the file ONLY if the linter complains; otherwise leave inline to keep the diff minimal). ### `README.md` - [ ] **Update the env-example block in step 3** (currently lines 44-52) of the Installation section. Add `ADMIN_INITIAL_PASSWORD` after `JWT_SECRET` with a comment. Final shape of that block: ```env POSTGRES_URL="postgresql://your-username:your-password@your-host/your-database" JWT_SECRET="" # Required for `npm run setup-db` — used once to hash the initial admin password. # Set in .env.local for local dev, or as a CI secret if you run setup from CI. ADMIN_INITIAL_PASSWORD="" # Optional — exercise the rate limiter locally. Without them, `lib/rate-limit.js` # warn-and-no-ops in dev. In production these are auto-provisioned by the # Vercel Upstash Marketplace integration. KV_REST_API_URL="https://.upstash.io" KV_REST_API_TOKEN="" ``` Keep the existing trailing line `` `JWT_SECRET` is **required** — `lib/auth-secret.js` throws at import time if it's unset. ``. Add a second sentence immediately after it: `` `ADMIN_INITIAL_PASSWORD` is **required** for `npm run setup-db` — the script exits with code 1 if it's unset. `` - [ ] **Replace the "🔐 Default Admin Account" section** (currently lines 115-119) with a "🔐 First-time admin setup" section. New copy (markdown body is suggestive, not verbatim — adjust prose to match repo voice, but every claim below MUST be present): ```markdown ## 🔐 First-time admin setup `npm run setup-db` creates a single admin user the first time it runs. The password is read from the `ADMIN_INITIAL_PASSWORD` environment variable; the script exits with code 1 (and does not open a database connection) if the variable is unset or empty. - **Local dev:** set `ADMIN_INITIAL_PASSWORD` in `.env.local` before running `npm run setup-db`. Use `openssl rand -base64 24` (or any other strong source) to generate the value. - **CI / Vercel:** set `ADMIN_INITIAL_PASSWORD` as a project secret if setup ever runs from CI. The env var is **only** read by the seed script; runtime auth uses the per-user password stored in the database. - **Admin email:** the seed creates `admin@tcgvault.com`. Change the password immediately after first login via the app's profile settings. > **Operators of envs that pre-date this change:** `npm run setup-db` is > idempotent (`ON CONFLICT (email) DO NOTHING`) — re-running it with > `ADMIN_INITIAL_PASSWORD` set will **not** rotate an existing admin row's > password. If your environment was set up before this change and still has > the weak default (`admin123`), rotate the password manually via the app > after logging in, or wait for the queued `rotate-default-admin` follow-up > convoy. ``` - [ ] **Remove every other occurrence of `admin123` in `README.md`.** Grep verification: `rg 'admin123' README.md` returns **zero** hits after this brief. - [ ] **Do NOT remove the line `**Admin Panel**: Manage cards and users`** under "## 🚀 Features" — that's a feature description, not a credential. - [ ] **Do NOT touch the "Database Schema" / "API Endpoints" / "Deployment" sections.** Out of scope. ### Repo-wide grep verification (run before opening PR) - [ ] `rg "'admin123'" --type js scripts/setup-neon-db.js` returns **zero** hits. - [ ] `rg 'admin123' README.md` returns **zero** hits. - [ ] `rg 'process\.env\.ADMIN_INITIAL_PASSWORD' --type js` returns exactly one hit — in `scripts/setup-neon-db.js`. - [ ] `rg 'admin123' --type js scripts/` returns hits in `scripts/reset-db.js` and `scripts/create-test-users.js`. **Leave those alone** — they are explicitly out of scope per the convoy spec (see § "Out of scope" in `.convoys/drop-public-setup.md` and § "Anything flagged but not acted on" in the architecture section). Document them in the PR description as known-but-deferred references. - [ ] `rg 'admin123' TESTING_GUIDE.md` still has one hit (line 7). Leave it; same follow-up bucket. ### Smoke (manual — no test runner for this brief; Decision C — C1) Run these in order and paste the relevant terminal output into the PR description: - [ ] **Fail-loud path.** In your shell, `unset ADMIN_INITIAL_PASSWORD` (or comment it out in `.env.local`), then run `npm run setup-db`. Expect: - Exit code is 1 (`echo $?` immediately after returns `1`). - Stderr contains the error message body (env var name, `.env.local` reference, `openssl rand -base64 24` suggestion, README pointer). - **Stdout does NOT contain `✅ Connecting to Neon database...`** — the check fires before the `neon(...)` call. - [ ] **Happy path.** Set `ADMIN_INITIAL_PASSWORD=temporary-strong-pw-for-smoke` in `.env.local` (or `export` it in the shell), then run `npm run setup-db`. Expect: - Exit code is 0. - Stdout includes `✅ Created admin user` (or the existing equivalent line if the row already exists — both are acceptable; `ON CONFLICT DO NOTHING` keeps the script idempotent). - Stdout does **NOT** contain the literal string `admin123` anywhere. - Stdout does **NOT** contain `temporary-strong-pw-for-smoke` (the chosen password) anywhere. - [ ] **End-to-end login.** With the env var still set, run `npm run dev`. From a second terminal: `curl -sX POST http://localhost:3000/api/auth/login -H 'content-type: application/json' -d '{"email":"admin@tcgvault.com","password":"temporary-strong-pw-for-smoke"}'`. Expect HTTP 200 + a JWT in the body **only if** the admin row was created by this run (i.e. the DB was empty for `admin@tcgvault.com` before step 2). If the row pre-existed with a different password (e.g. the weak `admin123` from before this convoy), expect 401 — that's the documented R1 / Decision A behavior (re-running setup-db does **not** rotate; that's manual rotation territory). Paste whichever outcome you got and note which case applies. - [ ] **Lint baseline.** `npm run lint` exits 0 (or matches the existing pre-PR baseline — pre-existing lint errors are fine; do not introduce new ones). - [ ] **Tests.** `npm run test:run` is green (no new tests added per Decision C; existing 16 auth tests should still pass — this brief does not touch any file they cover). ### Pre-merge operator checklist (paste into PR description) This is the human-side handoff. Reviewer confirms each item is acknowledged before merging: - [ ] **R1 callout:** "Existing deploys with the weak `admin123` hash are NOT rotated by this PR. If any deployed environment (production, staging, dev branches) currently has `admin@tcgvault.com / admin123` in its database, the operator must rotate the password manually after this merges — log in with the weak password, change it via profile settings, then verify the new hash. The queued `rotate-default-admin` follow-up convoy will land an idempotent rotation script if any real deploy still has the weak hash after manual triage." - [ ] **CI / Vercel env var:** "Before merging, confirm `ADMIN_INITIAL_PASSWORD` is set as a Vercel project secret on any branch that runs `npm run setup-db` from CI (today: none — this is a `package.json` script run manually). If/when a setup CI step is added, the secret MUST be in place first or the CI job will exit 1." - [ ] **Reviewer ran the fail-loud and happy-path smoke locally** OR has confirmed the PR description includes terminal output proving both paths. ### Out of scope (do not do these) - [ ] No edit to `scripts/reset-db.js`, `scripts/create-test-users.js`, `TESTING_GUIDE.md`. Sibling weak-credential references — convoy out-of-scope, architect-flagged follow-up. - [ ] No edit to `lib/auth-secret.js`. JWT secret model is `fix-auth-bypass` Brief 1's surface, already shipped. - [ ] No edit to `pages/api/auth/login.js` or `pages/api/auth/register.js`. CORS / rate-limit are `fix-auth-bypass` Brief 4's surface, already shipped (partial — `verify.js` deferred to `cors-tighten`). - [ ] No CommonJS → ESM conversion of `scripts/setup-neon-db.js`. Flagged for a separate convoy. - [ ] No idempotent rotation script under `scripts/migrations/`. Decision A defers this to the queued `rotate-default-admin` follow-up convoy. - [ ] No new `vitest` tests for the env-var path. Decision C — C1. - [ ] No `package.json` change. `bcryptjs` and `dotenv` are already installed. - [ ] No `AGENTS.md` or `.cursor/rules/*.mdc` updates. Doc-writer pass updates these AFTER the convoy lands. (Specifically: Gotcha #4 in `AGENTS.md` is the next doc-writer change; do not touch it in this PR.) - [ ] No `.github/workflows/*.yml` change. CI gates for forbidden endpoints are already in place from `fix-auth-bypass` Brief 3. ## Rationale (≤3 sentences) Gating the seed script behind a required env var converts the one remaining hardcoded credential in the source tree (the `'admin123'` bcrypt input) into operator-supplied input, while the README rewrite removes the same credential from the documentation surface — together they close P0 #3 from `.convoys/ship-readiness.md` and resolve `AGENTS.md` Gotcha #4. Keeping `ON CONFLICT (email) DO NOTHING` and deleting (rather than reformatting) the credential-echo `console.log` lines ensures the brief does not introduce a silent-rotation surprise (R4) or a stdout-leak surprise (R3) on top of the intended fix. Holding the no-go-zones reading at "operational change is allowed" — and documenting it explicitly in this brief — gives the next agent (likely doc-writer or whoever opens `rotate-default-admin`) a clear precedent for distinguishing operational hardening of `setup-neon-db.js` from the prohibited schema-edit case.