Fix Mixed Content on file uploads: presigned URLs now use public domain

The backend was generating presigned S3 URLs pointing to the internal
MinIO endpoint (http://minio:9000), which browsers block on HTTPS pages.

- Add a second S3 client in upload.service.ts configured with FRONTEND_URL
  for generating browser-facing presigned URLs (both upload and download)
- Add nginx proxy location for /indeedhub-private/ and /indeedhub-public/
  paths that forwards to MinIO without rewriting (preserves S3v4 signatures)
- Keep internal S3 client for server-side operations (copy, delete, etc.)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Dorian
2026-02-13 20:30:49 +00:00
parent abb83fe164
commit fc20c625fa
3 changed files with 55 additions and 11 deletions

View File

@@ -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: {
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,
});
}

View File

@@ -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

View File

@@ -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;