// Anmeldung, Registrierung, Passwort zurücksetzen und Startpasswortwechsel. "use strict"; import { ApiError, get, post } from "../api.js"; import { clear, el, mount } from "../dom.js"; import { set, state } from "../store.js"; /** Baut ein Formular ohne
-Element: dessen Standardverhalten * (Seite neu laden) stört hier nur. Enter löst trotzdem aus. */ function form({ title, lead, leadWarn, fields, submitLabel, onSubmit, links = [] }) { const inputs = {}; const error = el("p.error", { hidden: true }); const notice = el("p.notice", { hidden: true }); const button = el("button.primary", { type: "button" }, submitLabel); const showError = (message) => { error.textContent = message || ""; error.hidden = !message; }; const showNotice = (message) => { notice.textContent = message || ""; notice.hidden = !message; }; const run = async () => { showError(""); showNotice(""); button.disabled = true; try { const values = Object.fromEntries( Object.entries(inputs).map(([k, i]) => [k, i.value]) ); await onSubmit(values, { showNotice, showError }); } catch (err) { showError(err.message); } finally { button.disabled = false; } }; button.addEventListener("click", run); const rows = fields.map((f) => { const input = el("input", { id: `f-${f.name}`, type: f.type || "text", autocomplete: f.autocomplete || "off", maxLength: f.maxLength || 256, onkeydown: (ev) => { if (ev.key === "Enter") run(); }, }); inputs[f.name] = input; return [ el("label", { htmlFor: `f-${f.name}` }, f.label, f.hint ? el("span.hint", {}, ` ${f.hint}`) : null), input, ]; }); return el("section.card", {}, el("h1", {}, title), lead ? el(leadWarn ? "p.lead.warn" : "p.lead", {}, lead) : null, rows, error, notice, button, links.length ? el("p.switch", {}, links.flatMap((l, i) => [ i > 0 ? " · " : null, el("a", { href: "#", onclick: (ev) => { ev.preventDefault(); l.action(); } }, l.label), ])) : null ); } export function loginView(root, { goto, onSignedIn }) { const params = new URLSearchParams(location.search); const verified = params.get("verified"); const emailChange = params.get("adresswechsel"); const node = form({ title: state.appName, lead: "Bitte anmelden.", fields: [ { name: "email", label: "E-Mail-Adresse", type: "email", autocomplete: "username" }, { name: "password", label: "Passwort", type: "password", autocomplete: "current-password" }, ], submitLabel: "Anmelden", onSubmit: async ({ email, password }) => { const user = await post("/api/auth/login", { email: email.trim(), password }); onSignedIn(user); }, links: [ { label: "Konto anlegen", action: () => goto("register") }, { label: "Passwort vergessen", action: () => goto("forgot") }, ], }); mount(root, node); if (verified === "ok") { const notice = node.querySelector(".notice"); notice.textContent = "E-Mail-Adresse bestätigt. Du kannst dich jetzt anmelden."; notice.hidden = false; history.replaceState(null, "", "/"); } else if (verified === "invalid") { const error = node.querySelector(".error"); error.textContent = "Der Bestätigungslink ist ungültig oder abgelaufen."; error.hidden = false; history.replaceState(null, "", "/"); } if (emailChange) { const messages = { fertig: ["notice", "E-Mail-Adresse geändert. Bitte melde dich mit der neuen Adresse an."], teilweise: ["notice", "Bestätigung angekommen. Bei Administratorkonten muss auch die " + "zweite Adresse zustimmen – erst danach wird die Änderung wirksam."], abgelaufen: ["error", "Der Bestätigungslink ist abgelaufen."], erledigt: ["notice", "Dieser Adresswechsel ist bereits abgeschlossen."], unbekannt: ["error", "Dieser Bestätigungslink ist unbekannt."], }; const entry = messages[emailChange]; if (entry) { const target = node.querySelector(entry[0] === "error" ? ".error" : ".notice"); target.textContent = entry[1]; target.hidden = false; history.replaceState(null, "", "/"); } } node.querySelector("input").focus(); } export function registerView(root, { goto }) { mount(root, form({ title: "Konto anlegen", fields: [ { name: "email", label: "E-Mail-Adresse", type: "email", autocomplete: "username" }, { name: "display_name", label: "Anzeigename", hint: "(freiwillig)", maxLength: 80 }, { name: "password", label: "Passwort", hint: "(mindestens 12 Zeichen)", type: "password", autocomplete: "new-password" }, ], submitLabel: "Registrieren", onSubmit: async (v, { showNotice }) => { const result = await post("/api/auth/register", { email: v.email.trim(), password: v.password, display_name: v.display_name.trim() || null, }); showNotice(result.detail); }, links: [{ label: "Zurück zur Anmeldung", action: () => goto("login") }], })); } export function forgotView(root, { goto }) { mount(root, form({ title: "Passwort zurücksetzen", lead: "Wir schicken dir einen Link, sofern die Adresse bei uns registriert ist.", fields: [ { name: "email", label: "E-Mail-Adresse", type: "email", autocomplete: "username" }, ], submitLabel: "Link anfordern", onSubmit: async ({ email }, { showNotice }) => { const result = await post("/api/auth/password/reset-request", { email: email.trim() }); showNotice(result.detail); }, links: [{ label: "Zurück zur Anmeldung", action: () => goto("login") }], })); } export function resetView(root, { goto }) { const token = new URLSearchParams(location.search).get("token") || ""; mount(root, form({ title: "Neues Passwort setzen", fields: [ { name: "password", label: "Neues Passwort", hint: "(mindestens 12 Zeichen)", type: "password", autocomplete: "new-password" }, { name: "repeat", label: "Wiederholen", type: "password", autocomplete: "new-password" }, ], submitLabel: "Passwort setzen", onSubmit: async ({ password, repeat }, { showNotice }) => { if (password !== repeat) throw new Error("Die beiden Eingaben stimmen nicht überein."); const result = await post("/api/auth/password/reset", { token, password }); showNotice(result.detail); history.replaceState(null, "", "/"); setTimeout(() => goto("login"), 1500); }, })); } export function changePasswordView(root, { onDone, onLogout }) { mount(root, form({ title: "Passwort ändern", lead: "Dieses Konto wurde mit einem Startpasswort aus der Konfiguration angelegt. " + "Bevor du weiterarbeiten kannst, musst du ein eigenes Passwort setzen.", leadWarn: true, fields: [ { name: "current", label: "Bisheriges Passwort", type: "password", autocomplete: "current-password" }, { name: "next", label: "Neues Passwort", hint: "(mindestens 12 Zeichen)", type: "password", autocomplete: "new-password" }, { name: "repeat", label: "Wiederholen", type: "password", autocomplete: "new-password" }, ], submitLabel: "Passwort ändern", onSubmit: async ({ current, next, repeat }) => { if (next !== repeat) throw new Error("Die beiden Eingaben stimmen nicht überein."); await post("/api/auth/password/change", { current_password: current, new_password: next, }); const user = await get("/api/auth/me"); set({ user }); onDone(user); }, links: [{ label: "Abmelden", action: onLogout }], })); } export async function logout() { try { await post("/api/auth/logout"); } catch (err) { // Auch wenn der Server nicht erreichbar ist: lokal abmelden. if (!(err instanceof ApiError)) { /* offline - egal */ } } set({ user: null, lists: [], view: null }); }