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}
)}