2f3be5426f
Deploy to route.crispygoat.com / deploy (push) Failing after 2m50s
The /indian-river-direct/stops page and sitemap prerender at build time and call getPublicStopsForBrand / getActiveStopsForSitemap / getBrandSettingsPublic. Those actions fetch NEXT_PUBLIC_SUPABASE_URL via Supabase REST. During the GitHub/Gitea build, the Supabase secret is unset (or the value is ".supabase.co" which doesn't resolve), so the fetch errors with ECONNREFUSED and the build aborts. Return [] / not-configured when the env vars are missing so the prerender can complete. Runtime behavior is unchanged when the vars are set.
401 lines
14 KiB
TypeScript
401 lines
14 KiB
TypeScript
"use server";
|
|
|
|
import { getAdminUser } from "@/lib/admin-permissions";
|
|
import { svcHeaders } from "@/lib/svc-headers";
|
|
|
|
export type UploadLogoResult =
|
|
| { success: true; logoUrl: string }
|
|
| { success: false; error: string };
|
|
|
|
export async function uploadBrandLogo(
|
|
brandId: string,
|
|
file: File,
|
|
isDark: boolean = false
|
|
): Promise<UploadLogoResult> {
|
|
const adminUser = await getAdminUser();
|
|
if (!adminUser) return { success: false, error: "Not authenticated" };
|
|
if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") {
|
|
return { success: false, error: "Not authorized" };
|
|
}
|
|
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
|
|
return { success: false, error: "Not authorized for this brand" };
|
|
}
|
|
|
|
const validTypes = ["image/png", "image/jpeg", "image/webp", "image/svg+xml"];
|
|
if (!validTypes.includes(file.type)) {
|
|
return { success: false, error: "Invalid file type. Use PNG, JPEG, WebP, or SVG." };
|
|
}
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
return { success: false, error: "File too large. Max 5MB." };
|
|
}
|
|
|
|
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 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: { ...svcHeaders(supabaseKey), "Content-Type": file.type, "x-upsert": "true" },
|
|
body: buffer,
|
|
}
|
|
);
|
|
|
|
if (!uploadRes.ok) {
|
|
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
|
}
|
|
|
|
const publicUrl = `${supabaseUrl}/storage/v1/object/public/brand-logos/${storagePath}`;
|
|
|
|
// Save URL to brand_settings via RPC
|
|
const saveRes = await fetch(
|
|
`${supabaseUrl}/rest/v1/rpc/upsert_brand_settings`,
|
|
{
|
|
method: "POST",
|
|
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
p_brand_id: brandId,
|
|
[isDark ? "p_logo_url_dark" : "p_logo_url"]: publicUrl,
|
|
}),
|
|
}
|
|
);
|
|
|
|
if (!saveRes.ok) {
|
|
return { success: false, error: "Upload succeeded but failed to save URL" };
|
|
}
|
|
|
|
return { success: true, logoUrl: publicUrl };
|
|
}
|
|
|
|
export async function uploadOlatheSweetLogo(
|
|
brandId: string,
|
|
file: File
|
|
): Promise<UploadLogoResult> {
|
|
const adminUser = await getAdminUser();
|
|
if (!adminUser) return { success: false, error: "Not authenticated" };
|
|
if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") {
|
|
return { success: false, error: "Not authorized" };
|
|
}
|
|
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
|
|
return { success: false, error: "Not authorized for this brand" };
|
|
}
|
|
|
|
const validTypes = ["image/png", "image/jpeg", "image/webp", "image/svg+xml"];
|
|
if (!validTypes.includes(file.type)) {
|
|
return { success: false, error: "Invalid file type. Use PNG, JPEG, WebP, or SVG." };
|
|
}
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
return { success: false, error: "File too large. Max 5MB." };
|
|
}
|
|
|
|
const ext = file.type === "image/svg+xml" ? "svg" : file.type.split("/")[1];
|
|
const storagePath = `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: { ...svcHeaders(supabaseKey), "Content-Type": file.type, "x-upsert": "true" },
|
|
body: buffer,
|
|
}
|
|
);
|
|
|
|
if (!uploadRes.ok) {
|
|
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
|
}
|
|
|
|
const publicUrl = `${supabaseUrl}/storage/v1/object/public/brand-logos/${storagePath}`;
|
|
|
|
const saveRes = await fetch(
|
|
`${supabaseUrl}/rest/v1/rpc/upsert_brand_settings`,
|
|
{
|
|
method: "POST",
|
|
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
p_brand_id: brandId,
|
|
p_olathe_sweet_logo_url: publicUrl,
|
|
}),
|
|
}
|
|
);
|
|
|
|
if (!saveRes.ok) {
|
|
return { success: false, error: "Upload succeeded but failed to save URL" };
|
|
}
|
|
|
|
return { success: true, logoUrl: publicUrl };
|
|
}
|
|
|
|
export async function uploadOlatheSweetLogoDark(
|
|
brandId: string,
|
|
file: File
|
|
): Promise<UploadLogoResult> {
|
|
const adminUser = await getAdminUser();
|
|
if (!adminUser) return { success: false, error: "Not authenticated" };
|
|
if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") {
|
|
return { success: false, error: "Not authorized" };
|
|
}
|
|
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
|
|
return { success: false, error: "Not authorized for this brand" };
|
|
}
|
|
|
|
const validTypes = ["image/png", "image/jpeg", "image/webp", "image/svg+xml"];
|
|
if (!validTypes.includes(file.type)) {
|
|
return { success: false, error: "Invalid file type. Use PNG, JPEG, WebP, or SVG." };
|
|
}
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
return { success: false, error: "File too large. Max 5MB." };
|
|
}
|
|
|
|
const ext = file.type === "image/svg+xml" ? "svg" : file.type.split("/")[1];
|
|
const storagePath = `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: { ...svcHeaders(supabaseKey), "Content-Type": file.type, "x-upsert": "true" },
|
|
body: buffer,
|
|
}
|
|
);
|
|
|
|
if (!uploadRes.ok) {
|
|
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
|
}
|
|
|
|
const publicUrl = `${supabaseUrl}/storage/v1/object/public/brand-logos/${storagePath}`;
|
|
|
|
const saveRes = await fetch(
|
|
`${supabaseUrl}/rest/v1/rpc/upsert_brand_settings`,
|
|
{
|
|
method: "POST",
|
|
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
p_brand_id: brandId,
|
|
p_olathe_sweet_logo_url_dark: publicUrl,
|
|
}),
|
|
}
|
|
);
|
|
|
|
if (!saveRes.ok) {
|
|
return { success: false, error: "Upload succeeded but failed to save URL" };
|
|
}
|
|
|
|
return { success: true, logoUrl: publicUrl };
|
|
}
|
|
|
|
export type BrandSettings = {
|
|
id?: string;
|
|
brand_id: string;
|
|
brand_name: string;
|
|
legal_business_name: string | null;
|
|
phone: string | null;
|
|
email: string | null;
|
|
website_url: string | null;
|
|
street_address: string | null;
|
|
city: string | null;
|
|
state: string | null;
|
|
postal_code: string | null;
|
|
country: string | null;
|
|
logo_url: string | null;
|
|
logo_url_dark: string | null;
|
|
olathe_sweet_logo_url: string | null;
|
|
olathe_sweet_logo_url_dark: string | null;
|
|
default_email_signature: string | null;
|
|
invoice_footer_notes: string | null;
|
|
// Storefront customization fields
|
|
hero_tagline: string | null;
|
|
about_headline: string | null;
|
|
about_subheadline: string | null;
|
|
custom_footer_text: string | null;
|
|
show_wholesale_link: boolean | null;
|
|
show_zip_search: boolean | null;
|
|
show_schedule_pdf: boolean | null;
|
|
show_text_alerts: boolean | null;
|
|
schedule_pdf_notes: string | null;
|
|
hero_image_url: string | null;
|
|
// Color customization
|
|
brand_primary_color: string | null;
|
|
brand_secondary_color: string | null;
|
|
brand_bg_color: string | null;
|
|
brand_text_color: string | null;
|
|
// Tax settings
|
|
collect_sales_tax: boolean | null;
|
|
nexus_states: string[] | null;
|
|
updated_at?: string;
|
|
};
|
|
|
|
export type GetBrandSettingsResult =
|
|
| { success: true; settings: BrandSettings | null }
|
|
| { success: false; error: string };
|
|
|
|
export type SaveBrandSettingsResult =
|
|
| { success: true; settings: BrandSettings }
|
|
| { success: false; error: string };
|
|
|
|
export async function getBrandSettings(brandId: string): Promise<GetBrandSettingsResult> {
|
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
|
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
|
|
|
const response = await fetch(
|
|
`${supabaseUrl}/rest/v1/rpc/get_brand_settings`,
|
|
{
|
|
method: "POST",
|
|
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
|
body: JSON.stringify({ p_brand_id: brandId }),
|
|
}
|
|
);
|
|
|
|
if (!response.ok) return { success: false, error: "Failed to fetch brand settings" };
|
|
const data = await response.json();
|
|
return { success: true, settings: data };
|
|
}
|
|
|
|
// Public version for storefront pages — uses slug, no auth required
|
|
export async function getBrandSettingsPublic(brandSlug: string): Promise<GetBrandSettingsResult & { wholesaleEnabled?: boolean | null }> {
|
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
// Build-time prerender runs before Supabase env is configured. Return
|
|
// a not-configured result; the page falls back to slug-based defaults.
|
|
if (!supabaseUrl || !supabaseKey) {
|
|
return { success: false, error: "Supabase not configured", wholesaleEnabled: undefined };
|
|
}
|
|
|
|
const response = await fetch(
|
|
`${supabaseUrl}/rest/v1/rpc/get_brand_settings_by_slug`,
|
|
{
|
|
method: "POST",
|
|
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
|
body: JSON.stringify({ p_brand_slug: brandSlug }),
|
|
}
|
|
);
|
|
|
|
if (!response.ok) return { success: false, error: "Failed to fetch brand settings", wholesaleEnabled: undefined };
|
|
const data = await response.json();
|
|
return {
|
|
success: true,
|
|
settings: data,
|
|
wholesaleEnabled: data?.wholesale_enabled,
|
|
};
|
|
}
|
|
|
|
export async function saveBrandSettings(params: {
|
|
brandId: string;
|
|
legalBusinessName?: string;
|
|
phone?: string;
|
|
email?: string;
|
|
websiteUrl?: string;
|
|
streetAddress?: string;
|
|
city?: string;
|
|
state?: string;
|
|
postalCode?: string;
|
|
country?: string;
|
|
logoUrl?: string;
|
|
logoUrlDark?: string;
|
|
olatheSweetLogoUrl?: string;
|
|
olatheSweetLogoUrlDark?: string;
|
|
defaultEmailSignature?: string;
|
|
invoiceFooterNotes?: string;
|
|
// Storefront customization fields
|
|
heroTagline?: string;
|
|
aboutHeadline?: string;
|
|
aboutSubheadline?: string;
|
|
customFooterText?: string;
|
|
showWholesaleLink?: boolean;
|
|
showZipSearch?: boolean;
|
|
showSchedulePdf?: boolean;
|
|
showTextAlerts?: boolean;
|
|
schedulePdfNotes?: string;
|
|
heroImageUrl?: string;
|
|
brandPrimaryColor?: string;
|
|
brandSecondaryColor?: string;
|
|
brandBgColor?: string;
|
|
brandTextColor?: string;
|
|
collectSalesTax?: boolean;
|
|
nexusStates?: string[];
|
|
}): Promise<SaveBrandSettingsResult> {
|
|
const adminUser = await getAdminUser();
|
|
if (!adminUser) return { success: false, error: "Not authenticated" };
|
|
if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") {
|
|
return { success: false, error: "Not authorized" };
|
|
}
|
|
|
|
if (adminUser.role === "brand_admin" && adminUser.brand_id !== params.brandId) {
|
|
return { success: false, error: "Not authorized for this brand" };
|
|
}
|
|
|
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
|
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
|
|
|
const response = await fetch(
|
|
`${supabaseUrl}/rest/v1/rpc/upsert_brand_settings`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
...svcHeaders(supabaseKey),
|
|
"Content-Type": "application/json",
|
|
Prefer: "return=representation",
|
|
},
|
|
body: JSON.stringify({
|
|
p_brand_id: params.brandId,
|
|
p_legal_business_name: params.legalBusinessName ?? null,
|
|
p_phone: params.phone ?? null,
|
|
p_email: params.email ?? null,
|
|
p_website_url: params.websiteUrl ?? null,
|
|
p_street_address: params.streetAddress ?? null,
|
|
p_city: params.city ?? null,
|
|
p_state: params.state ?? null,
|
|
p_postal_code: params.postalCode ?? null,
|
|
p_country: params.country ?? null,
|
|
p_logo_url: params.logoUrl ?? null,
|
|
p_logo_url_dark: params.logoUrlDark ?? null,
|
|
p_olathe_sweet_logo_url: params.olatheSweetLogoUrl ?? null,
|
|
p_olathe_sweet_logo_url_dark: params.olatheSweetLogoUrlDark ?? null,
|
|
p_default_email_signature: params.defaultEmailSignature ?? null,
|
|
p_invoice_footer_notes: params.invoiceFooterNotes ?? null,
|
|
p_hero_tagline: params.heroTagline ?? null,
|
|
p_about_headline: params.aboutHeadline ?? null,
|
|
p_about_subheadline: params.aboutSubheadline ?? null,
|
|
p_custom_footer_text: params.customFooterText ?? null,
|
|
p_show_wholesale_link: params.showWholesaleLink ?? null,
|
|
p_show_zip_search: params.showZipSearch ?? null,
|
|
p_show_schedule_pdf: params.showSchedulePdf ?? null,
|
|
p_show_text_alerts: params.showTextAlerts ?? null,
|
|
p_schedule_pdf_notes: params.schedulePdfNotes ?? null,
|
|
p_hero_image_url: params.heroImageUrl ?? null,
|
|
p_brand_primary_color: params.brandPrimaryColor ?? null,
|
|
p_brand_secondary_color: params.brandSecondaryColor ?? null,
|
|
p_brand_bg_color: params.brandBgColor ?? null,
|
|
p_brand_text_color: params.brandTextColor ?? null,
|
|
p_collect_sales_tax: params.collectSalesTax ?? null,
|
|
p_nexus_states: params.nexusStates ?? null,
|
|
}),
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
const err = await response.text();
|
|
return { success: false, error: `Failed to save: ${err.slice(0, 200)}` };
|
|
}
|
|
|
|
const data = await response.json();
|
|
return { success: true, settings: data };
|
|
} |