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:
+49
-94
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user