mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
8f7f580b2f
commit
b353fb6ded
4 files changed
+41
-22
No files matched your search
@@ -8,7 +8,7 @@ import { storage } from '../storage';
|
|||||||
import { freeSoundService, type MusicTrack } from '../services/freesound';
|
import { freeSoundService, type MusicTrack } from '../services/freesound';
|
||||||
import { ffmpegService } from '../services/ffmpeg';
|
import { ffmpegService } from '../services/ffmpeg';
|
||||||
import { facebookService } from '../services/facebook';
|
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 { openRouterService } from '../services/openrouter';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { cloudinaryConfig } from '@shared/schema';
|
import { cloudinaryConfig } from '@shared/schema';
|
||||||
@@ -343,14 +343,14 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const config = await storage.getCloudinaryConfig();
|
const config = await storage.getCloudinaryConfig();
|
||||||
if (config && config.logoPublicId) {
|
if (config && config.logoPublicId) {
|
||||||
watermarkUrl = buildMinioUrl(config.cloudName, config.logoPublicId, config.publicUrl);
|
watermarkUrl = resolveInternalUrl(buildMinioUrl(config.cloudName, config.logoPublicId, config.publicUrl));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error fetching watermark configuration', e);
|
console.error('Error fetching watermark configuration', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traiter la vidéo via FFmpeg
|
// Traiter la vidéo via FFmpeg
|
||||||
const result = await ffmpegService.processReelFromUrl(media.originalUrl, {
|
const result = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
|
||||||
text: overlayText,
|
text: overlayText,
|
||||||
musicUrl: finalMusicUrl,
|
musicUrl: finalMusicUrl,
|
||||||
ttsEnabled,
|
ttsEnabled,
|
||||||
@@ -556,13 +556,13 @@ async function processReelBackground(
|
|||||||
try {
|
try {
|
||||||
const config = await storage.getCloudinaryConfig();
|
const config = await storage.getCloudinaryConfig();
|
||||||
if (config && config.logoPublicId) {
|
if (config && config.logoPublicId) {
|
||||||
watermarkUrl = buildMinioUrl(config.cloudName, config.logoPublicId, config.publicUrl);
|
watermarkUrl = resolveInternalUrl(buildMinioUrl(config.cloudName, config.logoPublicId, config.publicUrl));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error fetching watermark configuration', 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,
|
text: overlayText,
|
||||||
musicUrl: finalMusicUrl,
|
musicUrl: finalMusicUrl,
|
||||||
ttsEnabled,
|
ttsEnabled,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { bundle } from "@remotion/bundler";
|
|||||||
import { renderMedia, selectComposition } from "@remotion/renderer";
|
import { renderMedia, selectComposition } from "@remotion/renderer";
|
||||||
import { ffmpegService } from "../services/ffmpeg";
|
import { ffmpegService } from "../services/ffmpeg";
|
||||||
import { storage as dbStorage } from "../storage";
|
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 { facebookService } from "../services/facebook";
|
||||||
import * as musicMetadata from "music-metadata";
|
import * as musicMetadata from "music-metadata";
|
||||||
|
|
||||||
@@ -319,7 +319,7 @@ remotionRouter.post("/publish", async (req, res) => {
|
|||||||
|
|
||||||
if (!scheduledFor) {
|
if (!scheduledFor) {
|
||||||
console.log(`🚀 Publishing to ${page.pageName}...`);
|
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 });
|
await dbStorage.updateScheduledPost(scheduledPost.id, { publishedAt: new Date(), externalPostId: reelId });
|
||||||
results.push({ pageId, success: true, reelId });
|
results.push({ pageId, success: true, reelId });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { storage } from '../storage';
|
|||||||
import type { Post, SocialPage, Media } from '@shared/schema';
|
import type { Post, SocialPage, Media } from '@shared/schema';
|
||||||
import { imageProcessor } from './imageProcessor';
|
import { imageProcessor } from './imageProcessor';
|
||||||
import { cloudinaryService } from './cloudinary';
|
import { cloudinaryService } from './cloudinary';
|
||||||
|
import { resolvePublicUrl } from './minio';
|
||||||
|
|
||||||
interface FacebookPhotoResponse {
|
interface FacebookPhotoResponse {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -70,7 +71,7 @@ export class FacebookService {
|
|||||||
throw new Error('Reels require a video media');
|
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 {
|
} else {
|
||||||
// Default to feed
|
// Default to feed
|
||||||
const imageMedia = mediaList.filter(m => m.type === 'image');
|
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<string> {
|
private async publishPhotoPost(post: Post, page: SocialPage, media: Media): Promise<string> {
|
||||||
// Use original URL - Facebook handles the cropping
|
// Use original URL - Facebook handles the cropping
|
||||||
const photoUrl = media.originalUrl;
|
const photoUrl = resolvePublicUrl(media.originalUrl);
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
access_token: page.accessToken!,
|
access_token: page.accessToken!,
|
||||||
@@ -175,7 +176,7 @@ export class FacebookService {
|
|||||||
|
|
||||||
private async publishVideoPost(post: Post, page: SocialPage, media: Media): Promise<string> {
|
private async publishVideoPost(post: Post, page: SocialPage, media: Media): Promise<string> {
|
||||||
// Transform video URL to ensure Facebook compatibility (H.264/AAC/MP4)
|
// 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({
|
const params = new URLSearchParams({
|
||||||
access_token: page.accessToken!,
|
access_token: page.accessToken!,
|
||||||
@@ -215,7 +216,7 @@ export class FacebookService {
|
|||||||
|
|
||||||
for (const media of imageMedia) {
|
for (const media of imageMedia) {
|
||||||
// Use original URL - Facebook handles the cropping for carousel
|
// Use original URL - Facebook handles the cropping for carousel
|
||||||
const photoUrl = media.originalUrl;
|
const photoUrl = resolvePublicUrl(media.originalUrl);
|
||||||
|
|
||||||
if (!photoUrl) {
|
if (!photoUrl) {
|
||||||
console.warn(`Skipping media ${media.id} - no valid URL found`);
|
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<string> {
|
private async schedulePhotoPost(post: Post, page: SocialPage, scheduledTimestamp: number, media: Media): Promise<string> {
|
||||||
// Use original URL - Facebook handles the cropping
|
// Use original URL - Facebook handles the cropping
|
||||||
const photoUrl = media.originalUrl;
|
const photoUrl = resolvePublicUrl(media.originalUrl);
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
access_token: page.accessToken!,
|
access_token: page.accessToken!,
|
||||||
@@ -364,11 +365,11 @@ export class FacebookService {
|
|||||||
// Step 1: Upload photo as unpublished to get photo_id
|
// Step 1: Upload photo as unpublished to get photo_id
|
||||||
// Step 2: Publish the photo as a story using the 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) {
|
if (post.content && post.content.trim().length > 0) {
|
||||||
try {
|
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`);
|
photoUrl = await cloudinaryService.uploadStoryImageWithText(imageWithText, `story-${media.id}.png`);
|
||||||
console.log('Story image with text generated:', photoUrl);
|
console.log('Story image with text generated:', photoUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -424,7 +425,7 @@ export class FacebookService {
|
|||||||
// 2. UPLOAD phase - Upload the video to upload_url using file_url header
|
// 2. UPLOAD phase - Upload the video to upload_url using file_url header
|
||||||
// 3. FINISH phase - Finalize and publish the story
|
// 3. FINISH phase - Finalize and publish the story
|
||||||
|
|
||||||
const videoUrl = media.originalUrl;
|
const videoUrl = resolvePublicUrl(media.originalUrl);
|
||||||
const accessToken = page.accessToken!;
|
const accessToken = page.accessToken!;
|
||||||
|
|
||||||
// Phase 1: START - Initialize the upload
|
// Phase 1: START - Initialize the upload
|
||||||
|
|||||||
@@ -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.
|
* Resolves a possibly-relative URL to an absolute URL for use by external
|
||||||
* objectKey is the relative path from uploads/ (e.g. "media/123-file.jpg").
|
* services (Facebook API, Instagram API) that must download the file.
|
||||||
* bucketName and publicUrlOverride are ignored (local storage compatibility shim).
|
* Uses APP_URL env var (public-facing domain).
|
||||||
*/
|
*/
|
||||||
export function buildMinioUrl(_bucketName: string, objectKey: string, _publicUrlOverride?: string | null): string {
|
export function resolvePublicUrl(url: string): string {
|
||||||
return `${getAppUrl()}/uploads/${objectKey}`;
|
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 {
|
class LocalStorageService {
|
||||||
|
|||||||
Reference in new issue
Block a user