ST-cmovs862j: Admin test-send route + public/sw.js service worker
POST /api/internal/push/test-send: requireAdmin check (redirect bij niet-admin), optioneel body met defaults, roept sendPushToUser aan, 204. public/sw.js: push-handler met showNotification, notificationclick met same-origin guard, focus bestaand venster of openWindow. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
39484551e2
commit
aa385de635
2 changed files with 72 additions and 0 deletions
30
app/api/internal/push/test-send/route.ts
Normal file
30
app/api/internal/push/test-send/route.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { z } from 'zod'
|
||||
import { requireAdmin } from '@/lib/auth-guard'
|
||||
import { sendPushToUser } from '@/lib/push-server'
|
||||
|
||||
const schema = z.object({
|
||||
title: z.string().max(80).optional(),
|
||||
body: z.string().max(300).optional(),
|
||||
url: z.string().optional(),
|
||||
})
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await requireAdmin()
|
||||
|
||||
let input: z.infer<typeof schema> = {}
|
||||
try {
|
||||
const raw = await req.json()
|
||||
const parsed = schema.safeParse(raw)
|
||||
if (parsed.success) input = parsed.data
|
||||
} catch {
|
||||
// body is optional — use defaults
|
||||
}
|
||||
|
||||
await sendPushToUser(session.userId, {
|
||||
title: input.title ?? 'Test push',
|
||||
body: input.body ?? 'Admin test notification',
|
||||
url: input.url ?? '/',
|
||||
})
|
||||
|
||||
return new Response(null, { status: 204 })
|
||||
}
|
||||
42
public/sw.js
Normal file
42
public/sw.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Service Worker for Web Push notifications (PBI-55)
|
||||
|
||||
self.addEventListener('push', (event) => {
|
||||
let payload = { title: 'Scrum4Me', body: '', url: '/', tag: undefined }
|
||||
try {
|
||||
if (event.data) payload = { ...payload, ...event.data.json() }
|
||||
} catch (_) {}
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(payload.title, {
|
||||
body: payload.body,
|
||||
icon: '/icon-192.png',
|
||||
badge: '/icon-192.png',
|
||||
tag: payload.tag,
|
||||
data: { url: payload.url },
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
event.notification.close()
|
||||
|
||||
const rawUrl = event.notification.data?.url || '/'
|
||||
const targetUrl = new URL(rawUrl, self.location.origin)
|
||||
|
||||
// Same-origin guard
|
||||
if (targetUrl.origin !== self.location.origin) return
|
||||
|
||||
event.waitUntil(
|
||||
self.clients
|
||||
.matchAll({ type: 'window', includeUncontrolled: true })
|
||||
.then((clients) => {
|
||||
for (const client of clients) {
|
||||
if (client.url.startsWith(self.location.origin) && 'focus' in client) {
|
||||
client.navigate(targetUrl.href)
|
||||
return client.focus()
|
||||
}
|
||||
}
|
||||
return self.clients.openWindow(targetUrl.href)
|
||||
})
|
||||
)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue