rsj
This commit is contained in:
parent
c3addb7ba1
commit
a2ab6c6297
71
app.js
71
app.js
@ -74,6 +74,13 @@ const limiter = rateLimit({
|
|||||||
|
|
||||||
app.use(limiter);
|
app.use(limiter);
|
||||||
|
|
||||||
|
/* ========================
|
||||||
|
Lösung 2: Session Config
|
||||||
|
maxAge: 24h – Sessions laufen
|
||||||
|
automatisch ab, auch wenn der
|
||||||
|
Browser einfach geschlossen wurde.
|
||||||
|
======================== */
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
session({
|
session({
|
||||||
secret: process.env.SESSION_SECRET || "dynastyofknights_secret",
|
secret: process.env.SESSION_SECRET || "dynastyofknights_secret",
|
||||||
@ -82,7 +89,7 @@ app.use(
|
|||||||
cookie: {
|
cookie: {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === "production",
|
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("view engine", "ejs");
|
||||||
app.set("views", path.join(__dirname, "views"));
|
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
|
Login Middleware
|
||||||
======================== */
|
======================== */
|
||||||
@ -105,6 +116,49 @@ function requireLogin(req, res, next) {
|
|||||||
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
|
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] || {};
|
const buildingInfo = info[0] || {};
|
||||||
res.json({
|
res.json({
|
||||||
name: buildingInfo.name || "Gebäude",
|
name: buildingInfo.name || "Gebäude",
|
||||||
type: Number(buildingId), // als Number damit buildingModules[type] im Frontend matcht
|
type: Number(buildingId),
|
||||||
level: building.level,
|
level: building.level,
|
||||||
points: building.points,
|
points: building.points,
|
||||||
nextLevelPoints: nextLevel[0]?.required_points || null,
|
nextLevelPoints: nextLevel[0]?.required_points || null,
|
||||||
@ -156,7 +210,7 @@ app.get("/api/building/:id", requireLogin, async (req, res) => {
|
|||||||
upgradeWood: nextLevel[0]?.wood ?? null,
|
upgradeWood: nextLevel[0]?.wood ?? null,
|
||||||
upgradeStone: nextLevel[0]?.stone ?? null,
|
upgradeStone: nextLevel[0]?.stone ?? null,
|
||||||
upgradeGold: nextLevel[0]?.gold ?? null,
|
upgradeGold: nextLevel[0]?.gold ?? null,
|
||||||
upgradeRequiredPoints: nextLevel[0]?.required_points ?? null, // NEU
|
upgradeRequiredPoints: nextLevel[0]?.required_points ?? null,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -173,7 +227,6 @@ app.post("/api/building/:id/upgrade", requireLogin, async (req, res) => {
|
|||||||
const userId = req.session.user.id;
|
const userId = req.session.user.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Aktuelles Level holen
|
|
||||||
const [[userBuilding]] = await db.query(
|
const [[userBuilding]] = await db.query(
|
||||||
"SELECT id, level, points FROM user_buildings WHERE user_id = ? AND building_id = ?",
|
"SELECT id, level, points FROM user_buildings WHERE user_id = ? AND building_id = ?",
|
||||||
[userId, buildingId],
|
[userId, buildingId],
|
||||||
@ -185,7 +238,6 @@ app.post("/api/building/:id/upgrade", requireLogin, async (req, res) => {
|
|||||||
|
|
||||||
const nextLevel = userBuilding.level + 1;
|
const nextLevel = userBuilding.level + 1;
|
||||||
|
|
||||||
// Upgrade-Kosten für nächstes Level holen
|
|
||||||
const [[levelData]] = await db.query(
|
const [[levelData]] = await db.query(
|
||||||
"SELECT required_points, wood, stone, gold FROM building_levels WHERE building_id = ? AND level = ?",
|
"SELECT required_points, wood, stone, gold FROM building_levels WHERE building_id = ? AND level = ?",
|
||||||
[buildingId, nextLevel],
|
[buildingId, nextLevel],
|
||||||
@ -197,14 +249,12 @@ app.post("/api/building/:id/upgrade", requireLogin, async (req, res) => {
|
|||||||
.json({ error: "Maximales Level bereits erreicht" });
|
.json({ error: "Maximales Level bereits erreicht" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Punkte prüfen
|
|
||||||
if (userBuilding.points < levelData.required_points) {
|
if (userBuilding.points < levelData.required_points) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: `Nicht genügend Punkte. Benötigt: ${levelData.required_points}, Vorhanden: ${userBuilding.points}`,
|
error: `Nicht genügend Punkte. Benötigt: ${levelData.required_points}, Vorhanden: ${userBuilding.points}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ressourcen des Spielers prüfen
|
|
||||||
const [[currency]] = await db.query(
|
const [[currency]] = await db.query(
|
||||||
"SELECT wood, stone, gold FROM account_currency WHERE account_id = ?",
|
"SELECT wood, stone, gold FROM account_currency WHERE account_id = ?",
|
||||||
[userId],
|
[userId],
|
||||||
@ -234,13 +284,11 @@ app.post("/api/building/:id/upgrade", requireLogin, async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ressourcen abziehen
|
|
||||||
await db.query(
|
await db.query(
|
||||||
"UPDATE account_currency SET wood = wood - ?, stone = stone - ?, gold = gold - ? WHERE account_id = ?",
|
"UPDATE account_currency SET wood = wood - ?, stone = stone - ?, gold = gold - ? WHERE account_id = ?",
|
||||||
[levelData.wood, levelData.stone, levelData.gold, userId],
|
[levelData.wood, levelData.stone, levelData.gold, userId],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Level erhöhen, nur benötigte Punkte abziehen
|
|
||||||
await db.query(
|
await db.query(
|
||||||
"UPDATE user_buildings SET level = ?, points = points - ? WHERE id = ?",
|
"UPDATE user_buildings SET level = ?, points = points - ? WHERE id = ?",
|
||||||
[nextLevel, levelData.required_points, userBuilding.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
|
Routes
|
||||||
======================== */
|
======================== */
|
||||||
@ -342,6 +386,7 @@ app.use("/api", carddeckRoutes);
|
|||||||
app.use("/arena", arenaRoutes);
|
app.use("/arena", arenaRoutes);
|
||||||
app.use("/api", boosterRoutes);
|
app.use("/api", boosterRoutes);
|
||||||
app.use("/api", require("./routes/daily.route"));
|
app.use("/api", require("./routes/daily.route"));
|
||||||
|
|
||||||
/* ========================
|
/* ========================
|
||||||
404 Handler
|
404 Handler
|
||||||
======================== */
|
======================== */
|
||||||
|
|||||||
38
public/js/heartbeat.js
Normal file
38
public/js/heartbeat.js
Normal 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");
|
||||||
|
});
|
||||||
|
})();
|
||||||
@ -25,7 +25,7 @@ router.post("/", async (req, res) => {
|
|||||||
return res.render("index", {
|
return res.render("index", {
|
||||||
error: "Login fehlgeschlagen",
|
error: "Login fehlgeschlagen",
|
||||||
servers,
|
servers,
|
||||||
extraServers: [], // ← das fehlte
|
extraServers: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,45 +38,24 @@ router.post("/", async (req, res) => {
|
|||||||
return res.render("index", {
|
return res.render("index", {
|
||||||
error: "Login fehlgeschlagen",
|
error: "Login fehlgeschlagen",
|
||||||
servers,
|
servers,
|
||||||
extraServers: [], // ← das fehlte
|
extraServers: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================================
|
/* ================================
|
||||||
Prüfen ob User bereits eingeloggt
|
Lösung 1: Session regenerieren
|
||||||
(aktive Session mit passendem Token)
|
Alte Session wird IMMER zerstört
|
||||||
================================= */
|
und eine neue erstellt – egal ob
|
||||||
|
der Spieler noch "eingeloggt" ist.
|
||||||
|
Kein Blockieren mehr.
|
||||||
|
================================ */
|
||||||
|
|
||||||
if (user.session_token) {
|
await new Promise((resolve, reject) => {
|
||||||
// Prüfen ob wirklich eine aktive Session existiert
|
req.session.regenerate((err) => {
|
||||||
// durch Vergleich mit dem gespeicherten Token.
|
if (err) return reject(err);
|
||||||
// Gibt es noch eine aktive Session → Login blockieren.
|
resolve();
|
||||||
// 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: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================================
|
/* ================================
|
||||||
Session Token erstellen
|
Session Token erstellen
|
||||||
@ -85,7 +64,6 @@ router.post("/", async (req, res) => {
|
|||||||
const sessionToken = crypto.randomBytes(64).toString("hex");
|
const sessionToken = crypto.randomBytes(64).toString("hex");
|
||||||
|
|
||||||
/* Token in DB speichern (überschreibt alten Login) */
|
/* Token in DB speichern (überschreibt alten Login) */
|
||||||
|
|
||||||
await db.query("UPDATE accounts SET session_token = ? WHERE id = ?", [
|
await db.query("UPDATE accounts SET session_token = ? WHERE id = ?", [
|
||||||
sessionToken,
|
sessionToken,
|
||||||
user.id,
|
user.id,
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
href="/images/favicon/dok_favicon_32px.ico"
|
href="/images/favicon/dok_favicon_32px.ico"
|
||||||
type="image/x-icon"
|
type="image/x-icon"
|
||||||
/>
|
/>
|
||||||
|
<script src="/js/heartbeat.js"></script>
|
||||||
<style>
|
<style>
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Cinzel Decorative";
|
font-family: "Cinzel Decorative";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user