Shop Review A I4: Shop-Limit 300 je Stunde, hoechstens 3 offene Bestellungen je IP und Shop in 30 Minuten

Die IP wird nur als gesalzener Kurz-Hash (sha256, 16 Hex-Zeichen) im Bestell-JSON abgelegt; der Tagesjob entfernt ihn bei der Anonymisierung mit. Die Pruefung laeuft unter der Bestandssperre, Honigtopf und Fehlversuche legen nichts an und zaehlen nicht. Antwort 429 zu_viele_offen mit Retry-After 1800.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 09:34:03 +00:00
1 parent 55fe1c567f
commit cb6291410d
5 files changed
+71 -11

No files matched your search

+5 -5
View File
@@ -3,7 +3,7 @@ import { config } from '../config.js'
import { analyseDomains } from '../services/analyse.js'
import { getDocument } from '../services/appwriteAdmin.js'
import { bildMeta, bildPfad } from '../services/shop/bilder.js'
import { abschlussAusstehend, bestellungLesen, legeBestellungAn, pruefeRueckUrl, reserviert, schliesseAb, tokenPasst } from '../services/shop/bestellungen.js'
import { abschlussAusstehend, bestellungLesen, ipHashVon, legeBestellungAn, pruefeRueckUrl, reserviert, schliesseAb, tokenPasst } from '../services/shop/bestellungen.js'
import { sessionHolen } from '../services/shop/connect.js'
import { istGeoeffnet, oeffentlicheRechtstexte, shopPerSiteKey } from '../services/shop/kontext.js'
import { euro } from '../services/shop/geld.js'
@@ -458,7 +458,7 @@ router.post(
const PRUEFEN_JE_IP = 60
const BESTELLUNG_JE_IP = 10
const BESTELLUNG_FENSTER_MS = 10 * MINUTE_MS
const BESTELLUNG_JE_SHOP = 60
const BESTELLUNG_JE_SHOP = 300
const STATUS_JE_IP = 120
const STUNDE_MS = 60 * MINUTE_MS
const BESTELL_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/
@@ -466,7 +466,7 @@ const BESTELL_TOKEN_RE = /^[A-Za-z0-9_-]{16,128}$/
const GESCHLOSSEN_TEXT = 'Der Shop ist gerade geschlossen.'
const GESCHLOSSEN = () => fehlerMit(409, { _: GESCHLOSSEN_TEXT }, { code: 'geschlossen' })
// Fehlercodes aus berechne/legeBestellungAn, die Kaeufer sehen duerfen (Stripe-Fehler haben ebenfalls einen String-code)
const KAEUFER_CODES = new Set(['geschlossen', 'leer', 'nicht_verfuegbar', 'menge', 'variante', 'ausverkauft', 'lieferart', 'land', 'nur_abholung', 'mindestbestellwert', 'zu_viele', 'zahlung_nicht_moeglich'])
const KAEUFER_CODES = new Set(['geschlossen', 'leer', 'nicht_verfuegbar', 'menge', 'variante', 'ausverkauft', 'lieferart', 'land', 'nur_abholung', 'mindestbestellwert', 'zu_viele', 'zu_viele_offen', 'zahlung_nicht_moeglich'])
const KONFLIKT_CODES = new Set(['ausverkauft', 'nicht_verfuegbar', 'nur_abholung'])
/** Artikel-Map fuer berechne: id => normalisiertes Produkt (aus dem JSON-Text), aktiv aus Spalte und JSON (wie bei legeBestellungAn). */
@@ -487,7 +487,7 @@ function produktKarte(docs) {
/** Fehler aus legeBestellungAn fuer den Kaeufer: eigene Fehler (bekannter code) mit ihrem Text, alles andere 503 ohne Rohtext. */
function bestellFehler(err) {
if (typeof err?.code === 'string' && KAEUFER_CODES.has(err.code) && Number.isInteger(err.status) && err.status >= 400 && err.status < 600) {
const extra = { code: err.code, ...(typeof err.produktId === 'string' ? { produktId: err.produktId } : {}) }
const extra = { code: err.code, ...(typeof err.produktId === 'string' ? { produktId: err.produktId } : {}), ...(Number.isInteger(err.retryAfter) ? { retryAfter: err.retryAfter } : {}) }
if (err.status < 500) return fehlerMit(err.status, { _: String(err.message) }, extra)
return Object.assign(new Error(String(err.message)), { status: 503, fehler: { _: String(err.message) }, ...extra, dienst: true })
}
@@ -562,7 +562,7 @@ router.post(
if (istFalle(siteKey, b, now)) return { body: { url: pruefeRueckUrl(b.rueckUrl, ctx) } }
try {
const { url } = await legeBestellungAn(ctx, { ...warenkorbEingabe(b), rueckUrl: b.rueckUrl }, { now })
const { url } = await legeBestellungAn(ctx, { ...warenkorbEingabe(b), rueckUrl: b.rueckUrl }, { now, ipHash: ipHashVon(req.ip) })
return { body: { url } }
} catch (err) {
return bestellErgebnis(err)