vereinshaus/app/src/routes/api/push/senden/+server.ts
rene 77c6f513b5 Feature: SEPA-Export, Push-Notifications, Onboarding + vollständige UI
- Phosphor Icons (Icon.svelte, svg-Registry)
- Schema-Abgleich: alle Felder zwischen PB-Migrations und types.ts konsistent
- Stripe entfernt, SEPA pain.008 XML-Export implementiert (sepa.ts)
- Beiträge: vollständiges CRUD + SEPA-Einzug-Sheet mit Vorschau
- Termine: vollständiges CRUD (upcoming/vergangen, datetime-local)
- Mitglieder: Formulare um alle Felder erweitert (Adresse, SEPA-Mandat, Notizen)
- Nachrichten: Brevo E-Mail via PocketBase-Hook, UI mit Gruppen-Filter
- Push-Notifications: VAPID, Custom Service Worker (injectManifest),
  Subscribe/Send API-Routen, automatische Subscription nach Login
- Onboarding: 3-Schritt-Flow für neue Vereine, Guard im App-Layout
- Makefile: .env wird vollständig zur DS übertragen
2026-05-20 13:01:11 +02:00

57 lines
1.8 KiB
TypeScript

import { json } from '@sveltejs/kit';
import { env } from '$env/dynamic/private';
import webpush from 'web-push';
const PB_URL = () => env.PB_URL ?? 'http://localhost:8090';
export async function POST({ request }) {
const authHeader = request.headers.get('Authorization') ?? '';
const { titel, body, url = '/nachrichten' } = await request.json();
if (!titel) return json({ error: 'Titel fehlt.' }, { status: 400 });
const vapidPublic = env.PUBLIC_VAPID_KEY ?? '';
const vapidPrivate = env.VAPID_PRIVATE_KEY ?? '';
const vapidSubject = env.VAPID_SUBJECT ?? 'mailto:info@vereins.haus';
if (!vapidPublic || !vapidPrivate) {
return json({ error: 'VAPID-Keys nicht konfiguriert.' }, { status: 500 });
}
webpush.setVapidDetails(vapidSubject, vapidPublic, vapidPrivate);
// Alle Push-Subscriptions des Vereins laden (listRule erlaubt verein-weite Abfrage)
const subRes = await fetch(
`${PB_URL()}/api/collections/push_subscriptions/records?perPage=500`,
{ headers: { Authorization: authHeader } },
);
if (!subRes.ok) return json({ sent: 0 });
const { items } = await subRes.json();
if (!items?.length) return json({ sent: 0 });
const payload = JSON.stringify({ title: titel, body, url });
let sent = 0;
await Promise.allSettled(
items.map(async (sub: { endpoint: string; p256dh: string; auth: string; id: string }) => {
try {
await webpush.sendNotification(
{ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
payload,
);
sent++;
} catch (err: unknown) {
// 410 Gone = Subscription abgelaufen → löschen
if ((err as { statusCode?: number }).statusCode === 410) {
await fetch(
`${PB_URL()}/api/collections/push_subscriptions/records/${sub.id}`,
{ method: 'DELETE', headers: { Authorization: authHeader } },
).catch(() => {});
}
}
}),
);
return json({ sent });
}