diff --git a/server.js b/server.js index e84a8b7..5808864 100644 --- a/server.js +++ b/server.js @@ -11,6 +11,7 @@ const adminConfig = require('./services/adminConfig'); const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000; const adminEmail = (process.env.ADMIN_EMAIL || '').toLowerCase(); +const SIXTY_DAYS_MS = 60 * 24 * 60 * 60 * 1000; const app = express(); const port = process.env.PORT || 3000; @@ -82,6 +83,32 @@ function mergeStoresIntoConfig(config = [], stores = []) { return { merged: Array.from(map.values()), changed }; } +async function loadStoresForSession(session, settings, { forceRefresh = false } = {}) { + if (!session?.profile?.id) { + return { stores: [], refreshed: false }; + } + + const cache = session.storesCache; + const now = Date.now(); + const isCacheValid = + cache && + Array.isArray(cache.data) && + Number.isFinite(cache.fetchedAt) && + now - cache.fetchedAt <= SIXTY_DAYS_MS; + + if (isCacheValid && !forceRefresh) { + return { stores: cache.data, refreshed: false }; + } + + const stores = await foodsharingClient.fetchStores(session.cookieHeader, session.profile.id, { + delayBetweenRequestsMs: settings.storePickupCheckDelayMs + }); + sessionStore.update(session.id, { + storesCache: { data: stores, fetchedAt: now } + }); + return { stores, refreshed: true }; +} + async function restoreSessionsFromDisk() { const saved = credentialStore.loadAll(); const entries = Object.entries(saved); @@ -126,6 +153,9 @@ async function restoreSessionsFromDisk() { password: credentials.password, token: session.id }); + sessionStore.update(session.id, { + storesCache: { data: stores, fetchedAt: Date.now() } + }); scheduleConfig(session.id, config, schedulerSettings); console.log(`[RESTORE] Session fuer Profil ${profile.id} (${profile.name}) reaktiviert.`); } catch (error) { @@ -198,6 +228,9 @@ app.post('/api/auth/login', async (req, res) => { }, existingToken, ONE_YEAR_MS); credentialStore.save(profile.id, { email, password, token: session.id }); + sessionStore.update(session.id, { + storesCache: { data: stores, fetchedAt: Date.now() } + }); scheduleConfig(session.id, config, settings); return res.json({ @@ -222,14 +255,13 @@ app.post('/api/auth/logout', requireAuth, (req, res) => { app.get('/api/auth/session', requireAuth, async (req, res) => { const settings = adminConfig.readSettings(); - const stores = await foodsharingClient.fetchStores(req.session.cookieHeader, req.session.profile.id, { - delayBetweenRequestsMs: settings.storePickupCheckDelayMs - }); - let config = readConfig(req.session.profile.id); - const { merged, changed } = mergeStoresIntoConfig(config, stores); - if (changed) { - config = merged; - writeConfig(req.session.profile.id, config); + const { stores, refreshed } = await loadStoresForSession(req.session, settings); + if (refreshed) { + let config = readConfig(req.session.profile.id); + const { merged, changed } = mergeStoresIntoConfig(config, stores); + if (changed) { + writeConfig(req.session.profile.id, merged); + } } res.json({ profile: req.session.profile, @@ -262,14 +294,13 @@ app.post('/api/config', requireAuth, (req, res) => { app.get('/api/stores', requireAuth, async (req, res) => { const settings = adminConfig.readSettings(); - const stores = await foodsharingClient.fetchStores(req.session.cookieHeader, req.session.profile.id, { - delayBetweenRequestsMs: settings.storePickupCheckDelayMs - }); - let config = readConfig(req.session.profile.id); - const { merged, changed } = mergeStoresIntoConfig(config, stores); - if (changed) { - config = merged; - writeConfig(req.session.profile.id, config); + const { stores, refreshed } = await loadStoresForSession(req.session, settings, { forceRefresh: true }); + if (refreshed) { + let config = readConfig(req.session.profile.id); + const { merged, changed } = mergeStoresIntoConfig(config, stores); + if (changed) { + writeConfig(req.session.profile.id, merged); + } } res.json(stores); }); diff --git a/services/foodsharingClient.js b/services/foodsharingClient.js index 6aaadb3..9e74d42 100644 --- a/services/foodsharingClient.js +++ b/services/foodsharingClient.js @@ -168,7 +168,13 @@ async function annotateStoresWithPickupSlots(stores, cookieHeader, delayBetweenR const pickups = await fetchPickups(store.id, cookieHeader); hasPickupSlots = Array.isArray(pickups) && pickups.length > 0; } catch (error) { - console.warn(`Pickups für Store ${store.id} konnten nicht geprüft werden:`, error.message); + const status = error?.response?.status; + if (status === 403) { + hasPickupSlots = false; + console.warn(`Pickups für Store ${store.id} nicht erlaubt (403) – wird als ohne Slots behandelt.`); + } else { + console.warn(`Pickups für Store ${store.id} konnten nicht geprüft werden:`, error.message); + } } annotated.push({ ...store, hasPickupSlots }); } diff --git a/src/App.js b/src/App.js index 3697cbb..ebebd42 100644 --- a/src/App.js +++ b/src/App.js @@ -101,15 +101,13 @@ function App() { }, [resetSessionState]); const bootstrapSession = useCallback( - async (token, { withProgress = false } = {}) => { + async (token, { progress } = {}) => { if (!token) { return; } setLoading(true); setError(''); - if (withProgress) { - startSyncProgress('Session wird aufgebaut...', 10, true); - } + progress?.update?.('Session wird aufgebaut...', 20); try { const response = await fetch('/api/auth/session', { headers: { Authorization: `Bearer ${token}` } @@ -123,9 +121,7 @@ function App() { } const data = await response.json(); setSession({ token, profile: data.profile, isAdmin: data.isAdmin }); - if (withProgress) { - updateSyncProgress('Betriebe werden geprüft...', 45); - } + progress?.update?.('Betriebe werden geprüft...', 45); setStores(Array.isArray(data.stores) ? data.stores : []); setAdminSettings(data.isAdmin ? normalizeAdminSettings(data.adminSettings) : null); @@ -140,35 +136,35 @@ function App() { throw new Error(`HTTP ${configResponse.status}`); } const configData = await configResponse.json(); - if (withProgress) { - updateSyncProgress('Konfiguration wird geladen...', 75); - } + progress?.update?.('Konfiguration wird geladen...', 75); setConfig(Array.isArray(configData) ? configData : []); - if (withProgress) { - updateSyncProgress('Synchronisierung abgeschlossen', 95); - } + progress?.update?.('Synchronisierung abgeschlossen', 95); } catch (err) { setError(`Session konnte nicht wiederhergestellt werden: ${err.message}`); } finally { setLoading(false); - if (withProgress) { - finishSyncProgress(); - } } }, - [handleUnauthorized, normalizeAdminSettings, startSyncProgress, updateSyncProgress, finishSyncProgress] + [handleUnauthorized, normalizeAdminSettings] ); useEffect(() => { try { const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY); if (storedToken) { - bootstrapSession(storedToken); + startSyncProgress('Session wird wiederhergestellt...', 5, true); + (async () => { + try { + await bootstrapSession(storedToken, { progress: { update: updateSyncProgress } }); + } finally { + finishSyncProgress(); + } + })(); } } catch (err) { console.warn('Konnte gespeicherten Token nicht lesen:', err); } - }, [bootstrapSession]); + }, [bootstrapSession, startSyncProgress, updateSyncProgress, finishSyncProgress]); const authorizedFetch = useCallback( async (url, options = {}, tokenOverride) => { @@ -231,6 +227,7 @@ function App() { setLoading(true); setError(''); setStatus(''); + startSyncProgress('Anmeldung wird geprüft...', 5, true); try { const response = await fetch('/api/auth/login', { @@ -249,12 +246,15 @@ function App() { } catch (storageError) { console.warn('Konnte Token nicht speichern:', storageError); } - await bootstrapSession(data.token, { withProgress: true }); + updateSyncProgress('Zugang bestätigt. Session wird aufgebaut...', 25); + await bootstrapSession(data.token, { progress: { update: updateSyncProgress } }); + updateSyncProgress('Login abgeschlossen', 95); setStatus('Anmeldung erfolgreich. Konfiguration geladen.'); setTimeout(() => setStatus(''), 3000); } catch (err) { setError(`Login fehlgeschlagen: ${err.message}`); } finally { + finishSyncProgress(); setLoading(false); } }; @@ -273,7 +273,7 @@ function App() { } }; - const fetchConfig = async (tokenOverride, { silent = false } = {}) => { + const fetchConfig = useCallback(async (tokenOverride, { silent = false } = {}) => { const tokenToUse = tokenOverride || session?.token; if (!tokenToUse) { return; @@ -299,9 +299,9 @@ function App() { } finally { setLoading(false); } - }; + }, [session?.token, authorizedFetch]); - const fetchStoresList = async () => { + const fetchStoresList = useCallback(async () => { if (!session?.token) { return; } @@ -314,13 +314,12 @@ function App() { } const data = await response.json(); setStores(Array.isArray(data) ? data : []); - await fetchConfig(undefined, { silent: true }); setStatus('Betriebe aktualisiert.'); setTimeout(() => setStatus(''), 3000); } catch (err) { setError(`Fehler beim Laden der Betriebe: ${err.message}`); } - }; + }, [session?.token, authorizedFetch]); const refreshStoresAndConfig = useCallback( async ({ block = false } = {}) => { @@ -661,8 +660,9 @@ function App() { if (!session?.token) { return ( -
Lade Daten...
+ <> +Lade Daten...
+