diff --git a/app.py b/app.py index e840035..77421d1 100644 --- a/app.py +++ b/app.py @@ -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): """ +# ═══════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════ # 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} @@ -2415,7 +2439,8 @@ input{width:100%;padding:0.8rem;margin:0.4rem 0;border-radius:8px;border:1px sol

📋 Registrieren

{error_html}
- + +
E-Mail oder Telefonnummer (+49...)
@@ -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} @@ -2441,7 +2467,8 @@ input{width:100%;padding:0.8rem;margin:0.4rem 0;border-radius:8px;border:1px sol

🔐 Login

{error_html} - + +
E-Mail oder Telefonnummer
@@ -2467,7 +2494,8 @@ input{width:100%;padding:0.6rem;margin:0.3rem 0;border-radius:6px;border:1px sol

👤 Mein Profil

{msg_html} -
Telefon
{phone}
+
E-Mail
{email}
+
Telefon
{phone_display}
Name
@@ -2485,29 +2513,51 @@ 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}", + "
" + error + "
" 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}", + "
" + error + "
") else: - pw_hash = _hash_password(password) - db.execute( - "INSERT INTO users (phone, name, password_hash, verified) VALUES (?,?,?,1)", - (phone, name, pw_hash), - ) - db.commit() - user_id = db.execute("SELECT last_insert_rowid()").fetchone()[0] - 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", "") - error_html = "
" + error + "
" if error else "" - return _REGISTER_TPL.replace("{phone_val}", phone_val).replace("{error_html}", error_html) + 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}", + "
" + error + "
") + # 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, 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] + 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 + 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 = "
" + error + "
" 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 = "
" + msg + "
" 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()