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:
parent
21bcc6b962
commit
2927ae2672
11 changed files with 136 additions and 39 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ def list_partner_codes(user=Depends(require_admin)):
|
|||
with db() as conn:
|
||||
rows = conn.execute(
|
||||
"""SELECT pc.id, pc.code, pc.label, pc.grants_founder,
|
||||
pc.max_uses, pc.uses, pc.created_at, pc.owner_user_id,
|
||||
pc.max_uses, pc.uses, pc.created_at, pc.owner_user_id, pc.active,
|
||||
u.name AS created_by_name,
|
||||
o.name AS owner_name
|
||||
FROM partner_codes pc
|
||||
|
|
@ -48,6 +48,21 @@ def list_partner_codes(user=Depends(require_admin)):
|
|||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
@router.post("/admin/partner/codes/{code_id}/toggle")
|
||||
def toggle_partner_code(code_id: int, user=Depends(require_admin)):
|
||||
"""Notbremse: Code pausieren/reaktivieren (z. B. wenn er im Internet kursiert).
|
||||
Pausiert = Einlösung gesperrt, Stats und QR-Kontingente bleiben erhalten."""
|
||||
with db() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT active FROM partner_codes WHERE id=?", (code_id,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Partner-Code nicht gefunden.")
|
||||
new_state = 0 if row["active"] else 1
|
||||
conn.execute("UPDATE partner_codes SET active=? WHERE id=?", (new_state, code_id))
|
||||
return {"active": new_state}
|
||||
|
||||
|
||||
@router.post("/admin/partner/codes", status_code=201)
|
||||
def create_partner_code(data: PartnerCodeCreate, user=Depends(require_admin)):
|
||||
"""Neuen Partner-Code erstellen (admin only)."""
|
||||
|
|
@ -197,7 +212,7 @@ def partner_code_info(code: str):
|
|||
with db() as conn:
|
||||
row = conn.execute(
|
||||
"""SELECT code, label, grants_founder, max_uses, uses
|
||||
FROM partner_codes WHERE code=?""",
|
||||
FROM partner_codes WHERE code=? AND active=1""",
|
||||
(code.strip().upper(),)
|
||||
).fetchone()
|
||||
if not row:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue