From 5ff56467ea512c6fb118e254cc77fb011f80ea70 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 26 Jan 2026 14:00:22 -0800 Subject: [PATCH] error response improvements to logo url --- .../loginPage/upsertLoginPageBranding.ts | 67 +++++++++++++------ src/components/AuthPageBrandingForm.tsx | 67 +++++++++++++------ 2 files changed, 93 insertions(+), 41 deletions(-) diff --git a/server/private/routers/loginPage/upsertLoginPageBranding.ts b/server/private/routers/loginPage/upsertLoginPageBranding.ts index 8782db3d..e6e365be 100644 --- a/server/private/routers/loginPage/upsertLoginPageBranding.ts +++ b/server/private/routers/loginPage/upsertLoginPageBranding.ts @@ -37,27 +37,55 @@ const paramsSchema = z.strictObject({ const bodySchema = z.strictObject({ logoUrl: z .union([ - z.string().length(0), - z.url().refine( - async (url) => { + z.literal(""), + z + .url("Must be a valid URL") + .superRefine(async (url, ctx) => { try { - const response = await fetch(url); - return ( - response.status === 200 && - ( - response.headers.get("content-type") ?? "" - ).startsWith("image/") - ); + const response = await fetch(url, { + method: "HEAD" + }).catch(() => { + // If HEAD fails (CORS or method not allowed), try GET + return fetch(url, { method: "GET" }); + }); + + if (response.status !== 200) { + ctx.addIssue({ + code: "custom", + message: `Failed to load image. Please check that the URL is accessible.` + }); + return; + } + + const contentType = + response.headers.get("content-type") ?? ""; + if (!contentType.startsWith("image/")) { + ctx.addIssue({ + code: "custom", + message: `URL does not point to an image. Please provide a URL to an image file (e.g., .png, .jpg, .svg).` + }); + return; + } } catch (error) { - return false; + let errorMessage = + "Unable to verify image URL. Please check that the URL is accessible and points to an image file."; + + if (error instanceof TypeError && error.message.includes("fetch")) { + errorMessage = + "Network error: Unable to reach the URL. Please check your internet connection and verify the URL is correct."; + } else if (error instanceof Error) { + errorMessage = `Error verifying URL: ${error.message}`; + } + + ctx.addIssue({ + code: "custom", + message: errorMessage + }); } - }, - { - error: "Invalid logo URL, must be a valid image URL" - } - ) + }) ]) - .optional(), + .transform((val) => (val === "" ? null : val)) + .nullish(), logoWidth: z.coerce.number().min(1), logoHeight: z.coerce.number().min(1), resourceTitle: z.string(), @@ -117,9 +145,8 @@ export async function upsertLoginPageBranding( typeof loginPageBranding >; - if ((updateData.logoUrl ?? "").trim().length === 0) { - updateData.logoUrl = undefined; - } + // Empty strings are transformed to null by the schema, which will clear the logo URL in the database + // We keep it as null (not undefined) because undefined fields are omitted from Drizzle updates if ( build !== "saas" && diff --git a/src/components/AuthPageBrandingForm.tsx b/src/components/AuthPageBrandingForm.tsx index 4d070113..012c303a 100644 --- a/src/components/AuthPageBrandingForm.tsx +++ b/src/components/AuthPageBrandingForm.tsx @@ -43,25 +43,52 @@ export type AuthPageCustomizationProps = { const AuthPageFormSchema = z.object({ logoUrl: z.union([ - z.string().length(0), - z.url().refine( - async (url) => { - try { - const response = await fetch(url); - return ( - response.status === 200 && - (response.headers.get("content-type") ?? "").startsWith( - "image/" - ) - ); - } catch (error) { - return false; + z.literal(""), + z.url("Must be a valid URL").superRefine(async (url, ctx) => { + try { + const response = await fetch(url, { + method: "HEAD" + }).catch(() => { + // If HEAD fails (CORS or method not allowed), try GET + return fetch(url, { method: "GET" }); + }); + + if (response.status !== 200) { + ctx.addIssue({ + code: "custom", + message: `Failed to load image. Please check that the URL is accessible.` + }); + return; } - }, - { - error: "Invalid logo URL, must be a valid image URL" + + const contentType = response.headers.get("content-type") ?? ""; + if (!contentType.startsWith("image/")) { + ctx.addIssue({ + code: "custom", + message: `URL does not point to an image. Please provide a URL to an image file (e.g., .png, .jpg, .svg).` + }); + return; + } + } catch (error) { + let errorMessage = + "Unable to verify image URL. Please check that the URL is accessible and points to an image file."; + + if ( + error instanceof TypeError && + error.message.includes("fetch") + ) { + errorMessage = + "Network error: Unable to reach the URL. Please check your internet connection and verify the URL is correct."; + } else if (error instanceof Error) { + errorMessage = `Error verifying URL: ${error.message}`; + } + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: errorMessage + }); } - ) + }) ]), logoWidth: z.coerce.number().min(1), logoHeight: z.coerce.number().min(1), @@ -405,9 +432,7 @@ export default function AuthPageBrandingForm({