const express = require('express'); const cookieParser = require('cookie-parser'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const path = require('path'); const db = require('./db'); const posClient = require('./posClient'); const app = express(); const PORT = process.env.PORT || 80; app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); // Static Files app.use(express.static(path.join(__dirname, 'public'))); app.use('/Getraenkekarte.pdf', express.static(path.join(__dirname, 'Getraenkekarte.pdf'))); // Favicon dummy damit kein 502/404 geloggt wird app.get('/favicon.ico', (req, res) => res.status(204).end()); // JWT Auth Middleware function getSecret() { const row = db.prepare('SELECT value FROM config WHERE key = ?').get('jwt_secret'); return row ? row.value : 'default_secret_key'; } function authMiddleware(req, res, next) { const token = req.cookies.admin_token || req.headers.authorization?.replace('Bearer ', ''); if (!token) { return res.status(401).json({ error: 'Nicht authentifiziert' }); } try { const decoded = jwt.verify(token, getSecret()); req.user = decoded; next(); } catch (err) { res.status(401).json({ error: 'Ungültige Session' }); } } // ==================== Öffentliche API ==================== // 1. Produkte, Gruppen & Theme Settings für die Speisekarte app.get('/api/menu', (req, res) => { const groups = db.prepare('SELECT * FROM product_groups ORDER BY sort_order ASC, name ASC').all(); const products = db.prepare('SELECT id, text1, text2, langtext1, langtext2, langtext3, langtext4, group_id, preis1, unit FROM products WHERE active = 1 ORDER BY sort_order ASC, text1 ASC').all(); // Globale Einstellungen laden const configRows = db.prepare('SELECT key, value FROM config').all(); const settings = {}; for (const r of configRows) { if (r.key.startsWith('site_') || r.key.startsWith('theme_')) { settings[r.key] = r.value; } } const groupMap = {}; for (const g of groups) { groupMap[g.id] = { id: g.id, name: g.name, items: [] }; } const ungrouped = []; for (const p of products) { if (p.group_id && groupMap[p.group_id]) { groupMap[p.group_id].items.push(p); } else { ungrouped.push(p); } } const result = Object.values(groupMap).filter(g => g.items.length > 0); if (ungrouped.length > 0) { result.push({ id: 'other', name: 'Weitere Spezialitäten', items: ungrouped }); } res.json({ categories: result, allProducts: products, settings }); }); // ==================== Admin Auth ==================== // 2. Login app.post('/api/admin/login', (req, res) => { const { username, password } = req.body; const adminUserRow = db.prepare('SELECT value FROM config WHERE key = ?').get('admin_user'); const hashRow = db.prepare('SELECT value FROM config WHERE key = ?').get('admin_password_hash'); const adminUser = adminUserRow ? adminUserRow.value : 'admin'; const hash = hashRow ? hashRow.value : ''; if (username === adminUser && bcrypt.compareSync(password, hash)) { const token = jwt.sign({ user: username }, getSecret(), { expiresIn: '30d' }); res.cookie('admin_token', token, { httpOnly: true, maxAge: 30 * 24 * 60 * 60 * 1000, sameSite: 'lax' }); return res.json({ success: true, message: 'Erfolgreich eingeloggt' }); } res.status(401).json({ error: 'Benutzername oder Passwort falsch' }); }); // 3. Logout app.post('/api/admin/logout', (req, res) => { res.clearCookie('admin_token'); res.json({ success: true }); }); // 4. Auth-Check app.get('/api/admin/check', authMiddleware, (req, res) => { res.json({ authenticated: true, user: req.user.user }); }); // ==================== Admin POS Settings, Theme & Sync ==================== // 5. Config abrufen app.get('/api/admin/config', authMiddleware, (req, res) => { const config = posClient.getConfig(); delete config.admin_password_hash; delete config.jwt_secret; res.json(config); }); // 6. Config / Theme / Texte speichern app.post('/api/admin/config', authMiddleware, (req, res) => { const updates = req.body; const upsert = db.prepare('INSERT INTO config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value'); for (const [key, val] of Object.entries(updates)) { if (key === 'new_admin_password') { if (val && val.trim().length > 0) { const newHash = bcrypt.hashSync(val.trim(), 10); upsert.run('admin_password_hash', newHash); } } else if (val !== undefined && val !== null) { upsert.run(key, String(val)); } } res.json({ success: true, message: 'Einstellungen erfolgreich gespeichert' }); }); // 7. QR-Code Setup Handshake app.post('/api/admin/qr-setup', authMiddleware, async (req, res) => { const { qrContent } = req.body; if (!qrContent) { return res.status(400).json({ error: 'Kein QR-Code Inhalt übergeben' }); } try { const result = await posClient.processQrSetup(qrContent); res.json({ success: true, message: 'ServicePass erfolgreich registriert und aktiviert!', result }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 8. Sync auslösen app.post('/api/admin/sync', authMiddleware, async (req, res) => { const { type } = req.body; // 'ERSTEINRICHTUNG' oder 'AENDERUNGEN' try { const result = await posClient.performSync(type || 'ERSTEINRICHTUNG'); res.json({ success: true, result }); } catch (err) { db.prepare('INSERT INTO sync_logs (status, message) VALUES (?, ?)').run('ERROR', err.message); res.status(500).json({ error: err.message }); } }); // 9. Logs & Produkte im Admin anzeigen app.get('/api/admin/data', authMiddleware, (req, res) => { const products = db.prepare('SELECT * FROM products ORDER BY updated_at DESC').all(); const groups = db.prepare('SELECT * FROM product_groups').all(); const logs = db.prepare('SELECT * FROM sync_logs ORDER BY timestamp DESC LIMIT 20').all(); res.json({ products, groups, logs }); }); // 10. Manueller Import (Test/Demo) app.post('/api/admin/mock-import', authMiddleware, (req, res) => { const mockData = req.body; const count = posClient.importJsonData(mockData); res.json({ success: true, imported: count }); }); // Admin Route Frontend app.get('/admin', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'admin.html')); }); // Server Start app.listen(PORT, '0.0.0.0', () => { console.log(`Server läuft auf Port ${PORT}`); });