234 lines
7.9 KiB
JavaScript
234 lines
7.9 KiB
JavaScript
// 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 <form>-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 });
|
||
}
|