From bd906dbe154965d9ea5ecc5d328c3295770f9ebf Mon Sep 17 00:00:00 2001 From: Lago Date: Sun, 10 May 2026 00:52:35 +0200 Subject: [PATCH] feat: enhance n8n workflows with dynamic credential management and email configuration --- .env | 30 +++++-- docker-compose.local.yml | 2 + docker-compose.yml | 22 +++++ n8n/bootstrap/bootstrap-n8n.sh | 83 +++++++++++++++++++ .../01-qualification-payment-email.json | 78 ++++++++++------- n8n/workflows/README.md | 44 +++++----- 6 files changed, 201 insertions(+), 58 deletions(-) create mode 100644 n8n/bootstrap/bootstrap-n8n.sh diff --git a/.env b/.env index 94c51c2..f3c5016 100644 --- a/.env +++ b/.env @@ -36,11 +36,21 @@ ENABLE_EMAIL_SIGNUP=true ENABLE_EMAIL_AUTOCONFIRM=true ENABLE_ANONYMOUS_USERS=false -# ---- SMTP (dummy; real values needed only to send password-reset mail) ---- -SMTP_HOST=localhost -SMTP_PORT=2500 -SMTP_USER=fake -SMTP_PASS=fake +# ---- SMTP / IMAP (MC Cars mailbox) ---- +SMTP_HOST=heracles.mxrouting.net +SMTP_PORT=587 +SMTP_USER=office@mc-cars.at +SMTP_PASS=fhXTcjWMRpSLYYzXJsN8 + +IMAP_HOST=heracles.mxrouting.net +IMAP_PORT=993 +IMAP_USER=office@mc-cars.at +IMAP_PASS=fhXTcjWMRpSLYYzXJsN8 + +POP3_HOST=heracles.mxrouting.net +POP3_PORT=995 +POP3_USER=office@mc-cars.at +POP3_PASS=fhXTcjWMRpSLYYzXJsN8 # ---- Admin BOOTSTRAP credentials (seeded on first DB init) ---- # The user is flagged must_change_password=true. The REAL working password @@ -56,3 +66,13 @@ FILE_SIZE_LIMIT=52428800 N8N_ENCRYPTION_KEY=mc-cars-n8n-encryption-key-change-me N8N_USER_EMAIL=admin@mccars.local N8N_USER_PASSWORD=McCars-N8n-Admin1 +N8N_POSTGRES_CREDENTIAL_ID=AWozEaiOSymMj7JF +N8N_POSTGRES_CREDENTIAL_NAME=Postgres account +N8N_SMTP_CREDENTIAL_ID=nRMemi1sz2C0N4Vu +N8N_SMTP_CREDENTIAL_NAME=SMTP account +N8N_SMTP_HOST=heracles.mxrouting.net +N8N_SMTP_USER=office@mc-cars.at +N8N_SMTP_PASS=fhXTcjWMRpSLYYzXJsN8 +N8N_PAYPAL_KAUTION_LINK=https://www.google.at +N8N_PAYPAL_MIETE_LINK=https://www.google.at +N8N_PAYMENT_WORKFLOW_ID=rI1gUpcRXSikxWhh diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 0e87692..ba04e36 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -43,3 +43,5 @@ services: n8n: volumes: - ./data/n8n:/home/node/.n8n + - ./n8n/workflows:/opt/mc-cars/workflows:ro + - ./n8n/bootstrap:/opt/mc-cars/bootstrap:ro diff --git a/docker-compose.yml b/docker-compose.yml index 6bbf47c..d8ff936 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -389,6 +389,7 @@ services: N8N_PROTOCOL: http WEBHOOK_URL: http://localhost:55590/ N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY} + N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: "false" # Database (n8n stores its own data in the same Postgres) DB_TYPE: postgresdb @@ -409,8 +410,29 @@ services: # Allow importing workflows from filesystem N8N_USER_FOLDER: /home/node/.n8n + + # Workflow/credential bootstrap (re-import on every start) + N8N_POSTGRES_CREDENTIAL_ID: ${N8N_POSTGRES_CREDENTIAL_ID} + N8N_POSTGRES_CREDENTIAL_NAME: ${N8N_POSTGRES_CREDENTIAL_NAME} + N8N_SMTP_CREDENTIAL_ID: ${N8N_SMTP_CREDENTIAL_ID} + N8N_SMTP_CREDENTIAL_NAME: ${N8N_SMTP_CREDENTIAL_NAME} + N8N_SMTP_HOST: ${N8N_SMTP_HOST} + N8N_SMTP_USER: ${N8N_SMTP_USER} + N8N_SMTP_PASS: ${N8N_SMTP_PASS} + N8N_PAYPAL_KAUTION_LINK: ${N8N_PAYPAL_KAUTION_LINK} + N8N_PAYPAL_MIETE_LINK: ${N8N_PAYPAL_MIETE_LINK} + N8N_PAYMENT_WORKFLOW_ID: ${N8N_PAYMENT_WORKFLOW_ID} + N8N_WORKFLOW_TEMPLATE: /opt/mc-cars/workflows/01-qualification-payment-email.json volumes: - /mnt/user/appdata/mc-cars/data/n8n:/home/node/.n8n + - /mnt/user/appdata/mc-cars/n8n/workflows:/opt/mc-cars/workflows:ro + - /mnt/user/appdata/mc-cars/n8n/bootstrap:/opt/mc-cars/bootstrap:ro + entrypoint: ["/bin/sh", "-c"] + command: + - | + set -e + /bin/sh /opt/mc-cars/bootstrap/bootstrap-n8n.sh + exec n8n start ports: - "55590:5678" networks: [mccars] diff --git a/n8n/bootstrap/bootstrap-n8n.sh b/n8n/bootstrap/bootstrap-n8n.sh new file mode 100644 index 0000000..633026b --- /dev/null +++ b/n8n/bootstrap/bootstrap-n8n.sh @@ -0,0 +1,83 @@ +#!/bin/sh +set -eu + +WORKFLOW_TEMPLATE="${N8N_WORKFLOW_TEMPLATE:-/opt/mc-cars/workflows/01-qualification-payment-email.json}" +WORKFLOW_RENDERED="/tmp/01-qualification-payment-email.rendered.json" +CREDENTIALS_FILE="/tmp/mc-cars-credentials.json" + +required_var() { + var_name="$1" + eval "var_value=\${$var_name:-}" + if [ -z "$var_value" ]; then + echo "[n8n-bootstrap] Missing required env var: $var_name" >&2 + exit 1 + fi +} + +escape_sed() { + printf '%s' "$1" | sed -e 's/[\/&]/\\&/g' +} + +required_var N8N_POSTGRES_CREDENTIAL_ID +required_var N8N_POSTGRES_CREDENTIAL_NAME +required_var N8N_SMTP_CREDENTIAL_ID +required_var N8N_SMTP_CREDENTIAL_NAME +required_var N8N_SMTP_HOST +required_var N8N_SMTP_USER +required_var N8N_SMTP_PASS +required_var N8N_PAYPAL_KAUTION_LINK +required_var N8N_PAYPAL_MIETE_LINK +required_var DB_POSTGRESDB_PASSWORD +required_var N8N_PAYMENT_WORKFLOW_ID + +cat > "$CREDENTIALS_FILE" <&2 + exit 1 +fi + +POSTGRES_ID_ESCAPED="$(escape_sed "$N8N_POSTGRES_CREDENTIAL_ID")" +SMTP_ID_ESCAPED="$(escape_sed "$N8N_SMTP_CREDENTIAL_ID")" +KAUTION_LINK_ESCAPED="$(escape_sed "$N8N_PAYPAL_KAUTION_LINK")" +MIETE_LINK_ESCAPED="$(escape_sed "$N8N_PAYPAL_MIETE_LINK")" + +sed \ + -e "s/__POSTGRES_CREDENTIAL_ID__/${POSTGRES_ID_ESCAPED}/g" \ + -e "s/__SMTP_CREDENTIAL_ID__/${SMTP_ID_ESCAPED}/g" \ + -e "s|__PAYPAL_KAUTION_LINK__|${KAUTION_LINK_ESCAPED}|g" \ + -e "s|__PAYPAL_MIETE_LINK__|${MIETE_LINK_ESCAPED}|g" \ + "$WORKFLOW_TEMPLATE" > "$WORKFLOW_RENDERED" + +echo "[n8n-bootstrap] Importing credentials" +n8n import:credentials --input="$CREDENTIALS_FILE" + +echo "[n8n-bootstrap] Importing workflow" +n8n import:workflow --input="$WORKFLOW_RENDERED" + +echo "[n8n-bootstrap] Activating workflow ${N8N_PAYMENT_WORKFLOW_ID}" +n8n update:workflow --id="${N8N_PAYMENT_WORKFLOW_ID}" --active=true + +echo "[n8n-bootstrap] Bootstrap complete" diff --git a/n8n/workflows/01-qualification-payment-email.json b/n8n/workflows/01-qualification-payment-email.json index 4dfa51b..55c072b 100644 --- a/n8n/workflows/01-qualification-payment-email.json +++ b/n8n/workflows/01-qualification-payment-email.json @@ -3,59 +3,68 @@ "nodes": [ { "parameters": { - "triggerOnNotify": true, - "channel": "lead_qualified", - "additionalFields": {} + "schema": { + "__rl": true, + "mode": "list", + "value": "public" + }, + "tableName": { + "__rl": true, + "value": "sales_orders", + "mode": "list", + "cachedResultName": "sales_orders" + }, + "additionalFields": {}, + "options": {} }, - "id": "pg-trigger", + "id": "4337dcb2-b962-4bdb-8517-20e570016c05", "name": "Postgres Trigger", "type": "n8n-nodes-base.postgresTrigger", "typeVersion": 1, "position": [ - 250, - 300 + 0, + 0 ], "credentials": { "postgres": { - "id": "1", - "name": "MC Cars Postgres" + "id": "__POSTGRES_CREDENTIAL_ID__", + "name": "Postgres account" } } }, { "parameters": { "operation": "executeQuery", - "query": "SELECT c.id, c.name, c.email, c.phone,\n so.order_number, so.total_eur, so.deposit_eur,\n so.date_from, so.date_to, so.vehicle_label,\n so.daily_subtotal, so.weekend_subtotal,\n so.subtotal_eur, so.vat_eur,\n so.total_days, so.weekday_count, so.weekend_day_count\nFROM public.customers c\nJOIN public.sales_orders so ON so.customer_id = c.id\nWHERE so.id = '{{ $json.sales_order_id }}'::uuid", - "additionalFields": {} + "query": "SELECT c.id, c.name, c.email, c.phone,\n so.order_number, so.total_eur, so.deposit_eur,\n so.date_from, so.date_to, so.vehicle_label,\n so.daily_subtotal, so.weekend_subtotal,\n so.subtotal_eur, so.vat_eur,\n so.total_days, so.weekday_count, so.weekend_day_count\nFROM public.customers c\nJOIN public.sales_orders so ON so.customer_id = c.id\nWHERE so.id = '{{ $json.payload.id }}'::uuid", + "options": {} }, - "id": "fetch-order-data", + "id": "ca4ca61e-fea9-4044-9586-216af016cb2e", "name": "Fetch Order Data", "type": "n8n-nodes-base.postgres", "typeVersion": 2.5, "position": [ - 470, - 300 + 224, + 0 ], "credentials": { "postgres": { - "id": "1", - "name": "MC Cars Postgres" + "id": "__POSTGRES_CREDENTIAL_ID__", + "name": "Postgres account" } } }, { "parameters": { "mode": "runOnceForEachItem", - "language": "javaScript", - "jsCode": "const item = $json;\n\nconst formatEur = (value) => {\n const n = Number(value || 0);\n return new Intl.NumberFormat(\"de-AT\", { style: \"currency\", currency: \"EUR\" }).format(n);\n};\n\nconst formatDate = (value) => {\n if (!value) return \"-\";\n const d = new Date(value);\n if (Number.isNaN(d.getTime())) return String(value);\n return new Intl.DateTimeFormat(\"de-AT\", { day: \"2-digit\", month: \"2-digit\", year: \"numeric\" }).format(d);\n};\n\nconst orderNumber = item.order_number || \"\";\nconst dateFrom = formatDate(item.date_from);\nconst dateTo = formatDate(item.date_to);\nconst rentalRange = `${dateFrom} bis ${dateTo}`;\nconst paymentLink = `https://www.mc-cars.at/zahlung/${encodeURIComponent(orderNumber)}`;\n\nconst depositEur = formatEur(item.deposit_eur);\nconst rentalEur = formatEur(item.total_eur);\nconst subtotalEur = formatEur(item.subtotal_eur);\nconst vatEur = formatEur(item.vat_eur);\n\nconst safeName = item.name || \"Kundin/Kunde\";\nconst vehicle = item.vehicle_label || \"-\";\n\nconst subject = `MC Cars - Buchung bestaetigt (${orderNumber}) - Zahlungsinfos`;\n\nconst html = `\n
\n \n \n \n \n \n \n \n
\n
MC Cars
\n

