Erstanmeldung fragt nach, statt still ein zweites Konto anzulegen

Wer sich zum ersten Mal ueber den Anmeldedienst anmeldet und hier noch kein
Konto hat, bekam bisher wortlos ein neues - mit Rolle "Benutzer" und ohne
Verlauf. Wer schon ein Konto hatte, wollte aber fast immer genau das verknuepfen
und stand dann mit zwei Konten da.

Jetzt zeigt der Rueckweg eine Entscheidungsseite: bestehendes Konto verknuepfen
oder ein neues anlegen. Das Verknuepfen verlangt einmalig Benutzername und
Passwort des bestehenden Kontos. Genau dieser Nachweis ist der Grund, warum der
Anmeldeweg selbst nie fragen darf, welches Konto gemeint ist - ohne ihn koennte
sich jeder mit einem beliebigen Konto beim Anmeldedienst an ein fremdes
Vorrania-Konto haengen.

Die geprueften Angaben liegen dafuer kurzlebig und einmalig in
oidc_pending_links; vom Code steht dort nur der SHA-256-Hash, wie bei den
API-Tokens. Ein falsch getipptes Passwort verbraucht den Vorgang nicht, und
unbekannter Benutzername und falsches Passwort antworten gleich - sonst liessen
sich vorhandene Konten abzaehlen.

AUTO_CREATE=false blendet den Knopf zum Anlegen aus und laesst sich auch ueber
den Dialog nicht umgehen. Der App-Weg bleibt unveraendert, weil er keinen
Zwischenschritt anzeigen kann.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-09 22:28:38 +02:00
co-authored by Claude Opus 5
parent a134b2aad5
commit eb29143f97
9 changed files with 609 additions and 31 deletions
+14 -1
View File
@@ -21,6 +21,8 @@ export function setToken(token) {
// Pfade, bei denen ein 401 NICHT „Sitzung abgelaufen" heisst: beim Anmelden
// selbst heisst er „Passwort falsch", und die Anbieterliste ist oeffentlich.
const OHNE_ABMELDUNG = ["/auth/login", "/auth/oidc/providers"];
// Beim Verknuepfen einer Erstanmeldung heisst 401 „Passwort falsch".
const OHNE_ABMELDUNG_PRAEFIX = ["/auth/oidc/pending/"];
let beiAbmeldung = null;
export function setUnauthorizedHandler(fn) {
@@ -55,7 +57,9 @@ async function request(path, { method = "GET", body, form, formData } = {}) {
// Abgelaufene Anmeldung: EINMAL zentral behandeln statt in jeder Seite.
// Beim Anmelden selbst nicht – dort heisst 401 schlicht „Passwort falsch".
if (resp.status === 401 && !OHNE_ABMELDUNG.includes(path)) {
const ausgenommen = OHNE_ABMELDUNG.includes(path)
|| OHNE_ABMELDUNG_PRAEFIX.some((p) => path.startsWith(p));
if (resp.status === 401 && !ausgenommen) {
beiAbmeldung?.();
}
@@ -160,6 +164,15 @@ export const api = {
// laufenden Sitzung mitgeht - bei einer Seitennavigation ginge das nicht.
oidcLinkStart: (slug) => request(`/auth/oidc/${slug}/link`, { method: "POST" }),
oidcUnlink: (slug) => request(`/auth/oidc/${slug}/link`, { method: "DELETE" }),
// Erstanmeldung ohne passendes Konto: der Einmalcode aus der Adresse ist der
// Nachweis, dass die Anmeldung beim Dienst stattgefunden hat.
oidcPending: (code) => request(`/auth/oidc/pending/${code}`),
oidcPendingAnlegen: (code) =>
request(`/auth/oidc/pending/${code}/anlegen`, { method: "POST" }),
oidcPendingVerknuepfen: (code, username, password) =>
request(`/auth/oidc/pending/${code}/verknuepfen`, {
method: "POST", body: { username, password },
}),
// Produkte
listProducts: (q, categoryId) => {