From b353fb6dedf122120cfcb989555f9d8b20a4455f Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Wed, 25 Mar 2026 08:30:07 +0100 Subject: [PATCH] fix: use relative URLs for local media storage to fix CSP blocking Media files were stored with absolute http://localhost:5555/... URLs, which got blocked by the Content Security Policy when the app runs at https://socialflow.fnancy.fr. Now stores relative /uploads/... paths and resolves to absolute URLs only when needed by external APIs (Facebook, FFmpeg internal service). Co-Authored-By: Claude Sonnet 4.6 --- server/routes/reels.ts | 10 +++++----- server/routes/remotion.ts | 4 ++-- server/services/facebook.ts | 17 +++++++++-------- server/services/minio.ts | 32 +++++++++++++++++++++++++------- 4 files changed, 41 insertions(+), 22 deletions(-) diff --git a/server/routes/reels.ts b/server/routes/reels.ts index 39724fc..4af20c0 100644 --- a/server/routes/reels.ts +++ b/server/routes/reels.ts @@ -8,7 +8,7 @@ import { storage } from '../storage'; import { freeSoundService, type MusicTrack } from '../services/freesound'; import { ffmpegService } from '../services/ffmpeg'; import { facebookService } from '../services/facebook'; -import { minioService as cloudinaryService, buildMinioUrl } from '../services/minio'; +import { minioService as cloudinaryService, buildMinioUrl, resolveInternalUrl } from '../services/minio'; import { openRouterService } from '../services/openrouter'; import { db } from '../db'; import { cloudinaryConfig } from '@shared/schema'; @@ -343,14 +343,14 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => { try { const config = await storage.getCloudinaryConfig(); if (config && config.logoPublicId) { - watermarkUrl = buildMinioUrl(config.cloudName, config.logoPublicId, config.publicUrl); + watermarkUrl = resolveInternalUrl(buildMinioUrl(config.cloudName, config.logoPublicId, config.publicUrl)); } } catch (e) { console.error('Error fetching watermark configuration', e); } // Traiter la vidéo via FFmpeg - const result = await ffmpegService.processReelFromUrl(media.originalUrl, { + const result = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), { text: overlayText, musicUrl: finalMusicUrl, ttsEnabled, @@ -556,13 +556,13 @@ async function processReelBackground( try { const config = await storage.getCloudinaryConfig(); if (config && config.logoPublicId) { - watermarkUrl = buildMinioUrl(config.cloudName, config.logoPublicId, config.publicUrl); + watermarkUrl = resolveInternalUrl(buildMinioUrl(config.cloudName, config.logoPublicId, config.publicUrl)); } } catch (e) { console.error('Error fetching watermark configuration', e); } - const ffmpegResult = await ffmpegService.processReelFromUrl(media.originalUrl, { + const ffmpegResult = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), { text: overlayText, musicUrl: finalMusicUrl, ttsEnabled, diff --git a/server/routes/remotion.ts b/server/routes/remotion.ts index d515527..daccc40 100644 --- a/server/routes/remotion.ts +++ b/server/routes/remotion.ts @@ -6,7 +6,7 @@ import { bundle } from "@remotion/bundler"; import { renderMedia, selectComposition } from "@remotion/renderer"; import { ffmpegService } from "../services/ffmpeg"; import { storage as dbStorage } from "../storage"; -import { minioService as cloudinaryService, buildMinioUrl } from "../services/minio"; +import { minioService as cloudinaryService, buildMinioUrl, resolvePublicUrl } from "../services/minio"; import { facebookService } from "../services/facebook"; import * as musicMetadata from "music-metadata"; @@ -319,7 +319,7 @@ remotionRouter.post("/publish", async (req, res) => { if (!scheduledFor) { console.log(`🚀 Publishing to ${page.pageName}...`); - const reelId = await facebookService.publishReel(page, cloudinaryResult.originalUrl, description || ""); + const reelId = await facebookService.publishReel(page, resolvePublicUrl(cloudinaryResult.originalUrl), description || ""); await dbStorage.updateScheduledPost(scheduledPost.id, { publishedAt: new Date(), externalPostId: reelId }); results.push({ pageId, success: true, reelId }); } else { diff --git a/server/services/facebook.ts b/server/services/facebook.ts index e2b91ca..7bf1f91 100644 --- a/server/services/facebook.ts +++ b/server/services/facebook.ts @@ -2,6 +2,7 @@ import { storage } from '../storage'; import type { Post, SocialPage, Media } from '@shared/schema'; import { imageProcessor } from './imageProcessor'; import { cloudinaryService } from './cloudinary'; +import { resolvePublicUrl } from './minio'; interface FacebookPhotoResponse { id: string; @@ -70,7 +71,7 @@ export class FacebookService { throw new Error('Reels require a video media'); } - return await this.publishReel(page, videoMedia.originalUrl, post.content); + return await this.publishReel(page, resolvePublicUrl(videoMedia.originalUrl), post.content); } else { // Default to feed const imageMedia = mediaList.filter(m => m.type === 'image'); @@ -115,7 +116,7 @@ export class FacebookService { private async publishPhotoPost(post: Post, page: SocialPage, media: Media): Promise { // Use original URL - Facebook handles the cropping - const photoUrl = media.originalUrl; + const photoUrl = resolvePublicUrl(media.originalUrl); const params = new URLSearchParams({ access_token: page.accessToken!, @@ -175,7 +176,7 @@ export class FacebookService { private async publishVideoPost(post: Post, page: SocialPage, media: Media): Promise { // Transform video URL to ensure Facebook compatibility (H.264/AAC/MP4) - const videoUrl = this.getFacebookCompatibleVideoUrl(media.originalUrl); + const videoUrl = this.getFacebookCompatibleVideoUrl(resolvePublicUrl(media.originalUrl)); const params = new URLSearchParams({ access_token: page.accessToken!, @@ -215,7 +216,7 @@ export class FacebookService { for (const media of imageMedia) { // Use original URL - Facebook handles the cropping for carousel - const photoUrl = media.originalUrl; + const photoUrl = resolvePublicUrl(media.originalUrl); if (!photoUrl) { console.warn(`Skipping media ${media.id} - no valid URL found`); @@ -330,7 +331,7 @@ export class FacebookService { private async schedulePhotoPost(post: Post, page: SocialPage, scheduledTimestamp: number, media: Media): Promise { // Use original URL - Facebook handles the cropping - const photoUrl = media.originalUrl; + const photoUrl = resolvePublicUrl(media.originalUrl); const params = new URLSearchParams({ access_token: page.accessToken!, @@ -364,11 +365,11 @@ export class FacebookService { // Step 1: Upload photo as unpublished to get photo_id // Step 2: Publish the photo as a story using the photo_id - let photoUrl = media.originalUrl; + let photoUrl = resolvePublicUrl(media.originalUrl); if (post.content && post.content.trim().length > 0) { try { - const imageWithText = await imageProcessor.addTextToStoryImage(media.originalUrl, post.content); + const imageWithText = await imageProcessor.addTextToStoryImage(resolvePublicUrl(media.originalUrl), post.content); photoUrl = await cloudinaryService.uploadStoryImageWithText(imageWithText, `story-${media.id}.png`); console.log('Story image with text generated:', photoUrl); } catch (error) { @@ -424,7 +425,7 @@ export class FacebookService { // 2. UPLOAD phase - Upload the video to upload_url using file_url header // 3. FINISH phase - Finalize and publish the story - const videoUrl = media.originalUrl; + const videoUrl = resolvePublicUrl(media.originalUrl); const accessToken = page.accessToken!; // Phase 1: START - Initialize the upload diff --git a/server/services/minio.ts b/server/services/minio.ts index 5472c71..8f5e039 100644 --- a/server/services/minio.ts +++ b/server/services/minio.ts @@ -9,17 +9,35 @@ function ensureDir(dir: string): void { } } -function getAppUrl(): string { - return (process.env.APP_URL || 'http://localhost:5555').replace(/\/$/, ''); +/** + * Builds the public URL for a locally stored file. + * Returns a relative path so it works on any domain/protocol. + * objectKey is the relative path from uploads/ (e.g. "media/123-file.jpg"). + */ +export function buildMinioUrl(_bucketName: string, objectKey: string, _publicUrlOverride?: string | null): string { + return `/uploads/${objectKey}`; } /** - * Builds the public URL for a locally stored file. - * objectKey is the relative path from uploads/ (e.g. "media/123-file.jpg"). - * bucketName and publicUrlOverride are ignored (local storage compatibility shim). + * Resolves a possibly-relative URL to an absolute URL for use by external + * services (Facebook API, Instagram API) that must download the file. + * Uses APP_URL env var (public-facing domain). */ -export function buildMinioUrl(_bucketName: string, objectKey: string, _publicUrlOverride?: string | null): string { - return `${getAppUrl()}/uploads/${objectKey}`; +export function resolvePublicUrl(url: string): string { + if (!url || !url.startsWith('/')) return url; + const base = (process.env.APP_URL || 'http://localhost:5555').replace(/\/$/, ''); + return `${base}${url}`; +} + +/** + * Resolves a possibly-relative URL to an absolute URL for use by internal + * Docker services (FFmpeg, etc.) that communicate via internal Docker network. + * Uses INTERNAL_APP_URL env var. + */ +export function resolveInternalUrl(url: string): string { + if (!url || !url.startsWith('/')) return url; + const base = (process.env.INTERNAL_APP_URL || process.env.APP_URL || 'http://localhost:5555').replace(/\/$/, ''); + return `${base}${url}`; } class LocalStorageService {