const foodsharingClient = require('./foodsharingClient'); const sessionStore = require('./sessionStore'); const notificationService = require('./notificationService'); const ADMIN_SESSION_ERROR_COOLDOWN_MS = 6 * 60 * 60 * 1000; const DEFAULT_TRANSIENT_RETRY_DELAYS_MS = [1000, 3000]; const NON_RETRIABLE_TRANSIENT_LABELS = new Set(['bookSlot']); const adminSessionErrorCooldowns = new Map(); function wait(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function shouldNotifyAdminSessionError(key) { if (!key) { return false; } const now = Date.now(); const lastNotified = adminSessionErrorCooldowns.get(key) || 0; if (now - lastNotified < ADMIN_SESSION_ERROR_COOLDOWN_MS) { return false; } adminSessionErrorCooldowns.set(key, now); return true; } function isUnauthorizedError(error) { const status = error?.response?.status; if (status === 401) { return true; } if (status !== 403) { return false; } const data = error?.response?.data; const message = typeof data === 'string' ? data : typeof data?.message === 'string' ? data.message : ''; return message.toLowerCase().includes('not logged in'); } function isCsrfError(error) { const status = error?.response?.status; if (status !== 400) { return false; } const data = error?.response?.data; const message = typeof data === 'string' ? data : typeof data?.message === 'string' ? data.message : ''; return message.toLowerCase().includes('csrf'); } function isTransientNetworkError(error) { if (!error) { return false; } if (error.response) { return false; } const code = String(error.code || '').toUpperCase(); if (['ECONNRESET', 'ECONNABORTED', 'ETIMEDOUT', 'EPIPE', 'ENOTFOUND', 'EAI_AGAIN'].includes(code)) { return true; } const message = String(error.message || '').toLowerCase(); return ( message.includes('socket hang up') || message.includes('timeout') || message.includes('network error') || message.includes('connection reset') ); } async function refreshSession(session, { label } = {}) { if (!session?.credentials?.email || !session?.credentials?.password) { console.warn( `[SESSION] Session ${session?.id || 'unbekannt'} kann nicht erneuert werden – keine Zugangsdaten gespeichert.` ); return false; } try { const refreshed = await foodsharingClient.login( session.credentials.email, session.credentials.password ); sessionStore.update(session.id, { cookieHeader: refreshed.cookieHeader, csrfToken: refreshed.csrfToken, profile: { ...session.profile, ...refreshed.profile } }); console.log( `[SESSION] Session ${session.id} wurde erfolgreich erneuert${label ? ` (${label})` : ''}.` ); return true; } catch (error) { console.error( `[SESSION] Session ${session?.id || 'unbekannt'} konnte nicht erneuert werden${label ? ` (${label})` : ''}:`, error.message ); const profileId = session?.profile?.id ? String(session.profile.id) : null; const notifyKey = profileId || session?.id || null; if (shouldNotifyAdminSessionError(notifyKey)) { try { await notificationService.sendAdminSessionErrorNotification({ profileId, profileEmail: session?.credentials?.email || session?.profile?.email, profileName: session?.profile?.name, sessionId: session?.id, error: error?.message, label }); } catch (notifyError) { console.error('[NOTIFY] Admin-Session-Fehler konnte nicht gemeldet werden:', notifyError.message); } } return false; } } async function ensureSession(session) { if (!session?.profile?.id) { return false; } if (!session.cookieHeader) { return refreshSession(session, { label: 'missing-cookie' }); } return true; } async function withSessionRetry(session, action, { label, transientRetries } = {}) { if (!session) { throw new Error('Session fehlt'); } if (!session.cookieHeader && session.credentials) { const refreshed = await refreshSession(session, { label }); if (!refreshed) { throw new Error('Session konnte nicht erneuert werden'); } } const retryDelays = NON_RETRIABLE_TRANSIENT_LABELS.has(label) ? [] : DEFAULT_TRANSIENT_RETRY_DELAYS_MS.slice( 0, Number.isFinite(transientRetries) ? Math.max(0, transientRetries) : DEFAULT_TRANSIENT_RETRY_DELAYS_MS.length ); let transientAttempt = 0; while (true) { try { return await action(); } catch (error) { if (isTransientNetworkError(error) && transientAttempt < retryDelays.length) { transientAttempt += 1; const delay = retryDelays[transientAttempt - 1]; console.warn( `[SESSION] Transienter Fehler${label ? ` (${label})` : ''}: ${error.message}. ` + `Retry ${transientAttempt}/${retryDelays.length} in ${delay}ms.` ); await wait(delay); continue; } if (!isUnauthorizedError(error) && !isCsrfError(error)) { throw error; } const refreshed = await refreshSession(session, { label }); if (!refreshed) { throw error; } return action(); } } } module.exports = { ensureSession, isTransientNetworkError, refreshSession, withSessionRetry };