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:
|
if sender_id in pending_codes and pending_codes[sender_id] == entered:
|
||||||
del pending_codes[sender_id]
|
del pending_codes[sender_id]
|
||||||
app._pending_codes = pending_codes
|
app._pending_codes = pending_codes
|
||||||
# User anlegen
|
# User anlegen — phone_hash statt Klartext
|
||||||
existing = db.execute(
|
existing = _lookup_user_by_phone(db, sender_id)
|
||||||
"SELECT id FROM users WHERE phone=?", (sender_id,)
|
|
||||||
).fetchone()
|
|
||||||
if not existing:
|
if not existing:
|
||||||
|
phone_hash_val, phone_last4_val = _hash_phone(sender_id)
|
||||||
db.execute(
|
db.execute(
|
||||||
"INSERT INTO users (phone, name, verified) VALUES (?,?,1)",
|
"INSERT INTO users (phone, name, verified, phone_hash, phone_last4) VALUES (?,?,1,?,?)",
|
||||||
(sender_id, sender_name),
|
(phone_last4_val, sender_name, phone_hash_val, phone_last4_val),
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
if META_TOKEN:
|
if META_TOKEN:
|
||||||
@@ -1170,10 +1169,8 @@ def whatsapp_webhook():
|
|||||||
pass
|
pass
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# ── Cross-Channel: User-Profil zuordnen ──────────────
|
# ── Cross-Channel: User-Profil zuordnen (via phone_hash) ──
|
||||||
user_row = db.execute(
|
user_row = _lookup_user_by_phone(db, sender_id)
|
||||||
"SELECT id, name FROM users WHERE phone=?", (sender_id,)
|
|
||||||
).fetchone()
|
|
||||||
|
|
||||||
# Check for existing thread from this sender (5-min window)
|
# Check for existing thread from this sender (5-min window)
|
||||||
recent = db.execute(
|
recent = db.execute(
|
||||||
@@ -2352,6 +2349,7 @@ def submission_delete(sub_id: int):
|
|||||||
</html>"""
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
# Nutzerprofil-System (Cross-Channel Identity)
|
# Nutzerprofil-System (Cross-Channel Identity)
|
||||||
@@ -2373,6 +2371,31 @@ def _check_password(password: str, stored: str) -> bool:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
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:
|
def _create_session(db, user_id: int, channel: str = "web") -> str:
|
||||||
"""Erstellt Session-Token (gültig 30 Tage)."""
|
"""Erstellt Session-Token (gültig 30 Tage)."""
|
||||||
token = secrets.token_urlsafe(32)
|
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{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}
|
.btn:hover{background:#2a2caa}
|
||||||
.error{color:#ff453a;font-size:0.85rem;margin:0.5rem 0}
|
.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{text-align:center;margin-top:1rem;font-size:0.85rem}
|
||||||
.link a{color:#5e5ce6}
|
.link a{color:#5e5ce6}
|
||||||
</style></head><body>
|
</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>
|
<h1>📋 Registrieren</h1>
|
||||||
{error_html}
|
{error_html}
|
||||||
<form method="post">
|
<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="name" placeholder="Dein Name" required>
|
||||||
<input name="password" type="password" placeholder="Passwort (min. 4 Zeichen)" required>
|
<input name="password" type="password" placeholder="Passwort (min. 4 Zeichen)" required>
|
||||||
<button class="btn" type="submit">Registrieren</button>
|
<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{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}
|
.btn:hover{background:#2a2caa}
|
||||||
.error{color:#ff453a;font-size:0.85rem;margin:0.5rem 0}
|
.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{text-align:center;margin-top:1rem;font-size:0.85rem}
|
||||||
.link a{color:#5e5ce6}
|
.link a{color:#5e5ce6}
|
||||||
</style></head><body>
|
</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>
|
<h1>🔐 Login</h1>
|
||||||
{error_html}
|
{error_html}
|
||||||
<form method="post">
|
<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>
|
<input name="password" type="password" placeholder="Passwort" required>
|
||||||
<button class="btn" type="submit">Einloggen</button>
|
<button class="btn" type="submit">Einloggen</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -2467,7 +2494,8 @@ input{width:100%;padding:0.6rem;margin:0.3rem 0;border-radius:6px;border:1px sol
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h1>👤 Mein Profil</h1>
|
<h1>👤 Mein Profil</h1>
|
||||||
{msg_html}
|
{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">
|
<form method="post">
|
||||||
<div class="field"><div class="label">Name</div>
|
<div class="field"><div class="label">Name</div>
|
||||||
<input name="name" value="{name}" placeholder="Dein Name"></div>
|
<input name="name" value="{name}" placeholder="Dein Name"></div>
|
||||||
@@ -2485,18 +2513,41 @@ def user_register():
|
|||||||
db = get_db()
|
db = get_db()
|
||||||
error = ""
|
error = ""
|
||||||
if request.method == "POST":
|
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()
|
name = request.form.get("name", "").strip()
|
||||||
password = request.form.get("password", "").strip()
|
password = request.form.get("password", "").strip()
|
||||||
if not phone or not name or len(password) < 4:
|
if not login_id or not name or len(password) < 4:
|
||||||
error = "Bitte Telefon, Name und Passwort (min. 4 Zeichen) angeben."
|
error = "Bitte E-Mail/Telefon, Name und Passwort (min. 4 Zeichen) angeben."
|
||||||
elif db.execute("SELECT id FROM users WHERE phone=?", (phone,)).fetchone():
|
return _REGISTER_TPL.replace("{email_val}", login_id).replace("{error_html}",
|
||||||
error = "Diese Telefonnummer ist bereits registriert."
|
"<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:
|
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)
|
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(
|
db.execute(
|
||||||
"INSERT INTO users (phone, name, password_hash, verified) VALUES (?,?,?,1)",
|
"INSERT INTO users (phone, name, password_hash, verified, email, phone_hash, phone_last4) "
|
||||||
(phone, name, pw_hash),
|
"VALUES (?,?,?,1,?,?,?)",
|
||||||
|
(phone_display, name, pw_hash, email_val, phone_hash_val, phone_last4_val),
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
user_id = db.execute("SELECT last_insert_rowid()").fetchone()[0]
|
user_id = db.execute("SELECT last_insert_rowid()").fetchone()[0]
|
||||||
@@ -2505,9 +2556,8 @@ def user_register():
|
|||||||
resp = make_response(_redir("/profile"))
|
resp = make_response(_redir("/profile"))
|
||||||
resp.set_cookie("bsn_session", token, max_age=30*86400, httponly=True)
|
resp.set_cookie("bsn_session", token, max_age=30*86400, httponly=True)
|
||||||
return resp
|
return resp
|
||||||
phone_val = request.form.get("phone", "")
|
login_id = request.form.get("email", "")
|
||||||
error_html = "<div class='error'>" + error + "</div>" if error else ""
|
return _REGISTER_TPL.replace("{email_val}", login_id).replace("{error_html}", "")
|
||||||
return _REGISTER_TPL.replace("{phone_val}", phone_val).replace("{error_html}", error_html)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/login", methods=["GET", "POST"])
|
@app.route("/login", methods=["GET", "POST"])
|
||||||
@@ -2515,20 +2565,25 @@ def user_login():
|
|||||||
db = get_db()
|
db = get_db()
|
||||||
error = ""
|
error = ""
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
phone = request.form.get("phone", "").strip()
|
login_id = request.form.get("login_id", "").strip()
|
||||||
password = request.form.get("password", "").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 ""):
|
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:
|
else:
|
||||||
token = _create_session(db, user["id"])
|
token = _create_session(db, user["id"])
|
||||||
from flask import make_response, redirect as _redir
|
from flask import make_response, redirect as _redir
|
||||||
resp = make_response(_redir("/profile"))
|
resp = make_response(_redir("/profile"))
|
||||||
resp.set_cookie("bsn_session", token, max_age=30*86400, httponly=True)
|
resp.set_cookie("bsn_session", token, max_age=30*86400, httponly=True)
|
||||||
return resp
|
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 ""
|
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")
|
@app.route("/logout")
|
||||||
@@ -2559,7 +2614,11 @@ def user_profile():
|
|||||||
db.commit()
|
db.commit()
|
||||||
msg = "✅ Profil aktualisiert."
|
msg = "✅ Profil aktualisiert."
|
||||||
msg_html = "<div class='msg'>" + msg + "</div>" if msg else ""
|
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("/")
|
@app.route("/")
|
||||||
@@ -4230,6 +4289,12 @@ def api_submit():
|
|||||||
except Exception:
|
except Exception:
|
||||||
return jsonify({'error': 'Datei konnte nicht gespeichert werden.'}), 500
|
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
|
# Validate halle
|
||||||
halle_valid = None
|
halle_valid = None
|
||||||
if halle:
|
if halle:
|
||||||
@@ -4251,9 +4316,10 @@ def api_submit():
|
|||||||
|
|
||||||
db.execute(
|
db.execute(
|
||||||
"""INSERT INTO submissions
|
"""INSERT INTO submissions
|
||||||
(channel, sender_id, sender_name, type, content, media_path, status, halle, category, spiel_titel, email, phone, user_id)
|
(channel, sender_id, sender_name, type, content, media_path, status, halle, category, spiel_titel, email, phone, phone_hash, user_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, 'new', ?, ?, ?, ?, ?, ?)""",
|
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,
|
('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')),
|
(_get_user_from_session(db) or {}).get('id')),
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
Reference in New Issue
Block a user