From b5e47f75d6e6faa2d464266ae1fa42bcc4e24007 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Thu, 2 Oct 2025 13:48:41 +0000 Subject: [PATCH 1/4] Fix issue preventing posts from being published to Facebook Address a bug where posts appeared as sent but were not visible on Facebook. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/BuArSJC --- .replit | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.replit b/.replit index 7852907..ffeb98a 100644 --- a/.replit +++ b/.replit @@ -18,10 +18,6 @@ externalPort = 80 localPort = 5555 externalPort = 3002 -[[ports]] -localPort = 37219 -externalPort = 3003 - [[ports]] localPort = 40537 externalPort = 3000 From daadd46e8b67de0f1012cd979b1b82848fe5ed17 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Thu, 2 Oct 2025 14:07:08 +0000 Subject: [PATCH 2/4] Add automated posting to Facebook and Instagram with AI text generation Adds a new workflow to Social Flow for automated social media posting, integrating AI for text generation and Cloudinary for image uploads via an HTTP request node. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/BuArSJC --- .../My workflow 8_1759413913598.json | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 attached_assets/My workflow 8_1759413913598.json diff --git a/attached_assets/My workflow 8_1759413913598.json b/attached_assets/My workflow 8_1759413913598.json new file mode 100644 index 0000000..286544f --- /dev/null +++ b/attached_assets/My workflow 8_1759413913598.json @@ -0,0 +1,322 @@ +{ + "name": "My workflow 8", + "nodes": [ + { + "parameters": { + "formTitle": "SocialFlow", + "formFields": { + "values": [ + { + "fieldLabel": "Non du produit", + "placeholder": "nom_produit", + "requiredField": true + }, + { + "fieldLabel": "Prix", + "placeholder": "prix", + "requiredField": true + }, + { + "fieldLabel": "Caractéristiques", + "placeholder": "caractéristiques" + }, + { + "fieldLabel": "Promo", + "placeholder": "promo" + }, + { + "fieldLabel": "Date publication", + "fieldType": "date", + "requiredField": true + }, + { + "fieldLabel": "Heure de publication (HH:mm)", + "placeholder": "heure_publication", + "requiredField": true + }, + { + "fieldLabel": "Photo", + "fieldType": "file", + "acceptFileTypes": "photo" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.formTrigger", + "typeVersion": 2.2, + "position": [ + -720, + 160 + ], + "id": "3441e9d7-35ff-4f14-bee8-78397d0e2e28", + "name": "On form submission", + "webhookId": "17357858-667e-4a3d-afd1-82039f41c927" + }, + { + "parameters": { + "method": "POST", + "url": "=https://api.cloudinary.com/v1_1/dspr8yjfu/image/upload", + "sendBody": true, + "contentType": "multipart-form-data", + "bodyParameters": { + "parameters": [ + { + "name": "upload_preset", + "value": "=upload_n8n" + }, + { + "parameterType": "formBinaryData", + "name": "file", + "inputDataFieldName": "Photo" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + -208, + 16 + ], + "id": "49ff16a6-a004-4ff4-a1dc-cb6902f7f46a", + "name": "HTTP Request" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "14257b6b-82c8-4151-85a5-f56ab814f6f3", + "name": "image_ready_url", + "value": "={{$json.secure_url\n ? $json.secure_url.replace('/upload/','/upload/c_fill,g_auto,h_1350,w_1080,q_auto,f_auto,e_auto_contrast,e_auto_color,e_sharpen:50/')\n : ''}}\n", + "type": "string" + }, + { + "id": "74f9dbae-0e18-4cdb-b316-5976f65ad6c8", + "name": "image_public_id", + "value": "={{$json.public_id}}", + "type": "string" + } + ] + }, + "includeOtherFields": true, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 16, + 16 + ], + "id": "dc95401f-403a-4a72-87e6-622709b10046", + "name": "Edit Fields" + }, + { + "parameters": { + "promptType": "define", + "text": "=Langue: fr\nPAS DE TEXTE AVANT OU APRÈS.\n\nContraintes:\n- Accroche d'ouverture engageante\n- 1-2 phrases sur bénéfice client, pas style catalogue\n- Pas de lien, pas d'URL\n- 3-5 hashtags ciblés (locaux/thématiques) Ajouter #lafoirfouille\n- Ton chaleureux, concret, non agressif\n- Maximum 250 caractères \n\nContexte:\n- Produit: {{ $('On form submission').item.json['Non du produit'] }}\n- Prix: {{ $('On form submission').item.json.Prix }}\n- Caractéristiques: {{ $('On form submission').item.json['Caractéristiques'] }}\n- Promo: {{$json.promo || \"aucune\"}}\n", + "messages": { + "messageValues": [ + { + "message": "Tu es un community manager de boutique physique. Tu écris pour Facebook. Objectif: engagement + visites en magasin. Pas de lien web. pas de json que du texte pure avec quelques emoji. structure le texte pour un post facebook et instagram - ne parle pas de boutique locale." + } + ] + }, + "batching": {} + }, + "type": "@n8n/n8n-nodes-langchain.chainLlm", + "typeVersion": 1.7, + "position": [ + 240, + 16 + ], + "id": "b7de85b4-8a06-4cd6-b3b1-dd28f7e30720", + "name": "Basic LLM Chain", + "alwaysOutputData": true + }, + { + "parameters": { + "model": "openai/gpt-4o", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter", + "typeVersion": 1, + "position": [ + 224, + 320 + ], + "id": "03c7dc91-a5e0-4b58-946f-7217cfbb7f22", + "name": "OpenRouter Chat Model", + "credentials": { + "openRouterApi": { + "id": "QrDzqP7MluNzhC1L", + "name": "OpenRouter account" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "=https://graph.facebook.com/v23.0/119315771483782/photos", + "sendQuery": true, + "queryParameters": { + "parameters": [ + { + "name": "url", + "value": "={{ $('Edit Fields').first().json.image_ready_url.trim().replace(/\\)+$/,'') }}\n" + }, + { + "name": "message", + "value": "={{ $json.text }}" + }, + { + "name": "access_token", + "value": "EAASQALmsF1UBPlhtfZBbZCkuu9fZAbr8OpEAXuXcLuZCAx6F2BmhZBprak0AQfdqKZA0aGsR8pDtpqmkBjb7lIqZCadMJ919IrvwwE75nEo1uZCIIuAJDqkq3Cl9feQMX126sPIabGerrLtd8Dw5NGlQTNzYB7KYqUgcjZA6N5fDt7Bn4pMKMzsh0p5MCxMxq5xV1qyNLVykNMbgE7HZBkas1oLdeFj3M2hAZAYlS4NBuoZD" + }, + { + "name": "published", + "value": "=0" + }, + { + "name": "scheduled_publish_time", + "value": "={{ $json.scheduled_publish_time }}" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1040, + -16 + ], + "id": "6fbaf6c3-1744-4116-b599-047a978ba504", + "name": "Facebook Post Photo" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "// Récupère les valeurs depuis le Form Trigger, pas depuis l'item courant\nconst dateStr = $item(0).$node[\"On form submission\"].json[\"Date publication\"];\nconst timeStr = $item(0).$node[\"On form submission\"].json[\"Heure de publication (HH:mm)\"];\n\nif (!dateStr || !timeStr) {\n throw new Error(\"Champs manquants: \" + JSON.stringify({ dateStr, timeStr }));\n}\n\nconst [y,m,d] = dateStr.split('-').map(Number);\nconst [H,Mi] = timeStr.split(':').map(Number);\n\n// Epoch UTC\nlet ts = Math.floor(Date.UTC(y, m - 1, d, H, Mi) / 1000);\n\n// Sécurité: forcer ≥ maintenant+600s\nconst now = Math.floor(Date.now()/1000);\nif (ts < now + 600) ts = now + 1200;\n\nreturn { json: { ...$json, scheduled_publish_time: ts } };\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 816, + 144 + ], + "id": "93f14550-2ccd-4da7-bfb7-7ecacee3e830", + "name": "Code", + "alwaysOutputData": true + }, + { + "parameters": {}, + "type": "n8n-nodes-base.merge", + "typeVersion": 3.2, + "position": [ + 592, + 144 + ], + "id": "557d9b2a-ceb1-42d8-ad2d-29e8c2533d51", + "name": "Merge" + } + ], + "pinData": {}, + "connections": { + "On form submission": { + "main": [ + [ + { + "node": "HTTP Request", + "type": "main", + "index": 0 + }, + { + "node": "Merge", + "type": "main", + "index": 1 + } + ] + ] + }, + "HTTP Request": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + }, + "OpenRouter Chat Model": { + "ai_languageModel": [ + [ + { + "node": "Basic LLM Chain", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Edit Fields": { + "main": [ + [ + { + "node": "Basic LLM Chain", + "type": "main", + "index": 0 + } + ] + ] + }, + "Basic LLM Chain": { + "main": [ + [ + { + "node": "Merge", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "Facebook Post Photo", + "type": "main", + "index": 0 + } + ] + ] + }, + "Merge": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "72a43b73-4460-49de-88aa-90afde0ef73b", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "95265e6405e55fa03a3687565ed8e8c464bd659d0d56a9696d7d25f954abf511" + }, + "id": "cTnh6OMO9wxPEPBS", + "tags": [] +} \ No newline at end of file From 82b73620ddefc78fbdf16859bcb633600a88eb35 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Thu, 2 Oct 2025 14:14:42 +0000 Subject: [PATCH 3/4] Improve how posts are published and scheduled to Facebook Refactor FacebookService to correctly pass parameters via URL query strings for POST requests instead of request bodies in publishTextPost, publishPhotoPost, scheduleTextPost, and schedulePhotoPost methods. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/c9jWr93 --- .replit | 4 ++++ server/services/facebook.ts | 20 ++++++++------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.replit b/.replit index ffeb98a..b6bf8b4 100644 --- a/.replit +++ b/.replit @@ -18,6 +18,10 @@ externalPort = 80 localPort = 5555 externalPort = 3002 +[[ports]] +localPort = 40011 +externalPort = 3003 + [[ports]] localPort = 40537 externalPort = 3000 diff --git a/server/services/facebook.ts b/server/services/facebook.ts index b5e9ed9..41e1d54 100644 --- a/server/services/facebook.ts +++ b/server/services/facebook.ts @@ -39,16 +39,15 @@ export class FacebookService { } private async publishTextPost(post: Post, page: SocialPage): Promise { - const url = `${this.baseUrl}/${page.pageId}/feed`; - const params = new URLSearchParams({ access_token: page.accessToken!, message: post.content, }); + const url = `${this.baseUrl}/${page.pageId}/feed?${params.toString()}`; + const response = await fetch(url, { method: 'POST', - body: params, }); if (!response.ok) { @@ -61,8 +60,6 @@ export class FacebookService { } private async publishPhotoPost(post: Post, page: SocialPage, media: Media): Promise { - const url = `${this.baseUrl}/${page.pageId}/photos`; - // Use the Facebook Feed optimized URL (1200x630) const photoUrl = media.facebookFeedUrl || media.originalUrl; @@ -72,9 +69,10 @@ export class FacebookService { url: photoUrl, }); + const url = `${this.baseUrl}/${page.pageId}/photos?${params.toString()}`; + const response = await fetch(url, { method: 'POST', - body: params, }); if (!response.ok) { @@ -120,8 +118,6 @@ export class FacebookService { } private async scheduleTextPost(post: Post, page: SocialPage, scheduledTimestamp: number): Promise { - const url = `${this.baseUrl}/${page.pageId}/feed`; - const params = new URLSearchParams({ access_token: page.accessToken!, message: post.content, @@ -129,9 +125,10 @@ export class FacebookService { scheduled_publish_time: scheduledTimestamp.toString(), }); + const url = `${this.baseUrl}/${page.pageId}/feed?${params.toString()}`; + const response = await fetch(url, { method: 'POST', - body: params, }); if (!response.ok) { @@ -144,8 +141,6 @@ export class FacebookService { } private async schedulePhotoPost(post: Post, page: SocialPage, scheduledTimestamp: number, media: Media): Promise { - const url = `${this.baseUrl}/${page.pageId}/photos`; - const photoUrl = media.facebookFeedUrl || media.originalUrl; const params = new URLSearchParams({ @@ -156,9 +151,10 @@ export class FacebookService { scheduled_publish_time: scheduledTimestamp.toString(), }); + const url = `${this.baseUrl}/${page.pageId}/photos?${params.toString()}`; + const response = await fetch(url, { method: 'POST', - body: params, }); if (!response.ok) { From cce7c511d0fedc67c1021e13b0dc33837cae73f5 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Thu, 2 Oct 2025 16:45:44 +0000 Subject: [PATCH 4/4] Enable users to edit and delete their managed social media pages Adds PUT and DELETE API endpoints for social pages, along with UI elements for editing and deleting pages on the client-side. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/Ob3gzfQ --- .replit | 6 +- client/src/pages/pages.tsx | 131 ++++++++++++++++++++++++++++++++++--- server/routes.ts | 39 +++++++++++ 3 files changed, 161 insertions(+), 15 deletions(-) diff --git a/.replit b/.replit index b6bf8b4..0556bf9 100644 --- a/.replit +++ b/.replit @@ -15,13 +15,9 @@ localPort = 5000 externalPort = 80 [[ports]] -localPort = 5555 +localPort = 35981 externalPort = 3002 -[[ports]] -localPort = 40011 -externalPort = 3003 - [[ports]] localPort = 40537 externalPort = 3000 diff --git a/client/src/pages/pages.tsx b/client/src/pages/pages.tsx index 308d5c7..0f5c2b8 100644 --- a/client/src/pages/pages.tsx +++ b/client/src/pages/pages.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; -import { Plus, Facebook, Instagram, Trash2, RefreshCw } from "lucide-react"; +import { Plus, Facebook, Instagram, Trash2, RefreshCw, Edit } from "lucide-react"; import Sidebar from "@/components/sidebar"; import TopBar from "@/components/topbar"; import { Button } from "@/components/ui/button"; @@ -16,6 +16,7 @@ import type { SocialPage } from "@shared/schema"; export default function PagesManagement() { const [sidebarOpen, setSidebarOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); + const [editingPage, setEditingPage] = useState(null); const { toast } = useToast(); const { data: pages = [], isLoading } = useQuery({ @@ -69,6 +70,7 @@ export default function PagesManagement() {

+ !open && setEditingPage(null)} /> {isLoading ? ( @@ -125,15 +127,25 @@ export default function PagesManagement() {
ID: {page.pageId}
- +
+ + +
@@ -293,3 +305,102 @@ function AddPageDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (o ); } + +function EditPageDialog({ page, onOpenChange }: { page: SocialPage | null; onOpenChange: (open: boolean) => void }) { + const [accessToken, setAccessToken] = useState(''); + const { toast } = useToast(); + + const editMutation = useMutation({ + mutationFn: (data: any) => + apiRequest('PUT', `/api/pages/${page?.id}`, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/pages'] }); + toast({ + title: "Page modifiée", + description: "Le jeton d'accès a été mis à jour avec succès", + }); + onOpenChange(false); + setAccessToken(''); + }, + onError: () => { + toast({ + title: "Erreur", + description: "Impossible de modifier la page", + variant: "destructive", + }); + }, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!page) return; + editMutation.mutate({ + accessToken, + }); + }; + + if (!page) return null; + + return ( + + + + Modifier le jeton d'accès + + Mettez à jour le jeton d'accès pour {page.pageName} + + +
+
+ + setAccessToken(e.target.value)} + placeholder="EAAxxxxxxxxxxxxx" + required + data-testid="input-edit-access-token" + /> +

+ Obtenez un nouveau jeton depuis{' '} + + Facebook Graph API Explorer + +

+
+ +
+ + +
+
+
+
+ ); +} diff --git a/server/routes.ts b/server/routes.ts index 969dec8..22afc99 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -541,6 +541,45 @@ export async function registerRoutes(app: Express): Promise { } }); + app.put("/api/pages/:id", requireAuth, async (req, res) => { + try { + const user = req.user as User; + const userId = user.id; + const pageId = req.params.id; + + const existingPage = await storage.getSocialPage(pageId); + if (!existingPage || existingPage.userId !== userId) { + return res.status(404).json({ error: "Page non trouvée" }); + } + + const pageData = insertSocialPageSchema.partial().parse(req.body); + const updatedPage = await storage.updateSocialPage(pageId, pageData); + res.json(updatedPage); + } catch (error) { + console.error("Error updating page:", error); + res.status(500).json({ error: "Failed to update page" }); + } + }); + + app.delete("/api/pages/:id", requireAuth, async (req, res) => { + try { + const user = req.user as User; + const userId = user.id; + const pageId = req.params.id; + + const existingPage = await storage.getSocialPage(pageId); + if (!existingPage || existingPage.userId !== userId) { + return res.status(404).json({ error: "Page non trouvée" }); + } + + await storage.deleteSocialPage(pageId); + res.json({ success: true }); + } catch (error) { + console.error("Error deleting page:", error); + res.status(500).json({ error: "Failed to delete page" }); + } + }); + // AI Generations app.get("/api/ai/generations", requireAuth, async (req, res) => { try {