const express = require('express'); const path = require('path'); const cors = require('cors'); const sessionStore = require('./services/sessionStore'); const credentialStore = require('./services/credentialStore'); const { readConfig, writeConfig } = require('./services/configStore'); const foodsharingClient = require('./services/foodsharingClient'); const { scheduleConfig } = require('./services/pickupScheduler'); 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; app.use(cors()); app.use(express.json({ limit: '1mb' })); app.use(express.static(path.join(__dirname, 'build'))); function isAdmin(profile) { if (!adminEmail || !profile?.email) { return false; } return profile.email.toLowerCase() === adminEmail; } function scheduleWithCurrentSettings(sessionId, config) { const settings = adminConfig.readSettings(); scheduleConfig(sessionId, config, settings); } function rescheduleAllSessions() { const settings = adminConfig.readSettings(); sessionStore.list().forEach((session) => { if (!session?.profile?.id) { return; } const config = readConfig(session.profile.id); scheduleConfig(session.id, config, settings); }); } function mergeStoresIntoConfig(config = [], stores = []) { const entries = Array.isArray(config) ? config : []; const map = new Map(); entries.forEach((entry) => { if (!entry || !entry.id) { return; } map.set(String(entry.id), { ...entry, id: String(entry.id) }); }); let changed = false; stores.forEach((store) => { if (!store?.id) { return; } const id = String(store.id); if (!map.has(id)) { const hideByDefault = store.hasPickupSlots === false; map.set(id, { id, label: store.name || `Store ${id}`, active: false, checkProfileId: true, onlyNotify: false, hidden: hideByDefault }); changed = true; return; } const existing = map.get(id); if (!existing.label && store.name) { existing.label = store.name; changed = true; } }); 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); if (entries.length === 0) { return; } console.log(`[RESTORE] Versuche ${entries.length} gespeicherte Anmeldung(en) zu laden...`); const schedulerSettings = adminConfig.readSettings(); for (const [profileId, credentials] of entries) { if (!credentials?.email || !credentials?.password) { continue; } try { const auth = await foodsharingClient.login(credentials.email, credentials.password); const profile = { id: String(auth.profile.id), name: auth.profile.name, email: auth.profile.email || credentials.email }; const isAdminUser = isAdmin(profile); let config = readConfig(profile.id); const stores = await foodsharingClient.fetchStores(auth.cookieHeader, profile.id, { delayBetweenRequestsMs: schedulerSettings.storePickupCheckDelayMs }); const { merged, changed } = mergeStoresIntoConfig(config, stores); if (changed) { config = merged; writeConfig(profile.id, config); } const session = sessionStore.create({ cookieHeader: auth.cookieHeader, csrfToken: auth.csrfToken, profile, credentials, isAdmin: isAdminUser }, credentials.token, ONE_YEAR_MS); credentialStore.save(profile.id, { email: credentials.email, 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) { console.error(`[RESTORE] Login fuer Profil ${profileId} fehlgeschlagen:`, error.message); } } } function requireAuth(req, res, next) { const header = req.headers.authorization || ''; const [scheme, token] = header.split(' '); if (scheme !== 'Bearer' || !token) { return res.status(401).json({ error: 'Unautorisiert' }); } const session = sessionStore.get(token); if (!session) { return res.status(401).json({ error: 'Session nicht gefunden oder abgelaufen' }); } req.session = session; next(); } function requireAdmin(req, res, next) { if (!req.session?.isAdmin) { return res.status(403).json({ error: 'Nur für Admins verfügbar' }); } next(); } app.post('/api/auth/login', async (req, res) => { const { email, password } = req.body || {}; if (!email || !password) { return res.status(400).json({ error: 'E-Mail und Passwort sind erforderlich' }); } try { const auth = await foodsharingClient.login(email, password); const profile = { id: String(auth.profile.id), name: auth.profile.name, email: auth.profile.email || email }; const isAdminUser = isAdmin(profile); const settings = adminConfig.readSettings(); let config = readConfig(profile.id); const stores = await foodsharingClient.fetchStores(auth.cookieHeader, profile.id, { delayBetweenRequestsMs: settings.storePickupCheckDelayMs }); const { merged, changed } = mergeStoresIntoConfig(config, stores); if (changed) { config = merged; writeConfig(profile.id, config); } const existingCredentials = credentialStore.get(profile.id); const existingToken = existingCredentials?.token; if (existingToken) { sessionStore.delete(existingToken); } const session = sessionStore.create({ cookieHeader: auth.cookieHeader, csrfToken: auth.csrfToken, profile, credentials: { email, password }, isAdmin: isAdminUser }, 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({ token: session.id, profile, stores, config, isAdmin: isAdminUser, adminSettings: isAdminUser ? settings : undefined }); } catch (error) { console.error('Login fehlgeschlagen:', error.message); return res.status(401).json({ error: 'Login fehlgeschlagen' }); } }); app.post('/api/auth/logout', requireAuth, (req, res) => { sessionStore.delete(req.session.id); credentialStore.remove(req.session.profile.id); res.json({ success: true }); }); app.get('/api/auth/session', requireAuth, async (req, res) => { const settings = adminConfig.readSettings(); 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, stores, isAdmin: !!req.session.isAdmin, adminSettings: req.session.isAdmin ? settings : undefined }); }); app.get('/api/profile', requireAuth, async (req, res) => { const details = await foodsharingClient.fetchProfile(req.session.cookieHeader); res.json({ profile: details || req.session.profile }); }); app.get('/api/config', requireAuth, (req, res) => { const config = readConfig(req.session.profile.id); res.json(config); }); app.post('/api/config', requireAuth, (req, res) => { if (!Array.isArray(req.body)) { return res.status(400).json({ error: 'Konfiguration muss ein Array sein' }); } writeConfig(req.session.profile.id, req.body); scheduleWithCurrentSettings(req.session.id, req.body); res.json({ success: true }); }); app.get('/api/stores', requireAuth, async (req, res) => { const settings = adminConfig.readSettings(); 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); }); app.get('/api/admin/settings', requireAuth, requireAdmin, (_req, res) => { res.json(adminConfig.readSettings()); }); app.post('/api/admin/settings', requireAuth, requireAdmin, (req, res) => { const updated = adminConfig.writeSettings(req.body || {}); rescheduleAllSessions(); res.json(updated); }); app.get('/api/health', (_req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'build', 'index.html')); }); app.listen(port, () => { console.log(`Server läuft auf Port ${port}`); }); restoreSessionsFromDisk().catch((error) => { console.error('[RESTORE] Fehler bei der Session-Wiederherstellung:', error.message); });