From d23ecc3e17ba9a5a53e13fdb00ac3e8aa244c39b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:15:14 +0000 Subject: [PATCH] fix: use public endpoint for generated presigned storage URLs * Added `S3_PUBLIC_ENDPOINT` environment configuration to separate the internal MinIO endpoint used by the backend from the public URL seen by browsers. * Updated Zod environment validation and defaults. * Instantiated a separate `publicClient` in `S3ObjectStorageAdapter` initialized with this public endpoint. * `createUploadUrl` and `createDownloadUrl` now use `publicClient`, ensuring correct public-facing URLs. Co-authored-by: fdaei <81993335+fdaei@users.noreply.github.com> --- .env.example | 1 + apps/api/src/config/defaults.ts | 1 + apps/api/src/config/env.validation.ts | 1 + apps/api/src/config/files.config.ts | 1 + .../storage/s3-object-storage.adapter.ts | 11 +++++++++-- 5 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index eb49142..8880a14 100644 --- a/.env.example +++ b/.env.example @@ -34,6 +34,7 @@ PLAYWRIGHT_SERVER_TIMEOUT_MS=120000 LOAD_API_URL=http://localhost:4001/api SOAK_SECONDS=7200 S3_ENDPOINT=http://localhost:19000 +S3_PUBLIC_ENDPOINT=http://localhost:19000 S3_ACCESS_KEY=minio S3_SECRET_KEY=change-me S3_BUCKET=lingospeak diff --git a/apps/api/src/config/defaults.ts b/apps/api/src/config/defaults.ts index 07ddb14..92b505d 100644 --- a/apps/api/src/config/defaults.ts +++ b/apps/api/src/config/defaults.ts @@ -26,6 +26,7 @@ export const defaultConfig = { }, storage: { endpoint: 'http://localhost:9000', + publicEndpoint: 'http://localhost:19000', region: 'us-east-1', forcePathStyle: true, maxUploadBytes: 52_428_800, diff --git a/apps/api/src/config/env.validation.ts b/apps/api/src/config/env.validation.ts index f47b1a8..307a977 100644 --- a/apps/api/src/config/env.validation.ts +++ b/apps/api/src/config/env.validation.ts @@ -69,6 +69,7 @@ export const envSchema = z.object({ API_URL: z.string().url().default(defaultConfig.app.apiUrl), WEB_URL: z.string().url().default(defaultConfig.app.webUrl), S3_ENDPOINT: z.string().url().default(defaultConfig.storage.endpoint), + S3_PUBLIC_ENDPOINT: z.string().url().default(defaultConfig.storage.publicEndpoint), S3_ACCESS_KEY: z.string().min(1), S3_SECRET_KEY: z.string().min(1), S3_BUCKET: z.string().min(1), diff --git a/apps/api/src/config/files.config.ts b/apps/api/src/config/files.config.ts index fabce0e..8567c8f 100644 --- a/apps/api/src/config/files.config.ts +++ b/apps/api/src/config/files.config.ts @@ -4,6 +4,7 @@ export function filesConfig() { const env = config(); return { endpoint: env.S3_ENDPOINT, + publicEndpoint: env.S3_PUBLIC_ENDPOINT, region: env.S3_REGION, forcePathStyle: env.S3_FORCE_PATH_STYLE, accessKey: env.S3_ACCESS_KEY, diff --git a/apps/api/src/infrastructure/storage/s3-object-storage.adapter.ts b/apps/api/src/infrastructure/storage/s3-object-storage.adapter.ts index 13234aa..badaef1 100644 --- a/apps/api/src/infrastructure/storage/s3-object-storage.adapter.ts +++ b/apps/api/src/infrastructure/storage/s3-object-storage.adapter.ts @@ -15,9 +15,16 @@ export class S3ObjectStorageAdapter implements ObjectStorage { credentials: { accessKeyId: this.cfg.accessKey, secretAccessKey: this.cfg.secretKey }, }); + private readonly publicClient = new S3Client({ + region: this.cfg.region, + endpoint: this.cfg.publicEndpoint, + forcePathStyle: this.cfg.forcePathStyle, + credentials: { accessKeyId: this.cfg.accessKey, secretAccessKey: this.cfg.secretKey }, + }); + createUploadUrl(input: { key: string; contentType: string; contentLength: number; checksum: string }) { return getSignedUrl( - this.client, + this.publicClient, new PutObjectCommand({ Bucket: this.cfg.bucket, Key: input.key, @@ -50,7 +57,7 @@ export class S3ObjectStorageAdapter implements ObjectStorage { } createDownloadUrl(key: string) { - return getSignedUrl(this.client, new GetObjectCommand({ Bucket: this.cfg.bucket, Key: key }), { + return getSignedUrl(this.publicClient, new GetObjectCommand({ Bucket: this.cfg.bucket, Key: key }), { expiresIn: this.cfg.downloadUrlTtlSeconds, }); }