Compare commits

..
4 Commits
Author SHA1 Message Date
qcigano c505520f19 Rechtstexte mit VanVans echten Angaben vervollstaendigt
Build & Deploy / deploy (push) Waiting to run
Impressum, Datenschutzerklaerung, AGB, Widerrufsbelehrung und
Muster-Widerrufsformular enthielten noch Platzhalter ([Vorname Nachname],
[Anschrift] usw.) - jetzt mit den echten, von VanVan durchgegebenen Angaben
gefuellt (Name, Adresse, Telefon, E-Mail). USt-ID folgt noch, aktuell leer
= Kleinunternehmerregelung wird korrekt angenommen.

Zusaetzlich: Hosting-Absatz in der Datenschutzerklaerung korrigiert (netcup
statt Cloudflare-Platzhalter, Cloudflare als vorgeschaltetes CDN erwaehnt),
Cloudflare-D1-Referenz auf die echte Datenbank korrigiert, Resend als
E-Mail-Auftragsverarbeiter ergaenzt. Die "Muster-Text"-Warnbanner wurden
nicht komplett entfernt, sondern ehrlich umformuliert: die Daten sind jetzt
echt, eine fachliche Rechtspruefung vor echtem Verkaufsstart bleibt trotzdem
empfohlen. Kontaktseiten (alle 4 Sprachen) nutzen jetzt die echte E-Mail
statt der als Platzhalter markierten info@-Adresse.
2026-08-06 00:08:31 +02:00
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
29 changed files with 148 additions and 81 deletions
+1 -1
View File
@@ -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
View File
@@ -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
+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=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=
+42 -3
View File
@@ -4,11 +4,11 @@
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.
Umgebungsvariablen kommen aus einer .env-Datei — gestartet wird dieser Server mit
"node --env-file=.env index.js" (Node 22 kann .env-Dateien nativ laden, kein zusätzliches
Paket nötig).
Umgebungsvariablen kommen aus einer .env-Datei im server/-Ordner (siehe .env.example), geladen
über das "dotenv"-Paket direkt beim Start.
===================================================================== */
import "dotenv/config";
import express from "express";
import cookieParser from "cookie-parser";
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
// "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}`);
});
+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 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) {
let bin = "";
@@ -34,7 +42,7 @@ async function hmacKey(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 key = await hmacKey(secret);
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>
<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>
`;
}
+7 -8
View File
@@ -12,10 +12,9 @@
- 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 {
VERWALTUNG_COOKIE_NAME,
VERWALTUNG_SESSION_STUNDEN,
signVerwaltungSession,
verifyVerwaltungSession,
} 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 maxAge = SESSION_TAGE * 24 * 60 * 60 * 1000;
res.cookie(COOKIE_NAME, session, { path: "/", maxAge, httpOnly: true, secure: true, sameSite: "lax" });
res.cookie(ROLE_COOKIE_NAME, role, { path: "/", maxAge, secure: true, sameSite: "lax" });
// Bewusst OHNE maxAge/expires → reines Sitzungs-Cookie, verfällt beim Schließen des Browsers.
res.cookie(COOKIE_NAME, session, { path: "/", httpOnly: true, secure: true, sameSite: "lax" });
res.cookie(ROLE_COOKIE_NAME, role, { path: "/", secure: true, sameSite: "lax" });
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 maxAge = VERWALTUNG_SESSION_STUNDEN * 60 * 60 * 1000;
res.cookie(VERWALTUNG_COOKIE_NAME, session, { path: "/", maxAge, httpOnly: true, secure: true, sameSite: "lax" });
// Ebenfalls reines Sitzungs-Cookie (siehe gateAuthHandler oben).
res.cookie(VERWALTUNG_COOKIE_NAME, session, { path: "/", httpOnly: true, secure: true, sameSite: "lax" });
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] : "");
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",
"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": {
"node": ">=22.12.0"
"node": ">=20.0.0"
},
"scripts": {
"start": "node index.js"
@@ -12,6 +12,7 @@
"dependencies": {
"better-sqlite3": "^11.8.1",
"cookie-parser": "^1.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2"
}
}
+7 -6
View File
@@ -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);
});
}));
+8 -4
View File
@@ -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")));
+3 -2
View File
@@ -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 });
});
}));
+5 -4
View File
@@ -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 });
});
}));
+5 -4
View File
@@ -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." });
}
});
}));
+1 -1
View File
@@ -2,7 +2,7 @@
"sections": [
{
"heading": "§ 1 Geltungsbereich",
"body": "Diese Allgemeinen Geschäftsbedingungen gelten für alle Bestellungen, die Verbraucher über den Onlineshop von Van's DIY & Bastelbedarf (Einzelunternehmen, [Anschrift]) aufgeben."
"body": "Diese Allgemeinen Geschäftsbedingungen gelten für alle Bestellungen, die Verbraucher über den Onlineshop von Van's DIY & Bastelbedarf (Einzelunternehmen, Vanessa Geske, Von-Arnim-Str. 40, 17309 Rollwitz) aufgeben."
},
{
"heading": "§ 2 Vertragspartner, Vertragsschluss",
+4 -4
View File
@@ -2,7 +2,7 @@
"sections": [
{
"heading": "1. Verantwortlicher",
"body": "[Vorname Nachname], Van's DIY & Bastelbedarf, [Anschrift], [E-Mail-Adresse]"
"body": "Vanessa Geske, Van's DIY & Bastelbedarf, Von-Arnim-Str. 40, 17309 Rollwitz, [email protected]"
},
{
"heading": "2. Allgemeines zur Datenverarbeitung",
@@ -10,11 +10,11 @@
},
{
"heading": "3. Hosting",
"body": "Diese Website wird bei einem externen Dienstleister gehostet (Hoster). Die personenbezogenen Daten, die auf dieser Website erfasst werden, werden auf den Servern des Hosters gespeichert. [Konkreten Hosting-Anbieter, z. B. Cloudflare Pages, hier ergänzen inkl. Standort der Server und ggf. Auftragsverarbeitungsvertrag.]"
"body": "Diese Website wird auf einem eigenen Server der netcup GmbH (Daimlerstraße 25, 76185 Karlsruhe, Deutschland) im Rechenzentrum Nürnberg betrieben; mit netcup besteht ein Vertrag zur Auftragsverarbeitung. Die personenbezogenen Daten, die auf dieser Website erfasst werden, werden dort auf dem Server gespeichert. Dem Server vorgeschaltet ist als Content-Delivery-Network und Schutz vor Überlastungsangriffen Cloudflare (Cloudflare, Inc., 101 Townsend St, San Francisco, USA); dabei werden technisch notwendige Daten (u. a. IP-Adresse, Zeitstempel, aufgerufene URL) auch von Cloudflare verarbeitet. Eine Übermittlung in die USA ist dabei nicht ausgeschlossen, abgesichert über die EU-Standardvertragsklauseln bzw. das EU-US Data Privacy Framework. Näheres: cloudflare.com/privacypolicy."
},
{
"heading": "4. Bestellabwicklung",
"body": "Zur Abwicklung deiner Bestellung erheben und verarbeiten wir personenbezogene Daten, wenn dies zur Erfüllung des Vertrags erforderlich ist (Art. 6 Abs. 1 lit. b DSGVO). Dazu gehören Name, Anschrift, E-Mail-Adresse und Zahlungsdaten. Die Daten werden ausschließlich zur Vertragsabwicklung genutzt."
"body": "Zur Abwicklung deiner Bestellung erheben und verarbeiten wir personenbezogene Daten, wenn dies zur Erfüllung des Vertrags erforderlich ist (Art. 6 Abs. 1 lit. b DSGVO). Dazu gehören Name, Anschrift, E-Mail-Adresse und Zahlungsdaten. Die Daten werden ausschließlich zur Vertragsabwicklung genutzt. Die interne Benachrichtigung über eine neue Bestellung wird über den E-Mail-Versanddienst Resend (Resend, Inc., USA) verschickt, der dabei technisch als Auftragsverarbeiter die für diese eine Nachricht nötigen Bestelldaten verarbeitet; Resend ist nach dem EU-US Data Privacy Framework zertifiziert."
},
{
"heading": "5. Zahlungsdienstleister",
@@ -22,7 +22,7 @@
},
{
"heading": "6. Kundenkonto (Anmeldung mit Google oder PayPal)",
"body": "Für ein Kundenkonto bieten wir ausschließlich die Anmeldung über bestehende Google- oder PayPal-Konten an (OAuth 2.0 / OpenID Connect) — ein eigenes Passwort bei uns gibt es nicht. Beim ersten Login übermitteln uns Google bzw. PayPal deine E-Mail-Adresse, deinen Namen und eine eindeutige, anbieterseitige Kennung; wir speichern diese Angaben zusammen mit dem Anmeldezeitpunkt in unserer Datenbank (Cloudflare D1), um dich bei künftigen Besuchen wiederzuerkennen. Die Verarbeitung erfolgt auf Grundlage des Art. 6 Abs. 1 lit. b DSGVO (Erfüllung des Nutzungsvertrags für das Kundenkonto) sowie deiner Einwilligung durch den aktiven Klick auf den jeweiligen Anmelde-Button (Art. 6 Abs. 1 lit. a DSGVO). Verantwortlich für die Verarbeitung deiner Daten bei Google ist die Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland; die Muttergesellschaft Google LLC (USA) ist nach unserer Kenntnis nach dem EU-US Data Privacy Framework zertifiziert, das ein angemessenes Datenschutzniveau sicherstellt. Für PayPal gelten dieselben Angaben wie unter Punkt 5 (PayPal (Europe) S.à r.l. et Cie, S.C.A., DPF-Zertifizierung der US-Muttergesellschaft). Es gelten zusätzlich die jeweiligen Datenschutzhinweise von Google (policies.google.com/privacy) bzw. PayPal (paypal.com/de/webapps/mpp/ua/privacy-full). Du kannst dein Kundenkonto jederzeit selbst und ohne Rückfrage in deinem Kontobereich unwiderruflich löschen (Art. 17 DSGVO) oder deine gespeicherten Kontodaten als Datei herunterladen (Art. 15, 20 DSGVO); bereits aufgegebene Bestellungen bleiben davon unberührt, da für sie eigenständige, gesetzliche Aufbewahrungspflichten gelten (siehe Punkt 10)."
"body": "Für ein Kundenkonto bieten wir ausschließlich die Anmeldung über bestehende Google- oder PayPal-Konten an (OAuth 2.0 / OpenID Connect) — ein eigenes Passwort bei uns gibt es nicht. Beim ersten Login übermitteln uns Google bzw. PayPal deine E-Mail-Adresse, deinen Namen und eine eindeutige, anbieterseitige Kennung; wir speichern diese Angaben zusammen mit dem Anmeldezeitpunkt in unserer Datenbank auf dem eigenen Server (siehe Punkt 3), um dich bei künftigen Besuchen wiederzuerkennen. Die Verarbeitung erfolgt auf Grundlage des Art. 6 Abs. 1 lit. b DSGVO (Erfüllung des Nutzungsvertrags für das Kundenkonto) sowie deiner Einwilligung durch den aktiven Klick auf den jeweiligen Anmelde-Button (Art. 6 Abs. 1 lit. a DSGVO). Verantwortlich für die Verarbeitung deiner Daten bei Google ist die Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland; die Muttergesellschaft Google LLC (USA) ist nach unserer Kenntnis nach dem EU-US Data Privacy Framework zertifiziert, das ein angemessenes Datenschutzniveau sicherstellt. Für PayPal gelten dieselben Angaben wie unter Punkt 5 (PayPal (Europe) S.à r.l. et Cie, S.C.A., DPF-Zertifizierung der US-Muttergesellschaft). Es gelten zusätzlich die jeweiligen Datenschutzhinweise von Google (policies.google.com/privacy) bzw. PayPal (paypal.com/de/webapps/mpp/ua/privacy-full). Du kannst dein Kundenkonto jederzeit selbst und ohne Rückfrage in deinem Kontobereich unwiderruflich löschen (Art. 17 DSGVO) oder deine gespeicherten Kontodaten als Datei herunterladen (Art. 15, 20 DSGVO); bereits aufgegebene Bestellungen bleiben davon unberührt, da für sie eigenständige, gesetzliche Aufbewahrungspflichten gelten (siehe Punkt 10)."
},
{
"heading": "7. Kontaktformular",
+3 -3
View File
@@ -3,11 +3,11 @@
"sections": [
{
"heading": "Angaben gemäß § 5 DDG",
"body": "[Vorname Nachname] („VanVan\")\nVan's DIY & Bastelbedarf\nEinzelunternehmen\n[Straße und Hausnummer]\n[PLZ Ort]\nDeutschland"
"body": "Vanessa Geske („VanVan\")\nVan's DIY & Bastelbedarf\nEinzelunternehmen\nVon-Arnim-Str. 40\n17309 Rollwitz\nDeutschland"
},
{
"heading": "Kontakt",
"body": "Telefon: [Telefonnummer]\nE-Mail: [E-Mail-Adresse]"
"body": "Telefon: 015167505105\nE-Mail: [email protected]"
},
{
"heading": "Umsatzsteuer",
@@ -15,7 +15,7 @@
},
{
"heading": "Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV",
"body": "[Vorname Nachname], Anschrift wie oben."
"body": "Vanessa Geske, Anschrift wie oben."
},
{
"heading": "EU-Streitschlichtung",
+1 -1
View File
@@ -2,7 +2,7 @@
"sections": [
{
"heading": "Widerrufsrecht",
"body": "Du hast das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen. Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag, an dem du oder ein von dir benannter Dritter, der nicht der Beförderer ist, die Waren in Besitz genommen hast bzw. hat.\n\nUm dein Widerrufsrecht auszuüben, musst du uns ([Vorname Nachname], Van's DIY & Bastelbedarf, [Anschrift], [E-Mail-Adresse]) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder E-Mail) über deinen Entschluss, diesen Vertrag zu widerrufen, informieren. Ein Muster-Widerrufsformular findest du unten auf dieser Seite verlinkt, das jedoch nicht vorgeschrieben ist.\n\nZur Wahrung der Widerrufsfrist reicht es aus, dass du die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absendest."
"body": "Du hast das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen. Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag, an dem du oder ein von dir benannter Dritter, der nicht der Beförderer ist, die Waren in Besitz genommen hast bzw. hat.\n\nUm dein Widerrufsrecht auszuüben, musst du uns (Vanessa Geske, Van's DIY & Bastelbedarf, Von-Arnim-Str. 40, 17309 Rollwitz, [email protected]) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder E-Mail) über deinen Entschluss, diesen Vertrag zu widerrufen, informieren. Ein Muster-Widerrufsformular findest du unten auf dieser Seite verlinkt, das jedoch nicht vorgeschrieben ist.\n\nZur Wahrung der Widerrufsfrist reicht es aus, dass du die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absendest."
},
{
"heading": "Folgen des Widerrufs",
+7 -7
View File
@@ -76,7 +76,7 @@ export const ui = {
gdprPrefix: "Ich habe die", gdprLink: "Datenschutzerklärung", gdprSuffix: "gelesen und bin mit der Verarbeitung meiner Daten einverstanden.",
send: "Nachricht senden", sentDemo: "Danke! (Demo noch keine echte Übertragung in Phase 1)",
todoNote: "TODO Phase 2: Formular an eine echte Backend-/E-Mail-Anbindung (Cloudflare Worker) anschließen. Aktuell ohne echten Versand.",
directTitle: "Direkter Kontakt", emailPlaceholderNote: "(Platzhalter-Adresse)", orderNote: "Für Bestellfragen bitte die Bestellnummer angeben.",
directTitle: "Direkter Kontakt", emailPlaceholderNote: "", orderNote: "Für Bestellfragen bitte die Bestellnummer angeben.",
responseTitle: "Antwortzeit", responseNote: "In der Regel innerhalb von 12 Werktagen.",
},
about: {
@@ -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: {
@@ -394,7 +394,7 @@ export const ui = {
gdprPrefix: "I have read the", gdprLink: "privacy policy", gdprSuffix: "and agree to the processing of my data.",
send: "Send message", sentDemo: "Thank you! (Demo no real submission yet in Phase 1)",
todoNote: "TODO Phase 2: connect this form to a real backend/email service (Cloudflare Worker). Currently no real sending happens.",
directTitle: "Direct contact", emailPlaceholderNote: "(placeholder address)", orderNote: "Please include your order number for order-related questions.",
directTitle: "Direct contact", emailPlaceholderNote: "", orderNote: "Please include your order number for order-related questions.",
responseTitle: "Response time", responseNote: "Usually within 12 business days.",
},
about: {
@@ -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: {
@@ -712,7 +712,7 @@ export const ui = {
gdprPrefix: "Ich ha d'", gdprLink: "Datenschutzerklärig", gdprSuffix: "gläse und bin mit dr Verarbeitig vo mine Date iverstande.",
send: "Nachricht abschicke", sentDemo: "Merci! (Demo noch keine echte Übertragung in Phase 1)",
todoNote: "TODO Phase 2: Formular a e echti Backend-/E-Mail-Aabindig (Cloudflare Worker) aaschliesse. Momentan ohni echte Versand.",
directTitle: "Direkte Kontaktdate", emailPlaceholderNote: "(Platzhalter-Adrässe)", orderNote: "Für Bstellfrage bitte d'Bstellnummere aagäh.",
directTitle: "Direkte Kontaktdate", emailPlaceholderNote: "", orderNote: "Für Bstellfrage bitte d'Bstellnummere aagäh.",
responseTitle: "Antwortzyt", responseNote: "Meistens innerhalb vo 12 Werktage.",
},
about: {
@@ -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: {
@@ -1030,7 +1030,7 @@ export const ui = {
gdprPrefix: "J'ai lu la", gdprLink: "politique de confidentialité", gdprSuffix: "et j'accepte le traitement de mes données.",
send: "Envoyer le message", sentDemo: "Merci ! (Démo pas encore d'envoi réel en phase 1)",
todoNote: "À faire en phase 2 : connecter ce formulaire à un vrai service backend/e-mail (Cloudflare Worker). Actuellement aucun envoi réel n'a lieu.",
directTitle: "Contact direct", emailPlaceholderNote: "(adresse provisoire)", orderNote: "Pour toute question de commande, merci d'indiquer le numéro de commande.",
directTitle: "Contact direct", emailPlaceholderNote: "", orderNote: "Pour toute question de commande, merci d'indiquer le numéro de commande.",
responseTitle: "Délai de réponse", responseNote: "Généralement sous 1 à 2 jours ouvrés.",
},
about: {
+1 -1
View File
@@ -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} />
+3 -3
View File
@@ -7,9 +7,9 @@ import { agb, rechtstextAlsHtml } from "../../data/legal";
<div class="container">
<h1>Allgemeine Geschäftsbedingungen</h1>
<div class="legal-note">
⚠️ <strong>Muster-Text.</strong> Vorbereiteter Platzhalter, ersetzt keine rechtliche
Prüfung durch eine fachkundige Stelle vor Veröffentlichung — im Adminbereich unter
„⚖️ Rechtstexte & FAQ" bearbeitbar.
⚠️ Enthält die echten Angaben von VanVan, wurde aber noch nicht von einer fachkundigen
Stelle geprüft. Vor dem echten Verkaufsstart bitte fachlich prüfen lassen — im
Adminbereich unter „⚖️ Rechtstexte & FAQ" bearbeitbar.
</div>
{agb.sections.map((s) => (
+1 -1
View File
@@ -45,7 +45,7 @@ const t = useTranslations(lang);
<div class="card contact-highlight">
<h3><span aria-hidden="true">💌</span> {t.contact.directTitle}</h3>
<p class="small">E-Mail: <a href="mailto:info@vans-diy-bastelbedarf.de">info@vans-diy-bastelbedarf.de</a> <span class="small">{t.contact.emailPlaceholderNote}</span></p>
<p class="small">E-Mail: <a href="mailto:vansdiyundbastelbedarf@gmail.com">vansdiyundbastelbedarf@gmail.com</a> <span class="small">{t.contact.emailPlaceholderNote}</span></p>
<p class="small">{t.contact.orderNote}</p>
<h3><span aria-hidden="true">⏱️</span> {t.contact.responseTitle}</h3>
<p class="small">{t.contact.responseNote}</p>
+4 -4
View File
@@ -7,10 +7,10 @@ import { datenschutz, rechtstextAlsHtml } from "../../data/legal";
<div class="container">
<h1>Datenschutzerklärung</h1>
<div class="legal-note">
⚠️ <strong>Muster-Text.</strong> Vorbereiteter Platzhalter, ersetzt keine rechtliche
Prüfung. Vor Veröffentlichung an die tatsächlich eingesetzten Dienste (z. B. konkreter
Hosting-Anbieter, Zahlungsdienstleister, Versandtool) anpassen und rechtlich prüfen
lassen — im Adminbereich unter „⚖️ Rechtstexte & FAQ" bearbeitbar.
⚠️ Enthält die echten Angaben von VanVan und die tatsächlich eingesetzten Dienste
(Hosting, Zahlungsdienstleister, E-Mail-Versand), wurde aber noch nicht von einer
fachkundigen Stelle geprüft. Vor dem echten Verkaufsstart bitte fachlich prüfen lassen —
im Adminbereich unter „⚖️ Rechtstexte & FAQ" bearbeitbar.
</div>
{datenschutz.sections.map((s) => (
+1 -1
View File
@@ -45,7 +45,7 @@ const t = useTranslations(lang);
<div class="card contact-highlight">
<h3><span aria-hidden="true">💌</span> {t.contact.directTitle}</h3>
<p class="small">E-Mail: <a href="mailto:info@vans-diy-bastelbedarf.de">info@vans-diy-bastelbedarf.de</a> <span class="small">{t.contact.emailPlaceholderNote}</span></p>
<p class="small">E-Mail: <a href="mailto:vansdiyundbastelbedarf@gmail.com">vansdiyundbastelbedarf@gmail.com</a> <span class="small">{t.contact.emailPlaceholderNote}</span></p>
<p class="small">{t.contact.orderNote}</p>
<h3><span aria-hidden="true">⏱️</span> {t.contact.responseTitle}</h3>
<p class="small">{t.contact.responseNote}</p>
+1 -1
View File
@@ -45,7 +45,7 @@ const t = useTranslations(lang);
<div class="card contact-highlight">
<h3><span aria-hidden="true">💌</span> {t.contact.directTitle}</h3>
<p class="small">E-Mail: <a href="mailto:info@vans-diy-bastelbedarf.de">info@vans-diy-bastelbedarf.de</a> <span class="small">{t.contact.emailPlaceholderNote}</span></p>
<p class="small">E-Mail: <a href="mailto:vansdiyundbastelbedarf@gmail.com">vansdiyundbastelbedarf@gmail.com</a> <span class="small">{t.contact.emailPlaceholderNote}</span></p>
<p class="small">{t.contact.orderNote}</p>
<h3><span aria-hidden="true">⏱️</span> {t.contact.responseTitle}</h3>
<p class="small">{t.contact.responseNote}</p>
+4 -5
View File
@@ -7,11 +7,10 @@ import { impressum, rechtstextAlsHtml } from "../../data/legal";
<div class="container">
<h1>Impressum</h1>
<div class="legal-note">
⚠️ <strong>Muster-Text.</strong> Dies ist ein vorbereiteter Platzhalter gemäß Pflichtenheft
und ersetzt keine rechtliche Prüfung. Vor Veröffentlichung bitte von einer fachkundigen
Stelle (z. B. Anwalt/Anwältin oder IHK-Impressumsgenerator) prüfen und mit den echten
Angaben von VanVan vervollständigen — im Adminbereich unter „⚖️ Rechtstexte & FAQ"
bearbeitbar.
⚠️ Enthält die echten Angaben von VanVan, wurde aber noch nicht von einer fachkundigen
Stelle (z. B. Anwalt/Anwältin oder IHK-Impressumsgenerator) geprüft. Vor dem echten
Verkaufsstart bitte fachlich prüfen lassen — im Adminbereich unter
„⚖️ Rechtstexte & FAQ" bearbeitbar.
</div>
{impressum.sections.map((s) => (
+1 -1
View File
@@ -45,7 +45,7 @@ const t = useTranslations(lang);
<div class="card contact-highlight">
<h2><span aria-hidden="true">💌</span> {t.contact.directTitle}</h2>
<p class="small">E-Mail: <a href="mailto:info@vans-diy-bastelbedarf.de">info@vans-diy-bastelbedarf.de</a> <span class="small">{t.contact.emailPlaceholderNote}</span></p>
<p class="small">E-Mail: <a href="mailto:vansdiyundbastelbedarf@gmail.com">vansdiyundbastelbedarf@gmail.com</a> <span class="small">{t.contact.emailPlaceholderNote}</span></p>
<p class="small">{t.contact.orderNote}</p>
<h2><span aria-hidden="true">⏱️</span> {t.contact.responseTitle}</h2>
<p class="small">{t.contact.responseNote}</p>
@@ -5,16 +5,10 @@ import Layout from "../../layouts/Layout.astro";
<section class="section-tight">
<div class="container">
<h1>Muster-Widerrufsformular</h1>
<div class="legal-note">
⚠️ <strong>Muster-Text.</strong> Vorbereiteter Platzhalter, ersetzt keine rechtliche
Prüfung vor Veröffentlichung. Dieses Formular ist nicht verpflichtend — du kannst deinen
Widerruf auch formlos erklären.
</div>
<p>(Wenn du den Vertrag widerrufen möchtest, fülle bitte dieses Formular aus und sende es zurück.)</p>
<p>(Wenn du den Vertrag widerrufen möchtest, fülle bitte dieses Formular aus und sende es zurück. Dieses Formular ist nicht verpflichtend — du kannst deinen Widerruf auch formlos erklären.)</p>
<div class="card">
<p>An<br />[Vorname Nachname]<br />Van's DIY & Bastelbedarf<br />[Anschrift]<br />[E-Mail-Adresse]</p>
<p>An<br />Vanessa Geske<br />Van's DIY & Bastelbedarf<br />Von-Arnim-Str. 40, 17309 Rollwitz<br />[email protected]</p>
<p>
Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf
der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)
+3 -3
View File
@@ -7,9 +7,9 @@ import { widerruf, rechtstextAlsHtml } from "../../data/legal";
<div class="container">
<h1>Widerrufsbelehrung</h1>
<div class="legal-note">
⚠️ <strong>Muster-Text.</strong> Vorbereiteter Platzhalter, ersetzt keine rechtliche
Prüfung durch eine fachkundige Stelle vor Veröffentlichung — im Adminbereich unter
„⚖️ Rechtstexte & FAQ" bearbeitbar.
⚠️ Enthält die echten Angaben von VanVan, wurde aber noch nicht von einer fachkundigen
Stelle geprüft. Vor dem echten Verkaufsstart bitte fachlich prüfen lassen — im
Adminbereich unter „⚖️ Rechtstexte & FAQ" bearbeitbar.
</div>
{widerruf.sections.map((s) => (