diff --git a/agb.html b/agb.html index 63c696c..4a213d3 100644 --- a/agb.html +++ b/agb.html @@ -102,8 +102,8 @@

8. Verfügbarkeit und Änderungen
Der Anbieter bemüht sich um einen möglichst unterbrechungsfreien Zugang zum Supporter-Bereich, - garantiert aber keine ununterbrochene Verfügbarkeit (u. a. abhängig von Drittanbietern wie Cloudflare - und PayPal, siehe Datenschutzerklärung). Einzelne Inhalte, Meilensteine + garantiert aber keine ununterbrochene Verfügbarkeit (u. a. abhängig von Drittanbietern wie netcup, + Cloudflare und PayPal, siehe Datenschutzerklärung). Einzelne Inhalte, Meilensteine oder Prämien-Designs der Kollektionen können sich ändern, ohne dass dies den Kernanspruch auf Zugang zum Supporter-Bereich berührt.

diff --git a/kontakt.html b/kontakt.html index 017b502..019209d 100644 --- a/kontakt.html +++ b/kontakt.html @@ -75,7 +75,9 @@ Filipe Pereira Queiroz, 3, rue Arthur Thinnes, L-3919 Mondercange, Luxemburg — dogfather1608@gmail.com

2. Hosting & Server-Logdaten
- Diese Website wird über Cloudflare (Cloudflare, Inc., 101 Townsend St, San Francisco, USA; für EU-Nutzer:innen ggf. über Cloudflare-Rechenzentren in der EU) bereitgestellt. Beim Aufruf der Seite verarbeitet Cloudflare technisch notwendige Daten (u. a. IP-Adresse, Zeitstempel, aufgerufene URL, User-Agent) zur Bereitstellung, Absicherung (z. B. DDoS-Schutz) und Fehlerbehebung. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an sicherem, funktionsfähigem Betrieb). Näheres: cloudflare.com/privacypolicy.

+ 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. Beim Aufruf der Seite verarbeitet der Server technisch notwendige Daten (u. a. IP-Adresse, Zeitstempel, aufgerufene URL, User-Agent) zur Bereitstellung, Absicherung und Fehlerbehebung.
+ Dem Server vorgeschaltet ist als Content-Delivery-Network und Schutz vor Überlastungsangriffen Cloudflare (Cloudflare, Inc., 101 Townsend St, San Francisco, USA). Dabei werden dieselben technischen Daten 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.
+ Rechtsgrundlage für beides ist jeweils Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an sicherem, funktionsfähigem Betrieb).

3. Cookies & lokaler Speicher
Diese Website nutzt derzeit keine Marketing- oder Tracking-Cookies. Eingesetzt werden ausschließlich technisch notwendige Cookies bzw. Speicherungen im Browser:

@@ -92,7 +94,7 @@ Zahlungen für das DogiCrew-Supporter-Abo können über PayPal abgewickelt werden (PayPal (Europe) S.à r.l. et Cie, S.C.A., 22-24 Boulevard Royal, L-2449 Luxemburg). Dabei übermittelt PayPal die zur Zahlungsabwicklung notwendigen Daten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung). Näheres: paypal.com/privacy.

6. Bewerbungsformular
- Wer sich über ein Bewerbungsformular auf dieser Seite meldet, übermittelt die dort freiwillig angegebenen Daten (z. B. Name/Spitzname, Kontaktmöglichkeit, Nachricht). Diese werden ausschließlich zur Bearbeitung der Bewerbung genutzt und in einer Cloudflare-D1-Datenbank gespeichert, Zugriff haben nur berechtigte Teammitglieder. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b bzw. f DSGVO.

+ Wer sich über ein Bewerbungsformular auf dieser Seite meldet, übermittelt die dort freiwillig angegebenen Daten (z. B. Name/Spitzname, Kontaktmöglichkeit, Nachricht). Diese werden ausschließlich zur Bearbeitung der Bewerbung genutzt und in einer Datenbank auf dem eigenen Server (netcup, Rechenzentrum Nürnberg, siehe Ziffer 2) gespeichert, Zugriff haben nur berechtigte Teammitglieder. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b bzw. f DSGVO.

