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:
1 parent
55fe1c567f
commit
cb6291410d
5 files changed
+71
-11
No files matched your search
@@ -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)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { config } from '../../config.js'
|
||||
import { createDocument, getDocument, listAllDocuments, listDocuments, Query, updateDocument } from '../appwriteAdmin.js'
|
||||
import { analyseDomains } from '../analyse.js'
|
||||
import { withLocks } from '../keyedLock.js'
|
||||
import { ipSchluessel } from '../termine/limits.js'
|
||||
import { checkoutAnlegen, erstatten, sessionAblaufen, sessionHolen } from './connect.js'
|
||||
import { datumKurz, heute, plusTage, uhrzeit, zuUtc } from '../termine/zeit.js'
|
||||
import { euro } from './geld.js'
|
||||
@@ -29,6 +30,10 @@ const NUMMER_VERSUCHE = 5
|
||||
/** Stripe verlangt mindestens 30 Minuten; die Reserve deckt Zeit zwischen Berechnung und Aufruf ab. */
|
||||
const SESSION_RESERVE_S = 30
|
||||
const MAX_JSON = 60000
|
||||
/** Offene Bestellungen, die eine IP je Shop gleichzeitig haben darf (Fenster: Reservierungsdauer), und der Text dazu. */
|
||||
const OFFEN_JE_IP = 3
|
||||
const OFFEN_FENSTER_MS = 30 * 60 * 1000
|
||||
const ZU_VIELE_OFFEN_TEXT = 'Sie haben bereits offene Bestellungen. Bitte schließen Sie diese zuerst ab oder warten Sie 30 Minuten.'
|
||||
|
||||
const GESCHLOSSEN_TEXT = 'Der Shop ist gerade geschlossen.'
|
||||
const ZAHLUNG_TEXT = 'Die Zahlung kann gerade nicht gestartet werden. Bitte versuchen Sie es später noch einmal.'
|
||||
@@ -37,6 +42,11 @@ const KLARNA_HINWEIS =
|
||||
'Bitte aktiviere die Zahlungsarten in deinem Stripe-Dashboard unter Einstellungen > Zahlungsmethoden.'
|
||||
|
||||
const col = () => config.collections.shopOrders
|
||||
|
||||
/** Gesalzener Kurz-Hash der IP (16 Hex-Zeichen) fuer die Obergrenze offener Bestellungen; die Adresse selbst wird nie gespeichert. */
|
||||
export function ipHashVon(ip) {
|
||||
return createHash('sha256').update(`shop-ip:${config.sessionSecret}:${ipSchluessel(ip)}`).digest('hex').slice(0, 16)
|
||||
}
|
||||
const istObjekt = (wert) => wert !== null && typeof wert === 'object' && !Array.isArray(wert)
|
||||
|
||||
function fehler(status, code, text, extra = {}) {
|
||||
@@ -237,10 +247,11 @@ async function storniere(orderId, daten, was, now) {
|
||||
* Preise, Bestand und Versand kommen immer aus der Datenbank; der Browser liefert nur Artikel, Mengen, Lieferart und Land.
|
||||
* @param {object} ctx Kontext aus shopPerSiteKey
|
||||
* @param {{ positionen: Array, lieferart: string, land?: string, rueckUrl?: string }} eingabe
|
||||
* @param {{ now?: Date }} [opt]
|
||||
* @param {{ now?: Date, ipHash?: string }} [opt] ipHash (ipHashVon): hoechstens 3 offene Bestellungen je IP in 30 Minuten (429 zu_viele_offen),
|
||||
* der Hash steht in der Bestellung (wird vom Tagesjob mit anonymisiert)
|
||||
* @returns {Promise<{ url: string, orderId: string, bestellnummer: string }>}
|
||||
*/
|
||||
export async function legeBestellungAn(ctx, eingabe, { now = new Date() } = {}) {
|
||||
export async function legeBestellungAn(ctx, eingabe, { now = new Date(), ipHash } = {}) {
|
||||
const { shop } = ctx
|
||||
const e = shop.einstellungen
|
||||
const geschlossen = () => fehler(409, 'geschlossen', GESCHLOSSEN_TEXT)
|
||||
@@ -252,6 +263,12 @@ export async function legeBestellungAn(ctx, eingabe, { now = new Date() } = {})
|
||||
const angelegt = await withLocks([`shop-bestand:${shop.id}`], async () => {
|
||||
const docs = await listeProdukte(shop.id)
|
||||
if (!(await istGeoeffnet(ctx, { produkte: docs }))) throw geschlossen()
|
||||
if (typeof ipHash === 'string' && ipHash !== '') {
|
||||
const seit = jetzt.getTime() - OFFEN_FENSTER_MS
|
||||
const offene = await listAllDocuments(col(), [Query.equal('shopId', shop.id), Query.equal('status', 'offen')])
|
||||
const eigene = offene.filter((d) => new Date(d.erstelltAt).getTime() > seit && bestellDaten(d).ipHash === ipHash).length
|
||||
if (eigene >= OFFEN_JE_IP) throw fehler(429, 'zu_viele_offen', ZU_VIELE_OFFEN_TEXT, { retryAfter: OFFEN_FENSTER_MS / 1000 })
|
||||
}
|
||||
const produkte = produktKarte(docs)
|
||||
const ergebnis = berechne({
|
||||
produkte,
|
||||
@@ -288,6 +305,7 @@ export async function legeBestellungAn(ctx, eingabe, { now = new Date() } = {})
|
||||
zahlungsarten: e.zahlungsarten,
|
||||
...(ergebnis.lieferart === 'abholung' ? { abholung: { adresse: e.abholung.adresse, hinweis: e.abholung.hinweis } } : {}),
|
||||
rechtstexteHash: shop.rechtstexte.bestaetigt.hash,
|
||||
...(typeof ipHash === 'string' && ipHash !== '' ? { ipHash } : {}),
|
||||
verlauf: [{ at: jetzt.toISOString(), was: 'angelegt' }],
|
||||
}
|
||||
const json = JSON.stringify(daten)
|
||||
|
||||
@@ -160,6 +160,7 @@ async function anonymisiereBestellungen(lauf, shopId, grenze) {
|
||||
const daten = bestellDaten(doc)
|
||||
delete daten.lieferadresse
|
||||
delete daten.rechnungsadresse
|
||||
delete daten.ipHash
|
||||
daten.kaeufer = { name: '', email: '', telefon: '' }
|
||||
await updateDocument(sammlung, doc.$id, {
|
||||
kaeuferEmail: '',
|
||||
|
||||
Reference in new issue
Block a user