generated from knso/webklar-preview-template
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 <noreply@anthropic.com>
This commit is contained in:
@@ -212,13 +212,21 @@ async function demoHandler(request: Request, env: Env): Promise<Response> {
|
|||||||
mailKey = `demo:mail:${await emailHash(email, env.TURNSTILE_SECRET_KEY)}`;
|
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 heute = new Date().toISOString().slice(0, 10);
|
||||||
const tagKey = `demo:tag:${heute}`;
|
const tagKey = `demo:tag:${heute}`;
|
||||||
const ipKey = `demo:ip:${ip}`;
|
const ipKey = `demo:ip:${ip}:${heute}`;
|
||||||
const anonKey = `demo:anon:${ip}`;
|
const anonKey = `demo:anon:${ip}:${heute}`;
|
||||||
const budget = parseInt(env.DEMO_TAGES_BUDGET || '100', 10);
|
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([
|
const [tagWert, ipWert, anonWert, mailWert] = await Promise.all([
|
||||||
env.RATE_LIMIT.get(tagKey),
|
env.RATE_LIMIT.get(tagKey),
|
||||||
@@ -234,13 +242,14 @@ async function demoHandler(request: Request, env: Env): Promise<Response> {
|
|||||||
if (tagZahl >= budget) {
|
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);
|
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) {
|
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)
|
// Schicht 2/3: nach mehreren anonymen Analysen pro Anschluss E-Mail nötig;
|
||||||
if (!email && anonZahl >= 1) {
|
// pro E-Mail dauerhaft nur 1x
|
||||||
return demoFehler(403, 'email_noetig', 'Ihre erste Gratis-Analyse ist verbraucht. Für die zweite geben Sie bitte Ihre E-Mail-Adresse ein.', origin);
|
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) {
|
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);
|
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<Response> {
|
|||||||
content?: { type: string; text?: string }[];
|
content?: { type: string; text?: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Erst NACH erfolgreichem KI-Aufruf zählen (Fehler kosten kein Kontingent)
|
// Erst NACH erfolgreichem KI-Aufruf zählen (Fehler kosten kein Kontingent).
|
||||||
const MONAT_TTL = 60 * 60 * 24 * 30;
|
// Tag-, IP- und Anon-Zähler laufen nach 48h ab (sie sind tagesbasiert).
|
||||||
|
const TAG_TTL = 60 * 60 * 48;
|
||||||
const schreiben: Promise<void>[] = [
|
const schreiben: Promise<void>[] = [
|
||||||
env.RATE_LIMIT.put(tagKey, String(tagZahl + 1), { expirationTtl: 60 * 60 * 48 }),
|
env.RATE_LIMIT.put(tagKey, String(tagZahl + 1), { expirationTtl: TAG_TTL }),
|
||||||
env.RATE_LIMIT.put(ipKey, String(ipZahl + 1), { expirationTtl: MONAT_TTL }),
|
env.RATE_LIMIT.put(ipKey, String(ipZahl + 1), { expirationTtl: TAG_TTL }),
|
||||||
];
|
];
|
||||||
if (!email) {
|
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) {
|
if (mailKey) {
|
||||||
// bewusst OHNE TTL: „pro E-Mail für immer nur 1" — es liegt nur der Hash
|
// bewusst OHNE TTL: „pro E-Mail für immer nur 1" — es liegt nur der Hash
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ compatibility_date = "2026-07-01"
|
|||||||
TAGES_LIMIT = "20"
|
TAGES_LIMIT = "20"
|
||||||
TAGES_LIMIT_IP = "100"
|
TAGES_LIMIT_IP = "100"
|
||||||
# Web-Demo (/demo): hartes Tagesbudget über alle Nutzer (Kosten-Deckel,
|
# 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_TAGES_BUDGET = "100"
|
||||||
DEMO_LIMIT_IP = "3"
|
DEMO_LIMIT_IP = "8"
|
||||||
|
|
||||||
[[kv_namespaces]]
|
[[kv_namespaces]]
|
||||||
binding = "RATE_LIMIT"
|
binding = "RATE_LIMIT"
|
||||||
|
|||||||
@@ -339,8 +339,11 @@
|
|||||||
var eingabe = document.getElementById('eingabe');
|
var eingabe = document.getElementById('eingabe');
|
||||||
var fehlerBox = document.getElementById('fehler');
|
var fehlerBox = document.getElementById('fehler');
|
||||||
|
|
||||||
function benutzt() { return parseInt(localStorage.getItem('bk_demo_benutzt') || '0', 10); }
|
// Schlüsselname bewusst 'v2': setzt evtl. falsch gezählte Stände (aus der
|
||||||
function setzeBenutzt(n) { localStorage.setItem('bk_demo_benutzt', String(n)); }
|
// 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() {
|
function zeigeStand() {
|
||||||
// 2. Versuch: E-Mail-Feld einblenden; alles verbraucht: Eingabe sperren
|
// 2. Versuch: E-Mail-Feld einblenden; alles verbraucht: Eingabe sperren
|
||||||
@@ -445,7 +448,10 @@
|
|||||||
eingabe.style.display = 'block';
|
eingabe.style.display = 'block';
|
||||||
if (window.turnstile) window.turnstile.reset();
|
if (window.turnstile) window.turnstile.reset();
|
||||||
if (daten.code === 'email_noetig') {
|
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';
|
emailTeil.style.display = 'block';
|
||||||
emailFeld.focus();
|
emailFeld.focus();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user