Push ueber VAPID, Mail ueber SMTP, beide unabhaengig zuschaltbar. Die Schluessel eines Push-Geraets liegen verschluesselt in der Datenbank. Ein Geraet, das der Push-Dienst mit 404 oder 410 ablehnt, wird sofort entfernt; andere Fehler zaehlen hoch und fuehren nach fuenf Versuchen zum selben Ergebnis. Eine Liste toter Abos putzt sich so von allein. Der Nachweis wird vor dem Senden geschrieben, nicht danach: lieber eine verpasste Nachricht als eine, die im Minutentakt erneut versucht wird. Weder Endpunkt noch Schluessel noch Mailinhalt landen im Protokoll. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
121 lines
3.2 KiB
TypeScript
121 lines
3.2 KiB
TypeScript
import { NextResponse } from 'next/server'
|
||
import { z } from 'zod'
|
||
import { auditSchreiben } from '@/server/audit'
|
||
import { verlangeAnmeldungFuerApi } from '@/server/berechtigung'
|
||
import { geraetAbmelden, geraetAnmelden } from '@/server/benachrichtigung/push'
|
||
import { umgebung } from '@/server/umgebung'
|
||
|
||
export const dynamic = 'force-dynamic'
|
||
|
||
/**
|
||
* An- und Abmelden eines Browsers für Push-Nachrichten.
|
||
*
|
||
* Der öffentliche VAPID-Schlüssel kommt über GET; er ist nicht geheim, wird
|
||
* aber nur an Angemeldete herausgegeben — es gibt keinen Grund, die
|
||
* Einrichtung nach außen zu zeigen.
|
||
*/
|
||
|
||
const anmeldung = z.object({
|
||
endpunkt: z.url(),
|
||
p256dh: z.string().min(1).max(500),
|
||
auth: z.string().min(1).max(500),
|
||
bezeichnung: z.string().trim().max(60).nullable().default(null),
|
||
})
|
||
|
||
export async function GET() {
|
||
const benutzer = await verlangeAnmeldungFuerApi()
|
||
if (!benutzer) {
|
||
return NextResponse.json({ fehler: 'Nicht angemeldet.' }, { status: 401 })
|
||
}
|
||
|
||
const einst = umgebung()
|
||
return NextResponse.json(
|
||
{
|
||
aktiv: einst.pushAktiv,
|
||
oeffentlicherSchluessel: einst.pushAktiv ? einst.VAPID_OEFFENTLICH : null,
|
||
},
|
||
{ headers: { 'Cache-Control': 'no-store' } },
|
||
)
|
||
}
|
||
|
||
export async function POST(anfrage: Request) {
|
||
const benutzer = await verlangeAnmeldungFuerApi()
|
||
if (!benutzer) {
|
||
return NextResponse.json({ fehler: 'Nicht angemeldet.' }, { status: 401 })
|
||
}
|
||
if (!umgebung().pushAktiv) {
|
||
return NextResponse.json(
|
||
{ fehler: 'Push ist auf diesem Server nicht eingerichtet.' },
|
||
{ status: 400 },
|
||
)
|
||
}
|
||
|
||
let inhalt: unknown
|
||
try {
|
||
inhalt = await anfrage.json()
|
||
} catch {
|
||
return NextResponse.json({ fehler: 'Ungültige Anfrage.' }, { status: 400 })
|
||
}
|
||
|
||
const gelesen = anmeldung.safeParse(inhalt)
|
||
if (!gelesen.success) {
|
||
return NextResponse.json(
|
||
{ fehler: 'Die Anmeldung war unvollständig.' },
|
||
{ status: 400 },
|
||
)
|
||
}
|
||
|
||
const geraet = await geraetAnmelden(
|
||
benutzer.id,
|
||
{
|
||
endpunkt: gelesen.data.endpunkt,
|
||
p256dh: gelesen.data.p256dh,
|
||
auth: gelesen.data.auth,
|
||
},
|
||
gelesen.data.bezeichnung,
|
||
)
|
||
|
||
await auditSchreiben({
|
||
akteur: { id: benutzer.id, email: benutzer.email },
|
||
aktion: 'push.angemeldet',
|
||
objektTyp: 'PushGeraet',
|
||
objektId: geraet.id,
|
||
// Weder Endpunkt noch Schlüssel – beides taugt zum Zustellen.
|
||
details: { bezeichnung: gelesen.data.bezeichnung },
|
||
})
|
||
|
||
return NextResponse.json({ ok: true })
|
||
}
|
||
|
||
const abmeldung = z.object({ endpunkt: z.url() })
|
||
|
||
export async function DELETE(anfrage: Request) {
|
||
const benutzer = await verlangeAnmeldungFuerApi()
|
||
if (!benutzer) {
|
||
return NextResponse.json({ fehler: 'Nicht angemeldet.' }, { status: 401 })
|
||
}
|
||
|
||
let inhalt: unknown
|
||
try {
|
||
inhalt = await anfrage.json()
|
||
} catch {
|
||
return NextResponse.json({ fehler: 'Ungültige Anfrage.' }, { status: 400 })
|
||
}
|
||
|
||
const gelesen = abmeldung.safeParse(inhalt)
|
||
if (!gelesen.success) {
|
||
return NextResponse.json({ fehler: 'Ungültige Anfrage.' }, { status: 400 })
|
||
}
|
||
|
||
await geraetAbmelden(gelesen.data.endpunkt)
|
||
|
||
await auditSchreiben({
|
||
akteur: { id: benutzer.id, email: benutzer.email },
|
||
aktion: 'push.abgemeldet',
|
||
objektTyp: 'PushGeraet',
|
||
objektId: null,
|
||
})
|
||
|
||
return NextResponse.json({ ok: true })
|
||
}
|