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:
+164
@@ -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|.*\\..*).*)",
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user