Ihre Miete wurde freigegeben

\n
\n

Guten Tag ${safeName},

\n

\n Ihre Buchung wurde auf Basis der von Ihnen bereitgestellten Informationen geprueft und freigegeben.\n Nachfolgend finden Sie die Zahlungsanweisungen fuer Kaution und Mietbetrag.\n

\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Bestellnummer${orderNumber}
Fahrzeug${vehicle}
Mietzeitraum${rentalRange}
Zwischensumme${subtotalEur}
MwSt.${vatEur}
Mietbetrag${rentalEur}
Kaution${depositEur}
\n\n

1) Kaution per Ueberweisung

\n

Bitte ueberweisen Sie die Kaution in Hoehe von ${depositEur} mit folgendem Verwendungszweck: Kaution ${orderNumber}.

\n\n

2) Mietbetrag online bezahlen

\n

Den Mietbetrag von ${rentalEur} koennen Sie direkt ueber folgenden Link bezahlen:

\n\n

\n Zur Zahlung (${orderNumber})\n

\n\n

Falls Sie Fragen haben, antworten Sie einfach auf diese E-Mail.

\n

Freundliche Gruesse
MC Cars Team

\n
\n
`;\n\nreturn {\n toEmail: item.email,\n subject,\n html,\n};" + "jsCode": "const item = $json;\n\nconst formatEur = (value) => {\n const n = Number(value || 0);\n return new Intl.NumberFormat(\"de-AT\", { style: \"currency\", currency: \"EUR\" }).format(n);\n};\n\nconst formatDate = (value) => {\n if (!value) return \"-\";\n const d = new Date(value);\n if (Number.isNaN(d.getTime())) return String(value);\n return new Intl.DateTimeFormat(\"de-AT\", { day: \"2-digit\", month: \"2-digit\", year: \"numeric\" }).format(d);\n};\n\nconst orderNumber = item.order_number || \"\";\nconst dateFrom = formatDate(item.date_from);\nconst dateTo = formatDate(item.date_to);\nconst rentalRange = `${dateFrom} bis ${dateTo}`;\n\nconst depositEur = formatEur(item.deposit_eur);\nconst rentalEur = formatEur(item.total_eur);\nconst subtotalEur = formatEur(item.subtotal_eur);\nconst vatEur = formatEur(item.vat_eur);\n\nconst safeName = item.name || \"Kundin/Kunde\";\nconst vehicle = item.vehicle_label || \"-\";\n\nconst subject = `MC Cars – Buchung bestätigt (${orderNumber}) – Zahlungsinformationen`;\n\nconst paypalButton = (url, alt) => `\n \n \"${alt}\"\n \n`;\n\nconst html = `\n
\n \n \n \n \n \n \n \n
\n
MC Cars
\n

