From 2af1671891e0b30e2645e1ca916fdf8b66cd0008 Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Wed, 22 Apr 2026 03:08:36 +0000 Subject: [PATCH] =?UTF-8?q?fix(GRO-870):=20/api/branding=20returns=20raw?= =?UTF-8?q?=20S3=20URL=20=E2=80=94=20add=20public=20logo=20proxy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add GET /api/branding/logo as a public endpoint that proxies logo bytes from S3, and change /api/branding to return logoUrl: "/api/branding/logo" instead of calling getPresignedGetUrl(). Eliminates mixed-content warnings when the branding context is consumed on unauthenticated pages (portal, login). Co-Authored-By: Paperclip --- apps/api/src/index.ts | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 6d48d66..1ed08f2 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -19,7 +19,7 @@ import { impersonationRouter } from "./routes/impersonation.js"; import { settingsRouter } from "./routes/settings.js"; import { authProviderRouter } from "./routes/authProvider.js"; import { searchRouter } from "./routes/search.js"; -import { getPresignedGetUrl } from "./lib/s3.js"; +import { getObject } from "./lib/s3.js"; import { calendarRouter } from "./routes/calendar.js"; import { setupRouter } from "./routes/setup.js"; import { getDb, businessSettings, eq, staff } from "@groombook/db"; @@ -126,20 +126,31 @@ function validateLogoMagicBytes( } } +// Public logo proxy — no auth required, streams logo from S3 so browser never sees raw S3 URL +app.get("/api/branding/logo", async (c) => { + const db = getDb(); + const [row] = await db.select().from(businessSettings).limit(1); + if (!row) return c.json({ error: "Settings not found" }, 404); + if (!row.logoKey) return c.json({ error: "No logo on file" }, 404); + + const { body, contentType } = await getObject(row.logoKey); + return new Response(Buffer.from(body), { + status: 200, + headers: { + "Content-Type": contentType, + "Cache-Control": "public, max-age=86400", + }, + }); +}); + // Public branding endpoint — no auth required, returns business name/colors/logo app.get("/api/branding", async (c) => { const db = getDb(); const [row] = await db.select().from(businessSettings).limit(1); const settings = row ?? { businessName: "GroomBook", primaryColor: "#4f8a6f", accentColor: "#8b7355", logoBase64: null, logoMimeType: null, logoKey: null }; - let logoUrl: string | null = null; - if (settings.logoKey) { - try { - logoUrl = await getPresignedGetUrl(settings.logoKey); - } catch { - // If S3 URL generation fails, fall back to legacy base64 - } - } + // Return the public proxy path so browser never sees a raw S3 URL + const logoUrl = settings.logoKey ? "/api/branding/logo" : null; // Defensive: validate magic bytes to prevent MIME type confusion attacks // via the legacy base64 logo fields