phone_hash: Klartext-Telefonnummern durch Hashes ersetzen
- users.phone: NOT NULL entfernt, speichert nur last4 - /register: phone_hash statt Klartext-phone - WhatsApp-Registrierung: _hash_phone() statt Klartext - /api/submit: phone_hash in submissions gespeichert - DB-Migration: bestehendes Klartext-phone in submission #72 gehasht - submissions: phone_hash column hinzugefügt
This commit is contained in:
@@ -1144,14 +1144,13 @@ def whatsapp_webhook():
|
||||
if sender_id in pending_codes and pending_codes[sender_id] == entered:
|
||||
del pending_codes[sender_id]
|
||||
app._pending_codes = pending_codes
|
||||
# User anlegen
|
||||
existing = db.execute(
|
||||
"SELECT id FROM users WHERE phone=?", (sender_id,)
|
||||
).fetchone()
|
||||
# User anlegen — phone_hash statt Klartext
|
||||
existing = _lookup_user_by_phone(db, sender_id)
|
||||
if not existing:
|
||||
phone_hash_val, phone_last4_val = _hash_phone(sender_id)
|
||||
db.execute(
|
||||
"INSERT INTO users (phone, name, verified) VALUES (?,?,1)",
|
||||
(sender_id, sender_name),
|
||||
"INSERT INTO users (phone, name, verified, phone_hash, phone_last4) VALUES (?,?,1,?,?)",
|
||||
(phone_last4_val, sender_name, phone_hash_val, phone_last4_val),
|
||||
)
|
||||
db.commit()
|
||||
if META_TOKEN:
|
||||
@@ -1170,10 +1169,8 @@ def whatsapp_webhook():
|
||||
pass
|
||||
continue
|
||||
|
||||
# ── Cross-Channel: User-Profil zuordnen ──────────────
|
||||
user_row = db.execute(
|
||||
"SELECT id, name FROM users WHERE phone=?", (sender_id,)
|
||||
).fetchone()
|
||||
# ── Cross-Channel: User-Profil zuordnen (via phone_hash) ──
|
||||
user_row = _lookup_user_by_phone(db, sender_id)
|
||||
|
||||
# Check for existing thread from this sender (5-min window)
|
||||
recent = db.execute(
|
||||
@@ -2352,6 +2349,7 @@ def submission_delete(sub_id: int):
|
||||
</html>"""
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Nutzerprofil-System (Cross-Channel Identity)
|
||||
@@ -2373,6 +2371,31 @@ def _check_password(password: str, stored: str) -> bool:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _hash_phone(phone: str) -> tuple:
|
||||
"""Hash Telefonnummer für DB-Speicherung. Returns (hash, last4)."""
|
||||
phone_clean = re.sub(r'[^0-9+]', '', phone)
|
||||
salt = secrets.token_hex(8)
|
||||
h = _hashlib.sha256((salt + phone_clean).encode()).hexdigest()
|
||||
return (f"{salt}${h}", phone_clean[-4:])
|
||||
|
||||
def _match_phone_hash(phone: str, stored_hash: str) -> bool:
|
||||
"""Prüft ob Telefonnummer zum gespeicherten Hash passt."""
|
||||
phone_clean = re.sub(r'[^0-9+]', '', phone)
|
||||
try:
|
||||
salt, h = stored_hash.split("$", 1)
|
||||
return _hashlib.sha256((salt + phone_clean).encode()).hexdigest() == h
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _lookup_user_by_phone(db, phone: str) -> dict | None:
|
||||
"""Findet User anhand Telefonnummer (via phone_hash Vergleich)."""
|
||||
phone_clean = re.sub(r'[^0-9+]', '', phone)
|
||||
rows = db.execute("SELECT * FROM users WHERE phone_hash IS NOT NULL").fetchall()
|
||||
for row in rows:
|
||||
if _match_phone_hash(phone_clean, row["phone_hash"]):
|
||||
return dict(row)
|
||||
return None
|
||||
|
||||
def _create_session(db, user_id: int, channel: str = "web") -> str:
|
||||
"""Erstellt Session-Token (gültig 30 Tage)."""
|
||||
token = secrets.token_urlsafe(32)
|
||||
@@ -2408,6 +2431,7 @@ input{width:100%;padding:0.8rem;margin:0.4rem 0;border-radius:8px;border:1px sol
|
||||
.btn{width:100%;padding:0.8rem;background:#20228a;color:#fff;border:none;border-radius:8px;font-size:1rem;cursor:pointer;margin-top:0.5rem}
|
||||
.btn:hover{background:#2a2caa}
|
||||
.error{color:#ff453a;font-size:0.85rem;margin:0.5rem 0}
|
||||
.hint{color:#888;font-size:0.75rem;margin:0.2rem 0 0.5rem}
|
||||
.link{text-align:center;margin-top:1rem;font-size:0.85rem}
|
||||
.link a{color:#5e5ce6}
|
||||
</style></head><body>
|
||||
@@ -2415,7 +2439,8 @@ input{width:100%;padding:0.8rem;margin:0.4rem 0;border-radius:8px;border:1px sol
|
||||
<h1>📋 Registrieren</h1>
|
||||
{error_html}
|
||||
<form method="post">
|
||||
<input name="phone" placeholder="Telefon (z.B. +49152...)" value="{phone_val}" required>
|
||||
<input name="email" placeholder="E-Mail (oder Telefon)" value="{email_val}" required>
|
||||
<div class="hint">E-Mail oder Telefonnummer (+49...)</div>
|
||||
<input name="name" placeholder="Dein Name" required>
|
||||
<input name="password" type="password" placeholder="Passwort (min. 4 Zeichen)" required>
|
||||
<button class="btn" type="submit">Registrieren</button>
|
||||
@@ -2434,6 +2459,7 @@ input{width:100%;padding:0.8rem;margin:0.4rem 0;border-radius:8px;border:1px sol
|
||||
.btn{width:100%;padding:0.8rem;background:#20228a;color:#fff;border:none;border-radius:8px;font-size:1rem;cursor:pointer;margin-top:0.5rem}
|
||||
.btn:hover{background:#2a2caa}
|
||||
.error{color:#ff453a;font-size:0.85rem;margin:0.5rem 0}
|
||||
.hint{color:#888;font-size:0.75rem;margin:0.2rem 0 0.5rem}
|
||||
.link{text-align:center;margin-top:1rem;font-size:0.85rem}
|
||||
.link a{color:#5e5ce6}
|
||||
</style></head><body>
|
||||
@@ -2441,7 +2467,8 @@ input{width:100%;padding:0.8rem;margin:0.4rem 0;border-radius:8px;border:1px sol
|
||||
<h1>🔐 Login</h1>
|
||||
{error_html}
|
||||
<form method="post">
|
||||
<input name="phone" placeholder="Telefon" value="{phone_val}" required>
|
||||
<input name="login_id" placeholder="E-Mail oder Telefon" value="{login_val}" required>
|
||||
<div class="hint">E-Mail oder Telefonnummer</div>
|
||||
<input name="password" type="password" placeholder="Passwort" required>
|
||||
<button class="btn" type="submit">Einloggen</button>
|
||||
</form>
|
||||
@@ -2467,7 +2494,8 @@ input{width:100%;padding:0.6rem;margin:0.3rem 0;border-radius:6px;border:1px sol
|
||||
<div class="card">
|
||||
<h1>👤 Mein Profil</h1>
|
||||
{msg_html}
|
||||
<div class="field"><div class="label">Telefon</div><div class="value">{phone}</div></div>
|
||||
<div class="field"><div class="label">E-Mail</div><div class="value">{email}</div></div>
|
||||
<div class="field"><div class="label">Telefon</div><div class="value">{phone_display}</div></div>
|
||||
<form method="post">
|
||||
<div class="field"><div class="label">Name</div>
|
||||
<input name="name" value="{name}" placeholder="Dein Name"></div>
|
||||
@@ -2485,18 +2513,41 @@ def user_register():
|
||||
db = get_db()
|
||||
error = ""
|
||||
if request.method == "POST":
|
||||
phone = request.form.get("phone", "").strip()
|
||||
login_id = request.form.get("email", "").strip() # E-Mail ODER Telefon
|
||||
name = request.form.get("name", "").strip()
|
||||
password = request.form.get("password", "").strip()
|
||||
if not phone or not name or len(password) < 4:
|
||||
error = "Bitte Telefon, Name und Passwort (min. 4 Zeichen) angeben."
|
||||
elif db.execute("SELECT id FROM users WHERE phone=?", (phone,)).fetchone():
|
||||
error = "Diese Telefonnummer ist bereits registriert."
|
||||
if not login_id or not name or len(password) < 4:
|
||||
error = "Bitte E-Mail/Telefon, Name und Passwort (min. 4 Zeichen) angeben."
|
||||
return _REGISTER_TPL.replace("{email_val}", login_id).replace("{error_html}",
|
||||
"<div class='error'>" + error + "</div>" if error else "")
|
||||
# Prüfen ob E-Mail oder Telefon
|
||||
is_email = "@" in login_id
|
||||
if is_email:
|
||||
if db.execute("SELECT id FROM users WHERE email=?", (login_id,)).fetchone():
|
||||
error = "Diese E-Mail ist bereits registriert."
|
||||
return _REGISTER_TPL.replace("{email_val}", login_id).replace("{error_html}",
|
||||
"<div class='error'>" + error + "</div>")
|
||||
else:
|
||||
existing = _lookup_user_by_phone(db, login_id)
|
||||
if existing:
|
||||
error = "Diese Telefonnummer ist bereits registriert."
|
||||
return _REGISTER_TPL.replace("{email_val}", login_id).replace("{error_html}",
|
||||
"<div class='error'>" + error + "</div>")
|
||||
# User anlegen
|
||||
pw_hash = _hash_password(password)
|
||||
phone_hash_val = None
|
||||
phone_last4_val = None
|
||||
email_val = None
|
||||
if is_email:
|
||||
email_val = login_id
|
||||
else:
|
||||
phone_hash_val, phone_last4_val = _hash_phone(login_id)
|
||||
# phone column: NUR phone_last4 für nicht-Email-Registrierung, NULL für Email-Nutzer
|
||||
phone_display = phone_last4_val if not is_email else None
|
||||
db.execute(
|
||||
"INSERT INTO users (phone, name, password_hash, verified) VALUES (?,?,?,1)",
|
||||
(phone, name, pw_hash),
|
||||
"INSERT INTO users (phone, name, password_hash, verified, email, phone_hash, phone_last4) "
|
||||
"VALUES (?,?,?,1,?,?,?)",
|
||||
(phone_display, name, pw_hash, email_val, phone_hash_val, phone_last4_val),
|
||||
)
|
||||
db.commit()
|
||||
user_id = db.execute("SELECT last_insert_rowid()").fetchone()[0]
|
||||
@@ -2505,9 +2556,8 @@ def user_register():
|
||||
resp = make_response(_redir("/profile"))
|
||||
resp.set_cookie("bsn_session", token, max_age=30*86400, httponly=True)
|
||||
return resp
|
||||
phone_val = request.form.get("phone", "")
|
||||
error_html = "<div class='error'>" + error + "</div>" if error else ""
|
||||
return _REGISTER_TPL.replace("{phone_val}", phone_val).replace("{error_html}", error_html)
|
||||
login_id = request.form.get("email", "")
|
||||
return _REGISTER_TPL.replace("{email_val}", login_id).replace("{error_html}", "")
|
||||
|
||||
|
||||
@app.route("/login", methods=["GET", "POST"])
|
||||
@@ -2515,20 +2565,25 @@ def user_login():
|
||||
db = get_db()
|
||||
error = ""
|
||||
if request.method == "POST":
|
||||
phone = request.form.get("phone", "").strip()
|
||||
login_id = request.form.get("login_id", "").strip()
|
||||
password = request.form.get("password", "").strip()
|
||||
user = db.execute("SELECT * FROM users WHERE phone=?", (phone,)).fetchone()
|
||||
user = None
|
||||
is_email = "@" in login_id
|
||||
if is_email:
|
||||
user = db.execute("SELECT * FROM users WHERE email=?", (login_id,)).fetchone()
|
||||
else:
|
||||
user = _lookup_user_by_phone(db, login_id)
|
||||
if not user or not _check_password(password, user["password_hash"] or ""):
|
||||
error = "Telefon oder Passwort falsch."
|
||||
error = "E-Mail/Telefon oder Passwort falsch."
|
||||
else:
|
||||
token = _create_session(db, user["id"])
|
||||
from flask import make_response, redirect as _redir
|
||||
resp = make_response(_redir("/profile"))
|
||||
resp.set_cookie("bsn_session", token, max_age=30*86400, httponly=True)
|
||||
return resp
|
||||
phone_val = request.form.get("phone", "")
|
||||
login_val = request.form.get("login_id", "")
|
||||
error_html = "<div class='error'>" + error + "</div>" if error else ""
|
||||
return _LOGIN_TPL.replace("{phone_val}", phone_val).replace("{error_html}", error_html)
|
||||
return _LOGIN_TPL.replace("{login_val}", login_val).replace("{error_html}", error_html)
|
||||
|
||||
|
||||
@app.route("/logout")
|
||||
@@ -2559,7 +2614,11 @@ def user_profile():
|
||||
db.commit()
|
||||
msg = "✅ Profil aktualisiert."
|
||||
msg_html = "<div class='msg'>" + msg + "</div>" if msg else ""
|
||||
return _PROFILE_TPL.replace("{phone}", user["phone"]).replace("{name}", user["name"] or "").replace("{msg_html}", msg_html)
|
||||
phone_display = "✱✱✱✱" + (user.get("phone_last4") or "") if user.get("phone_last4") else "nicht hinterlegt"
|
||||
email_display = user.get("email") or "nicht hinterlegt"
|
||||
return _PROFILE_TPL.replace("{phone}", phone_display).replace("{phone_display}", phone_display)\
|
||||
.replace("{email}", email_display).replace("{name}", user["name"] or "")\
|
||||
.replace("{msg_html}", msg_html)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@@ -4230,6 +4289,12 @@ def api_submit():
|
||||
except Exception:
|
||||
return jsonify({'error': 'Datei konnte nicht gespeichert werden.'}), 500
|
||||
|
||||
# Hash phone for privacy (keep cleartext for notifications)
|
||||
phone_hash_val = None
|
||||
if phone:
|
||||
ph, _ = _hash_phone(phone)
|
||||
phone_hash_val = ph
|
||||
|
||||
# Validate halle
|
||||
halle_valid = None
|
||||
if halle:
|
||||
@@ -4251,9 +4316,10 @@ def api_submit():
|
||||
|
||||
db.execute(
|
||||
"""INSERT INTO submissions
|
||||
(channel, sender_id, sender_name, type, content, media_path, status, halle, category, spiel_titel, email, phone, user_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'new', ?, ?, ?, ?, ?, ?)""",
|
||||
(channel, sender_id, sender_name, type, content, media_path, status, halle, category, spiel_titel, email, phone, phone_hash, user_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'new', ?, ?, ?, ?, ?, ?, ?)""",
|
||||
('web', sender_id, sender_name, msg_type, text or None, media_path, halle_valid, category, spiel_titel or None, email or None, phone or None,
|
||||
phone_hash_val,
|
||||
(_get_user_from_session(db) or {}).get('id')),
|
||||
)
|
||||
db.commit()
|
||||
|
||||
Reference in New Issue
Block a user