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

@ -205,11 +205,26 @@ async def register(data: RegisterRequest, response: Response, request: Request):
).fetchone()
new_user_id = user["id"]
if data.ref_code:
code_upper = data.ref_code.strip().upper()
# Zuerst prüfen ob es ein Partner-Code ist
# QR-only-Flow: Die Scan-URL trägt bewusst KEINEN Klartext-Code mehr —
# der Partner-Code wird hier server-seitig aus dem QR-Token aufgelöst.
ref_code_in = data.ref_code
if not ref_code_in and data.qr_token:
qr_row = conn.execute(
"""SELECT pc.code FROM partner_qr_codes q
JOIN partner_qr_batches b ON b.id = q.batch_id
JOIN partner_codes pc ON pc.id = b.partner_code_id
WHERE q.token=?""",
(data.qr_token.strip(),)
).fetchone()
if qr_row:
ref_code_in = qr_row["code"]
if ref_code_in:
code_upper = ref_code_in.strip().upper()
# Zuerst prüfen ob es ein Partner-Code ist (active=0 = Notbremse bei
# geleakten Codes: wird wie nicht existent behandelt, Historie bleibt)
partner = conn.execute(
"SELECT id, grants_founder, max_uses FROM partner_codes WHERE code=?",
"SELECT id, grants_founder, max_uses FROM partner_codes WHERE code=? AND active=1",
(code_upper,)
).fetchone()
if partner: