From a2ab6c6297ec0853fb7f6da065ad5669b0a480ba Mon Sep 17 00:00:00 2001 From: cay Date: Wed, 8 Apr 2026 16:41:47 +0100 Subject: [PATCH] rsj --- app.js | 71 ++++++++++++++++++++++++++++++++++-------- public/js/heartbeat.js | 38 ++++++++++++++++++++++ routes/login.route.js | 50 +++++++++-------------------- views/launcher.ejs | 1 + 4 files changed, 111 insertions(+), 49 deletions(-) create mode 100644 public/js/heartbeat.js diff --git a/app.js b/app.js index 0beda2e..2641188 100644 --- a/app.js +++ b/app.js @@ -74,6 +74,13 @@ const limiter = rateLimit({ app.use(limiter); +/* ======================== + Lösung 2: Session Config + maxAge: 24h – Sessions laufen + automatisch ab, auch wenn der + Browser einfach geschlossen wurde. +======================== */ + app.use( session({ secret: process.env.SESSION_SECRET || "dynastyofknights_secret", @@ -82,7 +89,7 @@ app.use( cookie: { httpOnly: true, secure: process.env.NODE_ENV === "production", - maxAge: 1000 * 60 * 60 * 24, + maxAge: 1000 * 60 * 60 * 24, // 24 Stunden }, }), ); @@ -94,6 +101,10 @@ app.use( app.set("view engine", "ejs"); app.set("views", path.join(__dirname, "views")); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(express.static(path.join(__dirname, "public"))); + /* ======================== Login Middleware ======================== */ @@ -105,6 +116,49 @@ function requireLogin(req, res, next) { 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 ======================== */ @@ -144,7 +198,7 @@ app.get("/api/building/:id", requireLogin, async (req, res) => { const buildingInfo = info[0] || {}; res.json({ name: buildingInfo.name || "Gebäude", - type: Number(buildingId), // als Number damit buildingModules[type] im Frontend matcht + type: Number(buildingId), level: building.level, points: building.points, nextLevelPoints: nextLevel[0]?.required_points || null, @@ -156,7 +210,7 @@ app.get("/api/building/:id", requireLogin, async (req, res) => { upgradeWood: nextLevel[0]?.wood ?? null, upgradeStone: nextLevel[0]?.stone ?? null, upgradeGold: nextLevel[0]?.gold ?? null, - upgradeRequiredPoints: nextLevel[0]?.required_points ?? null, // NEU + upgradeRequiredPoints: nextLevel[0]?.required_points ?? null, }); } catch (err) { console.error(err); @@ -173,7 +227,6 @@ app.post("/api/building/:id/upgrade", requireLogin, async (req, res) => { const userId = req.session.user.id; try { - // Aktuelles Level holen const [[userBuilding]] = await db.query( "SELECT id, level, points FROM user_buildings WHERE user_id = ? AND building_id = ?", [userId, buildingId], @@ -185,7 +238,6 @@ app.post("/api/building/:id/upgrade", requireLogin, async (req, res) => { const nextLevel = userBuilding.level + 1; - // Upgrade-Kosten für nächstes Level holen const [[levelData]] = await db.query( "SELECT required_points, wood, stone, gold FROM building_levels WHERE building_id = ? AND level = ?", [buildingId, nextLevel], @@ -197,14 +249,12 @@ app.post("/api/building/:id/upgrade", requireLogin, async (req, res) => { .json({ error: "Maximales Level bereits erreicht" }); } - // Punkte prüfen if (userBuilding.points < levelData.required_points) { return res.status(400).json({ error: `Nicht genügend Punkte. Benötigt: ${levelData.required_points}, Vorhanden: ${userBuilding.points}`, }); } - // Ressourcen des Spielers prüfen const [[currency]] = await db.query( "SELECT wood, stone, gold FROM account_currency WHERE account_id = ?", [userId], @@ -234,13 +284,11 @@ app.post("/api/building/:id/upgrade", requireLogin, async (req, res) => { }); } - // Ressourcen abziehen await db.query( "UPDATE account_currency SET wood = wood - ?, stone = stone - ?, gold = gold - ? WHERE account_id = ?", [levelData.wood, levelData.stone, levelData.gold, userId], ); - // Level erhöhen, nur benötigte Punkte abziehen await db.query( "UPDATE user_buildings SET level = ?, points = points - ? WHERE id = ?", [nextLevel, levelData.required_points, userBuilding.id], @@ -317,10 +365,6 @@ app.get("/api/buildings", requireLogin, async (req, res) => { } }); -app.use(express.json()); -app.use(express.urlencoded({ extended: true })); -app.use(express.static(path.join(__dirname, "public"))); - /* ======================== Routes ======================== */ @@ -342,6 +386,7 @@ app.use("/api", carddeckRoutes); app.use("/arena", arenaRoutes); app.use("/api", boosterRoutes); app.use("/api", require("./routes/daily.route")); + /* ======================== 404 Handler ======================== */ diff --git a/public/js/heartbeat.js b/public/js/heartbeat.js new file mode 100644 index 0000000..7623d05 --- /dev/null +++ b/public/js/heartbeat.js @@ -0,0 +1,38 @@ +/* ================================================ + heartbeat.js – Session am Leben halten & + beim Tab-Schließen ausloggen + + Einbinden in launcher.ejs (oder deiner + Haupt-EJS-Datei) direkt vor : + + +================================================ */ + +(function () { + /* ── Heartbeat alle 60 Sekunden ───────────────── + Solange der Tab offen ist, wird die Session + server-seitig durch session.touch() verlängert. + Bleibt der Ping aus (Browser geschlossen), + läuft die Session nach maxAge (24h) ab. + ────────────────────────────────────────────── */ + const HEARTBEAT_INTERVAL_MS = 60_000; // 60 Sekunden + + function sendHeartbeat() { + navigator.sendBeacon("/api/heartbeat"); + } + + // Sofort beim Laden einmal pingen + sendHeartbeat(); + + // Danach regelmäßig + setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS); + + /* ── Logout beim Tab-/Browser-Schließen ───────── + sendBeacon ist zuverlässiger als fetch(), + weil es auch beim Schließen noch abgesendet + wird. Der Browser garantiert die Zustellung. + ────────────────────────────────────────────── */ + window.addEventListener("beforeunload", () => { + navigator.sendBeacon("/api/logout"); + }); +})(); diff --git a/routes/login.route.js b/routes/login.route.js index 2882d9c..0936cbb 100644 --- a/routes/login.route.js +++ b/routes/login.route.js @@ -25,7 +25,7 @@ router.post("/", async (req, res) => { return res.render("index", { error: "Login fehlgeschlagen", servers, - extraServers: [], // ← das fehlte + extraServers: [], }); } @@ -38,45 +38,24 @@ router.post("/", async (req, res) => { return res.render("index", { error: "Login fehlgeschlagen", servers, - extraServers: [], // ← das fehlte + extraServers: [], }); } /* ================================ - Prüfen ob User bereits eingeloggt - (aktive Session mit passendem Token) - ================================= */ + Lösung 1: Session regenerieren + Alte Session wird IMMER zerstört + und eine neue erstellt – egal ob + der Spieler noch "eingeloggt" ist. + Kein Blockieren mehr. + ================================ */ - if (user.session_token) { - // Prüfen ob wirklich eine aktive Session existiert - // durch Vergleich mit dem gespeicherten Token. - // Gibt es noch eine aktive Session → Login blockieren. - // Nach Server-Absturz ist session_token zwar gesetzt, - // aber keine gültige Session mehr vorhanden → - // Token wird einfach überschrieben (alter Login wird gekickt). - const hasActiveSession = req.sessionStore?.sessions - ? await new Promise((resolve) => { - req.sessionStore.all((err, sessions) => { - if (err || !sessions) return resolve(false); - const active = Object.values(sessions).some((s) => { - try { - const parsed = typeof s === "string" ? JSON.parse(s) : s; - return parsed?.user?.token === user.session_token; - } catch { return false; } - }); - resolve(active); - }); - }) - : false; - - if (hasActiveSession) { - return res.render("index", { - error: "Dieser Account ist bereits eingeloggt. Bitte zuerst ausloggen.", - servers, - extraServers: [], - }); - } - } + await new Promise((resolve, reject) => { + req.session.regenerate((err) => { + if (err) return reject(err); + resolve(); + }); + }); /* ================================ Session Token erstellen @@ -85,7 +64,6 @@ router.post("/", async (req, res) => { const sessionToken = crypto.randomBytes(64).toString("hex"); /* Token in DB speichern (überschreibt alten Login) */ - await db.query("UPDATE accounts SET session_token = ? WHERE id = ?", [ sessionToken, user.id, diff --git a/views/launcher.ejs b/views/launcher.ejs index 26835d3..8409552 100644 --- a/views/launcher.ejs +++ b/views/launcher.ejs @@ -8,6 +8,7 @@ href="/images/favicon/dok_favicon_32px.ico" type="image/x-icon" /> +