8df292dfee
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).
47 lines
1.9 KiB
TypeScript
47 lines
1.9 KiB
TypeScript
import { S3Client, PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
|
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",
|
|
secretAccessKey: process.env["STORAGE_SECRET_KEY"] ?? "minioadmin",
|
|
};
|
|
|
|
const s3 = new S3Client({
|
|
region: process.env["STORAGE_REGION"] ?? "us-east-1",
|
|
endpoint,
|
|
forcePathStyle: true,
|
|
credentials,
|
|
});
|
|
|
|
// Presigned URLs are PUT/GET directly by the browser, so they must be signed
|
|
// against an endpoint the browser can actually resolve — not the internal
|
|
// docker hostname `s3` (and `endpoint`) use for server-to-server calls. Signing
|
|
// is pure crypto (no network call), so a second client pointed at the public
|
|
// URL is safe even though it's never used to make a real request itself.
|
|
const presignS3 = new S3Client({
|
|
region: process.env["STORAGE_REGION"] ?? "us-east-1",
|
|
endpoint: publicUrl,
|
|
forcePathStyle: true,
|
|
credentials,
|
|
});
|
|
|
|
export async function createPresignedUploadUrl(key: string, contentType: string): Promise<string> {
|
|
const command = new PutObjectCommand({ Bucket: bucket, Key: key, ContentType: contentType });
|
|
return getSignedUrl(presignS3, command, { expiresIn: 300 });
|
|
}
|
|
|
|
export async function deleteObject(key: string): Promise<void> {
|
|
await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
|
}
|
|
|
|
export function getPublicUrl(key: string): string {
|
|
return `${clientPublicUrl}/${bucket}/${key}`;
|
|
}
|