Verbesserte

This commit is contained in:
root
2025-11-09 15:49:07 +01:00
parent 79a271b453
commit b8aceb695e
6 changed files with 345 additions and 96 deletions

196
server.js
View File

@@ -2,6 +2,7 @@ const express = require('express');
const path = require('path');
const cors = require('cors');
const { v4: uuid } = require('uuid');
const sessionStore = require('./services/sessionStore');
const credentialStore = require('./services/credentialStore');
const { readConfig, writeConfig } = require('./services/configStore');
@@ -15,6 +16,7 @@ const SIXTY_DAYS_MS = 60 * 24 * 60 * 60 * 1000;
const app = express();
const port = process.env.PORT || 3000;
const storeRefreshJobs = new Map();
app.use(cors());
app.use(express.json({ limit: '1mb' }));
@@ -83,30 +85,124 @@ function mergeStoresIntoConfig(config = [], stores = []) {
return { merged: Array.from(map.values()), changed };
}
async function loadStoresForSession(session, settings, { forceRefresh = false } = {}) {
function isStoreCacheFresh(session) {
if (!session?.storesCache?.fetchedAt) {
return false;
}
return Date.now() - session.storesCache.fetchedAt <= SIXTY_DAYS_MS;
}
function summarizeJob(job) {
if (!job) {
return null;
}
return {
id: job.id,
status: job.status,
processed: job.processed,
total: job.total,
currentStore: job.currentStore,
startedAt: job.startedAt,
finishedAt: job.finishedAt,
reason: job.reason || null,
error: job.error || null
};
}
function getStoreRefreshJob(sessionId) {
const job = storeRefreshJobs.get(sessionId);
if (!job) {
return null;
}
if (job.status === 'done' && Date.now() - (job.finishedAt || 0) > 5 * 60 * 1000) {
storeRefreshJobs.delete(sessionId);
return null;
}
return job;
}
function triggerStoreRefresh(session, { force = false, reason } = {}) {
if (!session?.id) {
return { started: false };
}
const existing = getStoreRefreshJob(session.id);
if (existing && existing.status === 'running') {
return { started: false, job: existing };
}
const cacheFresh = isStoreCacheFresh(session);
if (!force && cacheFresh) {
return { started: false, cacheFresh: true };
}
const job = {
id: uuid(),
status: 'queued',
processed: 0,
total: 0,
currentStore: null,
startedAt: null,
finishedAt: null,
reason: reason || null,
error: null
};
storeRefreshJobs.set(session.id, job);
runStoreRefreshJob(session, job).catch((error) => {
console.error('[STORE-REFRESH] Job fehlgeschlagen:', error.message);
job.status = 'error';
job.error = error.message;
job.finishedAt = Date.now();
});
return { started: true, job };
}
async function runStoreRefreshJob(session, job) {
job.status = 'running';
job.startedAt = Date.now();
const settings = adminConfig.readSettings();
const stores = await foodsharingClient.fetchStores(session.cookieHeader, session.profile.id, {
delayBetweenRequestsMs: settings.storePickupCheckDelayMs,
onStoreCheck: (store, processed, total) => {
job.processed = processed;
job.total = total;
job.currentStore = store.name || `Store ${store.id}`;
}
});
job.processed = stores.length;
job.total = stores.length;
job.currentStore = null;
sessionStore.update(session.id, {
storesCache: { data: stores, fetchedAt: Date.now() }
});
let config = readConfig(session.profile.id);
const { merged, changed } = mergeStoresIntoConfig(config, stores);
if (changed) {
config = merged;
writeConfig(session.profile.id, config);
scheduleWithCurrentSettings(session.id, config);
}
job.status = 'done';
job.finishedAt = Date.now();
}
async function loadStoresForSession(session, _settings, { forceRefresh = false, reason } = {}) {
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 cacheFresh = isStoreCacheFresh(session);
if ((forceRefresh || !cacheFresh) && session.cookieHeader) {
triggerStoreRefresh(session, { force: true, reason: reason || 'session-check' });
}
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 };
return {
stores: session.storesCache?.data || [],
refreshed: cacheFresh
};
}
async function restoreSessionsFromDisk() {
@@ -202,16 +298,7 @@ app.post('/api/auth/login', async (req, res) => {
};
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;
@@ -228,18 +315,18 @@ 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);
const refreshResult = triggerStoreRefresh(session, { force: true, reason: 'login' });
return res.json({
token: session.id,
profile,
stores,
stores: session.storesCache?.data || [],
config,
isAdmin: isAdminUser,
adminSettings: isAdminUser ? settings : undefined
adminSettings: isAdminUser ? settings : undefined,
storeRefreshJob: summarizeJob(refreshResult.job),
storesFresh: isStoreCacheFresh(session)
});
} catch (error) {
console.error('Login fehlgeschlagen:', error.message);
@@ -250,24 +337,20 @@ app.post('/api/auth/login', async (req, res) => {
app.post('/api/auth/logout', requireAuth, (req, res) => {
sessionStore.delete(req.session.id);
credentialStore.remove(req.session.profile.id);
storeRefreshJobs.delete(req.session.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);
}
}
const { stores } = await loadStoresForSession(req.session, settings, { reason: 'session-check' });
res.json({
profile: req.session.profile,
stores,
isAdmin: !!req.session.isAdmin,
adminSettings: req.session.isAdmin ? settings : undefined
adminSettings: req.session.isAdmin ? settings : undefined,
storeRefreshJob: summarizeJob(getStoreRefreshJob(req.session.id)),
storesFresh: isStoreCacheFresh(req.session)
});
});
@@ -293,16 +376,27 @@ app.post('/api/config', requireAuth, (req, res) => {
});
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);
res.json(req.session.storesCache?.data || []);
});
app.post('/api/stores/refresh', requireAuth, (req, res) => {
const force = req.body?.force !== undefined ? !!req.body.force : true;
const reason = req.body?.reason || 'manual';
const result = triggerStoreRefresh(req.session, { force, reason });
res.json({
started: !!result.started,
storesFresh: isStoreCacheFresh(req.session),
job: summarizeJob(result.job)
});
});
app.get('/api/stores/refresh/status', requireAuth, (req, res) => {
const job = getStoreRefreshJob(req.session.id);
res.json({
job: summarizeJob(job),
storesFresh: isStoreCacheFresh(req.session),
cacheUpdatedAt: req.session.storesCache?.fetchedAt || null
});
});
app.get('/api/admin/settings', requireAuth, requireAdmin, (_req, res) => {