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
+72 -74
View File
@@ -10,7 +10,7 @@
*
* IMPORTANT: This shim does NOT talk to a real database. It returns
* empty result sets. Legacy call sites that need real data must be
* rewritten against `pool` / `withDb` / `withTenant`.
* rewritten against `pool` / `withDb` / `withBrand`.
*
* The query-builder API surface supported here is intentionally narrow:
* - .from(table).select(cols?).eq(col, val).eq(...).is(col, null).
@@ -29,16 +29,21 @@
* If a call site needs more than that, migrate the call site.
*/
import { getMockTableData } from "./mock-data";
const useMockData =
process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true" || true;
type FilterOp = "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "is" | "like" | "ilike" | "in";
type Filter = { column: string; value: unknown; op: FilterOp };
class MockQueryBuilder {
private data: unknown[];
// Result types for the mock query builder
interface MockQueryResult<T> {
data: T[] | null;
error: null;
}
interface MockSingleResult<T> {
data: T | null;
error: null;
}
class MockQueryBuilder<T extends Record<string, unknown> = Record<string, unknown>> {
private tableName: string;
private filters: Filter[] = [];
private selectColumns: string = "*";
@@ -52,11 +57,10 @@ class MockQueryBuilder {
constructor(tableName: string) {
this.tableName = tableName;
this.data = [...(getMockTableData(tableName) || [])];
// No mock data - return empty results
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
select(columns: string = "*", _opts?: any) {
select(columns: string = "*", _opts?: Record<string, unknown>) {
this.selectColumns = columns;
return this;
}
@@ -131,55 +135,47 @@ class MockQueryBuilder {
// The legacy Supabase client returns a thenable from .single() so
// callers can write `.single().then(({ data, error }) => ...)` as
// well as `const { data, error } = await ....single()`. We return a
// proper Promise<{ data: any, error: any }> so destructured binding
// proper Promise<MockSingleResult<T>> so destructured binding
// patterns in callers work under `--strict` (no implicit `any`).
single() {
single(): Promise<MockSingleResult<T>> {
return Promise.resolve(this.executeSingle());
}
maybeSingle() {
maybeSingle(): Promise<MockSingleResult<T>> {
return Promise.resolve(this.executeSingle());
}
// Return a generic `any` to match the historical Supabase client
// typing (`data: T[]`, `data: T` for `.single()`). Without this, every
// consumer would have to be rewritten just to satisfy the type
// checker, which defeats the purpose of the shim.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
then<TResult1 = any, TResult2 = never>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onfulfilled?: ((value: any) => TResult1 | PromiseLike<TResult1>) | null,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
then<TResult1 = MockQueryResult<T>, TResult2 = never>(
onfulfilled?: ((value: MockQueryResult<T>) => TResult1 | PromiseLike<TResult1>) | null,
_onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
): PromiseLike<TResult1 | TResult2> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: any = this.execute();
return Promise.resolve(onfulfilled ? onfulfilled(result) : (result as TResult1));
const result = this.execute();
return Promise.resolve(onfulfilled ? onfulfilled(result) : (result as unknown as TResult1));
}
// Insert / update / delete mutators — these are mostly used by legacy
// auth flows and the AI preferences action. We capture the data and
// short-circuit to a successful no-op response so the call sites
// don't blow up. Real writes must go through server actions.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
executeSingle(): any {
executeSingle(): MockSingleResult<T> {
const result = this.execute();
if (Array.isArray(result.data) && result.data.length > 0) {
return { data: result.data[0], error: null };
return { data: result.data[0] as T, error: null };
}
return { data: null, error: null };
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
execute(): any {
execute(): MockQueryResult<T> {
if (this.mode === "update" || this.mode === "delete") {
return this.runMutation();
}
let filtered: unknown[] = [...this.data];
// No mock data - return empty array
let filtered: unknown[] = [];
for (const filter of this.filters) {
filtered = filtered.filter((row: any) => {
const rowValue = row[filter.column];
filtered = filtered.filter((row) => {
const r = row as Record<string, unknown>;
const rowValue = r[filter.column];
switch (filter.op) {
case "eq":
return rowValue === filter.value;
@@ -218,10 +214,9 @@ class MockQueryBuilder {
}
if (this.orderColumn) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
filtered.sort((a: any, b: any) => {
const aVal = a[this.orderColumn!];
const bVal = b[this.orderColumn!];
filtered.sort((a: unknown, b: unknown) => {
const aVal = (a as Record<string, unknown>)[this.orderColumn!] as string | number;
const bVal = (b as Record<string, unknown>)[this.orderColumn!] as string | number;
if (aVal < bVal) return this.orderDirection === "desc" ? 1 : -1;
if (aVal > bVal) return this.orderDirection === "desc" ? -1 : 1;
return 0;
@@ -234,11 +229,10 @@ class MockQueryBuilder {
filtered = filtered.slice(0, this.limitValue);
}
return { data: filtered, error: null };
return { data: filtered as T[], error: null };
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private runMutation(): any {
private runMutation(): MockQueryResult<T> {
if (this.mode === "delete") {
return { data: null, error: null };
}
@@ -248,16 +242,20 @@ class MockQueryBuilder {
...item,
id: (item as Record<string, unknown>).id ?? `generated-${Date.now()}-${i}`,
}));
return { data: returning, error: null };
return { data: returning as unknown as T[], error: null };
}
return { data: null, error: null };
}
}
class MockMutationBuilder {
interface MockMutationResult<T> {
data: T[] | null;
error: null;
}
class MockMutationBuilder<T extends Record<string, unknown> = Record<string, unknown>> {
private tableName: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private data: any;
private data: unknown;
constructor(tableName: string, data: unknown) {
this.tableName = tableName;
@@ -265,31 +263,28 @@ class MockMutationBuilder {
}
select() {
return new MockQueryBuilder(this.tableName);
return new MockQueryBuilder<T>(this.tableName);
}
eq() {
return this;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
then<TResult1 = any, TResult2 = never>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onfulfilled?: ((value: any) => TResult1 | PromiseLike<TResult1>) | null,
then<TResult1 = MockMutationResult<T>, TResult2 = never>(
onfulfilled?: ((value: MockMutationResult<T>) => TResult1 | PromiseLike<TResult1>) | null,
): PromiseLike<TResult1 | TResult2> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: any = {
const result: MockMutationResult<T> = {
data: Array.isArray(this.data)
? this.data.map((item: any, i: number) => ({
? this.data.map((item: Record<string, unknown>, i: number) => ({
...item,
id: item?.id ?? `generated-${Date.now()}-${i}`,
}))
})) as unknown as T[]
: this.data
? [{ ...this.data, id: this.data.id ?? `generated-${Date.now()}` }]
? [{ ...(this.data as Record<string, unknown>), id: (this.data as Record<string, unknown>).id ?? `generated-${Date.now()}` }] as unknown as T[]
: null,
error: null,
};
return Promise.resolve(onfulfilled ? onfulfilled(result) : (result as TResult1));
return Promise.resolve(onfulfilled ? onfulfilled(result) : (result as unknown as TResult1));
}
}
@@ -316,6 +311,17 @@ class MockStorageBuilder {
}
}
// Auth credential types
interface SignInCredentials {
email?: string;
password?: string;
}
interface UserAttributes {
email?: string;
data?: Record<string, unknown>;
}
function createMockClient() {
// The query builder is mutable, so we can't simply return a class
// instance + spread mutation methods. The cleanest way to support
@@ -323,15 +329,12 @@ function createMockClient() {
// (write) in a single chain is to delegate everything to one object
// and inspect `this.mode` lazily. We build that object via
// `Object.assign` to keep the TypeScript inference happy.
function makeFrom(table: string) {
const qb = new MockQueryBuilder(table);
function makeFrom<T extends Record<string, unknown> = Record<string, unknown>>(table: string) {
const qb = new MockQueryBuilder<T>(table);
return Object.assign(qb, {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
insert: (data: any) => new MockMutationBuilder(table, data),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
update: (data: any) => new MockMutationBuilder(table, data),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
upsert: (data: any) => new MockMutationBuilder(table, data),
insert: (data: Record<string, unknown>) => new MockMutationBuilder<T>(table, data),
update: (data: Record<string, unknown>) => new MockMutationBuilder<T>(table, data),
upsert: (data: Record<string, unknown>) => new MockMutationBuilder<T>(table, data),
delete: () => new MockDeleteBuilder(),
});
}
@@ -342,14 +345,12 @@ function createMockClient() {
auth: {
getSession: async () => ({ data: { session: null }, error: null }),
getUser: async () => ({ data: { user: null }, error: null }),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
signInWithPassword: async (_creds: any) => ({
signInWithPassword: async (_creds: SignInCredentials) => ({
data: { user: null, session: null },
error: null,
}),
signOut: async () => ({ error: null }),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
updateUser: async (_attrs: any): Promise<any> => ({
updateUser: async (_attrs: UserAttributes): Promise<{ data: { user: null }; error: null }> => ({
data: { user: null },
error: null,
}),
@@ -357,8 +358,7 @@ function createMockClient() {
data: { subscription: { unsubscribe: () => {} } },
}),
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rpc: async (_name: string, _params?: any): Promise<any> => ({
rpc: async (_name: string, _params?: Record<string, unknown>): Promise<{ data: null; error: null }> => ({
data: null,
error: null,
}),
@@ -369,6 +369,4 @@ function createMockClient() {
};
}
export const supabase = useMockData ? createMockClient() : createMockClient();
export { useMockData };
export const supabase = createMockClient();