Viel Push und achivements + AGB

This commit is contained in:
2025-09-16 23:41:34 +02:00
parent b2fc63e2d0
commit 5831d1bb91
8 changed files with 934 additions and 49 deletions

187
pentest/enumerate.py Normal file
View File

@@ -0,0 +1,187 @@
import requests
import uuid
import time
import json
from datetime import datetime
def enumerate_supabase_users():
base_url = "http://localhost:3000/api/v1/public/user-player"
found_users = []
total_requests = 0
print("🔍 STARTE USER ENUMERATION ÜBER SUPABASE USER IDS")
print("=" * 60)
print(f"Zeit: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Target: {base_url}")
print("=" * 60)
# Teste verschiedene UUID-Patterns
test_uuids = [
str(uuid.uuid4()) for _ in range(1000) # Zufällige UUIDs
]
print(f"📊 Teste {len(test_uuids)} UUIDs...")
print("-" * 60)
for i, uuid_str in enumerate(test_uuids, 1):
try:
response = requests.get(f"{base_url}/{uuid_str}", timeout=5)
total_requests += 1
if response.status_code == 200:
user_data = response.json()
if user_data.get("success"):
found_users.append(user_data["data"])
user = user_data["data"]
print(f"✅ [{i:4d}] USER GEFUNDEN!")
print(f" UUID: {uuid_str}")
print(f" Name: {user['firstname']} {user['lastname']}")
print(f" ID: {user['id']}")
print(f" RFID: {user['rfiduid']}")
print(f" Geburtsdatum: {user['birthdate']}")
print(f" Leaderboard: {user['show_in_leaderboard']}")
print("-" * 60)
else:
if i % 100 == 0: # Fortschritt alle 100 Requests
print(f"⏳ [{i:4d}] Kein User gefunden (Fortschritt: {i}/{len(test_uuids)})")
else:
if i % 100 == 0:
print(f"❌ [{i:4d}] HTTP {response.status_code} (Fortschritt: {i}/{len(test_uuids)})")
except requests.exceptions.RequestException as e:
print(f"🔥 [{i:4d}] Fehler bei UUID {uuid_str}: {e}")
continue
print("\n" + "=" * 60)
print("📈 ENUMERATION ABGESCHLOSSEN")
print("=" * 60)
print(f"Total Requests: {total_requests}")
print(f"Gefundene Users: {len(found_users)}")
print(f"Erfolgsrate: {(len(found_users)/total_requests*100):.2f}%" if total_requests > 0 else "0%")
if found_users:
print("\n🎯 GEFUNDENE USERS:")
print("-" * 60)
for i, user in enumerate(found_users, 1):
print(f"{i}. {user['firstname']} {user['lastname']}")
print(f" ID: {user['id']} | RFID: {user['rfiduid']} | Geburtstag: {user['birthdate']}")
print("-" * 60)
# Speichere Ergebnisse in Datei
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"enumerated_users_{timestamp}.json"
with open(filename, 'w', encoding='utf-8') as f:
json.dump(found_users, f, indent=2, ensure_ascii=False)
print(f"💾 Ergebnisse gespeichert in: {filename}")
else:
print("\n❌ Keine Users gefunden")
print(f"\n⏰ Abgeschlossen um: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
return found_users
def enumerate_rfid_uids(api_key, max_attempts=100):
"""RFID UID Enumeration (benötigt gültigen API-Key)"""
base_url = "http://localhost:3000/api/v1/private/users/find"
found_rfids = []
print("\n🔍 STARTE RFID UID ENUMERATION")
print("=" * 60)
print(f"Zeit: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Target: {base_url}")
print(f"API-Key: {api_key[:10]}...")
print("=" * 60)
# Generiere RFID UIDs zum Testen
for i in range(1, max_attempts + 1):
# Generiere RFID im Format AA:BB:CC:XX
rfid_uid = f"AA:BB:CC:{i:02X}"
try:
response = requests.post(
base_url,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
},
json={"uid": rfid_uid},
timeout=5
)
if response.status_code == 200:
data = response.json()
if data.get("success") and data.get("data", {}).get("exists"):
found_rfids.append(data["data"])
user = data["data"]
print(f"✅ [{i:3d}] RFID GEFUNDEN!")
print(f" RFID: {rfid_uid}")
print(f" Name: {user['firstname']} {user['lastname']}")
print(f" Alter: {user['alter']}")
print("-" * 60)
else:
if i % 20 == 0: # Fortschritt alle 20 Requests
print(f"⏳ [{i:3d}] Kein User für RFID {rfid_uid}")
else:
print(f"❌ [{i:3d}] HTTP {response.status_code} für RFID {rfid_uid}")
except requests.exceptions.RequestException as e:
print(f"🔥 [{i:3d}] Fehler bei RFID {rfid_uid}: {e}")
continue
print("\n📈 RFID ENUMERATION ABGESCHLOSSEN")
print(f"Gefundene RFIDs: {len(found_rfids)}")
return found_rfids
def test_admin_login():
"""Teste Admin Login Enumeration"""
base_url = "http://localhost:3000/api/v1/public/login"
# Häufige Admin-Usernamen
admin_usernames = [
"admin", "administrator", "root", "user", "test", "demo",
"admin1", "admin2", "superuser", "manager", "operator"
]
print("\n🔍 TESTE ADMIN LOGIN ENUMERATION")
print("=" * 60)
for username in admin_usernames:
try:
start_time = time.time()
response = requests.post(
base_url,
json={"username": username, "password": "wrongpassword"},
timeout=5
)
end_time = time.time()
response_time = (end_time - start_time) * 1000 # in ms
print(f"👤 {username:12} | Status: {response.status_code:3d} | Zeit: {response_time:6.1f}ms")
if response.status_code == 200:
print(f" ⚠️ MÖGLICHERWEISE GÜLTIGER USERNAME!")
except Exception as e:
print(f"🔥 Fehler bei {username}: {e}")
# Führe Enumeration aus
if __name__ == "__main__":
print("🚨 NINJA SERVER SECURITY AUDIT - USER ENUMERATION")
print("⚠️ WARNUNG: Nur für autorisierte Sicherheitstests!")
print()
# 1. Supabase User ID Enumeration
found_users = enumerate_supabase_users()
# 2. Admin Login Test
test_admin_login()
# 3. RFID Enumeration (nur mit gültigem API-Key)
api_key = input("\n🔑 API-Key für RFID Enumeration eingeben (oder Enter zum Überspringen): ").strip()
if api_key:
enumerate_rfid_uids(api_key, 50) # Teste nur 50 RFIDs
else:
print("⏭️ RFID Enumeration übersprungen")
print("\n🏁 AUDIT ABGESCHLOSSEN")

