From 58f38d92edc22ec16a2b3fc8fb0f1a2b9d6d9449 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Tue, 7 Apr 2026 12:38:08 -0500 Subject: [PATCH] Fix broken images by proxying through the app instead of redirecting to internal MinIO The presigned URLs pointed to a private IP unreachable from the browser. The image route now streams bytes directly and the card API returns proxy paths matching the dashboard's existing approach. Made-with: Cursor --- src/app/api/cards/[id]/route.ts | 12 +++++++----- src/app/api/images/[...path]/route.ts | 24 +++++++++++++++++++++--- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/app/api/cards/[id]/route.ts b/src/app/api/cards/[id]/route.ts index 47933fe..c676e1a 100644 --- a/src/app/api/cards/[id]/route.ts +++ b/src/app/api/cards/[id]/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/db"; -import { getPresignedUrl, deleteObject } from "@/lib/minio"; +import { deleteObject } from "@/lib/minio"; import { fireIntegrationEvent } from "@/lib/integrations"; import { logActivity, diffCardFields } from "@/lib/activity-log"; @@ -18,10 +18,12 @@ export async function GET( return NextResponse.json({ error: "Card not found" }, { status: 404 }); } - const [frontImageUrl, backImageUrl] = await Promise.all([ - card.frontImagePath ? getPresignedUrl(card.frontImagePath) : null, - card.backImagePath ? getPresignedUrl(card.backImagePath) : null, - ]); + const frontImageUrl = card.frontImagePath + ? `/api/images/${card.frontImagePath}` + : null; + const backImageUrl = card.backImagePath + ? `/api/images/${card.backImagePath}` + : null; return NextResponse.json({ ...card, diff --git a/src/app/api/images/[...path]/route.ts b/src/app/api/images/[...path]/route.ts index 92b6b66..eb15ed9 100644 --- a/src/app/api/images/[...path]/route.ts +++ b/src/app/api/images/[...path]/route.ts @@ -1,5 +1,14 @@ import { NextRequest, NextResponse } from "next/server"; -import { getPresignedUrl } from "@/lib/minio"; +import { getBuffer } from "@/lib/minio"; + +const EXT_TO_CONTENT_TYPE: Record = { + jpg: "image/jpeg", + jpeg: "image/jpeg", + png: "image/png", + webp: "image/webp", + gif: "image/gif", + pdf: "application/pdf", +}; export async function GET( _request: NextRequest, @@ -13,8 +22,17 @@ export async function GET( return NextResponse.json({ error: "Missing path" }, { status: 400 }); } - const url = await getPresignedUrl(key); - return NextResponse.redirect(url); + const ext = key.split(".").pop()?.toLowerCase() ?? ""; + const contentType = EXT_TO_CONTENT_TYPE[ext] || "application/octet-stream"; + + const buffer = await getBuffer(key); + + return new NextResponse(buffer, { + headers: { + "Content-Type": contentType, + "Cache-Control": "private, max-age=3600", + }, + }); } catch (error) { console.error("[images GET]", error); return NextResponse.json(