mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 11:02:33 -05:00
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.
58 lines
2.5 KiB
JavaScript
58 lines
2.5 KiB
JavaScript
'use strict';
|
|
|
|
// #2026 / #1377: browser push notifications. A member turns them on per
|
|
// device in Member Settings / Notification Settings; the browser gives a
|
|
// push subscription, and every notification they get in WeKan is also
|
|
// pushed there, so it arrives even with WeKan closed.
|
|
//
|
|
// A subscription's endpoint is an address the BROWSER chose, which the server
|
|
// then POSTs to - so it is accepted only over https and only at the push
|
|
// services browsers use. Anything else (an internal address, another
|
|
// site) is refused when it is saved, so a member cannot make the server
|
|
// send requests where they like. Pure: tests/webPush.test.cjs.
|
|
|
|
const PUSH_SERVICE_HOSTS = Object.freeze([
|
|
'fcm.googleapis.com', // Chrome, Edge, Opera, Brave, Samsung
|
|
'updates.push.services.mozilla.com', // Firefox
|
|
'web.push.apple.com', // Safari
|
|
'notify.windows.com', // legacy Edge (*.notify.windows.com)
|
|
'push.services.mozilla.com',
|
|
]);
|
|
|
|
function isPushServiceEndpoint(endpoint) {
|
|
let u;
|
|
try { u = new URL(String(endpoint || '')); } catch (e) { return false; }
|
|
if (u.protocol !== 'https:' || u.username || u.password || (u.port && u.port !== '443')) return false;
|
|
const host = u.hostname.toLowerCase();
|
|
return PUSH_SERVICE_HOSTS.some(h => host === h || host.endsWith(`.${h}`));
|
|
}
|
|
|
|
const B64URL = /^[A-Za-z0-9_-]{8,200}$/;
|
|
|
|
// A subscription as the browser's PushSubscription.toJSON() gives it, or null.
|
|
function cleanSubscription(input) {
|
|
const s = input && typeof input === 'object' ? input : {};
|
|
const keys = s.keys && typeof s.keys === 'object' ? s.keys : {};
|
|
if (!isPushServiceEndpoint(s.endpoint) || String(s.endpoint).length > 1000) return null;
|
|
if (!B64URL.test(String(keys.p256dh || '')) || !B64URL.test(String(keys.auth || ''))) return null;
|
|
return { endpoint: String(s.endpoint), keys: { p256dh: String(keys.p256dh), auth: String(keys.auth) } };
|
|
}
|
|
|
|
const MAX_SUBSCRIPTIONS = 10;
|
|
|
|
// The member's subscriptions after adding one: once each, newest kept.
|
|
function subscriptionsAfterAdd(current, sub, now = new Date()) {
|
|
const list = (Array.isArray(current) ? current : []).filter(s => s && s.endpoint !== sub.endpoint);
|
|
return [...list, { ...sub, createdAt: now }].slice(-MAX_SUBSCRIPTIONS);
|
|
}
|
|
|
|
function pushPayload({ title, body, url }) {
|
|
return JSON.stringify({
|
|
title: String(title || 'WeKan').slice(0, 200),
|
|
body: String(body || '').slice(0, 1000),
|
|
url: /^\/[^/]|^\/$/.test(String(url || '')) ? String(url) : '/',
|
|
});
|
|
}
|
|
|
|
module.exports = { PUSH_SERVICE_HOSTS, isPushServiceEndpoint, cleanSubscription, subscriptionsAfterAdd, pushPayload, MAX_SUBSCRIPTIONS };
|