View File

@@ -0,0 +1,312 @@
import requests
import time
import json
from datetime import datetime
import statistics
def test_admin_login_timing():
"""Detaillierte Timing-Analyse für Admin Login"""
base_url = "http://localhost:3000/api/v1/public/login"
# Erweiterte Liste von Admin-Usernamen
admin_usernames = [
"admin", "administrator", "root", "user", "test", "demo",
"admin1", "admin2", "superuser", "manager", "operator",
"ninja", "parkour", "system", "api", "service",
"backup", "support", "helpdesk", "it", "tech"
]
print("🔍 DETAILLIERTE ADMIN LOGIN TIMING-ANALYSE")
print("=" * 70)
print(f"Zeit: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Target: {base_url}")
print("=" * 70)
results = []
# Teste jeden Username mehrfach für bessere Statistik
for username in admin_usernames:
times = []
print(f"\n👤 Testing: {username}")
print("-" * 50)
for attempt in range(5): # 5 Versuche pro Username
try:
start_time = time.time()
response = requests.post(
base_url,
json={"username": username, "password": "wrongpassword123"},
timeout=10
)
end_time = time.time()
response_time = (end_time - start_time) * 1000 # in ms
times.append(response_time)
print(f" Attempt {attempt+1}: {response_time:6.1f}ms | Status: {response.status_code}")
# Kleine Pause zwischen Requests
time.sleep(0.1)
except Exception as e:
print(f" Attempt {attempt+1}: ERROR - {e}")
continue
if times:
avg_time = statistics.mean(times)
std_dev = statistics.stdev(times) if len(times) > 1 else 0
min_time = min(times)
max_time = max(times)
results.append({
'username': username,
'avg_time': avg_time,
'std_dev': std_dev,
'min_time': min_time,
'max_time': max_time,
'times': times,
'suspicious': avg_time > 50 # Verdächtig wenn > 50ms
})
print(f" 📊 Stats: Avg={avg_time:.1f}ms, Std={std_dev:.1f}ms, Range={min_time:.1f}-{max_time:.1f}ms")
if avg_time > 50:
print(f" ⚠️ SUSPEKT: Deutlich längere Response-Zeit!")
# Sortiere nach durchschnittlicher Response-Zeit
results.sort(key=lambda x: x['avg_time'], reverse=True)
print("\n" + "=" * 70)
print("📈 TIMING-ANALYSE ERGEBNISSE")
print("=" * 70)
print(f"{'Username':<15} {'Avg(ms)':<8} {'Std(ms)':<8} {'Min(ms)':<8} {'Max(ms)':<8} {'Status'}")
print("-" * 70)
for result in results:
status = "⚠️ SUSPEKT" if result['suspicious'] else "✅ Normal"
print(f"{result['username']:<15} {result['avg_time']:<8.1f} {result['std_dev']:<8.1f} "
f"{result['min_time']:<8.1f} {result['max_time']:<8.1f} {status}")
# Speichere detaillierte Ergebnisse
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"timing_analysis_{timestamp}.json"
with open(filename, 'w', encoding='utf-8') as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print(f"\n💾 Detaillierte Ergebnisse gespeichert in: {filename}")
return results
def test_rfid_creation_enumeration():
"""Teste RFID Enumeration über Spieler-Erstellung"""
base_url = "http://localhost:3000/api/v1/public/players/create-with-rfid"
print("\n🔍 RFID ENUMERATION ÜBER SPIELER-ERSTELLUNG")
print("=" * 70)
print(f"Zeit: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Target: {base_url}")
print("=" * 70)
found_rfids = []
# Teste verschiedene RFID-Patterns
test_patterns = [
"AA:BB:CC:DD", "AA:BB:CC:DE", "AA:BB:CC:DF",
"11:22:33:44", "11:22:33:45", "11:22:33:46",
"FF:FF:FF:FF", "00:00:00:00", "12:34:56:78",
"AB:CD:EF:12", "DE:AD:BE:EF", "CA:FE:BA:BE"
]
for i, rfid in enumerate(test_patterns, 1):
try:
payload = {
"rfiduid": rfid,
"firstname": "Test",
"lastname": "User",
"birthdate": "1990-01-01"
}
response = requests.post(base_url, json=payload, timeout=5)
print(f"[{i:2d}] RFID: {rfid:<12} | Status: {response.status_code:3d}", end="")
if response.status_code == 400:
try:
data = response.json()
if "existiert bereits" in data.get("message", ""):
print(" | ✅ EXISTIERT!")
if "existingPlayer" in data.get("details", {}):
existing = data["details"]["existingPlayer"]
found_rfids.append({
"rfid": rfid,
"existing_player": existing
})
print(f" → Name: {existing.get('firstname')} {existing.get('lastname')}")
else:
print(" | ❌ Anderer Fehler")
except:
print(" | ❌ JSON Parse Error")
else:
print(" | ⚠️ Unexpected Status")
except Exception as e:
print(f"[{i:2d}] RFID: {rfid:<12} | ERROR: {e}")
print(f"\n📊 RFID Enumeration abgeschlossen: {len(found_rfids)} gefunden")
return found_rfids
def test_leaderboard_data_leak():
"""Teste Leaderboard auf sensible Daten"""
base_url = "http://localhost:3000/api/v1/public/times-with-details"
print("\n🔍 LEADERBOARD DATENLEAK-TEST")
print("=" * 70)
print(f"Zeit: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Target: {base_url}")
print("=" * 70)
try:
response = requests.get(base_url, timeout=10)
print(f"Status: {response.status_code}")
if response.status_code == 200:
data = response.json()
if isinstance(data, list) and len(data) > 0:
print(f"✅ Leaderboard-Daten gefunden: {len(data)} Einträge")
# Analysiere erste paar Einträge
for i, entry in enumerate(data[:3]):
print(f"\n📋 Eintrag {i+1}:")
if 'player' in entry:
player = entry['player']
print(f" Name: {player.get('firstname')} {player.get('lastname')}")
print(f" RFID: {player.get('rfiduid')}")
print(f" ID: {player.get('id')}")
if 'location' in entry:
location = entry['location']
print(f" Location: {location.get('name')}")
print(f" Koordinaten: {location.get('latitude')}, {location.get('longitude')}")
if 'recorded_time_seconds' in entry:
print(f" Zeit: {entry['recorded_time_seconds']} Sekunden")
# Speichere alle Daten
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"leaderboard_data_{timestamp}.json"
with open(filename, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print(f"\n💾 Leaderboard-Daten gespeichert in: {filename}")
return data
else:
print("❌ Keine Leaderboard-Daten gefunden")
else:
print(f"❌ Fehler: HTTP {response.status_code}")
except Exception as e:
print(f"🔥 Fehler beim Abrufen der Leaderboard-Daten: {e}")
return None
def test_error_message_analysis():
"""Analysiere Error Messages auf Information Leakage"""
base_url = "http://localhost:3000/api/v1/public/user-player"
print("\n🔍 ERROR MESSAGE ANALYSE")
print("=" * 70)
test_uuids = [
"00000000-0000-0000-0000-000000000000", # Null UUID
"invalid-uuid-format", # Ungültiges Format
"12345678-1234-1234-1234-123456789012", # Gültiges Format, aber wahrscheinlich nicht existent
"../../../etc/passwd", # Path Traversal
"<script>alert('xss')</script>", # XSS Test
"'; DROP TABLE players; --" # SQL Injection Test
]
error_responses = {}
for i, test_input in enumerate(test_uuids, 1):
try:
response = requests.get(f"{base_url}/{test_input}", timeout=5)
status_code = response.status_code
print(f"[{i}] Input: {test_input:<30} | Status: {status_code}")
if status_code not in error_responses:
error_responses[status_code] = []
try:
json_data = response.json()
error_responses[status_code].append({
'input': test_input,
'response': json_data
})
except:
error_responses[status_code].append({
'input': test_input,
'response': response.text[:200] # Erste 200 Zeichen
})
except Exception as e:
print(f"[{i}] Input: {test_input:<30} | ERROR: {e}")
# Analysiere verschiedene Error-Messages
print(f"\n📊 Error-Message Analyse:")
print("-" * 50)
for status_code, responses in error_responses.items():
print(f"Status {status_code}: {len(responses)} Responses")
# Prüfe auf unterschiedliche Error-Messages
unique_messages = set()
for resp in responses:
if isinstance(resp['response'], dict):
message = resp['response'].get('message', 'No message')
else:
message = str(resp['response'])[:100]
unique_messages.add(message)
print(f" Unique messages: {len(unique_messages)}")
for msg in list(unique_messages)[:3]: # Zeige erste 3
print(f" - {msg}")
return error_responses
if __name__ == "__main__":
print("🚨 NINJA SERVER - REALISTISCHE SICHERHEITSTESTS")
print("⚠️ WARNUNG: Nur für autorisierte Sicherheitstests!")
print()
# 1. Admin Login Timing Analysis
timing_results = test_admin_login_timing()
# 2. RFID Enumeration über Spieler-Erstellung
rfid_results = test_rfid_creation_enumeration()
# 3. Leaderboard Datenleak-Test
leaderboard_data = test_leaderboard_data_leak()
# 4. Error Message Analysis
error_analysis = test_error_message_analysis()
print("\n" + "=" * 70)
print("🏁 REALISTISCHE SICHERHEITSTESTS ABGESCHLOSSEN")
print("=" * 70)
# Zusammenfassung
suspicious_users = [r for r in timing_results if r['suspicious']]
print(f"🔍 Timing-Suspicious Users: {len(suspicious_users)}")
print(f"🔍 Gefundene RFIDs: {len(rfid_results)}")
print(f"🔍 Leaderboard-Einträge: {len(leaderboard_data) if leaderboard_data else 0}")
if suspicious_users:
print(f"\n⚠️ SUSPEKTE USERNAMES (Timing):")
for user in suspicious_users:
print(f" - {user['username']}: {user['avg_time']:.1f}ms")
print(f"\n⏰ Abgeschlossen um: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")

239
public/agb.html Normal file
View File

@@ -0,0 +1,239 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Allgemeine Geschäftsbedingungen | NinjaCross</title>
<link rel="stylesheet" href="css/dashboard.css">
<style>
.agb-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #1e293b;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-top: 20px;
color: #e2e8f0;
}
.agb-header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #334155;
}
.agb-header h1 {
color: #00d4ff;
margin-bottom: 10px;
}
.agb-header .subtitle {
color: #94a3b8;
font-size: 16px;
}
.agb-content {
line-height: 1.6;
color: #e2e8f0;
}
.agb-section {
margin-bottom: 25px;
}
.agb-section h2 {
color: #00d4ff;
font-size: 20px;
margin-bottom: 15px;
padding-bottom: 5px;
border-bottom: 1px solid #334155;
}
.agb-section h3 {
color: #e2e8f0;
font-size: 16px;
margin-bottom: 10px;
margin-top: 20px;
}
.agb-section p {
margin-bottom: 12px;
}
.agb-section ul {
margin-left: 20px;
margin-bottom: 15px;
}
.agb-section li {
margin-bottom: 8px;
}
.highlight-box {
background: #0f172a;
border-left: 4px solid #00d4ff;
padding: 15px;
margin: 20px 0;
border-radius: 0 5px 5px 0;
}
.warning-box {
background: #451a03;
border-left: 4px solid #fbbf24;
padding: 15px;
margin: 20px 0;
border-radius: 0 5px 5px 0;
}
.back-button {
display: inline-block;
background: #00d4ff;
color: #0f172a;
padding: 12px 24px;
text-decoration: none;
border-radius: 5px;
margin-top: 30px;
transition: background-color 0.3s;
font-weight: bold;
}
.back-button:hover {
background: #0891b2;
}
.last-updated {
text-align: center;
color: #94a3b8;
font-style: italic;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #334155;
}
</style>
</head>
<body style="background: #0f172a; min-height: 100vh; padding: 20px;">
<div class="agb-container">
<div class="agb-header">
<h1>Allgemeine Geschäftsbedingungen</h1>
<p class="subtitle">NinjaCross Parkour System</p>
</div>
<div class="agb-content">
<div class="highlight-box">
<strong>Wichtig:</strong> Durch die Nutzung des NinjaCross Systems stimmen Sie zu, dass Ihre Daten
(Name, Zeiten, Standorte) im öffentlichen Leaderboard angezeigt werden können.
</div>
<div class="agb-section">
<h2>1. Geltungsbereich</h2>
<p>Diese Allgemeinen Geschäftsbedingungen (AGB) gelten für die Nutzung des NinjaCross Parkour Systems
im Schwimmbad. Mit der Registrierung und Nutzung des Systems erkennen Sie diese AGB als verbindlich an.</p>
</div>
<div class="agb-section">
<h2>2. Datenverarbeitung und Datenschutz</h2>
<h3>2.1 Erhebung von Daten</h3>
<p>Wir erheben folgende personenbezogene Daten:</p>
<ul>
<li>Vor- und Nachname</li>
<li>Geburtsdatum (zur Altersberechnung)</li>
<li>RFID-Kartennummer</li>
<li>Laufzeiten und Standortdaten</li>
<li>Zeitstempel der Aktivitäten</li>
</ul>
<h3>2.2 Verwendung der Daten</h3>
<p>Ihre Daten werden für folgende Zwecke verwendet:</p>
<ul>
<li><strong>Leaderboard-Anzeige:</strong> Name, Zeiten und Standorte werden im öffentlichen Leaderboard angezeigt</li>
<li><strong>Leistungsauswertung:</strong> Erfassung und Bewertung Ihrer Parkour-Zeiten</li>
<li><strong>System-Funktionalität:</strong> Zuordnung von Zeiten zu Ihrem Profil über RFID-Karten</li>
<li><strong>Statistiken:</strong> Anonymisierte Auswertungen für Systemverbesserungen</li>
</ul>
<div class="warning-box">
<strong>Wichtiger Hinweis:</strong> Durch die Annahme dieser AGB stimmen Sie ausdrücklich zu,
dass Ihr Name und Ihre Laufzeiten im öffentlichen Leaderboard sichtbar sind.
</div>
</div>
<div class="agb-section">
<h2>3. Leaderboard und Öffentlichkeit</h2>
<p>Das NinjaCross System verfügt über ein öffentlich zugängliches Leaderboard, das folgende Informationen anzeigt:</p>
<ul>
<li>Vollständiger Name der Teilnehmer</li>
<li>Erreichte Laufzeiten</li>
<li>Standort der Aktivität</li>
<li>Datum und Uhrzeit der Aktivität</li>
</ul>
<p><strong>Durch die Nutzung des Systems erklären Sie sich damit einverstanden, dass diese Daten öffentlich angezeigt werden.</strong></p>
</div>
<div class="agb-section">
<h2>4. Ihre Rechte</h2>
<h3>4.1 Recht auf Auskunft</h3>
<p>Sie haben das Recht, Auskunft über die zu Ihrer Person gespeicherten Daten zu verlangen.</p>
<h3>4.2 Recht auf Löschung</h3>
<p>Sie können jederzeit die Löschung Ihrer Daten und Ihres Profils beantragen.</p>
<h3>4.3 Recht auf Widerspruch</h3>
<p>Sie können der Verarbeitung Ihrer Daten für das Leaderboard widersprechen.
In diesem Fall werden Ihre Daten aus dem öffentlichen Leaderboard entfernt,
aber weiterhin für die Systemfunktionalität verwendet.</p>
</div>
<div class="agb-section">
<h2>5. Haftung und Verantwortung</h2>
<p>Die Teilnahme am NinjaCross System erfolgt auf eigene Gefahr. Wir haften nicht für:</p>
<ul>
<li>Verletzungen während der Nutzung der Parkour-Anlage</li>
<li>Verlust oder Diebstahl der RFID-Karte</li>
<li>Technische Ausfälle des Systems</li>
</ul>
</div>
<div class="agb-section">
<h2>6. Systemregeln</h2>
<p>Bei der Nutzung des Systems sind folgende Regeln zu beachten:</p>
<ul>
<li>Keine Manipulation der Zeiterfassung</li>
<li>Respektvoller Umgang mit anderen Teilnehmern</li>
<li>Beachtung der Sicherheitshinweise der Anlage</li>
<li>Keine Verwendung falscher Identitäten</li>
</ul>
</div>
<div class="agb-section">
<h2>7. Änderungen der AGB</h2>
<p>Wir behalten uns vor, diese AGB zu ändern. Wesentliche Änderungen werden Ihnen
mitgeteilt und erfordern Ihre erneute Zustimmung.</p>
</div>
<div class="agb-section">
<h2>8. Kontakt</h2>
<p>Bei Fragen zu diesen AGB oder zum Datenschutz wenden Sie sich an:</p>
<p>
<strong>NinjaCross Team</strong><br>
Schwimmbad Ulm<br>
E-Mail: info@ninjacross.de<br>
Telefon: 0731-123456
</p>
</div>
</div>
<div style="text-align: center;">
<a href="javascript:history.back()" class="back-button">Zurück</a>
</div>
<div class="last-updated">
<p>Stand: September 2024</p>
<p>Diese AGB sind Teil der Registrierung und gelten ab dem Zeitpunkt der Zustimmung.</p>
</div>
</div>
</body>
</html>

