TCG Vault - Trading Card Game Collection Management with OCR Scanning
Find a file
Randall Stillwell c40da7b83c convoy: scripts/rotate-admin-password.js — one-shot admin rotation (rotate-default-admin)
Closes the operator caveat from the `drop-public-setup` convoy: deployed
envs that ran `npm run setup-db` BEFORE `ff80753` (2026-05-22) still
carry the historical `admin123` bcrypt hash. The seed is idempotent
(`ON CONFLICT (email) DO NOTHING`), so re-running setup-db is a no-op
on existing rows.

## Design — D1: which option from the 3-option menu?

| Option | Picked? | Why |
|---|---|---|
| A. Close as no-op (defer rotation to manual app login) | No | Leaves a real-world residue if any pre-drop-public-setup env still exists — and an audit is harder than just shipping the script. |
| B. One-shot parameterized rotation script | **Yes** | Tightly scoped (~120 lines). Audit-trail-preserving (`updated_at` bump). Reusable for future rotations. No new auth surface in the app. |
| C. First-login forced password reset flow in the app | No | Right product answer, but heavier scope (new route, new flag column, UI work). Deferred as the queued `force-admin-password-reset-flow` convoy. |

## Script shape

`scripts/rotate-admin-password.js`:

- Reads `POSTGRES_URL` + `ADMIN_NEW_PASSWORD` from env (or `.env.local`).
- Optional `ADMIN_EMAIL` override; defaults to `admin@deckhearth.com`.
  Pass `admin@tcgvault.com` for envs that pre-date `pick-a-name`
  (squash `9abbab6`, 2026-05-24).
- Fail-loud-exits BEFORE opening any DB connection if:
  - `POSTGRES_URL` is unset
  - `ADMIN_NEW_PASSWORD` is unset or empty
  - `ADMIN_NEW_PASSWORD` is shorter than 12 chars
- Validates the target row EXISTS AND has `role = 'admin'` before
  touching it. Refuses to rotate non-admin rows even if `ADMIN_EMAIL`
  points at one. Refuses to rotate when multiple rows match (impossible
  given the UNIQUE(email) constraint, but checked anyway).
- Hashes with bcryptjs at 12 rounds — same as `setup-neon-db.js`.
- After UPDATE, re-fetches the row and runs `bcrypt.compare(newPassword,
  row.password_hash)`; exits non-zero if the compare fails (extremely
  unlikely, but catches silent UPDATE failures).
- NEVER echoes the password to stdout / stderr / shell history. The
  only output is the row id, email, role, and updated_at.