Ihre Miete wurde freigegeben

\n
\n

Guten Tag ${safeName},

\n

\n Ihre Buchung wurde auf Basis der von Ihnen bereitgestellten Informationen geprüft und freigegeben.\n Nachfolgend finden Sie die Zahlungsanweisungen für Kaution und Mietbetrag.\n

\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Bestellnummer${orderNumber}
Fahrzeug${vehicle}
Mietzeitraum${rentalRange}
Zwischensumme${subtotalEur}
MwSt.${vatEur}
Mietbetrag${rentalEur}
Kaution${depositEur}
\n\n

1) Kaution bezahlen

\n

Sie können die Kaution in Höhe von ${depositEur} per PayPal bezahlen:

\n ${paypalButton('__PAYPAL_KAUTION_LINK__', 'PayPal Kaution bezahlen')}\n

Alternativ ist Barzahlung möglich. In diesem Fall antworten Sie bitte so schnell wie möglich auf diese E-Mail, damit wir die Übergabe bestätigen können.

\n\n

2) Mietbetrag bezahlen

\n

Den Mietbetrag von ${rentalEur} können Sie über folgenden PayPal-Button bezahlen:

\n ${paypalButton('__PAYPAL_MIETE_LINK__', 'PayPal Mietbetrag bezahlen')}\n\n

