Sicherheits-, Stabilitaets- und Domainkorrekturen nach vollstaendiger Pruefung
Build & Deploy / deploy (push) Waiting to run
Build & Deploy / deploy (push) Waiting to run
- Absturzsicherheit: Express 4 faengt Fehler aus async-Handlern nicht ab, eine einzige fehlerhafte Anfrage konnte den ganzen Shop beenden - besonders heikel bei capture-order, wo das direkt nach der PayPal-Abbuchung passieren wuerde. Neu: lib/async-wrap.js um alle Handler, zentraler Fehler-Handler in index.js, .catch(next) in der Zugangsschranke, unhandledRejection/uncaughtException-Netz. - Fehlerhaftes JSON lieferte bisher Express' HTML-Fehlerseite mit komplettem Stacktrace und Serverpfaden (weil NODE_ENV nicht gesetzt war). Jetzt saubere JSON-Antwort, NODE_ENV=production in .env.example ergaenzt. - Sicherheits-Header und x-powered-by wie bei der DogFather-Seite. - Falsche Domain .de statt .com korrigiert: astro.config.mjs (betraf alle Canonical-URLs und die komplette Sitemap), robots.txt, Layout.astro sowie den Verwaltungs-Link in jeder Bestellbenachrichtigung (war ein toter Link). - Fehlende Uebersetzung nav.cart in EN/CH/FR ergaenzt: das Warenkorb-Symbol hatte in drei von vier Sprachen keinen Namen fuer Screenreader.
This commit is contained in:
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
@@ -42,7 +42,7 @@ function bestellBenachrichtigungHtml({ bestellnummer, name, email, land, artikel
|
||||
<p><strong>Artikel:</strong></p>
|
||||
<ul>${artikelZeilen}</ul>
|
||||
<p><strong>Summe: ${formatPreis(summe)}</strong></p>
|
||||
<p><a href="https://vans-diy-bastelbedarf.de/verwaltung/">→ Zur Bestellverwaltung</a></p>
|
||||
<p><a href="https://vans-diy-bastelbedarf.com/verwaltung/">→ Zur Bestellverwaltung</a></p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -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")));
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -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." });
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
+3
-3
@@ -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: {
|
||||
|
||||
@@ -51,7 +51,7 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;
|
||||
<title>{title} · Van's DIY & Bastelbedarf</title>
|
||||
<meta name="description" content={description} />
|
||||
{noindex && <meta name="robots" content="noindex, nofollow" />}
|
||||
<link rel="canonical" href={new URL(Astro.url.pathname, Astro.site ?? "https://vans-diy-bastelbedarf.de").toString()} />
|
||||
<link rel="canonical" href={new URL(Astro.url.pathname, Astro.site ?? "https://vans-diy-bastelbedarf.com").toString()} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Van's DIY & Bastelbedarf" />
|
||||
<meta property="og:title" content={title} />
|
||||
|
||||
Reference in New Issue
Block a user