Files
Lauri Ojansivu 0a5ae3e205 Browser push notifications, per device.
Member Settings / Notification Settings / "Browser notifications on this
device": the browser asks for permission and gives a push subscription,
and from then on every notification the member gets in WeKan - the same
recipients, watching and muted kinds as the tray and e-mail - also pops
up from that browser even with WeKan closed; a click opens the card. Turn
it on per browser and device, up to ten per account.

web-push 3.6.7 (pinned) sends it with this WeKan's own VAPID key pair,
made once and kept in a server-only collection; only the public key
leaves the server. A subscription's endpoint is chosen by the browser and
POSTed to by the server, so it is accepted only over https and only at
the push services browsers use - an internal address or another site is
refused when saved and again before every send. Gone subscriptions are
forgotten. The service worker opens only paths of this WeKan. Not
offered on Sandstorm.

models/lib/webPush.js decides; tests/webPush.test.cjs covers the push
services, internal and look-alike hosts, foreign click targets and the
bound; server/lib/tests/webPush.tests.js runs in Meteor (passing): one
key pair, only the public key out, an internal endpoint refused. With
the webhook activity picker (c3321926ab) and the all-boards activity
feed (656af74b2c), this completes both issues. The new strings are
English placeholders until the next translation pass.

Thanks to saurabharch, hingerl and xet7 !

Fixes #2026,
Fixes #1377.
2026-10-14 15:44:01 +03:00

37 lines
1.4 KiB
JavaScript

// #2026 / #1377: turning browser push on and off on this device
// (server/webPush.js). The service worker is public/pwa-service-worker.js.
import { Meteor } from 'meteor/meteor';
const { base64urlToBytes } = require('/models/lib/passkeys');
export function webPushSupported() {
return typeof window !== 'undefined' && 'serviceWorker' in navigator && 'PushManager' in window
&& 'Notification' in window && !Meteor.settings?.public?.sandstorm;
}
async function registration() {
return navigator.serviceWorker.ready;
}
export async function webPushEnabledHere() {
if (!webPushSupported()) return false;
const reg = await registration();
return !!(await reg.pushManager.getSubscription());
}
export async function enableWebPush() {
if ((await Notification.requestPermission()) !== 'granted') throw new Meteor.Error('web-push-denied');
const publicKey = await Meteor.callAsync('webPushPublicKey');
const reg = await registration();
const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: base64urlToBytes(publicKey) });
await Meteor.callAsync('webPushSubscribe', sub.toJSON());
}
export async function disableWebPush() {
const reg = await registration();
const sub = await reg.pushManager.getSubscription();
if (!sub) return;
await Meteor.callAsync('webPushUnsubscribe', sub.endpoint);
await sub.unsubscribe();
}