user-profiles: Cross-Channel-Identität mit Registrierung, Login, Profil + WhatsApp/Telegram-Flow

This commit is contained in:
Hermes Agent
2026-06-19 17:44:35 +02:00
parent ffff6d1fa3
commit 392845afb4
3 changed files with 365 additions and 9 deletions
+254 -6
View File
@@ -1120,6 +1120,61 @@ def whatsapp_webhook():
pass
continue
# ── Nutzer-Registrierung per WhatsApp ────────────────
pending_codes = getattr(app, '_pending_codes', {})
if msg_type == "text":
clean = content.strip().lower()
# "registrieren" → Code senden
if clean == "registrieren":
import random
code = str(random.randint(100000, 999999))
pending_codes[sender_id] = code
app._pending_codes = pending_codes
if META_TOKEN:
try:
send_whatsapp_message(sender_id,
f"🔐 Dein Verifikationscode: *{code}*\n\n"
f"Antworte einfach mit: code {code}")
except Exception:
pass
continue
# "code 123456" → verifizieren + User anlegen
if clean.startswith("code ") and len(clean) >= 10:
entered = clean[5:].strip()
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()
if not existing:
db.execute(
"INSERT INTO users (phone, name, verified) VALUES (?,?,1)",
(sender_id, sender_name),
)
db.commit()
if META_TOKEN:
try:
send_whatsapp_message(sender_id,
"✅ Du bist jetzt registriert! Dein Profil: "
"https://chat.datenhimmel.work/profile")
except Exception:
pass
else:
if META_TOKEN:
try:
send_whatsapp_message(sender_id,
"❌ Falscher Code. Schick \"registrieren\" für einen neuen.")
except Exception:
pass
continue
# ── Cross-Channel: User-Profil zuordnen ──────────────
user_row = db.execute(
"SELECT id, name FROM users WHERE phone=?", (sender_id,)
).fetchone()
# Check for existing thread from this sender (5-min window)
recent = db.execute(
"""SELECT id, content, media_path, halle FROM submissions
@@ -1186,9 +1241,10 @@ def whatsapp_webhook():
cat = category_override or None
db.execute(
"""INSERT INTO submissions
(channel, sender_id, sender_name, type, content, media_path, status, halle, category, spiel_titel)
VALUES (?, ?, ?, ?, ?, ?, 'new', ?, ?, ?)""",
("whatsapp", sender_id, sender_name, msg_type, content, media_path, halle, cat, spiel_titel),
(channel, sender_id, sender_name, type, content, media_path, status, halle, category, spiel_titel, user_id)
VALUES (?, ?, ?, ?, ?, ?, 'new', ?, ?, ?, ?)""",
("whatsapp", sender_id, sender_name, msg_type, content, media_path, halle, cat, spiel_titel,
user_row["id"] if user_row else None),
)
# Auto-mark matching neuheiten as sold out
if cat == "ausverkauft" and spiel_titel:
@@ -2296,6 +2352,194 @@ def submission_delete(sub_id: int):
</html>"""
# ═══════════════════════════════════════════════════════════════════
# Nutzerprofil-System (Cross-Channel Identity)
# ═══════════════════════════════════════════════════════════════════
import hashlib, secrets
def _hash_password(password: str) -> str:
"""SHA-256 mit Salt."""
salt = secrets.token_hex(16)
h = hashlib.sha256((salt + password).encode()).hexdigest()
return f"{salt}${h}"
def _check_password(password: str, stored: str) -> bool:
"""Vergleicht Passwort mit gespeichertem Hash."""
try:
salt, h = stored.split("$", 1)
return hashlib.sha256((salt + password).encode()).hexdigest() == h
except Exception:
return False
def _create_session(db, user_id: int, channel: str = "web") -> str:
"""Erstellt Session-Token (gültig 30 Tage)."""
token = secrets.token_urlsafe(32)
db.execute(
"INSERT INTO user_sessions (user_id, token, channel, expires_at) VALUES (?,?,?,datetime('now','+30 days'))",
(user_id, token, channel),
)
db.commit()
return token
def _get_user_from_session(db) -> dict | None:
"""Liest User aus Session-Cookie."""
token = request.cookies.get("bsn_session", "")
if not token:
return None
row = db.execute(
"SELECT u.* FROM users u JOIN user_sessions s ON u.id=s.user_id "
"WHERE s.token=? AND s.expires_at > datetime('now')",
(token,),
).fetchone()
return dict(row) if row else None
@app.route("/register", methods=["GET", "POST"])
def user_register():
db = get_db()
error = ""
if request.method == "POST":
phone = request.form.get("phone", "").strip()
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."
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)
resp = make_response(redirect("/profile"))
resp.set_cookie("bsn_session", token, max_age=30*86400, httponly=True)
return resp
return f"""<!DOCTYPE html>
<html lang="de"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Registrieren BSN Community</title>
<style>
body{{font-family:-apple-system,sans-serif;background:#1a1a2e;color:#eee;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0}}
.card{{background:#16213e;border-radius:16px;padding:2rem;width:100%;max-width:400px;box-shadow:0 8px 32px #00000040}}
h1{{color:#20228a;margin:0 0 1rem;font-size:1.4rem;text-align:center}}
input{{width:100%;padding:0.8rem;margin:0.4rem 0;border-radius:8px;border:1px solid #333;background:#0f3460;color:#fff;box-sizing:border-box;font-size:1rem}}
.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}}
.link{{text-align:center;margin-top:1rem;font-size:0.85rem}}
.link a{{color:#5e5ce6}}
</style></head><body>
<div class="card">
<h1>📋 Registrieren</h1>
{"<div class='error'>{}</div>".format(error) if error else ""}
<form method="post">
<input name="phone" placeholder="Telefon (z.B. +49152...)" value="{{}}" required>
<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>
</form>
<div class="link">Bereits registriert? <a href="/login">Einloggen</a></div>
</div></body></html>""".format(request.form.get("phone",""))
@app.route("/login", methods=["GET", "POST"])
def user_login():
db = get_db()
error = ""
if request.method == "POST":
phone = request.form.get("phone", "").strip()
password = request.form.get("password", "").strip()
user = db.execute("SELECT * FROM users WHERE phone=?", (phone,)).fetchone()
if not user or not _check_password(password, user["password_hash"] or ""):
error = "Telefon oder Passwort falsch."
else:
token = _create_session(db, user["id"])
resp = make_response(redirect("/profile"))
resp.set_cookie("bsn_session", token, max_age=30*86400, httponly=True)
return resp
return f"""<!DOCTYPE html>
<html lang="de"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login BSN Community</title>
<style>
body{{font-family:-apple-system,sans-serif;background:#1a1a2e;color:#eee;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0}}
.card{{background:#16213e;border-radius:16px;padding:2rem;width:100%;max-width:400px;box-shadow:0 8px 32px #00000040}}
h1{{color:#20228a;margin:0 0 1rem;font-size:1.4rem;text-align:center}}
input{{width:100%;padding:0.8rem;margin:0.4rem 0;border-radius:8px;border:1px solid #333;background:#0f3460;color:#fff;box-sizing:border-box;font-size:1rem}}
.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}}
.link{{text-align:center;margin-top:1rem;font-size:0.85rem}}
.link a{{color:#5e5ce6}}
</style></head><body>
<div class="card">
<h1>🔐 Login</h1>
{"<div class='error'>{}</div>".format(error) if error else ""}
<form method="post">
<input name="phone" placeholder="Telefon" value="{{}}" required>
<input name="password" type="password" placeholder="Passwort" required>
<button class="btn" type="submit">Einloggen</button>
</form>
<div class="link">Neu hier? <a href="/register">Registrieren</a></div>
</div></body></html>""".format(request.form.get("phone",""))
@app.route("/logout")
def user_logout():
token = request.cookies.get("bsn_session", "")
if token:
db = get_db()
db.execute("DELETE FROM user_sessions WHERE token=?", (token,))
db.commit()
resp = make_response(redirect("/"))
resp.set_cookie("bsn_session", "", max_age=0)
return resp
@app.route("/profile", methods=["GET", "POST"])
def user_profile():
db = get_db()
user = _get_user_from_session(db)
if not user:
return redirect("/login")
msg = ""
if request.method == "POST":
name = request.form.get("name", "").strip()
if name:
db.execute("UPDATE users SET name=? WHERE id=?", (name, user["id"]))
db.commit()
msg = "✅ Profil aktualisiert."
return f"""<!DOCTYPE html>
<html lang="de"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Profil BSN Community</title>
<style>
body{{font-family:-apple-system,sans-serif;background:#1a1a2e;color:#eee;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0}}
.card{{background:#16213e;border-radius:16px;padding:2rem;width:100%;max-width:400px;box-shadow:0 8px 32px #00000040}}
h1{{color:#20228a;margin:0 0 1rem;font-size:1.4rem}}
.field{{margin:0.8rem 0}}
.label{{color:#888;font-size:0.8rem;text-transform:uppercase}}
.value{{font-size:1.1rem}}
input{{width:100%;padding:0.6rem;margin:0.3rem 0;border-radius:6px;border:1px solid #333;background:#0f3460;color:#fff;box-sizing:border-box}}
.btn{{padding:0.6rem 1.2rem;background:#20228a;color:#fff;border:none;border-radius:6px;cursor:pointer}}
.btn:hover{{background:#2a2caa}}
.msg{{color:#30d158;margin:0.5rem 0}}
.nav{{margin-top:1rem;display:flex;gap:0.5rem}}
</style></head><body>
<div class="card">
<h1>👤 Mein Profil</h1>
{"<div class='msg'>{}</div>".format(msg) if msg else ""}
<div class="field"><div class="label">Telefon</div><div class="value">{user['phone']}</div></div>
<form method="post">
<div class="field"><div class="label">Name</div>
<input name="name" value="{user['name'] or ''}" placeholder="Dein Name"></div>
<button class="btn" type="submit">Speichern</button>
</form>
<div class="nav">
<a href="/" style="color:#5e5ce6"> Startseite</a>
<a href="/logout" style="color:#ff453a">Abmelden</a>
</div>
</div></body></html>"""
@app.route("/")
def public_frontend():
"""Public frontend showing published submissions — SPIEL Essen inspired."""
@@ -3268,6 +3512,9 @@ def public_frontend():
<ul class="offcanvas-nav-links">
<li class="nav-section">Navigation</li>
<li><a href="/">Startseite</a></li>
<li class="nav-section">Community</li>
<li><a href="/register">📋 Registrieren</a></li>
<li><a href="/login">🔐 Login</a></li>
<li class="nav-section">Rechtliches</li>
<li><a href="/impressum">Impressum</a></li>
<li><a href="/datenschutz">Datenschutzerklärung</a></li>
@@ -3982,9 +4229,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)
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),
(channel, sender_id, sender_name, type, content, media_path, status, halle, category, spiel_titel, email, phone, 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,
(_get_user_from_session(db) or {}).get('id')),
)
db.commit()
sub_id = db.execute("SELECT last_insert_rowid()").fetchone()[0]
+22
View File
@@ -39,6 +39,28 @@ CREATE INDEX IF NOT EXISTS idx_submissions_channel ON submissions(channel);
CREATE INDEX IF NOT EXISTS idx_submissions_category ON submissions(category);
CREATE INDEX IF NOT EXISTS idx_knowledge_key ON knowledge(key);
CREATE INDEX IF NOT EXISTS idx_knowledge_category ON knowledge(category);
-- Cross-channel user profiles (phone = universal ID)
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
phone TEXT UNIQUE NOT NULL,
name TEXT,
password_hash TEXT,
profile_pic TEXT,
verified INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS user_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
token TEXT UNIQUE NOT NULL,
channel TEXT DEFAULT 'web',
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_user_sessions_token ON user_sessions(token);
CREATE INDEX IF NOT EXISTS idx_users_phone ON users(phone);
"""
SAMPLE_SUBMISSIONS = [
+89 -3
View File
@@ -194,6 +194,91 @@ def telegram_webhook():
spiel_titel = None
category_override = None
# ── Nutzer-Registrierung per Telegram ──────────────────────────
pending_codes = getattr(app, '_tg_pending_codes', {})
if msg_type == "text":
clean = content.strip().lower()
if clean == "registrieren":
import random
code = str(random.randint(100000, 999999))
pending_codes[sender_id] = code
app._tg_pending_codes = pending_codes
if TELEGRAM_TOKEN:
import requests as _r
try:
_r.post(
"{}/sendMessage".format(TELEGRAM_API),
json={"chat_id": int(sender_id),
"text": "🔐 Dein Verifikationscode: *{}*\n\n"
"Antworte einfach mit: code {}".format(code, code)},
timeout=10,
)
except Exception:
pass
return "OK", 200
if clean.startswith("code ") and len(clean) >= 10:
entered = clean[5:].strip()
if sender_id in pending_codes and pending_codes[sender_id] == entered:
del pending_codes[sender_id]
app._tg_pending_codes = pending_codes
if TELEGRAM_TOKEN:
import requests as _r
try:
_r.post(
"{}/sendMessage".format(TELEGRAM_API),
json={"chat_id": int(sender_id),
"text": "✅ Du bist verifiziert! Welche Telefonnummer hast du? "
"Antworte einfach mit: phone +49152..."},
timeout=10,
)
except Exception:
pass
else:
if TELEGRAM_TOKEN:
import requests as _r
try:
_r.post(
"{}/sendMessage".format(TELEGRAM_API),
json={"chat_id": int(sender_id),
"text": "❌ Falscher Code. Schick \"registrieren\" für einen neuen."},
timeout=10,
)
except Exception:
pass
return "OK", 200
# "phone +49..." → Telefonnummer verknüpfen
if clean.startswith("phone ") or clean.startswith("+49"):
phone = clean[6:].strip() if clean.startswith("phone ") else clean.strip()
# Normalize: remove spaces
phone = phone.replace(" ", "").replace("-", "")
existing = db.execute("SELECT id FROM users WHERE phone=?", (phone,)).fetchone()
if existing:
db.execute("UPDATE users SET name=? WHERE phone=?",
(sender_name, phone))
else:
db.execute("INSERT INTO users (phone, name, verified) VALUES (?,?,1)",
(phone, sender_name))
db.commit()
if TELEGRAM_TOKEN:
import requests as _r
try:
_r.post(
"{}/sendMessage".format(TELEGRAM_API),
json={"chat_id": int(sender_id),
"text": "✅ Deine Telefonnummer ist jetzt verknüpft! "
"Dein Profil: https://chat.datenhimmel.work/profile"},
timeout=10,
)
except Exception:
pass
return "OK", 200
# ── Cross-Channel: User ueber Telegram-ID + phone verknuepfung ─
user_row = db.execute(
"SELECT id, name FROM users WHERE phone=?",
(sender_id,), # Telegram-ID als Fallback
).fetchone()
if "#out" in content.lower():
category_override = "ausverkauft"
idx = content.lower().find("#out")
@@ -256,9 +341,10 @@ def telegram_webhook():
cat = category_override or None
db.execute(
"""INSERT INTO submissions
(channel, sender_id, sender_name, type, content, media_path, status, halle, category, spiel_titel)
VALUES (?, ?, ?, ?, ?, ?, 'new', ?, ?, ?)""",
("telegram", sender_id, sender_name, msg_type, content, media_path, halle, cat, spiel_titel),
(channel, sender_id, sender_name, type, content, media_path, status, halle, category, spiel_titel, user_id)
VALUES (?, ?, ?, ?, ?, ?, 'new', ?, ?, ?, ?)""",
("telegram", sender_id, sender_name, msg_type, content, media_path, halle, cat, spiel_titel,
user_row["id"] if user_row else None),
)
if cat == "ausverkauft" and spiel_titel:
mark_ausverkauft_in_neuheiten(db, spiel_titel)