7cd0603cfb
BREAKING: dev_session cookie bypass removed. Admin access now requires a real Auth.js v5 session (Google OAuth in production). Provision users by inserting into users + tenant_users tables. New in this commit: - db/migrations/0001_init.sql: 18-table SaaS schema with RLS (tenants, users, tenant_users, plans, add_ons, subscriptions, tenant_add_ons, products, product_images, stops, customers, orders, order_items, brand_settings, email_templates, campaigns, files, audit_log) - db/schema/: Drizzle TypeScript mirror of every table - db/client.ts: withTenant() / withPlatformAdmin() query wrappers that set Postgres GUCs (app.current_tenant_id, app.platform_admin) for RLS enforcement. Never query a tenant-scoped table without one. - db/seed.ts: seeds 3 plans, 6 add-ons, 2 tenants (Tuxedo, Indian River Direct), brand_settings, sample products/stops/customers - scripts/migrate.js: applies migrations in lexical order with tracking - scripts/db-reset.js: drops + recreates DB, runs migrate + seed - DATABASE_URL now uses rc_app (non-superuser, NOBYPASSRLS). RLS is enforced even for the app user. DATABASE_ADMIN_URL for migrations. - src/lib/admin-permissions.ts: getAdminUser() reads Auth.js session, looks up user + tenant in Postgres. brand_id kept as alias for backward compat. - src/middleware.ts: Auth.js-only route protection, dev_session gone - src/app/login/LoginClient.tsx: Google OAuth only, no demo mode - src/components/admin/AdminSidebar.tsx + AdminHeader.tsx: signOutAction replaces supabase signout - @/db/* path aliases in tsconfig.json + vitest.config.ts - drizzle.config.ts added - db/auth_schema.sql removed (was a stub; replaced by real schema) - src/app/api/dev-login/route.ts deleted - tests: updated to remove dev_session coverage
205 lines
8.4 KiB
Markdown
205 lines
8.4 KiB
Markdown
# YOLO Auth Migration — Final Report
|
||
|
||
**Date:** 2026-06-06 → 2026-06-07
|
||
**Mission:** Stand up a working Auth.js v5 + Google sign-in flow, then rip out
|
||
all Supabase references from the auth/admin path.
|
||
|
||
---
|
||
|
||
## What was built
|
||
|
||
### 1. Auth.js v5 (NextAuth) integration — DONE
|
||
|
||
- **`src/lib/auth.ts`** — Auth.js v5 config. Google is the only provider
|
||
(no Supabase-backed Credentials provider). Sessions are JWTs; no DB
|
||
adapter. Exports `handlers`, `auth`, `signIn`, `signOut`.
|
||
- **`src/lib/admin-permissions.ts`** — `getAdminUser()` resolves the
|
||
current admin. Three branches in precedence order:
|
||
1. `NEXT_PUBLIC_USE_MOCK_DATA=true` → platform_admin dev shim
|
||
2. `dev_session` cookie → matching dev shim (platform/brand/store)
|
||
3. Auth.js session → `null` (no DB lookup yet — see Follow-ups)
|
||
- **`src/actions/auth-actions.ts`** — `signInWithGoogle()` and
|
||
`signOutAction()`. `signInWithPassword` was removed.
|
||
- **`src/app/login/LoginClient.tsx`** + **`page.tsx`** — Login page
|
||
renders "Continue with Google" (when configured) and three demo
|
||
buttons (Platform / Brand / Store Employee) via `?demo=1`. Email /
|
||
password form and "Forgot password" are gone.
|
||
- **`src/app/api/auth/[...nextauth]/route.ts`** — Auth.js route handler.
|
||
|
||
### 2. Direct Postgres connection (`src/lib/db.ts`) — DONE
|
||
|
||
- Single shared `pg.Pool` with lazy initialization. Throws a clear
|
||
error if `DATABASE_URL` is not set.
|
||
- Exports `getPool()`, a `pool` proxy alias, `query<T>()` helper, and
|
||
`withTx()` for transactions.
|
||
- Tuned for Vercel/serverless: 10 max conns, 30s idle, 10s connect
|
||
timeout. All overridable via `PG_POOL_*` env vars.
|
||
|
||
### 3. Server actions → `pg` (no Supabase) — DONE
|
||
|
||
- **`src/actions/admin/users.ts`** — Full rewrite. `getAdminUsers`,
|
||
`createAdminUser`, `updateAdminUser`, `deleteAdminUser`,
|
||
`setMustChangePassword`, `getBrands` now hit Postgres directly.
|
||
`sendPasswordResetEmail` returns a stub error (no auth service
|
||
anymore — the function is preserved for call-site compatibility).
|
||
- The `dev_session` cookie continues to be the demo-flow bypass.
|
||
|
||
### 4. Cleanup — DONE
|
||
|
||
- **`src/actions/admin/force-login.ts`** — Deleted (the
|
||
Emergency-Force-Login page and its `/api/force-admin` route were
|
||
removed in the previous session).
|
||
- **`src/actions/admin/users.ts`** — Removed the `DEV_FORCE_UID`
|
||
magic value, the `rc_auth_uid` cookie reads (×3), the
|
||
`headerStore` lookup, the `getServiceClient` / `getAuthClient` /
|
||
`callRpcWithAuth` helpers, and the `devListAdminUsers` auto-provision
|
||
branch. All Supabase imports gone.
|
||
- **`src/app/admin/me/AdminMeClient.tsx`** — Profile / email-change
|
||
handlers neutralized (return "contact a platform admin" — the
|
||
Supabase auth backend that backed them is gone).
|
||
- **`src/middleware.ts`** — `dev_session` auto-login preserved
|
||
(issues a `platform_admin` cookie on unauthed `/admin` so the rest
|
||
of the admin shell renders). `auth()` wrapper in place.
|
||
|
||
### 5. Test infrastructure — DONE
|
||
|
||
- **Vitest 2.1.9** with `@vitejs/plugin-react`, jsdom, manual
|
||
`@/* → src/*` alias. No `vite-tsconfig-paths` (ESM/require
|
||
incompatibility).
|
||
- **`tests/setup.ts`** — Mocks `next/navigation` (not in jsdom).
|
||
- **`tests/unit/getAdminUser.test.ts`** — 12 tests covering
|
||
dev_session, mock-data, Auth.js session, defensive error handling.
|
||
- **`tests/unit/auth-actions.test.ts`** — 2 tests for
|
||
`signInWithGoogle` + `signOutAction`.
|
||
- Both test files stub `server-only` with `vi.mock("server-only", () => ({}))`.
|
||
- **Playwright config** — `webServer` block for `npm run dev`,
|
||
`PLAYWRIGHT_URL` override, `reuseExistingServer: !process.env.CI`.
|
||
- **`tests/login/login-flow.spec.ts`** — Login form rendering +
|
||
demo mode (3 roles) tests.
|
||
- **`tests/e2e/auth.spec.ts`** — Auth.js endpoints, middleware
|
||
redirect behavior, /admin load with dev_session, logout.
|
||
|
||
### 6. `import "server-only"` markers — DONE
|
||
|
||
- Added to `src/lib/auth.ts`, `src/lib/admin-permissions.ts`,
|
||
`src/lib/db.ts`, `src/actions/auth-actions.ts`,
|
||
`src/actions/admin/users.ts`.
|
||
- ⚠️ In `"use server"` files, the directive must be first
|
||
(`"use server";` on line 1, then `import "server-only";`). The dev
|
||
build surfaced this; fixed in `auth-actions.ts` and `users.ts`.
|
||
|
||
---
|
||
|
||
## Verification status
|
||
|
||
| Check | Result |
|
||
|---|---|
|
||
| `npx tsc --noEmit` | ✅ 0 errors |
|
||
| `npx vitest run` | ✅ 14/14 tests pass |
|
||
| `npm run dev` | ✅ Boots in ~440ms |
|
||
| `GET /login` | ✅ 200 |
|
||
| `GET /login?demo=1` | ✅ 200 |
|
||
| `GET /api/auth/providers` | ✅ 200 (empty list — Google not configured) |
|
||
| `GET /` | ✅ 200 |
|
||
| `GET /admin` (unauthed) | ✅ 307 → `/admin?demo=1` + `dev_session` cookie |
|
||
| `GET /admin?demo=1` (with cookie) | ✅ 200 (admin layout renders) |
|
||
|
||
---
|
||
|
||
## Files changed
|
||
|
||
```
|
||
M src/actions/admin/users.ts # full rewrite, pg-only
|
||
M src/actions/auth-actions.ts # signInWithPassword removed
|
||
M src/app/admin/me/AdminMeClient.tsx # handlers stubbed
|
||
M src/app/login/LoginClient.tsx # email/password form removed
|
||
M src/app/login/page.tsx # passes hasGoogle prop
|
||
M src/lib/admin-permissions.ts # Supabase REST calls removed
|
||
M src/lib/auth.ts # Credentials provider removed
|
||
M tests/unit/auth-actions.test.ts # signInWithPassword tests removed
|
||
M tests/unit/getAdminUser.test.ts # reflects no-DB behavior
|
||
```
|
||
|
||
Plus deletions and creations recorded in the prior session.
|
||
|
||
---
|
||
|
||
## Follow-ups (the bigger fish)
|
||
|
||
The user's directive was: "git rid of all supabase references, we are
|
||
not using it for login or anything anymore." The auth/admin path is
|
||
now Supabase-free. **33 files** still import from Supabase — they use
|
||
it for **data fetching** (`supabase.from(...).select(...)`), not auth.
|
||
|
||
**Migrating those to `pg` is a follow-up.** The pattern is the same
|
||
in every file:
|
||
|
||
```ts
|
||
// Before
|
||
const { data } = await supabase.from("products").select("*").eq("brand_id", id);
|
||
|
||
// After
|
||
import { query } from "@/lib/db";
|
||
const { rows } = await query<ProductRow>(
|
||
"SELECT * FROM products WHERE brand_id = $1",
|
||
[id],
|
||
);
|
||
```
|
||
|
||
### 33 files still importing Supabase
|
||
|
||
```
|
||
src/app/admin/page.tsx, products/[id]/page.tsx, products/page.tsx,
|
||
orders/page.tsx, stops/[id]/page.tsx, stops/new/page.tsx,
|
||
stops/page.tsx, reports/page.tsx, taxes/page.tsx,
|
||
settings/billing/page.tsx, settings/integrations/page.tsx,
|
||
settings/shipping/page.tsx
|
||
src/app/tuxedo/page.tsx, about/page.tsx, contact/ContactClientPage.tsx,
|
||
stops/[slug]/page.tsx
|
||
src/app/indian-river-direct/page.tsx, faq/FAQClientPage.tsx,
|
||
contact/ContactClientPage.tsx, stops/[slug]/page.tsx
|
||
src/app/api/tuxedo/schedule-pdf/route.ts,
|
||
src/app/api/indian-river-direct/schedule-pdf/route.ts
|
||
src/app/reset-password/page.tsx, src/app/test/page.tsx
|
||
src/components/admin/AdminHeader.tsx, AdminSidebar.tsx
|
||
src/lib/supabase.ts, src/lib/supabase/server.ts # the client modules
|
||
src/actions/wholesale-auth.ts, reset-admin.ts, audit.ts
|
||
src/actions/ai/preferences.ts
|
||
```
|
||
|
||
### Additional follow-ups
|
||
|
||
1. **Provision a `pg` pool in production.** `DATABASE_URL` must be
|
||
set in the hosting dashboard (same env var as
|
||
`supabase/push-migrations.js` uses).
|
||
2. **Apply migration 204** if not already applied — adds
|
||
`email`, `auth_provider`, `auth_subject` columns to `admin_users`
|
||
and the `get_admin_user_for_session` RPC.
|
||
3. **Wire `getAdminUser()`'s Auth.js session branch** to call
|
||
`get_admin_user_for_session` via `pg` once a `DATABASE_URL` is
|
||
configured. Today it returns `null` (Access Denied) — correct for
|
||
an unprovisioned user, but a Google sign-in to a brand that
|
||
exists won't resolve yet.
|
||
4. **Drop the `next-auth` Credentials provider module path** in
|
||
`auth.ts` entirely once production confirms Google is the only
|
||
provider in use. Currently it's gated on env var presence.
|
||
5. **Re-implement `sendPasswordResetEmail`** if/when Auth.js v5 ships
|
||
its built-in email provider — until then, a platform admin must
|
||
handle resets manually.
|
||
6. **Decommission `src/lib/supabase.ts` and `src/lib/supabase/server.ts`**
|
||
once all 33 remaining files have been ported to `pg`. They are
|
||
the only remaining `@supabase/*` import surface.
|
||
7. **Delete `@supabase/ssr` and `@supabase/supabase-js` from
|
||
`package.json`** (last step once no file imports them).
|
||
|
||
---
|
||
|
||
## Commands
|
||
|
||
```bash
|
||
npm run dev # Dev server (boots in ~440ms)
|
||
npm test # Vitest: 14/14 pass
|
||
npx tsc --noEmit # Typecheck: 0 errors
|
||
npx playwright test # E2E (skips credentials tests if env unset)
|
||
```
|