Viel Push und achivements + AGB
This commit is contained in:
187
pentest/enumerate.py
Normal file
187
pentest/enumerate.py
Normal 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")
|
||||||
312
pentest/realistic_enumeration.py
Normal file
312
pentest/realistic_enumeration.py
Normal 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
239
public/agb.html
Normal 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>
|
||||||
@@ -703,6 +703,21 @@
|
|||||||
<input type="date" id="playerBirthdate" class="form-input" style="text-align: center;">
|
<input type="date" id="playerBirthdate" class="form-input" style="text-align: center;">
|
||||||
</div>
|
</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">
|
<button class="btn btn-primary" onclick="createRfidPlayerRecord()" style="width: 100%;" data-de="Spieler erstellen" data-en="Create Player">
|
||||||
Spieler erstellen
|
Spieler erstellen
|
||||||
</button>
|
</button>
|
||||||
@@ -768,6 +783,6 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="/js/cookie-consent.js"></script>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ function filterData() {
|
|||||||
displayAchievements();
|
displayAchievements();
|
||||||
} else {
|
} else {
|
||||||
currentPlayers = filteredData;
|
currentPlayers = filteredData;
|
||||||
displayPlayers();
|
displayPlayersWithAchievements();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -427,7 +427,7 @@ function refreshData() {
|
|||||||
if (currentAchievementMode === 'achievements') {
|
if (currentAchievementMode === 'achievements') {
|
||||||
loadAchievements();
|
loadAchievements();
|
||||||
} else {
|
} else {
|
||||||
loadPlayers();
|
loadPlayersWithAchievements();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1372,7 +1372,7 @@ async function toggleAchievementMode() {
|
|||||||
currentAchievementMode = 'players';
|
currentAchievementMode = 'players';
|
||||||
document.getElementById('dataTitle').textContent = '👥 Spieler-Achievements';
|
document.getElementById('dataTitle').textContent = '👥 Spieler-Achievements';
|
||||||
document.getElementById('searchInput').placeholder = 'Spieler durchsuchen...';
|
document.getElementById('searchInput').placeholder = 'Spieler durchsuchen...';
|
||||||
await loadPlayers();
|
await loadPlayersWithAchievements();
|
||||||
} else {
|
} else {
|
||||||
currentAchievementMode = 'achievements';
|
currentAchievementMode = 'achievements';
|
||||||
document.getElementById('dataTitle').textContent = '🏆 Achievement-Verwaltung';
|
document.getElementById('dataTitle').textContent = '🏆 Achievement-Verwaltung';
|
||||||
@@ -1382,7 +1382,7 @@ async function toggleAchievementMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load all players with achievement statistics
|
// Load all players with achievement statistics
|
||||||
async function loadPlayers() {
|
async function loadPlayersWithAchievements() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/v1/admin/achievements/players');
|
const response = await fetch('/api/v1/admin/achievements/players');
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@@ -1390,7 +1390,7 @@ async function loadPlayers() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
currentPlayers = result.data;
|
currentPlayers = result.data;
|
||||||
currentData = result.data; // Set for filtering
|
currentData = result.data; // Set for filtering
|
||||||
displayPlayers();
|
displayPlayersWithAchievements();
|
||||||
} else {
|
} else {
|
||||||
showError('Fehler beim Laden der Spieler: ' + result.message);
|
showError('Fehler beim Laden der Spieler: ' + result.message);
|
||||||
}
|
}
|
||||||
@@ -1401,7 +1401,7 @@ async function loadPlayers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Display players in table
|
// Display players in table
|
||||||
function displayPlayers() {
|
function displayPlayersWithAchievements() {
|
||||||
const content = document.getElementById('dataContent');
|
const content = document.getElementById('dataContent');
|
||||||
|
|
||||||
if (currentPlayers.length === 0) {
|
if (currentPlayers.length === 0) {
|
||||||
|
|||||||
@@ -618,6 +618,7 @@ async function createRfidPlayerRecord() {
|
|||||||
const firstname = document.getElementById('playerFirstname').value.trim();
|
const firstname = document.getElementById('playerFirstname').value.trim();
|
||||||
const lastname = document.getElementById('playerLastname').value.trim();
|
const lastname = document.getElementById('playerLastname').value.trim();
|
||||||
const birthdate = document.getElementById('playerBirthdate').value;
|
const birthdate = document.getElementById('playerBirthdate').value;
|
||||||
|
const agbAccepted = document.getElementById('agbAccepted').checked;
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
if (!rawUid) {
|
if (!rawUid) {
|
||||||
@@ -652,6 +653,14 @@ async function createRfidPlayerRecord() {
|
|||||||
return;
|
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 {
|
try {
|
||||||
// Format the UID to match database format
|
// Format the UID to match database format
|
||||||
const formattedUid = formatRfidUid(rawUid);
|
const formattedUid = formatRfidUid(rawUid);
|
||||||
@@ -672,7 +681,8 @@ async function createRfidPlayerRecord() {
|
|||||||
firstname: firstname,
|
firstname: firstname,
|
||||||
lastname: lastname,
|
lastname: lastname,
|
||||||
birthdate: birthdate,
|
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('playerFirstname').value = '';
|
||||||
document.getElementById('playerLastname').value = '';
|
document.getElementById('playerLastname').value = '';
|
||||||
document.getElementById('playerBirthdate').value = '';
|
document.getElementById('playerBirthdate').value = '';
|
||||||
|
document.getElementById('agbAccepted').checked = false;
|
||||||
|
|
||||||
// Hide create player section since user is now linked
|
// Hide create player section since user is now linked
|
||||||
const createPlayerSection = document.getElementById('createPlayerSection');
|
const createPlayerSection = document.getElementById('createPlayerSection');
|
||||||
@@ -1139,7 +1150,7 @@ let currentAchievementCategory = 'all';
|
|||||||
|
|
||||||
// Load achievements for the current player
|
// Load achievements for the current player
|
||||||
async function loadPlayerAchievements() {
|
async function loadPlayerAchievements() {
|
||||||
if (!currentPlayerId) {
|
if (!currentPlayerId || currentPlayerId === 'undefined' || currentPlayerId === 'null') {
|
||||||
showAchievementsNotAvailable();
|
showAchievementsNotAvailable();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1372,7 +1383,7 @@ function showStatistics() {
|
|||||||
|
|
||||||
async function loadAnalyticsData() {
|
async function loadAnalyticsData() {
|
||||||
try {
|
try {
|
||||||
if (!currentPlayerId) {
|
if (!currentPlayerId || currentPlayerId === 'undefined' || currentPlayerId === 'null') {
|
||||||
console.error('No player ID available - user not linked');
|
console.error('No player ID available - user not linked');
|
||||||
// Show fallback data when user is not linked
|
// Show fallback data when user is not linked
|
||||||
displayAnalyticsFallback();
|
displayAnalyticsFallback();
|
||||||
@@ -1400,7 +1411,7 @@ async function loadAnalyticsData() {
|
|||||||
|
|
||||||
async function loadStatisticsData() {
|
async function loadStatisticsData() {
|
||||||
try {
|
try {
|
||||||
if (!currentPlayerId) {
|
if (!currentPlayerId || currentPlayerId === 'undefined' || currentPlayerId === 'null') {
|
||||||
console.error('No player ID available - user not linked');
|
console.error('No player ID available - user not linked');
|
||||||
// Show fallback data when user is not linked
|
// Show fallback data when user is not linked
|
||||||
displayStatisticsFallback();
|
displayStatisticsFallback();
|
||||||
@@ -1682,7 +1693,7 @@ function showAchievementsNotAvailable() {
|
|||||||
|
|
||||||
// Check achievements for current player
|
// Check achievements for current player
|
||||||
async function checkPlayerAchievements() {
|
async function checkPlayerAchievements() {
|
||||||
if (!currentPlayerId) return;
|
if (!currentPlayerId || currentPlayerId === 'undefined' || currentPlayerId === 'null') return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/achievements/check/${currentPlayerId}?t=${Date.now()}`, {
|
const response = await fetch(`/api/achievements/check/${currentPlayerId}?t=${Date.now()}`, {
|
||||||
@@ -1738,8 +1749,13 @@ function showAchievementNotification(newAchievements) {
|
|||||||
|
|
||||||
// Initialize achievements when player is loaded
|
// Initialize achievements when player is loaded
|
||||||
function initializeAchievements(playerId) {
|
function initializeAchievements(playerId) {
|
||||||
|
if (playerId && playerId !== 'undefined' && playerId !== 'null') {
|
||||||
currentPlayerId = playerId;
|
currentPlayerId = playerId;
|
||||||
loadPlayerAchievements();
|
loadPlayerAchievements();
|
||||||
|
} else {
|
||||||
|
console.warn('Invalid player ID provided to initializeAchievements:', playerId);
|
||||||
|
currentPlayerId = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert VAPID key from base64url to Uint8Array
|
// Convert VAPID key from base64url to Uint8Array
|
||||||
@@ -1764,6 +1780,16 @@ function urlBase64ToUint8Array(base64String) {
|
|||||||
// Web Notification Functions
|
// Web Notification Functions
|
||||||
function showWebNotification(title, message, icon = '🏆') {
|
function showWebNotification(title, message, icon = '🏆') {
|
||||||
if ('Notification' in window && Notification.permission === 'granted') {
|
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, {
|
const notification = new Notification(title, {
|
||||||
body: message,
|
body: message,
|
||||||
icon: '/pictures/icon-192.png',
|
icon: '/pictures/icon-192.png',
|
||||||
@@ -1809,7 +1835,7 @@ async function checkBestTimeNotifications() {
|
|||||||
const { daily, weekly, monthly } = result.data;
|
const { daily, weekly, monthly } = result.data;
|
||||||
|
|
||||||
// Check if current player has best times
|
// Check if current player has best times
|
||||||
if (currentPlayerId) {
|
if (currentPlayerId && currentPlayerId !== 'undefined' && currentPlayerId !== 'null') {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const isEvening = now.getHours() >= 19;
|
const isEvening = now.getHours() >= 19;
|
||||||
|
|
||||||
@@ -1910,7 +1936,7 @@ async function checkBestTimeNotifications() {
|
|||||||
// Check for new achievements and show notifications
|
// Check for new achievements and show notifications
|
||||||
async function checkAchievementNotifications() {
|
async function checkAchievementNotifications() {
|
||||||
try {
|
try {
|
||||||
if (!currentPlayerId) return;
|
console.log('🔍 checkAchievementNotifications() called');
|
||||||
|
|
||||||
// Check if push notifications are enabled
|
// Check if push notifications are enabled
|
||||||
const pushPlayerId = localStorage.getItem('pushPlayerId');
|
const pushPlayerId = localStorage.getItem('pushPlayerId');
|
||||||
@@ -1919,21 +1945,78 @@ async function checkAchievementNotifications() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`/api/achievements/player/${currentPlayerId}?t=${Date.now()}`);
|
console.log('🔍 Push notifications enabled for player:', pushPlayerId);
|
||||||
|
|
||||||
|
// 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();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
const newAchievements = result.data.filter(achievement => {
|
console.log('🔍 Checking achievements for notifications:', {
|
||||||
// Check if achievement was earned in the last 5 minutes
|
totalAchievements: result.data.length,
|
||||||
const earnedAt = new Date(achievement.earned_at);
|
playerId: pushPlayerId
|
||||||
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
|
||||||
return earnedAt > fiveMinutesAgo;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) {
|
if (newAchievements.length > 0) {
|
||||||
for (const achievement of newAchievements) {
|
for (const achievement of newAchievements) {
|
||||||
// Check if notification was already sent for this achievement
|
// 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();
|
const achievementResult = await achievementCheck.json();
|
||||||
|
|
||||||
if (!achievementResult.wasSent) {
|
if (!achievementResult.wasSent) {
|
||||||
@@ -1949,10 +2032,10 @@ async function checkAchievementNotifications() {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
playerId: currentPlayerId,
|
playerId: pushPlayerId,
|
||||||
notificationType: 'achievement',
|
notificationType: 'achievement',
|
||||||
achievementId: achievement.achievement_id,
|
achievementId: achievementId,
|
||||||
locationId: achievement.location_id || null
|
locationId: locationId || null
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
console.log(`🏆 Achievement notification sent: ${achievement.name}`);
|
console.log(`🏆 Achievement notification sent: ${achievement.name}`);
|
||||||
@@ -2015,7 +2098,7 @@ function updateLeaderboardSetting() {
|
|||||||
|
|
||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
try {
|
try {
|
||||||
if (!currentPlayerId) {
|
if (!currentPlayerId || currentPlayerId === 'undefined' || currentPlayerId === 'null') {
|
||||||
console.error('No player ID available');
|
console.error('No player ID available');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1045,7 +1045,7 @@ const { checkNameAgainstBlacklist, addToBlacklist, removeFromBlacklist, getBlack
|
|||||||
|
|
||||||
// Create new player with RFID and blacklist validation (no auth required for dashboard)
|
// Create new player with RFID and blacklist validation (no auth required for dashboard)
|
||||||
router.post('/v1/public/players/create-with-rfid', async (req, res) => {
|
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
|
// Validierung
|
||||||
if (!rfiduid || !firstname || !lastname || !birthdate) {
|
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
|
// Spieler in Datenbank einfügen
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`INSERT INTO players (rfiduid, firstname, lastname, birthdate, supabase_user_id, created_at, show_in_leaderboard)
|
`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)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
RETURNING id, rfiduid, firstname, lastname, birthdate, created_at`,
|
RETURNING id, rfiduid, firstname, lastname, birthdate, created_at, agb_accepted`,
|
||||||
[rfiduid, firstname, lastname, birthdate, supabase_user_id || null, new Date(), true]
|
[rfiduid, firstname, lastname, birthdate, supabase_user_id || null, new Date(), !!agb_accepted, !!agb_accepted]
|
||||||
);
|
);
|
||||||
|
|
||||||
const newPlayer = result.rows[0];
|
const newPlayer = result.rows[0];
|
||||||
@@ -1825,6 +1825,12 @@ router.delete('/v1/admin/players/:id', requireAdminAuth, async (req, res) => {
|
|||||||
// Erst alle zugehörigen Zeiten löschen
|
// Erst alle zugehörigen Zeiten löschen
|
||||||
await pool.query('DELETE FROM times WHERE player_id = $1', [playerId]);
|
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
|
// Dann den Spieler löschen
|
||||||
const result = await pool.query('DELETE FROM players WHERE id = $1', [playerId]);
|
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)
|
// Get all times with player and location details, ordered by time (fastest first)
|
||||||
// Only show times from players who have opted into leaderboard visibility
|
// Only show times from players who have opted into leaderboard visibility
|
||||||
|
// SECURITY: Only return data needed for leaderboard display
|
||||||
const result = await pool.query(`
|
const result = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
t.id,
|
|
||||||
EXTRACT(EPOCH FROM t.recorded_time) as recorded_time_seconds,
|
EXTRACT(EPOCH FROM t.recorded_time) as recorded_time_seconds,
|
||||||
t.created_at,
|
t.created_at,
|
||||||
json_build_object(
|
json_build_object(
|
||||||
'id', p.id,
|
|
||||||
'firstname', p.firstname,
|
'firstname', p.firstname,
|
||||||
'lastname', p.lastname,
|
'lastname', p.lastname
|
||||||
'rfiduid', p.rfiduid
|
|
||||||
) as player,
|
) as player,
|
||||||
json_build_object(
|
json_build_object(
|
||||||
'id', l.id,
|
'name', l.name
|
||||||
'name', l.name,
|
|
||||||
'latitude', l.latitude,
|
|
||||||
'longitude', l.longitude
|
|
||||||
) as location
|
) as location
|
||||||
FROM times t
|
FROM times t
|
||||||
LEFT JOIN players p ON t.player_id = p.id
|
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 ====================
|
// ==================== PUSH NOTIFICATION ENDPOINTS ====================
|
||||||
|
|
||||||
// Subscribe to push notifications
|
// 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 { playerId, type } = req.params;
|
||||||
const { achievementId, locationId } = req.query;
|
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 today = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
const result = await pool.query(`
|
const result = await pool.query(`
|
||||||
@@ -3058,6 +3099,19 @@ router.post('/v1/public/notification-sent', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { playerId, notificationType, achievementId, locationId } = req.body;
|
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(`
|
await pool.query(`
|
||||||
INSERT INTO sent_notifications (player_id, notification_type, achievement_id, location_id)
|
INSERT INTO sent_notifications (player_id, notification_type, achievement_id, location_id)
|
||||||
VALUES ($1, $2, $3, $4)
|
VALUES ($1, $2, $3, $4)
|
||||||
|
|||||||
@@ -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)
|
// 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(`
|
const result = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
t.id,
|
|
||||||
EXTRACT(EPOCH FROM t.recorded_time) as recorded_time_seconds,
|
EXTRACT(EPOCH FROM t.recorded_time) as recorded_time_seconds,
|
||||||
t.created_at,
|
t.created_at,
|
||||||
json_build_object(
|
json_build_object(
|
||||||
'id', p.id,
|
|
||||||
'firstname', p.firstname,
|
'firstname', p.firstname,
|
||||||
'lastname', p.lastname,
|
'lastname', p.lastname
|
||||||
'rfiduid', p.rfiduid
|
|
||||||
) as player,
|
) as player,
|
||||||
json_build_object(
|
json_build_object(
|
||||||
'id', l.id,
|
'name', l.name
|
||||||
'name', l.name,
|
|
||||||
'latitude', l.latitude,
|
|
||||||
'longitude', l.longitude
|
|
||||||
) as location
|
) as location
|
||||||
FROM times t
|
FROM times t
|
||||||
LEFT JOIN players p ON t.player_id = p.id
|
LEFT JOIN players p ON t.player_id = p.id
|
||||||
|
|||||||
Reference in New Issue
Block a user