View File

@@ -703,6 +703,21 @@
<input type="date" id="playerBirthdate" class="form-input" style="text-align: center;">
</div>
<!-- AGB Section -->
<div class="agb-section" style="background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 15px; margin: 15px 0;">
<div class="agb-checkbox" style="display: flex; align-items: flex-start; gap: 10px; margin-bottom: 10px;">
<input type="checkbox" id="agbAccepted" name="agbAccepted" required style="width: auto; margin: 0; margin-top: 3px;">
<label for="agbAccepted" style="color: #e2e8f0; font-size: 0.85rem; line-height: 1.4; margin: 0; font-weight: normal;">
Ich habe die <a href="/agb.html" target="_blank" style="color: #00d4ff; text-decoration: none; font-weight: bold;">Allgemeinen Geschäftsbedingungen</a>
gelesen und stimme zu, dass mein Name und meine Laufzeiten im öffentlichen Leaderboard angezeigt werden.
</label>
</div>
<div class="agb-warning" style="color: #fbbf24; font-size: 0.8rem; margin-top: 10px;">
⚠️ <strong>Wichtig:</strong> Ohne Zustimmung zu den AGB können Sie das System nutzen,
aber Ihre Zeiten werden nicht im öffentlichen Leaderboard angezeigt.
</div>
</div>
<button class="btn btn-primary" onclick="createRfidPlayerRecord()" style="width: 100%;" data-de="Spieler erstellen" data-en="Create Player">
Spieler erstellen
</button>
@@ -768,6 +783,6 @@
</footer>
<script src="/js/cookie-consent.js"></script>
<script src="/js/dashboard.js?v=1.1"></script>
<script src="/js/dashboard.js?v=1.6"></script>
</body>
</html>

