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:
co-authored by
Claude Opus 5
parent
ab1b898e44
commit
7f8f505126
@@ -97,6 +97,29 @@ SMTP_ABSENDER=
|
|||||||
# true bei Port 465 (SMTPS), false bei 587 mit STARTTLS.
|
# true bei Port 465 (SMTPS), false bei 587 mit STARTTLS.
|
||||||
SMTP_TLS=false
|
SMTP_TLS=false
|
||||||
|
|
||||||
|
# --- Integrationen ---------------------------------------------------------
|
||||||
|
# Alles freiwillig. Was fehlt, erscheint unter "Integrationen" als nicht
|
||||||
|
# eingerichtet; der Rest laeuft weiter.
|
||||||
|
# Welche Integration eine Instanz ueberhaupt darf, entscheidet INSTANZ:
|
||||||
|
# Zammad und Wartungsplaner nur "firma", Telegram nur "daheim".
|
||||||
|
|
||||||
|
# Zammad (nur firma). Token in Zammad unter Profil > Token-Zugriff anlegen;
|
||||||
|
# er sieht genau das, was der zugehoerige Benutzer sehen darf.
|
||||||
|
ZAMMAD_URL=
|
||||||
|
ZAMMAD_TOKEN=
|
||||||
|
# Suchausdruck in Zammads Syntax. Was darauf passt, wird zur Aufgabe.
|
||||||
|
ZAMMAD_ABFRAGE=owner_id:me AND (state.name:new OR state.name:open)
|
||||||
|
|
||||||
|
# Telegram (nur daheim). Bot bei @BotFather anlegen, Token hier eintragen.
|
||||||
|
# Welche Chats mitreden duerfen, wird in der Anwendung gekoppelt.
|
||||||
|
TELEGRAM_TOKEN=
|
||||||
|
|
||||||
|
# Spiegel: firma sendet, daheim empfaengt. Dasselbe Geheimnis auf beiden
|
||||||
|
# Seiten, mindestens 32 Zeichen: openssl rand -base64 32
|
||||||
|
SPIEGEL_GEHEIMNIS=
|
||||||
|
# Nur auf der sendenden Seite (firma): die Adresse der anderen Instanz.
|
||||||
|
SPIEGEL_ZIEL=
|
||||||
|
|
||||||
# --- Sonstiges -------------------------------------------------------------
|
# --- Sonstiges -------------------------------------------------------------
|
||||||
# Zeitzone fuer Anzeigen und Tagesgrenzen.
|
# Zeitzone fuer Anzeigen und Tagesgrenzen.
|
||||||
TZ=Europe/Berlin
|
TZ=Europe/Berlin
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "aufgabe" ADD COLUMN "spiegeln" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "benachrichtigungseinstellung" ADD COLUMN "perTelegram" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "integrationszustand" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"quelle" "Quelle" NOT NULL,
|
||||||
|
"merker" TEXT,
|
||||||
|
"letzterLauf" TIMESTAMP(3),
|
||||||
|
"letzterErfolg" TIMESTAMP(3),
|
||||||
|
"letzterFehler" TEXT,
|
||||||
|
"fehlversuche" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"aktualisiertAm" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "integrationszustand_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "spiegelbelegung" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"quelleRef" TEXT NOT NULL,
|
||||||
|
"beginn" TIMESTAMP(3) NOT NULL,
|
||||||
|
"ende" TIMESTAMP(3) NOT NULL,
|
||||||
|
"empfangenAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "spiegelbelegung_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "telegram_chat" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"chatId" BIGINT NOT NULL,
|
||||||
|
"bezeichnung" TEXT,
|
||||||
|
"benutzerId" TEXT,
|
||||||
|
"meldungen" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"zuletztBenutzt" TIMESTAMP(3),
|
||||||
|
|
||||||
|
CONSTRAINT "telegram_chat_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "integrationszustand_quelle_key" ON "integrationszustand"("quelle");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "spiegelbelegung_quelleRef_key" ON "spiegelbelegung"("quelleRef");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "spiegelbelegung_beginn_idx" ON "spiegelbelegung"("beginn");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "telegram_chat_chatId_key" ON "telegram_chat"("chatId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "telegram_chat_benutzerId_idx" ON "telegram_chat"("benutzerId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "telegram_chat" ADD CONSTRAINT "telegram_chat_benutzerId_fkey" FOREIGN KEY ("benutzerId") REFERENCES "benutzer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "telegram_kopplung" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"code" TEXT NOT NULL,
|
||||||
|
"benutzerId" TEXT NOT NULL,
|
||||||
|
"gueltigBis" TIMESTAMP(3) NOT NULL,
|
||||||
|
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "telegram_kopplung_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "telegram_kopplung_code_key" ON "telegram_kopplung"("code");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "telegram_kopplung_gueltigBis_idx" ON "telegram_kopplung"("gueltigBis");
|
||||||
+93
-3
@@ -38,6 +38,7 @@ model Benutzer {
|
|||||||
benachrichtigung Benachrichtigungseinstellung?
|
benachrichtigung Benachrichtigungseinstellung?
|
||||||
pushGeraete PushGeraet[]
|
pushGeraete PushGeraet[]
|
||||||
meldungen Meldungsnachweis[]
|
meldungen Meldungsnachweis[]
|
||||||
|
telegramChats TelegramChat[]
|
||||||
|
|
||||||
@@index([aktiv])
|
@@index([aktiv])
|
||||||
@@map("benutzer")
|
@@map("benutzer")
|
||||||
@@ -222,6 +223,10 @@ model Aufgabe {
|
|||||||
/// Kennung beim Ursprungssystem, z. B. die Ticketnummer.
|
/// Kennung beim Ursprungssystem, z. B. die Ticketnummer.
|
||||||
quelleRef String?
|
quelleRef String?
|
||||||
|
|
||||||
|
/// Ausdruecklich zum Spiegeln freigegeben. Nur was hier true ist, verlaesst
|
||||||
|
/// die Instanz mit Titel – alles andere bleibt, wo es ist.
|
||||||
|
spiegeln Boolean @default(false)
|
||||||
|
|
||||||
erstelltAm DateTime @default(now())
|
erstelltAm DateTime @default(now())
|
||||||
aktualisiertAm DateTime @updatedAt
|
aktualisiertAm DateTime @updatedAt
|
||||||
|
|
||||||
@@ -391,9 +396,10 @@ model Benachrichtigungseinstellung {
|
|||||||
benutzerId String @unique
|
benutzerId String @unique
|
||||||
|
|
||||||
/// Kommagetrennte Anlaesse, siehe src/lib/benachrichtigung.ts.
|
/// Kommagetrennte Anlaesse, siehe src/lib/benachrichtigung.ts.
|
||||||
anlaesse String @default("tagesueberblick,faelligkeit")
|
anlaesse String @default("tagesueberblick,faelligkeit")
|
||||||
perPush Boolean @default(false)
|
perPush Boolean @default(false)
|
||||||
perMail Boolean @default(false)
|
perMail Boolean @default(false)
|
||||||
|
perTelegram Boolean @default(false)
|
||||||
|
|
||||||
/// Alles als Minuten seit Mitternacht, Europe/Berlin.
|
/// Alles als Minuten seit Mitternacht, Europe/Berlin.
|
||||||
ueberblickUm Int @default(420)
|
ueberblickUm Int @default(420)
|
||||||
@@ -448,3 +454,87 @@ model Meldungsnachweis {
|
|||||||
@@index([gesendetAm])
|
@@index([gesendetAm])
|
||||||
@@map("meldungsnachweis")
|
@@map("meldungsnachweis")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Zustand einer Integration: wie weit sie gelesen hat und ob es klappt.
|
||||||
|
///
|
||||||
|
/// Eine Zeile je Quelle. Der Merker ist bewusst ein freier Text – jede
|
||||||
|
/// Integration legt darin ab, was sie zum Fortsetzen braucht: Zammad einen
|
||||||
|
/// Zeitstempel, Telegram den Zaehler der zuletzt geholten Nachricht.
|
||||||
|
model Integrationszustand {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
quelle Quelle @unique
|
||||||
|
|
||||||
|
/// Woher es weitergeht. Bedeutung haengt an der Quelle.
|
||||||
|
merker String?
|
||||||
|
|
||||||
|
letzterLauf DateTime?
|
||||||
|
letzterErfolg DateTime?
|
||||||
|
letzterFehler String?
|
||||||
|
/// Fehlschlaege in Folge; bei Erfolg wieder null.
|
||||||
|
fehlversuche Int @default(0)
|
||||||
|
|
||||||
|
erstelltAm DateTime @default(now())
|
||||||
|
aktualisiertAm DateTime @updatedAt
|
||||||
|
|
||||||
|
@@map("integrationszustand")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dienstliche Belegung, die aus der anderen Instanz kommt.
|
||||||
|
///
|
||||||
|
/// Bewusst ohne Titel: Daheim soll sehen, *dass* die Zeit weg ist, nicht
|
||||||
|
/// woran gearbeitet wird. Firmeninhalte verlassen die Firma nicht.
|
||||||
|
model Spiegelbelegung {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
|
||||||
|
/// Kennung des Zeitblocks in der sendenden Instanz.
|
||||||
|
quelleRef String @unique
|
||||||
|
|
||||||
|
beginn DateTime
|
||||||
|
ende DateTime
|
||||||
|
|
||||||
|
empfangenAm DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([beginn])
|
||||||
|
@@map("spiegelbelegung")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ein Telegram-Chat, der mit dem Planer reden darf.
|
||||||
|
///
|
||||||
|
/// Wer nicht hier steht, wird verworfen – ein Bot-Token laesst sich erraten
|
||||||
|
/// oder abgreifen, die Chatkennung ist der zweite Riegel.
|
||||||
|
model TelegramChat {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
/// Chatkennung bei Telegram; kann negativ sein (Gruppen).
|
||||||
|
chatId BigInt @unique
|
||||||
|
|
||||||
|
bezeichnung String?
|
||||||
|
benutzerId String?
|
||||||
|
|
||||||
|
/// Empfaengt dieser Chat auch Meldungen, nicht nur Aufgaben?
|
||||||
|
meldungen Boolean @default(true)
|
||||||
|
|
||||||
|
erstelltAm DateTime @default(now())
|
||||||
|
zuletztBenutzt DateTime?
|
||||||
|
|
||||||
|
benutzer Benutzer? @relation(fields: [benutzerId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
@@index([benutzerId])
|
||||||
|
@@map("telegram_chat")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Einmalcode, mit dem sich ein Telegram-Chat anmeldet.
|
||||||
|
///
|
||||||
|
/// Ohne ihn muesste die Chatkennung von Hand eingetragen werden – und um sie
|
||||||
|
/// zu erfahren, muesste der Planer auf Nachrichten Unbekannter antworten.
|
||||||
|
/// Genau das soll er nicht.
|
||||||
|
model TelegramKopplung {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
code String @unique
|
||||||
|
benutzerId String
|
||||||
|
gueltigBis DateTime
|
||||||
|
|
||||||
|
erstelltAm DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([gueltigBis])
|
||||||
|
@@map("telegram_kopplung")
|
||||||
|
}
|
||||||
|
|||||||
+39
-1
@@ -1,6 +1,6 @@
|
|||||||
import 'server-only'
|
import 'server-only'
|
||||||
import { z } from 'zod'
|
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
|
* 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.
|
/// Implizites TLS (Port 465). Sonst wird STARTTLS versucht.
|
||||||
SMTP_TLS: jaNein.default(false),
|
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),
|
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. */
|
/** Web-Push ist erst mit vollstaendigem VAPID-Satz moeglich. */
|
||||||
pushAktiv: boolean
|
pushAktiv: boolean
|
||||||
mailAktiv: 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
|
let zwischengespeichert: Umgebung | null = null
|
||||||
@@ -95,6 +121,18 @@ function lesen(): Umgebung {
|
|||||||
werte.VAPID_OEFFENTLICH && werte.VAPID_PRIVAT && werte.VAPID_KONTAKT,
|
werte.VAPID_OEFFENTLICH && werte.VAPID_PRIVAT && werte.VAPID_KONTAKT,
|
||||||
),
|
),
|
||||||
mailAktiv: Boolean(werte.SMTP_HOST && werte.SMTP_ABSENDER),
|
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user