diff --git a/backend/src/upload/upload.service.ts b/backend/src/upload/upload.service.ts index 4044561..14c05d2 100644 --- a/backend/src/upload/upload.service.ts +++ b/backend/src/upload/upload.service.ts @@ -21,14 +21,20 @@ import { getPrivateS3Url } from 'src/common/helper'; @Injectable() export class UploadService { private s3: S3Client; + + // Separate client for generating presigned URLs that browsers can reach. + // Uses the public-facing FRONTEND_URL instead of the internal MinIO endpoint + // so presigned URLs are https://domain/bucket/key instead of http://minio:9000/... + private presignS3: S3Client; + constructor() { - const s3Config: any = { - region: process.env.AWS_REGION || 'us-east-1', - credentials: { - accessKeyId: process.env.AWS_ACCESS_KEY, - secretAccessKey: process.env.AWS_SECRET_KEY, - }, + const credentials = { + accessKeyId: process.env.AWS_ACCESS_KEY, + secretAccessKey: process.env.AWS_SECRET_KEY, }; + const region = process.env.AWS_REGION || 'us-east-1'; + + const s3Config: any = { region, credentials }; // MinIO compatibility: if S3_ENDPOINT is set, override the endpoint // and force path-style access (MinIO requires this) @@ -38,6 +44,21 @@ export class UploadService { } this.s3 = new S3Client(s3Config); + + // For presigned URLs served to the browser, use the public domain + // so URLs are reachable over HTTPS (avoids Mixed Content errors). + // Nginx proxies /indeedhub-*/ paths to MinIO internally. + const presignEndpoint = process.env.FRONTEND_URL; + if (presignEndpoint && process.env.S3_ENDPOINT) { + this.presignS3 = new S3Client({ + region, + credentials, + endpoint: presignEndpoint, + forcePathStyle: true, + }); + } else { + this.presignS3 = this.s3; + } } async initialize({ @@ -72,7 +93,7 @@ export class UploadService { PartNumber: index + 1, }); promises.push( - getSignedUrl(this.s3, command, { + getSignedUrl(this.presignS3, command, { expiresIn: 60 * 60 * 24 * 7, }), ); @@ -180,7 +201,7 @@ export class UploadService { Bucket: process.env.S3_PRIVATE_BUCKET_NAME, Key: key, }); - return getSignedUrl(this.s3, command, { + return getSignedUrl(this.presignS3, command, { expiresIn: expires ?? 60 * 60 * 24 * 7, }); } diff --git a/docker-compose.yml b/docker-compose.yml index 940ff3e..7505098 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ services: context: . dockerfile: Dockerfile args: - CACHEBUST: "8" + CACHEBUST: "9" VITE_USE_MOCK_DATA: "false" VITE_CONTENT_ORIGIN: ${FRONTEND_URL} VITE_INDEEHUB_API_URL: /api @@ -47,7 +47,7 @@ services: context: ./backend dockerfile: Dockerfile args: - CACHEBUST: "8" + CACHEBUST: "9" restart: unless-stopped environment: # ── Core ───────────────────────────────────────────── @@ -179,7 +179,7 @@ services: context: ./backend dockerfile: Dockerfile.ffmpeg args: - CACHEBUST: "8" + CACHEBUST: "9" restart: unless-stopped environment: ENVIRONMENT: production diff --git a/nginx.conf b/nginx.conf index 82747f1..604a245 100644 --- a/nginx.conf +++ b/nginx.conf @@ -84,6 +84,29 @@ server { add_header Cache-Control "no-store"; } + # ── MinIO direct proxy (for presigned URL uploads/downloads) ── + # The backend generates presigned URLs pointing to the public domain. + # This location proxies those requests to MinIO WITHOUT rewriting the + # path, so the S3v4 signature (which includes the path) stays valid. + location ~ ^/(indeedhub-private|indeedhub-public)/ { + resolver 127.0.0.11 valid=30s ipv6=off; + set $minio_upstream http://minio:9000; + + proxy_pass $minio_upstream; + proxy_http_version 1.1; + # Pass the original Host so MinIO's signature verification matches + # the host the presigned URL was generated for. + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # Allow large file uploads (up to 5GB per chunk) + client_max_body_size 5g; + + # No caching for upload responses + add_header Cache-Control "no-store"; + } + # ── WebSocket proxy to Nostr relay (Docker service) ──────── location /relay { resolver 127.0.0.11 valid=30s ipv6=off;