View File

@@ -400,7 +400,7 @@ function filterData() {
displayAchievements();
} else {
currentPlayers = filteredData;
displayPlayers();
displayPlayersWithAchievements();
}
break;
}
@@ -427,7 +427,7 @@ function refreshData() {
if (currentAchievementMode === 'achievements') {
loadAchievements();
} else {
loadPlayers();
loadPlayersWithAchievements();
}
break;
}
@@ -1372,7 +1372,7 @@ async function toggleAchievementMode() {
currentAchievementMode = 'players';
document.getElementById('dataTitle').textContent = '👥 Spieler-Achievements';
document.getElementById('searchInput').placeholder = 'Spieler durchsuchen...';
await loadPlayers();
await loadPlayersWithAchievements();
} else {
currentAchievementMode = 'achievements';
document.getElementById('dataTitle').textContent = '🏆 Achievement-Verwaltung';
@@ -1382,7 +1382,7 @@ async function toggleAchievementMode() {
}
// Load all players with achievement statistics
async function loadPlayers() {
async function loadPlayersWithAchievements() {
try {
const response = await fetch('/api/v1/admin/achievements/players');
const result = await response.json();
@@ -1390,7 +1390,7 @@ async function loadPlayers() {
if (result.success) {
currentPlayers = result.data;
currentData = result.data; // Set for filtering
displayPlayers();
displayPlayersWithAchievements();
} else {
showError('Fehler beim Laden der Spieler: ' + result.message);
}
@@ -1401,7 +1401,7 @@ async function loadPlayers() {
}
// Display players in table
function displayPlayers() {
function displayPlayersWithAchievements() {
const content = document.getElementById('dataContent');
if (currentPlayers.length === 0) {

View File

@@ -618,6 +618,7 @@ async function createRfidPlayerRecord() {
const firstname = document.getElementById('playerFirstname').value.trim();
const lastname = document.getElementById('playerLastname').value.trim();
const birthdate = document.getElementById('playerBirthdate').value;
const agbAccepted = document.getElementById('agbAccepted').checked;
// Validation
if (!rawUid) {
@@ -652,6 +653,14 @@ async function createRfidPlayerRecord() {
return;
}
if (!agbAccepted) {
const agbErrorMsg = currentLanguage === 'de' ?
'Bitte stimme den Allgemeinen Geschäftsbedingungen zu' :
'Please accept the Terms of Service';
showMessage('rfidMessage', agbErrorMsg, 'error');
return;
}
try {
// Format the UID to match database format
const formattedUid = formatRfidUid(rawUid);
@@ -672,7 +681,8 @@ async function createRfidPlayerRecord() {
firstname: firstname,
lastname: lastname,
birthdate: birthdate,
supabase_user_id: currentUser?.id || null
supabase_user_id: currentUser?.id || null,
agb_accepted: agbAccepted
})
});
@@ -689,6 +699,7 @@ async function createRfidPlayerRecord() {
document.getElementById('playerFirstname').value = '';
document.getElementById('playerLastname').value = '';
document.getElementById('playerBirthdate').value = '';
document.getElementById('agbAccepted').checked = false;
// Hide create player section since user is now linked
const createPlayerSection = document.getElementById('createPlayerSection');
@@ -1139,7 +1150,7 @@ let currentAchievementCategory = 'all';
// Load achievements for the current player
async function loadPlayerAchievements() {
if (!currentPlayerId) {
if (!currentPlayerId || currentPlayerId === 'undefined' || currentPlayerId === 'null') {
showAchievementsNotAvailable();
return;
}
@@ -1372,7 +1383,7 @@ function showStatistics() {
async function loadAnalyticsData() {
try {
if (!currentPlayerId) {
if (!currentPlayerId || currentPlayerId === 'undefined' || currentPlayerId === 'null') {
console.error('No player ID available - user not linked');
// Show fallback data when user is not linked
displayAnalyticsFallback();
@@ -1400,7 +1411,7 @@ async function loadAnalyticsData() {
async function loadStatisticsData() {
try {
if (!currentPlayerId) {
if (!currentPlayerId || currentPlayerId === 'undefined' || currentPlayerId === 'null') {
console.error('No player ID available - user not linked');
// Show fallback data when user is not linked
displayStatisticsFallback();
@@ -1682,7 +1693,7 @@ function showAchievementsNotAvailable() {
// Check achievements for current player
async function checkPlayerAchievements() {
if (!currentPlayerId) return;
if (!currentPlayerId || currentPlayerId === 'undefined' || currentPlayerId === 'null') return;
try {
const response = await fetch(`/api/achievements/check/${currentPlayerId}?t=${Date.now()}`, {
@@ -1738,8 +1749,13 @@ function showAchievementNotification(newAchievements) {
// Initialize achievements when player is loaded
function initializeAchievements(playerId) {
currentPlayerId = playerId;
loadPlayerAchievements();
if (playerId && playerId !== 'undefined' && playerId !== 'null') {
currentPlayerId = playerId;
loadPlayerAchievements();
} else {
console.warn('Invalid player ID provided to initializeAchievements:', playerId);
currentPlayerId = null;
}
}
// Convert VAPID key from base64url to Uint8Array
@@ -1764,6 +1780,16 @@ function urlBase64ToUint8Array(base64String) {
// Web Notification Functions
function showWebNotification(title, message, icon = '🏆') {
if ('Notification' in window && Notification.permission === 'granted') {
// Log notification details to console
console.log('🔔 Web Notification sent:', {
title: title,
message: message,
icon: icon,
playerId: currentPlayerId || 'unknown',
pushPlayerId: localStorage.getItem('pushPlayerId') || 'unknown',
timestamp: new Date().toISOString()
});
const notification = new Notification(title, {
body: message,
icon: '/pictures/icon-192.png',
@@ -1809,7 +1835,7 @@ async function checkBestTimeNotifications() {
const { daily, weekly, monthly } = result.data;
// Check if current player has best times
if (currentPlayerId) {
if (currentPlayerId && currentPlayerId !== 'undefined' && currentPlayerId !== 'null') {
const now = new Date();
const isEvening = now.getHours() >= 19;
@@ -1910,30 +1936,87 @@ async function checkBestTimeNotifications() {
// Check for new achievements and show notifications
async function checkAchievementNotifications() {
try {
if (!currentPlayerId) return;
console.log('🔍 checkAchievementNotifications() called');
// Check if push notifications are enabled
const pushPlayerId = localStorage.getItem('pushPlayerId');
if (!pushPlayerId) {
console.log('🔕 Push notifications disabled, skipping achievement check');
return;
}
console.log('🔍 Push notifications enabled for player:', pushPlayerId);
const response = await fetch(`/api/achievements/player/${currentPlayerId}?t=${Date.now()}`);
// Use pushPlayerId for notifications instead of currentPlayerId
if (!pushPlayerId || pushPlayerId === 'undefined' || pushPlayerId === 'null') return;
const response = await fetch(`/api/achievements/player/${pushPlayerId}?t=${Date.now()}`);
const result = await response.json();
if (result.success && result.data) {
const newAchievements = result.data.filter(achievement => {
// Check if achievement was earned in the last 5 minutes
const earnedAt = new Date(achievement.earned_at);
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
return earnedAt > fiveMinutesAgo;
console.log('🔍 Checking achievements for notifications:', {
totalAchievements: result.data.length,
playerId: pushPlayerId
});
const newAchievements = result.data.filter(achievement => {
// Only check completed achievements
if (!achievement.is_completed) {
return false;
}
// Check if achievement was earned in the last 10 minutes (extended window)
const earnedAt = achievement.earned_at ? new Date(achievement.earned_at) : null;
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
// If no earned_at date, check if it was completed recently by checking completion_count
if (!earnedAt) {
// Send to server console
fetch('/api/v1/public/log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
level: 'info',
message: `🔍 Achievement completed but no earned_at: ${achievement.name} (completion_count: ${achievement.completion_count})`,
playerId: pushPlayerId
})
}).catch(() => {}); // Ignore errors
// For achievements without earned_at, assume they are new if completion_count is 1
// This is a fallback for recently completed achievements
return achievement.completion_count === 1;
}
const isNew = earnedAt > tenMinutesAgo;
// Send detailed info to server console
fetch('/api/v1/public/log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
level: 'info',
message: `🔍 Achievement check: ${achievement.name} - earned_at: ${earnedAt.toISOString()}, isNew: ${isNew}, completed: ${achievement.is_completed}`,
playerId: pushPlayerId
})
}).catch(() => {}); // Ignore errors
return isNew;
});
console.log('🔍 New achievements found:', newAchievements.length);
if (newAchievements.length > 0) {
for (const achievement of newAchievements) {
// Check if notification was already sent for this achievement
const achievementCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/achievement?achievementId=${achievement.achievement_id}&locationId=${achievement.location_id || ''}`);
const achievementId = achievement.id || achievement.achievement_id;
const locationId = achievement.location_id || '';
if (!achievementId) {
console.warn('Achievement ID is missing for achievement:', achievement);
continue;
}
const achievementCheck = await fetch(`/api/v1/public/notification-sent/${pushPlayerId}/achievement?achievementId=${achievementId}&locationId=${locationId}`);
const achievementResult = await achievementCheck.json();
if (!achievementResult.wasSent) {
@@ -1949,10 +2032,10 @@ async function checkAchievementNotifications() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
playerId: currentPlayerId,
playerId: pushPlayerId,
notificationType: 'achievement',
achievementId: achievement.achievement_id,
locationId: achievement.location_id || null
achievementId: achievementId,
locationId: locationId || null
})
});
console.log(`🏆 Achievement notification sent: ${achievement.name}`);
@@ -2015,7 +2098,7 @@ function updateLeaderboardSetting() {
async function saveSettings() {
try {
if (!currentPlayerId) {
if (!currentPlayerId || currentPlayerId === 'undefined' || currentPlayerId === 'null') {
console.error('No player ID available');
return;
}

View File

@@ -1045,7 +1045,7 @@ const { checkNameAgainstBlacklist, addToBlacklist, removeFromBlacklist, getBlack
// Create new player with RFID and blacklist validation (no auth required for dashboard)
router.post('/v1/public/players/create-with-rfid', async (req, res) => {
const { rfiduid, firstname, lastname, birthdate, supabase_user_id } = req.body;
const { rfiduid, firstname, lastname, birthdate, supabase_user_id, agb_accepted } = req.body;
// Validierung
if (!rfiduid || !firstname || !lastname || !birthdate) {
@@ -1119,10 +1119,10 @@ router.post('/v1/public/players/create-with-rfid', async (req, res) => {
// Spieler in Datenbank einfügen
const result = await pool.query(
`INSERT INTO players (rfiduid, firstname, lastname, birthdate, supabase_user_id, created_at, show_in_leaderboard)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, rfiduid, firstname, lastname, birthdate, created_at`,
[rfiduid, firstname, lastname, birthdate, supabase_user_id || null, new Date(), true]
`INSERT INTO players (rfiduid, firstname, lastname, birthdate, supabase_user_id, created_at, show_in_leaderboard, agb_accepted)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, rfiduid, firstname, lastname, birthdate, created_at, agb_accepted`,
[rfiduid, firstname, lastname, birthdate, supabase_user_id || null, new Date(), !!agb_accepted, !!agb_accepted]
);
const newPlayer = result.rows[0];
@@ -1824,6 +1824,12 @@ router.delete('/v1/admin/players/:id', requireAdminAuth, async (req, res) => {
try {
// Erst alle zugehörigen Zeiten löschen
await pool.query('DELETE FROM times WHERE player_id = $1', [playerId]);
// Alle zugehörigen Notifications löschen
await pool.query('DELETE FROM sent_notifications WHERE player_id = $1', [playerId]);
// Alle zugehörigen Player Achievements löschen
await pool.query('DELETE FROM player_achievements WHERE player_id = $1', [playerId]);
// Dann den Spieler löschen
const result = await pool.query('DELETE FROM players WHERE id = $1', [playerId]);
@@ -2212,22 +2218,17 @@ router.get('/v1/public/times-with-details', async (req, res) => {
// Get all times with player and location details, ordered by time (fastest first)
// Only show times from players who have opted into leaderboard visibility
// SECURITY: Only return data needed for leaderboard display
const result = await pool.query(`
SELECT
t.id,
EXTRACT(EPOCH FROM t.recorded_time) as recorded_time_seconds,
t.created_at,
json_build_object(
'id', p.id,
'firstname', p.firstname,
'lastname', p.lastname,
'rfiduid', p.rfiduid
'lastname', p.lastname
) as player,
json_build_object(
'id', l.id,
'name', l.name,
'latitude', l.latitude,
'longitude', l.longitude
'name', l.name
) as location
FROM times t
LEFT JOIN players p ON t.player_id = p.id
@@ -2843,6 +2844,23 @@ router.get('/v1/public/best-times', async (req, res) => {
}
});
// ==================== LOGGING ENDPOINT ====================
// Log endpoint for client-side logging
router.post('/v1/public/log', async (req, res) => {
try {
const { level, message, playerId } = req.body;
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level?.toUpperCase() || 'INFO'}] [Player: ${playerId || 'unknown'}] ${message}`);
res.json({ success: true });
} catch (error) {
console.error('Error processing log:', error);
res.status(500).json({ success: false });
}
});
// ==================== PUSH NOTIFICATION ENDPOINTS ====================
// Subscribe to push notifications
@@ -3026,6 +3044,29 @@ router.get('/v1/public/notification-sent/:playerId/:type', async (req, res) => {
const { playerId, type } = req.params;
const { achievementId, locationId } = req.query;
// Validate required parameters
if (!playerId || playerId === 'undefined' || playerId === 'null') {
return res.status(400).json({
success: false,
message: 'Player ID ist erforderlich'
});
}
if (!type) {
return res.status(400).json({
success: false,
message: 'Notification Type ist erforderlich'
});
}
// Validate achievementId if provided
if (achievementId && (achievementId === 'undefined' || achievementId === 'null')) {
return res.status(400).json({
success: false,
message: 'Ungültige Achievement ID'
});
}
const today = new Date().toISOString().split('T')[0];
const result = await pool.query(`
@@ -3058,6 +3099,19 @@ router.post('/v1/public/notification-sent', async (req, res) => {
try {
const { playerId, notificationType, achievementId, locationId } = req.body;
// Log notification details to server console
const logData = {
playerId: playerId,
notificationType: notificationType,
timestamp: new Date().toISOString()
};
// Only add achievementId and locationId if they exist
if (achievementId) logData.achievementId = achievementId;
if (locationId) logData.locationId = locationId;
console.log('📱 Push Notification marked as sent:', logData);
await pool.query(`
INSERT INTO sent_notifications (player_id, notification_type, achievement_id, location_id)
VALUES ($1, $2, $3, $4)

View File

@@ -97,22 +97,17 @@ router.get('/times-with-details', async (req, res) => {
}
// Get all times with player and location details, ordered by time (fastest first)
// SECURITY: Only return data needed for leaderboard display
const result = await pool.query(`
SELECT
t.id,
EXTRACT(EPOCH FROM t.recorded_time) as recorded_time_seconds,
t.created_at,
json_build_object(
'id', p.id,
'firstname', p.firstname,
'lastname', p.lastname,
'rfiduid', p.rfiduid
'lastname', p.lastname
) as player,
json_build_object(
'id', l.id,
'name', l.name,
'latitude', l.latitude,
'longitude', l.longitude
'name', l.name
) as location
FROM times t
LEFT JOIN players p ON t.player_id = p.id