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