import { type NextRequest, NextResponse } from "next/server"; import { eq } from "drizzle-orm"; import { getObject } from "~/lib/object-storage"; import { db } from "~/server/db"; import { businesses } from "~/server/db/schema"; export const runtime = "nodejs"; const RASTERIZABLE_MIME_TYPES = new Set(["image/svg+xml", "image/webp"]); // Intentionally unauthenticated: a business logo must be viewable on public, // token-based invoice pages without a session. Business IDs are random // UUIDs, so this only serves images to callers who already know the ID. export async function GET( req: NextRequest, { params }: { params: Promise<{ businessId: string }> }, ) { const { businessId } = await params; const business = await db.query.businesses.findFirst({ where: eq(businesses.id, businessId), columns: { logoStorageKey: true, logoMimeType: true }, }); if (!business?.logoStorageKey || !business.logoMimeType) { return NextResponse.json({ error: "Not found" }, { status: 404 }); } // @react-pdf/renderer's Image component only decodes PNG/JPEG, so PDF // generation requests a rasterized copy of SVG/WebP logos via this param. const wantsPng = new URL(req.url).searchParams.get("format") === "png" && RASTERIZABLE_MIME_TYPES.has(business.logoMimeType); try { const body = await getObject(business.logoStorageKey); if (wantsPng) { const { default: sharp } = await import("sharp"); const isSvg = business.logoMimeType === "image/svg+xml"; // SVG is vector: rasterize at a high density so the PNG stays crisp at // the size it's actually displayed (PDF header, up to ~2.2in wide). // withoutEnlargement only makes sense for the WebP (already-raster) // case — for SVG it would cap us at whatever tiny canvas the source's // intrinsic viewBox implies, even though the vector has no such limit. const png = await sharp(body, isSvg ? { density: 600 } : undefined) .resize({ width: 1024, height: 1024, fit: "inside", withoutEnlargement: !isSvg, }) .png() .toBuffer(); return new NextResponse(new Uint8Array(png), { headers: { "Content-Type": "image/png", "Cache-Control": "public, max-age=300, must-revalidate", "X-Content-Type-Options": "nosniff", }, }); } return new NextResponse(new Uint8Array(body), { headers: { "Content-Type": business.logoMimeType, "Cache-Control": "public, max-age=300, must-revalidate", "X-Content-Type-Options": "nosniff", }, }); } catch (error) { console.error("[business-logo] Failed to serve logo", { backendError: error, businessId, wantsPng, }); return NextResponse.json({ error: "Logo not found" }, { status: 404 }); } }