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);
|
||||
|
||||
/* ========================
|
||||
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
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", {
|
||||
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,
|
||||
|
||||
@ -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";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user