This commit is contained in:
cay 2026-04-10 13:12:15 +01:00
parent f789fe6405
commit 3844e74936
6 changed files with 509 additions and 7 deletions

5
.env
View File

@ -15,4 +15,7 @@ SESSION_SECRET=irgendein_langer_geheimer_zufallstext_123!
MAIL_HOST=smtp.ionos.de
MAIL_USER=register@dynastyofknights.com
MAIL_PASS=111168-j-62217DwmbwPK
MAIL_PASS=111168-j-62217DwmbwPK
STRIPE_SECRET_KEY=sk_live_:J1d5&Sa0!_~>lLHuyxa$VC:AuOh
STRIPE_WEBHOOK_SECRET=whsec_oaZPQsV2Hj<D"=B%6oW$0fZ`x+3

21
app.js
View File

@ -28,6 +28,7 @@ const { registerArenaHandlers } = require("./sockets/arena");
const { registerChatHandlers } = require("./sockets/chat");
const boosterRoutes = require("./routes/booster.route");
const pointsRoutes = require("./routes/points.route");
const { router: shopRoutes } = require("./routes/shop.route");
const compression = require("compression");
@ -57,18 +58,26 @@ app.use(
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'", "'unsafe-inline'", "https://js.stripe.com"],
scriptSrcAttr: ["'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "blob:"],
connectSrc: ["'self'", "ws:", "wss:"],
frameAncestors: ["'self'"], // Erlaubt iframe von eigener Domain
imgSrc: ["'self'", "data:", "blob:", "https://*.stripe.com"],
connectSrc: ["'self'", "ws:", "wss:", "https://api.stripe.com"],
frameSrc: ["https://js.stripe.com", "https://hooks.stripe.com"],
frameAncestors: ["'self'"],
},
},
}),
);
/* ========================
Stripe Webhook (raw body
muss VOR express.json stehen!)
======================== */
const { router: shopRouteWebhook } = require("./routes/shop.route");
app.use("/api", shopRouteWebhook);
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5000,
@ -319,7 +328,7 @@ app.get("/api/hud", requireLogin, async (req, res) => {
[userId],
);
const [[currency]] = await db.query(
"SELECT silver, gold, gems, wood, stone FROM account_currency WHERE account_id = ?",
"SELECT silver, gold, gems, wood, stone, iron FROM account_currency WHERE account_id = ?",
[userId],
);
res.json({
@ -329,6 +338,7 @@ app.get("/api/hud", requireLogin, async (req, res) => {
gems: currency?.gems || 0,
wood: currency?.wood || 0,
stone: currency?.stone || 0,
iron: currency?.iron || 0,
});
} catch (err) {
console.error(err);
@ -385,6 +395,7 @@ app.use("/arena", arenaRoutes);
app.use("/api", boosterRoutes);
app.use("/api", require("./routes/daily.route"));
app.use("/api/points", pointsRoutes);
app.use("/api", shopRoutes);
/* ========================
404 Handler

View File

@ -300,3 +300,202 @@
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6));
display: inline-block;
}
/* =========================
Shop Overlay & Popup
========================= */
#shop-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 3999;
display: none;
}
#shop-overlay.active { display: block; }
#shop-popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.92);
opacity: 0;
pointer-events: none;
width: min(1100px, 95vw);
max-height: 90vh;
overflow-y: auto;
background: url("/images/parchment.png") center / cover no-repeat;
border: 4px solid #6b4b2a;
border-radius: 12px;
box-shadow: 0 0 60px rgba(0,0,0,0.95);
z-index: 4000;
display: flex;
flex-direction: column;
transition: opacity 0.25s ease, transform 0.25s ease;
}
#shop-popup.active {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
pointer-events: all;
}
#shop-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
background: linear-gradient(#6b4b2a, #3c2414);
border-bottom: 2px solid #8b6a3c;
border-radius: 8px 8px 0 0;
flex-shrink: 0;
}
#shop-title {
font-family: "Tangerine", serif;
font-size: 46px;
color: #f0d9a6;
text-shadow: 0 2px 6px black;
}
#shop-close {
font-size: 22px;
color: #f0d9a6;
cursor: pointer;
width: 34px; height: 34px;
display: flex; align-items: center; justify-content: center;
border-radius: 6px;
transition: 0.2s;
}
#shop-close:hover { background: rgba(255,255,255,0.1); color: #fff; }
#shop-subtitle {
text-align: center;
font-family: "Cinzel", serif;
font-size: 14px;
color: #5c3b20;
padding: 14px 24px 0;
flex-shrink: 0;
}
#shop-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
padding: 20px 30px;
}
.shop-loading {
grid-column: 1 / -1;
text-align: center;
font-family: "Cinzel", serif;
color: #8b6a3c;
padding: 40px;
}
/* Shop Karte */
.shop-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 20px 14px 14px;
background: linear-gradient(145deg, rgba(58,40,16,0.85), rgba(26,15,4,0.85));
border: 2px solid #8b6a3c;
border-radius: 12px;
cursor: pointer;
transition: 0.2s;
box-shadow: 0 4px 16px rgba(0,0,0,0.5);
}
.shop-card:hover {
border-color: #f0d060;
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0,0,0,0.7), 0 0 14px rgba(200,160,60,0.3);
}
.shop-badge {
position: absolute;
top: -10px; right: -10px;
background: linear-gradient(#c8200a, #8b1006);
border: 2px solid #ff6040;
border-radius: 20px;
font-family: "Cinzel", serif;
font-size: 10px;
font-weight: bold;
color: #fff;
padding: 3px 8px;
white-space: nowrap;
}
.shop-gem-icon {
font-size: 42px;
filter: drop-shadow(0 2px 6px rgba(0,100,255,0.5));
}
.shop-package-name {
font-family: "Tangerine", serif;
font-size: 28px;
color: #f0d9a6;
text-shadow: 0 1px 4px black;
}
.shop-gems-amount {
font-family: "Cinzel", serif;
font-size: 22px;
font-weight: bold;
color: #f0d060;
}
.shop-gems-label {
font-family: "Cinzel", serif;
font-size: 11px;
color: #a08060;
text-transform: uppercase;
letter-spacing: 1px;
}
.shop-breakdown {
font-family: "Cinzel", serif;
font-size: 11px;
color: #c8a86a;
}
.shop-price-per {
font-family: "Cinzel", serif;
font-size: 10px;
color: #8b6a3c;
}
.shop-buy-btn {
margin-top: 8px;
width: 100%;
padding: 10px;
background: linear-gradient(#6b4b2a, #3c2414);
border: 2px solid #f0d060;
border-radius: 8px;
color: #f0d9a6;
font-family: "Cinzel", serif;
font-size: 15px;
font-weight: bold;
cursor: pointer;
transition: 0.2s;
}
.shop-buy-btn:hover {
background: linear-gradient(#8b6b3a, #5c3a1a);
color: #fff;
box-shadow: 0 0 12px rgba(240,208,96,0.4);
}
.shop-buy-btn:disabled { opacity: 0.5; cursor: not-allowed; }
#shop-footer {
text-align: center;
font-family: "Cinzel", serif;
font-size: 11px;
color: #8b6a3c;
padding: 0 24px 20px;
flex-shrink: 0;
}

115
public/js/shop.js Normal file
View File

@ -0,0 +1,115 @@
/* ============================================================
public/js/shop.js
Shop-Popup Gems kaufen
============================================================ */
const shopOverlay = document.getElementById("shop-overlay");
const shopPopup = document.getElementById("shop-popup");
const shopClose = document.getElementById("shop-close");
const shopGrid = document.getElementById("shop-grid");
/*
Shop öffnen
*/
export async function openShop() {
shopOverlay.classList.add("active");
shopPopup.classList.add("active");
await loadPackages();
}
/*
Shop schließen
*/
function closeShop() {
shopOverlay.classList.remove("active");
shopPopup.classList.remove("active");
}
shopClose?.addEventListener("click", closeShop);
shopOverlay?.addEventListener("click", closeShop);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeShop();
});
/*
Pakete laden
*/
async function loadPackages() {
shopGrid.innerHTML = `<div class="shop-loading">Lade Angebote...</div>`;
try {
const res = await fetch("/api/shop/packages");
const pkgs = await res.json();
renderPackages(pkgs);
} catch {
shopGrid.innerHTML = `<div class="shop-loading">Fehler beim Laden.</div>`;
}
}
/*
Pakete rendern
*/
function renderPackages(pkgs) {
shopGrid.innerHTML = pkgs.map(pkg => {
const total = pkg.gems + pkg.bonus;
const priceEur = (pkg.price / 100).toFixed(2).replace(".", ",");
const hasBonus = pkg.bonus > 0;
const perGem = (pkg.price / total).toFixed(1);
return `
<div class="shop-card" data-id="${pkg.id}">
${hasBonus ? `<div class="shop-badge">+${pkg.bonus} Bonus!</div>` : ""}
<div class="shop-gem-icon">💠</div>
<div class="shop-package-name">${pkg.name}</div>
<div class="shop-gems-amount">${total.toLocaleString("de-DE")}</div>
<div class="shop-gems-label">Gems</div>
${hasBonus ? `<div class="shop-breakdown">${pkg.gems} + <span style="color:#f0d060">${pkg.bonus} Bonus</span></div>` : ""}
<div class="shop-price-per">~ ${perGem}¢ pro Gem</div>
<button class="shop-buy-btn" data-id="${pkg.id}">
${priceEur}
</button>
</div>`;
}).join("");
shopGrid.querySelectorAll(".shop-buy-btn").forEach(btn => {
btn.addEventListener("click", () => checkout(btn.dataset.id, btn));
});
}
/*
Checkout starten
*/
async function checkout(packageId, btn) {
btn.disabled = true;
btn.textContent = "Weiterleiten...";
try {
const res = await fetch("/api/shop/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ packageId }),
});
const data = await res.json();
if (!res.ok || !data.url) {
alert(data.error || "Fehler beim Starten der Zahlung.");
btn.disabled = false;
btn.textContent = getPrice(packageId);
return;
}
window.location.href = data.url;
} catch {
alert("Verbindungsfehler.");
btn.disabled = false;
btn.textContent = "Fehler";
}
}
function getPrice(id) {
const prices = {
starter: "1,99 €", abenteurer: "7,99 €",
ritter: "14,99 €", fuerst: "34,99 €", koenig: "59,99 €"
};
return prices[id] || "Kaufen";
}

