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:
@@ -21,14 +21,20 @@ import { getPrivateS3Url } from 'src/common/helper';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class UploadService {
|
export class UploadService {
|
||||||
private s3: S3Client;
|
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() {
|
constructor() {
|
||||||
const s3Config: any = {
|
const credentials = {
|
||||||
region: process.env.AWS_REGION || 'us-east-1',
|
accessKeyId: process.env.AWS_ACCESS_KEY,
|
||||||
credentials: {
|
secretAccessKey: process.env.AWS_SECRET_KEY,
|
||||||
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
|
// MinIO compatibility: if S3_ENDPOINT is set, override the endpoint
|
||||||
// and force path-style access (MinIO requires this)
|
// and force path-style access (MinIO requires this)
|
||||||
@@ -38,6 +44,21 @@ export class UploadService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.s3 = new S3Client(s3Config);
|
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({
|
async initialize({
|
||||||
@@ -72,7 +93,7 @@ export class UploadService {
|
|||||||
PartNumber: index + 1,
|
PartNumber: index + 1,
|
||||||
});
|
});
|
||||||
promises.push(
|
promises.push(
|
||||||
getSignedUrl(this.s3, command, {
|
getSignedUrl(this.presignS3, command, {
|
||||||
expiresIn: 60 * 60 * 24 * 7,
|
expiresIn: 60 * 60 * 24 * 7,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -180,7 +201,7 @@ export class UploadService {
|
|||||||
Bucket: process.env.S3_PRIVATE_BUCKET_NAME,
|
Bucket: process.env.S3_PRIVATE_BUCKET_NAME,
|
||||||
Key: key,
|
Key: key,
|
||||||
});
|
});
|
||||||
return getSignedUrl(this.s3, command, {
|
return getSignedUrl(this.presignS3, command, {
|
||||||
expiresIn: expires ?? 60 * 60 * 24 * 7,
|
expiresIn: expires ?? 60 * 60 * 24 * 7,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
args:
|
||||||
CACHEBUST: "8"
|
CACHEBUST: "9"
|
||||||
VITE_USE_MOCK_DATA: "false"
|
VITE_USE_MOCK_DATA: "false"
|
||||||
VITE_CONTENT_ORIGIN: ${FRONTEND_URL}
|
VITE_CONTENT_ORIGIN: ${FRONTEND_URL}
|
||||||
VITE_INDEEHUB_API_URL: /api
|
VITE_INDEEHUB_API_URL: /api
|
||||||
@@ -47,7 +47,7 @@ services:
|
|||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
args:
|
||||||
CACHEBUST: "8"
|
CACHEBUST: "9"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
# ── Core ─────────────────────────────────────────────
|
# ── Core ─────────────────────────────────────────────
|
||||||
@@ -179,7 +179,7 @@ services:
|
|||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile.ffmpeg
|
dockerfile: Dockerfile.ffmpeg
|
||||||
args:
|
args:
|
||||||
CACHEBUST: "8"
|
CACHEBUST: "9"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
ENVIRONMENT: production
|
ENVIRONMENT: production
|
||||||
|
|||||||
23
nginx.conf
23
nginx.conf
@@ -84,6 +84,29 @@ server {
|
|||||||
add_header Cache-Control "no-store";
|
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) ────────
|
# ── WebSocket proxy to Nostr relay (Docker service) ────────
|
||||||
location /relay {
|
location /relay {
|
||||||
resolver 127.0.0.11 valid=30s ipv6=off;
|
resolver 127.0.0.11 valid=30s ipv6=off;
|
||||||
|
|||||||
Reference in New Issue
Block a user