558 lines
16 KiB
JavaScript
558 lines
16 KiB
JavaScript
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.socket");
|
||
const { registerHimmelstorHandlers } = require("./sockets/1vKI_daily.socket");
|
||
const { registerChatHandlers } = require("./sockets/chat");
|
||
const boosterRoutes = require("./routes/booster.route");
|
||
const pointsRoutes = require("./routes/points.route");
|
||
const combineRoutes = require("./routes/combine.route");
|
||
const bazaarRoutes = require("./routes/bazaar.route");
|
||
const himmelstorRoutes = require("./routes/himmelstor.route");
|
||
const himmelstorDailyRoutes = require("./routes/himmelstor-daily.route");
|
||
const gildenhalleRoutes = require("./routes/gildenhalle.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"));
|
||
|
||
const shopRoutes = require("./routes/shop.route");
|
||
|
||
/* ========================
|
||
WICHTIG: Shop/Webhook VOR express.json()
|
||
registrieren – Stripe braucht raw body!
|
||
======================== */
|
||
app.use("/api", shopRoutes);
|
||
|
||
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;
|
||
const ENERGY_MAX = 40;
|
||
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, energy, energy_reset FROM account_currency WHERE account_id = ?",
|
||
[userId],
|
||
);
|
||
|
||
/* ── Täglicher Energie-Reset ────────────────────────────
|
||
Wenn energy_reset < heute (oder NULL) → Energie auf Max
|
||
────────────────────────────────────────────────────────── */
|
||
const today = new Date().toISOString().slice(0, 10);
|
||
const lastReset = currency?.energy_reset
|
||
? new Date(currency.energy_reset).toISOString().slice(0, 10)
|
||
: null;
|
||
|
||
let currentEnergy = currency?.energy ?? ENERGY_MAX;
|
||
|
||
if (lastReset !== today) {
|
||
currentEnergy = ENERGY_MAX;
|
||
await db.query(
|
||
"UPDATE account_currency SET energy = ?, energy_reset = ? WHERE account_id = ?",
|
||
[ENERGY_MAX, today, 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,
|
||
energy: currentEnergy,
|
||
energy_max: ENERGY_MAX,
|
||
});
|
||
} 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);
|
||
app.use("/api", bazaarRoutes);
|
||
app.use("/himmelstor", himmelstorRoutes);
|
||
app.use("/api/himmelstor/daily", himmelstorDailyRoutes);
|
||
app.use("/api", gildenhalleRoutes);
|
||
|
||
/* ========================
|
||
Energie abfragen
|
||
======================== */
|
||
|
||
app.get("/api/energy", requireLogin, async (req, res) => {
|
||
const userId = req.session.user.id;
|
||
const ENERGY_MAX = 40;
|
||
try {
|
||
const [[row]] = await db.query(
|
||
"SELECT energy, energy_bought FROM account_currency WHERE account_id = ?",
|
||
[userId],
|
||
);
|
||
const today = new Date().toISOString().slice(0, 10);
|
||
const boughtDate = row?.energy_bought
|
||
? new Date(row.energy_bought).toISOString().slice(0, 10)
|
||
: null;
|
||
res.json({
|
||
energy: row?.energy ?? ENERGY_MAX,
|
||
energy_max: ENERGY_MAX,
|
||
bought_today: boughtDate === today,
|
||
});
|
||
} catch (err) {
|
||
res.status(500).json({ error: "DB Fehler" });
|
||
}
|
||
});
|
||
|
||
/* ========================
|
||
Energie kaufen
|
||
======================== */
|
||
|
||
app.post("/api/energy/buy", requireLogin, async (req, res) => {
|
||
const userId = req.session.user.id;
|
||
const ENERGY_MAX = 40;
|
||
const ENERGY_BUY = 10; // Energie die dazugekauft wird
|
||
const COST_GEMS = 10;
|
||
const COST_GOLD = 200;
|
||
const { currency: payWith } = req.body; // "gems" oder "gold"
|
||
|
||
if (!["gems", "gold"].includes(payWith)) {
|
||
return res.status(400).json({ error: "Ungültige Zahlungsmethode." });
|
||
}
|
||
|
||
try {
|
||
const today = new Date().toISOString().slice(0, 10);
|
||
|
||
const [[row]] = await db.query(
|
||
"SELECT energy, gems, gold, energy_bought FROM account_currency WHERE account_id = ?",
|
||
[userId],
|
||
);
|
||
if (!row) return res.status(404).json({ error: "Account nicht gefunden." });
|
||
|
||
/* Bereits heute gekauft? */
|
||
const boughtDate = row.energy_bought
|
||
? new Date(row.energy_bought).toISOString().slice(0, 10)
|
||
: null;
|
||
if (boughtDate === today) {
|
||
return res.status(400).json({ error: "Du hast heute bereits Energie aufgefüllt." });
|
||
}
|
||
|
||
/* Genug Währung? */
|
||
if (payWith === "gems" && row.gems < COST_GEMS) {
|
||
return res.status(400).json({ error: `Nicht genug Gems. Benötigt: ${COST_GEMS}.` });
|
||
}
|
||
if (payWith === "gold" && row.gold < COST_GOLD) {
|
||
return res.status(400).json({ error: `Nicht genug Gold. Benötigt: ${COST_GOLD}.` });
|
||
}
|
||
|
||
/* Energie darf max auf ENERGY_MAX steigen */
|
||
const newEnergy = Math.min((row.energy ?? ENERGY_MAX) + ENERGY_BUY, ENERGY_MAX);
|
||
|
||
if (payWith === "gems") {
|
||
await db.query(
|
||
`UPDATE account_currency
|
||
SET energy = ?, gems = gems - ?, energy_bought = ?
|
||
WHERE account_id = ?`,
|
||
[newEnergy, COST_GEMS, today, userId],
|
||
);
|
||
} else {
|
||
await db.query(
|
||
`UPDATE account_currency
|
||
SET energy = ?, gold = gold - ?, energy_bought = ?
|
||
WHERE account_id = ?`,
|
||
[newEnergy, COST_GOLD, today, userId],
|
||
);
|
||
}
|
||
|
||
res.json({ success: true, energy: newEnergy, energy_max: ENERGY_MAX });
|
||
} catch (err) {
|
||
console.error("[Energy Buy]", err);
|
||
res.status(500).json({ error: "DB Fehler" });
|
||
}
|
||
});
|
||
|
||
/* ========================
|
||
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);
|
||
registerHimmelstorHandlers(io, socket);
|
||
});
|
||
|
||
/* ========================
|
||
Server Start
|
||
======================== */
|
||
|
||
server.listen(PORT, () => {
|
||
console.log(`Dynasty of Knights Server läuft auf http://localhost:${PORT}`);
|
||
});
|