* convoy: scope fix-layout-default-user (P0 #7 — Layout maintainer-email leak) The last remaining P0 ship-blocker from .convoys/ship-readiness.md. components/Layout.js line 562 defaults the user prop to a real email address (me@randallstillwell.com); any page that renders Layout without passing user explicitly impersonates the maintainer. Scope: components/Layout.js + audit of 17 pages that import Layout (grep-confirmed list in convoy file). Single PR likely. Auditor cohort skipped (no design-system, IA, or browser-smoke surface). Architect to address: - Q1: logged-out rendering branch design (navbar, mobile-nav, auth-only items treatment) - Q2: page audit triage into always-auth / public-or-auth / anonymous-allowed buckets - Q3: brief decomposition (single brief / 2 briefs in 1 PR / fan-out) - Q4: whether to add vitest coverage for the logged-out branch (recommend yes — small surface, high regression protection) Hard out-of-scope: branding (pick-a-name), auth-provider collapse (single-auth-provider), Layout god-component split (god-component-split). depends_on: bump-next-js (shipped), fix-auth-bypass (shipped), drop-public-setup (shipped) addresses: P0 #7 from .convoys/ship-readiness.md parent: ship-readiness Co-authored-by: Cursor <cursoragent@cursor.com> * architect(fix-layout-default-user): plan + briefs 1-2 (Layout fix + page audit) 2 briefs, single PR. ~12 files net (down from the 18 in the original scope — 10 of the 17 Layout-importing pages already pass user explicitly). Brief 1: components/Layout.js default user=null + Sign-in CTA branch in UserProfileDropdown when logged out. Adds first jsdom test in the repo at test/components/Layout.test.js (Decision D2) with 5 regression-lock assertions. devDeps: jsdom@^29, @testing-library/react@^16. Brief 2: page audit sweep — 7 pages need code changes: - Pass user={user} to Layout: scanner.js, deck-builder.js (×4), deck/[id].js (×3), decks.js (×3) - Replace page-level useState({email: 'me@...'}) → useState(null) + null-guards: profile.js, settings.js - Replace hardcoded const user = {email: 'me@...'} with useAuth(): card/[id].js Discovered second anti-pattern: profile.js, settings.js, card/[id].js seed page-level state with the maintainer email. Folded into Brief 2 since success metric "no real email address remains in any component default-prop" reads naturally to include page-level seed values. Decisions: A1 — Sign-in CTA replaces avatar+email+dropdown when user===null; hides auth-only dropdown (Profile/Settings/Logout/Admin); keeps public + community nav visible B — Per-page bucket assignment (10 already correct, 7 need fix); full per-page table with justification in convoy file C2 — Two briefs in one PR (Brief 1 = Layout + test; Brief 2 = page sweep depends on Brief 1). C1 buries the conceptual change under mechanical edits; C3 is over-orchestrated for this scope D2 — vitest lock-in; first jsdom test in repo; same negative-regression style as test/lib/permission-middleware.test.js (synthetic-admin shape). devDeps jsdom + @testing-library/react Risks tracked R1-R8. Biggest: R2 (useState(null) null-deref in 3 leaky pages — mitigated by audit-pass mandate + manual smoke). MobileNavigation deliberately NOT folded in: its user prop is dead code (never reads user.*); different bug class; cleanup queued separately to avoid scope expansion. Flagged-but-deferred: - 4 pages still import useAuth from lib/auth-context.js → single-auth-provider (queued P1 #9) - Layout headers still render "Deck Hearth" / "DH" branding → pick-a-name (queued P1 #12) - MobileNavigation dead user prop → cleanup-mobile-nav-dead-props or fold into god-component-split addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) parent: ship-readiness Co-authored-by: Cursor <cursoragent@cursor.com> * feat(layout): default user=null + Sign-in CTA when logged out (Brief 1 of fix-layout-default-user) Closes the source-side half of P0 #7 from .convoys/ship-readiness.md. The page-side sweep (Brief 2) follows in a separate commit. components/Layout.js: - Default user prop is now null (was hardcoded to { email: 'me@randallstillwell.com', role: 'user' }) - UserProfileDropdown renders a "Sign in" link to /login when user === null instead of the maintainer's email + auth-only menu items (Decision A1) - All user.* accesses guarded with optional chaining or null checks - useState hook stays above the new null-user early return to satisfy rules-of-hooks (boot-the-brief caught this on the first try; see AGENTS.md Gotcha #11.5) test/components/Layout.test.js (new): - First jsdom test in the repo (Decision D2) - 5 regression-lock assertions: no maintainer email ever rendered (prop omitted, prop=null), Sign-in link exists with href=/login, supplied email renders when prop is set, no "Guest" placeholder (locks A1 copy choice) - Mocks next/link, next/router (prefetch, replace, events, query), and theme-context.useTheme for jsdom safety under Next 16 package.json + package-lock.json: - Add jsdom@^29 and @testing-library/react@^16 to devDependencies - @testing-library/dom@^10 added explicitly (peer auto-install skipped it under npm 11; brief anticipated this fallback) vitest.config.js (deviation from brief — see PR description): - Add esbuild { loader: 'jsx', jsx: 'automatic' } so vitest can parse JSX in .js files. Required to import any React component written in the repo's Next.js pages-router .js convention (AGENTS.md Gotcha #9). The brief said "no change" to this file, but JSX-in-.js parsing is a hard prerequisite for the new test to import components/Layout.js — the alternatives (rename test to .test.jsx; rewrite test in React.createElement) either break the test glob or still hit the same Layout.js parse failure. Other tests are unaffected (they import non-JSX modules). Smoke output: see PR description. addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) Co-authored-by: Cursor <cursoragent@cursor.com> * feat(pages): pass user explicitly + null-guard leaky page seeds (Brief 2 of fix-layout-default-user) Closes the page-side half of P0 #7 from .convoys/ship-readiness.md. Brief 1 (commit ddf8fd2) handled the Layout-side fix. Per the architect's per-page bucket table (Decision B in .convoys/fix-layout-default-user.md), 7 pages needed code changes; the other 10 of 17 Layout-importing pages already pass `user` correctly. Pass user={user} to Layout (4 pages, 11 call sites): - pages/scanner.js (1 call) - pages/decks.js (3 calls) - pages/deck-builder.js (4 calls) - pages/deck/[id].js (3 calls) (All four still import useAuth from lib/auth-context.js — that's intentional and stays as-is until the single-auth-provider convoy collapses the three parallel auth surfaces.) Replace leaky page-level seed values with useState(null) + null guards (2 pages, R2 mitigation): - pages/profile.js: useState({email: 'me@...', role: 'user', ...}) → useState(null) + ?. on every sync user.* read + early-return guards in getDisplayName/getInitials + conditional render around the "Member since" block so formatDate(undefined) never runs - pages/settings.js: same pattern (single user.email reader guarded) Replace hardcoded const with useAuth from lib/use-auth.js (1 page): - pages/card/[id].js: const user = {email: 'me@...'} → const { user } = useAuth() (called unconditionally at the top of the component; rules-of-hooks safe) Verification: - grep 'me@randallstillwell.com' pages/ → 0 hits - 21/21 vitest tests pass (16 pre-existing + 5 from Brief 1) - npm run lint matches baseline (128 problems pre, 128 post; verified via git stash before/after) - Manual static read-through of every diff; ReadLints clean on the 7 files - Dev-server smoke: /cards anonymous returned HTTP 200 with 0 'me@randallstillwell' matches before the user's shared dev server became unresponsive mid-session (same dev-server-shared-by-user constraint flagged in Brief 1); interactive logged-in smoke is parent/operator gated Flagged-but-deferred (untouched per scope): - 4 pages still import useAuth from lib/auth-context.js → single-auth-provider (queued P1 #9) - components/MobileNavigation.js still receives dead user prop → cleanup-mobile-nav-dead-props (or fold into god-component-split) addresses: P0 #7 from .convoys/ship-readiness.md (last P0 ship-blocker) Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|---|---|---|
| .convoys | ||
| .cursor | ||
| .github | ||
| components | ||
| docs | ||
| lib | ||
| pages | ||
| public | ||
| scripts | ||
| styles | ||
| test | ||
| tests/smoke | ||
| .agent-context-manifest.yml | ||
| .gitignore | ||
| .npmrc | ||
| AGENTS.md | ||
| eslint.config.mjs | ||
| next.config.js | ||
| package-lock.json | ||
| package.json | ||
| postcss.config.js | ||
| README.md | ||
| tailwind.config.js | ||
| TESTING_GUIDE.md | ||
| vercel.json | ||
| vitest.config.js | ||
TCG Vault
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.mdGotcha #9) - Backend: Next.js API Routes
- Database: Neon PostgreSQL (serverless)
- Authentication: JWT with bcrypt (24-hour expiry;
lib/auth-secret.jsis the single source of truth forJWT_SECRET) - Rate limiting:
@upstash/ratelimiton/api/auth/login+/api/auth/register(5 attempts / 15 min per IP) - Testing: Vitest (unit); Playwright queued
- Styling: Tailwind CSS
- Deployment: Vercel
📦 Installation
-
Clone the repository
git clone <repository-url> cd tcg-vault -
Install dependencies
npm install -
Set up environment variables
cp .env.example .env.localUpdate
.env.localwith 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_SECRETis required —lib/auth-secret.jsthrows at import time if it's unset.ADMIN_INITIAL_PASSWORDis required fornpm run setup-db— the script exits with code 1 if it's unset. -
Set up the database
npm run setup-db -
Start development server
npm run dev
🗄️ Database Schema
The application uses the following tables:
users- User accounts and authenticationcards- Card information and metadatauser_cards- User's card collectionscollections- Named card collectionscollection_cards- Cards in collectionsdecks- Deck definitionsdeck_cards- Cards in decks
🔧 API Endpoints
Authentication
POST /api/auth/register- User registrationPOST /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 infix-auth-bypassBrief 3 (commitfc0dd73) and CI now blocks their reintroduction. Don't recreate them.
🚀 Deployment
This app is configured for deployment on Vercel:
- Connect your repository to Vercel
- Set environment variables in Vercel dashboard
- 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 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_PASSWORDin.env.localbefore runningnpm run setup-db. Useopenssl rand -base64 24(or any other strong source) to generate the value. - CI / Vercel: set
ADMIN_INITIAL_PASSWORDas 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-dbis idempotent (ON CONFLICT (email) DO NOTHING) — re-running it withADMIN_INITIAL_PASSWORDset 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 queuedrotate-default-adminfollow-up convoy.
🤝 Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
📄 License
MIT License - see LICENSE file for details