From bdd8501467fa5f977627b97b0484a9955c83cc34 Mon Sep 17 00:00:00 2001 From: Meik Date: Fri, 8 May 2026 00:09:13 +0200 Subject: [PATCH] Allow internal Paperless webhook target --- .gitignore | 1 + README.md | 1 + frontend/src/api/config.ts | 3 +++ frontend/src/locales/de/common.json | 5 ++++- frontend/src/locales/en/common.json | 5 ++++- frontend/src/routes/Settings.tsx | 13 +++++++++++++ src/config.ts | 2 ++ src/index.ts | 11 +++++++++-- src/runtimeSettings.ts | 2 ++ src/settingsStore.ts | 1 + 10 files changed, 40 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index a8a891b..5ec7c26 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ frontend/node_modules/ +frontend/dist/ diff --git a/README.md b/README.md index 58ae697..63e5f3a 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ Für Portainer-Deployments ist `docker-compose.yml` als Git-basierter Stack vorb | `AUTH_TOKEN_EXPIRES_IN_HOURS` | `12` | Lebensdauer der Tokens. | | `PAPERLESS_BASE_URL` | *(leer)* | API-URL deiner paperless-ngx Instanz. | | `PAPERLESS_EXTERNAL_URL` | *(leer)* | Optionale externe URL für Direktlinks im Browser. | +| `PAPERLESS_WEBHOOK_URL` | *(leer)* | Optionale Ziel-URL für den automatisch angelegten Paperless-Webhook. Sinnvoll für interne URLs wie `http://host:8112/integrations/paperless/webhook`, wenn der externe Proxy eingeschränkt ist. | | `APP_EXTERNAL_URL` | *(leer)* | Optionale externe URL der App (z. B. für Links im iCal-Feed), überschreibt den Host der eingehenden Anfrage. | | `APP_LOCALE` | `de` | Sprache für systemseitige Nachrichten (z. B. ntfy, E-Mail-Betreff); derzeit `de` oder `en`. | | `PAPERLESS_TOKEN` | *(leer)* | API-Token aus paperless-ngx. | diff --git a/frontend/src/api/config.ts b/frontend/src/api/config.ts index b547456..5e88657 100644 --- a/frontend/src/api/config.ts +++ b/frontend/src/api/config.ts @@ -6,6 +6,7 @@ export interface ServerConfig { databasePath: string; paperlessBaseUrl: string | null; paperlessExternalUrl: string | null; + paperlessWebhookUrl: string | null; appExternalUrl: string | null; appLocale: string; paperlessConfigured: boolean; @@ -32,6 +33,7 @@ export interface SettingsResponse { values: { paperlessBaseUrl: string | null; paperlessExternalUrl: string | null; + paperlessWebhookUrl: string | null; appExternalUrl: string | null; appLocale: string; schedulerIntervalMinutes: number; @@ -69,6 +71,7 @@ export interface SettingsResponse { export type UpdateSettingsPayload = Partial<{ paperlessBaseUrl: string | null; paperlessExternalUrl: string | null; + paperlessWebhookUrl: string | null; appExternalUrl: string | null; appLocale: string; paperlessToken: string | null; diff --git a/frontend/src/locales/de/common.json b/frontend/src/locales/de/common.json index 910be1f..65cf7a9 100644 --- a/frontend/src/locales/de/common.json +++ b/frontend/src/locales/de/common.json @@ -203,12 +203,15 @@ "webhookSetupRunning": "Konfiguriere Paperless…", "webhookSetupSuccess": "Paperless-Workflow konfiguriert (ID {{id}})", "webhookSetupError": "Paperless-Workflow konnte nicht konfiguriert werden", - "webhookSetupHelp": "Legt ein Secret an und erstellt oder aktualisiert in Paperless den Workflow \"Contract Companion AI Review\".", + "webhookSetupHelp": "Legt ein Secret an und erstellt oder aktualisiert in Paperless den Workflow \"Contract Companion AI Review\". Wenn eine Webhook-Ziel-URL gesetzt ist, nutzt Paperless diese URL.", "webhookWorkflowInfo": "Paperless-Workflow ID {{id}} ist verknüpft.", "ical": "iCal-Abo", "icalFeedUrl": "Feed-URL", "paperlessApiUrl": "Paperless API URL", "paperlessExternalUrl": "Paperless externe URL (für Direktlink)", + "paperlessWebhookUrl": "Paperless Webhook-Ziel-URL", + "paperlessWebhookUrlExample": "http://192.168.178.33:8112/integrations/paperless/webhook", + "paperlessWebhookUrlHelp": "URL, die Paperless beim Workflow aufruft. Nutze hier bevorzugt eine interne URL zur Backend-API, wenn der externe Proxy Zugriffsbeschränkungen hat.", "appExternalUrl": "Externe URL der App", "paperlessExample": "https://paperless.example.com", "appExternalExample": "https://contracts.example.com", diff --git a/frontend/src/locales/en/common.json b/frontend/src/locales/en/common.json index a63ebb0..302271d 100644 --- a/frontend/src/locales/en/common.json +++ b/frontend/src/locales/en/common.json @@ -203,12 +203,15 @@ "webhookSetupRunning": "Configuring Paperless…", "webhookSetupSuccess": "Paperless workflow configured (ID {{id}})", "webhookSetupError": "Unable to configure Paperless workflow", - "webhookSetupHelp": "Creates a secret and creates or updates the Paperless workflow \"Contract Companion AI Review\".", + "webhookSetupHelp": "Creates a secret and creates or updates the Paperless workflow \"Contract Companion AI Review\". If a webhook target URL is set, Paperless uses that URL.", "webhookWorkflowInfo": "Paperless workflow ID {{id}} is linked.", "ical": "iCal subscription", "icalFeedUrl": "Feed URL", "paperlessApiUrl": "Paperless API URL", "paperlessExternalUrl": "Paperless external URL (for direct link)", + "paperlessWebhookUrl": "Paperless webhook target URL", + "paperlessWebhookUrlExample": "http://192.168.178.33:8112/integrations/paperless/webhook", + "paperlessWebhookUrlHelp": "URL called by the Paperless workflow. Prefer an internal backend API URL when the external proxy has access restrictions.", "appExternalUrl": "App external URL", "paperlessExample": "https://paperless.example.com", "appExternalExample": "https://contracts.example.com", diff --git a/frontend/src/routes/Settings.tsx b/frontend/src/routes/Settings.tsx index 2ac508e..5cbcbb7 100644 --- a/frontend/src/routes/Settings.tsx +++ b/frontend/src/routes/Settings.tsx @@ -59,6 +59,7 @@ interface HealthResponse { type FormValues = { paperlessBaseUrl: string; paperlessExternalUrl: string; + paperlessWebhookUrl: string; appExternalUrl: string; appLocale: string; paperlessToken: string; @@ -91,6 +92,7 @@ type FormValues = { const defaultValues: FormValues = { paperlessBaseUrl: "", paperlessExternalUrl: "", + paperlessWebhookUrl: "", appExternalUrl: "", appLocale: "de", paperlessToken: "", @@ -187,6 +189,7 @@ export default function SettingsPage() { const values: FormValues = { paperlessBaseUrl: settingsData.values.paperlessBaseUrl ?? "", paperlessExternalUrl: settingsData.values.paperlessExternalUrl ?? "", + paperlessWebhookUrl: settingsData.values.paperlessWebhookUrl ?? "", appExternalUrl: settingsData.values.appExternalUrl ?? "", appLocale: settingsData.values.appLocale ?? "de", paperlessToken: "", @@ -354,6 +357,9 @@ export default function SettingsPage() { if (formValues.paperlessExternalUrl !== initial.paperlessExternalUrl) { payload.paperlessExternalUrl = trimOrNull(formValues.paperlessExternalUrl); } + if (formValues.paperlessWebhookUrl !== initial.paperlessWebhookUrl) { + payload.paperlessWebhookUrl = trimOrNull(formValues.paperlessWebhookUrl); + } if (formValues.appExternalUrl !== initial.appExternalUrl) { payload.appExternalUrl = trimOrNull(formValues.appExternalUrl); } @@ -707,6 +713,13 @@ export default function SettingsPage() { placeholder={t("settings.paperlessExample")} fullWidth /> + { databasePath: config.databasePath, paperlessBaseUrl: runtime.paperlessBaseUrl, paperlessExternalUrl: runtime.paperlessExternalUrl, + paperlessWebhookUrl: runtime.paperlessWebhookUrl, appExternalUrl: runtime.appExternalUrl, appLocale: runtime.appLocale, paperlessConfigured, @@ -431,6 +434,9 @@ app.put("/settings", (req, res) => { if (Object.prototype.hasOwnProperty.call(payload, "paperlessExternalUrl")) { update.paperlessExternalUrl = payload.paperlessExternalUrl ?? null; } + if (Object.prototype.hasOwnProperty.call(payload, "paperlessWebhookUrl")) { + update.paperlessWebhookUrl = payload.paperlessWebhookUrl ?? null; + } if (Object.prototype.hasOwnProperty.call(payload, "appExternalUrl")) { update.appExternalUrl = payload.appExternalUrl ?? null; } @@ -542,8 +548,9 @@ app.post("/settings/paperless-webhook/setup", async (req, res, next) => { } const secret = runtime.paperlessWebhookSecret ?? generateSecret(32); - const appUrl = resolveAppBaseUrl(req, runtime); - const webhookUrl = `${appUrl}/api/integrations/paperless/webhook`; + const webhookUrl = runtime.paperlessWebhookUrl + ? runtime.paperlessWebhookUrl.replace(/\/$/, "") + : `${resolveAppBaseUrl(req, runtime)}/api/integrations/paperless/webhook`; const workflow = await paperlessClient.upsertContractCompanionWorkflow(webhookUrl, secret); const workflowId = typeof workflow.id === "number" ? workflow.id : null; const updated = updateRuntimeSettings({ diff --git a/src/runtimeSettings.ts b/src/runtimeSettings.ts index a98bb55..0af4dfd 100644 --- a/src/runtimeSettings.ts +++ b/src/runtimeSettings.ts @@ -12,6 +12,7 @@ import { export interface RuntimeSettings { paperlessBaseUrl: string | null; paperlessExternalUrl: string | null; + paperlessWebhookUrl: string | null; appExternalUrl: string | null; appLocale: string; paperlessToken: string | null; @@ -123,6 +124,7 @@ export function getRuntimeSettings(): RuntimeSettings { return { paperlessBaseUrl: coerceString(stored.paperlessBaseUrl, config.paperlessBaseUrl ?? null), paperlessExternalUrl: coerceString(stored.paperlessExternalUrl, config.paperlessExternalUrl ?? null), + paperlessWebhookUrl: coerceString(stored.paperlessWebhookUrl, config.paperlessWebhookUrl ?? null), appExternalUrl: coerceString(stored.appExternalUrl, config.appExternalUrl ?? null), appLocale: normalizeLocale(stored.appLocale, config.appLocale), paperlessToken: coerceString(stored.paperlessToken, config.paperlessToken ?? null), diff --git a/src/settingsStore.ts b/src/settingsStore.ts index ae37570..33c9572 100644 --- a/src/settingsStore.ts +++ b/src/settingsStore.ts @@ -10,6 +10,7 @@ const listStmt = db.prepare("SELECT key, value FROM settings"); export type SettingKey = | "paperlessBaseUrl" | "paperlessExternalUrl" + | "paperlessWebhookUrl" | "appExternalUrl" | "appLocale" | "paperlessToken"