Files
Philip d4c82867d4 Fix guest signup not showing payment info after purchase
/api/settings was missing from the middleware public routes allowlist,
causing unauthenticated (guest) requests to be blocked before reaching
the route handler. The error was silently caught, leaving settings null
and hiding the amount owed, payment methods, and payment instructions.
Logged-in users were unaffected as their session token passed middleware.

Also update CLAUDE.md to reflect the WebSocket userId-based auth change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-18 08:26:42 -08:00

199 lines
12 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Super Bowl Squares — a web application that runs a 10x10 grid-based betting game for the Super Bowl. Players purchase squares, random numbers (0-9) are assigned to each row/column for AFC and NFC teams, and winners are determined by the last digit of each team's score at the end of each quarter.
This is a rewrite of a legacy PHP/MySQL application (preserved in `legacy/` for reference). The legacy app has significant security issues (SQL injection, plaintext passwords, no CSRF protection) and uses early-2000s HTML patterns.
## Tech Stack
- **Framework**: Next.js 14 (App Router) with TypeScript
- **Database**: PostgreSQL 16 via Prisma ORM
- **Auth**: NextAuth.js with JWT strategy and credentials provider
- **Styling**: Tailwind CSS with custom theme (dark mode, `primary`/`accent` color scales, glow shadows)
- **Real-time**: WebSocket server (ws) for live chat, mounted at `/ws/chat`
- **Deployment**: Docker Compose (app + postgres)
## Development Commands
```bash
# Start everything (build + run with Docker Compose)
docker compose up --build -d
# View logs
docker compose logs app --tail 50
docker compose logs app -f # follow
# Rebuild after code changes
docker compose up --build -d
# Stop
docker compose down
# Database operations (run from host with node_modules present)
npx prisma db push # sync schema to DB
npx prisma generate # regenerate client after schema changes
npx tsx prisma/seed.ts # seed default data
npm run db:migrate # deploy prisma migrations (production)
npm run db:generate # regenerate Prisma client (alias)
# Dev server (requires local postgres with DATABASE_URL pointing to it)
npm run dev # runs tsx server.ts -> server.js
```
**Note**: The app runs inside Docker where `DATABASE_URL` points to the `db` service. For local dev outside Docker, update `DATABASE_URL` to `postgresql://superbowl:superbowl@localhost:5432/superbowl`.
## Environment Variables
Required variables (see `.env.example`):
- `DATABASE_URL` — PostgreSQL connection string
- `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` — Database credentials
- `NEXTAUTH_SECRET` — Random secret for JWT signing (generate with `openssl rand -base64 32`)
- `NEXTAUTH_URL` — Full URL where app is deployed (e.g., `http://localhost:3000`)
## Architecture
### Custom Server (`server.js`)
Next.js runs behind a custom HTTP server that also handles WebSocket upgrades. This is required for the live chat and real-time grid update features. The server has two boot modes:
- **Dev mode** (`NODE_ENV !== 'production'`): Uses the standard `next()` API with `app.prepare()`, creates its own HTTP server, and adds WebSocket upgrade handling on `/ws/chat`.
- **Production standalone mode**: Monkey-patches `http.createServer` to intercept the HTTP server that Next.js's `startServer()` creates, injecting WebSocket upgrade handling for `/ws/chat` before Next.js registers its own upgrade handler. Reads the embedded `nextConfig` from `server.standalone.js` (saved during Docker build) and sets `__NEXT_PRIVATE_STANDALONE_CONFIG` env var so Next.js skips webpack loading.
The server also runs:
- Chat message broadcasting with blacklist filtering; user identity resolved by looking up `userId` (sent from client session) in the DB — the NextAuth session cookie is HttpOnly so the JWT cannot be read client-side
- `squares:changed``squares:refresh` broadcast for real-time grid updates
- Payment reminder scheduler (15-minute interval) that checks unconfirmed squares approaching grace period deadline
`server.ts` is just a dev shim that requires `server.js`.
**Critical Dockerfile detail**: The standalone build output includes its own `server.js`. The Dockerfile saves it as `server.standalone.js` then copies our custom `server.js` over it. Extra node_modules not included in standalone output (`ws`, `nodemailer`, `next-auth`, `jose`, `@panva`, `uuid`, `@babel`, `preact`, `oauth`, `openid-client`, `cookie`) are explicitly copied in the Dockerfile.
### Authentication Flow
- `src/lib/auth.ts` — NextAuth config with credentials provider (email/password, bcrypt)
- `src/middleware.ts` — Route protection: public routes (`/`, `/login`, `/register`, `/signup`, `/setup`, `/api/auth`, `/api/setup`), admin routes require ADMIN or VIEWER role, `/my-squares` requires any authenticated user
- `src/types/index.ts` — Augments NextAuth session/JWT types with `role` and `id`
- JWT tokens carry `role` (ADMIN/VIEWER/PLAYER) and `id` fields
### Database Pattern
- Prisma schema at `prisma/schema.prisma`
- Singleton pattern: `GameSettings`, `Score`, and `EmailSettings` use `id: "singleton"` — there's always exactly one row
- `prisma/seed.ts` — Seeds 100 squares (positions "00"-"99"), default settings, default score row, email settings, and 8 email templates (welcome, square_confirmation, square_confirmed, square_released, payment_reminder, numbers_assigned, game_results, custom). Uses upserts so it's idempotent. Email templates use `update: {}` to preserve manual edits (only creates, never overwrites).
- Shared Prisma client at `src/lib/prisma.ts` (global singleton to avoid connection exhaustion in dev)
### Page Structure
- `/` — Main page: 10x10 grid + chat window. Redirects to `/setup` if no admin exists.
- `/setup` — First-run admin account creation
- `/login`, `/register` — Auth pages
- `/signup?squares=01,02,...` — Square purchase form (guest or authenticated)
- `/my-squares` — Player's own squares (requires auth)
- `/admin/*` — Admin panel with sidebar nav (`layout.tsx` is a client component with role-based nav filtering)
Admin sub-pages: dashboard, squares, numbers, scores, balance, settings, users, email, chat, backup. Viewers are restricted from settings, users, and backup.
### API Routes (`src/app/api/`)
All API routes use `getServerSession(authOptions)` for auth checks. Pattern: GET for reads, POST for creates, PUT for full updates, PATCH for partial updates.
Key routes:
- `squares/` — GET (list all), POST (purchase), PATCH (admin: confirm/release/reserve/bulk-reserve/edit)
- `settings/` — GET (public), PUT (admin-only, handles payment methods separately)
- `numbers/` — POST generates random 0-9 shuffles for NFC/AFC axes
- `scores/` — PUT updates quarter scores and determines winners
- `backup/` — Export/import game data
- `upload/` — Image upload for team/SB logos
### Component Patterns
- Server components fetch data directly via Prisma (e.g., `page.tsx` for `/`)
- Client components use `'use client'` and fetch via API routes
- `src/components/Providers.tsx` wraps app with `SessionProvider` + `WebSocketProvider` + `ToastProvider`
- Grid components: `SquareGrid` (10x10 table with real-time WS refresh), `SquareCell` (individual cell), `GridHeader` (team logos/matchup banner/event info)
- `src/lib/ws.tsx` — React Context `WebSocketProvider` shares a single WebSocket connection app-wide. Exports `useWebSocket()` hook returning `{ messages, connected, sendMessage, deleteMessage, squaresVersion, notifySquaresChanged }`. The `squaresVersion` counter increments on `squares:refresh` events; components watch it to re-fetch grid data.
### Styling Conventions
- Dark theme throughout: `bg-gray-950` base, `bg-gray-900` cards
- Custom Tailwind classes in `globals.css`: `.btn-primary`, `.btn-secondary`, `.btn-danger`, `.btn-success`, `.input-field`, `.card`, `.card-elevated`
- Custom color scales: `primary` (blue), `accent` (purple), `success`, `warning`, `danger`
- Custom glow shadows: `shadow-glow-sm`, `shadow-glow`, `shadow-glow-lg`, `shadow-glow-green`, `shadow-glow-amber`
### Docker Setup
- `Dockerfile` — Multi-stage build: builder (npm install, next build, compile seed script) -> runner (standalone output, prisma CLI for runtime migrations). The runner stage saves the original standalone `server.js` as `server.standalone.js` then copies our custom `server.js` over it, and copies extra node_modules not included in standalone output.
- `docker-entrypoint.sh` — Runs `prisma db push`, seeds DB, then starts `node server.js`
- `docker-compose.yml` — Two services: `app` (port 3000) and `db` (postgres:16-alpine, port 5432, healthcheck)
### Next.js Configuration
`next.config.js` settings:
- `output: 'standalone'` — Minimal production build with embedded dependencies for Docker
- `serverComponentsExternalPackages: ['nodemailer']` — Prevents bundling nodemailer (requires native modules)
- `images: { unoptimized: true }` — Disables Next.js image optimization for simpler Docker deployment
### Real-time Updates
Two real-time channels share a single WebSocket connection per client:
- **Chat**: Messages are stored in DB and broadcast to all connected clients
- **Grid refresh**: When squares change (purchase, admin action), the acting client sends `{type: 'squares:changed'}` via WS. The server broadcasts `{type: 'squares:refresh'}` to all clients. Clients increment `squaresVersion` which triggers a re-fetch of `/api/squares`.
### Path Alias
`@/*` maps to `./src/*` (configured in `tsconfig.json`). Use `@/lib/prisma`, `@/components/ui/Button`, etc.
## Game Logic
- 100 squares in a 10x10 grid, positions "00" through "99" (row digit + column digit)
- Row = NFC team axis, Column = AFC team axis
- Numbers (0-9) are randomly shuffled independently for each axis; can only be generated when all 100 squares are claimed
- Winners: last digit of each team's quarter score maps to the grid number → intersecting square wins
- Four quarters: Q1 (first), Q2 (half), Q3 (third), Final — each with configurable payout percentage
- Max 10 squares per purchase submission
- Squares can be guest-purchased (name+email) or purchased by authenticated players
## Feature Requirements
### Role-Based Access (3 roles)
**Admin** (full control): manage all accounts, game settings (bet amount, teams, logos, payouts, payment methods, rules), generate numbers, edit squares, edit scores, email system, backup/restore, chat moderation
**Viewer** (limited admin — for co-commissioners): view balance sheet, edit squares, update scores, send emails, moderate chat
**Player** (authenticated participant): view own squares, purchase squares, live chat, change own name/password
### Email System
- Templates with `{{placeholder}}` variables (stored in DB, seeded with defaults)
- Configurable SMTP/SSL transport via `EmailSettings`
- `src/lib/email.ts``sendEmail()`, `renderTemplate()`, `getTransporter()` core functions
- `src/lib/autoEmail.ts` — Fire-and-forget automated emails triggered by game events:
- `sendWelcomeEmail` — on user registration (from `POST /api/users`)
- `sendPurchaseConfirmationEmail` — on square purchase (from `POST /api/squares`)
- `sendSquareConfirmedEmail` / `sendSquareReleasedEmail` — on admin confirm/release (from `PATCH /api/squares`)
- `sendNumbersAssignedEmails` — to all participants when numbers generated (from `POST /api/numbers`)
- `sendGameResultsEmails` — to all participants when final score entered (from `PUT /api/scores`)
- `checkPaymentReminders` — scheduled in server.js every 15 minutes, sends reminders 2 hours before grace period deadline
- Admin Send tab (`POST /api/email`) populates all template variables per-recipient from DB (squares, amount, payment info, winners)
- Available template variables: `{{name}}`, `{{email}}`, `{{squares}}`, `{{amountDue}}`, `{{commissioner}}`, `{{eventName}}`, `{{gameUrl}}`, `{{graceHours}}`, `{{paymentInstructions}}`, `{{paymentMethods}}`, `{{winners}}`, `{{rulesText}}`
### Live Chat
- WebSocket-based, embedded on front page (YouTube/Twitch live-chat style)
- Blacklist-based word filtering
- Admin/viewer can delete messages
### Backup/Restore
- Config-only or full (config + square/game data) export/import
## Testing
No testing framework is currently configured. To add tests, consider installing Jest or Vitest with React Testing Library.
## Assets
Team logos in `public/images/`: `afc-{team}.png` and `nfc-{team}.png` for all 32 NFL teams, plus conference logos, generic placeholders, Super Bowl event logos, and background images.