From 8718cf5c706b2349393a5c7af9457a36896d3e59 Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 7 Jun 2026 07:05:02 +0200 Subject: [PATCH] Enhance Authentication and Feature Usage Handling - Refactored the logout function in AuthContext to handle asynchronous logout operations, improving session management. - Updated the FeatureUsageBadge component to display error messages when feature data retrieval fails, enhancing user feedback. - Replaced lazy loading of OnboardingPage with lazyWithRetry for improved loading reliability. - Adjusted the EntitlementsContext to determine club ID using utility functions for better governance form handling. --- frontend/src/App.jsx | 14 ++++++------- frontend/src/components/FeatureUsageBadge.jsx | 18 +++++++++++++++-- frontend/src/context/AuthContext.jsx | 7 ++++++- frontend/src/context/EntitlementsContext.jsx | 7 ++++++- frontend/src/utils/lazyWithRetry.js | 20 +++++++++++++++++++ 5 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 frontend/src/utils/lazyWithRetry.js diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 38702a0..f1feb8c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,4 +1,6 @@ import React, { Suspense, lazy } from 'react' +import LoginPage from './pages/LoginPage' +import { lazyWithRetry } from './utils/lazyWithRetry' import { RouterProvider, createBrowserRouter, @@ -21,8 +23,7 @@ import ActiveClubSwitcher from './components/ActiveClubSwitcher' import InactiveMembershipBanner from './components/InactiveMembershipBanner' import './app.css' -const LoginPage = lazy(() => import('./pages/LoginPage')) -const OnboardingPage = lazy(() => import('./pages/OnboardingPage')) +const OnboardingPage = lazyWithRetry(() => import('./pages/OnboardingPage')) const VerifyPage = lazy(() => import('./pages/VerifyPage')) const Dashboard = lazy(() => import('./pages/Dashboard')) const AccountSettingsPage = lazy(() => import('./pages/AccountSettingsPage')) @@ -125,11 +126,10 @@ function Nav({ showAdminNav, onboardingOnly }) { function ProtectedLayout() { const { isAuthenticated, loading, user, logout } = useAuth() - const handleLogout = () => { - if (confirm('Wirklich abmelden?')) { - logout() - window.location.href = '/' - } + const handleLogout = async () => { + if (!confirm('Wirklich abmelden?')) return + await logout() + window.location.href = '/login' } if (loading) { diff --git a/frontend/src/components/FeatureUsageBadge.jsx b/frontend/src/components/FeatureUsageBadge.jsx index 9bc7a09..d27fdab 100644 --- a/frontend/src/components/FeatureUsageBadge.jsx +++ b/frontend/src/components/FeatureUsageBadge.jsx @@ -5,7 +5,7 @@ import { useEntitlements } from '../context/EntitlementsContext' * Unbegrenzt (limit null) → nichts rendern. */ export default function FeatureUsageBadge({ featureId = 'ai_calls', label = 'KI-Kontingent' }) { - const { entitlements, loading, getFeature } = useEntitlements() + const { entitlements, loading, error, getFeature } = useEntitlements() const feat = getFeature(featureId) if (loading && !feat) { @@ -16,9 +16,23 @@ export default function FeatureUsageBadge({ featureId = 'ai_calls', label = 'KI- ) } - if (!feat) return null + if (!feat) { + if (error) { + return ( + + {label}: — + + ) + } + return null + } const { used = 0, limit, remaining, allowed } = feat + // limit === 0 (z. B. Free-Plan ai_calls) anzeigen; nur echtes Unbegrenzt (null) ausblenden if (limit == null) return null const tone = !allowed || remaining === 0 ? 'var(--danger)' : 'var(--text2)' diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index 50a877e..d770977 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -122,7 +122,12 @@ export function AuthProvider({ children }) { setUser(payload) }, []) - const logout = useCallback(() => { + const logout = useCallback(async () => { + try { + await api.logout() + } catch { + /* Session lokal trotzdem beenden */ + } setUser(null) localStorage.removeItem('authToken') localStorage.removeItem(ACTIVE_CLUB_STORAGE_KEY) diff --git a/frontend/src/context/EntitlementsContext.jsx b/frontend/src/context/EntitlementsContext.jsx index 21241ee..449c944 100644 --- a/frontend/src/context/EntitlementsContext.jsx +++ b/frontend/src/context/EntitlementsContext.jsx @@ -1,5 +1,9 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { getMeEntitlements } from '../utils/api' +import { + getDefaultClubIdForGovernanceForms, + getResolvedActiveClubIdForUi, +} from '../utils/activeClub' import { useAuth } from './AuthContext' const EntitlementsContext = createContext(null) @@ -10,7 +14,8 @@ export function EntitlementsProvider({ children }) { const [loading, setLoading] = useState(false) const [error, setError] = useState(null) - const clubId = user?.effective_club_id ?? null + const clubId = + getResolvedActiveClubIdForUi(user) ?? getDefaultClubIdForGovernanceForms(user) const refreshEntitlements = useCallback(async () => { if (!isAuthenticated) { diff --git a/frontend/src/utils/lazyWithRetry.js b/frontend/src/utils/lazyWithRetry.js new file mode 100644 index 0000000..ca33529 --- /dev/null +++ b/frontend/src/utils/lazyWithRetry.js @@ -0,0 +1,20 @@ +import { lazy } from 'react' + +/** + * Lazy-Import mit Reload bei fehlendem Chunk (nach Deploy alte Hashes im Browser-Cache). + */ +export function lazyWithRetry(importFn) { + return lazy(() => + importFn().catch((err) => { + const key = 'sj_chunk_reload' + const reloaded = sessionStorage.getItem(key) + if (!reloaded) { + sessionStorage.setItem(key, '1') + window.location.reload() + return new Promise(() => {}) + } + sessionStorage.removeItem(key) + throw err + }), + ) +}