Files
TREK/client/src/components/Settings/WebPushCard.tsx
T
Maurice 650451bdf7 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.
2026-09-24 23:17:03 +00:00

48 lines
1.8 KiB
TypeScript

import React from 'react'
import { useTranslation } from '../../i18n'
import { useWebPush } from '../../hooks/useWebPush'
/**
* The "Push notifications on this device" card of the Notifications tab. Only
* markup: the logic is `useWebPush`, shared with the phone's `MWebPushCard`.
* The tab renders it while the admin has the push channel switched on.
*/
export default function WebPushCard(): React.ReactElement {
const { t } = useTranslation()
const push = useWebPush()
return (
<div className="mb-4 rounded-lg border border-edge bg-surface-secondary p-3">
<p className="mb-1 text-caption font-semibold text-content-secondary">{t('settings.webPush.title')}</p>
<p className="mb-2 text-caption text-content-faint">{t('settings.webPush.hint')}</p>
{push.stateText && (
<p role="status" className="mb-2 text-caption text-content-secondary">
{push.stateText}
</p>
)}
{push.canSwitch && (
<div className="flex items-center gap-2">
<button
type="button"
onClick={push.toggle}
disabled={push.busy}
className={`rounded-md px-3 py-1.5 text-caption disabled:cursor-not-allowed disabled:opacity-60 ${
push.on ? 'border border-edge bg-transparent text-content-secondary' : 'bg-accent text-accent-text'
}`}
>
{push.switchLabel}
</button>
<button
type="button"
onClick={push.sendTest}
disabled={!push.canTest}
className="rounded-md border border-edge bg-transparent px-3 py-1.5 text-caption text-content-secondary disabled:cursor-not-allowed disabled:opacity-50"
>
{t('settings.notificationPreferences.sendTest')}
</button>
</div>
)}
</div>
)
}