dok/app.js
2026-04-14 18:42:30 +01:00

558 lines
16 KiB
JavaScript
Raw Permalink 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.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}`);
});