- 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
57 lines
1.8 KiB
TypeScript
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 });
|
|
}
|