Node.js/Express-Server als Ersatz für Cloudflare Pages Functions + D1 (läuft auf eigenem Server statt Cloudflare)
Build & Deploy / deploy (push) Waiting to run
Build & Deploy / deploy (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
/* =====================================================================
|
||||
routes/account.js — Kundenkonto-Selbstbedienung: me, logout, delete (DSGVO Art. 17),
|
||||
export (DSGVO Art. 15/20). 1:1 portiert aus functions/api/account/*.js.
|
||||
===================================================================== */
|
||||
|
||||
import { Router } from "express";
|
||||
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) => {
|
||||
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) => {
|
||||
const customer = await getCustomer(req);
|
||||
if (!customer) return res.status(401).json({ ok: false, error: "Nicht angemeldet." });
|
||||
|
||||
try {
|
||||
db.prepare(`DELETE FROM customers WHERE id = ?`).run(customer.id);
|
||||
} catch {
|
||||
return res.status(500).json({ ok: false, error: "Konto konnte nicht gelöscht werden. Bitte versuche es erneut." });
|
||||
}
|
||||
|
||||
clearCustomerCookie(res);
|
||||
return res.json({ ok: true });
|
||||
});
|
||||
|
||||
accountRouter.get("/api/account/export", async (req, res) => {
|
||||
const customer = await getCustomer(req);
|
||||
if (!customer) return res.status(401).json({ ok: false, error: "Nicht angemeldet." });
|
||||
|
||||
const daten = {
|
||||
konto: { email: customer.email, name: customer.name, anmeldeart: customer.provider, erstelltAm: customer.created_at },
|
||||
hinweis:
|
||||
"Dies sind alle Daten, die Van's DIY & Bastelbedarf zu deinem Kundenkonto speichert. " +
|
||||
"Bereits aufgegebene Bestellungen werden getrennt davon aus gesetzlichen Aufbewahrungsgründen " +
|
||||
"weiter gespeichert (siehe Datenschutzerklärung, Abschnitt Bestellabwicklung).",
|
||||
};
|
||||
|
||||
res.setHeader("Content-Disposition", 'attachment; filename="meine-daten.json"');
|
||||
return res.status(200).json(daten);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
/* =====================================================================
|
||||
routes/auth.js — OAuth-Start/Callback für Google + PayPal-Login. 1:1 portiert aus
|
||||
functions/api/auth/google/{start,callback}.js + functions/api/auth/paypal/{start,callback}.js
|
||||
(beide Anbieter nutzen dieselbe gemeinsame Logik, siehe lib/oauth-handlers.js).
|
||||
===================================================================== */
|
||||
|
||||
import { Router } from "express";
|
||||
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"));
|
||||
@@ -0,0 +1,28 @@
|
||||
/* =====================================================================
|
||||
routes/bestseller.js — GET /api/bestseller. 1:1 portiert aus functions/api/bestseller.js,
|
||||
D1-Abfrage auf synchrones better-sqlite3 umgestellt.
|
||||
===================================================================== */
|
||||
|
||||
import { Router } from "express";
|
||||
import { db } from "../db.js";
|
||||
|
||||
export const bestsellerRouter = Router();
|
||||
|
||||
bestsellerRouter.get("/api/bestseller", (req, res) => {
|
||||
try {
|
||||
const results = db
|
||||
.prepare(
|
||||
`SELECT oi.produkt_slug AS slug, SUM(oi.menge) AS verkauft
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE o.status NOT IN ('storniert', 'zahlungOffen') AND oi.gratis = 0
|
||||
GROUP BY oi.produkt_slug
|
||||
ORDER BY verkauft DESC, slug ASC
|
||||
LIMIT 6`
|
||||
)
|
||||
.all();
|
||||
return res.json({ ok: true, slugs: results.map((r) => r.slug) });
|
||||
} catch {
|
||||
return res.json({ ok: true, slugs: [] });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/* =====================================================================
|
||||
routes/orders.js — POST /api/orders, legt eine Überweisungs-Bestellung (Vorkasse) an.
|
||||
1:1 portiert aus functions/api/orders.js. Lehnt zahlungsart:"paypal" bewusst ab (Betrugsschutz
|
||||
— PayPal-Bestellungen entstehen NUR über routes/paypal.js, nach echter Zahlungsprüfung).
|
||||
===================================================================== */
|
||||
|
||||
import { Router } from "express";
|
||||
import { erstelleBestellung } from "../lib/bestellung-erstellen.js";
|
||||
|
||||
export const ordersRouter = Router();
|
||||
|
||||
ordersRouter.post("/api/orders", async (req, res) => {
|
||||
const body = req.body || {};
|
||||
const zahlungsart = String(body.zahlungsart || "");
|
||||
|
||||
if (zahlungsart === "paypal") {
|
||||
return res.status(400).json({
|
||||
ok: false,
|
||||
error: "PayPal-Bestellungen laufen über den PayPal-Button im Checkout, nicht über diese Route.",
|
||||
});
|
||||
}
|
||||
if (zahlungsart !== "ueberweisung") {
|
||||
return res.status(400).json({ ok: false, error: "Ungültige oder nicht unterstützte Zahlungsart." });
|
||||
}
|
||||
|
||||
const ergebnis = await erstelleBestellung(body, { status: "zahlungOffen" });
|
||||
if (!ergebnis.ok) {
|
||||
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 });
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
/* =====================================================================
|
||||
routes/paypal.js — POST /api/paypal/create-order + /api/paypal/capture-order. 1:1 portiert
|
||||
aus functions/api/paypal/create-order.js + capture-order.js. Der entscheidende Sicherheits-
|
||||
Schritt bleibt unverändert: die Bestellung wird erst NACH server-seitig bei PayPal bestätigter
|
||||
Zahlung angelegt, mit Betragsabgleich (Toleranz 0,02 €).
|
||||
===================================================================== */
|
||||
|
||||
import { Router } from "express";
|
||||
import { paypalKonfiguriert, erstellePaypalBestellung, erfassePaypalZahlung } from "../lib/paypal.js";
|
||||
import { erstelleBestellung } from "../lib/bestellung-erstellen.js";
|
||||
|
||||
export const paypalRouter = Router();
|
||||
|
||||
const BETRAG_TOLERANZ_EURO = 0.02;
|
||||
|
||||
paypalRouter.post("/api/paypal/create-order", async (req, res) => {
|
||||
if (!paypalKonfiguriert()) {
|
||||
return res.status(503).json({
|
||||
ok: false,
|
||||
error: "PayPal ist auf dieser Seite noch nicht eingerichtet (fehlende Zugangsdaten). Bitte eine andere Zahlungsart wählen.",
|
||||
});
|
||||
}
|
||||
|
||||
const body = req.body || {};
|
||||
const summe = Number(body.summe);
|
||||
if (!Number.isFinite(summe) || summe <= 0) {
|
||||
return res.status(400).json({ ok: false, error: "Ungültiger Betrag." });
|
||||
}
|
||||
|
||||
try {
|
||||
const paypalOrderId = await erstellePaypalBestellung(summe);
|
||||
return res.json({ ok: true, id: paypalOrderId });
|
||||
} 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) => {
|
||||
if (!paypalKonfiguriert()) {
|
||||
return res.status(503).json({ ok: false, error: "PayPal ist auf dieser Seite noch nicht eingerichtet." });
|
||||
}
|
||||
|
||||
const body = req.body || {};
|
||||
const paypalOrderId = typeof body.paypalOrderId === "string" ? body.paypalOrderId.trim() : "";
|
||||
if (!paypalOrderId) {
|
||||
return res.status(400).json({ ok: false, error: "Fehlende PayPal-Bestell-ID." });
|
||||
}
|
||||
|
||||
const summe = Number(body.summe);
|
||||
if (!Number.isFinite(summe) || summe <= 0) {
|
||||
return res.status(400).json({ ok: false, error: "Ungültige Bestellsumme." });
|
||||
}
|
||||
|
||||
const zahlung = await erfassePaypalZahlung(paypalOrderId);
|
||||
if (!zahlung.ok) {
|
||||
return res.status(402).json({ ok: false, error: `Zahlung konnte nicht bestätigt werden: ${zahlung.error}` });
|
||||
}
|
||||
|
||||
if (Math.abs(zahlung.betragEuro - summe) > BETRAG_TOLERANZ_EURO) {
|
||||
return res.status(402).json({
|
||||
ok: false,
|
||||
error: `Der bei PayPal gezahlte Betrag (${zahlung.betragEuro.toFixed(2)} €) stimmt nicht mit der Bestellsumme (${summe.toFixed(2)} €) überein.`,
|
||||
});
|
||||
}
|
||||
|
||||
const ergebnis = await erstelleBestellung({ ...body, zahlungsart: "paypal" }, { status: "bezahlt", paypalOrderId });
|
||||
if (!ergebnis.ok) {
|
||||
return res.status(500).json({
|
||||
ok: false,
|
||||
error: `Deine Zahlung wurde erfolgreich bei PayPal abgebucht (Beleg-ID: ${zahlung.captureId}), aber die Bestellung konnte nicht gespeichert werden. Bitte kontaktiere uns mit dieser Beleg-ID, damit wir das von Hand nachtragen — bitte NICHT erneut bezahlen.`,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(201).json({ ok: true, id: ergebnis.id, bestellnummer: ergebnis.bestellnummer });
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
/* =====================================================================
|
||||
routes/verwaltung.js — GET /api/verwaltung/orders (Liste) + PATCH /api/verwaltung/orders/:id
|
||||
(Status/Tracking ändern). 1:1 portiert aus functions/api/verwaltung/orders.js +
|
||||
functions/api/verwaltung/orders/[id].js. Prüft zusätzlich zur gate-Middleware (siehe
|
||||
middleware/gate.js) hier nochmal eigenständig beide Schranken — Verteidigung in der Tiefe.
|
||||
===================================================================== */
|
||||
|
||||
import { Router } from "express";
|
||||
import { getGateRole, unauthorizedJson } from "../lib/auth.js";
|
||||
import { hatGueltigeVerwaltungSitzung } from "../lib/verwaltung-auth.js";
|
||||
import { db } from "../db.js";
|
||||
|
||||
export const verwaltungRouter = Router();
|
||||
|
||||
const ERLAUBTE_STATUS = ["zahlungOffen", "bezahlt", "bearbeitung", "versandVorbereitet", "versendet", "abgeschlossen", "storniert"];
|
||||
|
||||
async function pruefeSchranken(req, res) {
|
||||
const role = await getGateRole(req);
|
||||
if (!role) {
|
||||
unauthorizedJson(res);
|
||||
return false;
|
||||
}
|
||||
if (!(await hatGueltigeVerwaltungSitzung(req))) {
|
||||
res.status(401).json({ ok: false, error: "Zusätzlicher Verwaltungs-Code erforderlich." });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
verwaltungRouter.get("/api/verwaltung/orders", async (req, res) => {
|
||||
if (!(await pruefeSchranken(req, res))) return;
|
||||
|
||||
try {
|
||||
const orders = db.prepare(`SELECT * FROM orders ORDER BY created_at DESC`).all();
|
||||
if (orders.length === 0) return res.json({ ok: true, orders: [] });
|
||||
|
||||
const ids = orders.map((o) => o.id);
|
||||
const placeholders = ids.map(() => "?").join(",");
|
||||
const items = db.prepare(`SELECT * FROM order_items WHERE order_id IN (${placeholders}) ORDER BY id ASC`).all(...ids);
|
||||
|
||||
const itemsByOrder = new Map();
|
||||
items.forEach((item) => {
|
||||
const liste = itemsByOrder.get(item.order_id) ?? [];
|
||||
liste.push(item);
|
||||
itemsByOrder.set(item.order_id, liste);
|
||||
});
|
||||
|
||||
const result = orders.map((o) => ({ ...o, artikel: itemsByOrder.get(o.id) ?? [] }));
|
||||
return res.json({ ok: true, orders: result });
|
||||
} catch (err) {
|
||||
return res.status(500).json({ ok: false, error: "Bestellungen konnten nicht geladen werden." });
|
||||
}
|
||||
});
|
||||
|
||||
verwaltungRouter.patch("/api/verwaltung/orders/:id", async (req, res) => {
|
||||
if (!(await pruefeSchranken(req, res))) return;
|
||||
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
return res.status(400).json({ ok: false, error: "Ungültige Bestell-ID." });
|
||||
}
|
||||
|
||||
const body = req.body || {};
|
||||
const updates = [];
|
||||
const values = [];
|
||||
|
||||
if (body.status !== undefined) {
|
||||
if (!ERLAUBTE_STATUS.includes(body.status)) {
|
||||
return res.status(400).json({ ok: false, error: "Ungültiger Status." });
|
||||
}
|
||||
updates.push("status = ?");
|
||||
values.push(body.status);
|
||||
}
|
||||
if (body.tracking !== undefined) {
|
||||
updates.push("tracking = ?");
|
||||
values.push(String(body.tracking || ""));
|
||||
}
|
||||
if (updates.length === 0) {
|
||||
return res.status(400).json({ ok: false, error: "Nichts zu ändern übergeben." });
|
||||
}
|
||||
|
||||
updates.push("updated_at = ?");
|
||||
values.push(new Date().toISOString());
|
||||
values.push(id);
|
||||
|
||||
try {
|
||||
const result = db.prepare(`UPDATE orders SET ${updates.join(", ")} WHERE id = ?`).run(...values);
|
||||
if (result.changes === 0) {
|
||||
return res.status(404).json({ ok: false, error: "Bestellung nicht gefunden." });
|
||||
}
|
||||
return res.json({ ok: true });
|
||||
} catch (err) {
|
||||
return res.status(500).json({ ok: false, error: "Bestellung konnte nicht aktualisiert werden." });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user