Compare commits

...
3 Commits
Author SHA1 Message Date
qcigano ab3a5da892 Sicherheits-, Stabilitaets- und Domainkorrekturen nach vollstaendiger Pruefung
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.
2026-08-05 22:33:49 +02:00
qcigano 654f4455cc Zugangs-Sperre: Code wird nach dem Schliessen des Browsers wieder gefragt
Build & Deploy / deploy (push) Waiting to run
Sitzungs-Cookie ohne maxAge/expires statt 90-Tage-Cookie (auch fuer die
zweite Verwaltungs-Schranke); zusaetzlich Notbremse von 12 Stunden im
signierten Token, falls ein Browser sehr lange offen bleibt.
Kundenkonten (customer-auth.js, 30 Tage) bleiben bewusst unveraendert.
2026-08-05 20:00:34 +02:00
qcigano c9e4b0cdbb Fix: dotenv-Paket ergaenzen, Node-Mindestversion auf 20 gesenkt
Build & Deploy / deploy (push) Waiting to run
2026-08-05 16:36:32 +02:00
16 changed files with 115 additions and 41 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ import sitemap from '@astrojs/sitemap';
// https://astro.build/config // https://astro.build/config
// TODO Phase 2: echte Domain eintragen, sobald Cloudflare-Domain steht (siehe Tech-Stack-Entscheidung.md) // 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({ export default defineConfig({
site: SITE_URL, site: SITE_URL,
+1 -1
View File
@@ -1,4 +1,4 @@
User-agent: * User-agent: *
Allow: / Allow: /
Sitemap: https://vans-diy-bastelbedarf.de/sitemap-index.xml Sitemap: https://vans-diy-bastelbedarf.com/sitemap-index.xml
+3
View File
@@ -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, auf dem der Node-Server lokal lauscht (Caddy leitet von außen dorthin weiter)
PORT=4000 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 # Zugangscode-Schranke (ganze Seite) — selbst gewählte, zufällige Werte
SITE_ACCESS_SECRET= SITE_ACCESS_SECRET=
SITE_ACCESS_CODE_QCIGA= SITE_ACCESS_CODE_QCIGA=
+42 -3
View File
@@ -4,11 +4,11 @@
einem Deployment vereint hat. Die Zugangscode-Schranke (middleware/gate.js) läuft vor JEDER einem Deployment vereint hat. Die Zugangscode-Schranke (middleware/gate.js) läuft vor JEDER
Anfrage, exakt wie functions/_middleware.js das vorher bei Cloudflare tat. Anfrage, exakt wie functions/_middleware.js das vorher bei Cloudflare tat.
Umgebungsvariablen kommen aus einer .env-Datei — gestartet wird dieser Server mit Umgebungsvariablen kommen aus einer .env-Datei im server/-Ordner (siehe .env.example), geladen
"node --env-file=.env index.js" (Node 22 kann .env-Dateien nativ laden, kein zusätzliches über das "dotenv"-Paket direkt beim Start.
Paket nötig).
===================================================================== */ ===================================================================== */
import "dotenv/config";
import express from "express"; import express from "express";
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
@@ -35,6 +35,19 @@ const app = express();
// Läuft hinter Caddy (Reverse Proxy) — nötig, damit req.protocol/req.get("host") und // 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. // "secure"-Cookies korrekt funktionieren, statt anzunehmen, die Verbindung sei unverschlüsselt.
app.set("trust proxy", 1); 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(cookieParser());
app.use(express.json({ limit: "2mb" })); 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", () => { app.listen(PORT, "127.0.0.1", () => {
console.log(`Van's DIY & Bastelbedarf Server läuft auf http://127.0.0.1:${PORT}`); console.log(`Van's DIY & Bastelbedarf Server läuft auf http://127.0.0.1:${PORT}`);
}); });
+16
View File
@@ -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);
+10 -2
View File
@@ -9,7 +9,15 @@
export const COOKIE_NAME = "vandiy_gate_session"; export const COOKIE_NAME = "vandiy_gate_session";
export const ROLE_COOKIE_NAME = "vandiy_gate_role"; export const ROLE_COOKIE_NAME = "vandiy_gate_role";
export const SESSION_TAGE = 90;
/* Zugang gilt nur für die laufende Browser-Sitzung (Wunsch 05.08.2026: "jedes Mal wenn wir den
Browser zu machen, soll der Code wieder gefragt werden"). Umgesetzt über ein reines
Sitzungs-Cookie OHNE maxAge/expires in middleware/gate.js — das löscht der Browser beim
Schließen selbst. SESSION_STUNDEN ist nur die zusätzliche Notbremse im signierten Token für
den Fall, dass ein Browser tage-/wochenlang offen bleibt (dann läuft der Zugang trotzdem ab).
Betrifft NUR die Zugangsschranke — Kundenkonten (customer-auth.js, 30 Tage) bleiben unberührt,
echte Kund:innen sollen sich nicht bei jedem Besuch neu anmelden müssen. */
export const SESSION_STUNDEN = 12;
function b64urlEncode(bytes) { function b64urlEncode(bytes) {
let bin = ""; let bin = "";
@@ -34,7 +42,7 @@ async function hmacKey(secret) {
} }
export async function signSession(role, secret) { export async function signSession(role, secret) {
const payload = JSON.stringify({ role, exp: Date.now() + SESSION_TAGE * 24 * 60 * 60 * 1000 }); const payload = JSON.stringify({ role, exp: Date.now() + SESSION_STUNDEN * 60 * 60 * 1000 });
const payloadB64 = b64urlEncode(new TextEncoder().encode(payload)); const payloadB64 = b64urlEncode(new TextEncoder().encode(payload));
const key = await hmacKey(secret); const key = await hmacKey(secret);
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payloadB64)); const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payloadB64));
+1 -1
View File
@@ -42,7 +42,7 @@ function bestellBenachrichtigungHtml({ bestellnummer, name, email, land, artikel
<p><strong>Artikel:</strong></p> <p><strong>Artikel:</strong></p>
<ul>${artikelZeilen}</ul> <ul>${artikelZeilen}</ul>
<p><strong>Summe: ${formatPreis(summe)}</strong></p> <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> </div>
`; `;
} }
+7 -8
View File
@@ -12,10 +12,9 @@
- API-Routen bekommen bei fehlender Sitzung 401-JSON statt HTML-Redirect. - API-Routen bekommen bei fehlender Sitzung 401-JSON statt HTML-Redirect.
===================================================================== */ ===================================================================== */
import { COOKIE_NAME, ROLE_COOKIE_NAME, SESSION_TAGE, signSession, verifySession } from "../lib/auth.js"; import { COOKIE_NAME, ROLE_COOKIE_NAME, signSession, verifySession } from "../lib/auth.js";
import { import {
VERWALTUNG_COOKIE_NAME, VERWALTUNG_COOKIE_NAME,
VERWALTUNG_SESSION_STUNDEN,
signVerwaltungSession, signVerwaltungSession,
verifyVerwaltungSession, verifyVerwaltungSession,
} from "../lib/verwaltung-auth.js"; } from "../lib/verwaltung-auth.js";
@@ -44,9 +43,9 @@ export async function gateAuthHandler(req, res) {
} }
const session = await signSession(role, process.env.SITE_ACCESS_SECRET); const session = await signSession(role, process.env.SITE_ACCESS_SECRET);
const maxAge = SESSION_TAGE * 24 * 60 * 60 * 1000; // Bewusst OHNE maxAge/expires → reines Sitzungs-Cookie, verfällt beim Schließen des Browsers.
res.cookie(COOKIE_NAME, session, { path: "/", maxAge, httpOnly: true, secure: true, sameSite: "lax" }); res.cookie(COOKIE_NAME, session, { path: "/", httpOnly: true, secure: true, sameSite: "lax" });
res.cookie(ROLE_COOKIE_NAME, role, { path: "/", maxAge, secure: true, sameSite: "lax" }); res.cookie(ROLE_COOKIE_NAME, role, { path: "/", secure: true, sameSite: "lax" });
return res.json({ ok: true, next }); return res.json({ ok: true, next });
} }
@@ -62,8 +61,8 @@ export async function verwaltungAuthHandler(req, res) {
} }
const session = await signVerwaltungSession(process.env.VERWALTUNG_SESSION_SECRET); const session = await signVerwaltungSession(process.env.VERWALTUNG_SESSION_SECRET);
const maxAge = VERWALTUNG_SESSION_STUNDEN * 60 * 60 * 1000; // Ebenfalls reines Sitzungs-Cookie (siehe gateAuthHandler oben).
res.cookie(VERWALTUNG_COOKIE_NAME, session, { path: "/", maxAge, httpOnly: true, secure: true, sameSite: "lax" }); res.cookie(VERWALTUNG_COOKIE_NAME, session, { path: "/", httpOnly: true, secure: true, sameSite: "lax" });
return res.json({ ok: true, next }); return res.json({ ok: true, next });
} }
@@ -124,5 +123,5 @@ export function gateMiddleware(req, res, next) {
const nextParam = pfad + (req.originalUrl.includes("?") ? "?" + req.originalUrl.split("?")[1] : ""); const nextParam = pfad + (req.originalUrl.includes("?") ? "?" + req.originalUrl.split("?")[1] : "");
return res.redirect(302, `/gate?next=${encodeURIComponent(nextParam)}`); return res.redirect(302, `/gate?next=${encodeURIComponent(nextParam)}`);
})(); })().catch(next); // ergänzt 05.08.2026: verhindert Serverabsturz bei einem Fehler in der Prüfung
} }
+2 -1
View File
@@ -4,7 +4,7 @@
"version": "1.0.0", "version": "1.0.0",
"description": "Eigener Node.js/Express-Server für Van's DIY & Bastelbedarf Ersatz für Cloudflare Pages Functions + D1, läuft auf dem eigenen Netcup-Server. Portiert 1:1 aus functions/ (Cloudflare Pages Functions), gleiche Sicherheitslogik, gleiche Datenbankstruktur (SQLite statt D1).", "description": "Eigener Node.js/Express-Server für Van's DIY & Bastelbedarf Ersatz für Cloudflare Pages Functions + D1, läuft auf dem eigenen Netcup-Server. Portiert 1:1 aus functions/ (Cloudflare Pages Functions), gleiche Sicherheitslogik, gleiche Datenbankstruktur (SQLite statt D1).",
"engines": { "engines": {
"node": ">=22.12.0" "node": ">=20.0.0"
}, },
"scripts": { "scripts": {
"start": "node index.js" "start": "node index.js"
@@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"better-sqlite3": "^11.8.1", "better-sqlite3": "^11.8.1",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2" "express": "^4.21.2"
} }
} }
+7 -6
View File
@@ -4,26 +4,27 @@
===================================================================== */ ===================================================================== */
import { Router } from "express"; import { Router } from "express";
import { wrap } from "../lib/async-wrap.js";
import { getCustomer, clearCustomerCookie } from "../lib/customer-auth.js"; import { getCustomer, clearCustomerCookie } from "../lib/customer-auth.js";
import { db } from "../db.js"; import { db } from "../db.js";
export const accountRouter = Router(); 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); const customer = await getCustomer(req);
if (!customer) return res.json({ ok: false }); if (!customer) return res.json({ ok: false });
return res.json({ return res.json({
ok: true, ok: true,
customer: { email: customer.email, name: customer.name, provider: customer.provider, seit: customer.created_at }, customer: { email: customer.email, name: customer.name, provider: customer.provider, seit: customer.created_at },
}); });
}); }));
accountRouter.post("/api/account/logout", (req, res) => { accountRouter.post("/api/account/logout", (req, res) => {
clearCustomerCookie(res); clearCustomerCookie(res);
return res.json({ ok: true }); 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); const customer = await getCustomer(req);
if (!customer) return res.status(401).json({ ok: false, error: "Nicht angemeldet." }); 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); clearCustomerCookie(res);
return res.json({ ok: true }); 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); const customer = await getCustomer(req);
if (!customer) return res.status(401).json({ ok: false, error: "Nicht angemeldet." }); 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"'); res.setHeader("Content-Disposition", 'attachment; filename="meine-daten.json"');
return res.status(200).json(daten); return res.status(200).json(daten);
}); }));
+8 -4
View File
@@ -9,7 +9,11 @@ import { starteOAuth, verarbeiteOAuthCallback } from "../lib/oauth-handlers.js";
export const authRouter = Router(); export const authRouter = Router();
authRouter.get("/api/auth/google/start", (req, res) => starteOAuth(req, res, "google")); /* wrap() ergänzt 05.08.2026: Express 4 fängt Fehler aus async-Handlern nicht selbst ab — ohne das
authRouter.get("/api/auth/google/callback", (req, res) => verarbeiteOAuthCallback(req, res, "google")); würde z.B. ein Netzwerkfehler beim Token-Austausch mit Google/PayPal den ganzen Shop beenden. */
authRouter.get("/api/auth/paypal/start", (req, res) => starteOAuth(req, res, "paypal")); const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
authRouter.get("/api/auth/paypal/callback", (req, res) => verarbeiteOAuthCallback(req, res, "paypal"));
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")));
+3 -2
View File
@@ -5,11 +5,12 @@
===================================================================== */ ===================================================================== */
import { Router } from "express"; import { Router } from "express";
import { wrap } from "../lib/async-wrap.js";
import { erstelleBestellung } from "../lib/bestellung-erstellen.js"; import { erstelleBestellung } from "../lib/bestellung-erstellen.js";
export const ordersRouter = Router(); export const ordersRouter = Router();
ordersRouter.post("/api/orders", async (req, res) => { ordersRouter.post("/api/orders", wrap(async (req, res) => {
const body = req.body || {}; const body = req.body || {};
const zahlungsart = String(body.zahlungsart || ""); 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(ergebnis.httpStatus || 500).json({ ok: false, error: ergebnis.error });
} }
return res.status(201).json({ ok: true, id: ergebnis.id, bestellnummer: ergebnis.bestellnummer }); return res.status(201).json({ ok: true, id: ergebnis.id, bestellnummer: ergebnis.bestellnummer });
}); }));
+5 -4
View File
@@ -6,6 +6,7 @@
===================================================================== */ ===================================================================== */
import { Router } from "express"; import { Router } from "express";
import { wrap } from "../lib/async-wrap.js";
import { paypalKonfiguriert, erstellePaypalBestellung, erfassePaypalZahlung } from "../lib/paypal.js"; import { paypalKonfiguriert, erstellePaypalBestellung, erfassePaypalZahlung } from "../lib/paypal.js";
import { erstelleBestellung } from "../lib/bestellung-erstellen.js"; import { erstelleBestellung } from "../lib/bestellung-erstellen.js";
@@ -13,7 +14,7 @@ export const paypalRouter = Router();
const BETRAG_TOLERANZ_EURO = 0.02; 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()) { if (!paypalKonfiguriert()) {
return res.status(503).json({ return res.status(503).json({
ok: false, ok: false,
@@ -33,9 +34,9 @@ paypalRouter.post("/api/paypal/create-order", async (req, res) => {
} catch (err) { } catch (err) {
return res.status(502).json({ ok: false, error: err instanceof Error ? err.message : "PayPal-Bestellung fehlgeschlagen." }); 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()) { if (!paypalKonfiguriert()) {
return res.status(503).json({ ok: false, error: "PayPal ist auf dieser Seite noch nicht eingerichtet." }); 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 }); return res.status(201).json({ ok: true, id: ergebnis.id, bestellnummer: ergebnis.bestellnummer });
}); }));
+5 -4
View File
@@ -6,6 +6,7 @@
===================================================================== */ ===================================================================== */
import { Router } from "express"; import { Router } from "express";
import { wrap } from "../lib/async-wrap.js";
import { getGateRole, unauthorizedJson } from "../lib/auth.js"; import { getGateRole, unauthorizedJson } from "../lib/auth.js";
import { hatGueltigeVerwaltungSitzung } from "../lib/verwaltung-auth.js"; import { hatGueltigeVerwaltungSitzung } from "../lib/verwaltung-auth.js";
import { db } from "../db.js"; import { db } from "../db.js";
@@ -27,7 +28,7 @@ async function pruefeSchranken(req, res) {
return true; 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; if (!(await pruefeSchranken(req, res))) return;
try { try {
@@ -50,9 +51,9 @@ verwaltungRouter.get("/api/verwaltung/orders", async (req, res) => {
} catch (err) { } catch (err) {
return res.status(500).json({ ok: false, error: "Bestellungen konnten nicht geladen werden." }); 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; if (!(await pruefeSchranken(req, res))) return;
const id = Number(req.params.id); const id = Number(req.params.id);
@@ -92,4 +93,4 @@ verwaltungRouter.patch("/api/verwaltung/orders/:id", async (req, res) => {
} catch (err) { } catch (err) {
return res.status(500).json({ ok: false, error: "Bestellung konnte nicht aktualisiert werden." }); return res.status(500).json({ ok: false, error: "Bestellung konnte nicht aktualisiert werden." });
} }
}); }));
+3 -3
View File
@@ -325,7 +325,7 @@ export const ui = {
}, },
}, },
en: { 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" }, accountMenu: { myAccount: "My Account", logout: "Sign out" },
topbar: "Free shipping from €75 (DE) / €85 (AT, LU) / €100 (CH)", topbar: "Free shipping from €75 (DE) / €85 (AT, LU) / €100 (CH)",
footer: { footer: {
@@ -643,7 +643,7 @@ export const ui = {
}, },
}, },
ch: { 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" }, accountMenu: { myAccount: "Mis Konto", logout: "Abmälde" },
topbar: "Gratisversand ab 75 € (DE) / 85 € (AT, LU) / 100 € (CH)", topbar: "Gratisversand ab 75 € (DE) / 85 € (AT, LU) / 100 € (CH)",
footer: { footer: {
@@ -961,7 +961,7 @@ export const ui = {
}, },
}, },
fr: { 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" }, accountMenu: { myAccount: "Mon compte", logout: "Se déconnecter" },
topbar: "Livraison gratuite dès 75 € (DE) / 85 € (AT, LU) / 100 € (CH)", topbar: "Livraison gratuite dès 75 € (DE) / 85 € (AT, LU) / 100 € (CH)",
footer: { footer: {
+1 -1
View File
@@ -51,7 +51,7 @@ const p = (href: string) => `${localePrefix(lang)}${href}`;
<title>{title} · Van's DIY & Bastelbedarf</title> <title>{title} · Van's DIY & Bastelbedarf</title>
<meta name="description" content={description} /> <meta name="description" content={description} />
{noindex && <meta name="robots" content="noindex, nofollow" />} {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:type" content="website" />
<meta property="og:site_name" content="Van's DIY & Bastelbedarf" /> <meta property="og:site_name" content="Van's DIY & Bastelbedarf" />
<meta property="og:title" content={title} /> <meta property="og:title" content={title} />