7. DogiCrew-/Supporter-Registrierung
Bei der Registrierung als Supporter:in werden Name/Anzeigename, TikTok-Benutzername, E-Mail-Adresse, ein Passwort (nur als Hash gespeichert, im Klartext nicht einsehbar) sowie Abo-/Zahlungsstatus gespeichert, um den Zugang zum Supporter-Bereich zu verwalten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung). Ein eigenständiges Löschen des Kontos ist im Supporter-Bereich jederzeit selbst möglich. Beim Abschluss des kostenpflichtigen Abos wird zusätzlich die ausdrückliche Zustimmung zum sofortigen Leistungsbeginn samt Erlöschen des Widerrufsrechts (siehe AGB, Ziffer 6) mit Zeitstempel gespeichert, um dies im Streitfall nachweisen zu können.

diff --git a/server-internal/index.js b/server-internal/index.js index a77b453..e613c10 100644 --- a/server-internal/index.js +++ b/server-internal/index.js @@ -66,6 +66,14 @@ initDb(); const PORT = Number(process.env.PORT || 4200); const app = express(); app.set("trust proxy", 1); +app.disable("x-powered-by"); // verrät sonst unnötig den eingesetzten Technik-Stack + +app.use((req, res, next) => { + res.setHeader("X-Content-Type-Options", "nosniff"); + res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); + res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); + next(); +}); app.use(cors()); app.use( @@ -81,45 +89,54 @@ app.use( }) ); +/* ---------- Absturzsicherheit ---------- + Express 4 fängt Fehler aus async-Handlern NICHT selbst ab: eine abgelehnte Promise wird zu einer + "unhandled rejection", und Node beendet daraufhin den ganzen Prozess — im Cloudflare Worker war + ein Fehler dagegen immer auf die eine betroffene Anfrage begrenzt. Ohne die folgende Absicherung + könnte z.B. eine Anfrage mit fehlender ID (better-sqlite3 wirft bei `undefined` als Parameter) + den kompletten internen Bereich offline nehmen. `wrap()` leitet jeden Fehler stattdessen an den + Fehler-Handler am Ende der Datei weiter. */ +const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); + /* ---------- Middlewares: interne Team-Session bzw. Supporter-Session ---------- */ -async function requireTeamSession(req, res, next) { +const requireTeamSession = wrap(async (req, res, next) => { const session = await requireSession(req); if (!session) return json(res, { ok: false, error: "Sitzung abgelaufen oder ungültig. Bitte erneut anmelden." }, 401); req.session = session; next(); -} +}); -async function requireSupporterAuth(req, res, next) { +const requireSupporterAuth = wrap(async (req, res, next) => { const supporter = await requireSupporterSession(req); if (!supporter) return json(res, { ok: false, error: "Sitzung abgelaufen oder ungültig. Bitte erneut einloggen." }, 401); req.supporter = supporter; next(); -} +}); /** Reicht die injizierte session/supporter als drittes Argument an den Handler weiter (wie im Cloudflare-Original). */ -const withSession = (handler) => (req, res) => handler(req, res, req.session); -const withSupporter = (handler) => (req, res) => handler(req, res, req.supporter); +const withSession = (handler) => wrap((req, res) => handler(req, res, req.session)); +const withSupporter = (handler) => wrap((req, res) => handler(req, res, req.supporter)); /* ---------- Öffentliche Routen (keine Session) ---------- */ app.get("/health", (req, res) => json(res, { ok: true, service: "dogfather-universe-interner-bereich" })); -app.post("/submit", submitApplication); -app.get("/live-status", handleLiveStatusGet); -app.post("/auth/login", login); +app.post("/submit", wrap(submitApplication)); +app.get("/live-status", wrap(handleLiveStatusGet)); +app.post("/auth/login", wrap(login)); -app.post("/supporter/register", registerSupporter); -app.post("/supporter/verify-email", verifySupporterEmail); -app.post("/supporter/login-request", requestSupporterLogin); -app.post("/supporter/verify-login", verifySupporterLogin); -app.post("/supporter/google-login", googleLoginSupporter); -app.get("/supporter/public-config", getPublicConfig); +app.post("/supporter/register", wrap(registerSupporter)); +app.post("/supporter/verify-email", wrap(verifySupporterEmail)); +app.post("/supporter/login-request", wrap(requestSupporterLogin)); +app.post("/supporter/verify-login", wrap(verifySupporterLogin)); +app.post("/supporter/google-login", wrap(googleLoginSupporter)); +app.get("/supporter/public-config", wrap(getPublicConfig)); // PayPal ruft diese Adresse direkt auf — eigene Signaturprüfung statt Session (siehe lib/paypal.js). -app.post("/supporter/paypal-webhook", handlePaypalWebhook); +app.post("/supporter/paypal-webhook", wrap(handlePaypalWebhook)); /* ---------- Team-Bereich (interne Session, requireTeamSession) ---------- */ -app.post("/auth/logout", requireTeamSession, logout); +app.post("/auth/logout", requireTeamSession, wrap(logout)); app.post("/admin/owner/set-code", requireTeamSession, withSession(setOwnerCode)); app.post("/live-status/set", requireTeamSession, withSession(handleLiveStatusSet)); @@ -170,7 +187,7 @@ app.post("/admin/supporter/collections/upsert", requireTeamSession, withSession( /* ---------- Öffentlicher Supporter-Bereich (eigene Session, requireSupporterAuth) ---------- */ -app.post("/supporter/logout", requireSupporterAuth, logoutSupporter); +app.post("/supporter/logout", requireSupporterAuth, wrap(logoutSupporter)); app.post("/supporter/me", requireSupporterAuth, withSupporter(getSupporterMe)); app.post("/supporter/update-profile", requireSupporterAuth, withSupporter(updateSupporterProfile)); app.post("/supporter/request-email-change", requireSupporterAuth, withSupporter(requestSupporterEmailChange)); @@ -183,6 +200,29 @@ app.post("/supporter/paypal/cancel", requireSupporterAuth, withSupporter(cancelP app.use((req, res) => json(res, { ok: false, error: "Not found." }, 404)); +/* ---------- Fehler-Handler (muss NACH allen Routen stehen) ---------- + Fängt alles ab, was in einem Handler schiefgeht, und antwortet mit sauberem JSON statt einer + HTML-Fehlerseite mit Stacktrace. Ersetzt gleichzeitig die try/catch-Behandlung für kaputtes + JSON, die im Cloudflare-Original noch in jeder Route einzeln stand. */ +// eslint-disable-next-line no-unused-vars +app.use((err, req, res, next) => { + if (err?.type === "entity.parse.failed") { + return json(res, { ok: false, error: "Ungültiges JSON." }, 400); + } + if (err?.type === "entity.too.large") { + return json(res, { ok: false, error: "Die Anfrage ist zu groß." }, 413); + } + console.error(`[fehler] ${req.method} ${req.originalUrl}:`, err?.stack || err); + if (res.headersSent) return; + return json(res, { ok: false, error: "Interner Fehler." }, 500); +}); + +/* Letztes Netz: Ein unbeaufsichtigter Fehler darf den Dienst nicht beenden. Ohne diese beiden + Zeilen würde Node den Prozess bei einer unbehandelten Promise-Ablehnung sofort beenden und der + gesamte interne Bereich (inkl. Zahlungsverarbeitung) wäre bis zum systemd-Neustart offline. */ +process.on("unhandledRejection", (grund) => console.error("[unhandledRejection]", grund)); +process.on("uncaughtException", (fehler) => console.error("[uncaughtException]", fehler?.stack || fehler)); + /* ---------- Cron: läuft jede Minute, hält den Live-Punkt aktuell (ersetzt Cloudflares scheduled()) ---------- */ cron.schedule("* * * * *", () => { refreshLiveFromTikTok().catch((err) => console.error("[live-status] Fehler beim Aktualisieren:", err?.message || err)); diff --git a/server-internal/lib/auth.js b/server-internal/lib/auth.js index 35c3237..d99cf6a 100644 --- a/server-internal/lib/auth.js +++ b/server-internal/lib/auth.js @@ -12,8 +12,21 @@ const LOCKOUT_MINUTES = 15; const INACTIVITY_MS = 24 * 60 * 60 * 1000; const SESSION_MAX_AGE_MS = 24 * 60 * 60 * 1000; +/* Schlüssel für die Fehlversuchs-Zählung (Login-Sperre). + + SICHERHEITSKORREKTUR 05.08.2026: Vorher stand hier `req.headers["cf-connecting-ip"] || req.ip`. + Bei Cloudflare war das sicher, weil Cloudflare diesen Header selbst setzt und einen vom Client + mitgeschickten überschreibt. Auf dem eigenen Server gilt das NICHT mehr: der Ursprungsserver ist + (nachgewiesen am 05.08.) auch direkt unter seiner IP erreichbar, also an Cloudflare vorbei — ein + Angreifer konnte dort bei jedem Versuch einen anderen `CF-Connecting-IP`-Wert mitschicken und so + die 5-Versuche-Sperre komplett aushebeln (unbegrenztes Durchprobieren des Owner-Codes). + + `req.ip` ist hier nicht fälschbar: `app.set("trust proxy", 1)` in index.js lässt Express genau + eine Proxy-Ebene (den lokalen Caddy) vertrauen, und Caddy hängt die echte Verbindungs-IP hinten + an `X-Forwarded-For` an — ein vom Client vorgetäuschter Wert landet weiter vorne und wird + ignoriert. */ export function clientKey(req) { - return req.headers["cf-connecting-ip"] || req.ip || "unknown"; + return req.ip || req.socket?.remoteAddress || "unknown"; } function timingSafeEqual(a, b) { diff --git a/server/gate.js b/server/gate.js index 2d0386d..88bd852 100644 --- a/server/gate.js +++ b/server/gate.js @@ -80,7 +80,9 @@ export function gateMiddleware(req, res, next) { if (!codesKonfiguriert) return next(); if (pfad === "/gate-auth" && req.method === "POST") { - return gateAuthHandler(req, res); + // .catch(next) ergänzt 05.08.2026: ohne das würde ein Fehler beim Signieren zu einer + // unbehandelten Promise-Ablehnung und damit zum Absturz des ganzen Servers führen. + return Promise.resolve(gateAuthHandler(req, res)).catch(next); } const sessionToken = req.cookies?.[COOKIE_NAME]; @@ -101,5 +103,5 @@ export function gateMiddleware(req, res, next) { const nextParam = pfad === "/" ? "/index.html" : pfad + (req.originalUrl.includes("?") ? "?" + req.originalUrl.split("?")[1] : ""); return res.redirect(302, `/gate.html?next=${encodeURIComponent(nextParam)}`); - })(); + })().catch(next); // siehe oben: verhindert Serverabsturz bei einem Fehler in der Prüfung } diff --git a/server/index.js b/server/index.js index e3ac177..d8e2201 100644 --- a/server/index.js +++ b/server/index.js @@ -18,6 +18,20 @@ const PORT = Number(process.env.PORT || 4100); const app = express(); 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. Vorher standen diese in der Datei `_headers` — die + funktioniert aber NUR bei Cloudflare Pages/Netlify und wurde auf dem eigenen Server schlicht + ignoriert, d.h. die Seite lief seit dem Umzug komplett ohne diese Schutzmaßnahmen (nachgeprüft + an den Live-Antwortheadern). */ +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=()"); + res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); + next(); +}); app.use(cookieParser()); app.use(express.json({ limit: "1mb" })); @@ -31,6 +45,19 @@ app.use((req, res) => { }); }); +/* Fehler-Handler (muss NACH allen Routen stehen), ergänzt 05.08.2026: antwortet mit einer + schlichten Meldung statt einer Express-Standardseite mit komplettem Stacktrace und Dateipfaden. */ +// eslint-disable-next-line no-unused-vars +app.use((err, req, res, next) => { + console.error(`[fehler] ${req.method} ${req.originalUrl}:`, err?.stack || err); + if (res.headersSent) return; + res.status(500).send("Es ist ein Fehler aufgetreten. Bitte später erneut versuchen."); +}); + +/* Letztes Netz: Ein unbehandelter Fehler darf die Website nicht offline nehmen. */ +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(`DogFather Universe – Server läuft auf http://127.0.0.1:${PORT}`); });