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
+164
View File
@@ -0,0 +1,164 @@
/**
* Edge-level route protection for Route Commerce (proxy.ts for Next.js 16+).
*
* This middleware runs at the edge (before any server code) and protects
* /admin/* and /api/admin/* routes by checking for a valid Neon Auth
* session cookie.
*
* Neon Auth session cookies:
* - __Secure-neon-auth.session-token (secure/HTTPS)
* - __Secure-neon-auth.session_token (secure/HTTPS)
* - neon-auth.session-token (development)
* - neon-auth.session_token (development)
*
* If no session is found for a protected route, the user is redirected
* to /login with the original path as a query parameter.
*/
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
// ── Routes ─────────────────────────────────────────────────────────────────
/** Routes that require an authenticated session */
const PROTECTED_ROUTES = ["/admin", "/api/admin"] as const;
/** Routes that are always accessible without authentication */
const PUBLIC_ROUTES = [
"/",
"/login",
"/logout",
"/register",
"/forgot-password",
"/reset-password",
"/change-password",
"/tuxedo",
"/indian-river-direct",
"/wholesale",
"/water",
"/api/auth",
"/api/auth/[...nextauth]",
"/api/forgot-password",
"/api/reset-password",
"/api/change-password",
"/api/stripe/webhook",
"/api/water-qr",
"/api/water-photo-upload",
"/api/wholesale/checkout",
"/api/wholesale/webhook",
"/api/resend/webhook",
"/api/square/webhook",
"/api/cron",
"/changelog",
"/pricing",
"/contact",
"/blog",
"/roadmap",
"/privacy-policy",
"/terms-and-conditions",
"/waitlist",
"/test",
"/test-simple.html",
"/robots.txt",
"/sitemap.xml",
] as const;
// ── Helpers ─────────────────────────────────────────────────────────────────
/** Returns true when the given pathname starts with any protected route prefix. */
function isProtectedRoute(pathname: string): boolean {
return PROTECTED_ROUTES.some((route) => pathname.startsWith(route));
}
/** Returns true when the given pathname matches a public route exactly. */
function isPublicRoute(pathname: string): boolean {
return PUBLIC_ROUTES.some(
(route) =>
pathname === route ||
// Handle wildcard patterns like /api/auth/*
(route.includes("[...") && pathname.startsWith(route.replace("/[...nextauth]", "")))
);
}
/**
* Checks for any Neon Auth session cookie.
* Neon Auth sets cookies with names like:
* - __Secure-neon-auth.session-token
* - __Secure-neon-auth.session_token
* - neon-auth.session-token
* - neon-auth.session_token
*
* Also checks for dev_session cookie in development mode.
*/
function hasNeonAuthSession(request: NextRequest): boolean {
const cookies = request.cookies;
// Check for Neon Auth session cookies
if (
cookies.get("__Secure-neon-auth.session-token")?.value ||
cookies.get("__Secure-neon-auth.session_token")?.value ||
cookies.get("neon-auth.session-token")?.value ||
cookies.get("neon-auth.session_token")?.value
) {
return true;
}
// Development bypass: allow dev_session cookie for local testing
// This bypass only works when NODE_ENV is not "production"
if (process.env.NODE_ENV !== "production") {
const devSession = cookies.get("dev_session")?.value;
if (devSession === "platform_admin" || devSession === "brand_admin" || devSession === "store_employee") {
return true;
}
}
return false;
}
// ── Middleware ───────────────────────────────────────────────────────────────
export default async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
// Allow public routes without any auth check
if (isPublicRoute(pathname)) {
return NextResponse.next();
}
// For protected routes, verify the session
if (isProtectedRoute(pathname)) {
if (!hasNeonAuthSession(request)) {
// No session — redirect to login, preserving the intended destination
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("redirect", pathname);
return NextResponse.redirect(loginUrl, 307);
}
}
// Allow all other routes through (storefront, etc.)
return NextResponse.next();
}
// ── Matcher ──────────────────────────────────────────────────────────────────
/**
* Next.js matcher configuration.
*
* Excludes:
* - Static files and Next.js internals
* - _next/static (webpack HMR)
* - _next/image (image optimization)
* - favicon.ico, etc.
*/
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization)
* - favicon.ico, sitemap.xml, robots.txt (static assets)
* - .*\..* (files with extensions like .js, .css, .png)
*/
"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|.*\\..*).*)",
],
};