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>
103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
import { alsAnsicht, type BenachrichtigungAnsicht } from "@/lib/benachrichtigung/einstellungen";
|
|
import { cronBeschreiben, webhookZielKurz } from "@/lib/benachrichtigung/regeln";
|
|
import { prisma } from "@/lib/db";
|
|
import { VERSION } from "@/lib/version";
|
|
import { oidcAktiv, oidcName } from "@/server/auth/oidc";
|
|
import { benachrichtigungLesen } from "@/server/services/benachrichtigung";
|
|
|
|
/**
|
|
* Lesemodell der Einstellungsseite (#028). Lesen duerfen alle Rollen; Geheimnisse verlassen
|
|
* diese Datei nur als „gesetzt ja/nein“ (`alsAnsicht`).
|
|
*/
|
|
|
|
export type ZustellungZeile = {
|
|
readonly sendung: string;
|
|
readonly zeitpunkt: Date;
|
|
readonly kanal: "EMAIL" | "WEBHOOK";
|
|
readonly betreff: string;
|
|
readonly anlaesse: number;
|
|
readonly ergebnis: "GESENDET" | "FEHLGESCHLAGEN";
|
|
readonly versuche: number;
|
|
readonly fehler: string | null;
|
|
readonly naechsterVersuchAm: Date | null;
|
|
};
|
|
|
|
export type Zeitplan = { readonly ausdruck: string; readonly beschreibung: string | null };
|
|
|
|
export type EinstellungenSeite = {
|
|
readonly benachrichtigung: BenachrichtigungAnsicht;
|
|
readonly webhookZiel: string | null;
|
|
readonly zustellungen: readonly ZustellungZeile[];
|
|
readonly abgleich: Zeitplan;
|
|
readonly meldung: Zeitplan;
|
|
readonly instanz: {
|
|
readonly version: string;
|
|
readonly deploymentKey: { readonly name: string; readonly fingerprint: string } | null;
|
|
readonly oidc: { readonly name: string; readonly konfiguriert: boolean };
|
|
readonly notfall: { readonly vorhanden: boolean; readonly totpEingerichtet: boolean };
|
|
};
|
|
};
|
|
|
|
function zeitplan(variable: string, standard: string): Zeitplan {
|
|
const ausdruck = process.env[variable]?.trim() || standard;
|
|
return { ausdruck, beschreibung: cronBeschreiben(ausdruck) };
|
|
}
|
|
|
|
/** Je Nachricht eine Zeile: alle Anlaesse einer Sendung teilen Kennung, Betreff und Ergebnis. */
|
|
export async function letzteZustellungen(anzahl = 12): Promise<ZustellungZeile[]> {
|
|
const zeilen = await prisma.zustellung.findMany({
|
|
orderBy: { zeitpunkt: "desc" },
|
|
take: 500,
|
|
select: {
|
|
sendung: true,
|
|
zeitpunkt: true,
|
|
kanal: true,
|
|
betreff: true,
|
|
ergebnis: true,
|
|
versuche: true,
|
|
fehler: true,
|
|
naechsterVersuchAm: true,
|
|
},
|
|
});
|
|
const gruppen = new Map<string, ZustellungZeile>();
|
|
for (const z of zeilen) {
|
|
const vorhanden = gruppen.get(z.sendung);
|
|
if (vorhanden) {
|
|
gruppen.set(z.sendung, {
|
|
...vorhanden,
|
|
anlaesse: vorhanden.anlaesse + 1,
|
|
versuche: Math.max(vorhanden.versuche, z.versuche),
|
|
});
|
|
} else {
|
|
if (gruppen.size >= anzahl) continue;
|
|
gruppen.set(z.sendung, { ...z, anlaesse: 1 });
|
|
}
|
|
}
|
|
return [...gruppen.values()];
|
|
}
|
|
|
|
export async function einstellungenSeite(): Promise<EinstellungenSeite> {
|
|
const [gespeichert, zustellungen, deploymentKey, notfall] = await Promise.all([
|
|
benachrichtigungLesen(),
|
|
letzteZustellungen(),
|
|
prisma.schluessel.findFirst({
|
|
where: { instanzStandard: true },
|
|
select: { name: true, fingerprint: true },
|
|
}),
|
|
prisma.benutzer.findFirst({ where: { istNotfall: true }, select: { totpEingerichtetAm: true } }),
|
|
]);
|
|
return {
|
|
benachrichtigung: alsAnsicht(gespeichert),
|
|
webhookZiel: gespeichert.webhook.url ? webhookZielKurz(gespeichert.webhook.url) : null,
|
|
zustellungen,
|
|
abgleich: zeitplan("SSHKV_SYNC_CRON", "30 2 * * *"),
|
|
meldung: zeitplan("SSHKV_MELDUNG_CRON", "0 7 * * *"),
|
|
instanz: {
|
|
version: VERSION,
|
|
deploymentKey,
|
|
oidc: { name: oidcName(), konfiguriert: oidcAktiv() },
|
|
notfall: { vorhanden: notfall !== null, totpEingerichtet: Boolean(notfall?.totpEingerichtetAm) },
|
|
},
|
|
};
|
|
}
|