feat(benachrichtigung): E-Mail und Webhook bei Drift und Ablauf (#028)

Drift, ablaufende und abgelaufene Schluessel, unerreichbare Hosts und
geaenderte Host-Keys sollen auffallen, ohne dass jemand taeglich in die
Oberflaeche schaut. Versand per SMTP (Microsoft 365, STARTTLS) und per
Webhook mit HMAC-Signatur, eingestellt unter /einstellungen.

Die Anlaesse liest der Worker aus der Datenbank, statt sie im Abgleich
auszuloesen: der Abgleich-Ablauf bleibt unberuehrt, und ein verpasster
Lauf holt sich der naechste. Ein Versandprotokoll (Zustellung) mit
Anlass-Fingerabdruck verhindert Doppelmeldungen und haelt Fehlschlaege
fuer begrenzte Wiederholungen fest. Taeglich eine Zusammenfassung,
geaenderte Host-Keys im Fuenf-Minuten-Takt sofort.

Versand und Testnachricht laufen im Worker, damit der Webprozess keinen
Weg zu Mailserver oder Webhook-Ziel braucht. Passwort und Geheimnis
liegen nur verschluesselt in der Datenbank; die Seite zeigt "gesetzt".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 16:54:43 +02:00
co-authored by Claude Opus 5.5
parent 83bd9e0922
commit 8f5b31c0cb
26 changed files with 3243 additions and 12 deletions
+37
View File
@@ -114,6 +114,8 @@ enum VorgangsArt {
ZIELKONTO_ANLEGEN
/// sudoers und – falls von sshkv angelegt – den Benutzer entfernen (#018, #024).
ZIELKONTO_ENTFERNEN
/// Testnachricht ueber einen Benachrichtigungskanal (#028).
BENACHRICHTIGUNG_TEST
}
enum VorgangsStatus {
@@ -125,6 +127,17 @@ enum VorgangsStatus {
ABGEBROCHEN
}
/// Versandweg einer Benachrichtigung (#028).
enum MeldungsKanal {
EMAIL
WEBHOOK
}
enum ZustellErgebnis {
GESENDET
FEHLGESCHLAGEN
}
enum ProtokollErgebnis {
ERFOLG
FEHLER
@@ -496,6 +509,30 @@ model Einstellung {
geaendertAm DateTime @updatedAt
}
/// Versandprotokoll der Benachrichtigungen (#028): je Anlass und Kanal hoechstens eine Zeile.
/// Vor jedem Versand wird hiergegen geprueft – so meldet sshkv dieselbe Abweichung oder dieselbe
/// Ablaufstufe nie zweimal, auch wenn ein Zeitplan doppelt feuert oder der Worker neu startet.
model Zustellung {
id String @id @default(cuid())
/// Fingerabdruck des Anlasses, z. B. drift:<abweichungId>, ablauf:<schluesselId>:7:<datum>.
anlass String
kanal MeldungsKanal
ergebnis ZustellErgebnis
/// Kurzer Fehlertext ohne Zugangsdaten.
fehler String?
versuche Int @default(1)
/// Wann ein Fehlschlag fruehestens erneut versucht wird; leer = nicht mehr.
naechsterVersuchAm DateTime?
/// Gemeinsame Kennung aller Anlaesse einer Nachricht – die Oberflaeche zeigt je Nachricht eine Zeile.
sendung String
betreff String
zeitpunkt DateTime @default(now())
@@unique([anlass, kanal])
@@index([zeitpunkt])
@@index([sendung])
}
/// Einfaches Rate-Limit fuer Anmeldung und Re-Auth (feste Fenster).
model Ratenbegrenzung {
schluessel String @id