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
+23 -15
View File
@@ -41,6 +41,15 @@ import { PageHeader, AdminButton, AdminSearchInput, AdminFilterTabs, AdminBadge
type Tab = "dashboard" | "customers" | "products" | "orders" | "settings";
// SVG Icon for Wholesale - defined at module level to prevent recreation on each render
function WholesaleIcon() {
return (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
);
}
export default function WholesaleClient({ brandId }: { brandId: string }) {
const [tab, setTab] = useState<Tab>("dashboard");
const [msg, setMsg] = useState<{ kind: "success" | "error"; text: string } | null>(null);
@@ -101,13 +110,6 @@ export default function WholesaleClient({ brandId }: { brandId: string }) {
);
}
// SVG Icon for Wholesale
const WholesaleIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
);
const tabs = [
{ value: "dashboard", label: "Dashboard", count: undefined },
{ value: "customers", label: "Customers", count: customers.filter(c => c.account_status !== "pending_approval" && c.account_status !== "rejected").length },
@@ -116,10 +118,13 @@ export default function WholesaleClient({ brandId }: { brandId: string }) {
{ value: "settings", label: "Settings" },
];
// Declare icon reference for PageHeader
const pageIcon = <WholesaleIcon />;
return (
<div className="min-h-screen bg-[var(--admin-bg)]">
<PageHeader
icon={<WholesaleIcon />}
icon={pageIcon}
title="Wholesale Portal"
subtitle="Manage wholesale orders, customers, and products"
className="mb-0"
@@ -1843,13 +1848,7 @@ function SettingsTab({ settings, brandId, onMsg, onRefresh, canManageSettings }:
onRefresh: () => void;
canManageSettings: boolean;
}) {
if (!canManageSettings) {
return (
<div className="flex items-center justify-center h-64">
<p className="text-slate-400">You do not have permission to manage settings.</p>
</div>
);
}
// Hooks must be called unconditionally - always declare them first
const [form, setForm] = useState({
requireApproval: settings?.require_approval ?? true,
minOrderAmount: settings?.min_order_amount ?? "",
@@ -1868,6 +1867,15 @@ function SettingsTab({ settings, brandId, onMsg, onRefresh, canManageSettings }:
});
const [saving, setSaving] = useState(false);
// Permission check after hooks
if (!canManageSettings) {
return (
<div className="flex items-center justify-center h-64">
<p className="text-slate-400">You do not have permission to manage settings.</p>
</div>
);
}
async function handleSave() {
setSaving(true);
const result = await saveWholesaleSettings({
+5 -18
View File
@@ -1,25 +1,12 @@
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
import { getAdminUser } from "@/lib/admin-permissions";
import { getActiveBrandId } from "@/lib/brand-scope";
import WholesaleClient from "./WholesaleClient";
export default async function WholesalePage() {
const cookieStore = await cookies();
const devSession = cookieStore.get("dev_session")?.value;
const isDevMode =
devSession === "platform_admin" ||
devSession === "brand_admin" ||
devSession === "store_employee";
const adminUser = await getAdminUser();
if (!adminUser) redirect("/login");
if (!isDevMode) {
const adminUser = await getAdminUser();
if (!adminUser) redirect("/login");
const activeBrandId = await getActiveBrandId(adminUser);
return <WholesaleClient brandId={activeBrandId ?? ""} />;
}
// Dev mode: platform_admin sees all brands, use first brand as default
const brandId = devSession === "platform_admin" ? "" : (cookieStore.get("dev_brand_id")?.value ?? "64294306-5f42-463d-a5e8-2ad6c81a96de");
return <WholesaleClient brandId={brandId} />;
}
const activeBrandId = await getActiveBrandId(adminUser);
return <WholesaleClient brandId={activeBrandId ?? ""} />;
}