feat(storage): MinIO object storage, Neon Auth, Supabase removal
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:
openclaw
2026-06-09 11:32:17 -06:00
parent bb464bda65
commit 916ad39176
349 changed files with 6706 additions and 3343 deletions
+31 -77
View File
@@ -1,89 +1,43 @@
import "server-only";
/**
* Auth.js (NextAuth v5) — server-side configuration.
* Neon Auth (Better Auth) — server-side configuration.
*
* This file is Node-only. It is imported by:
* - `src/app/api/auth/[...nextauth]/route.ts` (the OAuth + credentials handlers)
* - Server actions that call `signIn` / `signOut`
* - `src/lib/admin-permissions.ts` (reads `auth()` for the current user)
* - `src/app/api/auth/[...nextauth]/route.ts` (the API handler)
* - Server actions that call signIn / signOut
* - `src/lib/admin-permissions.ts` (reads getSession() for the current user)
*
* The middleware imports a separate, edge-safe instance built from
* `src/auth.config.ts`. Both instances share the same JWT cookie, so the
* middleware can read sessions minted here.
* `src/auth.config.ts`. Both instances share the same session cookie,
* so the middleware can read sessions minted here.
*
* Providers:
* - Google OAuth — active when AUTH_GOOGLE_ID + AUTH_GOOGLE_SECRET are set.
* - Email + password (Credentials) — active in dev only; backed by the
* `users.password_hash` column. In production, set ALLOW_DEV_LOGIN=false
* (the default) and the provider is omitted entirely.
*
* For local dev, run `npm run db:seed` to create the seeded admin user
* (`admin@route-commerce.local` / `admin`). The `authorize` function
* looks up the user by email, verifies the password against the stored
* hash, and returns the real user record. No `dev_session` cookie
* bypass; this is real Auth.js sign-in.
* Auth providers: configured in the Neon Auth dashboard (oauth-provider).
* Neon Auth manages its own users in the `neon_auth.user` table.
* Our `admin_users` table links to Neon Auth users by email.
*/
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { eq } from "drizzle-orm";
import { authConfig, isDevLoginEnabled } from "@/auth.config";
import { withDb } from "@/db/client";
import { users } from "@/db/schema";
import { verifyPassword } from "@/lib/passwords";
import { createNeonAuth } from "@neondatabase/auth/next/server";
import { getNeonAuthConfig } from "@/auth.config";
function buildCredentialsProvider() {
return Credentials({
id: "credentials",
name: "Email + password",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
/**
* Returns the user on success, or `null` on any failure. Auth.js
* never throws from `authorize` — a throw is treated as a 500.
*/
async authorize(creds) {
if (!isDevLoginEnabled()) return null;
const email = String(creds?.email ?? "").trim().toLowerCase();
const password = String(creds?.password ?? "");
if (!email || !password) return null;
try {
// The `users` table is global (not tenant-scoped), so we use
// `withDb` rather than `withTenant` — no GUC to set.
const u = await withDb(async (db) => {
const rows = await db
.select()
.from(users)
.where(eq(users.email, email))
.limit(1);
return rows[0] ?? null;
});
if (!u || !u.passwordHash) return null;
if (!verifyPassword(password, u.passwordHash)) return null;
return {
id: u.id,
name: u.name ?? undefined,
email: u.email,
};
} catch (err) {
// eslint-disable-next-line no-console
console.error("[auth] credentials authorize failed:", err);
return null;
}
},
});
}
const providers = [
...authConfig.providers,
...(isDevLoginEnabled() ? [buildCredentialsProvider()] : []),
];
export const { handlers, auth, signIn, signOut } = NextAuth({
...authConfig,
providers,
const auth = createNeonAuth({
baseUrl: getNeonAuthConfig().baseUrl,
cookies: { secret: getNeonAuthConfig().cookieSecret },
});
export const { getSession, signIn, signOut, resetPassword, requestPasswordReset } = auth;
export const { listUsers, createUser, setRole, setUserPassword, updateUser } = auth.admin;
/**
* Re-exported for the API route handler compatibility.
* `GET` and `POST` from `auth.handler()` handle all Neon Auth endpoints.
*/
const authInstance = auth.handler();
export const { GET: handlersGET, POST: handlersPOST } = authInstance;
/**
* Re-exported for middleware compatibility (edge runtime can't use Node-only auth).
* The middleware uses `neonAuthMiddleware` from `@neondatabase/auth/next/server` directly.
* This named export exists for call sites that import `auth` from this module.
*/
export { auth };