Schutz gegen kursierende Partner-Codes (Rene: 'Bonus-Codes kursieren gerne das Internet')

1. QR-URL verrät den Code nicht mehr: /q/{token} → /?qr=TOKEN (vorher stand
   der tippbare Code in der Adresszeile jedes Scanners). Registrierung löst
   den Code server-seitig aus dem Token auf (auch ohne ref_code).
2. Notbremse: partner_codes.active — Admin kann Codes pausieren (Einlösung
   gesperrt, Info-Endpoint 404, Historie/QR-Kontingente bleiben) und
   reaktivieren. UI: ⏸/▶-Toggle + pausiert-Badge in der Codes-Tabelle.
3. max_uses im Anlege-Formular standardmäßig 50 statt unbegrenzt.

Tests: QR-only-Registrierung, Pause→keine Einlösung→Reaktivierung,
Redirect ohne Klartext-Code. Suite: 54 passed.
This commit is contained in:
rene 2026-06-07 19:35:31 +02:00
parent 21bcc6b962
commit 2927ae2672
11 changed files with 136 additions and 39 deletions

View file

@ -50,7 +50,9 @@ def test_scan_redirects_and_counts(client, admin):
r = client.get(f"/q/{token}", follow_redirects=False)
assert r.status_code == 302
assert r.headers["location"] == f"/?ref={code['code']}&qr={token}"
# Bewusst KEIN Klartext-Code in der URL — sonst liest jeder Scanner den Code ab
assert r.headers["location"] == f"/?qr={token}"
assert code["code"] not in r.headers["location"]
client.get(f"/q/{token}", follow_redirects=False)
r = client.get("/api/admin/partner/qr-batches", headers=admin["headers"])
@ -104,6 +106,52 @@ def test_registration_attributed_to_qr(client, admin):
assert regs[0]["created_at"]
def test_registration_with_qr_only(client, admin):
"""Registrierung NUR mit qr_token (ohne ref_code) -> Code wird server-seitig aufgeloest."""
code = _create_code(client, admin)
batch = _create_batch(client, admin, code["id"], quantity=1)
token = _batch_tokens(batch["id"])[0]
email = f"qro-{secrets.token_hex(4)}@example.com"
r = client.post("/api/auth/register", json={
"email": email, "password": "QrTest1234!", "name": f"qro{secrets.token_hex(3)}",
"qr_token": token,
})
assert r.status_code == 200, r.text
from database import db
with db() as conn:
row = conn.execute("SELECT referred_by, referred_qr FROM users WHERE email=?", (email,)).fetchone()
assert row["referred_by"] == -code["id"]
assert row["referred_qr"] == token
def test_paused_code_not_redeemable(client, admin):
"""Pausierter Code (Notbremse) -> keine Einloesung, Info-Endpoint 404; reaktivierbar."""
code = _create_code(client, admin)
r = client.post(f"/api/admin/partner/codes/{code['id']}/toggle", headers=admin["headers"])
assert r.status_code == 200 and r.json()["active"] == 0
# Info-Endpoint: wie nicht existent
assert client.get(f"/api/partner/codes/{code['code']}/info").status_code == 404
# Registrierung mit pausiertem Code -> keine Zuordnung
email = f"qrp-{secrets.token_hex(4)}@example.com"
r = client.post("/api/auth/register", json={
"email": email, "password": "QrTest1234!", "name": f"qrp{secrets.token_hex(3)}",
"ref_code": code["code"],
})
assert r.status_code == 200, r.text
from database import db
with db() as conn:
row = conn.execute("SELECT referred_by FROM users WHERE email=?", (email,)).fetchone()
assert row["referred_by"] is None
# Reaktivieren funktioniert
r = client.post(f"/api/admin/partner/codes/{code['id']}/toggle", headers=admin["headers"])
assert r.json()["active"] == 1
assert client.get(f"/api/partner/codes/{code['code']}/info").status_code == 200
def test_qr_token_must_match_code(client, admin):
"""QR-Token eines FREMDEN Codes wird nicht zugeordnet (Manipulationsschutz)."""
code_a = _create_code(client, admin)