aktueller Stand
This commit is contained in:
348
server.js
348
server.js
@@ -1,126 +1,290 @@
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const cors = require('cors');
|
||||
const fs = require('fs');
|
||||
const bodyParser = require('body-parser');
|
||||
const mqtt = require('mqtt');
|
||||
|
||||
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;
|
||||
const mqttBroker = process.env.MQTT_BROKER || 'mqtt://192.168.1.100:1883';
|
||||
const mqttTopic = process.env.MQTT_TOPIC || 'iobroker/pickupCheck/config';
|
||||
const mqttUser = process.env.MQTT_USER || 'iobroker';
|
||||
const mqttPassword = process.env.MQTT_PASSWORD || 'password';
|
||||
const configPath = './config/pickup-config.json';
|
||||
|
||||
// Logger mit Timestamp
|
||||
function logWithTimestamp(...args) {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.log(`[${timestamp}]`, ...args);
|
||||
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 errorWithTimestamp(...args) {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.error(`[${timestamp}]`, ...args);
|
||||
function scheduleWithCurrentSettings(sessionId, config) {
|
||||
const settings = adminConfig.readSettings();
|
||||
scheduleConfig(sessionId, config, settings);
|
||||
}
|
||||
|
||||
// MQTT-Client mit Authentifizierung initialisieren
|
||||
const mqttClient = mqtt.connect(mqttBroker, {
|
||||
clientId: 'pickup-config-web-' + Math.random().toString(16).substring(2, 8),
|
||||
clean: true,
|
||||
username: mqttUser,
|
||||
password: mqttPassword
|
||||
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' });
|
||||
}
|
||||
});
|
||||
|
||||
// MQTT-Events
|
||||
mqttClient.on('connect', () => {
|
||||
logWithTimestamp('Verbunden mit MQTT-Broker:', mqttBroker);
|
||||
app.post('/api/auth/logout', requireAuth, (req, res) => {
|
||||
sessionStore.delete(req.session.id);
|
||||
credentialStore.remove(req.session.profile.id);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
mqttClient.subscribe(mqttTopic, (err) => {
|
||||
if (!err) {
|
||||
logWithTimestamp('Abonniert auf Topic:', mqttTopic);
|
||||
mqttClient.publish(mqttTopic + '/get', '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
|
||||
});
|
||||
});
|
||||
|
||||
mqttClient.on('error', (error) => {
|
||||
errorWithTimestamp('MQTT-Fehler:', error);
|
||||
app.get('/api/profile', requireAuth, async (req, res) => {
|
||||
const details = await foodsharingClient.fetchProfile(req.session.cookieHeader);
|
||||
res.json({
|
||||
profile: details || req.session.profile
|
||||
});
|
||||
});
|
||||
|
||||
mqttClient.on('message', (topic, message) => {
|
||||
logWithTimestamp('Nachricht erhalten auf Topic:', topic);
|
||||
app.get('/api/config', requireAuth, (req, res) => {
|
||||
const config = readConfig(req.session.profile.id);
|
||||
res.json(config);
|
||||
});
|
||||
|
||||
if (topic === mqttTopic) {
|
||||
try {
|
||||
const config = JSON.parse(message.toString());
|
||||
logWithTimestamp('Konfiguration vom MQTT-Broker erhalten');
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||
} catch (error) {
|
||||
errorWithTimestamp('Fehler beim Verarbeiten der MQTT-Nachricht:', error);
|
||||
}
|
||||
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 });
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json());
|
||||
app.use(express.static(path.join(__dirname, 'build')));
|
||||
|
||||
// Sicherstellen, dass Konfigurationsordner existiert
|
||||
const configDir = path.dirname(configPath);
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Initiale Konfigurationsdatei erstellen, falls nicht vorhanden
|
||||
if (!fs.existsSync(configPath)) {
|
||||
const initialConfig = [
|
||||
{ id: "63448", active: false, checkProfileId: true, onlyNotify: true, label: "Penny Baden-Oos" },
|
||||
{ id: "44975", active: false, checkProfileId: true, onlyNotify: false, label: "Aldi Kuppenheim", desiredWeekday: "Samstag" },
|
||||
{ id: "44972", active: false, checkProfileId: true, onlyNotify: false, label: "Aldi Biblisweg", desiredWeekday: "Dienstag" },
|
||||
{ id: "44975", active: false, checkProfileId: true, onlyNotify: false, label: "Aldi Kuppenheim", desiredDate: "2025-05-18" },
|
||||
{ id: "33875", active: false, checkProfileId: true, onlyNotify: false, label: "Cap Markt", desiredWeekday: "Donnerstag" },
|
||||
{ id: "42322", active: false, checkProfileId: false, onlyNotify: false, label: "Edeka Haueneberstein" },
|
||||
{ id: "51450", active: false, checkProfileId: true, onlyNotify: false, label: "Hornbach Grünwinkel" }
|
||||
];
|
||||
|
||||
fs.writeFileSync(configPath, JSON.stringify(initialConfig, null, 2));
|
||||
mqttClient.publish(mqttTopic, JSON.stringify(initialConfig));
|
||||
logWithTimestamp('Initiale Konfiguration erstellt und an MQTT gesendet');
|
||||
}
|
||||
|
||||
// API: Konfiguration abrufen
|
||||
app.get('/api/iobroker/pickup-config', (req, res) => {
|
||||
try {
|
||||
const configData = fs.readFileSync(configPath, 'utf8');
|
||||
res.json(JSON.parse(configData));
|
||||
} catch (error) {
|
||||
errorWithTimestamp('Error reading configuration:', error);
|
||||
res.status(500).json({ error: 'Failed to read configuration' });
|
||||
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);
|
||||
});
|
||||
|
||||
// API: Konfiguration speichern
|
||||
app.post('/api/iobroker/pickup-config', (req, res) => {
|
||||
try {
|
||||
const newConfig = req.body;
|
||||
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
|
||||
mqttClient.publish(mqttTopic, JSON.stringify(newConfig));
|
||||
logWithTimestamp('Konfiguration über MQTT gesendet');
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
errorWithTimestamp('Error saving configuration:', error);
|
||||
res.status(500).json({ error: 'Failed to save configuration' });
|
||||
}
|
||||
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() });
|
||||
});
|
||||
|
||||
// React-App ausliefern
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'build', 'index.html'));
|
||||
});
|
||||
|
||||
// Server starten
|
||||
app.listen(port, () => {
|
||||
logWithTimestamp(`Server läuft auf Port ${port}`);
|
||||
console.log(`Server läuft auf Port ${port}`);
|
||||
});
|
||||
|
||||
restoreSessionsFromDisk().catch((error) => {
|
||||
console.error('[RESTORE] Fehler bei der Session-Wiederherstellung:', error.message);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user