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;">
|
||||
</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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user