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:
@@ -2,21 +2,16 @@
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { pool } from "@/lib/db";
|
||||
import { uploadObject, BUCKETS } from "@/lib/storage";
|
||||
|
||||
export type UploadLogoResult =
|
||||
| { success: true; logoUrl: string }
|
||||
| { success: false; error: string };
|
||||
|
||||
/**
|
||||
* Upload a brand logo (light or dark variant) to Supabase Storage and
|
||||
* save the resulting public URL to the brand_settings row via the
|
||||
* Upload a brand logo (light or dark variant) to MinIO and save the
|
||||
* resulting public URL to the brand_settings row via the
|
||||
* `upsert_brand_settings` SECURITY DEFINER RPC.
|
||||
*
|
||||
* NOTE: the storage PUT itself is not a database call, so it still
|
||||
* goes through the Supabase Storage REST API. Storage migration to an
|
||||
* S3-compatible backend is tracked separately; until that lands, the
|
||||
* public URL pattern (`/storage/v1/object/public/...`) keeps working
|
||||
* for any pre-existing bucket.
|
||||
*/
|
||||
export async function uploadBrandLogo(
|
||||
brandId: string,
|
||||
@@ -42,37 +37,28 @@ export async function uploadBrandLogo(
|
||||
|
||||
const ext = file.type === "image/svg+xml" ? "svg" : file.type.split("/")[1];
|
||||
const path = isDark ? `logo-dark.${ext}` : `logo.${ext}`;
|
||||
const storagePath = `brand-logos/${brandId}/${path}`;
|
||||
const storageKey = `brand-logos/${brandId}/${path}`;
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
const uploadRes = await fetch(
|
||||
`${supabaseUrl}/storage/v1/object/brand-logos/${storagePath}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { apikey: supabaseKey, "Content-Type": file.type, "x-upsert": "true" },
|
||||
try {
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
const logoUrl = await uploadObject({
|
||||
bucket: BUCKETS.BRAND_LOGOS,
|
||||
key: storageKey,
|
||||
body: buffer,
|
||||
contentType: file.type,
|
||||
});
|
||||
|
||||
const saveOk = await callUpsertBrandSettings(brandId, {
|
||||
[isDark ? "p_logo_url_dark" : "p_logo_url"]: logoUrl,
|
||||
});
|
||||
if (!saveOk) {
|
||||
return { success: false, error: "Upload succeeded but failed to save URL" };
|
||||
}
|
||||
);
|
||||
|
||||
if (!uploadRes.ok) {
|
||||
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
||||
return { success: true, logoUrl };
|
||||
} catch (err) {
|
||||
return { success: false, error: `Upload failed: ${err instanceof Error ? err.message : String(err)}` };
|
||||
}
|
||||
|
||||
const publicUrl = `${supabaseUrl}/storage/v1/object/public/brand-logos/${storagePath}`;
|
||||
|
||||
const saveOk = await callUpsertBrandSettings(brandId, {
|
||||
[isDark ? "p_logo_url_dark" : "p_logo_url"]: publicUrl,
|
||||
});
|
||||
if (!saveOk) {
|
||||
return { success: false, error: "Upload succeeded but failed to save URL" };
|
||||
}
|
||||
|
||||
return { success: true, logoUrl: publicUrl };
|
||||
}
|
||||
|
||||
export async function uploadOlatheSweetLogo(
|
||||
@@ -97,37 +83,28 @@ export async function uploadOlatheSweetLogo(
|
||||
}
|
||||
|
||||
const ext = file.type === "image/svg+xml" ? "svg" : file.type.split("/")[1];
|
||||
const storagePath = `brand-logos/${brandId}/olathe-sweet-logo.${ext}`;
|
||||
const storageKey = `brand-logos/${brandId}/olathe-sweet-logo.${ext}`;
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
const uploadRes = await fetch(
|
||||
`${supabaseUrl}/storage/v1/object/brand-logos/${storagePath}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { apikey: supabaseKey, "Content-Type": file.type, "x-upsert": "true" },
|
||||
try {
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
const logoUrl = await uploadObject({
|
||||
bucket: BUCKETS.BRAND_LOGOS,
|
||||
key: storageKey,
|
||||
body: buffer,
|
||||
contentType: file.type,
|
||||
});
|
||||
|
||||
const saveOk = await callUpsertBrandSettings(brandId, {
|
||||
p_olathe_sweet_logo_url: logoUrl,
|
||||
});
|
||||
if (!saveOk) {
|
||||
return { success: false, error: "Upload succeeded but failed to save URL" };
|
||||
}
|
||||
);
|
||||
|
||||
if (!uploadRes.ok) {
|
||||
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
||||
return { success: true, logoUrl };
|
||||
} catch (err) {
|
||||
return { success: false, error: `Upload failed: ${err instanceof Error ? err.message : String(err)}` };
|
||||
}
|
||||
|
||||
const publicUrl = `${supabaseUrl}/storage/v1/object/public/brand-logos/${storagePath}`;
|
||||
|
||||
const saveOk = await callUpsertBrandSettings(brandId, {
|
||||
p_olathe_sweet_logo_url: publicUrl,
|
||||
});
|
||||
if (!saveOk) {
|
||||
return { success: false, error: "Upload succeeded but failed to save URL" };
|
||||
}
|
||||
|
||||
return { success: true, logoUrl: publicUrl };
|
||||
}
|
||||
|
||||
export async function uploadOlatheSweetLogoDark(
|
||||
@@ -152,37 +129,28 @@ export async function uploadOlatheSweetLogoDark(
|
||||
}
|
||||
|
||||
const ext = file.type === "image/svg+xml" ? "svg" : file.type.split("/")[1];
|
||||
const storagePath = `brand-logos/${brandId}/olathe-sweet-logo-dark.${ext}`;
|
||||
const storageKey = `brand-logos/${brandId}/olathe-sweet-logo-dark.${ext}`;
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
const uploadRes = await fetch(
|
||||
`${supabaseUrl}/storage/v1/object/brand-logos/${storagePath}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { apikey: supabaseKey, "Content-Type": file.type, "x-upsert": "true" },
|
||||
try {
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
const logoUrl = await uploadObject({
|
||||
bucket: BUCKETS.BRAND_LOGOS,
|
||||
key: storageKey,
|
||||
body: buffer,
|
||||
contentType: file.type,
|
||||
});
|
||||
|
||||
const saveOk = await callUpsertBrandSettings(brandId, {
|
||||
p_olathe_sweet_logo_url_dark: logoUrl,
|
||||
});
|
||||
if (!saveOk) {
|
||||
return { success: false, error: "Upload succeeded but failed to save URL" };
|
||||
}
|
||||
);
|
||||
|
||||
if (!uploadRes.ok) {
|
||||
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
||||
return { success: true, logoUrl };
|
||||
} catch (err) {
|
||||
return { success: false, error: `Upload failed: ${err instanceof Error ? err.message : String(err)}` };
|
||||
}
|
||||
|
||||
const publicUrl = `${supabaseUrl}/storage/v1/object/public/brand-logos/${storagePath}`;
|
||||
|
||||
const saveOk = await callUpsertBrandSettings(brandId, {
|
||||
p_olathe_sweet_logo_url_dark: publicUrl,
|
||||
});
|
||||
if (!saveOk) {
|
||||
return { success: false, error: "Upload succeeded but failed to save URL" };
|
||||
}
|
||||
|
||||
return { success: true, logoUrl: publicUrl };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user