// Einstellungen des angemeldeten Kontos: Anzeigename, Passwort, // Benachrichtigungen. "use strict"; import { del, patch, post } from "../api.js"; import { el, mount } from "../dom.js"; import * as push from "../push.js"; import { set, state } from "../store.js"; function formatDate(iso) { return new Date(iso).toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric", }); } export async function settingsView(root, { back, admin }) { let banner = null; let pushStatus = null; let devices = []; async function reload() { pushStatus = await push.status(); devices = pushStatus.serverEnabled ? await push.listDevices().catch(() => []) : []; } function say(message, kind = "notice") { banner = { message, kind }; render(); setTimeout(() => { banner = null; render(); }, 8000); } async function guarded(fn) { try { await fn(); await reload(); render(); } catch (err) { say(err.message, "error"); } } // ------------------------------------------------------------------- // Benachrichtigungen // ------------------------------------------------------------------- function pushSection() { if (!pushStatus.serverEnabled) { return el("section.card", {}, el("h2", {}, "Benachrichtigungen"), el("p.lead", {}, "Auf diesem Server sind Push-Benachrichtigungen nicht eingerichtet. " + "Dafür wird ein VAPID-Schlüsselpaar in der Konfiguration benötigt.")); } // Auf iOS gibt es Push nur in der installierten App - das ist eine // Einschränkung von Apple, kein Fehler der Anwendung. if (pushStatus.needsInstall) { return el("section.card", {}, el("h2", {}, "Benachrichtigungen"), el("p.lead.warn", {}, "Auf iPhone und iPad funktionieren Benachrichtigungen nur, wenn die " + "App über „Teilen → Zum Home-Bildschirm“ installiert wurde. " + "Öffne sie danach über das Symbol auf dem Startbildschirm und " + "komm hierher zurück.")); } if (!pushStatus.supported) { return el("section.card", {}, el("h2", {}, "Benachrichtigungen"), el("p.lead", {}, "Dieser Browser unterstützt keine Push-Benachrichtigungen. " + "Über eine unverschlüsselte Verbindung sind sie ebenfalls nicht " + "möglich – dann hilft der Zugriff über HTTPS.")); } const blocked = pushStatus.permission === "denied"; return el("section.card", {}, el("h2", {}, "Benachrichtigungen"), el("p.lead", {}, "Wenn jemand eine geteilte Liste ändert, bekommst du eine Meldung – ", el("strong", {}, `höchstens eine je Liste alle ${pushStatus.throttleHours} Stunden`), ". Wer im Laden steht und abhakt, erzeugt sonst im Minutentakt " + "Benachrichtigungen."), blocked ? el("p.lead.warn", {}, "Benachrichtigungen wurden für diese Seite abgelehnt. Das lässt " + "sich nur in den Einstellungen des Browsers wieder ändern – " + "meist über das Schloss- oder Info-Symbol in der Adresszeile.") : null, pushStatus.subscribed ? el("div.menu-actions", {}, el("button", { type: "button", onclick: () => guarded(async () => { await push.sendTest(); say("Testnachricht unterwegs. Kommt sie nicht an, steht der " + "Grund im Log des api-Containers."); }), }, "Testnachricht senden"), el("button.danger", { type: "button", onclick: () => guarded(async () => { await push.unsubscribe(); say("Dieses Gerät bekommt keine Benachrichtigungen mehr."); }), }, "Auf diesem Gerät abschalten")) : el("button.primary", { type: "button", disabled: blocked, onclick: () => guarded(async () => { const result = await push.subscribe(); if (!result.ok) { say(result.reason, "error"); return; } say("Benachrichtigungen für dieses Gerät eingeschaltet."); }), }, "Auf diesem Gerät einschalten"), devices.length ? el("div", {}, el("h3.device-heading", {}, "Angemeldete Geräte"), el("ul.share-list", {}, devices.map((device) => el("li", {}, el("div.who-block", {}, el("span.name", {}, device.label || "Unbenanntes Gerät"), el("span.sub", {}, `seit ${formatDate(device.created_at)}`, device.last_success_at ? ` · zuletzt erreicht ${formatDate(device.last_success_at)}` : " · noch nichts zugestellt")), el("div.member-actions", {}, el("button.danger", { type: "button", onclick: () => guarded(() => del(`/api/push/subscriptions/${device.id}`)), }, "Entfernen")))))) : null ); } // ------------------------------------------------------------------- // Profil und Passwort // ------------------------------------------------------------------- function profileSection() { const nameField = el("input", { type: "text", value: state.user.display_name || "", maxLength: 80, placeholder: "wird anderen Mitgliedern angezeigt", }); return el("section.card", {}, el("h2", {}, "Profil"), el("p.lead", {}, state.user.email), el("label", {}, "Anzeigename ", el("span.hint", {}, "(freiwillig)")), el("div.row", {}, nameField, el("button.primary", { type: "button", onclick: () => guarded(async () => { const user = await patch("/api/auth/me", { display_name: nameField.value.trim() || null, }); set({ user }); say("Gespeichert."); }), }, "Speichern")) ); } function passwordSection() { const current = el("input", { type: "password", autocomplete: "current-password" }); const next = el("input", { type: "password", autocomplete: "new-password" }); const repeat = el("input", { type: "password", autocomplete: "new-password" }); return el("section.card", {}, el("h2", {}, "Passwort ändern"), el("p.lead", {}, "Nach der Änderung werden alle anderen Geräte abgemeldet."), el("label", {}, "Bisheriges Passwort"), current, el("label", {}, "Neues Passwort ", el("span.hint", {}, "(mindestens 12 Zeichen)")), next, el("label", {}, "Wiederholen"), repeat, el("button.primary", { type: "button", onclick: () => guarded(async () => { if (next.value !== repeat.value) { throw new Error("Die beiden Eingaben stimmen nicht überein."); } const result = await post("/api/auth/password/change", { current_password: current.value, new_password: next.value, }); current.value = next.value = repeat.value = ""; say(result.detail); }), }, "Passwort ändern") ); } function render() { mount(root, el("header.bar", {}, el("button.linklike", { type: "button", onclick: back }, "‹ Zurück"), el("span.who", {}, state.user.display_name || state.user.email)), banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null, el("section.card", {}, el("h1", {}, "Einstellungen"), // Nur für Administratoren sichtbar - der Endpunkt prüft // zusätzlich, die Oberfläche allein wäre kein Schutz. state.user.is_admin ? el("div", {}, el("p.lead", {}, "Du hast Administratorrechte."), el("button.secondary", { type: "button", onclick: admin }, "Benutzerverwaltung")) : null), pushSection(), profileSection(), passwordSection() ); } await reload(); render(); return render; }