166 lines
5.2 KiB
JavaScript
166 lines
5.2 KiB
JavaScript
// An- und Abmeldung für Push-Benachrichtigungen.
|
|
//
|
|
// Ablauf: Der Browser holt sich beim Push-Dienst seines Herstellers einen
|
|
// Endpunkt, den er zusammen mit Schlüsselmaterial an unseren Server
|
|
// meldet. Der Server kann darüber verschlüsselte Nachrichten schicken -
|
|
// lesen kann sie nur dieses Gerät, auch der Push-Dienst nicht.
|
|
"use strict";
|
|
|
|
import { get, post } from "./api.js";
|
|
|
|
/** Der öffentliche VAPID-Schlüssel kommt als base64url ohne
|
|
* Auffüllzeichen; die Browser-API will ein Uint8Array. */
|
|
function decodeKey(base64url) {
|
|
const padded = base64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
const raw = atob(padded + "=".repeat((4 - (padded.length % 4)) % 4));
|
|
return Uint8Array.from(raw, (c) => c.charCodeAt(0));
|
|
}
|
|
|
|
/** Kurze Gerätebezeichnung, damit man mehrere Anmeldungen unterscheiden
|
|
* kann. Bewusst grob - der vollständige User-Agent wäre ein
|
|
* Wiedererkennungsmerkmal und wird nicht gebraucht. */
|
|
function deviceLabel() {
|
|
const ua = navigator.userAgent;
|
|
const system =
|
|
/Android/i.test(ua) ? "Android"
|
|
: /iPhone|iPad|iPod/i.test(ua) ? "iPhone/iPad"
|
|
: /Macintosh/i.test(ua) ? "Mac"
|
|
: /Windows/i.test(ua) ? "Windows"
|
|
: /Linux/i.test(ua) ? "Linux"
|
|
: "Gerät";
|
|
const browser =
|
|
/Firefox\//i.test(ua) ? "Firefox"
|
|
: /Edg\//i.test(ua) ? "Edge"
|
|
: /Chrome\//i.test(ua) ? "Chrome"
|
|
: /Safari\//i.test(ua) ? "Safari"
|
|
: "Browser";
|
|
return `${browser} auf ${system}`.slice(0, 80);
|
|
}
|
|
|
|
/** Läuft die App als installierte PWA? Auf iOS ist das die
|
|
* Voraussetzung dafür, dass Push überhaupt funktioniert. */
|
|
export function isInstalled() {
|
|
return (
|
|
window.matchMedia?.("(display-mode: standalone)")?.matches === true ||
|
|
navigator.standalone === true
|
|
);
|
|
}
|
|
|
|
export function isApple() {
|
|
return /iPhone|iPad|iPod/i.test(navigator.userAgent) ||
|
|
(/Macintosh/i.test(navigator.userAgent) && navigator.maxTouchPoints > 1);
|
|
}
|
|
|
|
/** Was der Browser hier grundsätzlich kann. */
|
|
export function supported() {
|
|
return (
|
|
"serviceWorker" in navigator &&
|
|
"PushManager" in window &&
|
|
"Notification" in window &&
|
|
window.isSecureContext
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Aktueller Zustand.
|
|
* @returns {Promise<{
|
|
* supported: boolean, serverEnabled: boolean, permission: string,
|
|
* subscribed: boolean, needsInstall: boolean, throttleHours: number
|
|
* }>}
|
|
*/
|
|
export async function status() {
|
|
const base = {
|
|
supported: supported(),
|
|
serverEnabled: false,
|
|
permission: "Notification" in window ? Notification.permission : "unsupported",
|
|
subscribed: false,
|
|
// iOS lässt Push nur in der installierten App zu.
|
|
needsInstall: isApple() && !isInstalled(),
|
|
throttleHours: 2,
|
|
};
|
|
|
|
try {
|
|
const config = await get("/api/push/config");
|
|
base.serverEnabled = config.enabled;
|
|
base.throttleHours = config.throttle_hours;
|
|
} catch {
|
|
return base;
|
|
}
|
|
|
|
if (!base.supported) return base;
|
|
|
|
try {
|
|
const registration = await navigator.serviceWorker.ready;
|
|
base.subscribed = Boolean(await registration.pushManager.getSubscription());
|
|
} catch {
|
|
// Kein Service Worker bereit - dann eben nicht angemeldet.
|
|
}
|
|
return base;
|
|
}
|
|
|
|
/** Fragt die Erlaubnis ab und meldet das Gerät an.
|
|
* @returns {Promise<{ok: boolean, reason?: string}>} */
|
|
export async function subscribe() {
|
|
if (!supported()) {
|
|
return { ok: false, reason: "Dieser Browser unterstützt keine Benachrichtigungen." };
|
|
}
|
|
|
|
const config = await get("/api/push/config");
|
|
if (!config.enabled || !config.public_key) {
|
|
return { ok: false, reason: "Auf diesem Server sind Benachrichtigungen nicht eingerichtet." };
|
|
}
|
|
|
|
const permission = await Notification.requestPermission();
|
|
if (permission !== "granted") {
|
|
return {
|
|
ok: false,
|
|
reason: permission === "denied"
|
|
? "Benachrichtigungen wurden für diese Seite abgelehnt. Das lässt sich nur in den Browsereinstellungen wieder ändern."
|
|
: "Keine Erlaubnis erteilt.",
|
|
};
|
|
}
|
|
|
|
const registration = await navigator.serviceWorker.ready;
|
|
let subscription = await registration.pushManager.getSubscription();
|
|
|
|
if (!subscription) {
|
|
subscription = await registration.pushManager.subscribe({
|
|
// Pflicht bei allen aktuellen Browsern: Nachrichten ohne
|
|
// sichtbare Meldung sind nicht erlaubt.
|
|
userVisibleOnly: true,
|
|
applicationServerKey: decodeKey(config.public_key),
|
|
});
|
|
}
|
|
|
|
const json = subscription.toJSON();
|
|
await post("/api/push/subscribe", {
|
|
endpoint: json.endpoint,
|
|
keys: { p256dh: json.keys.p256dh, auth: json.keys.auth },
|
|
label: deviceLabel(),
|
|
});
|
|
|
|
return { ok: true };
|
|
}
|
|
|
|
/** Meldet dieses Gerät ab - lokal und auf dem Server. */
|
|
export async function unsubscribe() {
|
|
if (!supported()) return;
|
|
const registration = await navigator.serviceWorker.ready;
|
|
const subscription = await registration.pushManager.getSubscription();
|
|
if (!subscription) return;
|
|
|
|
const endpoint = subscription.endpoint;
|
|
// Erst lokal abmelden: Schlägt der Server fehl, sollen trotzdem keine
|
|
// Meldungen mehr ankommen.
|
|
await subscription.unsubscribe().catch(() => {});
|
|
await post("/api/push/unsubscribe", { endpoint }).catch(() => {});
|
|
}
|
|
|
|
export function sendTest() {
|
|
return post("/api/push/test");
|
|
}
|
|
|
|
export function listDevices() {
|
|
return get("/api/push/subscriptions");
|
|
}
|