source: Klonkt/src/routes/push.js@ 053bf51

main
Last change on this file since 053bf51 was 053bf51, checked in by Robin <roboburr@…>, 7 weeks ago

Feature: web push slice 2, enable/disable UI + service worker delivery

The visible half of docs/webpush-design.md: an owner can now turn on
notifications per device, pick what to be notified about, and send a test.

  • Routes: GET /push/vapid (the public key, public by design), POST /push/subscribe|/unsubscribe|/alerts|/test (logged-in; a subscription row is personal, only its creator may touch it). Mounted before the /:slug catch-all, like /paid.
  • Service worker: push handler (shows the encrypted JSON payload {type,title,body,url}; same-type bursts collapse via tag) and notificationclick (focus an open tab and navigate, else open a window). Cache name bumped to v19.
  • Beheer -> Notificaties (/admin/push): per-device toggle, alert-type checkboxes (saved prefs shown for the current device), test button, linked-devices list with remove, iOS install hint (push needs an installed PWA there), plain <script> so injectCspNonce provides the real nonce.
  • Not premium-gated: notifications are infrastructure, not an extra.

Verified live on a dev server: /push/vapid serves the generated key,
storage/.vapid is 0600, sw.js carries both handlers, and an unauthenticated
subscribe is refused.

Changed files:
src/server.js

  • mount /push + /admin/push; sw.js push/notificationclick handlers, v19

src/views/pages/admin.ejs

  • "Notificaties" button (always visible, not premium)

New file:
src/routes/push.js

  • vapid/subscribe/unsubscribe/alerts/test

src/routes/admin-push.js

  • the Beheer page (requireSiteManager)

src/views/pages/admin-push.ejs

  • device toggle, prefs, test, device list, iOS hint

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 2.6 KB
Line 
1/**
2 * Web Push (docs/webpush-design.md) slice 2: enable/disable + test.
3 * The public VAPID key is public by design (it only identifies this server to
4 * the browser's push service); everything that touches a subscription is a
5 * logged-in action. Web Push delivery itself is cookie-less.
6 */
7import express from 'express';
8import db from '../config/database.js';
9import { requireAuth } from '../middleware/auth.js';
10import Push from '../services/PushService.js';
11
12const router = express.Router();
13
14// A subscription row is personal: only its creator may touch it.
15function ownRow(endpoint, userId) {
16 if (!endpoint) return null;
17 const row = db.prepare('SELECT endpoint, user_id FROM push_subscriptions WHERE endpoint = ?').get(String(endpoint));
18 return row && row.user_id === userId ? row : null;
19}
20
21router.get('/vapid', async (req, res) => {
22 const key = await Push.publicKey();
23 if (!key) return res.status(503).json({ error: 'push_unavailable' });
24 res.json({ publicKey: key });
25});
26
27router.post('/subscribe', requireAuth, express.json({ limit: '16kb' }), async (req, res) => {
28 if (!(await Push.pushReady())) return res.status(503).json({ error: 'push_unavailable' });
29 const s = req.body && req.body.subscription;
30 const keys = s && s.keys;
31 const ok = Push.saveSubscription({
32 endpoint: s && s.endpoint, userId: req.session.user.id,
33 p256dh: keys && keys.p256dh, auth: keys && keys.auth,
34 alertTypes: req.body.alerts || null,
35 uaLabel: String(req.body.uaLabel || '').slice(0, 120) || null,
36 });
37 if (!ok) return res.status(400).json({ error: 'bad_subscription' });
38 res.json({ ok: true });
39});
40
41router.post('/unsubscribe', requireAuth, express.json({ limit: '4kb' }), (req, res) => {
42 const row = ownRow(req.body && req.body.endpoint, req.session.user.id);
43 if (!row) return res.status(404).json({ error: 'not_found' });
44 Push.deleteSubscription(row.endpoint);
45 res.json({ ok: true });
46});
47
48router.post('/alerts', requireAuth, express.json({ limit: '4kb' }), (req, res) => {
49 const row = ownRow(req.body && req.body.endpoint, req.session.user.id);
50 if (!row) return res.status(404).json({ error: 'not_found' });
51 Push.updateAlerts(row.endpoint, req.session.user.id, req.body.alerts || {});
52 res.json({ ok: true });
53});
54
55// A test ping to all of the caller's own devices (bypasses alert prefs).
56router.post('/test', requireAuth, async (req, res) => {
57 const sent = await Push.notifyUser(req.session.user.id, {
58 type: 'test', title: 'Klonkt-testnotificatie',
59 body: 'Werkt. Zo komen meldingen binnen op dit apparaat.', url: '/admin/push',
60 });
61 res.json({ ok: true, sent });
62});
63
64export default router;
Note: See TracBrowser for help on using the repository browser.