285 lines
7.9 KiB
JavaScript
285 lines
7.9 KiB
JavaScript
import express from "express";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import { fileURLToPath } from "url";
|
|
import { CONFIG, DISCORD_WEBHOOK_URL, PUBLIC_BASE_URL, PORT } from "./config.js";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
const app = express();
|
|
|
|
// Trust proxy - CRITICAL for reverse proxy setup
|
|
app.set('trust proxy', true);
|
|
|
|
app.use(express.json());
|
|
|
|
// Logging middleware
|
|
app.use((req, res, next) => {
|
|
console.log(`${req.method} ${req.url} - ${req.headers['x-forwarded-proto'] || 'http'}`);
|
|
next();
|
|
});
|
|
|
|
// CRITICAL: Serve static files with proper headers
|
|
app.use(express.static(path.join(__dirname, "public"), {
|
|
setHeaders: (res, filepath) => {
|
|
if (filepath.endsWith('.js')) {
|
|
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
}
|
|
if (filepath.endsWith('.json')) {
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
}
|
|
if (filepath.endsWith('service-worker.js')) {
|
|
res.setHeader('Service-Worker-Allowed', '/');
|
|
res.setHeader('Cache-Control', 'no-cache');
|
|
}
|
|
}
|
|
}));
|
|
|
|
const stateFile = path.join(__dirname, "state.json");
|
|
|
|
// --------------------
|
|
// State Handling - MIT PERSISTENT lastReason
|
|
// --------------------
|
|
function getState() {
|
|
if (!fs.existsSync(stateFile)) {
|
|
const initialState = {
|
|
current: "A",
|
|
lastReason: null
|
|
};
|
|
fs.writeFileSync(
|
|
stateFile,
|
|
JSON.stringify(initialState, null, 2)
|
|
);
|
|
console.log("✅ Neue state.json erstellt");
|
|
return initialState;
|
|
}
|
|
|
|
try {
|
|
const state = JSON.parse(fs.readFileSync(stateFile, "utf-8"));
|
|
// Ensure lastReason exists in state
|
|
if (!state.hasOwnProperty('lastReason')) {
|
|
state.lastReason = null;
|
|
}
|
|
console.log(`📖 State geladen: ${state.current}, Letzter Grund: ${state.lastReason || 'keiner'}`);
|
|
return state;
|
|
} catch (err) {
|
|
console.error("❌ Fehler beim Lesen von state.json:", err);
|
|
return { current: "A", lastReason: null };
|
|
}
|
|
}
|
|
|
|
function setState(newState) {
|
|
try {
|
|
fs.writeFileSync(stateFile, JSON.stringify(newState, null, 2));
|
|
console.log(`💾 State gespeichert: ${newState.current}, Grund: ${newState.lastReason || 'keiner'}`);
|
|
} catch (err) {
|
|
console.error("❌ Fehler beim Speichern von state.json:", err);
|
|
}
|
|
}
|
|
|
|
// --------------------
|
|
// Discord Webhook
|
|
// --------------------
|
|
async function sendDiscordEmbed(previousKey, currentKey, reason) {
|
|
const previous = CONFIG[previousKey];
|
|
const current = CONFIG[currentKey];
|
|
|
|
if (!DISCORD_WEBHOOK_URL) {
|
|
console.log("⚠️ DISCORD_WEBHOOK_URL nicht gesetzt, keine Benachrichtigung gesendet");
|
|
return;
|
|
}
|
|
|
|
const fields = [
|
|
{
|
|
name: "Paid",
|
|
value: previous.name,
|
|
inline: true
|
|
},
|
|
{
|
|
name: "Now's turn",
|
|
value: current.name,
|
|
inline: true
|
|
}
|
|
];
|
|
|
|
// Grund hinzufügen falls vorhanden
|
|
if (reason) {
|
|
fields.push({
|
|
name: "📝 Grund",
|
|
value: reason,
|
|
inline: false
|
|
});
|
|
}
|
|
|
|
const embed = {
|
|
title: "💸 Pay-Switch",
|
|
description: `**${current.name}**'s turn`,
|
|
color: current.colorDiscord,
|
|
thumbnail: {
|
|
url: `${PUBLIC_BASE_URL}/img/${current.icon}`
|
|
},
|
|
fields: fields,
|
|
footer: {
|
|
text: new Date().toLocaleString("de-DE")
|
|
}
|
|
};
|
|
|
|
// Debug-Ausgabe
|
|
console.log(`\n📤 Sende Discord Webhook:`);
|
|
console.log(` Von: ${previous.name} → Zu: ${current.name}`);
|
|
console.log(` Grund: ${reason || '(keiner)'}`);
|
|
console.log(` Farbe: 0x${current.colorDiscord.toString(16)}`);
|
|
console.log(` Webhook URL: ${DISCORD_WEBHOOK_URL.substring(0, 50)}...`);
|
|
|
|
try {
|
|
const response = await fetch(DISCORD_WEBHOOK_URL, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ embeds: [embed] })
|
|
});
|
|
|
|
if (response.ok) {
|
|
console.log(`✅ Discord Benachrichtigung gesendet (HTTP ${response.status})`);
|
|
} else {
|
|
const errorText = await response.text();
|
|
console.error(`❌ Discord Webhook Fehler (HTTP ${response.status}):`, errorText);
|
|
}
|
|
} catch (err) {
|
|
console.error("❌ Discord Webhook Fehler:", err.message);
|
|
}
|
|
console.log(""); // Leerzeile für bessere Lesbarkeit
|
|
}
|
|
|
|
// --------------------
|
|
// API Routes
|
|
// --------------------
|
|
|
|
// Config API endpoint - sendet Config an Frontend
|
|
app.get("/config", (req, res) => {
|
|
// Nur relevante Daten fürs Frontend senden
|
|
const frontendConfig = {
|
|
A: {
|
|
name: CONFIG.A.name,
|
|
colorHex: CONFIG.A.colorHex,
|
|
icon: CONFIG.A.icon,
|
|
background: CONFIG.A.background
|
|
},
|
|
B: {
|
|
name: CONFIG.B.name,
|
|
colorHex: CONFIG.B.colorHex,
|
|
icon: CONFIG.B.icon,
|
|
background: CONFIG.B.background
|
|
}
|
|
};
|
|
res.json(frontendConfig);
|
|
});
|
|
|
|
app.get("/status", (req, res) => {
|
|
const state = getState();
|
|
res.json(state);
|
|
});
|
|
|
|
app.post("/toggle", async (req, res) => {
|
|
const state = getState();
|
|
const previousKey = state.current;
|
|
const reason = req.body?.reason;
|
|
|
|
// Wechseln
|
|
state.current = state.current === "A" ? "B" : "A";
|
|
|
|
// Grund speichern (oder null wenn keiner angegeben)
|
|
state.lastReason = reason || null;
|
|
|
|
// State persistent speichern
|
|
setState(state);
|
|
|
|
const logMessage = reason
|
|
? `Pay-Switch: ${CONFIG[previousKey].name} → ${CONFIG[state.current].name} (Grund: ${reason})`
|
|
: `Pay-Switch: ${CONFIG[previousKey].name} → ${CONFIG[state.current].name} (kein Grund)`;
|
|
|
|
console.log(logMessage);
|
|
|
|
// Discord Benachrichtigung - IMMER senden, auch ohne Grund!
|
|
await sendDiscordEmbed(previousKey, state.current, reason);
|
|
|
|
res.json({
|
|
current: state.current,
|
|
name: CONFIG[state.current].name,
|
|
lastReason: state.lastReason
|
|
});
|
|
});
|
|
|
|
app.get("/health", (req, res) => {
|
|
res.json({ status: "ok" });
|
|
});
|
|
|
|
// --------------------
|
|
// SPA Fallback - NO CRASH
|
|
// --------------------
|
|
app.use((req, res, next) => {
|
|
if (res.headersSent) return;
|
|
|
|
if (req.url.startsWith('/api/')) {
|
|
return res.status(404).json({ error: 'Not found' });
|
|
}
|
|
|
|
const acceptsHtml = req.accepts('html');
|
|
if (acceptsHtml) {
|
|
return res.sendFile(path.join(__dirname, "public/index.html"));
|
|
}
|
|
|
|
res.status(404).send('Not found');
|
|
});
|
|
|
|
// Error handler
|
|
app.use((err, req, res, next) => {
|
|
console.error('Server error:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
});
|
|
|
|
// --------------------
|
|
// Start
|
|
// --------------------
|
|
app.listen(PORT, "0.0.0.0", () => {
|
|
console.log(`\n✅ WhoPays läuft auf http://0.0.0.0:${PORT}\n`);
|
|
console.log(`📁 Static files directory: ${path.join(__dirname, "public")}`);
|
|
console.log(`🔒 Trust proxy: enabled (for reverse proxy support)`);
|
|
|
|
// Initial state laden und anzeigen
|
|
const initialState = getState();
|
|
console.log(`\n🎲 Aktueller Status:`);
|
|
console.log(` 👤 Wer zahlt: ${CONFIG[initialState.current].name}`);
|
|
console.log(` 📝 Letzter Grund: ${initialState.lastReason || 'Keiner gespeichert'}`);
|
|
|
|
console.log(`\n👥 Konfigurierte Personen:`);
|
|
console.log(` A: ${CONFIG.A.name} (${CONFIG.A.colorHex})`);
|
|
console.log(` B: ${CONFIG.B.name} (${CONFIG.B.colorHex})`);
|
|
|
|
// Discord Webhook Test
|
|
console.log(`\n🔔 Discord Webhook:`);
|
|
if (DISCORD_WEBHOOK_URL) {
|
|
console.log(` ✅ Konfiguriert: ${DISCORD_WEBHOOK_URL.substring(0, 50)}...`);
|
|
} else {
|
|
console.log(` ❌ NICHT konfiguriert!`);
|
|
}
|
|
|
|
// Verify critical files exist
|
|
const criticalFiles = [
|
|
'public/index.html',
|
|
'public/service-worker.js',
|
|
'public/manifest.json'
|
|
];
|
|
|
|
console.log('\n🔍 Checking critical files:');
|
|
criticalFiles.forEach(file => {
|
|
const fullPath = path.join(__dirname, file);
|
|
const exists = fs.existsSync(fullPath);
|
|
console.log(` ${exists ? '✅' : '❌'} ${file}`);
|
|
});
|
|
|
|
console.log('\n📝 Test URLs:');
|
|
console.log(` http://localhost:${PORT}/`);
|
|
console.log(` http://localhost:${PORT}/service-worker.js`);
|
|
console.log(` http://localhost:${PORT}/manifest.json\n`);
|
|
}); |