feat(storage): add public endpoint support for presigned URLs

When services run in Docker with internal endpoints (e.g., http://minio:9000),
presigned URLs are inaccessible from browsers. This adds S3_PUBLIC_ENDPOINT
support to generate presigned URLs using a publicly accessible endpoint
(e.g., https://minio.mana.how) while keeping internal operations on the
Docker network.

Changes:
- Add publicEndpoint to StorageConfig type
- Create separate S3Client for presigned URL generation
- Add S3_PUBLIC_ENDPOINT to factory configuration
- Configure lightwrite-backend with public MinIO endpoint

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-02-16 15:35:18 +01:00
parent 9dc6c111d3
commit d6303e4998
4 changed files with 33 additions and 4 deletions

View file

@ -609,6 +609,7 @@ services:
MANA_CORE_AUTH_URL: http://mana-auth:3001
CORS_ORIGINS: https://lightwrite.mana.how,https://mana.how
S3_ENDPOINT: http://minio:9000
S3_PUBLIC_ENDPOINT: https://minio.mana.how
S3_REGION: us-east-1
S3_ACCESS_KEY: ${MINIO_ACCESS_KEY:-minioadmin}
S3_SECRET_KEY: ${MINIO_SECRET_KEY:-minioadmin}

View file

@ -22,9 +22,11 @@ import type {
*/
export class StorageClient {
private client: S3Client;
private presignClient: S3Client;
private bucket: BucketConfig;
constructor(config: StorageConfig, bucket: BucketConfig) {
// Main client for internal operations (upload, download, delete, etc.)
this.client = new S3Client({
endpoint: config.endpoint,
region: config.region,
@ -34,6 +36,20 @@ export class StorageClient {
},
forcePathStyle: config.forcePathStyle ?? true,
});
// Separate client for presigned URLs (uses public endpoint if available)
// This allows internal operations to use Docker network addresses
// while presigned URLs use publicly accessible endpoints
this.presignClient = new S3Client({
endpoint: config.publicEndpoint ?? config.endpoint,
region: config.region,
credentials: {
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
},
forcePathStyle: config.forcePathStyle ?? true,
});
this.bucket = bucket;
}
@ -142,6 +158,7 @@ export class StorageClient {
/**
* Generate a presigned URL for uploading (PUT)
* Uses the public endpoint if configured, allowing browser access
*/
async getUploadUrl(key: string, options: PresignedUrlOptions = {}): Promise<string> {
const command = new PutObjectCommand({
@ -149,13 +166,14 @@ export class StorageClient {
Key: key,
});
return getSignedUrl(this.client, command, {
return getSignedUrl(this.presignClient, command, {
expiresIn: options.expiresIn ?? 3600,
});
}
/**
* Generate a presigned URL for downloading (GET)
* Uses the public endpoint if configured, allowing browser access
*/
async getDownloadUrl(key: string, options: PresignedUrlOptions = {}): Promise<string> {
const command = new GetObjectCommand({
@ -163,7 +181,7 @@ export class StorageClient {
Key: key,
});
return getSignedUrl(this.client, command, {
return getSignedUrl(this.presignClient, command, {
expiresIn: options.expiresIn ?? 3600,
});
}

View file

@ -7,6 +7,7 @@ import type { StorageConfig, BucketConfig, BucketName } from './types';
*/
const ENV_KEYS = {
ENDPOINT: 'S3_ENDPOINT',
PUBLIC_ENDPOINT: 'S3_PUBLIC_ENDPOINT',
REGION: 'S3_REGION',
ACCESS_KEY: 'S3_ACCESS_KEY',
SECRET_KEY: 'S3_SECRET_KEY',
@ -29,15 +30,17 @@ const MINIO_DEFAULTS: StorageConfig = {
*/
export function getStorageConfig(): StorageConfig {
const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV;
const endpoint = process.env[ENV_KEYS.ENDPOINT] ?? (isDev ? MINIO_DEFAULTS.endpoint : '');
// Use environment variables if available, otherwise use MinIO defaults
return {
endpoint: process.env[ENV_KEYS.ENDPOINT] ?? (isDev ? MINIO_DEFAULTS.endpoint : ''),
endpoint,
publicEndpoint: process.env[ENV_KEYS.PUBLIC_ENDPOINT],
region: process.env[ENV_KEYS.REGION] ?? MINIO_DEFAULTS.region,
accessKeyId: process.env[ENV_KEYS.ACCESS_KEY] ?? (isDev ? MINIO_DEFAULTS.accessKeyId : ''),
secretAccessKey:
process.env[ENV_KEYS.SECRET_KEY] ?? (isDev ? MINIO_DEFAULTS.secretAccessKey : ''),
forcePathStyle: isDev || process.env[ENV_KEYS.ENDPOINT]?.includes('localhost'),
forcePathStyle: isDev || endpoint?.includes('localhost') || endpoint?.includes('minio'),
};
}

View file

@ -12,6 +12,13 @@ export interface StorageConfig {
secretAccessKey: string;
/** Force path-style URLs (required for MinIO) */
forcePathStyle?: boolean;
/**
* Public endpoint for generating presigned URLs accessible from browsers.
* Use this when the internal endpoint (e.g., http://minio:9000) differs
* from the public URL (e.g., https://minio.mana.how).
* If not set, presigned URLs use the main endpoint.
*/
publicEndpoint?: string;
}
/**