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
+11 -11
View File
@@ -2,7 +2,7 @@
import { and, eq, ilike, or, SQL, sql } from "drizzle-orm";
import { getAdminUser } from "@/lib/admin-permissions";
import { withTenant } from "@/db/client";
import { withBrand } from "@/db/client";
import { customers, brandSettings } from "@/db/schema";
import type { AudienceRules } from "@/actions/communications/campaigns";
@@ -55,7 +55,7 @@ export type Segment = {
export type CustomerSample = {
id: string;
email: string;
name: string;
fullName: string | null;
tags: string[];
phone: string | null;
};
@@ -70,11 +70,11 @@ function isSegmentList(v: unknown): v is Segment[] {
}
async function loadSegments(brandId: string): Promise<Segment[]> {
const rows = await withTenant(brandId, (db) =>
const rows = await withBrand(brandId, (db) =>
db
.select({ flags: brandSettings.featureFlags })
.from(brandSettings)
.where(eq(brandSettings.tenantId, brandId))
.where(eq(brandSettings.brandId, brandId))
.limit(1),
);
const raw = (rows[0]?.flags ?? {}) as Record<string, unknown>;
@@ -83,11 +83,11 @@ async function loadSegments(brandId: string): Promise<Segment[]> {
}
async function saveSegments(brandId: string, segments: Segment[]): Promise<void> {
await withTenant(brandId, async (db) => {
await withBrand(brandId, async (db) => {
const existing = await db
.select({ flags: brandSettings.featureFlags })
.from(brandSettings)
.where(eq(brandSettings.tenantId, brandId))
.where(eq(brandSettings.brandId, brandId))
.limit(1);
const baseFlags = (existing[0]?.flags ?? {}) as Record<string, unknown>;
const nextFlags: Record<string, unknown> = {
@@ -97,7 +97,7 @@ async function saveSegments(brandId: string, segments: Segment[]): Promise<void>
await db
.update(brandSettings)
.set({ featureFlags: nextFlags, updatedAt: new Date() })
.where(eq(brandSettings.tenantId, brandId));
.where(eq(brandSettings.brandId, brandId));
});
}
@@ -221,15 +221,15 @@ export async function previewSegmentWithCustomers(
}
void rules;
const conds: SQL[] = [eq(customers.tenantId, brandId), eq(customers.emailOptIn, true)];
const conds: SQL[] = [eq(customers.brandId, brandId), eq(customers.emailOptIn, true)];
try {
const [samples, counts] = await withTenant(brandId, async (db) => {
const [samples, counts] = await withBrand(brandId, async (db) => {
const sample = await db
.select({
id: customers.id,
email: customers.email,
name: customers.name,
fullName: customers.fullName,
phone: customers.phone,
})
.from(customers)
@@ -247,7 +247,7 @@ export async function previewSegmentWithCustomers(
sample_customers: samples.map((s) => ({
id: s.id,
email: s.email ?? "",
name: s.name,
fullName: s.fullName,
tags: [],
phone: s.phone,
})),