diff --git a/src/App.js b/src/App.js index d17e8c1..f1f5870 100644 --- a/src/App.js +++ b/src/App.js @@ -10,6 +10,7 @@ import useNotificationSettings from './hooks/useNotificationSettings'; import useConfigManager from './hooks/useConfigManager'; import useStoreSync from './hooks/useStoreSync'; import useDirtyNavigationGuard from './hooks/useDirtyNavigationGuard'; +import useSessionManager from './hooks/useSessionManager'; import NavigationTabs from './components/NavigationTabs'; import LoginView from './components/LoginView'; import DashboardView from './components/DashboardView'; @@ -20,10 +21,7 @@ import ConfirmationDialog from './components/ConfirmationDialog'; import StoreSyncOverlay from './components/StoreSyncOverlay'; import RangePickerModal from './components/RangePickerModal'; -const TOKEN_STORAGE_KEY = 'pickupConfigToken'; - function App() { - const [session, setSession] = useState(null); const [credentials, setCredentials] = useState({ email: '', password: '' }); const [stores, setStores] = useState([]); const [loading, setLoading] = useState(false); @@ -40,6 +38,10 @@ function App() { const minSelectableDate = useMemo(() => startOfDay(new Date()), []); const weekdays = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']; + const unauthorizedResetRef = useRef(() => {}); + const notifyUnauthorized = useCallback(() => { + unauthorizedResetRef.current?.(); + }, []); const normalizeConfigEntries = useCallback((entries) => { if (!Array.isArray(entries)) { @@ -117,27 +119,21 @@ function App() { }; }, []); - const handleUnauthorizedRef = useRef(() => {}); - - const authorizedFetch = useCallback( - async (url, options = {}, tokenOverride) => { - const activeToken = tokenOverride || session?.token; - if (!activeToken) { - throw new Error('Keine aktive Session'); - } - const headers = { - Authorization: `Bearer ${activeToken}`, - ...(options.headers || {}) - }; - const response = await fetch(url, { ...options, headers }); - if (response.status === 401) { - handleUnauthorizedRef.current(); - throw new Error('Nicht autorisiert'); - } - return response; - }, - [session?.token] - ); + const { + session, + authorizedFetch, + bootstrapSession, + performLogout, + handleUnauthorized, + storeToken, + getStoredToken + } = useSessionManager({ + normalizeConfigEntries, + normalizeAdminSettings, + onUnauthorized: notifyUnauthorized, + setError, + setLoading + }); const { config, @@ -185,8 +181,20 @@ function App() { onDiscard: () => setIsDirty(false) }); + const applyBootstrapResult = useCallback( + (result = {}) => { + if (!result) { + return {}; + } + setStores(Array.isArray(result.stores) ? result.stores : []); + setAdminSettings(result.adminSettings ?? null); + setConfig(Array.isArray(result.config) ? result.config : []); + return result; + }, + [setStores, setAdminSettings, setConfig] + ); + const resetSessionState = useCallback(() => { - setSession(null); setConfig([]); setStores([]); setStatus(''); @@ -195,73 +203,20 @@ function App() { setAdminSettingsLoading(false); setAvailableCollapsed(true); setInitializing(false); - }, [setConfig]); - - const handleUnauthorized = useCallback(() => { - resetSessionState(); - try { - localStorage.removeItem(TOKEN_STORAGE_KEY); - } catch (storageError) { - console.warn('Konnte Token nicht aus dem Speicher entfernen:', storageError); - } - }, [resetSessionState]); + }, [ + setConfig, + setStores, + setStatus, + setError, + setAdminSettings, + setAdminSettingsLoading, + setAvailableCollapsed, + setInitializing + ]); useEffect(() => { - handleUnauthorizedRef.current = handleUnauthorized; - }, [handleUnauthorized]); - - const bootstrapSession = useCallback( - async (token, { progress } = {}) => { - if (!token) { - return {}; - } - setLoading(true); - setError(''); - progress?.update?.('Session wird aufgebaut...', 20); - try { - const response = await fetch('/api/auth/session', { - headers: { Authorization: `Bearer ${token}` } - }); - if (response.status === 401) { - handleUnauthorized(); - return; - } - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - const data = await response.json(); - setSession({ token, profile: data.profile, isAdmin: data.isAdmin }); - progress?.update?.('Betriebe werden geprüft...', 45); - setStores(Array.isArray(data.stores) ? data.stores : []); - setAdminSettings(data.isAdmin ? normalizeAdminSettings(data.adminSettings) : null); - - const configResponse = await fetch('/api/config', { - headers: { Authorization: `Bearer ${token}` } - }); - if (configResponse.status === 401) { - handleUnauthorized(); - return; - } - if (!configResponse.ok) { - throw new Error(`HTTP ${configResponse.status}`); - } - const configData = await configResponse.json(); - progress?.update?.('Konfiguration wird geladen...', 75); - setConfig(normalizeConfigEntries(Array.isArray(configData) ? configData : [])); - progress?.update?.('Synchronisierung abgeschlossen', 95); - return { - storeRefreshJob: data.storeRefreshJob, - storesFresh: data.storesFresh - }; - } catch (err) { - setError(`Session konnte nicht wiederhergestellt werden: ${err.message}`); - } finally { - setLoading(false); - } - return {}; - }, - [handleUnauthorized, normalizeAdminSettings, normalizeConfigEntries, setConfig] - ); + unauthorizedResetRef.current = resetSessionState; + }, [resetSessionState]); const { notificationSettings, @@ -339,14 +294,12 @@ function App() { } const data = await response.json(); - try { - localStorage.setItem(TOKEN_STORAGE_KEY, data.token); - } catch (storageError) { - console.warn('Konnte Token nicht speichern:', storageError); - } + storeToken(data.token); clearInterval(ticker); updateSyncProgress('Zugang bestätigt. Session wird aufgebaut...', 45); - const bootstrapResult = await bootstrapSession(data.token, { progress: { update: updateSyncProgress } }); + const bootstrapResult = applyBootstrapResult( + await bootstrapSession(data.token, { progress: { update: updateSyncProgress } }) + ); const needsStoreSync = !bootstrapResult?.storesFresh || !!bootstrapResult?.storeRefreshJob; if (needsStoreSync) { await syncStoresWithProgress({ @@ -370,20 +323,6 @@ function App() { } }; - const performLogout = useCallback(async () => { - if (!session?.token) { - handleUnauthorized(); - return; - } - try { - await authorizedFetch('/api/auth/logout', { method: 'POST' }); - } catch (err) { - console.warn('Logout fehlgeschlagen:', err); - } finally { - handleUnauthorized(); - } - }, [session?.token, authorizedFetch, handleUnauthorized]); - const handleLogout = () => { requestNavigation('dich abzumelden', performLogout); }; @@ -417,12 +356,7 @@ function App() { let ticker; let cancelled = false; (async () => { - let storedToken = null; - try { - storedToken = localStorage.getItem(TOKEN_STORAGE_KEY); - } catch (err) { - console.warn('Konnte gespeicherten Token nicht lesen:', err); - } + const storedToken = getStoredToken(); if (!storedToken) { return; } @@ -430,7 +364,9 @@ function App() { try { startSyncProgress('Session wird wiederhergestellt...', 5, true); ticker = setInterval(() => nudgeSyncProgress('Session wird wiederhergestellt...', 1, 40), 1000); - const result = await bootstrapSession(storedToken, { progress: { update: updateSyncProgress } }); + const result = applyBootstrapResult( + await bootstrapSession(storedToken, { progress: { update: updateSyncProgress } }) + ); if (ticker) { clearInterval(ticker); ticker = null; @@ -462,12 +398,14 @@ function App() { } }; }, [ + applyBootstrapResult, bootstrapSession, - startSyncProgress, - updateSyncProgress, finishSyncProgress, + getStoredToken, nudgeSyncProgress, - syncStoresWithProgress + startSyncProgress, + syncStoresWithProgress, + updateSyncProgress ]); useEffect(() => { diff --git a/src/hooks/useSessionManager.js b/src/hooks/useSessionManager.js new file mode 100644 index 0000000..78237f1 --- /dev/null +++ b/src/hooks/useSessionManager.js @@ -0,0 +1,158 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +const TOKEN_STORAGE_KEY = 'pickupConfigToken'; + +const useSessionManager = ({ + normalizeConfigEntries, + normalizeAdminSettings, + onUnauthorized, + setError, + setLoading +}) => { + const [session, setSession] = useState(null); + const unauthorizedRef = useRef(onUnauthorized || (() => {})); + + useEffect(() => { + unauthorizedRef.current = onUnauthorized || (() => {}); + }, [onUnauthorized]); + + const handleUnauthorized = useCallback(() => { + setSession(null); + try { + localStorage.removeItem(TOKEN_STORAGE_KEY); + } catch (storageError) { + console.warn('Konnte Token nicht aus dem Speicher entfernen:', storageError); + } + unauthorizedRef.current(); + }, []); + + const handleUnauthorizedRef = useRef(() => {}); + useEffect(() => { + handleUnauthorizedRef.current = handleUnauthorized; + }, [handleUnauthorized]); + + const authorizedFetch = useCallback( + async (url, options = {}, tokenOverride) => { + const activeToken = tokenOverride || session?.token; + if (!activeToken) { + throw new Error('Keine aktive Session'); + } + const headers = { + Authorization: `Bearer ${activeToken}`, + ...(options.headers || {}) + }; + const response = await fetch(url, { ...options, headers }); + if (response.status === 401) { + handleUnauthorizedRef.current(); + throw new Error('Nicht autorisiert'); + } + return response; + }, + [session?.token] + ); + + const storeToken = useCallback((token) => { + if (!token) { + return; + } + try { + localStorage.setItem(TOKEN_STORAGE_KEY, token); + } catch (storageError) { + console.warn('Konnte Token nicht speichern:', storageError); + } + }, []); + + const getStoredToken = useCallback(() => { + try { + return localStorage.getItem(TOKEN_STORAGE_KEY); + } catch (storageError) { + console.warn('Konnte gespeicherten Token nicht lesen:', storageError); + return null; + } + }, []); + + const bootstrapSession = useCallback( + async (token, { progress } = {}) => { + if (!token) { + return {}; + } + setLoading(true); + setError(''); + progress?.update?.('Session wird aufgebaut...', 20); + try { + const response = await fetch('/api/auth/session', { + headers: { Authorization: `Bearer ${token}` } + }); + if (response.status === 401) { + handleUnauthorized(); + return {}; + } + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const data = await response.json(); + const sessionData = { token, profile: data.profile, isAdmin: data.isAdmin }; + setSession(sessionData); + progress?.update?.('Betriebe werden geprüft...', 45); + const stores = Array.isArray(data.stores) ? data.stores : []; + const adminSettings = data.isAdmin ? normalizeAdminSettings(data.adminSettings) : null; + + const configResponse = await fetch('/api/config', { + headers: { Authorization: `Bearer ${token}` } + }); + if (configResponse.status === 401) { + handleUnauthorized(); + return {}; + } + if (!configResponse.ok) { + throw new Error(`HTTP ${configResponse.status}`); + } + const configData = await configResponse.json(); + progress?.update?.('Konfiguration wird geladen...', 75); + const config = normalizeConfigEntries(Array.isArray(configData) ? configData : []); + progress?.update?.('Synchronisierung abgeschlossen', 95); + + return { + session: sessionData, + stores, + adminSettings, + config, + storeRefreshJob: data.storeRefreshJob, + storesFresh: data.storesFresh + }; + } catch (error) { + setError(`Session konnte nicht wiederhergestellt werden: ${error.message}`); + return {}; + } finally { + setLoading(false); + } + }, + [normalizeAdminSettings, normalizeConfigEntries, handleUnauthorized, setError, setLoading] + ); + + const performLogout = useCallback(async () => { + if (!session?.token) { + handleUnauthorized(); + return; + } + try { + await authorizedFetch('/api/auth/logout', { method: 'POST' }); + } catch (err) { + console.warn('Logout fehlgeschlagen:', err); + } finally { + handleUnauthorized(); + } + }, [session?.token, authorizedFetch, handleUnauthorized]); + + return { + session, + authorizedFetch, + bootstrapSession, + performLogout, + handleUnauthorized, + storeToken, + getStoredToken + }; +}; + +export default useSessionManager;