101 lines
6.7 KiB
Markdown
101 lines
6.7 KiB
Markdown
# 2026-05-02 — Auth.js + Admin Shell
|
|
|
|
> Companion to `Initial.md`. Predecessor: `2026-05-01-end-to-end.md`. Closes Step 5a of `Initial.md` §9.
|
|
|
|
## Milestone
|
|
|
|
The practice has its first usable web surface: admin can sign in via magic-link email, see today's confirmed bookings, and sign out. Auth.js v5 is wired through to the same SMTP transport we built for booking confirmations — Mailpit catches sign-in emails in dev, Resend will catch them in prod with one config change.
|
|
|
|
## What's verified end-to-end
|
|
|
|
In a real browser-equivalent flow (curl + Mailpit API):
|
|
|
|
1. Anonymous `GET /admin` → `307` redirect to `/login`
|
|
2. Submit `email=admin@touchbase.local` to `/api/auth/signin/nodemailer` → magic link delivered to Mailpit (subject "Sign in to localhost:3000")
|
|
3. Click magic link from email → JWT session cookie set, redirect to `/`
|
|
4. `GET /admin/bookings` with the cookie → renders the booking table (1 row from earlier `book-on-behalf` run: Tue May 5 10:00 AM, Alex Park, 60-minute Swedish, Mei Tanaka, Room 1 — Sunset, CONFIRMED)
|
|
|
|
Plus: `pnpm test` 64/64, `pnpm lint` clean, `pnpm exec tsc --noEmit` clean.
|
|
|
|
## What landed
|
|
|
|
| Path | Role |
|
|
|---|---|
|
|
| `src/auth.ts` | Auth.js v5 config: PrismaAdapter, Nodemailer provider via SMTP, JWT sessions, role-aware session/JWT callbacks |
|
|
| `src/app/api/auth/[...nextauth]/route.ts` | Required Auth.js route handler |
|
|
| `src/app/page.tsx` | Marketing-thin home with link to staff sign-in |
|
|
| `src/app/login/page.tsx` | Magic-link request form (server action) |
|
|
| `src/app/login/check-email/page.tsx` | Post-submission landing page |
|
|
| `src/app/admin/layout.tsx` | Auth-required layout; redirects to `/login` if no session, shows 403 if not ADMIN; header with email + sign-out |
|
|
| `src/app/admin/page.tsx` | Redirects to `/admin/bookings` |
|
|
| `src/app/admin/bookings/page.tsx` | Server-rendered table of upcoming HOLD/CONFIRMED bookings, times in `APP_TZ` |
|
|
| `prisma/schema.prisma` | `User.image String?` (Auth.js convention) + `VerificationToken` model |
|
|
| `prisma/migrations/20260501231153_auth/` | Migration |
|
|
| `.env` | `AUTH_SECRET` set |
|
|
|
|
## Decisions ratified
|
|
|
|
| Decision | Resolution |
|
|
|---|---|
|
|
| Auth.js version | v5 beta (5.0.0-beta.31). v4 stable doesn't support the App Router patterns we want |
|
|
| Session strategy | **JWT**, not database. Reason: simpler — no Account/Session tables needed, no extra DB roundtrip per request |
|
|
| Magic-link expiry | 24h (Auth.js default). Acceptable for staff |
|
|
| Email provider for auth | Same Nodemailer SMTP transport as our booking confirmations. No separate config |
|
|
| Mailpit AUTH workaround | Mailpit advertises `AUTH PLAIN`; nodemailer attempts it; without creds it errors. Fix: pass `auth: { user: "dev", pass: "dev" }` (Mailpit is configured with `MP_SMTP_AUTH_ACCEPT_ANY=1`) |
|
|
| Role refresh in JWT callback | On subsequent calls (no `user`), re-fetch role from DB if missing. Tradeoff: tiny extra query per request, but role changes apply on next request without re-login |
|
|
| `/admin/bookings` is server-rendered with `dynamic = "force-dynamic"` | Reason: bookings change continuously; static caching would lie. Cheap enough at our scale |
|
|
| Time formatting in admin table | `Intl.DateTimeFormat` with `APP_TZ` (America/Detroit). No client JS needed |
|
|
| Sign-out via server action with `signOut({ redirectTo: "/" })` | Reason: stays consistent with sign-in pattern; no client component needed |
|
|
| `session.user.id` and `session.user.role` typing | Module augmentation in `src/auth.ts` for `next-auth` and `next-auth/jwt`. Required `import type {} from "next-auth/jwt"` to force module loading before augmentation |
|
|
|
|
## Gotchas hit
|
|
|
|
### 1. Mailpit SMTP AUTH advertisement
|
|
nodemailer attempted `AUTH PLAIN` because Mailpit advertises it; threw "Missing credentials for PLAIN" when given `auth: undefined`. Resolution above. Same code path in `src/lib/email.ts` (booking confirmations) doesn't hit this because those tests run after Mailpit-bound env is set, but the prod path with real SMTP credentials wouldn't have the same issue regardless. Worth a note: setting `auth: { user: "dev", pass: "dev" }` works for Mailpit and harmless if real creds are set later via env.
|
|
|
|
### 2. Module augmentation for `next-auth/jwt`
|
|
TypeScript couldn't find `next-auth/jwt` for the `declare module` block until we added `import type {} from "next-auth/jwt"` to force the module to be loaded. Subtle but well-known TS quirk with submodule augmentation.
|
|
|
|
### 3. `PrismaAdapter` typing vs Prisma 7's generated client
|
|
`PrismaAdapter(db)` complained because Prisma 7's client generic doesn't match Auth.js's adapter expectation. Cast as `db as any` with an inline ESLint disable. Auth.js adapter doesn't actually use the unmatched types at runtime; this is purely a typing gap that may close as either side updates.
|
|
|
|
### 4. Workspace-root warning from Next.js
|
|
A stray `/Users/noise/package-lock.json` made Next.js ambiguous about the project root. Visible warning at startup; not blocking. Worth setting `turbopack.root` in `next.config.ts` later, or just removing the offending lockfile.
|
|
|
|
## Open questions still unresolved
|
|
|
|
1. Customer-visible brand name (TouchBase or other) — affects email sender/subject lines
|
|
2. Currency — USD assumed
|
|
3. Stripe account ownership — needed when 5c lands
|
|
|
|
## Roadmap status (`Initial.md` §9)
|
|
|
|
1. Spike — done 2026-04-30
|
|
2. Schema + seed — done 2026-05-01
|
|
3. Availability algorithm — done 2026-05-01
|
|
4. First end-to-end story (admin booking + email) — done 2026-05-01
|
|
5. Public self-booking → Stripe → reminders
|
|
- **5a Auth.js + admin shell — done 2026-05-02 (this session)**
|
|
- 5b Admin "create booking" page (slot picker + form using existing pipeline) — recommended next
|
|
- 5c Public booking page (CUSTOMER role, magic-link signup, slot search + checkout)
|
|
- 5d Stripe deposit flow + webhook
|
|
- 5e Email reminders (pg-boss scheduled jobs)
|
|
|
|
## Recommended next step
|
|
|
|
**5b — admin "create booking" page**. Wraps the `book-on-behalf` CLI in a UI: pick service, pick customer (search), pick a slot from `findSlots`, submit. Reuses the loader + `createHold` + email pipeline directly. ~1 day. After this lands, the practice has everything needed to take phone bookings without leaving the browser.
|
|
|
|
Then 5c (public self-booking page) becomes mostly a "remove the customer-search step + add public-facing styling" exercise on top of the admin form — most of the slot-picker UI carries over.
|
|
|
|
## How to resume
|
|
|
|
```bash
|
|
cd /Users/noise/Documents/code/touchbase
|
|
docker-compose up -d postgres mailpit
|
|
pnpm db:seed # admin@touchbase.local exists in seed
|
|
pnpm dev # http://localhost:3000
|
|
# Open http://localhost:3000/login → submit admin@touchbase.local
|
|
# Open http://localhost:8025 → click magic link
|
|
# Lands on http://localhost:3000/admin/bookings
|
|
```
|