diff --git a/VERSION b/VERSION
index 0f7dc0e..3420149 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1264
\ No newline at end of file
+1265
\ No newline at end of file
diff --git a/backend/database.py b/backend/database.py
index 1169b5d..dba33c8 100644
--- a/backend/database.py
+++ b/backend/database.py
@@ -627,6 +627,8 @@ def _migrate(conn_factory):
("users", "referred_qr", "TEXT"),
# Partner-Code → Besitzer (für Self-Service: eigene QR-Kontingente + Stats einsehen)
("partner_codes", "owner_user_id", "INTEGER"),
+ # Notbremse für geleakte Codes: 0 = pausiert (Einlösung gesperrt, Historie bleibt)
+ ("partner_codes", "active", "INTEGER NOT NULL DEFAULT 1"),
# Passwort-Zurücksetzen
("users", "password_reset_token", "TEXT"),
("users", "password_reset_expires", "TEXT"),
diff --git a/backend/main.py b/backend/main.py
index 309e184..4f35a98 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -2165,12 +2165,7 @@ async def partner_qr_scan(token: str):
token = token.strip()
with _db() as conn:
row = conn.execute(
- """SELECT q.token, 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 = ?""",
- (token,)
+ "SELECT token FROM partner_qr_codes WHERE token = ?", (token,)
).fetchone()
if not row:
return _Redirect("/", status_code=302)
@@ -2182,8 +2177,10 @@ async def partner_qr_scan(token: str):
WHERE token = ?""",
(token,)
)
- # ?ref= nutzt den bestehenden Partner-Code-Flow, ?qr= ergänzt die Einzelcode-Zuordnung
- return _Redirect(f"/?ref={row['code']}&qr={row['token']}", status_code=302)
+ # Bewusst NUR der Token in der URL — der tippbare Partner-Code bleibt verborgen
+ # (sonst könnte jeder Sticker-Scanner den Code ablesen und beliebig weitergeben).
+ # Die Registrierung löst den Code server-seitig aus dem Token auf.
+ return _Redirect(f"/?qr={row['token']}", status_code=302)
# ------------------------------------------------------------------
diff --git a/backend/routes/auth.py b/backend/routes/auth.py
index db5683d..403eae8 100644
--- a/backend/routes/auth.py
+++ b/backend/routes/auth.py
@@ -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:
diff --git a/backend/routes/partner.py b/backend/routes/partner.py
index f7a61df..115517b 100644
--- a/backend/routes/partner.py
+++ b/backend/routes/partner.py
@@ -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:
diff --git a/backend/static/index.html b/backend/static/index.html
index e462ddc..4d30aec 100644
--- a/backend/static/index.html
+++ b/backend/static/index.html
@@ -86,14 +86,14 @@
Ban Yaro
-
+
-
-
-
-
-
+
+
+
+
+
@@ -616,11 +616,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -630,7 +630,7 @@
-
+
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index 9ddb2b9..1903e4a 100644
--- a/backend/static/js/app.js
+++ b/backend/static/js/app.js
@@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
-const APP_VER = '1264'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '1265'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
window.APP_VERSION = APP_VERSION;
@@ -1142,11 +1142,14 @@ const App = (() => {
const urlParams = new URLSearchParams(window.location.search);
const refCode = urlParams.get('ref');
const qrToken = urlParams.get('qr');
- if (refCode) {
+ if (refCode || qrToken) {
try {
- localStorage.setItem('by_ref_code', refCode.toUpperCase());
- localStorage.setItem('by_ref_code_ts', String(Date.now()));
- // Partner-QR-Token (Sticker/Flyer) für Einzelcode-Rückverfolgung mitspeichern
+ if (refCode) {
+ localStorage.setItem('by_ref_code', refCode.toUpperCase());
+ localStorage.setItem('by_ref_code_ts', String(Date.now()));
+ }
+ // Partner-QR-Token (Sticker/Flyer): kommt bewusst OHNE Klartext-Code —
+ // die Registrierung löst den Partner-Code server-seitig aus dem Token auf
if (qrToken) localStorage.setItem('by_qr_token', qrToken);
} catch {}
// URL bereinigen ohne Reload
diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js
index c2f5760..b07b51f 100644
--- a/backend/static/js/pages/admin.js
+++ b/backend/static/js/pages/admin.js
@@ -2326,8 +2326,8 @@ window.Page_admin = (() => {
-
-
+
+
{
${codes.map(c => `
-
+
${c.code}
+ ${c.active ? '' : `⏸ pausiert `}
|
${c.label}
@@ -2379,7 +2380,12 @@ window.Page_admin = (() => {
|
${c.grants_founder ? '✓' : '—'}
|
-
+ |
+
|