feat(notify): Benachrichtigungen ueber ntfy, Webhook und E-Mail
Drei Anlaesse: fehlgeschlagener Abgleich, rote CI, neues Release. Die Daten dafuer liegen seit dem CI-Status vor, es fehlte nur der Weg nach draussen. Der Kern ist die Entprellung. Gemeldet wird ausschliesslich bei einer Aenderung gegenueber dem zuletzt gemeldeten Stand, der am Projekt vermerkt ist. Ein Repo mit abgelaufenem Token meldet sich damit einmal statt bei jedem stuendlichen Durchlauf. Zwei Feinheiten: - Beim allerersten Abgleich wird kein Release gemeldet, sonst waere jede bereits vorhandene Version eine "Neuigkeit". - Der Merkstand wird auch ohne eingerichtetes Ziel fortgeschrieben, sonst kaeme beim ersten Ziel die ganze Vergangenheit hinterher. Zustellwege: - ntfy mit Titel, Prioritaet und Symbol als Kopfzeilen, optionalem Bearer-Token. Emojis werden aus den Kopfzeilen entfernt statt durch Fragezeichen ersetzt - Kopfzeilen duerfen nur Latin-1 tragen, und das Sinnbild liefert ohnehin der Tags-Header. - Webhook mit JSON, Projektbezug und Tieflink. Mit Geheimnis wird der Rumpf per HMAC-SHA256 signiert, damit der Empfaenger die Herkunft pruefen kann. - E-Mail ueber SMTP, verfuegbar sobald SMTP_HOST gesetzt ist. Zieladressen sind Benutzereingaben, die der Server abruft - sie laufen deshalb durch denselben SSRF-Schutz wie die Git-Verbindungen. Redirects werden nicht verfolgt, weil ein Umleitungsziel nicht geprueft waere. Fehler beim Zustellen werden geschluckt: eine kaputte Benachrichtigung darf den Abgleich nicht scheitern lassen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -170,6 +170,40 @@ export const adminUserUpdateSchema = z.object({
|
||||
password: z.preprocess(emptyToNull, z.string().max(200).nullable().optional()),
|
||||
});
|
||||
|
||||
// ── Benachrichtigungen ───────────────────────────────────────
|
||||
|
||||
const CHANNELS = ["WEBHOOK", "NTFY", "EMAIL"] as const;
|
||||
const EVENTS = ["SYNC_FAILED", "CI_FAILED", "NEW_RELEASE"] as const;
|
||||
|
||||
export const notificationTargetCreateSchema = z
|
||||
.object({
|
||||
name: z.string().trim().min(1, "Name darf nicht leer sein").max(60),
|
||||
channel: z.enum(CHANNELS),
|
||||
target: z.string().trim().min(1, "Ziel fehlt").max(500),
|
||||
/** Bearer-Token fuer ntfy oder Signaturschluessel fuer den Webhook. */
|
||||
secret: z.preprocess(emptyToNull, z.string().trim().max(300).nullable().optional()),
|
||||
events: z.array(z.enum(EVENTS)).min(1, "Mindestens ein Anlass"),
|
||||
isActive: z.boolean().default(true),
|
||||
})
|
||||
.refine(
|
||||
(value) =>
|
||||
value.channel === "EMAIL"
|
||||
? /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value.target)
|
||||
: /^https?:\/\//i.test(value.target),
|
||||
{
|
||||
message: "E-Mail-Adresse bzw. URL mit http:// oder https:// erwartet",
|
||||
path: ["target"],
|
||||
},
|
||||
);
|
||||
|
||||
export const notificationTargetUpdateSchema = z.object({
|
||||
name: z.string().trim().min(1).max(60).optional(),
|
||||
target: z.string().trim().min(1).max(500).optional(),
|
||||
secret: z.preprocess(emptyToNull, z.string().trim().max(300).nullable().optional()),
|
||||
events: z.array(z.enum(EVENTS)).min(1).optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const passkeyNameSchema = z.object({
|
||||
name: z.preprocess(emptyToNull, z.string().trim().max(60).nullable().optional()),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user