From 4b96e1a6b0735bdb75b9b8d2bdb557c73b252cb6 Mon Sep 17 00:00:00 2001 From: karim Date: Sat, 30 May 2026 15:49:47 +0200 Subject: [PATCH] fix: Platzhalter-Keys nicht als echt werten + Crash-Schutz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - env.js: '…CHANGE-ME'-Platzhalter aus .env.example zählen als NICHT gesetzt. Vorher galt sk_test_CHANGE-ME als echter Stripe-Key → echter API-Call mit ungültigem Key → 401 → unhandledRejection → Server-Crash. - billing.js: /checkout in try/catch → 502 statt Empty-Reply/Crash. - index.js: globaler Express-Error-Handler + unhandledRejection-Guard, damit ein einzelner async-Fehler nie den ganzen Prozess killt. E2E verifiziert (Mock): register→checkout→instance, idempotent (1 sub/1 inst), 401 bei falschem PW, Server lebt nach allen Requests. Co-Authored-By: Claude Opus 4.8 --- server/env.js | 8 ++++-- server/index.js | 11 +++++++++ server/routes/billing.js | 53 ++++++++++++++++++++++------------------ 3 files changed, 46 insertions(+), 26 deletions(-) diff --git a/server/env.js b/server/env.js index 147ea41..e4099cd 100644 --- a/server/env.js +++ b/server/env.js @@ -46,8 +46,12 @@ export const env = { // MOCK-Modus: ohne echte Stripe-/Rapport-Keys läuft alles lokal simuliert, // damit der End-to-End-Flow ohne externe Dienste testbar ist. -export const stripeEnabled = !!env.stripe.secretKey && env.stripe.secretKey.startsWith("sk_"); -export const provisioningMock = !env.rapport.apiUrl || !env.rapport.serviceKey; +// WICHTIG: Platzhalter-Werte aus .env.example (…CHANGE-ME) zählen als NICHT +// gesetzt — sonst würde ein sk_test_CHANGE-ME als echter Key durchgehen und +// echte Stripe-Calls mit ungültigem Key abfeuern (401 → Crash). +const isPlaceholder = (v) => !v || v.includes("CHANGE-ME"); +export const stripeEnabled = !isPlaceholder(env.stripe.secretKey) && env.stripe.secretKey.startsWith("sk_"); +export const provisioningMock = isPlaceholder(env.rapport.apiUrl) || isPlaceholder(env.rapport.serviceKey); if (env.jwtSecret === "dev-insecure-secret-change-me") { console.warn("⚠ JWT_SECRET nicht gesetzt — unsicheres Dev-Secret in Verwendung."); diff --git a/server/index.js b/server/index.js index 71a65f8..a03eb25 100644 --- a/server/index.js +++ b/server/index.js @@ -29,6 +29,17 @@ app.get("*", (req, res, next) => { res.sendFile(path.join(dist, "index.html"), (err) => err && next()); }); +// Express-Fehlerhandler: fängt synchron geworfene Fehler aus Routen → 500 +// statt stiller Empty-Reply. +app.use((err, _req, res, _next) => { + console.error("Unbehandelter Routen-Fehler:", err); + if (!res.headersSent) res.status(500).json({ error: "Interner Fehler." }); +}); + +// Letzte Verteidigungslinie: ein einzelner async-Fehler darf den Prozess nicht +// killen (im Dev-Test sonst "connection refused" für alle Folge-Requests). +process.on("unhandledRejection", (reason) => console.error("unhandledRejection:", reason)); + app.listen(env.port, () => { console.log(`RAPPORT-HOST API läuft auf :${env.port} (Base: ${env.publicBaseUrl})`); }); diff --git a/server/routes/billing.js b/server/routes/billing.js index e7e25fc..0e6ff44 100644 --- a/server/routes/billing.js +++ b/server/routes/billing.js @@ -50,31 +50,36 @@ billingRouter.post("/checkout", requireAuth, async (req, res) => { const plan = getPlan(req.body?.planId); if (!plan) return res.status(400).json({ error: "Unbekannter Plan." }); - // ── MOCK: sofort freischalten ────────────────────────────────────────────── - if (!stripeEnabled) { - await fulfill({ - accountId: req.account.id, - plan, - stripeCustomerId: "mock_cus_" + req.account.id, - stripeSubscriptionId: "mock_sub_" + req.account.id + "_" + plan.id, - status: "active", - periodEnd: new Date(Date.now() + 30 * 864e5), - }); - return res.json({ mock: true, url: `${env.publicBaseUrl}/dashboard?provisioned=1` }); - } + try { + // ── MOCK: sofort freischalten ──────────────────────────────────────────── + if (!stripeEnabled) { + await fulfill({ + accountId: req.account.id, + plan, + stripeCustomerId: "mock_cus_" + req.account.id, + stripeSubscriptionId: "mock_sub_" + req.account.id + "_" + plan.id, + status: "active", + periodEnd: new Date(Date.now() + 30 * 864e5), + }); + return res.json({ mock: true, url: `${env.publicBaseUrl}/dashboard?provisioned=1` }); + } - // ── Stripe-Checkout-Session ──────────────────────────────────────────────── - if (!plan.stripePriceId) return res.status(500).json({ error: "Kein Stripe-Price für diesen Plan konfiguriert." }); - const session = await stripe.checkout.sessions.create({ - mode: "subscription", - line_items: [{ price: plan.stripePriceId, quantity: 1 }], - customer_email: req.account.email, - client_reference_id: req.account.id, - metadata: { accountId: req.account.id, planId: plan.id }, - success_url: `${env.publicBaseUrl}/dashboard?provisioned=1`, - cancel_url: `${env.publicBaseUrl}/plans?canceled=1`, - }); - res.json({ url: session.url }); + // ── Stripe-Checkout-Session ────────────────────────────────────────────── + if (!plan.stripePriceId) return res.status(500).json({ error: "Kein Stripe-Price für diesen Plan konfiguriert." }); + const session = await stripe.checkout.sessions.create({ + mode: "subscription", + line_items: [{ price: plan.stripePriceId, quantity: 1 }], + customer_email: req.account.email, + client_reference_id: req.account.id, + metadata: { accountId: req.account.id, planId: plan.id }, + success_url: `${env.publicBaseUrl}/dashboard?provisioned=1`, + cancel_url: `${env.publicBaseUrl}/plans?canceled=1`, + }); + res.json({ url: session.url }); + } catch (err) { + console.error("checkout fehlgeschlagen:", err.message); + res.status(502).json({ error: "Checkout fehlgeschlagen: " + err.message }); + } }); // Stripe-Webhook. Braucht RAW Body (Signaturprüfung) — in index.js so verdrahtet.