Files
einkaufsapp/web/html/js/views/auth.js
2026-08-08 20:31:58 +02:00

234 lines
7.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 });
}