diff --git a/astro.config.mjs b/astro.config.mjs index bac8f1c..475be68 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -4,7 +4,7 @@ import sitemap from '@astrojs/sitemap'; // https://astro.build/config // TODO Phase 2: echte Domain eintragen, sobald Cloudflare-Domain steht (siehe Tech-Stack-Entscheidung.md) -const SITE_URL = 'https://www.vans-diy-bastelbedarf.de'; +const SITE_URL = 'https://vans-diy-bastelbedarf.com'; export default defineConfig({ site: SITE_URL, diff --git a/public/robots.txt b/public/robots.txt index 1d6b031..ad393c6 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,4 +1,4 @@ User-agent: * Allow: / -Sitemap: https://vans-diy-bastelbedarf.de/sitemap-index.xml +Sitemap: https://vans-diy-bastelbedarf.com/sitemap-index.xml diff --git a/server/.env.example b/server/.env.example index 67c619c..592c744 100644 --- a/server/.env.example +++ b/server/.env.example @@ -7,6 +7,9 @@ DB_PATH=/var/lib/vandiy-shop/vandiy-shop.db # Port, auf dem der Node-Server lokal lauscht (Caddy leitet von außen dorthin weiter) PORT=4000 +# Produktivbetrieb: sorgt u.a. dafuer, dass im Fehlerfall keine internen Details nach aussen gehen. +NODE_ENV=production + # Zugangscode-Schranke (ganze Seite) — selbst gewählte, zufällige Werte SITE_ACCESS_SECRET= SITE_ACCESS_CODE_QCIGA= diff --git a/server/index.js b/server/index.js index ba0c8c5..6536aa4 100644 --- a/server/index.js +++ b/server/index.js @@ -35,6 +35,19 @@ const app = express(); // Läuft hinter Caddy (Reverse Proxy) — nötig, damit req.protocol/req.get("host") und // "secure"-Cookies korrekt funktionieren, statt anzunehmen, die Verbindung sei unverschlüsselt. app.set("trust proxy", 1); +app.disable("x-powered-by"); // verrät sonst unnötig den eingesetzten Technik-Stack + +/* Sicherheits-Header, ergänzt 05.08.2026 (bei Cloudflare Pages kamen die teils von der Plattform; + auf dem eigenen Server muss der Server sie selbst setzen). `payment=(self)` bleibt erlaubt, + damit die PayPal-Zahlung im eigenen Fenster weiter funktioniert. */ +app.use((req, res, next) => { + res.setHeader("X-Content-Type-Options", "nosniff"); + res.setHeader("X-Frame-Options", "SAMEORIGIN"); + res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); + res.setHeader("Permissions-Policy", "geolocation=(), microphone=(), camera=(), payment=(self)"); + res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); + next(); +}); app.use(cookieParser()); app.use(express.json({ limit: "2mb" })); @@ -63,6 +76,32 @@ app.use((req, res) => { }); }); +/* ---------- Fehler-Handler (muss NACH allen Routen stehen), ergänzt 05.08.2026 ---------- + Express 4 fängt Fehler aus async-Handlern nicht selbst ab. Ohne diesen Handler landete kaputtes + JSON in Express' Standard-Fehlerseite — inklusive komplettem Stacktrace mit Serverpfaden, solange + NODE_ENV nicht auf "production" steht. Jetzt kommt in jedem Fall eine saubere JSON-Antwort. */ +// eslint-disable-next-line no-unused-vars +app.use((err, req, res, next) => { + if (err?.type === "entity.parse.failed") { + return res.status(400).json({ ok: false, error: "Ungültige Anfrage." }); + } + if (err?.type === "entity.too.large") { + return res.status(413).json({ ok: false, error: "Die Anfrage ist zu groß." }); + } + console.error(`[fehler] ${req.method} ${req.originalUrl}:`, err?.stack || err); + if (res.headersSent) return; + if (req.path.startsWith("/api/")) { + return res.status(500).json({ ok: false, error: "Interner Fehler." }); + } + return res.status(500).send("Es ist ein Fehler aufgetreten. Bitte später erneut versuchen."); +}); + +/* Letztes Netz: Ein unbehandelter Fehler darf den Shop nicht offline nehmen — besonders wichtig + direkt nach einer PayPal-Abbuchung, wo ein Absturz die Kundin ohne Rückmeldung zurücklassen + würde, obwohl das Geld bereits eingezogen ist. */ +process.on("unhandledRejection", (grund) => console.error("[unhandledRejection]", grund)); +process.on("uncaughtException", (fehler) => console.error("[uncaughtException]", fehler?.stack || fehler)); + app.listen(PORT, "127.0.0.1", () => { console.log(`Van's DIY & Bastelbedarf – Server läuft auf http://127.0.0.1:${PORT}`); }); diff --git a/server/lib/async-wrap.js b/server/lib/async-wrap.js new file mode 100644 index 0000000..a81db42 --- /dev/null +++ b/server/lib/async-wrap.js @@ -0,0 +1,16 @@ +/* ===================================================================== + lib/async-wrap.js — kleiner Helfer, ergänzt am 05.08.2026. + + Warum es das braucht: Express 4 fängt Fehler aus `async`-Route-Handlern NICHT selbst ab. Eine + abgelehnte Promise wird zu einer "unhandled rejection" — und Node beendet daraufhin den ganzen + Prozess. Bei Cloudflare Pages war ein Fehler dagegen immer auf die eine betroffene Anfrage + begrenzt; dort konnte ein einzelner Datenbankfehler niemals den Shop offline nehmen. + + Besonders kritisch bei /api/paypal/capture-order: dort würde ein Absturz genau in dem Moment + passieren, in dem das Geld bei PayPal bereits abgebucht wurde — die Kundin bekäme statt der + sorgfältig formulierten "bitte NICHT erneut bezahlen"-Meldung nur einen Verbindungsabbruch. + + `wrap()` leitet jeden Fehler stattdessen an den zentralen Fehler-Handler in index.js weiter. + ===================================================================== */ + +export const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); diff --git a/server/lib/bestellung-erstellen.js b/server/lib/bestellung-erstellen.js index 5fe65c9..59aa9aa 100644 --- a/server/lib/bestellung-erstellen.js +++ b/server/lib/bestellung-erstellen.js @@ -42,7 +42,7 @@ function bestellBenachrichtigungHtml({ bestellnummer, name, email, land, artikel
Artikel:
Summe: ${formatPreis(summe)}
- + `; } diff --git a/server/middleware/gate.js b/server/middleware/gate.js index 1b140b5..ae8e6e3 100644 --- a/server/middleware/gate.js +++ b/server/middleware/gate.js @@ -123,5 +123,5 @@ export function gateMiddleware(req, res, next) { const nextParam = pfad + (req.originalUrl.includes("?") ? "?" + req.originalUrl.split("?")[1] : ""); return res.redirect(302, `/gate?next=${encodeURIComponent(nextParam)}`); - })(); + })().catch(next); // ergänzt 05.08.2026: verhindert Serverabsturz bei einem Fehler in der Prüfung } diff --git a/server/routes/account.js b/server/routes/account.js index 27009ff..902d96c 100644 --- a/server/routes/account.js +++ b/server/routes/account.js @@ -4,26 +4,27 @@ ===================================================================== */ import { Router } from "express"; +import { wrap } from "../lib/async-wrap.js"; import { getCustomer, clearCustomerCookie } from "../lib/customer-auth.js"; import { db } from "../db.js"; export const accountRouter = Router(); -accountRouter.get("/api/account/me", async (req, res) => { +accountRouter.get("/api/account/me", wrap(async (req, res) => { const customer = await getCustomer(req); if (!customer) return res.json({ ok: false }); return res.json({ ok: true, customer: { email: customer.email, name: customer.name, provider: customer.provider, seit: customer.created_at }, }); -}); +})); accountRouter.post("/api/account/logout", (req, res) => { clearCustomerCookie(res); return res.json({ ok: true }); }); -accountRouter.post("/api/account/delete", async (req, res) => { +accountRouter.post("/api/account/delete", wrap(async (req, res) => { const customer = await getCustomer(req); if (!customer) return res.status(401).json({ ok: false, error: "Nicht angemeldet." }); @@ -35,9 +36,9 @@ accountRouter.post("/api/account/delete", async (req, res) => { clearCustomerCookie(res); return res.json({ ok: true }); -}); +})); -accountRouter.get("/api/account/export", async (req, res) => { +accountRouter.get("/api/account/export", wrap(async (req, res) => { const customer = await getCustomer(req); if (!customer) return res.status(401).json({ ok: false, error: "Nicht angemeldet." }); @@ -51,4 +52,4 @@ accountRouter.get("/api/account/export", async (req, res) => { res.setHeader("Content-Disposition", 'attachment; filename="meine-daten.json"'); return res.status(200).json(daten); -}); +})); diff --git a/server/routes/auth.js b/server/routes/auth.js index b342c31..296bec7 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -9,7 +9,11 @@ import { starteOAuth, verarbeiteOAuthCallback } from "../lib/oauth-handlers.js"; export const authRouter = Router(); -authRouter.get("/api/auth/google/start", (req, res) => starteOAuth(req, res, "google")); -authRouter.get("/api/auth/google/callback", (req, res) => verarbeiteOAuthCallback(req, res, "google")); -authRouter.get("/api/auth/paypal/start", (req, res) => starteOAuth(req, res, "paypal")); -authRouter.get("/api/auth/paypal/callback", (req, res) => verarbeiteOAuthCallback(req, res, "paypal")); +/* wrap() ergänzt 05.08.2026: Express 4 fängt Fehler aus async-Handlern nicht selbst ab — ohne das + würde z.B. ein Netzwerkfehler beim Token-Austausch mit Google/PayPal den ganzen Shop beenden. */ +const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); + +authRouter.get("/api/auth/google/start", wrap((req, res) => starteOAuth(req, res, "google"))); +authRouter.get("/api/auth/google/callback", wrap((req, res) => verarbeiteOAuthCallback(req, res, "google"))); +authRouter.get("/api/auth/paypal/start", wrap((req, res) => starteOAuth(req, res, "paypal"))); +authRouter.get("/api/auth/paypal/callback", wrap((req, res) => verarbeiteOAuthCallback(req, res, "paypal"))); diff --git a/server/routes/orders.js b/server/routes/orders.js index 652b802..cf017ae 100644 --- a/server/routes/orders.js +++ b/server/routes/orders.js @@ -5,11 +5,12 @@ ===================================================================== */ import { Router } from "express"; +import { wrap } from "../lib/async-wrap.js"; import { erstelleBestellung } from "../lib/bestellung-erstellen.js"; export const ordersRouter = Router(); -ordersRouter.post("/api/orders", async (req, res) => { +ordersRouter.post("/api/orders", wrap(async (req, res) => { const body = req.body || {}; const zahlungsart = String(body.zahlungsart || ""); @@ -28,4 +29,4 @@ ordersRouter.post("/api/orders", async (req, res) => { return res.status(ergebnis.httpStatus || 500).json({ ok: false, error: ergebnis.error }); } return res.status(201).json({ ok: true, id: ergebnis.id, bestellnummer: ergebnis.bestellnummer }); -}); +})); diff --git a/server/routes/paypal.js b/server/routes/paypal.js index f4f768b..88a8406 100644 --- a/server/routes/paypal.js +++ b/server/routes/paypal.js @@ -6,6 +6,7 @@ ===================================================================== */ import { Router } from "express"; +import { wrap } from "../lib/async-wrap.js"; import { paypalKonfiguriert, erstellePaypalBestellung, erfassePaypalZahlung } from "../lib/paypal.js"; import { erstelleBestellung } from "../lib/bestellung-erstellen.js"; @@ -13,7 +14,7 @@ export const paypalRouter = Router(); const BETRAG_TOLERANZ_EURO = 0.02; -paypalRouter.post("/api/paypal/create-order", async (req, res) => { +paypalRouter.post("/api/paypal/create-order", wrap(async (req, res) => { if (!paypalKonfiguriert()) { return res.status(503).json({ ok: false, @@ -33,9 +34,9 @@ paypalRouter.post("/api/paypal/create-order", async (req, res) => { } catch (err) { return res.status(502).json({ ok: false, error: err instanceof Error ? err.message : "PayPal-Bestellung fehlgeschlagen." }); } -}); +})); -paypalRouter.post("/api/paypal/capture-order", async (req, res) => { +paypalRouter.post("/api/paypal/capture-order", wrap(async (req, res) => { if (!paypalKonfiguriert()) { return res.status(503).json({ ok: false, error: "PayPal ist auf dieser Seite noch nicht eingerichtet." }); } @@ -72,4 +73,4 @@ paypalRouter.post("/api/paypal/capture-order", async (req, res) => { } return res.status(201).json({ ok: true, id: ergebnis.id, bestellnummer: ergebnis.bestellnummer }); -}); +})); diff --git a/server/routes/verwaltung.js b/server/routes/verwaltung.js index 6d74dea..6ffce24 100644 --- a/server/routes/verwaltung.js +++ b/server/routes/verwaltung.js @@ -6,6 +6,7 @@ ===================================================================== */ import { Router } from "express"; +import { wrap } from "../lib/async-wrap.js"; import { getGateRole, unauthorizedJson } from "../lib/auth.js"; import { hatGueltigeVerwaltungSitzung } from "../lib/verwaltung-auth.js"; import { db } from "../db.js"; @@ -27,7 +28,7 @@ async function pruefeSchranken(req, res) { return true; } -verwaltungRouter.get("/api/verwaltung/orders", async (req, res) => { +verwaltungRouter.get("/api/verwaltung/orders", wrap(async (req, res) => { if (!(await pruefeSchranken(req, res))) return; try { @@ -50,9 +51,9 @@ verwaltungRouter.get("/api/verwaltung/orders", async (req, res) => { } catch (err) { return res.status(500).json({ ok: false, error: "Bestellungen konnten nicht geladen werden." }); } -}); +})); -verwaltungRouter.patch("/api/verwaltung/orders/:id", async (req, res) => { +verwaltungRouter.patch("/api/verwaltung/orders/:id", wrap(async (req, res) => { if (!(await pruefeSchranken(req, res))) return; const id = Number(req.params.id); @@ -92,4 +93,4 @@ verwaltungRouter.patch("/api/verwaltung/orders/:id", async (req, res) => { } catch (err) { return res.status(500).json({ ok: false, error: "Bestellung konnte nicht aktualisiert werden." }); } -}); +})); diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts index 57ef8c1..d90fe3d 100644 --- a/src/i18n/ui.ts +++ b/src/i18n/ui.ts @@ -325,7 +325,7 @@ export const ui = { }, }, en: { - nav: { shop: "Shop", sale: "Sale", creative: "About the Maker", faq: "FAQ", contact: "Contact", account: "My Account", login: "Sign in" }, + nav: { shop: "Shop", sale: "Sale", creative: "About the Maker", faq: "FAQ", contact: "Contact", account: "My Account", login: "Sign in", cart: "Cart" }, accountMenu: { myAccount: "My Account", logout: "Sign out" }, topbar: "Free shipping from €75 (DE) / €85 (AT, LU) / €100 (CH)", footer: { @@ -643,7 +643,7 @@ export const ui = { }, }, ch: { - nav: { shop: "Shop", sale: "Aktion", creative: "Über d'Kreativi", faq: "FAQ", contact: "Kontakt", account: "Mis Konto", login: "Aamälde" }, + nav: { shop: "Shop", sale: "Aktion", creative: "Über d'Kreativi", faq: "FAQ", contact: "Kontakt", account: "Mis Konto", login: "Aamälde", cart: "Warechorb" }, accountMenu: { myAccount: "Mis Konto", logout: "Abmälde" }, topbar: "Gratisversand ab 75 € (DE) / 85 € (AT, LU) / 100 € (CH)", footer: { @@ -961,7 +961,7 @@ export const ui = { }, }, fr: { - nav: { shop: "Boutique", sale: "Soldes", creative: "À propos de la créatrice", faq: "FAQ", contact: "Contact", account: "Mon compte", login: "Se connecter" }, + nav: { shop: "Boutique", sale: "Soldes", creative: "À propos de la créatrice", faq: "FAQ", contact: "Contact", account: "Mon compte", login: "Se connecter", cart: "Panier" }, accountMenu: { myAccount: "Mon compte", logout: "Se déconnecter" }, topbar: "Livraison gratuite dès 75 € (DE) / 85 € (AT, LU) / 100 € (CH)", footer: { diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 717e2d8..ad6ad5d 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -51,7 +51,7 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;