From 8df292dfee1b40d3750d8a513745369be7c93edc Mon Sep 17 00:00:00 2001 From: Arnaud Date: Sun, 12 Jul 2026 13:53:15 +0200 Subject: [PATCH] fix: photo thumbnails always used localhost:9000 in production MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getPublicUrl() runs in the browser (called from client components rendering recipe thumbnails), but read the plain STORAGE_PUBLIC_URL env var — never inlined into the client bundle, so every browser fell back to the hardcoded localhost:9000 default regardless of the real deployed storage domain, tripping CSP img-src and mixed-content blocks in production. Added a NEXT_PUBLIC_STORAGE_PUBLIC_URL build arg (Dockerfile, compose.prod.yml) wired from the same STORAGE_PUBLIC_URL value, and getPublicUrl() now reads that. Verified locally: building with a fake public storage domain set shows it correctly inlined into the client JS chunk (previously only the localhost fallback ever appeared there). --- Dockerfile | 5 +++++ apps/web/lib/storage.ts | 5 ++++- compose.prod.yml | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c0da1eb..0b09aa3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,6 +44,11 @@ ENV NEXT_PUBLIC_AUTHENTIK_ENABLED=$NEXT_PUBLIC_AUTHENTIK_ENABLED # also needs to be a build arg, not just a runtime env var (unlike STORAGE_ENDPOINT). ARG STORAGE_PUBLIC_URL ENV STORAGE_PUBLIC_URL=$STORAGE_PUBLIC_URL +# lib/storage.ts's getPublicUrl() is called from client components to render photo +# tags — a plain (non-NEXT_PUBLIC_) env var is never inlined into the browser +# bundle, so it must be duplicated under a NEXT_PUBLIC_ name to reach the client. +ARG NEXT_PUBLIC_STORAGE_PUBLIC_URL +ENV NEXT_PUBLIC_STORAGE_PUBLIC_URL=$NEXT_PUBLIC_STORAGE_PUBLIC_URL RUN pnpm --filter web build # ---- runtime ---- diff --git a/apps/web/lib/storage.ts b/apps/web/lib/storage.ts index b50a828..e66cf9d 100644 --- a/apps/web/lib/storage.ts +++ b/apps/web/lib/storage.ts @@ -4,6 +4,9 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; const bucket = process.env["STORAGE_BUCKET"] || "epicure-uploads"; const endpoint = process.env["STORAGE_ENDPOINT"] || "http://localhost:9000"; const publicUrl = process.env["STORAGE_PUBLIC_URL"] || "http://localhost:9000"; +// getPublicUrl() below runs in the browser too — non-NEXT_PUBLIC_ vars are never +// inlined into the client bundle, so it needs its own NEXT_PUBLIC_ copy of the value. +const clientPublicUrl = process.env.NEXT_PUBLIC_STORAGE_PUBLIC_URL || "http://localhost:9000"; const credentials = { accessKeyId: process.env["STORAGE_ACCESS_KEY"] ?? "minioadmin", @@ -39,5 +42,5 @@ export async function deleteObject(key: string): Promise { } export function getPublicUrl(key: string): string { - return `${publicUrl}/${bucket}/${key}`; + return `${clientPublicUrl}/${bucket}/${key}`; } diff --git a/compose.prod.yml b/compose.prod.yml index 0c226a3..c0fadc4 100644 --- a/compose.prod.yml +++ b/compose.prod.yml @@ -85,6 +85,7 @@ services: NEXT_PUBLIC_DISCORD_ENABLED: ${NEXT_PUBLIC_DISCORD_ENABLED} NEXT_PUBLIC_AUTHENTIK_ENABLED: ${NEXT_PUBLIC_AUTHENTIK_ENABLED} STORAGE_PUBLIC_URL: ${STORAGE_PUBLIC_URL} + NEXT_PUBLIC_STORAGE_PUBLIC_URL: ${STORAGE_PUBLIC_URL} restart: always environment: DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}