From b27c69c4bdafe0b733bab78791e1f643bd08ebdb Mon Sep 17 00:00:00 2001 From: JUSN Date: Fri, 24 Jul 2026 13:51:37 +0200 Subject: [PATCH] Web-Demo Fix: geteilte Anschluesse blockierten den ersten Versuch Fehler: Der anonyme Erst-Versuch war auf 1 pro IP (30 Tage) begrenzt. Teilen sich mehrere Menschen eine Leitung (Familie, WLAN, Mobilfunk/CGNAT), bekam der Zweite sofort 'E-Mail noetig' statt einer Analyse. Trat auch beim Testen ueber denselben Anschluss auf. - IP- und Anon-Zaehler jetzt TAEGLICH (Datum im Schluessel), 48h TTL - Anonyme Erst-Analysen pro Anschluss/Tag: 3 (statt 1/30 Tage) - IP-Gesamtlimit pro Tag: 8 (statt 3/30 Tage) - 'Wallet'-Schutz unveraendert: globales Tagesbudget 100 + Turnstile + E-Mail-Hash (dauerhaft 1x/Adresse) - Client: email_noetig markiert den Browser NICHT mehr als verbraucht (sonst dauerhafte Blockade ohne gelungene Analyse); localStorage-Key auf 'bk_demo_v2' umbenannt -> setzt falsche Testphasen-Staende zurueck Ende-zu-Ende verifiziert: frischer Erst-Upload liefert wieder das Ergebnis. Co-Authored-By: Claude Fable 5 --- proxy/src/index.ts | 38 ++++++++++++++++++++++++-------------- proxy/wrangler.toml | 6 ++++-- webseite/demo.html | 12 +++++++++--- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/proxy/src/index.ts b/proxy/src/index.ts index edb863f..c49eb4c 100644 --- a/proxy/src/index.ts +++ b/proxy/src/index.ts @@ -212,13 +212,21 @@ async function demoHandler(request: Request, env: Env): Promise { mailKey = `demo:mail:${await emailHash(email, env.TURNSTILE_SECRET_KEY)}`; } - // Schichten 2-5: Limits lesen + // Schichten 2-5: Limits lesen. IP- und Anon-Zähler sind bewusst TÄGLICH + // (Datum im Schlüssel): Ein Anschluss, den sich mehrere Menschen teilen + // (Familie, WLAN, Mobilfunk/CGNAT), darf pro Tag mehrere erste Analysen + // machen. Sonst würde der zweite Besucher an derselben Leitung sofort zur + // E-Mail gezwungen. Die eigentliche "2 pro Person"-Grenze setzt der + // Browser (localStorage) + der E-Mail-Hash; IP ist nur ein Missbrauchs-Damm. const heute = new Date().toISOString().slice(0, 10); const tagKey = `demo:tag:${heute}`; - const ipKey = `demo:ip:${ip}`; - const anonKey = `demo:anon:${ip}`; + const ipKey = `demo:ip:${ip}:${heute}`; + const anonKey = `demo:anon:${ip}:${heute}`; const budget = parseInt(env.DEMO_TAGES_BUDGET || '100', 10); - const ipLimit = parseInt(env.DEMO_LIMIT_IP || '3', 10); + const ipLimit = parseInt(env.DEMO_LIMIT_IP || '8', 10); + // Wie viele anonyme (ohne E-Mail) Analysen ein Anschluss pro Tag darf, + // bevor eine E-Mail nötig wird — großzügig, damit geteilte Leitungen gehen. + const ANON_PRO_IP = 3; const [tagWert, ipWert, anonWert, mailWert] = await Promise.all([ env.RATE_LIMIT.get(tagKey), @@ -234,13 +242,14 @@ async function demoHandler(request: Request, env: Env): Promise { if (tagZahl >= budget) { return demoFehler(429, 'budget', 'Die Gratis-Demo ist für heute ausgebucht. Kommen Sie morgen wieder — oder tragen Sie sich in die Warteliste ein.', origin); } - // Schicht 4: IP-Gesamtlimit (30 Tage) + // Schicht 4: IP-Gesamtlimit pro Tag (Missbrauchs-Damm, großzügig) if (ipZahl >= ipLimit) { - return demoFehler(429, 'aufgebraucht', 'Die Gratis-Analysen für diesen Anschluss sind aufgebraucht. In der App gibt es 3 weitere gratis.', origin); + return demoFehler(429, 'aufgebraucht', 'Für heute wurden über diesen Anschluss viele Gratis-Analysen genutzt. Kommen Sie morgen wieder — oder holen Sie sich die App.', origin); } - // Schicht 2/3: ohne E-Mail nur 1x, pro E-Mail nur 1x (dauerhaft) - if (!email && anonZahl >= 1) { - return demoFehler(403, 'email_noetig', 'Ihre erste Gratis-Analyse ist verbraucht. Für die zweite geben Sie bitte Ihre E-Mail-Adresse ein.', origin); + // Schicht 2/3: nach mehreren anonymen Analysen pro Anschluss E-Mail nötig; + // pro E-Mail dauerhaft nur 1x + if (!email && anonZahl >= ANON_PRO_IP) { + return demoFehler(403, 'email_noetig', 'Für weitere Gratis-Analysen über diesen Anschluss geben Sie bitte Ihre E-Mail-Adresse ein.', origin); } if (mailKey && mailWert !== null) { return demoFehler(429, 'aufgebraucht', 'Mit dieser E-Mail-Adresse wurde die Gratis-Analyse schon genutzt. In der App gibt es 3 weitere gratis.', origin); @@ -291,14 +300,15 @@ async function demoHandler(request: Request, env: Env): Promise { content?: { type: string; text?: string }[]; }; - // Erst NACH erfolgreichem KI-Aufruf zählen (Fehler kosten kein Kontingent) - const MONAT_TTL = 60 * 60 * 24 * 30; + // Erst NACH erfolgreichem KI-Aufruf zählen (Fehler kosten kein Kontingent). + // Tag-, IP- und Anon-Zähler laufen nach 48h ab (sie sind tagesbasiert). + const TAG_TTL = 60 * 60 * 48; const schreiben: Promise[] = [ - env.RATE_LIMIT.put(tagKey, String(tagZahl + 1), { expirationTtl: 60 * 60 * 48 }), - env.RATE_LIMIT.put(ipKey, String(ipZahl + 1), { expirationTtl: MONAT_TTL }), + env.RATE_LIMIT.put(tagKey, String(tagZahl + 1), { expirationTtl: TAG_TTL }), + env.RATE_LIMIT.put(ipKey, String(ipZahl + 1), { expirationTtl: TAG_TTL }), ]; if (!email) { - schreiben.push(env.RATE_LIMIT.put(anonKey, String(anonZahl + 1), { expirationTtl: MONAT_TTL })); + schreiben.push(env.RATE_LIMIT.put(anonKey, String(anonZahl + 1), { expirationTtl: TAG_TTL })); } if (mailKey) { // bewusst OHNE TTL: „pro E-Mail für immer nur 1" — es liegt nur der Hash diff --git a/proxy/wrangler.toml b/proxy/wrangler.toml index 7df1ae1..3a8faee 100644 --- a/proxy/wrangler.toml +++ b/proxy/wrangler.toml @@ -9,9 +9,11 @@ compatibility_date = "2026-07-01" TAGES_LIMIT = "20" TAGES_LIMIT_IP = "100" # Web-Demo (/demo): hartes Tagesbudget über alle Nutzer (Kosten-Deckel, -# 100 Analysen ≈ 2,50 €/Tag Maximalschaden) + IP-Limit über 30 Tage +# 100 Analysen ≈ 2,50 €/Tag Maximalschaden) + IP-Gesamtlimit PRO TAG +# (großzügig, damit geteilte Anschlüsse/Familien/Mobilfunk nicht blockiert +# werden; der Missbrauch wird zusätzlich durch Turnstile + Budget begrenzt) DEMO_TAGES_BUDGET = "100" -DEMO_LIMIT_IP = "3" +DEMO_LIMIT_IP = "8" [[kv_namespaces]] binding = "RATE_LIMIT" diff --git a/webseite/demo.html b/webseite/demo.html index 6b94143..6510de8 100644 --- a/webseite/demo.html +++ b/webseite/demo.html @@ -339,8 +339,11 @@ var eingabe = document.getElementById('eingabe'); var fehlerBox = document.getElementById('fehler'); - function benutzt() { return parseInt(localStorage.getItem('bk_demo_benutzt') || '0', 10); } - function setzeBenutzt(n) { localStorage.setItem('bk_demo_benutzt', String(n)); } + // Schlüsselname bewusst 'v2': setzt evtl. falsch gezählte Stände (aus der + // Testphase) bei bestehenden Besuchern automatisch einmalig zurück. + var SPEICHER_SCHLUESSEL = 'bk_demo_v2'; + function benutzt() { return parseInt(localStorage.getItem(SPEICHER_SCHLUESSEL) || '0', 10); } + function setzeBenutzt(n) { localStorage.setItem(SPEICHER_SCHLUESSEL, String(n)); } function zeigeStand() { // 2. Versuch: E-Mail-Feld einblenden; alles verbraucht: Eingabe sperren @@ -445,7 +448,10 @@ eingabe.style.display = 'block'; if (window.turnstile) window.turnstile.reset(); if (daten.code === 'email_noetig') { - setzeBenutzt(Math.max(benutzt(), 1)); + // Nur das E-Mail-Feld zeigen — NICHT als verbraucht markieren. + // (Sonst würde ein serverseitiges "E-Mail nötig", z. B. bei + // geteiltem Anschluss, den Browser dauerhaft blockieren, obwohl + // gar keine Analyse gelungen ist.) emailTeil.style.display = 'block'; emailFeld.focus(); }