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