feat(seed): require ADMIN_INITIAL_PASSWORD env var; strip admin123 from README

Closes P0 #3 from .convoys/ship-readiness.md.

scripts/setup-neon-db.js:
  - Read ADMIN_INITIAL_PASSWORD env var at the top of setupNeonDatabase()
    before any DB connection. Fail loudly (process.exit(1)) with an
    actionable message if unset or empty.
  - Replace bcrypt.hash('admin123', 12) with bcrypt.hash(adminPassword, 12).
  - Delete the two console.log lines that echoed admin user + password to
    stdout (R3 - stdout leak into CI logs).
  - Keep ON CONFLICT (email) DO NOTHING unchanged. Re-running setup-db
    on an env with the admin row already present is a no-op for the
    password (R4 - silent rotation prevention). Rotation of existing
    weak-hash admin rows is out of scope (Decision A - queued for the
    rotate-default-admin follow-up convoy).

README.md:
  - Add ADMIN_INITIAL_PASSWORD to the install-step env-example block
    with a CI-secret note (and add KV_REST_API_URL/KV_REST_API_TOKEN
    for completeness; they're optional for local dev).
  - Replace the "Default Admin Account" section with "First-time
    admin setup", documenting the env var, openssl rand suggestion,
    and the operator rotation note for envs that predate this change.
  - Zero occurrences of 'admin123' remain in README.md (the operator
    rotation note refers to "the prior weak default" instead of naming
    the literal string, so grep verification A2 holds).

Decisions A1 (going-forward only), B (operational change allowed),
C1 (no vitest coverage - manual smoke in PR description) per
.convoys/drop-public-setup.md section Decisions.

Smoke output: see PR description.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Randall Stillwell 2026-05-23 14:57:31 -05:00
parent cd4ba35356
commit ba1022b5db
2 changed files with 40 additions and 7 deletions

View file

@ -44,6 +44,9 @@ A modern trading card game collection manager built with Next.js and Neon Databa
```env ```env
POSTGRES_URL="postgresql://your-username:your-password@your-host/your-database" POSTGRES_URL="postgresql://your-username:your-password@your-host/your-database"
JWT_SECRET="<generate with: openssl rand -hex 32>" JWT_SECRET="<generate with: openssl rand -hex 32>"
# 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="<generate with: openssl rand -base64 24>"
# Optional — exercise the rate limiter locally. Without them, `lib/rate-limit.js` # 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 # warn-and-no-ops in dev. In production these are auto-provisioned by the
# Vercel Upstash Marketplace integration. # Vercel Upstash Marketplace integration.
@ -51,6 +54,7 @@ A modern trading card game collection manager built with Next.js and Neon Databa
KV_REST_API_TOKEN="<your-upstash-rest-token>" KV_REST_API_TOKEN="<your-upstash-rest-token>"
``` ```
`JWT_SECRET` is **required**`lib/auth-secret.js` throws at import time if it's unset. `JWT_SECRET` is **required**`lib/auth-secret.js` throws at import time if it's unset.
`ADMIN_INITIAL_PASSWORD` is **required** for `npm run setup-db` — the script exits with code 1 if it's unset.
4. **Set up the database** 4. **Set up the database**
```bash ```bash
@ -112,11 +116,29 @@ tcg-vault/
└── .env.local # Environment variables └── .env.local # Environment variables
``` ```
## 🔐 Default Admin Account ## 🔐 First-time admin setup
After running the database setup: `npm run setup-db` creates a single admin user the first time it runs. The
- **Email**: admin@tcgvault.com password is read from the `ADMIN_INITIAL_PASSWORD` environment variable; the
- **Password**: admin123 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 prior weak default, rotate the password manually via the app
> after logging in, or wait for the queued `rotate-default-admin` follow-up
> convoy.
## 🤝 Contributing ## 🤝 Contributing

View file

@ -13,6 +13,18 @@ require('dotenv').config({ path: '.env.local' });
const { neon } = require('@neondatabase/serverless'); const { neon } = require('@neondatabase/serverless');
async function setupNeonDatabase() { async function setupNeonDatabase() {
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);
}
const sql = neon(process.env.POSTGRES_URL); const sql = neon(process.env.POSTGRES_URL);
try { try {
@ -128,7 +140,7 @@ async function setupNeonDatabase() {
// Create admin user // Create admin user
const bcrypt = require('bcryptjs'); const bcrypt = require('bcryptjs');
const hashedPassword = await bcrypt.hash('admin123', 12); const hashedPassword = await bcrypt.hash(adminPassword, 12);
await sql` await sql`
INSERT INTO users (email, password, role) INSERT INTO users (email, password, role)
@ -141,8 +153,7 @@ async function setupNeonDatabase() {
console.log(''); console.log('');
console.log('📋 Database Details:'); console.log('📋 Database Details:');
console.log(' Database: Neon PostgreSQL'); console.log(' Database: Neon PostgreSQL');
console.log(' Admin User: admin@tcgvault.com'); console.log(' Admin user ready (email: admin@tcgvault.com)');
console.log(' Admin Password: admin123');
console.log(''); console.log('');
console.log('🔧 Next Steps:'); console.log('🔧 Next Steps:');
console.log(' 1. Test the API endpoints'); console.log(' 1. Test the API endpoints');