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
+49 -94
View File
@@ -1,108 +1,63 @@
import type { NextAuthConfig, DefaultSession } from "next-auth";
import Google from "next-auth/providers/google";
/**
* Edge-safe Auth.js v5 configuration.
* Edge-safe Neon Auth configuration.
*
* This file is imported by `src/middleware.ts`, which runs in the Edge
* runtime. It must NOT import:
* - `pg` / `@auth/pg-adapter` (Node-only)
* - `next-auth/providers/credentials` (uses Node `crypto` internally)
* - `pg` / any Node-only database driver
* - The Drizzle client
* - Anything else that touches the database or Node-only APIs
* - Anything else that touches the Node.js runtime
*
* Provider definitions, callbacks, and pages all live here. The full
* server-side handler in `src/lib/auth.ts` extends this with the
* Credentials provider (Node-only) for email + password sign-in.
* Both the middleware and the server-side handler in `src/lib/auth.ts`
* share the same session cookie, so the middleware can read sessions
* minted by the server-side handler.
*
* Both instances share the same session cookie, so the middleware can
* read JWTs minted by the server-side handler.
* Environment variables required:
* NEON_AUTH_BASE_URL — from `neonctl neon-auth status` (Auth Base URL)
* NEON_AUTH_COOKIE_SECRET — min 32-char secret: openssl rand -base64 32
*/
declare module "next-auth" {
interface Session {
user: {
id: string;
} & DefaultSession["user"];
}
export interface NeonAuthBaseConfig {
baseUrl: string;
cookieSecret: string;
}
const hasGoogleCreds = !!(
process.env.AUTH_GOOGLE_ID && process.env.AUTH_GOOGLE_SECRET
);
const googleProvider = hasGoogleCreds
? [
Google({
clientId: process.env.AUTH_GOOGLE_ID,
clientSecret: process.env.AUTH_GOOGLE_SECRET,
}),
]
: [];
export const authConfig = {
trustHost: true,
pages: { signIn: "/login" },
session: { strategy: "jwt" as const },
providers: googleProvider,
callbacks: {
/**
* Gate /admin, /wholesale, and /protected-example. Anything on those
* paths and not signed in is redirected to /login (the default
* pages.signIn). Public storefronts and the homepage pass through.
*
* The actual role-based gating still happens in `getAdminUser()` —
* this callback only ensures the user is *signed in*.
*/
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnAdmin = nextUrl.pathname.startsWith("/admin");
const isOnWholesale = nextUrl.pathname.startsWith("/wholesale");
const isOnProtectedExample = nextUrl.pathname.startsWith(
"/protected-example"
);
if (isOnAdmin || isOnWholesale || isOnProtectedExample) {
return isLoggedIn;
}
return true;
},
/**
* Forward the user id into the JWT on initial sign-in. With
* `session.strategy: "jwt"` this is the field downstream code reads
* via `session.user.id`.
*/
async jwt({ token, user }) {
if (user) {
if (user.id) token.id = user.id;
if (user.email) token.email = user.email;
}
return token;
},
async session({ session, token }) {
if (session.user) {
session.user.id =
(typeof token.id === "string" && token.id) ||
(typeof token.sub === "string" && token.sub) ||
"";
}
return session;
},
},
} satisfies NextAuthConfig;
/**
* Is the dev Credentials provider enabled?
* - Disabled in production (NODE_ENV === "production")
* - Can be force-disabled by setting `ALLOW_DEV_LOGIN=false`
*
* Surfaced so `src/lib/auth.ts` can decide whether to include the
* Credentials provider in its providers list.
* Returns the Neon Auth base config for server-side use.
* Called by both the edge middleware and the Node-only auth lib.
*/
export function isDevLoginEnabled(): boolean {
return (
process.env.NODE_ENV !== "production" &&
process.env.ALLOW_DEV_LOGIN !== "false"
);
export function getNeonAuthConfig(): NeonAuthBaseConfig {
const baseUrl = process.env.NEON_AUTH_BASE_URL;
if (!baseUrl) {
throw new Error(
"NEON_AUTH_BASE_URL is not set. Run `neonctl neon-auth status` to get the value."
);
}
const cookieSecret = process.env.NEON_AUTH_COOKIE_SECRET;
if (!cookieSecret) {
throw new Error(
"NEON_AUTH_COOKIE_SECRET is not set. Generate one: openssl rand -base64 32"
);
}
if (cookieSecret.length < 32) {
throw new Error("NEON_AUTH_COOKIE_SECRET must be at least 32 characters.");
}
return { baseUrl, cookieSecret };
}
/**
* Returns the Neon Auth base config, or null if not configured.
* Use this for middleware that should gracefully degrade if auth is not set up.
*/
export function getNeonAuthConfigOptional(): NeonAuthBaseConfig | null {
const baseUrl = process.env.NEON_AUTH_BASE_URL;
const cookieSecret = process.env.NEON_AUTH_COOKIE_SECRET;
if (!baseUrl || !cookieSecret || cookieSecret.length < 32) {
return null;
}
return { baseUrl, cookieSecret };
}