// Benutzerverwaltung für Administratoren. "use strict"; import { del, get, post, put } from "../api.js"; import { el, mount } from "../dom.js"; import { state } from "../store.js"; let openId = null; function formatDate(iso) { if (!iso) return null; return new Date(iso).toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric", }); } /** Wie lange ist das her, grob. Für "seit wann nicht mehr gesehen" ist * der Monat die passende Auflösung - der Tag wäre Scheingenauigkeit. */ function ago(iso) { if (!iso) return "nie"; const days = Math.floor((Date.now() - new Date(iso)) / 86400000); if (days < 1) return "heute"; if (days === 1) return "gestern"; if (days < 31) return `vor ${days} Tagen`; const months = Math.round(days / 30); if (months < 24) return `vor ${months} Monaten`; return `vor ${Math.round(months / 12)} Jahren`; } export async function adminView(root, { back }) { let users = []; let stats = null; let config = null; let banner = null; async function reload() { [users, stats, config] = await Promise.all([ get("/api/admin/users"), get("/api/admin/stats"), get("/api/admin/settings"), ]); } function say(message, kind = "notice") { banner = { message, kind }; render(); setTimeout(() => { banner = null; render(); }, 10000); } async function guarded(fn) { try { const result = await fn(); await reload(); render(); if (result && result.detail) say(result.detail); } catch (err) { say(err.message, "error"); } } // ------------------------------------------------------------------- // Kennzahlen // ------------------------------------------------------------------- function statsCard() { const tile = (label, value, warn = false) => el("div", { className: `stat${warn && value ? " warn" : ""}` }, el("span.value", {}, String(value)), el("span.label", {}, label)); return el("section.card", {}, el("h1", {}, "Benutzer"), el("div.stats", {}, tile("gesamt", stats.total), tile("aktiv", stats.active), tile("deaktiviert", stats.inactive), tile("nicht eingerichtet", stats.unverified), tile("Administratoren", stats.admins)), stats.due_deactivation || stats.due_deletion ? el("p.lead.warn", {}, "Beim nächsten nächtlichen Durchlauf: ", stats.due_deactivation ? `${stats.due_deactivation} Konto/Konten werden deaktiviert` : "", stats.due_deactivation && stats.due_deletion ? ", " : "", stats.due_deletion ? `${stats.due_deletion} werden gelöscht` : "", ".") : null ); } /** Steht bei den Konten, nicht bei der Bereinigung: Es geht darum, * WER hinzukommen darf - das gehört zur Kontenverwaltung. */ function registrationToggle() { const box = el("input", { type: "checkbox", checked: config.allow_self_registration, disabled: config.locked_by_env, onchange: (ev) => guarded(async () => { await put("/api/admin/settings", { allow_self_registration: ev.target.checked, }); say(ev.target.checked ? "Selbstregistrierung eingeschaltet." : "Selbstregistrierung abgeschaltet – Zugang nur noch per Einladung."); }), }); return el("div.reg-toggle", {}, el("label.checkline", {}, box, el("span", {}, "Selbstregistrierung erlauben")), config.locked_by_env ? el("p.sub", {}, "Über die Umgebungsvariable ALLOW_SELF_REGISTRATION festgelegt " + "und hier nicht änderbar.") : el("p.sub", {}, "Ist sie aus, kommen neue Personen nur über „Konto anlegen“ " + "oder eine Listeneinladung hinein.")); } // ------------------------------------------------------------------- // Neues Konto // ------------------------------------------------------------------- function createCard() { const email = el("input", { type: "email", placeholder: "person@example.de" }); const name = el("input", { type: "text", maxLength: 80, placeholder: "freiwillig" }); const asAdmin = el("input", { type: "checkbox" }); return el("section.card", {}, el("h2", {}, "Konto anlegen"), el("p.lead", {}, "Die Person bekommt eine Willkommensnachricht mit einem Link, über " + "den sie ihr Passwort selbst festlegt. Ein vom Administrator " + "vergebenes Passwort wäre ihm bekannt und ginge im Klartext per Mail."), el("label", {}, "E-Mail-Adresse"), email, el("label", {}, "Anzeigename"), name, el("label.checkline", {}, asAdmin, el("span", {}, "Administratorrechte erteilen")), el("button.primary", { type: "button", onclick: () => { const value = email.value.trim(); if (!value) return; guarded(async () => { await post("/api/admin/users", { email: value, display_name: name.value.trim() || null, is_admin: asAdmin.checked, }); email.value = ""; name.value = ""; asAdmin.checked = false; say(`Konto angelegt, Willkommensnachricht an ${value} versendet.`); }); }, }, "Anlegen und einladen") ); } // ------------------------------------------------------------------- // Ein Konto // ------------------------------------------------------------------- function userActions(user) { const self = user.id === state.user.id; const newEmail = el("input", { type: "email", placeholder: "neue Adresse" }); const blocks = [ el("label", {}, "E-Mail-Adresse ändern"), el("p.sub", {}, user.is_admin ? "Administratorkonto: Die Änderung wird erst wirksam, wenn BEIDE " + "Adressen bestätigt haben – die neue und die bisherige." : "Die neue Adresse muss bestätigen; die bisherige bekommt einen " + "Hinweis, damit eine untergeschobene Änderung auffällt."), el("div.row", {}, newEmail, el("button.secondary", { type: "button", onclick: () => { const value = newEmail.value.trim(); if (!value) return; guarded(async () => { const result = await post(`/api/admin/users/${user.id}/email`, { new_email: value }); newEmail.value = ""; return result; }); }, }, "Ändern")), ]; if (user.pending_email) { blocks.push(el("p.lead.warn", {}, `Adresswechsel zu ${user.pending_email} läuft. `, el("button.linklike", { type: "button", onclick: () => guarded(() => del(`/api/admin/users/${user.id}/email`)), }, "Zurückziehen"))); } if (!user.verified) { blocks.push(el("div.menu-actions", {}, el("button", { type: "button", onclick: () => guarded(() => post(`/api/admin/users/${user.id}/welcome`)), }, "Willkommensnachricht erneut senden"))); } // Administratorkonten sind vor Deaktivierung und Löschung geschützt. if (user.is_admin) { blocks.push(el("p.sub", {}, "Administratorkonten lassen sich nicht deaktivieren oder löschen. " + "Sonst könnte sich die Verwaltung selbst aussperren.")); return el("div.item-menu", {}, blocks); } if (self) { blocks.push(el("p.sub", {}, "Das eigene Konto lässt sich hier nicht ändern.")); return el("div.item-menu", {}, blocks); } blocks.push(el("div.menu-actions", {}, user.is_active ? el("button", { type: "button", onclick: () => { if (!confirm( `Konto ${user.email} deaktivieren?\n\n` + "Die Person kann sich nicht mehr anmelden. Listen und " + "Mitgliedschaften bleiben erhalten." )) return; guarded(() => post(`/api/admin/users/${user.id}/deactivate`)); }, }, "Deaktivieren") : el("button", { type: "button", onclick: () => guarded(() => post(`/api/admin/users/${user.id}/activate`)), }, "Reaktivieren"))); blocks.push(el("hr.thin", {})); blocks.push(el("label", {}, "Konto löschen")); blocks.push(el("p.sub", {}, user.owned_lists ? `Diesem Konto gehören ${user.owned_lists} Liste(n). Geteilte Listen ` + "gehen an das dienstälteste andere Mitglied über, Listen ohne " + "weitere Mitglieder werden gelöscht." : "Diesem Konto gehören keine Listen.")); blocks.push(el("button.danger.wide", { type: "button", onclick: () => { const typed = prompt( `Konto ${user.email} endgültig löschen?\n\n` + "Zur Bestätigung die E-Mail-Adresse eingeben:"); if (!typed) return; guarded(() => post(`/api/admin/users/${user.id}/delete`, { lists: "handover", confirm_email: typed.trim(), })); }, }, "Endgültig löschen")); return el("div.item-menu", {}, blocks); } function userRow(user) { const facts = [ user.is_admin ? "Administrator" : null, !user.verified ? "noch nicht eingerichtet" : null, !user.is_active ? `deaktiviert seit ${formatDate(user.deactivated_at)}` : null, user.is_active && user.verified ? `zuletzt ${ago(user.last_seen_at)}` : null, user.owned_lists ? `${user.owned_lists} eigene Liste(n)` : null, user.memberships ? `${user.memberships} Mitgliedschaft(en)` : null, ].filter(Boolean); return el("li", { className: `admin-user${user.is_active ? "" : " inactive"}`, }, el("button.user-open", { type: "button", onclick: () => { openId = openId === user.id ? null : user.id; render(); }, }, el("span.name", {}, user.display_name || user.email), el("span.sub", {}, user.display_name ? `${user.email} · ` : "", facts.join(" · "))), openId === user.id ? userActions(user) : null ); } // ------------------------------------------------------------------- // Automatik // ------------------------------------------------------------------- function automationCard() { const deactivate = el("input.months", { type: "number", min: "0", max: "600", value: String(config.auto_deactivate_months), }); const remove = el("input.months", { type: "number", min: "0", max: "600", value: String(config.auto_delete_months), }); const archive = el("input.months", { type: "number", min: "0", max: "600", value: String(config.auto_archive_months), }); const archiveDelete = el("input.months", { type: "number", min: "0", max: "600", value: String(config.archive_delete_months), }); return el("section.card", {}, el("h2", {}, "Automatische Bereinigung"), el("p.lead", {}, "Läuft einmal täglich. ", el("strong", {}, "0 bedeutet abgeschaltet"), ", nicht „sofort“. Administratorkonten sind ausgenommen."), el("label", {}, "Deaktivieren nach … Monaten ohne Anmeldung"), deactivate, el("label", {}, "Löschen nach … Monaten Deaktivierung"), remove, el("h3.device-heading", {}, "Listen"), el("label", {}, "Archivieren nach … Monaten ohne Öffnen"), archive, el("p.sub", {}, "Gilt je Person: Die Liste verschwindet nur aus der Übersicht " + "derjenigen, die sie nicht geöffnet hat."), el("label", {}, "Löschen nach … Monaten im Archiv"), archiveDelete, el("p.sub", {}, "Maßgeblich ist allein das Archivdatum des Eigentümers. Er wird " + "7 Tage und 1 Tag vorher per E-Mail gewarnt."), el("button.primary", { type: "button", onclick: () => guarded(async () => { await put("/api/admin/settings", { auto_deactivate_months: Number(deactivate.value) || 0, auto_delete_months: Number(remove.value) || 0, auto_archive_months: Number(archive.value) || 0, archive_delete_months: Number(archiveDelete.value) || 0, }); say("Einstellungen gespeichert."); }), }, "Speichern"), el("div.menu-actions", {}, el("button", { type: "button", onclick: () => guarded(() => post("/api/admin/cleanup")), }, "Jetzt aufräumen")) ); } // ------------------------------------------------------------------- function render() { mount(root, el("header.bar", {}, el("button.linklike", { type: "button", onclick: back }, "‹ Zurück")), banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null, statsCard(), el("section.card", {}, el("h2", {}, "Konten"), registrationToggle(), el("ul.admin-users", {}, users.map(userRow))), createCard(), automationCard() ); } await reload(); render(); return render; }