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>
|
||
|---|---|---|
| .. | ||
| migrations | ||
| add-card-columns.js | ||
| add-collaboration-features.js | ||
| add-collection-slugs.js | ||
| add-favorites-system.js | ||
| add-image-column.js | ||
| add-system-collection-column.js | ||
| add-updated-at-column.js | ||
| add-user-profile-columns.js | ||
| add-user-profile-fields.js | ||
| bulk-import-all.js | ||
| create-sample-cards.js | ||
| create-sample-collections.js | ||
| create-test-users.js | ||
| demote-admin-to-user.js | ||
| fix-lorcana-images.js | ||
| fix-user-cards-constraints.js | ||
| import-popular-sets.js | ||
| list-users.js | ||
| log-convoy-event.sh | ||
| promote-user-to-admin.js | ||
| README.md | ||
| reset-db.js | ||
| rotate-admin-password.js | ||
| seed-collections-alice-bob.js | ||
| seed-collections-with-cards.js | ||
| setup-neon-db.js | ||
| wt.sh | ||
Deck Hearth Bulk Import Scripts
This directory contains scripts for bulk importing TCG card data into the database.
Available Scripts
Card Import Scripts
1. import-popular-sets.js - Popular Sets Import
Imports the most popular and recent sets from all three TCGs (Magic, Pokemon, Lorcana).
Usage:
npm run import-popular
What it imports:
- Magic: The Gathering: ~100+ sets from Alpha to recent releases
- Pokemon: ~100+ sets from Base Set to current Scarlet & Violet
Estimated time: 2-4 hours (depending on API response times)
2. bulk-import-all.js - Complete Import
Imports ALL available sets from all TCGs (comprehensive import).
Usage:
npm run import-all
What it imports:
- Magic: The Gathering: 100+ sets (Alpha to current)
- Pokemon: 100+ sets (Base Set to current)
Estimated time: 4-8 hours (depending on API response times)
User Management Scripts
3. list-users.js - List All Users
Lists all users in the database with their roles and details.
Usage:
node scripts/list-users.js
Output: Shows user ID, email, role (admin/user), and creation date.
4. promote-user-to-admin.js - Promote User to Admin
Promotes a regular user to admin role.
Usage:
node scripts/promote-user-to-admin.js user@example.com
Requirements: User must be registered first.
5. demote-admin-to-user.js - Demote Admin to User
Demotes an admin back to regular user role.
Usage:
node scripts/demote-admin-to-user.js admin@example.com
Safety: Ensures at least one admin always remains in the system.
Database Management Scripts
6. setup-neon-db.js - Database Setup
Sets up the Neon PostgreSQL database with all required tables and creates the default admin user.
Usage:
node scripts/setup-neon-db.js
7. reset-db.js - Database Reset
Resets the database by dropping and recreating all tables.
Usage:
node scripts/reset-db.js
How It Works
- Sequential Import: Scripts import sets one by one to avoid overwhelming the APIs
- Error Handling: Failed imports are logged but don't stop the process
- Progress Tracking: Real-time console output shows progress
- Results Logging: Detailed results are saved to JSON files
- Rate Limiting: 1-second delays between imports to be respectful to APIs
Output Files
After running, you'll get timestamped JSON files with detailed results:
popular-sets-import-results-[timestamp].jsonbulk-import-results-[timestamp].json
These files contain:
- Success/failure status for each set
- Number of cards imported per set
- Error messages for failed imports
- Summary statistics
Prerequisites
- Server Running: Make sure your Next.js dev server is running (
npm run dev) - Database Setup: Ensure the database is initialized (
npm run setup-db) - Dependencies: All required packages are installed
Recommendations
For First-Time Setup
Start with the popular sets import:
npm run import-popular
This will give you a solid foundation with the most relevant cards.
For Complete Database
If you want everything, use the full import:
npm run import-all
For Ongoing Management
After the initial bulk import:
-
Weekly catalog sync (recommended): Vercel Cron hits
GET /api/cron/sync-catalogevery Monday 06:00 UTC whenCRON_SECRETis set in the Vercel project. The job discovers missing MTG + Pokémon sets and imports up to 3 per run (paced for upstream rate limits). Manual trigger:curl -H "Authorization: Bearer $CRON_SECRET" https://<your-deployment>/api/cron/sync-catalog -
Admin UI:
/admin/card-importfor one-off set imports (MTG + Pokémon). -
Lorcana: still manual via admin/scripts until dynamic set discovery lands.
Legacy manual path (still valid):
- Importing new sets as they release
- Selective imports of specific sets
- Monitoring import progress
Troubleshooting
Common Issues
- API Rate Limits: If you get rate limit errors, the scripts will continue but log failures
- Network Issues: Scripts will retry and continue with the next set
- Server Restart: If the server restarts, just restart the import script
Monitoring Progress
Watch the console output for:
- ✅ Successful imports with card counts
- ❌ Failed imports with error messages
- 📊 Summary statistics at the end
Stopping and Resuming
You can stop the script with Ctrl+C and restart it later. The scripts will start from the beginning, but the database will only store unique cards (no duplicates).
API Endpoints Used
POST /api/cards/import-mtg- Magic: The Gathering importsPOST /api/cards/import-pokemon- Pokemon imports
Data Sources
- Magic: The Gathering: Scryfall API
- Pokemon: PokemonTCG/pokemon-tcg-data on GitHub (sets + per-set JSON; images from linked CDNs). Optional override:
POKEMON_TCG_DATA_BASE_URL(defaults tomasterbranch raw URLs). The legacy Pokemon TCG API is no longer used by catalog sync.
Performance Notes
- Each set typically contains 100-400 cards
- Total database size after full import: ~50,000-100,000 cards
- Import speed: ~1 set per minute (with delays)
- Database storage: ~100-200MB after full import