Files
Pickup-Config/server.js
2025-11-09 13:50:17 +01:00

291 lines
8.6 KiB
JavaScript

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 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)) {
map.set(id, {
id,
label: store.name || `Store ${id}`,
active: false,
checkProfileId: true,
onlyNotify: false,
hidden: false
});
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 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);
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
});
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);
let config = readConfig(profile.id);
const stores = await foodsharingClient.fetchStores(auth.cookieHeader, profile.id);
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 });
const settings = adminConfig.readSettings();
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 stores = await foodsharingClient.fetchStores(req.session.cookieHeader, req.session.profile.id);
let config = readConfig(req.session.profile.id);
const { merged, changed } = mergeStoresIntoConfig(config, stores);
if (changed) {
config = merged;
writeConfig(req.session.profile.id, config);
}
res.json({
profile: req.session.profile,
stores,
isAdmin: !!req.session.isAdmin,
adminSettings: req.session.isAdmin ? adminConfig.readSettings() : 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 stores = await foodsharingClient.fetchStores(req.session.cookieHeader, req.session.profile.id);
let config = readConfig(req.session.profile.id);
const { merged, changed } = mergeStoresIntoConfig(config, stores);
if (changed) {
config = merged;
writeConfig(req.session.profile.id, config);
}
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);
});