This commit is contained in:
cay 2026-04-08 16:41:47 +01:00
parent c3addb7ba1
commit a2ab6c6297
4 changed files with 111 additions and 49 deletions

71
app.js
View File

@ -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
======================== */

38
public/js/heartbeat.js Normal file
View File

@ -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 </body>:
<script src="/js/heartbeat.js"></script>
================================================ */
(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");
});
})();

View File

@ -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,

View File

@ -8,6 +8,7 @@
href="/images/favicon/dok_favicon_32px.ico"
type="image/x-icon"
/>
<script src="/js/heartbeat.js"></script>
<style>
@font-face {
font-family: "Cinzel Decorative";