/** * Versand von Web-Push-Benachrichtigungen an einen Empfänger (alle seine * Geräte). Analog zu sendMail: wirft NIE, ein Push-Fehler darf keinen * aufrufenden Vorgang (Buchung, Storno, ...) kippen. */ import { WebPushError } from "web-push"; import { prisma } from "@/src/lib/prisma"; import { getWebPush, isPushConfigured } from "./web-push"; export interface PushPayload { title: string; body: string; /** Ziel-URL, die beim Klick auf die Benachrichtigung geöffnet wird. */ url?: string; } /** * Sendet eine Push-Benachrichtigung an alle registrierten Geräte eines * Empfängers (Manager-, Employee- oder User-ID). Tote Subscriptions (Browser * hat sie serverseitig verworfen, HTTP 404/410) werden dabei aus der DB * entfernt. */ export async function sendPushToRecipient( recipientId: string, payload: PushPayload ): Promise { if (!isPushConfigured()) return; try { const subscriptions = await prisma.pushSubscription.findMany({ where: { recipientId }, }); if (subscriptions.length === 0) return; const webpush = getWebPush(); const body = JSON.stringify(payload); const results = await Promise.allSettled( subscriptions.map((sub) => webpush.sendNotification( { endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth }, }, body ) ) ); const deadEndpoints: string[] = []; results.forEach((result, i) => { if (result.status === "rejected") { const error = result.reason; // 404/410 = Subscription existiert clientseitig nicht mehr // (Browser-Daten gelöscht, App deinstalliert, ...) - dauerhaft tot. if ( error instanceof WebPushError && (error.statusCode === 404 || error.statusCode === 410) ) { deadEndpoints.push(subscriptions[i].endpoint); } else { console.error("sendPushToRecipient: Zustellung fehlgeschlagen:", error); } } }); if (deadEndpoints.length > 0) { await prisma.pushSubscription .deleteMany({ where: { endpoint: { in: deadEndpoints } } }) .catch((error) => console.error("sendPushToRecipient: Aufräumen fehlgeschlagen:", error)); } } catch (error) { console.error("sendPushToRecipient fehlgeschlagen:", error); } }