Files
getrankekarte/server.js
DanielS 1b0726deef
All checks were successful
Build and Deploy / build (push) Successful in 54s
feat: Text 2 als feste Inhaltsangabe und Langtext 1-4 als aufklappbare Details (Hover/Touch)
2026-09-22 23:36:12 +02:00

200 lines
6.4 KiB
JavaScript

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}`);
});