aktueller stand
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const requestLogStore = require('./requestLogStore');
|
const requestLogStore = require('./requestLogStore');
|
||||||
|
const sessionStore = require('./sessionStore');
|
||||||
|
|
||||||
const BASE_URL = 'https://foodsharing.de';
|
const BASE_URL = 'https://foodsharing.de';
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ client.interceptors.response.use(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[REQUEST-LOG] Outgoing-Log fehlgeschlagen:', error.message);
|
console.warn('[REQUEST-LOG] Outgoing-Log fehlgeschlagen:', error.message);
|
||||||
}
|
}
|
||||||
|
updateSessionCookiesFromResponse(response);
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@@ -60,6 +62,9 @@ client.interceptors.response.use(
|
|||||||
} catch (logError) {
|
} catch (logError) {
|
||||||
console.warn('[REQUEST-LOG] Outgoing-Error-Log fehlgeschlagen:', logError.message);
|
console.warn('[REQUEST-LOG] Outgoing-Error-Log fehlgeschlagen:', logError.message);
|
||||||
}
|
}
|
||||||
|
if (error?.response) {
|
||||||
|
updateSessionCookiesFromResponse(error.response);
|
||||||
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -158,12 +163,75 @@ function buildRequestConfig({ cookieHeader, csrfToken, context, params } = {}) {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getCurrentUserDetails(cookieHeader, context) {
|
function mergeCookieHeaders(existingHeader = '', setCookies = []) {
|
||||||
|
const cookieMap = new Map();
|
||||||
|
if (existingHeader) {
|
||||||
|
existingHeader.split(';').forEach((part) => {
|
||||||
|
const trimmed = part.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [name, ...valueParts] = trimmed.split('=');
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cookieMap.set(name, valueParts.join('='));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(setCookies)) {
|
||||||
|
setCookies.forEach((cookie) => {
|
||||||
|
if (!cookie || typeof cookie !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [nameValue] = cookie.split(';');
|
||||||
|
if (!nameValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [name, ...valueParts] = nameValue.split('=');
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cookieMap.set(name, valueParts.join('='));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(cookieMap.entries())
|
||||||
|
.map(([name, value]) => `${name}=${value}`)
|
||||||
|
.join('; ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSessionCookiesFromResponse(response) {
|
||||||
|
const setCookies = response?.headers?.['set-cookie'];
|
||||||
|
if (!Array.isArray(setCookies) || setCookies.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const metadata = response?.config?.metadata || {};
|
||||||
|
const sessionId = metadata.sessionId;
|
||||||
|
if (!sessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const session = sessionStore.get(sessionId);
|
||||||
|
if (!session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mergedCookieHeader = mergeCookieHeaders(session.cookieHeader, setCookies);
|
||||||
|
if (!mergedCookieHeader) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const csrfToken = extractCsrfTokenFromCookieHeader(mergedCookieHeader) || session.csrfToken || null;
|
||||||
|
sessionStore.update(sessionId, {
|
||||||
|
cookieHeader: mergedCookieHeader,
|
||||||
|
csrfToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCurrentUserDetails(cookieHeader, context, options = {}) {
|
||||||
const response = await client.get(
|
const response = await client.get(
|
||||||
'/api/user/current/details',
|
'/api/user/current/details',
|
||||||
buildRequestConfig({ cookieHeader, context })
|
buildRequestConfig({ cookieHeader, context })
|
||||||
);
|
);
|
||||||
return response.data;
|
return options.raw ? response : response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function login(email, password) {
|
async function login(email, password) {
|
||||||
@@ -185,8 +253,14 @@ async function login(email, password) {
|
|||||||
const response = await client.post('/api/user/login', payload, { headers });
|
const response = await client.post('/api/user/login', payload, { headers });
|
||||||
const cookies = response.headers['set-cookie'] || [];
|
const cookies = response.headers['set-cookie'] || [];
|
||||||
const csrfToken = extractCsrfToken(cookies);
|
const csrfToken = extractCsrfToken(cookies);
|
||||||
const cookieHeader = serializeCookies(cookies);
|
let cookieHeader = serializeCookies(cookies);
|
||||||
const details = await getCurrentUserDetails(cookieHeader);
|
const detailsResponse = await getCurrentUserDetails(cookieHeader, null, { raw: true });
|
||||||
|
const detailsCookies = detailsResponse?.headers?.['set-cookie'] || [];
|
||||||
|
if (detailsCookies.length > 0) {
|
||||||
|
cookieHeader = mergeCookieHeaders(cookieHeader, detailsCookies);
|
||||||
|
}
|
||||||
|
const updatedCsrf = extractCsrfTokenFromCookieHeader(cookieHeader) || csrfToken;
|
||||||
|
const details = detailsResponse?.data;
|
||||||
if (!details?.id) {
|
if (!details?.id) {
|
||||||
throw new Error('Profil-ID konnte nicht ermittelt werden.');
|
throw new Error('Profil-ID konnte nicht ermittelt werden.');
|
||||||
}
|
}
|
||||||
@@ -200,7 +274,7 @@ async function login(email, password) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
csrfToken,
|
csrfToken: updatedCsrf,
|
||||||
cookieHeader,
|
cookieHeader,
|
||||||
profile: {
|
profile: {
|
||||||
id: String(details.id),
|
id: String(details.id),
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ function wait(ms) {
|
|||||||
|
|
||||||
const DEFAULT_STORE_WATCH_STATUS_MAX_AGE_MINUTES = 120;
|
const DEFAULT_STORE_WATCH_STATUS_MAX_AGE_MINUTES = 120;
|
||||||
const storeWatchInFlight = new Map();
|
const storeWatchInFlight = new Map();
|
||||||
|
const pickupCheckInFlight = new Map();
|
||||||
|
const pickupCheckLastRun = new Map();
|
||||||
|
const PICKUP_CHECK_DEDUP_MS = 30 * 1000;
|
||||||
|
|
||||||
async function fetchSharedStoreStatus(session, storeId, { forceRefresh = false, maxAgeMs } = {}) {
|
async function fetchSharedStoreStatus(session, storeId, { forceRefresh = false, maxAgeMs } = {}) {
|
||||||
if (!storeId) {
|
if (!storeId) {
|
||||||
@@ -216,6 +219,15 @@ function persistEntryDeactivation(profileId, entryId, options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isEntryActiveInConfig(profileId, entryId) {
|
||||||
|
if (!profileId || !entryId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const config = readConfig(profileId);
|
||||||
|
const entry = config.find((item) => String(item?.id) === String(entryId));
|
||||||
|
return !!entry && entry.active !== false;
|
||||||
|
}
|
||||||
|
|
||||||
function toDateValue(input) {
|
function toDateValue(input) {
|
||||||
if (!input) {
|
if (!input) {
|
||||||
return null;
|
return null;
|
||||||
@@ -434,18 +446,34 @@ async function checkEntry(sessionId, entry, settings) {
|
|||||||
if (!session) {
|
if (!session) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const dedupKey = `${sessionId}:${entry?.id ?? 'unknown'}`;
|
||||||
|
const lastRun = pickupCheckLastRun.get(dedupKey);
|
||||||
|
if (lastRun && Date.now() - lastRun < PICKUP_CHECK_DEDUP_MS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pickupCheckLastRun.set(dedupKey, Date.now());
|
||||||
|
const inFlightKey = `${sessionId}:${entry?.id ?? 'unknown'}`;
|
||||||
|
if (pickupCheckInFlight.has(inFlightKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pickupCheckInFlight.set(inFlightKey, Date.now());
|
||||||
|
|
||||||
|
try {
|
||||||
if (desiredWindowExpired(entry)) {
|
if (desiredWindowExpired(entry)) {
|
||||||
await handleExpiredDesiredWindow(session, entry);
|
await handleExpiredDesiredWindow(session, entry);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isEntryActiveInConfig(session.profile.id, entry.id)) {
|
||||||
|
deactivateEntryInMemory(entry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const ready = await ensureSession(session);
|
const ready = await ensureSession(session);
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const pickups = await withSessionRetry(
|
const pickups = await withSessionRetry(
|
||||||
session,
|
session,
|
||||||
() => foodsharingClient.fetchPickups(entry.id, session.cookieHeader, session),
|
() => foodsharingClient.fetchPickups(entry.id, session.cookieHeader, session),
|
||||||
@@ -458,21 +486,30 @@ async function checkEntry(sessionId, entry, settings) {
|
|||||||
|
|
||||||
pickups.forEach((pickup) => {
|
pickups.forEach((pickup) => {
|
||||||
const pickupDate = new Date(pickup.date);
|
const pickupDate = new Date(pickup.date);
|
||||||
|
if (
|
||||||
|
entry.checkProfileId &&
|
||||||
|
pickup.occupiedSlots?.some((slot) => String(slot.profile?.id) === String(session.profile.id))
|
||||||
|
) {
|
||||||
|
hasProfileId = true;
|
||||||
|
}
|
||||||
if (!matchesDesiredDate(pickupDate, entry.desiredDate, entry.desiredDateRange)) {
|
if (!matchesDesiredDate(pickupDate, entry.desiredDate, entry.desiredDateRange)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!matchesDesiredWeekday(pickupDate, desiredWeekday)) {
|
if (!matchesDesiredWeekday(pickupDate, desiredWeekday)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (entry.checkProfileId && pickup.occupiedSlots?.some((slot) => slot.profile?.id === session.profile.id)) {
|
|
||||||
hasProfileId = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pickup.isAvailable && !availablePickup) {
|
if (pickup.isAvailable && !availablePickup) {
|
||||||
availablePickup = pickup;
|
availablePickup = pickup;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (entry.checkProfileId && hasProfileId) {
|
||||||
|
console.log(
|
||||||
|
`[INFO] Profil bereits in einem Slot für ${entry.label || entry.id} eingetragen – überspringe Buchung.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!availablePickup) {
|
if (!availablePickup) {
|
||||||
console.log(
|
console.log(
|
||||||
`[INFO] Kein freier Slot für ${entry.label || entry.id} in dieser Runde gefunden. Profil bereits eingetragen: ${
|
`[INFO] Kein freier Slot für ${entry.label || entry.id} in dieser Runde gefunden. Profil bereits eingetragen: ${
|
||||||
@@ -492,6 +529,8 @@ async function checkEntry(sessionId, entry, settings) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[ERROR] Pickup-Check für Store ${entry.id} fehlgeschlagen:`, error.message);
|
console.error(`[ERROR] Pickup-Check für Store ${entry.id} fehlgeschlagen:`, error.message);
|
||||||
|
} finally {
|
||||||
|
pickupCheckInFlight.delete(inFlightKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,10 +676,6 @@ function scheduleEntry(sessionId, entry, settings) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
sessionStore.attachJob(sessionId, job);
|
sessionStore.attachJob(sessionId, job);
|
||||||
setTimeout(
|
|
||||||
() => checkEntry(sessionId, entry, settings),
|
|
||||||
randomDelayMs(settings.initialDelayMinSeconds, settings.initialDelayMaxSeconds)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleConfig(sessionId, config, settings) {
|
function scheduleConfig(sessionId, config, settings) {
|
||||||
|
|||||||
@@ -6,6 +6,21 @@ function isUnauthorizedError(error) {
|
|||||||
return status === 401 || status === 403;
|
return status === 401 || status === 403;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshSession(session, { label } = {}) {
|
async function refreshSession(session, { label } = {}) {
|
||||||
if (!session?.credentials?.email || !session?.credentials?.password) {
|
if (!session?.credentials?.email || !session?.credentials?.password) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -62,7 +77,7 @@ async function withSessionRetry(session, action, { label } = {}) {
|
|||||||
try {
|
try {
|
||||||
return await action();
|
return await action();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!isUnauthorizedError(error)) {
|
if (!isUnauthorizedError(error) && !isCsrfError(error)) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
const refreshed = await refreshSession(session, { label });
|
const refreshed = await refreshSession(session, { label });
|
||||||
|
|||||||
Reference in New Issue
Block a user