require("dotenv").config(); const express = require("express"); const path = require("path"); const helmet = require("helmet"); const rateLimit = require("express-rate-limit"); const http = require("http"); const { Server } = require("socket.io"); const db = require("./database/database"); const serverRoutes = require("./routes/servers.route"); const registerRoutes = require("./routes/register.route"); const verifyRoutes = require("./routes/verify.route"); const characterRoutes = require("./routes/character.route"); const session = require("express-session"); const loginRoutes = require("./routes/login.route"); const launcherRoutes = require("./routes/launcher.route"); const buildingRoutes = require("./routes/buildings.route"); const inventory = require("./routes/inventory.route"); const avatar = require("./routes/avatar.route"); const equip = require("./routes/equip.route"); const equipment = require("./routes/equipment.route"); const blackmarket = require("./routes/blackmarket.route"); const mineRoute = require("./routes/mine.route"); const carddeckRoutes = require("./routes/carddeck.route"); const arenaRoutes = require("./routes/arena.route"); const { registerArenaHandlers } = require("./sockets/arena"); const { registerChatHandlers } = require("./sockets/chat"); const boosterRoutes = require("./routes/booster.route"); const pointsRoutes = require("./routes/points.route"); const shopRoutes = require("./routes/shop.route"); const compression = require("compression"); const app = express(); app.set("trust proxy", 1); const PORT = process.env.PORT || 3000; /* ======================== Chatserver ======================== */ const server = http.createServer(app); const io = new Server(server); /* ======================== Compression ======================== */ app.use(compression()); /* ======================== Security Middleware ======================== */ app.use( helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'", "https://js.stripe.com"], scriptSrcAttr: ["'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], fontSrc: ["'self'", "https://fonts.gstatic.com"], imgSrc: ["'self'", "data:", "blob:", "https://*.stripe.com"], connectSrc: ["'self'", "ws:", "wss:", "https://api.stripe.com"], frameSrc: ["https://js.stripe.com", "https://hooks.stripe.com"], frameAncestors: ["'self'"], }, }, }), ); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5000, }); app.use(limiter); /* ======================== Lösung 2: Session Config ======================== */ app.use( session({ secret: process.env.SESSION_SECRET || "dynastyofknights_secret", resave: false, saveUninitialized: false, cookie: { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: 1000 * 60 * 60 * 24, }, }), ); /* ======================== Express Settings ======================== */ app.set("view engine", "ejs"); app.set("views", path.join(__dirname, "views")); /* Webhook braucht raw body – alle anderen json */ app.use((req, res, next) => { if (req.originalUrl === "/api/shop/webhook") { express.raw({ type: "application/json" })(req, res, next); } else { express.json()(req, res, next); } }); app.use(express.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname, "public"))); /* ======================== Login Middleware ======================== */ function requireLogin(req, res, next) { if (!req.session.user) { return res.status(401).json({ error: "Nicht eingeloggt" }); } next(); } /* ======================== Lösung 3a: Heartbeat Empfängt den Ping des Frontends alle 60 Sekunden. Solange Pings kommen, gilt der Spieler als aktiv. Bleibt der Ping aus (Tab zu), läuft die Session nach maxAge ab. ======================== */ app.post("/api/heartbeat", requireLogin, (req, res) => { // Session-Ablauf neu starten bei jedem Ping req.session.touch(); res.sendStatus(204); // No Content }); /* ======================== Lösung 3b: Logout Wird per sendBeacon beim Tab-Schließen aufgerufen. Zerstört Session + löscht Token. ======================== */ app.post("/api/logout", async (req, res) => { try { const userId = req.session?.user?.id; if (userId) { // Token in DB löschen await db.query("UPDATE accounts SET session_token = NULL WHERE id = ?", [ userId, ]); } req.session.destroy(() => { res.clearCookie("connect.sid"); res.sendStatus(204); // No Content }); } catch (err) { console.error("Logout Fehler:", err); res.sendStatus(500); } }); /* ======================== Route für Ajax für Gebäude ======================== */ app.get("/api/building/:id", requireLogin, async (req, res) => { const buildingId = req.params.id; const userId = req.session.user.id; try { const [userBuilding] = await db.query( "SELECT level, points FROM user_buildings WHERE user_id=? AND building_id=?", [userId, buildingId], ); let building; if (!userBuilding.length) { return res.status(404).json({ error: "Gebäude nicht gefunden" }); } else { building = userBuilding[0]; } const [nextLevel] = await db.query( "SELECT required_points, wood, stone, gold FROM building_levels WHERE building_id=? AND level=?", [buildingId, building.level + 1], ); const [info] = await db.query( "SELECT name,description,history FROM buildings WHERE id=?", [buildingId], ); const buildingInfo = info[0] || {}; res.json({ name: buildingInfo.name || "Gebäude", type: Number(buildingId), level: building.level, points: building.points, nextLevelPoints: nextLevel[0]?.required_points || null, description: buildingInfo.description || "", history: buildingInfo.history || "", upgradeCost: nextLevel[0] ? `${nextLevel[0].wood} Holz, ${nextLevel[0].stone} Stein, ${nextLevel[0].gold} Gold` : "Max Level erreicht", upgradeWood: nextLevel[0]?.wood ?? null, upgradeStone: nextLevel[0]?.stone ?? null, upgradeGold: nextLevel[0]?.gold ?? null, upgradeRequiredPoints: nextLevel[0]?.required_points ?? null, }); } catch (err) { console.error(err); res.status(500).json({ error: "DB Fehler" }); } }); /* ======================== Route für Gebäude Upgrade ======================== */ app.post("/api/building/:id/upgrade", requireLogin, async (req, res) => { const buildingId = req.params.id; const userId = req.session.user.id; try { const [[userBuilding]] = await db.query( "SELECT id, level, points FROM user_buildings WHERE user_id = ? AND building_id = ?", [userId, buildingId], ); if (!userBuilding) { return res.status(404).json({ error: "Gebäude nicht gefunden" }); } const nextLevel = userBuilding.level + 1; const [[levelData]] = await db.query( "SELECT required_points, wood, stone, gold FROM building_levels WHERE building_id = ? AND level = ?", [buildingId, nextLevel], ); if (!levelData) { return res .status(400) .json({ error: "Maximales Level bereits erreicht" }); } if (userBuilding.points < levelData.required_points) { return res.status(400).json({ error: `Nicht genügend Punkte. Benötigt: ${levelData.required_points}, Vorhanden: ${userBuilding.points}`, }); } const [[currency]] = await db.query( "SELECT wood, stone, gold FROM account_currency WHERE account_id = ?", [userId], ); if (!currency) { return res.status(400).json({ error: "Keine Währungsdaten gefunden" }); } if ( currency.wood < levelData.wood || currency.stone < levelData.stone || currency.gold < levelData.gold ) { return res.status(400).json({ error: "Nicht genügend Ressourcen", required: { wood: levelData.wood, stone: levelData.stone, gold: levelData.gold, }, current: { wood: currency.wood, stone: currency.stone, gold: currency.gold, }, }); } await db.query( "UPDATE account_currency SET wood = wood - ?, stone = stone - ?, gold = gold - ? WHERE account_id = ?", [levelData.wood, levelData.stone, levelData.gold, userId], ); await db.query( "UPDATE user_buildings SET level = ?, points = points - ? WHERE id = ?", [nextLevel, levelData.required_points, userBuilding.id], ); res.json({ success: true, newLevel: nextLevel, cost: { wood: levelData.wood, stone: levelData.stone, gold: levelData.gold, }, }); } catch (err) { console.error(err); res.status(500).json({ error: "DB Fehler" }); } }); /* ======================== HUD API ======================== */ app.get("/api/hud", requireLogin, async (req, res) => { const userId = req.session.user.id; try { const [[account]] = await db.query( "SELECT ingame_name FROM accounts WHERE id = ?", [userId], ); const [[currency]] = await db.query( "SELECT silver, gold, gems, wood, stone, iron FROM account_currency WHERE account_id = ?", [userId], ); res.json({ name: account?.ingame_name || "Held", silver: currency?.silver || 0, gold: currency?.gold || 0, gems: currency?.gems || 0, wood: currency?.wood || 0, stone: currency?.stone || 0, iron: currency?.iron || 0, }); } catch (err) { console.error(err); res.status(500).json({ error: "DB Fehler" }); } }); app.get("/api/buildings", requireLogin, async (req, res) => { const userId = req.session.user.id; try { const [rows] = await db.query( ` SELECT b.id, b.name, b.description, b.history, ub.level, ub.points FROM buildings b LEFT JOIN user_buildings ub ON ub.building_id = b.id AND ub.user_id = ? `, [userId], ); res.json(rows); } catch (err) { console.error(err); res.status(500).json({ error: "DB Fehler" }); } }); /* ======================== Routes ======================== */ app.use("/", serverRoutes); app.use("/register", registerRoutes); app.use("/verify", verifyRoutes); app.use("/create-character", characterRoutes); app.use("/login", loginRoutes); app.use("/launcher", launcherRoutes); app.use("/", buildingRoutes); app.use("/api/inventory", inventory); app.use("/api/avatar", avatar); app.use("/api/equip", equip); app.use("/api/equipment", equipment); app.use("/api/blackmarket", blackmarket); app.use("/api/mine", mineRoute); app.use("/api", carddeckRoutes); app.use("/arena", arenaRoutes); app.use("/api", boosterRoutes); app.use("/api", require("./routes/daily.route")); app.use("/api/points", pointsRoutes); app.use("/api", shopRoutes); /* ======================== 404 Handler ======================== */ app.use((req, res) => { res.status(404).send("Seite nicht gefunden"); }); /* ======================== Socket.io Handler ======================== */ io.on("connection", (socket) => { console.log("Spieler verbunden:", socket.id); registerChatHandlers(io, socket); registerArenaHandlers(io, socket); }); /* ======================== Server Start ======================== */ server.listen(PORT, () => { console.log(`Dynasty of Knights Server läuft auf http://localhost:${PORT}`); });