Falls Sie Fragen haben, antworten Sie einfach auf diese E-Mail.

\n

Freundliche Grüße
MC Cars Team

\n
\n
`;\n\nreturn {\n toEmail: item.email,\n subject,\n html,\n};" }, - "id": "build-payment-email", + "id": "a39ba066-d093-4444-bd45-6a295d599637", "name": "Build Payment Email", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 690, - 300 + 448, + 0 ] }, { @@ -63,26 +72,28 @@ "fromEmail": "office@mc-cars.at", "toEmail": "={{ $json.toEmail }}", "subject": "={{ $json.subject }}", - "emailType": "html", "html": "={{ $json.html }}", - "options": {} + "options": { + "appendAttribution": false + } }, - "id": "send-email", + "id": "c537fb83-bc97-4015-b9fe-5ff3ca8b2c97", "name": "Send Payment Email", "type": "n8n-nodes-base.emailSend", "typeVersion": 2.1, "position": [ - 910, - 300 + 672, + 0 ], "credentials": { "smtp": { - "id": "2", - "name": "MC Cars SMTP" + "id": "__SMTP_CREDENTIAL_ID__", + "name": "SMTP account" } } } ], + "pinData": {}, "connections": { "Postgres Trigger": { "main": [ @@ -118,14 +129,19 @@ ] } }, + "active": true, "settings": { - "executionOrder": "v1" + "executionOrder": "v1", + "binaryMode": "separate" }, - "staticData": null, + "versionId": "814e8f72-95dc-4097-bc94-76fe437448ee", + "meta": { + "templateCredsSetupCompleted": true + }, + "id": "rI1gUpcRXSikxWhh", "tags": [ { "name": "mc-cars" } - ], - "triggerCount": 1 + ] } diff --git a/n8n/workflows/README.md b/n8n/workflows/README.md index 3f6b42d..15cb5d2 100644 --- a/n8n/workflows/README.md +++ b/n8n/workflows/README.md @@ -30,36 +30,36 @@ This folder contains exportable n8n workflow definitions for the MC Cars qualifi ## Setup Instructions -### 1. Create Postgres credential in n8n -- **Name:** `MC Cars Postgres` -- **Host:** `db` -- **Port:** `5432` -- **Database:** `postgres` -- **User:** `postgres` -- **Password:** (value of `POSTGRES_PASSWORD` from `.env`) +### 1. Configure `.env` +The stack now bootstraps n8n credentials/workflow automatically on every `docker compose up`. -### 2. Create SMTP credential in n8n -- **Name:** `MC Cars SMTP` -- **Host:** `heracles.mxrouting.net` -- **Port:** `587` (STARTTLS) or `465` (SSL/TLS) -- **User:** `office@mc-cars.at` -- **Password:** use the mailbox password provided out-of-band (do not commit secrets to git) -- **From:** `office@mc-cars.at` +Required env variables: +- `POSTGRES_PASSWORD` +- `N8N_POSTGRES_CREDENTIAL_ID` +- `N8N_POSTGRES_CREDENTIAL_NAME` +- `N8N_SMTP_CREDENTIAL_ID` +- `N8N_SMTP_CREDENTIAL_NAME` +- `N8N_SMTP_HOST` +- `N8N_SMTP_USER` +- `N8N_SMTP_PASS` +- `N8N_PAYPAL_KAUTION_LINK` +- `N8N_PAYPAL_MIETE_LINK` +- `N8N_PAYMENT_WORKFLOW_ID` -### 3. Mailbox reference (for future incoming-email workflows) +### 2. Mailbox reference (for future incoming-email workflows) - **IMAP host:** `heracles.mxrouting.net` (port `993`, SSL/TLS) - **POP3 host:** `heracles.mxrouting.net` (port `995`, SSL/TLS) - **Username:** `office@mc-cars.at` - **Password:** same mailbox password as SMTP -### 4. Import workflows -1. Open n8n at http://localhost:55590 -2. Go to **Workflows** → **Import from file** -3. Import `01-qualification-payment-email.json` -4. Import `02-mietvertrag-pdf-email.json` -5. Open each workflow → assign the credentials created above → **Activate** +### 3. Import behavior +On startup, n8n runs `/opt/mc-cars/bootstrap/bootstrap-n8n.sh` which: +1. Creates/updates Postgres and SMTP credentials from `.env` +2. Renders `01-qualification-payment-email.json` placeholders +3. Imports the workflow so nodes are always linked to the expected credential IDs +4. Activates the payment workflow automatically (`n8n update:workflow --active=true`) -### 5. Upload Mietvertrag template (optional) +### 4. Upload Mietvertrag template (optional) 1. Open Admin panel → **Einstellungen** tab 2. Upload a DOCX file in the "Mietvertrag-Vorlage" section 3. The template should contain these placeholders: