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