mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
feat(notifications): Web Push delivers notifications to every device, even with TREK closed
A fourth built-in channel next to email, webhook and ntfy. Browsers register per device, the server encrypts per RFC 8291 and signs per RFC 8292 with node:crypto and sends only to known push services through the SSRF guard. The event matrix decides per category what arrives by push; admins switch the channel on under Notifications. VAPID keys come from the environment or are generated once, only while neither stored row exists, and kept encrypted. A pair that does not decrypt or match is never overwritten and never swapped for another one: push stays off with an error in the log and every device stays registered until the key is fixed. The subject is VAPID_SUBJECT, then an https APP_URL, then the project URL. Only chat messages collapse into one notification per trip, and a replacement still alerts. A 403 removes a device only after three failures in a row; 404 and 410 remove it at once. Password change, reset and an admin setting a password drop the user's devices with the other credentials. The request log redacts subscriptions and endpoints, boot errors print *** for every secret variable, and logout forgets the device with a plain fetch so an expired session cannot redirect midway. Addresses discussion #894.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import apiClient, { parseInDev } from './client'
|
||||
import {
|
||||
pushPublicKeyResultSchema,
|
||||
pushSubscribeResultSchema,
|
||||
pushUnsubscribeResultSchema,
|
||||
type PushPublicKeyResult,
|
||||
type PushSubscribeRequest,
|
||||
type PushSubscribeResult,
|
||||
type PushSubscriptionInput,
|
||||
type PushUnsubscribeRequest,
|
||||
type PushUnsubscribeResult,
|
||||
} from '@trek/shared'
|
||||
|
||||
/**
|
||||
* The Web Push device endpoints, in their own file rather than in the
|
||||
* `client.ts` god module. Types come from the shared contracts, so a server
|
||||
* change the client has not caught up with is a typecheck failure rather than
|
||||
* a runtime surprise.
|
||||
*
|
||||
* These calls are online only by nature: a subscription is a device talking to
|
||||
* the server and to the browser's push service at the same moment, so there is
|
||||
* nothing to queue or to cache. `push/webPush.ts` is the only caller. The one
|
||||
* DELETE that does not come through here is the logout teardown's: it is a
|
||||
* plain fetch in webPush.ts, so a session that already ran out cannot set off
|
||||
* apiClient's 401 redirect in the middle of a logout.
|
||||
*/
|
||||
export const pushApi = {
|
||||
/** The VAPID public key a browser subscribes with. */
|
||||
getPublicKey: (): Promise<PushPublicKeyResult> =>
|
||||
apiClient
|
||||
.get('/notifications/push/public-key')
|
||||
.then(r => parseInDev(pushPublicKeyResultSchema, r.data, 'push.publicKey')),
|
||||
|
||||
/** Registers this device. Repeating it for the same endpoint only refreshes the row. */
|
||||
subscribe: (subscription: PushSubscriptionInput): Promise<PushSubscribeResult> => {
|
||||
const body: PushSubscribeRequest = { subscription }
|
||||
return apiClient
|
||||
.post('/notifications/push/subscriptions', body)
|
||||
.then(r => parseInDev(pushSubscribeResultSchema, r.data, 'push.subscribe'))
|
||||
},
|
||||
|
||||
/** Forgets one device, by the endpoint its subscription delivers to. */
|
||||
unsubscribe: (endpoint: string): Promise<PushUnsubscribeResult> => {
|
||||
const body: PushUnsubscribeRequest = { endpoint }
|
||||
return apiClient
|
||||
.delete('/notifications/push/subscriptions', { data: body })
|
||||
.then(r => parseInDev(pushUnsubscribeResultSchema, r.data, 'push.unsubscribe'))
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user