mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
650451bdf7
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.
50 lines
2.1 KiB
TypeScript
50 lines
2.1 KiB
TypeScript
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'))
|
|
},
|
|
}
|