142
routes/shop.route.js Normal file
View File

@ -0,0 +1,142 @@
/* ============================================================
routes/shop.route.js
Stripe Shop Gems kaufen
.env benötigt:
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
APP_URL=https://spiel.dynastyofknights.com
============================================================ */
const express = require("express");
const router = express.Router();
const db = require("../database/database");
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
function requireLogin(req, res, next) {
if (!req.session?.user) return res.status(401).json({ error: "Nicht eingeloggt" });
next();
}
/*
Shop-Pakete (Preis in Cent!)
*/
const PACKAGES = [
{ id: "starter", name: "Starter", gems: 100, price: 199, bonus: 0 },
{ id: "abenteurer", name: "Abenteurer", gems: 500, price: 799, bonus: 50 },
{ id: "ritter", name: "Ritter", gems: 1000, price: 1499, bonus: 150 },
{ id: "fuerst", name: "Fürst", gems: 2500, price: 3499, bonus: 500 },
{ id: "koenig", name: "König", gems: 5000, price: 5999, bonus: 1500 },
];
/*
GET /api/shop/packages
Pakete für Frontend laden
*/
router.get("/shop/packages", requireLogin, (req, res) => {
res.json(PACKAGES);
});
/*
POST /api/shop/checkout
Stripe Checkout Session erstellen
Body: { packageId: "ritter" }
*/
router.post("/shop/checkout", requireLogin, async (req, res) => {
const userId = req.session.user.id;
const { packageId } = req.body;
const pkg = PACKAGES.find(p => p.id === packageId);
if (!pkg) return res.status(400).json({ error: "Paket nicht gefunden." });
try {
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card", "paypal"],
line_items: [{
price_data: {
currency: "eur",
product_data: {
name: `${pkg.name} Paket ${pkg.gems + pkg.bonus} 💠 Gems`,
description: pkg.bonus > 0
? `${pkg.gems} Gems + ${pkg.bonus} Bonus-Gems`
: `${pkg.gems} Gems`,
images: [`${process.env.APP_URL}/images/items/blauer-cristal.png`],
},
unit_amount: pkg.price,
},
quantity: 1,
}],
mode: "payment",
success_url: `${process.env.APP_URL}/launcher?payment=success`,
cancel_url: `${process.env.APP_URL}/launcher?payment=cancel`,
metadata: {
userId: String(userId),
packageId: pkg.id,
gems: String(pkg.gems + pkg.bonus),
},
});
res.json({ url: session.url });
} catch (err) {
console.error("Stripe Fehler:", err);
res.status(500).json({ error: "Zahlung konnte nicht gestartet werden." });
}
});
/*
POST /api/shop/webhook
Stripe Webhook Zahlung bestätigt
WICHTIG: Muss VOR express.json() registriert
werden raw body nötig!
*/
router.post(
"/shop/webhook",
express.raw({ type: "application/json" }),
async (req, res) => {
const sig = req.headers["stripe-signature"];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error("Webhook Signatur-Fehler:", err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
if (event.type === "checkout.session.completed") {
const session = event.data.object;
const userId = parseInt(session.metadata.userId);
const gems = parseInt(session.metadata.gems);
const packageId = session.metadata.packageId;
try {
/* Gems gutschreiben */
await db.query(
"UPDATE account_currency SET gems = gems + ? WHERE account_id = ?",
[gems, userId]
);
/* Kauf protokollieren */
await db.query(
`INSERT INTO shop_purchases
(user_id, package_id, gems, stripe_session_id, created_at)
VALUES (?, ?, ?, ?, NOW())`,
[userId, packageId, gems, session.id]
);
console.log(`✅ Shop: User ${userId} erhält ${gems} Gems (${packageId})`);
} catch (err) {
console.error("Gems gutschreiben Fehler:", err);
return res.status(500).end();
}
}
res.json({ received: true });
}
);
module.exports = { router, PACKAGES };

View File

@ -667,7 +667,7 @@
<span class="hud-res-icon">🪙</span>
<span class="hud-res-value" id="hud-gold">0</span>
</div>
<button id="hud-gold-btn">Gold</button>
<button id="hud-gold-btn">Shop</button>
</div>
<div class="hud-res-row">
@ -892,6 +892,22 @@
<div class="qm-popup-body" id="qm-body-boosterjagd"></div>
</div>
<!-- ================================
Shop Popup
================================ -->
<div id="shop-overlay"></div>
<div id="shop-popup">
<div id="shop-header">
<span id="shop-title">💠 Gems kaufen</span>
<span id="shop-close">✕</span>
</div>
<div id="shop-subtitle">Unterstütze das Spiel und erhalte wertvolle Gems!</div>
<div id="shop-grid"></div>
<div id="shop-footer">
Sichere Zahlung via Stripe · Kreditkarte & PayPal akzeptiert
</div>
</div>
<div id="game-notification-overlay"></div>
<div id="game-notification">
<div class="notification-header">
@ -1022,7 +1038,23 @@
<script type="module" src="/js/quickmenu.js?v=2"></script>
<script type="module">
import { loadHud } from "/js/hud.js?v=2";
import { openShop } from "/js/shop.js";
loadHud();
// Shop-Button
document.getElementById("hud-gold-btn")
?.addEventListener("click", openShop);
// Stripe Rückkehr-Benachrichtigung
const params = new URLSearchParams(window.location.search);
if (params.get("payment") === "success") {
window.showNotification("Zahlung erfolgreich! Deine Gems wurden gutgeschrieben. 💠", "Shop", "💠");
history.replaceState({}, "", "/launcher");
loadHud(); // HUD sofort aktualisieren
} else if (params.get("payment") === "cancel") {
window.showNotification("Zahlung abgebrochen.", "Shop", "💠");
history.replaceState({}, "", "/launcher");
}
</script>
<script>