feat(storage): MinIO object storage, Neon Auth, Supabase removal
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject, deleteObject, presigned URL helpers, and BUCKETS constant - Wire product images, brand logos, and water log photos to MinIO via the new storage client - Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call) - Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge Function proxy) - Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass brand_settings.logo_url from all call sites - Update email templates to use dynamic logoUrl instead of hardcoded Supabase bucket URLs - Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage, TimeTrackingFieldClient; use brand_settings props + local public/ fallback - Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to public/ for local development - Add MinIO env vars to .env.example (endpoint, access key, secret, buckets) - Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props - Fix admin/users.ts logoUrl type (null → undefined for optional string) - Remove stale sb- cookie from wholesale-auth - Migrate tuxedo/about page to remove supabase import and use pool query for wholesale_settings lookup
This commit is contained in:
@@ -16,9 +16,9 @@ Do **not** add GitHub remotes. There is no `origin` on github.com and no separat
|
||||
|
||||
Route Commerce is a multi-tenant B2B e-commerce platform for fresh produce wholesale distribution. Brands sell to customers who pick up at scheduled stops or receive shipments. The platform includes admin dashboards for order management, stop/route scheduling, product catalogs, payment processing (Stripe + Square), and a communications module ("Harvest Reach") for email/SMS campaigns.
|
||||
|
||||
Tech stack: Next.js 16 (App Router) · **Postgres** (direct — Supabase is being removed) · Auth.js (NextAuth v5, in-progress migration from bespoke cookie auth) · Stripe · Square · Resend (email) · Tailwind CSS v4
|
||||
Tech stack: Next.js 16 (App Router) · **Postgres** (direct) · **Neon Auth** (Better Auth) · Stripe · Square · Resend (email) · Tailwind CSS v4
|
||||
|
||||
> **Direction:** Supabase is being removed in favor of a direct Postgres connection. The `supabase/` directory is kept as a path for migrations tooling only (no Supabase platform/CLI/auth). Until the Auth.js migration ships, auth still flows through the `dev_session` / `rc_auth_uid` cookies — see the Authentication section. New DB code should connect to Postgres directly (via `pg` or the chosen driver — see Database section) and **must not** import from `@supabase/*` or call Supabase REST.
|
||||
> **Direction:** Auth is handled by Neon Auth (Better Auth). The `admin_users` table links to Neon Auth users by email. New DB code should connect to Postgres directly (via `pg` or the chosen driver — see Database section) and **must not** import from `@supabase/*` or call Supabase REST.
|
||||
|
||||
---
|
||||
|
||||
@@ -46,43 +46,47 @@ E2E tests live in `tests/` and run via Playwright. Specs include `tests/smoke.sp
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
**Auth.js v5 (NextAuth)** is the active auth system. Config lives in `src/lib/auth.ts`, with the route handler at `src/app/api/auth/[...nextauth]/route.ts`. The login page (`src/app/login/LoginClient.tsx`) renders a "Continue with Google" button alongside the email/password form, both backed by Auth.js providers. Server-side code reads the session via `await auth()` from `@/lib/auth`; client code that needs to sign out can call the `signOutAction` server action from `@/actions/auth-actions`.
|
||||
**Neon Auth (Better Auth)** is the active auth system. Config lives in `src/lib/auth.ts`, with the route handler at `src/app/api/auth/[...nextauth]/route.ts`. Neon Auth manages users in the `neon_auth.user` table; our `admin_users` table links to Neon Auth users by email.
|
||||
|
||||
**Providers (see `src/lib/auth.ts`):**
|
||||
- **Google OAuth** — primary sign-in. Active when `AUTH_GOOGLE_ID` + `AUTH_GOOGLE_SECRET` are set. Users auto-provision as `platform_admin` if their Google `sub` is UUID-shaped (rare) — for Google sign-ins, `admin_users` rows must be provisioned manually by an existing admin until the `email`-based provisioning flow lands.
|
||||
- **Email/password (Supabase-backed)** — wraps the existing `auth/v1/token?grant_type=password` flow. This is transitional; once Supabase auth is fully removed, this provider goes away.
|
||||
**Auth flow:**
|
||||
1. User signs in via `/api/auth/sign-in` → Neon Auth validates credentials → session cookie set
|
||||
2. `getAdminUser()` in `src/lib/admin-permissions.ts` reads the Neon Auth session and looks up `admin_users` by email
|
||||
3. Middleware (`src/middleware.ts`) guards `/admin/*` routes at the edge level
|
||||
|
||||
**Demo / dev mode** still works through a `dev_session` cookie:
|
||||
- `dev_session=platform_admin` — full access, all brands
|
||||
- `dev_session=brand_admin` — full access to assigned brand only
|
||||
- `dev_session=store_employee` — limited access (orders, pickup, wholesale only)
|
||||
- The login page renders "Demo Mode" buttons that set this cookie client-side; the middleware also auto-issues `dev_session=platform_admin` for the `/admin` demo flow when Supabase isn't configured.
|
||||
**Key files:**
|
||||
- `src/lib/auth.ts` — Neon Auth configuration (getSession, signIn, signOut, resetPassword, requestPasswordReset)
|
||||
- `src/auth.config.ts` — Edge-safe config (baseUrl, cookieSecret)
|
||||
- `src/middleware.ts` — Edge-level route protection
|
||||
- `src/app/api/auth/sign-in/route.ts` — Email/password sign-in API
|
||||
- `src/app/api/auth/forgot-password/route.ts` — Password reset request API
|
||||
- `src/app/api/auth/reset-password/route.ts` — Password reset confirmation API
|
||||
- `src/actions/auth-actions.ts` — Server actions (signOutAction)
|
||||
- `src/actions/admin/password.ts` — Admin password update (setUserPassword)
|
||||
|
||||
**Single source of truth for the current admin user:** `getAdminUser()` in `src/lib/admin-permissions.ts`. It reads `dev_session` first, then the Auth.js session via `auth()` from `@/lib/auth`. The Supabase `admin_users` lookup still uses `rest/v1/admin_users?user_id=eq.<uid>` — when the `pg` pool at `src/lib/db.ts` lands, that lookup should switch to a direct `SELECT`. **Never import `admin-permissions.ts` into Client Components** — use the `getCurrentAdminUser` server action from `@/actions/admin-user` instead.
|
||||
**Environment variables:**
|
||||
- `NEON_AUTH_BASE_URL` — from `neonctl neon-auth status`
|
||||
- `NEON_AUTH_COOKIE_SECRET` — min 32-char secret: `openssl rand -base64 32`
|
||||
- `NEXT_PUBLIC_SITE_URL` — site URL for redirects and Origin header
|
||||
|
||||
The middleware (`src/middleware.ts`) uses Auth.js v5's `auth()` wrapper. It guards `/admin/*` and `/login`, preserves the `dev_session` bypass, and adds baseline security headers.
|
||||
**Single source of truth for the current admin user:** `getAdminUser()` in `src/lib/admin-permissions.ts`. It reads the Neon Auth session via `getSession()` from `@/lib/auth`, then looks up `admin_users` by email. **Never import `admin-permissions.ts` into Client Components** — use server actions instead.
|
||||
|
||||
The `AdminUser` type lives in `src/lib/admin-permissions-types.ts` and is shared across server/client boundary.
|
||||
|
||||
#### Migration status
|
||||
#### Auth API routes
|
||||
|
||||
- ✅ Auth.js v5 installed (`next-auth@beta`, currently `5.0.0-beta.31`)
|
||||
- ✅ `src/lib/auth.ts` + `src/app/api/auth/[...nextauth]/route.ts` in place
|
||||
- ✅ Google + Credentials (Supabase-backed) providers configured
|
||||
- ✅ `getAdminUser()` reads from `auth()` session
|
||||
- ✅ Middleware uses `auth()` wrapper
|
||||
- ✅ Old `/api/login`, `/api/logout`, `/api/auth/uid`, `/api/set-auth-cookie` removed
|
||||
- ⏳ Add `email` column to `admin_users` and provision Google users by email (TODO)
|
||||
- ⏳ Switch the `admin_users` lookup in `getAdminUser()` to direct `pg` (TODO — needs `src/lib/db.ts`)
|
||||
- ⏳ Remove the email/password (Supabase) provider when Supabase auth is fully cut over (TODO)
|
||||
- ⏳ The `rc_auth_uid` cookie is no longer the source of truth, but `actions/admin/users.ts` still reads it for backward compat with pre-existing sessions — the `DEV_FORCE_UID` constant and its branches are now dead code (the `/api/force-admin` route that set it was deleted) and should be removed in a follow-up
|
||||
| Route | Method | Purpose |
|
||||
|-------|--------|---------|
|
||||
| `/api/auth/sign-in` | POST | Email/password sign-in |
|
||||
| `/api/auth/forgot-password` | POST | Request password reset email |
|
||||
| `/api/auth/reset-password` | POST | Set new password (after reset link clicked) |
|
||||
| `/api/auth/[...nextauth]` | GET/POST | Neon Auth handler (sign-out, OAuth callbacks, etc.) |
|
||||
|
||||
### Server Actions Pattern
|
||||
|
||||
All database writes go through server actions in `src/actions/`. These:
|
||||
1. Call `getAdminUser()` to verify auth
|
||||
2. Check role/permission flags (`can_manage_orders`, etc.)
|
||||
3. Call Supabase REST APIs (not the Supabase client directly in server actions) to trigger SECURITY DEFINER RPCs
|
||||
3. Call Postgres RPCs via the `pg` driver
|
||||
4. Return typed results (`{ success: true, ... } | { success: false, error: string }`)
|
||||
|
||||
Server actions are "use server" files that export async functions. Client components import and call them directly.
|
||||
@@ -266,13 +270,15 @@ Separate from orders/stops — tracks irrigation/water usage per brand. `src/act
|
||||
|
||||
| Concern | Location |
|
||||
|---|---|
|
||||
| Neon Auth configuration | `src/lib/auth.ts`, `src/auth.config.ts` |
|
||||
| Auth API routes | `src/app/api/auth/sign-in/route.ts`, `src/app/api/auth/forgot-password/route.ts`, `src/app/api/auth/reset-password/route.ts`, `src/app/api/auth/[...nextauth]/route.ts` |
|
||||
| Admin auth + permissions | `src/lib/admin-permissions.ts`, `src/lib/admin-permissions-types.ts` |
|
||||
| Middleware (route protection) | `src/middleware.ts` |
|
||||
| Server actions | `src/actions/*.ts` (one file per domain; also grouped into `src/actions/{admin,ai,billing,communications,harvest-reach,integrations,orders,products,settings,shipping,stops,water-log,platform,route-trace,time-tracking,email-automation}/`) |
|
||||
| Admin pages | `src/app/admin/[module]/page.tsx` |
|
||||
| Admin client components | `src/components/admin/*.tsx` |
|
||||
| Migrations | `supabase/migrations/` (kept for now; will likely move to `db/migrations/` in a later pass) |
|
||||
| Postgres pool / driver | `src/lib/db.ts` (TBD — create during the Supabase removal pass) |
|
||||
| Migrations | `db/migrations/` |
|
||||
| Postgres pool / driver | `src/lib/db.ts` (TBD) |
|
||||
| Email templates | `src/lib/email-templates.ts` |
|
||||
| Date formatting | `src/lib/format-date.ts` |
|
||||
| Feature flags | `src/lib/feature-flags.ts` |
|
||||
@@ -290,7 +296,8 @@ Separate from orders/stops — tracks irrigation/water usage per brand. `src/act
|
||||
|
||||
- **Dev mode `brand_id: null`**: `getAdminUser()` returns `brand_id: null` for platform_admin dev sessions. Always pass explicit `brandId` to server action functions that accept it — don't rely on `adminUser.brand_id` alone.
|
||||
- **Communications = no RLS**: The communications tables (campaigns, templates, contacts, message_logs) have no row-level policies. All brand scoping must be enforced in server actions.
|
||||
- **Supabase residue in the wild**: `grep -r "@supabase" src/` will still find imports during the transition. Do not add new ones; if you're touching a file that imports from Supabase, replace the call with the equivalent `pg`-pool call before merging.
|
||||
- **Webhook event_id**: `log_communication_messages` never populates `event_id`, so the Resend webhook uses `customer_email + subject` lookup instead.
|
||||
- **Mixed fulfillment orders**: An order can have both pickup and ship items. `get_shipping_orders` RPC returns orders with at least one `fulfillment = 'ship'` item.
|
||||
- **SMS opt-in defaults**: `communication_contacts.sms_opt_in` defaults to `FALSE` (opt-out by default). `email_opt_in` defaults to `TRUE`. Always check `sms_opt_in` specifically for SMS sends, not `email_opt_in`.
|
||||
- **SMS opt-in defaults**: `communication_contacts.sms_opt_in` defaults to `FALSE` (opt-out by default). `email_opt_in` defaults to `TRUE`. Always check `sms_opt_in` specifically for SMS sends, not `email_opt_in`.
|
||||
- **Neon Auth session cookie**: The session cookie is managed by Neon Auth. Do not manually set or clear it — use `signOut()` from `@/lib/auth` instead.
|
||||
- **Password reset**: The forgot-password API returns success even if the email doesn't exist (to prevent email enumeration). Check server logs to verify reset emails were sent.
|
||||
Reference in New Issue
Block a user