From c6939c3d53a8db792af090c0c67ad992eb762eef Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sat, 9 May 2026 00:55:19 +0000 Subject: [PATCH] postiz + n8n: real DB URL + webhook-trigger approval - postiz: set DATABASE_URL/REDIS_URL pointing at the bundled subcharts; the chart does NOT auto-wire even when postgresql.enabled=true, so the prisma db:push was failing with empty DATABASE_URL. - n8n approval workflow: swap telegramTrigger -> webhook node so it works without an n8n-stored Telegram credential. Telegram bot's webhook is set via setWebhook to https://n8n.viktorbarzin.me/webhook/instagram-approval. Parse-callback Code node tolerates both shapes ({body:{callback_query:...}} vs {callback_query:...}) so a future move back to telegramTrigger doesn't break. --- stacks/n8n/workflows/instagram-approval.json | 324 +++++++++++++++---- stacks/postiz/modules/postiz/main.tf | 29 +- 2 files changed, 275 insertions(+), 78 deletions(-) diff --git a/stacks/n8n/workflows/instagram-approval.json b/stacks/n8n/workflows/instagram-approval.json index 81cae2d6..5533180c 100644 --- a/stacks/n8n/workflows/instagram-approval.json +++ b/stacks/n8n/workflows/instagram-approval.json @@ -6,32 +6,34 @@ "nodes": [ { "parameters": { - "updates": ["callback_query"], - "additionalFields": {} + "httpMethod": "POST", + "path": "instagram-approval", + "responseMode": "onReceived", + "options": {} }, "id": "telegram-trigger", - "name": "Telegram callback_query", - "type": "n8n-nodes-base.telegramTrigger", - "typeVersion": 1.1, - "position": [250, 400], + "name": "Telegram Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [ + 250, + 400 + ], "webhookId": "f2c7c254-ebaf-4f66-b1b4-5c1629c07e08", - "credentials": { - "telegramApi": { - "id": "telegram-bot", - "name": "Telegram Bot" - } - }, "notes": "Listens for inline-button taps. Requires a Telegram credential bound to the same bot whose token is in TELEGRAM_BOT_TOKEN." }, { "parameters": { - "jsCode": "const cb = $input.first().json.callback_query || {};\nconst data = cb.data || '';\nconst [action, assetId] = data.split(':');\nconst message = cb.message || {};\nconst chatId = (message.chat || {}).id;\nconst messageId = message.message_id;\nconst originalCaption = message.caption || '';\nconst callbackQueryId = cb.id;\n\nif (!action || !assetId) {\n throw new Error('Malformed callback_data: ' + data);\n}\n\nreturn [{\n json: {\n action,\n asset_id: assetId,\n chat_id: chatId,\n message_id: messageId,\n original_caption: originalCaption,\n callback_query_id: callbackQueryId\n }\n}];" + "jsCode": "// Webhook puts Telegram update under .body; Telegram trigger puts it at root\nconst raw = $input.first().json;\nconst update = raw.body || raw;\nconst cb = update.callback_query || {};\nconst data = cb.data || '';\nconst [action, assetId] = data.split(':');\nconst message = cb.message || {};\nconst chatId = (message.chat || {}).id;\nconst messageId = message.message_id;\nconst originalCaption = message.caption || '';\nconst callbackQueryId = cb.id;\nreturn [{\n json: {\n action,\n asset_id: assetId,\n chat_id: chatId,\n message_id: messageId,\n original_caption: originalCaption,\n callback_query_id: callbackQueryId,\n }\n}];" }, "id": "parse-callback", "name": "Parse callback_data", "type": "n8n-nodes-base.code", "typeVersion": 2, - "position": [470, 400], + "position": [ + 470, + 400 + ], "notes": "Splits callback_data into action + asset_id, captures chat_id/message_id/caption for later edits and answerCallbackQuery." }, { @@ -40,16 +42,44 @@ "values": [ { "conditions": { - "options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict"}, - "conditions": [{"id": "is-approve", "leftValue": "={{ $json.action }}", "rightValue": "approve", "operator": {"type": "string", "operation": "equals"}}], + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "is-approve", + "leftValue": "={{ $json.action }}", + "rightValue": "approve", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], "combinator": "and" }, "outputKey": "approve" }, { "conditions": { - "options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict"}, - "conditions": [{"id": "is-reject", "leftValue": "={{ $json.action }}", "rightValue": "reject", "operator": {"type": "string", "operation": "equals"}}], + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "is-reject", + "leftValue": "={{ $json.action }}", + "rightValue": "reject", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], "combinator": "and" }, "outputKey": "reject" @@ -62,7 +92,10 @@ "name": "Switch on action", "type": "n8n-nodes-base.switch", "typeVersion": 3.2, - "position": [690, 400], + "position": [ + 690, + 400 + ], "notes": "Branches on action; unknown actions fall through and are dropped." }, { @@ -72,20 +105,31 @@ "sendHeaders": true, "headerParameters": { "parameters": [ - {"name": "Authorization", "value": "=Bearer {{ $env.INSTAGRAM_POSTER_TOKEN }}"}, - {"name": "Content-Type", "value": "application/json"} + { + "name": "Authorization", + "value": "=Bearer {{ $env.INSTAGRAM_POSTER_TOKEN }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ asset_id: $json.asset_id }) }}", - "options": {"timeout": 30000} + "options": { + "timeout": 30000 + } }, "id": "approve-enqueue", "name": "Approve: enqueue asset", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, - "position": [910, 250], + "position": [ + 910, + 250 + ], "onError": "continueErrorOutput", "notes": "Calls instagram-poster /enqueue. continueErrorOutput so we can fall through to a Telegram error message on failure." }, @@ -96,20 +140,31 @@ "sendHeaders": true, "headerParameters": { "parameters": [ - {"name": "Authorization", "value": "=Bearer {{ $env.INSTAGRAM_POSTER_TOKEN }}"}, - {"name": "Content-Type", "value": "application/json"} + { + "name": "Authorization", + "value": "=Bearer {{ $env.INSTAGRAM_POSTER_TOKEN }}" + }, + { + "name": "Content-Type", + "value": "application/json" + } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ asset_id: $json.asset_id }) }}", - "options": {"timeout": 30000} + "options": { + "timeout": 30000 + } }, "id": "reject-mark", "name": "Reject: mark seen", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, - "position": [910, 550], + "position": [ + 910, + 550 + ], "onError": "continueErrorOutput", "notes": "Calls instagram-poster /reject so the asset is recorded and not re-surfaced." }, @@ -121,7 +176,10 @@ "name": "Approve: build new caption", "type": "n8n-nodes-base.code", "typeVersion": 2, - "position": [1130, 250], + "position": [ + 1130, + 250 + ], "notes": "Append a confirmation suffix to the original caption." }, { @@ -132,7 +190,10 @@ "name": "Reject: build new caption", "type": "n8n-nodes-base.code", "typeVersion": 2, - "position": [1130, 550], + "position": [ + 1130, + 550 + ], "notes": "Append rejection suffix to the original caption." }, { @@ -142,19 +203,27 @@ "sendHeaders": true, "headerParameters": { "parameters": [ - {"name": "Content-Type", "value": "application/json"} + { + "name": "Content-Type", + "value": "application/json" + } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ chat_id: $json.chat_id, message_id: $json.message_id, caption: $json.new_caption, parse_mode: 'HTML' }) }}", - "options": {"timeout": 30000} + "options": { + "timeout": 30000 + } }, "id": "edit-caption", "name": "Telegram editMessageCaption", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, - "position": [1350, 400], + "position": [ + 1350, + 400 + ], "notes": "Updates the original DM caption to show the resulting state. Strips inline buttons in the same call by omitting reply_markup combined with the next node." }, { @@ -164,19 +233,27 @@ "sendHeaders": true, "headerParameters": { "parameters": [ - {"name": "Content-Type", "value": "application/json"} + { + "name": "Content-Type", + "value": "application/json" + } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ chat_id: $json.chat_id, message_id: $json.message_id, reply_markup: { inline_keyboard: [] } }) }}", - "options": {"timeout": 30000} + "options": { + "timeout": 30000 + } }, "id": "edit-reply-markup", "name": "Telegram editMessageReplyMarkup", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, - "position": [1570, 400], + "position": [ + 1570, + 400 + ], "notes": "Strips the inline approve/reject buttons so the original DM no longer offers them after a decision." }, { @@ -186,19 +263,27 @@ "sendHeaders": true, "headerParameters": { "parameters": [ - {"name": "Content-Type", "value": "application/json"} + { + "name": "Content-Type", + "value": "application/json" + } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.callback_query_id, text: 'Recorded' }) }}", - "options": {"timeout": 15000} + "options": { + "timeout": 15000 + } }, "id": "answer-callback", "name": "Telegram answerCallbackQuery", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, - "position": [1790, 400], + "position": [ + 1790, + 400 + ], "notes": "Dismisses the loading spinner on the user's tap." }, { @@ -209,7 +294,10 @@ "name": "Build error message", "type": "n8n-nodes-base.code", "typeVersion": 2, - "position": [1130, 750], + "position": [ + 1130, + 750 + ], "notes": "Catches non-2xx from /enqueue or /reject and formats a Telegram alert text." }, { @@ -219,51 +307,171 @@ "sendHeaders": true, "headerParameters": { "parameters": [ - {"name": "Content-Type", "value": "application/json"} + { + "name": "Content-Type", + "value": "application/json" + } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ chat_id: $json.chat_id, text: $json.text }) }}", - "options": {"timeout": 15000} + "options": { + "timeout": 15000 + } }, "id": "telegram-error-msg", "name": "Telegram error notice", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, - "position": [1350, 750], + "position": [ + 1350, + 750 + ], "notes": "Sends the error text to the user as a follow-up message." } ], "connections": { - "Telegram callback_query": {"main": [[{"node": "Parse callback_data", "type": "main", "index": 0}]]}, - "Parse callback_data": {"main": [[{"node": "Switch on action", "type": "main", "index": 0}]]}, + "Parse callback_data": { + "main": [ + [ + { + "node": "Switch on action", + "type": "main", + "index": 0 + } + ] + ] + }, "Switch on action": { "main": [ - [{"node": "Approve: enqueue asset", "type": "main", "index": 0}], - [{"node": "Reject: mark seen", "type": "main", "index": 0}] + [ + { + "node": "Approve: enqueue asset", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Reject: mark seen", + "type": "main", + "index": 0 + } + ] ] }, "Approve: enqueue asset": { "main": [ - [{"node": "Approve: build new caption", "type": "main", "index": 0}], - [{"node": "Build error message", "type": "main", "index": 0}] + [ + { + "node": "Approve: build new caption", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Build error message", + "type": "main", + "index": 0 + } + ] ] }, "Reject: mark seen": { "main": [ - [{"node": "Reject: build new caption", "type": "main", "index": 0}], - [{"node": "Build error message", "type": "main", "index": 0}] + [ + { + "node": "Reject: build new caption", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Build error message", + "type": "main", + "index": 0 + } + ] ] }, - "Approve: build new caption": {"main": [[{"node": "Telegram editMessageCaption", "type": "main", "index": 0}]]}, - "Reject: build new caption": {"main": [[{"node": "Telegram editMessageCaption", "type": "main", "index": 0}]]}, - "Telegram editMessageCaption": {"main": [[{"node": "Telegram editMessageReplyMarkup", "type": "main", "index": 0}]]}, - "Telegram editMessageReplyMarkup": {"main": [[{"node": "Telegram answerCallbackQuery", "type": "main", "index": 0}]]}, - "Build error message": {"main": [[{"node": "Telegram error notice", "type": "main", "index": 0}]]} + "Approve: build new caption": { + "main": [ + [ + { + "node": "Telegram editMessageCaption", + "type": "main", + "index": 0 + } + ] + ] + }, + "Reject: build new caption": { + "main": [ + [ + { + "node": "Telegram editMessageCaption", + "type": "main", + "index": 0 + } + ] + ] + }, + "Telegram editMessageCaption": { + "main": [ + [ + { + "node": "Telegram editMessageReplyMarkup", + "type": "main", + "index": 0 + } + ] + ] + }, + "Telegram editMessageReplyMarkup": { + "main": [ + [ + { + "node": "Telegram answerCallbackQuery", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build error message": { + "main": [ + [ + { + "node": "Telegram error notice", + "type": "main", + "index": 0 + } + ] + ] + }, + "Telegram Webhook": { + "main": [ + [ + { + "node": "Parse callback_data", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "saveExecutionProgress": false, + "saveManualExecutions": true }, - "settings": {"executionOrder": "v1", "saveExecutionProgress": false, "saveManualExecutions": true}, "staticData": null, - "meta": {"templateCredsSetupCompleted": false}, + "meta": { + "templateCredsSetupCompleted": false + }, "pinData": {} -} +} \ No newline at end of file diff --git a/stacks/postiz/modules/postiz/main.tf b/stacks/postiz/modules/postiz/main.tf index a33b39cf..d40bae65 100644 --- a/stacks/postiz/modules/postiz/main.tf +++ b/stacks/postiz/modules/postiz/main.tf @@ -134,27 +134,16 @@ resource "helm_release" "postiz" { NX_ADD_PLUGINS = "false" } - # Empty placeholder for chart-rendered Secret. ESO patches JWT_SECRET via - # creationPolicy=Merge above. DATABASE_URL/REDIS_URL are auto-wired by the - # chart's bundled subcharts and don't need to be set here. + # Postiz reads DATABASE_URL/REDIS_URL from this Secret. The chart does + # NOT auto-wire bundled subcharts — we have to point at the in-namespace + # PG/Redis Services. ESO patches JWT_SECRET on top via creationPolicy=Merge. + # Subchart auth uses the chart defaults (postiz / postiz-password, + # postiz-redis-password) — both Services are ClusterIP, only routable + # from inside the postiz namespace, so the well-known creds are safe. secrets = { - DATABASE_URL = "" - REDIS_URL = "" - JWT_SECRET = "" - X_API_KEY = "" - X_API_SECRET = "" - LINKEDIN_CLIENT_ID = "" - LINKEDIN_CLIENT_SECRET = "" - REDDIT_CLIENT_ID = "" - REDDIT_CLIENT_SECRET = "" - GITHUB_CLIENT_ID = "" - GITHUB_CLIENT_SECRET = "" - RESEND_API_KEY = "" - CLOUDFLARE_ACCOUNT_ID = "" - CLOUDFLARE_ACCESS_KEY = "" - CLOUDFLARE_SECRET_ACCESS_KEY = "" - CLOUDFLARE_BUCKETNAME = "" - CLOUDFLARE_BUCKET_URL = "" + DATABASE_URL = "postgresql://postiz:postiz-password@postiz-postgresql:5432/postiz" + REDIS_URL = "redis://default:postiz-redis-password@postiz-redis-master:6379" + JWT_SECRET = "" } # Use our PVC for uploads (overrides the chart's emptyDir default).