Erste Produktivversion
This commit is contained in:
165
web/html/js/push.js
Normal file
165
web/html/js/push.js
Normal file
@@ -0,0 +1,165 @@
|
||||
// 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");
|
||||
}
|
||||
Reference in New Issue
Block a user