From fdfd1f3d032f5a5a38aaa5ab5ee5873b99ce078d Mon Sep 17 00:00:00 2001 From: Rees Morris Date: Mon, 7 Jul 2025 21:06:51 +0100 Subject: [PATCH] feat: add S3_FORCE_PATH_STYLE configuration for MinIO compatibility (#100) * feat: add S3_FORCE_PATH_STYLE configuration for MinIO compatibility * docs: add S3_FORCE_PATH_STYLE to env example --- .env.example | 1 + README.md | 1 + apps/web/next.config.js | 5 ++++- apps/web/src/env.ts | 1 + apps/web/src/pages/api/upload/image.ts | 2 ++ cloud/docker-compose.yml | 1 + docker-compose.yml | 1 + packages/auth/src/auth.ts | 1 + turbo.json | 1 + 9 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index d874055a..42668521 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,7 @@ S3_REGION= S3_ENDPOINT= S3_ACCESS_KEY_ID= S3_SECRET_ACCESS_KEY= +S3_FORCE_PATH_STYLE= NEXT_PUBLIC_STORAGE_URL= NEXT_PUBLIC_AVATAR_BUCKET_NAME= NEXT_PUBLIC_STORAGE_DOMAIN= diff --git a/README.md b/README.md index d0d5b05b..805b8108 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,7 @@ pnpm dev | `S3_ENDPOINT` | S3 endpoint URL | For file uploads | `https://xxx.r2.cloudflarestorage.com` | | `S3_ACCESS_KEY_ID` | S3 access key | For file uploads | `xxx` | | `S3_SECRET_ACCESS_KEY` | S3 secret key | For file uploads | `xxx` | +| `S3_FORCE_PATH_STYLE` | Use path-style URLs for S3 | For file uploads | `true` | | `NEXT_PUBLIC_STORAGE_URL` | Storage service URL | For file uploads | `https://storage.kanbn.com` | | `NEXT_PUBLIC_STORAGE_DOMAIN` | Storage domain name | For file uploads | `kanbn.com` | | `NEXT_PUBLIC_AVATAR_BUCKET_NAME` | S3 bucket name for avatars | For file uploads | `avatars` | diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 91c807ee..a174c534 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -31,9 +31,12 @@ const config = { images: { remotePatterns: [ + { protocol: "https", - hostname: `*.${env("NEXT_PUBLIC_STORAGE_DOMAIN")}`, + hostname: process.env.S3_FORCE_PATH_STYLE === "true" + ? `${env("NEXT_PUBLIC_STORAGE_DOMAIN")}` + : `*.${env("NEXT_PUBLIC_STORAGE_DOMAIN")}`, }, { protocol: "http", diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts index 3e4cb490..a3cba6ae 100644 --- a/apps/web/src/env.ts +++ b/apps/web/src/env.ts @@ -69,6 +69,7 @@ export const env = createEnv({ S3_SECRET_ACCESS_KEY: z.string().optional(), S3_REGION: z.string().optional(), S3_ENDPOINT: z.string().optional(), + S3_FORCE_PATH_STYLE: z.string().optional(), EMAIL_FROM: z.string().optional(), }, diff --git a/apps/web/src/pages/api/upload/image.ts b/apps/web/src/pages/api/upload/image.ts index 758a2ced..2633023a 100644 --- a/apps/web/src/pages/api/upload/image.ts +++ b/apps/web/src/pages/api/upload/image.ts @@ -46,6 +46,7 @@ export default async function handler( const client = new S3Client({ region: env.S3_REGION ?? "", endpoint: env.S3_ENDPOINT ?? "", + forcePathStyle: env.S3_FORCE_PATH_STYLE === "true", credentials: { accessKeyId: env.S3_ACCESS_KEY_ID ?? "", secretAccessKey: env.S3_SECRET_ACCESS_KEY ?? "", @@ -58,6 +59,7 @@ export default async function handler( new PutObjectCommand({ Bucket: nextRuntimeEnv("NEXT_PUBLIC_AVATAR_BUCKET_NAME") ?? "", Key: filename, + ACL: "public-read", }), ); diff --git a/cloud/docker-compose.yml b/cloud/docker-compose.yml index b74def0b..faa8e230 100644 --- a/cloud/docker-compose.yml +++ b/cloud/docker-compose.yml @@ -36,6 +36,7 @@ services: - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} - S3_REGION=${S3_REGION} - S3_ENDPOINT=${S3_ENDPOINT} + - S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE} - NEXT_PUBLIC_STORAGE_URL=${NEXT_PUBLIC_STORAGE_URL} - NEXT_PUBLIC_AVATAR_BUCKET_NAME=${NEXT_PUBLIC_AVATAR_BUCKET_NAME} - NEXT_PUBLIC_STORAGE_DOMAIN=${NEXT_PUBLIC_STORAGE_DOMAIN} diff --git a/docker-compose.yml b/docker-compose.yml index afc1a137..89e54df0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,7 @@ services: - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} - S3_REGION=${S3_REGION} - S3_ENDPOINT=${S3_ENDPOINT} + - S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE} - NEXT_PUBLIC_STORAGE_URL=${NEXT_PUBLIC_STORAGE_URL} - NEXT_PUBLIC_AVATAR_BUCKET_NAME=${NEXT_PUBLIC_AVATAR_BUCKET_NAME} - NEXT_PUBLIC_STORAGE_DOMAIN=${NEXT_PUBLIC_STORAGE_DOMAIN} diff --git a/packages/auth/src/auth.ts b/packages/auth/src/auth.ts index 496a5c4d..5fd18d5b 100644 --- a/packages/auth/src/auth.ts +++ b/packages/auth/src/auth.ts @@ -181,6 +181,7 @@ export const initAuth = (db: dbClient) => { const client = new S3Client({ region: env("S3_REGION") ?? "", endpoint: env("S3_ENDPOINT") ?? "", + forcePathStyle: env("S3_FORCE_PATH_STYLE") === "true", credentials: { accessKeyId: env("S3_ACCESS_KEY_ID") ?? "", secretAccessKey: env("S3_SECRET_ACCESS_KEY") ?? "", diff --git a/turbo.json b/turbo.json index 54918eae..93e8d331 100644 --- a/turbo.json +++ b/turbo.json @@ -106,6 +106,7 @@ "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY", "S3_ENDPOINT", + "S3_FORCE_PATH_STYLE", "PORT", "BETTER_AUTH_SECRET", "BETTER_AUTH_TRUSTED_ORIGINS"