dok/app.js
2026-04-11 14:00:01 +01:00

416 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 combineRoutes = require("./routes/combine.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'"],
scriptSrcAttr: ["'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
imgSrc: ["'self'", "data:", "blob:"],
connectSrc: ["'self'", "ws:", "wss:"],
frameAncestors: ["'self'"], // Erlaubt iframe von eigener Domain
},
},
}),
);
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5000,
});
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",
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 1000 * 60 * 60 * 24, // 24 Stunden
},
}),
);
/* ========================
Express Settings
======================== */
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
======================== */
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 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,
});
} 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", combineRoutes);
/* ========================
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}`);
});