feat: Datenmodell und Umgebung fuer Integrationen

Ein Zustand je Quelle haelt fest, wie weit gelesen wurde und ob es
klappt. Der Merker ist bewusst ein freier Text: Zammad braucht darin
einen Zeitstempel, Telegram den Zaehler der zuletzt geholten Nachricht.

Dazu die gespiegelte Belegung ohne Titel, das Adressbuch der erlaubten
Telegram-Chats und der Einmalcode, mit dem sich ein Chat anmeldet.

Welche Integration eine Instanz darf, entscheidet weiterhin INSTANZ. Ein
Zammad-Token in der Instanz daheim bleibt wirkungslos — das steht in den
Faehigkeiten, nicht in einer Einstellung, die sich verstellen liesse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-11 23:03:32 +02:00
co-authored by Claude Opus 5
parent ab1b898e44
commit 7f8f505126
5 changed files with 233 additions and 4 deletions
+39 -1
View File
@@ -1,6 +1,6 @@
import 'server-only'
import { z } from 'zod'
import { INSTANZEN, type Instanz } from '@/lib/instanz'
import { INSTANZEN, istErlaubt, type Instanz } from '@/lib/instanz'
/**
* Jede Einstellung von aussen wird geprueft, bevor sie benutzt wird. Fehlt
@@ -57,6 +57,26 @@ const schema = z.object({
/// Implizites TLS (Port 465). Sonst wird STARTTLS versucht.
SMTP_TLS: jaNein.default(false),
// --- Integrationen (M6) ---
// Zammad, nur in der Instanz "firma". Der Token gehoert einem Benutzer und
// sieht genau das, was dieser Benutzer sehen darf.
ZAMMAD_URL: z.string().trim().default(''),
ZAMMAD_TOKEN: z.string().trim().default(''),
/// Suchausdruck in Zammads Syntax. Was hier passt, wird zur Aufgabe.
ZAMMAD_ABFRAGE: z
.string()
.trim()
.default('owner_id:me AND (state.name:new OR state.name:open)'),
// Telegram, nur in der Instanz "daheim".
TELEGRAM_TOKEN: z.string().trim().default(''),
// Spiegel: firma sendet, daheim empfaengt. Dasselbe Geheimnis auf beiden
// Seiten; ohne gueltige Signatur nimmt der Empfaenger nichts an.
SPIEGEL_GEHEIMNIS: z.string().trim().default(''),
/// Nur auf der sendenden Seite: wohin geschickt wird.
SPIEGEL_ZIEL: z.string().trim().default(''),
DEMO_MODUS: jaNein.default(false),
})
@@ -70,6 +90,12 @@ export type Umgebung = z.infer<typeof schema> & {
/** Web-Push ist erst mit vollstaendigem VAPID-Satz moeglich. */
pushAktiv: boolean
mailAktiv: boolean
/** Zammad braucht Adresse und Token – und die Instanz muss es erlauben. */
zammadAktiv: boolean
telegramAktiv: boolean
/** Senden geht nur mit Ziel, Empfangen schon mit dem Geheimnis allein. */
spiegelSendenAktiv: boolean
spiegelEmpfangenAktiv: boolean
}
let zwischengespeichert: Umgebung | null = null
@@ -95,6 +121,18 @@ function lesen(): Umgebung {
werte.VAPID_OEFFENTLICH && werte.VAPID_PRIVAT && werte.VAPID_KONTAKT,
),
mailAktiv: Boolean(werte.SMTP_HOST && werte.SMTP_ABSENDER),
// Die Instanz entscheidet mit: eine Integration, die hier nicht erlaubt
// ist, laesst sich auch mit vollstaendiger Konfiguration nicht anschalten.
zammadAktiv:
istErlaubt(werte.INSTANZ, 'integration:zammad') &&
Boolean(werte.ZAMMAD_URL && werte.ZAMMAD_TOKEN),
telegramAktiv:
istErlaubt(werte.INSTANZ, 'integration:telegram') && Boolean(werte.TELEGRAM_TOKEN),
spiegelSendenAktiv:
istErlaubt(werte.INSTANZ, 'spiegel:senden') &&
Boolean(werte.SPIEGEL_GEHEIMNIS && werte.SPIEGEL_ZIEL),
spiegelEmpfangenAktiv:
istErlaubt(werte.INSTANZ, 'spiegel:empfangen') && Boolean(werte.SPIEGEL_GEHEIMNIS),
}
}