import express from "express"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; 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"); // -------------------- // Konfiguration // -------------------- const CONFIG = { A: { name: "Marc", color: 0x2ecc71, icon: "a.png" }, B: { name: "Laila", color: 0xe74c3c, icon: "b.png" } }; const PUBLIC_BASE_URL = process.env.PUBLIC_BASE_URL || "http://localhost:3000"; const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL || "https://discord.com/api/webhooks/1450872860607381515/zRbiXOykKPUfLJ_H87wD2OxQJPLdro6yQzU2T48Pg4fxSNaXEqBFgxdHGTiIDmmKGHIF"; // -------------------- // State Handling // -------------------- function getState() { if (!fs.existsSync(stateFile)) { fs.writeFileSync( stateFile, JSON.stringify({ current: "A", lastReason: null }, null, 2) ); } return JSON.parse(fs.readFileSync(stateFile, "utf-8")); } function setState(newState) { fs.writeFileSync(stateFile, JSON.stringify(newState, null, 2)); } // -------------------- // Discord Webhook // -------------------- async function sendDiscordEmbed(previousKey, currentKey, reason) { const previous = CONFIG[previousKey]; const current = CONFIG[currentKey]; if (!DISCORD_WEBHOOK_URL) return; const fields = [ { name: "Von", value: previous.name, inline: true }, { name: "Zu", value: current.name, inline: true } ]; if (reason) { fields.push({ name: "๐Ÿ“ Grund", value: reason, inline: false }); } const embed = { title: "๐Ÿ’ธ Zahl-Wechsel", description: `**${current.name}** ist jetzt dran zu zahlen`, color: current.color, thumbnail: { url: `${PUBLIC_BASE_URL}/img/${current.icon}` }, fields: fields, footer: { text: new Date().toLocaleString("de-DE") } }; try { await fetch(DISCORD_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ embeds: [embed] }) }); console.log("โœ… Discord Benachrichtigung gesendet"); } catch (err) { console.error("โŒ Discord Webhook Fehler:", err.message); } } // -------------------- // API Routes // -------------------- app.get("/status", (req, res) => { res.json(getState()); }); app.post("/toggle", async (req, res) => { const state = getState(); const previousKey = state.current; const reason = req.body?.reason; state.current = state.current === "A" ? "B" : "A"; state.lastReason = reason || null; setState(state); const logMessage = reason ? `Zahl-Wechsel: ${CONFIG[previousKey].name} โ†’ ${CONFIG[state.current].name} (Grund: ${reason})` : `Zahl-Wechsel: ${CONFIG[previousKey].name} โ†’ ${CONFIG[state.current].name}`; console.log(logMessage); 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 // -------------------- // Only serve index.html for navigation requests, not for missing static files app.use((req, res, next) => { // If request already handled by static middleware, skip if (res.headersSent) return; // If it's an API route that wasn't matched, 404 if (req.url.startsWith('/api/')) { return res.status(404).json({ error: 'Not found' }); } // If Accept header indicates browser navigation (HTML), serve index.html const acceptsHtml = req.accepts('html'); if (acceptsHtml) { return res.sendFile(path.join(__dirname, "public/index.html")); } // Otherwise 404 res.status(404).send('Not found'); }); // Error handler to prevent crashes app.use((err, req, res, next) => { console.error('Server error:', err); res.status(500).json({ error: 'Internal server error' }); }); // -------------------- // Start // -------------------- const PORT = process.env.PORT || 3000; 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)`); // 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 (adjust domain for your setup):'); console.log(` http://localhost:${PORT}/`); console.log(` http://localhost:${PORT}/service-worker.js`); console.log(` http://localhost:${PORT}/manifest.json\n`); });