Same import shape as the existing `scripts/migrations/2026-05-24-rename-admin-email.js`
(ESM, `dotenv.config({ path: '.env.local' })`, `import { neon } from
'@neondatabase/serverless'`, tagged-template SQL) — keeps the "11
scripts/* using neon() directly" graveyard from gaining new patterns;
fits the `purge-neondatabase-serverless-fully` follow-up convoy's
existing audit shape.

## Out of scope

- Sibling test users (alice / bob in `scripts/create-test-users.js`) —
  dev fixtures, not real auth surfaces. Documented inline + in
  AGENTS.md Gotcha #4.
- First-login forced password reset flow — deferred as the queued
  `force-admin-password-reset-flow` convoy (it's the right product
  answer, but heavier scope than this hygiene PR).
- Email rotation (already handled by
  `scripts/migrations/2026-05-24-rename-admin-email.js`).

## Test plan

- [x] `node --check scripts/rotate-admin-password.js` — syntax OK
- [x] `npm run lint` — clean (1 pre-existing unrelated warning)
- [x] `npm run test:run` — 118 tests pass
- [ ] CI on this PR
- [ ] Operator-side smoke test (NOT covered by CI):
  - Set `ADMIN_NEW_PASSWORD=test-rotation-12chars` against a throwaway
    Neon branch DB, run the script, log in via the app with the new
    password, run the script again with a different password, log in
    again. Skip if there's no convenient throwaway DB.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 23:15:35 -05:00
.convoys convoy: scripts/rotate-admin-password.js — one-shot admin rotation (rotate-default-admin) 2026-06-12 23:15:35 -05:00
.cursor feat(design-system): Liquid Glass redesign portfolio — foundation + primitives + Layout (#95) 2026-06-03 20:12:33 -05:00
.github convoy: flip visual-diff to a hard merge gate (harden-visual-diff-gate brief 2/2) (#140) 2026-06-12 22:03:17 -05:00
components refactor(glass): migrate Button.secondary + Input + MobileNav off bespoke glass-surface (#131) 2026-06-05 06:23:55 -05:00
docs feat(design-system): Liquid Glass redesign portfolio — foundation + primitives + Layout (#95) 2026-06-03 20:12:33 -05:00
lib feat(design-system): redesign v2 #2 + #5 — sidebar pill, wordmark, Daily Ember (#103) 2026-06-04 10:59:44 -05:00
migrations ci: run migrations against Postgres service container in CI (#64) 2026-06-02 11:01:18 -05:00
pages refactor(styles): retire .card; migrate 7 consumers to .glass-panel (#122) 2026-06-04 15:30:29 -05:00
public Major redesign: Enhanced card display with particle effects, improved filters, and search functionality 2025-07-23 21:26:54 -05:00
scripts convoy: scripts/rotate-admin-password.js — one-shot admin rotation (rotate-default-admin) 2026-06-12 23:15:35 -05:00
styles refactor(glass): migrate Button.secondary + Input + MobileNav off bespoke glass-surface (#131) 2026-06-05 06:23:55 -05:00
test refactor(layout): migrate 3 floating popovers to .glass-panel-strong (#124) 2026-06-04 16:25:14 -05:00
tests convoy: flip visual-diff to a hard merge gate (harden-visual-diff-gate brief 2/2) (#140) 2026-06-12 22:03:17 -05:00
.agent-context-manifest.yml bootstrap: agent pipeline v0.5.0 + ship-readiness review 2026-05-23 02:31:26 -05:00
.gitignore feat(convoy-metrics): un-gitignore .metrics.jsonl + add CI gate on convoy PRs (#134) 2026-06-12 17:46:28 -05:00
.npmrc Fix build compatibility issues 2025-07-22 10:32:15 -05:00
AGENTS.md convoy: scripts/rotate-admin-password.js — one-shot admin rotation (rotate-default-admin) 2026-06-12 23:15:35 -05:00
eslint.config.mjs chore(lint): forbid require() in scripts/** under "type": "module" (#29) 2026-05-26 22:53:31 -05:00
next.config.js bump: next 15.4.3 -> 16.2.6, ESLint flat config (v9 fallback), typescript devDep 2026-05-23 02:31:26 -05:00
package-lock.json Purge direct @neondatabase/serverless dependency from operational scripts. (#57) 2026-06-02 00:42:11 -05:00
package.json Purge direct @neondatabase/serverless dependency from operational scripts. (#57) 2026-06-02 00:42:11 -05:00
playwright.config.js chore(docs): sweep stale visual-baseline references + queue gate-hardening convoy (#135) 2026-06-12 20:30:34 -05:00
postcss.config.js fix(lint): clear lib/config baseline and make CI lint blocking. (#63) 2026-06-02 01:03:42 -05:00
README.md feat(infra): adopt node-pg-migrate + backfill initial schema migration (#32) 2026-05-26 23:01:58 -05:00
tailwind.config.js fix(lint): clear lib/config baseline and make CI lint blocking. (#63) 2026-06-02 01:03:42 -05:00
TESTING_GUIDE.md fix(scripts): require TEST_USERS_PASSWORD + purge weak literals from test-user helpers (#27) 2026-05-26 22:53:24 -05:00
vercel.json Add admin panel button to trigger catalog sync. (#50) 2026-05-28 09:18:01 -05:00
vitest.config.js fix(layout+pages): default user=null + page audit sweep (P0 #7) (#15) 2026-05-24 14:31:37 -05:00

Deck Hearth

A modern trading card game collection manager built with Next.js and Neon Database.

🚀 Features

  • Card Management: Track your MTG, Pokémon, and Lorcana cards
  • Collection Organization: Create and manage card collections
  • Deck Building: Build and share decks
  • Authentication: Secure user accounts with JWT
  • Admin Panel: Manage cards and users
  • Real-time Pricing: Track card values

🛠️ Tech Stack

  • Frontend: Next.js 16 (Pages router), React 18, JavaScript (TypeScript is a devDep only — see AGENTS.md Gotcha #9)
  • Backend: Next.js API Routes
  • Database: Neon PostgreSQL (serverless)
  • Authentication: JWT with bcrypt (24-hour expiry; lib/auth-secret.js is the single source of truth for JWT_SECRET)
  • Rate limiting: @upstash/ratelimit on /api/auth/login + /api/auth/register (5 attempts / 15 min per IP)
  • Testing: Vitest (unit); Playwright queued
  • Styling: Tailwind CSS
  • Deployment: Vercel

📦 Installation

  1. Clone the repository

    git clone <repository-url>
    cd tcg-vault
    
  2. Install dependencies

    npm install
    
  3. Set up environment variables

    cp .env.example .env.local
    

    Update .env.local with your Neon database URL and a real JWT secret:

    POSTGRES_URL="postgresql://your-username:your-password@your-host/your-database"
    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`
    # warn-and-no-ops in dev. In production these are auto-provisioned by the
    # Vercel Upstash Marketplace integration.
    KV_REST_API_URL="https://<your-upstash-host>.upstash.io"
    KV_REST_API_TOKEN="<your-upstash-rest-token>"
    

    JWT_SECRET is requiredlib/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

    npm run setup-db
    

    This applies every pending migration under migrations/ (via node-pg-migrate) and then seeds the initial admin user. To run just the migration step without seeding, use npm run migrate up.

  5. Start development server

    npm run dev
    

🧱 Schema changes (post-migration-tool convoy)

Schema is now managed by node-pg-migrate. New migrations live under migrations/ at the repo root (the legacy scripts/migrations/ placeholder is preserved for the one pre-existing dated script and is not used going forward).

# 1. Generate a new migration file (JS template, timestamp-prefixed)
npm run migrate create add-foo-column -- -j js

# 2. Edit the generated file under migrations/<timestamp>_add-foo-column.js
#    Put your DDL in up(); write a real down() if rollback is safe.

# 3. Apply it locally (POSTGRES_URL from .env.local)
npm run migrate up

# 4. Commit the migration file + any docs/SCHEMA_MAP.md updates together.

Onboarding a new env is now exactly npm install → seed .env.localnpm run setup-db (which chains npm run migrate up and then seeds the admin user).

Do not edit historical scripts/add-*.js / scripts/fix-*.js / scripts/seed-*.js — those are already-run, append-only jobs. They are preserved for the audit trail; new column / constraint work goes through a migration file instead.

🗄️ Database Schema

The application uses the following tables:

  • users - User accounts and authentication
  • cards - Card information and metadata
  • user_cards - User's card collections
  • collections - Named card collections
  • collection_cards - Cards in collections
  • decks - Deck definitions
  • deck_cards - Cards in decks

🔧 API Endpoints

Authentication

  • POST /api/auth/register - User registration
  • POST /api/auth/login - User login

Admin

  • GET /api/admin - Admin panel data

Health Check

  • GET /api/health - Application health

Note: Earlier revisions of this README also listed GET /api/test-db (and three other unauthenticated dev endpoints: /api/simple, /api/test-auth, /api/setup-database). All four were deleted in fix-auth-bypass Brief 3 (commit fc0dd73) and CI now blocks their reintroduction. Don't recreate them.

🚀 Deployment

This app is configured for deployment on Vercel:

  1. Connect your repository to Vercel
  2. Set environment variables in Vercel dashboard
  3. Deploy automatically on push to main branch

📁 Project Structure

tcg-vault/
├── pages/                 # Next.js pages and API routes
│   ├── api/              # API endpoints
│   │   ├── auth/         # Authentication routes
│   │   └── admin/        # Admin routes
│   ├── _app.js           # App wrapper
│   └── index.js          # Home page
├── lib/                  # Utility libraries
│   └── database.js       # Database adapter
├── scripts/              # Database setup scripts
├── public/               # Static assets
└── .env.local           # Environment variables

🔐 First-time admin setup

npm run setup-db first applies every pending migration (via npm run migrate up), then creates a single admin user. 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@deckhearth.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.

Operators of envs that pre-date the pick-a-name convoy (2026-05-24): the admin row was renamed from admin@tcgvault.com to admin@deckhearth.com. Run node scripts/migrations/2026-05-24-rename-admin-email.js once after deploy to UPDATE any existing @tcgvault.com user rows (the admin row, plus alice/bob if npm run create-test-users was ever run). Re-running the migration after the first run is idempotent and prints "Nothing to migrate." Verify post-migration with psql $POSTGRES_URL -c "SELECT email FROM users WHERE email LIKE '%@tcgvault.com'" — expect zero rows.

🤝 Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Submit a pull request

📄 License

MIT License - see LICENSE file for details