Allow internal Paperless webhook target
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
|
frontend/dist/
|
||||||
|
|||||||
@@ -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. |
|
| `AUTH_TOKEN_EXPIRES_IN_HOURS` | `12` | Lebensdauer der Tokens. |
|
||||||
| `PAPERLESS_BASE_URL` | *(leer)* | API-URL deiner paperless-ngx Instanz. |
|
| `PAPERLESS_BASE_URL` | *(leer)* | API-URL deiner paperless-ngx Instanz. |
|
||||||
| `PAPERLESS_EXTERNAL_URL` | *(leer)* | Optionale externe URL für Direktlinks im Browser. |
|
| `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_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`. |
|
| `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. |
|
| `PAPERLESS_TOKEN` | *(leer)* | API-Token aus paperless-ngx. |
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface ServerConfig {
|
|||||||
databasePath: string;
|
databasePath: string;
|
||||||
paperlessBaseUrl: string | null;
|
paperlessBaseUrl: string | null;
|
||||||
paperlessExternalUrl: string | null;
|
paperlessExternalUrl: string | null;
|
||||||
|
paperlessWebhookUrl: string | null;
|
||||||
appExternalUrl: string | null;
|
appExternalUrl: string | null;
|
||||||
appLocale: string;
|
appLocale: string;
|
||||||
paperlessConfigured: boolean;
|
paperlessConfigured: boolean;
|
||||||
@@ -32,6 +33,7 @@ export interface SettingsResponse {
|
|||||||
values: {
|
values: {
|
||||||
paperlessBaseUrl: string | null;
|
paperlessBaseUrl: string | null;
|
||||||
paperlessExternalUrl: string | null;
|
paperlessExternalUrl: string | null;
|
||||||
|
paperlessWebhookUrl: string | null;
|
||||||
appExternalUrl: string | null;
|
appExternalUrl: string | null;
|
||||||
appLocale: string;
|
appLocale: string;
|
||||||
schedulerIntervalMinutes: number;
|
schedulerIntervalMinutes: number;
|
||||||
@@ -69,6 +71,7 @@ export interface SettingsResponse {
|
|||||||
export type UpdateSettingsPayload = Partial<{
|
export type UpdateSettingsPayload = Partial<{
|
||||||
paperlessBaseUrl: string | null;
|
paperlessBaseUrl: string | null;
|
||||||
paperlessExternalUrl: string | null;
|
paperlessExternalUrl: string | null;
|
||||||
|
paperlessWebhookUrl: string | null;
|
||||||
appExternalUrl: string | null;
|
appExternalUrl: string | null;
|
||||||
appLocale: string;
|
appLocale: string;
|
||||||
paperlessToken: string | null;
|
paperlessToken: string | null;
|
||||||
|
|||||||
@@ -203,12 +203,15 @@
|
|||||||
"webhookSetupRunning": "Konfiguriere Paperless…",
|
"webhookSetupRunning": "Konfiguriere Paperless…",
|
||||||
"webhookSetupSuccess": "Paperless-Workflow konfiguriert (ID {{id}})",
|
"webhookSetupSuccess": "Paperless-Workflow konfiguriert (ID {{id}})",
|
||||||
"webhookSetupError": "Paperless-Workflow konnte nicht konfiguriert werden",
|
"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.",
|
"webhookWorkflowInfo": "Paperless-Workflow ID {{id}} ist verknüpft.",
|
||||||
"ical": "iCal-Abo",
|
"ical": "iCal-Abo",
|
||||||
"icalFeedUrl": "Feed-URL",
|
"icalFeedUrl": "Feed-URL",
|
||||||
"paperlessApiUrl": "Paperless API URL",
|
"paperlessApiUrl": "Paperless API URL",
|
||||||
"paperlessExternalUrl": "Paperless externe URL (für Direktlink)",
|
"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",
|
"appExternalUrl": "Externe URL der App",
|
||||||
"paperlessExample": "https://paperless.example.com",
|
"paperlessExample": "https://paperless.example.com",
|
||||||
"appExternalExample": "https://contracts.example.com",
|
"appExternalExample": "https://contracts.example.com",
|
||||||
|
|||||||
@@ -203,12 +203,15 @@
|
|||||||
"webhookSetupRunning": "Configuring Paperless…",
|
"webhookSetupRunning": "Configuring Paperless…",
|
||||||
"webhookSetupSuccess": "Paperless workflow configured (ID {{id}})",
|
"webhookSetupSuccess": "Paperless workflow configured (ID {{id}})",
|
||||||
"webhookSetupError": "Unable to configure Paperless workflow",
|
"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.",
|
"webhookWorkflowInfo": "Paperless workflow ID {{id}} is linked.",
|
||||||
"ical": "iCal subscription",
|
"ical": "iCal subscription",
|
||||||
"icalFeedUrl": "Feed URL",
|
"icalFeedUrl": "Feed URL",
|
||||||
"paperlessApiUrl": "Paperless API URL",
|
"paperlessApiUrl": "Paperless API URL",
|
||||||
"paperlessExternalUrl": "Paperless external URL (for direct link)",
|
"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",
|
"appExternalUrl": "App external URL",
|
||||||
"paperlessExample": "https://paperless.example.com",
|
"paperlessExample": "https://paperless.example.com",
|
||||||
"appExternalExample": "https://contracts.example.com",
|
"appExternalExample": "https://contracts.example.com",
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ interface HealthResponse {
|
|||||||
type FormValues = {
|
type FormValues = {
|
||||||
paperlessBaseUrl: string;
|
paperlessBaseUrl: string;
|
||||||
paperlessExternalUrl: string;
|
paperlessExternalUrl: string;
|
||||||
|
paperlessWebhookUrl: string;
|
||||||
appExternalUrl: string;
|
appExternalUrl: string;
|
||||||
appLocale: string;
|
appLocale: string;
|
||||||
paperlessToken: string;
|
paperlessToken: string;
|
||||||
@@ -91,6 +92,7 @@ type FormValues = {
|
|||||||
const defaultValues: FormValues = {
|
const defaultValues: FormValues = {
|
||||||
paperlessBaseUrl: "",
|
paperlessBaseUrl: "",
|
||||||
paperlessExternalUrl: "",
|
paperlessExternalUrl: "",
|
||||||
|
paperlessWebhookUrl: "",
|
||||||
appExternalUrl: "",
|
appExternalUrl: "",
|
||||||
appLocale: "de",
|
appLocale: "de",
|
||||||
paperlessToken: "",
|
paperlessToken: "",
|
||||||
@@ -187,6 +189,7 @@ export default function SettingsPage() {
|
|||||||
const values: FormValues = {
|
const values: FormValues = {
|
||||||
paperlessBaseUrl: settingsData.values.paperlessBaseUrl ?? "",
|
paperlessBaseUrl: settingsData.values.paperlessBaseUrl ?? "",
|
||||||
paperlessExternalUrl: settingsData.values.paperlessExternalUrl ?? "",
|
paperlessExternalUrl: settingsData.values.paperlessExternalUrl ?? "",
|
||||||
|
paperlessWebhookUrl: settingsData.values.paperlessWebhookUrl ?? "",
|
||||||
appExternalUrl: settingsData.values.appExternalUrl ?? "",
|
appExternalUrl: settingsData.values.appExternalUrl ?? "",
|
||||||
appLocale: settingsData.values.appLocale ?? "de",
|
appLocale: settingsData.values.appLocale ?? "de",
|
||||||
paperlessToken: "",
|
paperlessToken: "",
|
||||||
@@ -354,6 +357,9 @@ export default function SettingsPage() {
|
|||||||
if (formValues.paperlessExternalUrl !== initial.paperlessExternalUrl) {
|
if (formValues.paperlessExternalUrl !== initial.paperlessExternalUrl) {
|
||||||
payload.paperlessExternalUrl = trimOrNull(formValues.paperlessExternalUrl);
|
payload.paperlessExternalUrl = trimOrNull(formValues.paperlessExternalUrl);
|
||||||
}
|
}
|
||||||
|
if (formValues.paperlessWebhookUrl !== initial.paperlessWebhookUrl) {
|
||||||
|
payload.paperlessWebhookUrl = trimOrNull(formValues.paperlessWebhookUrl);
|
||||||
|
}
|
||||||
if (formValues.appExternalUrl !== initial.appExternalUrl) {
|
if (formValues.appExternalUrl !== initial.appExternalUrl) {
|
||||||
payload.appExternalUrl = trimOrNull(formValues.appExternalUrl);
|
payload.appExternalUrl = trimOrNull(formValues.appExternalUrl);
|
||||||
}
|
}
|
||||||
@@ -707,6 +713,13 @@ export default function SettingsPage() {
|
|||||||
placeholder={t("settings.paperlessExample")}
|
placeholder={t("settings.paperlessExample")}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
|
<TextField
|
||||||
|
label={t("settings.paperlessWebhookUrl")}
|
||||||
|
{...register("paperlessWebhookUrl")}
|
||||||
|
placeholder={t("settings.paperlessWebhookUrlExample")}
|
||||||
|
fullWidth
|
||||||
|
helperText={t("settings.paperlessWebhookUrlHelp")}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
label={t("settings.appExternalUrl")}
|
label={t("settings.appExternalUrl")}
|
||||||
{...register("appExternalUrl")}
|
{...register("appExternalUrl")}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const configSchema = z.object({
|
|||||||
paperlessBaseUrl: z.string().url().optional(),
|
paperlessBaseUrl: z.string().url().optional(),
|
||||||
paperlessToken: z.string().min(1).optional(),
|
paperlessToken: z.string().min(1).optional(),
|
||||||
paperlessExternalUrl: z.string().url().optional(),
|
paperlessExternalUrl: z.string().url().optional(),
|
||||||
|
paperlessWebhookUrl: z.string().url().optional(),
|
||||||
appExternalUrl: z.string().url().optional(),
|
appExternalUrl: z.string().url().optional(),
|
||||||
appLocale: z.enum(supportedLocales).default("de"),
|
appLocale: z.enum(supportedLocales).default("de"),
|
||||||
schedulerIntervalMinutes: z.coerce.number().min(5).default(60),
|
schedulerIntervalMinutes: z.coerce.number().min(5).default(60),
|
||||||
@@ -62,6 +63,7 @@ const rawConfig = {
|
|||||||
paperlessBaseUrl: readEnv("PAPERLESS_BASE_URL"),
|
paperlessBaseUrl: readEnv("PAPERLESS_BASE_URL"),
|
||||||
paperlessToken: readEnv("PAPERLESS_TOKEN"),
|
paperlessToken: readEnv("PAPERLESS_TOKEN"),
|
||||||
paperlessExternalUrl: readEnv("PAPERLESS_EXTERNAL_URL"),
|
paperlessExternalUrl: readEnv("PAPERLESS_EXTERNAL_URL"),
|
||||||
|
paperlessWebhookUrl: readEnv("PAPERLESS_WEBHOOK_URL"),
|
||||||
appExternalUrl: readEnv("APP_EXTERNAL_URL"),
|
appExternalUrl: readEnv("APP_EXTERNAL_URL"),
|
||||||
appLocale: readEnv("APP_LOCALE"),
|
appLocale: readEnv("APP_LOCALE"),
|
||||||
schedulerIntervalMinutes: readEnv("SCHEDULER_INTERVAL_MINUTES"),
|
schedulerIntervalMinutes: readEnv("SCHEDULER_INTERVAL_MINUTES"),
|
||||||
|
|||||||
11
src/index.ts
11
src/index.ts
@@ -90,6 +90,7 @@ const loginSchema = z.object({
|
|||||||
const settingsUpdateSchema = z.object({
|
const settingsUpdateSchema = z.object({
|
||||||
paperlessBaseUrl: z.string().url().nullable().optional(),
|
paperlessBaseUrl: z.string().url().nullable().optional(),
|
||||||
paperlessExternalUrl: z.string().url().nullable().optional(),
|
paperlessExternalUrl: z.string().url().nullable().optional(),
|
||||||
|
paperlessWebhookUrl: z.string().url().nullable().optional(),
|
||||||
appExternalUrl: z.string().url().nullable().optional(),
|
appExternalUrl: z.string().url().nullable().optional(),
|
||||||
appLocale: z.enum(["de", "en"]).optional(),
|
appLocale: z.enum(["de", "en"]).optional(),
|
||||||
paperlessToken: z.string().min(1).nullable().optional(),
|
paperlessToken: z.string().min(1).nullable().optional(),
|
||||||
@@ -129,6 +130,7 @@ function formatSettingsResponse(runtime: RuntimeSettings) {
|
|||||||
values: {
|
values: {
|
||||||
paperlessBaseUrl: runtime.paperlessBaseUrl,
|
paperlessBaseUrl: runtime.paperlessBaseUrl,
|
||||||
paperlessExternalUrl: runtime.paperlessExternalUrl,
|
paperlessExternalUrl: runtime.paperlessExternalUrl,
|
||||||
|
paperlessWebhookUrl: runtime.paperlessWebhookUrl,
|
||||||
appExternalUrl: runtime.appExternalUrl,
|
appExternalUrl: runtime.appExternalUrl,
|
||||||
appLocale: runtime.appLocale,
|
appLocale: runtime.appLocale,
|
||||||
schedulerIntervalMinutes: runtime.schedulerIntervalMinutes,
|
schedulerIntervalMinutes: runtime.schedulerIntervalMinutes,
|
||||||
@@ -393,6 +395,7 @@ app.get("/config", (_req, res) => {
|
|||||||
databasePath: config.databasePath,
|
databasePath: config.databasePath,
|
||||||
paperlessBaseUrl: runtime.paperlessBaseUrl,
|
paperlessBaseUrl: runtime.paperlessBaseUrl,
|
||||||
paperlessExternalUrl: runtime.paperlessExternalUrl,
|
paperlessExternalUrl: runtime.paperlessExternalUrl,
|
||||||
|
paperlessWebhookUrl: runtime.paperlessWebhookUrl,
|
||||||
appExternalUrl: runtime.appExternalUrl,
|
appExternalUrl: runtime.appExternalUrl,
|
||||||
appLocale: runtime.appLocale,
|
appLocale: runtime.appLocale,
|
||||||
paperlessConfigured,
|
paperlessConfigured,
|
||||||
@@ -431,6 +434,9 @@ app.put("/settings", (req, res) => {
|
|||||||
if (Object.prototype.hasOwnProperty.call(payload, "paperlessExternalUrl")) {
|
if (Object.prototype.hasOwnProperty.call(payload, "paperlessExternalUrl")) {
|
||||||
update.paperlessExternalUrl = payload.paperlessExternalUrl ?? null;
|
update.paperlessExternalUrl = payload.paperlessExternalUrl ?? null;
|
||||||
}
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, "paperlessWebhookUrl")) {
|
||||||
|
update.paperlessWebhookUrl = payload.paperlessWebhookUrl ?? null;
|
||||||
|
}
|
||||||
if (Object.prototype.hasOwnProperty.call(payload, "appExternalUrl")) {
|
if (Object.prototype.hasOwnProperty.call(payload, "appExternalUrl")) {
|
||||||
update.appExternalUrl = payload.appExternalUrl ?? null;
|
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 secret = runtime.paperlessWebhookSecret ?? generateSecret(32);
|
||||||
const appUrl = resolveAppBaseUrl(req, runtime);
|
const webhookUrl = runtime.paperlessWebhookUrl
|
||||||
const webhookUrl = `${appUrl}/api/integrations/paperless/webhook`;
|
? runtime.paperlessWebhookUrl.replace(/\/$/, "")
|
||||||
|
: `${resolveAppBaseUrl(req, runtime)}/api/integrations/paperless/webhook`;
|
||||||
const workflow = await paperlessClient.upsertContractCompanionWorkflow(webhookUrl, secret);
|
const workflow = await paperlessClient.upsertContractCompanionWorkflow(webhookUrl, secret);
|
||||||
const workflowId = typeof workflow.id === "number" ? workflow.id : null;
|
const workflowId = typeof workflow.id === "number" ? workflow.id : null;
|
||||||
const updated = updateRuntimeSettings({
|
const updated = updateRuntimeSettings({
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
export interface RuntimeSettings {
|
export interface RuntimeSettings {
|
||||||
paperlessBaseUrl: string | null;
|
paperlessBaseUrl: string | null;
|
||||||
paperlessExternalUrl: string | null;
|
paperlessExternalUrl: string | null;
|
||||||
|
paperlessWebhookUrl: string | null;
|
||||||
appExternalUrl: string | null;
|
appExternalUrl: string | null;
|
||||||
appLocale: string;
|
appLocale: string;
|
||||||
paperlessToken: string | null;
|
paperlessToken: string | null;
|
||||||
@@ -123,6 +124,7 @@ export function getRuntimeSettings(): RuntimeSettings {
|
|||||||
return {
|
return {
|
||||||
paperlessBaseUrl: coerceString(stored.paperlessBaseUrl, config.paperlessBaseUrl ?? null),
|
paperlessBaseUrl: coerceString(stored.paperlessBaseUrl, config.paperlessBaseUrl ?? null),
|
||||||
paperlessExternalUrl: coerceString(stored.paperlessExternalUrl, config.paperlessExternalUrl ?? null),
|
paperlessExternalUrl: coerceString(stored.paperlessExternalUrl, config.paperlessExternalUrl ?? null),
|
||||||
|
paperlessWebhookUrl: coerceString(stored.paperlessWebhookUrl, config.paperlessWebhookUrl ?? null),
|
||||||
appExternalUrl: coerceString(stored.appExternalUrl, config.appExternalUrl ?? null),
|
appExternalUrl: coerceString(stored.appExternalUrl, config.appExternalUrl ?? null),
|
||||||
appLocale: normalizeLocale(stored.appLocale, config.appLocale),
|
appLocale: normalizeLocale(stored.appLocale, config.appLocale),
|
||||||
paperlessToken: coerceString(stored.paperlessToken, config.paperlessToken ?? null),
|
paperlessToken: coerceString(stored.paperlessToken, config.paperlessToken ?? null),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const listStmt = db.prepare("SELECT key, value FROM settings");
|
|||||||
export type SettingKey =
|
export type SettingKey =
|
||||||
| "paperlessBaseUrl"
|
| "paperlessBaseUrl"
|
||||||
| "paperlessExternalUrl"
|
| "paperlessExternalUrl"
|
||||||
|
| "paperlessWebhookUrl"
|
||||||
| "appExternalUrl"
|
| "appExternalUrl"
|
||||||
| "appLocale"
|
| "appLocale"
|
||||||
| "paperlessToken"
|
| "paperlessToken"
|
||||||
|
|||||||
Reference in New Issue
Block a user