From eddbb3710ee2dc711961f12e203fea80b14ecd93 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sun, 16 Aug 2026 11:07:55 +0200 Subject: [PATCH] Web: abgelaufene Anmeldung erkennen statt leere Seiten zeigen Zwei Defekte, die zusammen das gemeldete Muster ergeben. 1) Es gab keine zentrale Behandlung von HTTP 401. AuthProvider prueft die Anmeldung EINMAL beim Start; laeuft das Token danach ab, bleibt der Zustand "angemeldet" stehen, waehrend jede Abfrage scheitert. Seiten mit .catch(() => {}) (18 Stellen) zeigen dann schlicht nichts, andere eine englische Servermeldung - genau das "mal gar nichts, mal die Haelfte". Jetzt raeumt der API-Client bei 401 die Sitzung auf und der Login sagt, dass sie abgelaufen ist. Beim Anmelden selbst greift das nicht, dort heisst 401 "Passwort falsch". 2) Der Start hatte ein blankes `catch`, das das Token bei JEDEM Fehler loeschte - auch bei 502 oder weggebrochenem Netz. Wer waehrend eines Server-Neustarts F5 drueckte, war deshalb abgemeldet, obwohl die Sitzung voellig in Ordnung war. Das duerfte der haeufigere Ausloeser gewesen sein, weil in letzter Zeit oft neu deployt wurde. Jetzt wird nur bei 401/403 abgemeldet; bei allem anderen bleibt die Anmeldung liegen und es erscheint "Server nicht erreichbar" mit einem Knopf zum erneuten Versuchen. Co-Authored-By: Claude Opus 5 --- web/src/App.jsx | 24 ++++++++++++++- web/src/api.js | 14 +++++++++ web/src/auth.jsx | 65 ++++++++++++++++++++++++++++++++++------- web/src/pages/Login.jsx | 7 ++++- 4 files changed, 97 insertions(+), 13 deletions(-) diff --git a/web/src/App.jsx b/web/src/App.jsx index a920c90..23d3118 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -115,9 +115,31 @@ function Sidebar() { ); } +/** Server nicht erreichbar – die Anmeldung bleibt bestehen, nur der Abruf nicht. */ +function StartFehler({ text, erneut }) { + return ( +
+
+ +
+
{text}
+
+ Deine Anmeldung ist noch gültig – sobald der Server wieder antwortet, + geht es weiter. +
+
+
+ +
+ ); +} + function Protected({ children, adminOnly = false, wide = false }) { - const { user, isAdmin, loading } = useAuth(); + const { user, isAdmin, loading, startFehler, erneutVersuchen } = useAuth(); if (loading) return
Lädt…
; + if (startFehler) return ; if (!user) return ; if (adminOnly && !isAdmin) return ; return ( diff --git a/web/src/api.js b/web/src/api.js index 63acf73..aa67940 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -15,6 +15,14 @@ export function setToken(token) { else localStorage.removeItem(TOKEN_KEY); } +// Wird bei einer abgelaufenen/ungueltigen Anmeldung gerufen (HTTP 401). +// Ohne das bleibt die Oberflaeche „angemeldet", waehrend jede Abfrage scheitert – +// man sieht dann leere Seiten statt eines Hinweises. +let beiAbmeldung = null; +export function setUnauthorizedHandler(fn) { + beiAbmeldung = fn; +} + export class ApiError extends Error { constructor(message, status) { super(message); @@ -41,6 +49,12 @@ async function request(path, { method = "GET", body, form, formData } = {}) { const resp = await fetch(`${API_BASE}${path}`, { method, headers, body: payload }); + // Abgelaufene Anmeldung: EINMAL zentral behandeln statt in jeder Seite. + // Beim Anmelden selbst nicht – dort heisst 401 schlicht „Passwort falsch". + if (resp.status === 401 && path !== "/auth/login") { + beiAbmeldung?.(); + } + if (resp.status === 204) return null; let data = null; diff --git a/web/src/auth.jsx b/web/src/auth.jsx index 08db2df..2990637 100644 --- a/web/src/auth.jsx +++ b/web/src/auth.jsx @@ -1,24 +1,56 @@ import { createContext, useContext, useEffect, useState } from "react"; -import { api, getToken, setToken } from "./api"; +import { api, ApiError, getToken, setToken, setUnauthorizedHandler } from "./api"; const AuthContext = createContext(null); export function AuthProvider({ children }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); + // Anmeldung war da, ist aber abgelaufen – der Login-Bildschirm sagt warum. + const [abgelaufen, setAbgelaufen] = useState(false); + // Start fehlgeschlagen, ABER nicht wegen der Anmeldung (Server weg, Netz weg). + // Dann bleibt das Token liegen; alles andere hiesse, ein kurzer Aussetzer + // meldet einen ab. + const [startFehler, setStartFehler] = useState(null); - useEffect(() => { - async function boot() { - if (getToken()) { - try { - setUser(await api.me()); - } catch { - setToken(null); - } + async function pruefen() { + setStartFehler(null); + if (!getToken()) { + setLoading(false); + return; + } + try { + setUser(await api.me()); + } catch (err) { + // NUR bei abgelehnter Anmeldung abmelden. Ein 502 waehrend eines + // Server-Neustarts ist kein Grund, die Sitzung wegzuwerfen. + if (err instanceof ApiError && (err.status === 401 || err.status === 403)) { + setToken(null); + setUser(null); + } else if (err instanceof ApiError) { + setStartFehler(err.message); + } else { + // Kein HTTP-Fehler, sondern gar keine Antwort (Netz weg, Server aus). + // Die Browser-Meldung („Failed to fetch") hilft niemandem. + setStartFehler("Server nicht erreichbar."); } + } finally { setLoading(false); } - boot(); + } + + useEffect(() => { pruefen(); }, []); + + // Laeuft die Anmeldung waehrend der Nutzung ab, scheitert ab da JEDE Abfrage. + // Ohne diesen Griff bliebe die Oberflaeche „angemeldet" und zeigte nur noch + // leere Seiten – genau das war der Fehler. + useEffect(() => { + setUnauthorizedHandler(() => { + setToken(null); + setUser(null); + setAbgelaufen(true); + }); + return () => setUnauthorizedHandler(null); }, []); async function login(username, password) { @@ -26,18 +58,29 @@ export function AuthProvider({ children }) { setToken(res.access_token); const me = await api.me(); setUser(me); + setAbgelaufen(false); + setStartFehler(null); return me; } function logout() { setToken(null); setUser(null); + setAbgelaufen(false); + } + + async function erneutVersuchen() { + setLoading(true); + await pruefen(); } const isAdmin = user?.role === "admin"; return ( - + {children} ); diff --git a/web/src/pages/Login.jsx b/web/src/pages/Login.jsx index 520661d..0d55534 100644 --- a/web/src/pages/Login.jsx +++ b/web/src/pages/Login.jsx @@ -5,7 +5,7 @@ import Icon from "../components/Icon"; import BrandMark from "../components/BrandMark"; export default function Login() { - const { login } = useAuth(); + const { login, abgelaufen } = useAuth(); const navigate = useNavigate(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); @@ -33,6 +33,11 @@ export default function Login() {

Lebensmittel-Lagerverwaltung

+ {abgelaufen && !error && ( +
+ Deine Sitzung ist abgelaufen – bitte melde dich neu an. +
+ )} {error && (
{error}
)}