AdminDashboard
This commit is contained in:
904
public/admin-dashboard.html
Normal file
904
public/admin-dashboard.html
Normal file
@@ -0,0 +1,904 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Dashboard - NinjaCross</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||
min-height: 100vh;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #1e3c72;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.access-badge {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 15px;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.access-badge.level-1 {
|
||||
background: #ff9800;
|
||||
}
|
||||
|
||||
.access-badge.level-2 {
|
||||
background: #4CAF50;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 0.9em;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #1976D2;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #d32f2f;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #ff9800;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #f57c00;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #4CAF50;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #388E3C;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 30px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
color: #1e3c72;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2.5em;
|
||||
font-weight: bold;
|
||||
color: #1e3c72;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: #f5f5f5;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.data-table tr:hover {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
margin: 10% auto;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 15px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 5px 10px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🛡️ Admin Dashboard</h1>
|
||||
<div class="user-info">
|
||||
<span id="username">Loading...</span>
|
||||
<span id="accessBadge" class="access-badge">Level ?</span>
|
||||
<button id="generatorBtn" class="btn btn-success" style="display: none;">
|
||||
🔧 Lizenzgenerator
|
||||
</button>
|
||||
<button id="logoutBtn" class="btn btn-danger">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- Statistiken -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="totalPlayers">-</div>
|
||||
<div class="stat-label">Spieler</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="totalRuns">-</div>
|
||||
<div class="stat-label">Läufe</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="totalLocations">-</div>
|
||||
<div class="stat-label">Standorte</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="totalAdminUsers">-</div>
|
||||
<div class="stat-label">Admin-Benutzer</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Cards -->
|
||||
<div class="dashboard-grid">
|
||||
<!-- Benutzer-Verwaltung -->
|
||||
<div class="card">
|
||||
<h3><span class="icon">👥</span> Benutzer-Verwaltung</h3>
|
||||
<p>Verwalte Supabase-Benutzer und deren RFID-Verknüpfungen</p>
|
||||
<button class="btn" onclick="showUserManagement()">Benutzer anzeigen</button>
|
||||
</div>
|
||||
|
||||
<!-- Spieler-Verwaltung -->
|
||||
<div class="card">
|
||||
<h3><span class="icon">🏃</span> Spieler-Verwaltung</h3>
|
||||
<p>Verwalte Spieler und deren RFID-UIDs</p>
|
||||
<button class="btn" onclick="showPlayerManagement()">Spieler anzeigen</button>
|
||||
</div>
|
||||
|
||||
<!-- Läufe-Verwaltung -->
|
||||
<div class="card">
|
||||
<h3><span class="icon">⏱️</span> Läufe-Verwaltung</h3>
|
||||
<p>Zeige und lösche Läufe</p>
|
||||
<button class="btn" onclick="showRunManagement()">Läufe anzeigen</button>
|
||||
</div>
|
||||
|
||||
<!-- Standort-Verwaltung -->
|
||||
<div class="card">
|
||||
<h3><span class="icon">📍</span> Standort-Verwaltung</h3>
|
||||
<p>Verwalte Standorte und deren Koordinaten</p>
|
||||
<button class="btn" onclick="showLocationManagement()">Standorte anzeigen</button>
|
||||
</div>
|
||||
|
||||
<!-- Admin-Benutzer -->
|
||||
<div class="card">
|
||||
<h3><span class="icon">🔐</span> Admin-Benutzer</h3>
|
||||
<p>Verwalte Admin-Benutzer und Zugriffsrechte</p>
|
||||
<button class="btn" onclick="showAdminUserManagement()">Admins anzeigen</button>
|
||||
</div>
|
||||
|
||||
<!-- System-Info -->
|
||||
<div class="card">
|
||||
<h3><span class="icon">📊</span> System-Informationen</h3>
|
||||
<p>Server-Status und Systemdaten</p>
|
||||
<button class="btn" onclick="showSystemInfo()">Info anzeigen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Daten-Anzeige Bereich -->
|
||||
<div id="dataSection" style="display: none;">
|
||||
<div class="card">
|
||||
<h3 id="dataTitle">Daten</h3>
|
||||
<div class="search-container">
|
||||
<input type="text" id="searchInput" class="search-input" placeholder="Suchen...">
|
||||
<button class="btn" onclick="refreshData()">🔄 Aktualisieren</button>
|
||||
<button class="btn btn-success" onclick="showAddModal()">➕ Hinzufügen</button>
|
||||
</div>
|
||||
<div id="dataContent">
|
||||
<div class="loading">Lade Daten...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modals -->
|
||||
<div id="addModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close">×</span>
|
||||
<h3 id="modalTitle">Element hinzufügen</h3>
|
||||
<div class="message" id="modalMessage"></div>
|
||||
<form id="addForm">
|
||||
<div id="formFields"></div>
|
||||
<button type="submit" class="btn">Speichern</button>
|
||||
<button type="button" class="btn btn-danger" onclick="closeModal()">Abbrechen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="confirmModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close">×</span>
|
||||
<h3>Bestätigung</h3>
|
||||
<p id="confirmMessage">Sind Sie sicher?</p>
|
||||
<button id="confirmYes" class="btn btn-danger">Ja, löschen</button>
|
||||
<button id="confirmNo" class="btn">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentUser = null;
|
||||
let currentDataType = null;
|
||||
let currentData = [];
|
||||
|
||||
// Beim Laden der Seite
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
checkAuth();
|
||||
loadStatistics();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
// Logout Button
|
||||
document.getElementById('logoutBtn').addEventListener('click', logout);
|
||||
|
||||
// Generator Button
|
||||
document.getElementById('generatorBtn').addEventListener('click', function() {
|
||||
window.location.href = '/generator';
|
||||
});
|
||||
|
||||
// Modal Close Buttons
|
||||
document.querySelectorAll('.close').forEach(closeBtn => {
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
});
|
||||
|
||||
// Confirm Modal Buttons
|
||||
document.getElementById('confirmNo').addEventListener('click', closeModal);
|
||||
|
||||
// Search Input
|
||||
document.getElementById('searchInput').addEventListener('input', filterData);
|
||||
|
||||
// Add Form
|
||||
document.getElementById('addForm').addEventListener('submit', handleAddSubmit);
|
||||
}
|
||||
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const response = await fetch('/api/check-session');
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
window.location.href = '/adminlogin.html';
|
||||
return;
|
||||
}
|
||||
|
||||
currentUser = result.user;
|
||||
document.getElementById('username').textContent = currentUser.username;
|
||||
|
||||
const accessBadge = document.getElementById('accessBadge');
|
||||
accessBadge.textContent = `Level ${currentUser.access_level}`;
|
||||
accessBadge.className = `access-badge level-${currentUser.access_level}`;
|
||||
|
||||
// Generator-Button nur für Level 2
|
||||
if (currentUser.access_level >= 2) {
|
||||
document.getElementById('generatorBtn').style.display = 'inline-block';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Auth check failed:', error);
|
||||
window.location.href = '/adminlogin.html';
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('/api/logout', { method: 'POST' });
|
||||
window.location.href = '/adminlogin.html';
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
window.location.href = '/adminlogin.html';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStatistics() {
|
||||
try {
|
||||
const response = await fetch('/api/admin-stats');
|
||||
const stats = await response.json();
|
||||
|
||||
if (stats.success) {
|
||||
document.getElementById('totalPlayers').textContent = stats.data.players || 0;
|
||||
document.getElementById('totalRuns').textContent = stats.data.runs || 0;
|
||||
document.getElementById('totalLocations').textContent = stats.data.locations || 0;
|
||||
document.getElementById('totalAdminUsers').textContent = stats.data.adminUsers || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load statistics:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function showUserManagement() {
|
||||
showDataSection('users', 'Benutzer-Verwaltung');
|
||||
loadUsers();
|
||||
}
|
||||
|
||||
function showPlayerManagement() {
|
||||
showDataSection('players', 'Spieler-Verwaltung');
|
||||
loadPlayers();
|
||||
}
|
||||
|
||||
function showRunManagement() {
|
||||
showDataSection('runs', 'Läufe-Verwaltung');
|
||||
loadRuns();
|
||||
}
|
||||
|
||||
function showLocationManagement() {
|
||||
showDataSection('locations', 'Standort-Verwaltung');
|
||||
loadLocations();
|
||||
}
|
||||
|
||||
function showAdminUserManagement() {
|
||||
showDataSection('adminusers', 'Admin-Benutzer');
|
||||
loadAdminUsers();
|
||||
}
|
||||
|
||||
function showSystemInfo() {
|
||||
showDataSection('system', 'System-Informationen');
|
||||
loadSystemInfo();
|
||||
}
|
||||
|
||||
function showDataSection(type, title) {
|
||||
currentDataType = type;
|
||||
document.getElementById('dataTitle').textContent = title;
|
||||
document.getElementById('dataSection').style.display = 'block';
|
||||
document.getElementById('searchInput').value = '';
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
// Implementation für Supabase-Benutzer
|
||||
document.getElementById('dataContent').innerHTML = '<div class="no-data">Supabase-Benutzer werden über die Supabase-Console verwaltet</div>';
|
||||
}
|
||||
|
||||
async function loadPlayers() {
|
||||
try {
|
||||
const response = await fetch('/api/admin-players');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
currentData = result.data;
|
||||
displayPlayersTable(result.data);
|
||||
} else {
|
||||
showError('Fehler beim Laden der Spieler');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load players:', error);
|
||||
showError('Fehler beim Laden der Spieler');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRuns() {
|
||||
try {
|
||||
const response = await fetch('/api/admin-runs');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
currentData = result.data;
|
||||
displayRunsTable(result.data);
|
||||
} else {
|
||||
showError('Fehler beim Laden der Läufe');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load runs:', error);
|
||||
showError('Fehler beim Laden der Läufe');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLocations() {
|
||||
try {
|
||||
const response = await fetch('/api/admin-locations');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
currentData = result.data;
|
||||
displayLocationsTable(result.data);
|
||||
} else {
|
||||
showError('Fehler beim Laden der Standorte');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load locations:', error);
|
||||
showError('Fehler beim Laden der Standorte');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAdminUsers() {
|
||||
try {
|
||||
const response = await fetch('/api/admin-adminusers');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
currentData = result.data;
|
||||
displayAdminUsersTable(result.data);
|
||||
} else {
|
||||
showError('Fehler beim Laden der Admin-Benutzer');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load admin users:', error);
|
||||
showError('Fehler beim Laden der Admin-Benutzer');
|
||||
}
|
||||
}
|
||||
|
||||
function displayPlayersTable(players) {
|
||||
let html = '<div class="table-container"><table class="data-table">';
|
||||
html += '<thead><tr><th>ID</th><th>Name</th><th>RFID UID</th><th>Supabase User</th><th>Registriert</th><th>Aktionen</th></tr></thead><tbody>';
|
||||
|
||||
players.forEach(player => {
|
||||
html += `<tr>
|
||||
<td>${player.id}</td>
|
||||
<td>${player.full_name || '-'}</td>
|
||||
<td>${player.rfiduid || '-'}</td>
|
||||
<td>${player.supabase_user_id ? '✅' : '❌'}</td>
|
||||
<td>${new Date(player.created_at).toLocaleDateString('de-DE')}</td>
|
||||
<td class="action-buttons">
|
||||
<button class="btn btn-small btn-danger" onclick="deletePlayer('${player.id}')">Löschen</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
document.getElementById('dataContent').innerHTML = html;
|
||||
}
|
||||
|
||||
function displayRunsTable(runs) {
|
||||
let html = '<div class="table-container"><table class="data-table">';
|
||||
html += '<thead><tr><th>ID</th><th>Spieler</th><th>Standort</th><th>Zeit</th><th>Datum</th><th>Aktionen</th></tr></thead><tbody>';
|
||||
|
||||
runs.forEach(run => {
|
||||
// Use the time_seconds value from the backend
|
||||
const timeInSeconds = parseFloat(run.time_seconds) || 0;
|
||||
|
||||
html += `<tr>
|
||||
<td>${run.id}</td>
|
||||
<td>${run.player_name || `Player ${run.player_id}`}</td>
|
||||
<td>${run.location_name || `Location ${run.location_id}`}</td>
|
||||
<td>${timeInSeconds.toFixed(3)}s</td>
|
||||
<td>${new Date(run.created_at).toLocaleDateString('de-DE')} ${new Date(run.created_at).toLocaleTimeString('de-DE')}</td>
|
||||
<td class="action-buttons">
|
||||
<button class="btn btn-small btn-danger" onclick="deleteRun('${run.id}')">Löschen</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
document.getElementById('dataContent').innerHTML = html;
|
||||
}
|
||||
|
||||
function displayLocationsTable(locations) {
|
||||
let html = '<div class="table-container"><table class="data-table">';
|
||||
html += '<thead><tr><th>ID</th><th>Name</th><th>Latitude</th><th>Longitude</th><th>Erstellt</th><th>Aktionen</th></tr></thead><tbody>';
|
||||
|
||||
locations.forEach(location => {
|
||||
html += `<tr>
|
||||
<td>${location.id}</td>
|
||||
<td>${location.name}</td>
|
||||
<td>${location.latitude}</td>
|
||||
<td>${location.longitude}</td>
|
||||
<td>${new Date(location.created_at).toLocaleDateString('de-DE')}</td>
|
||||
<td class="action-buttons">
|
||||
<button class="btn btn-small btn-danger" onclick="deleteLocation('${location.id}')">Löschen</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
document.getElementById('dataContent').innerHTML = html;
|
||||
}
|
||||
|
||||
function displayAdminUsersTable(users) {
|
||||
let html = '<div class="table-container"><table class="data-table">';
|
||||
html += '<thead><tr><th>ID</th><th>Benutzername</th><th>Access Level</th><th>Aktiv</th><th>Letzter Login</th><th>Aktionen</th></tr></thead><tbody>';
|
||||
|
||||
users.forEach(user => {
|
||||
const isCurrentUser = user.id === currentUser.id;
|
||||
html += `<tr>
|
||||
<td>${user.id}</td>
|
||||
<td>${user.username} ${isCurrentUser ? '(Du)' : ''}</td>
|
||||
<td>Level ${user.access_level}</td>
|
||||
<td>${user.is_active ? '✅' : '❌'}</td>
|
||||
<td>${user.last_login ? new Date(user.last_login).toLocaleDateString('de-DE') : 'Nie'}</td>
|
||||
<td class="action-buttons">
|
||||
${!isCurrentUser ? `<button class="btn btn-small btn-danger" onclick="deleteAdminUser(${user.id})">Löschen</button>` : ''}
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
document.getElementById('dataContent').innerHTML = html;
|
||||
}
|
||||
|
||||
function loadSystemInfo() {
|
||||
document.getElementById('dataContent').innerHTML = `
|
||||
<div style="padding: 20px;">
|
||||
<h4>Server-Informationen</h4>
|
||||
<p><strong>Node.js Version:</strong> ${navigator.userAgent}</p>
|
||||
<p><strong>Aktuelle Zeit:</strong> ${new Date().toLocaleString('de-DE')}</p>
|
||||
<p><strong>Angemeldeter Benutzer:</strong> ${currentUser.username} (Level ${currentUser.access_level})</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function filterData() {
|
||||
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
||||
if (!currentData) return;
|
||||
|
||||
let filteredData = currentData.filter(item => {
|
||||
return Object.values(item).some(value =>
|
||||
value && value.toString().toLowerCase().includes(searchTerm)
|
||||
);
|
||||
});
|
||||
|
||||
switch(currentDataType) {
|
||||
case 'players':
|
||||
displayPlayersTable(filteredData);
|
||||
break;
|
||||
case 'runs':
|
||||
displayRunsTable(filteredData);
|
||||
break;
|
||||
case 'locations':
|
||||
displayLocationsTable(filteredData);
|
||||
break;
|
||||
case 'adminusers':
|
||||
displayAdminUsersTable(filteredData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshData() {
|
||||
switch(currentDataType) {
|
||||
case 'players':
|
||||
loadPlayers();
|
||||
break;
|
||||
case 'runs':
|
||||
loadRuns();
|
||||
break;
|
||||
case 'locations':
|
||||
loadLocations();
|
||||
break;
|
||||
case 'adminusers':
|
||||
loadAdminUsers();
|
||||
break;
|
||||
case 'system':
|
||||
loadSystemInfo();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function showAddModal() {
|
||||
// Implementierung für Add Modal je nach Datentyp
|
||||
document.getElementById('addModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.querySelectorAll('.modal').forEach(modal => {
|
||||
modal.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function handleAddSubmit(e) {
|
||||
e.preventDefault();
|
||||
// Implementierung für das Hinzufügen von Daten
|
||||
closeModal();
|
||||
}
|
||||
|
||||
async function deletePlayer(id) {
|
||||
if (await confirmDelete(`Spieler mit ID ${id} löschen?`)) {
|
||||
try {
|
||||
const response = await fetch(`/api/admin-players/${id}`, { method: 'DELETE' });
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showSuccess('Spieler erfolgreich gelöscht');
|
||||
loadPlayers();
|
||||
} else {
|
||||
showError('Fehler beim Löschen des Spielers');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete failed:', error);
|
||||
showError('Fehler beim Löschen des Spielers');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRun(id) {
|
||||
if (await confirmDelete(`Lauf mit ID ${id} löschen?`)) {
|
||||
try {
|
||||
const response = await fetch(`/api/admin-runs/${id}`, { method: 'DELETE' });
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showSuccess('Lauf erfolgreich gelöscht');
|
||||
loadRuns();
|
||||
} else {
|
||||
showError('Fehler beim Löschen des Laufs');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete failed:', error);
|
||||
showError('Fehler beim Löschen des Laufs');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteLocation(id) {
|
||||
if (await confirmDelete(`Standort mit ID ${id} löschen?`)) {
|
||||
try {
|
||||
const response = await fetch(`/api/admin-locations/${id}`, { method: 'DELETE' });
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showSuccess('Standort erfolgreich gelöscht');
|
||||
loadLocations();
|
||||
} else {
|
||||
showError('Fehler beim Löschen des Standorts');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete failed:', error);
|
||||
showError('Fehler beim Löschen des Standorts');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAdminUser(id) {
|
||||
if (await confirmDelete(`Admin-Benutzer mit ID ${id} löschen?`)) {
|
||||
try {
|
||||
const response = await fetch(`/api/admin-adminusers/${id}`, { method: 'DELETE' });
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showSuccess('Admin-Benutzer erfolgreich gelöscht');
|
||||
loadAdminUsers();
|
||||
} else {
|
||||
showError('Fehler beim Löschen des Admin-Benutzers');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete failed:', error);
|
||||
showError('Fehler beim Löschen des Admin-Benutzers');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(message) {
|
||||
return new Promise((resolve) => {
|
||||
document.getElementById('confirmMessage').textContent = message;
|
||||
document.getElementById('confirmModal').style.display = 'block';
|
||||
|
||||
document.getElementById('confirmYes').onclick = () => {
|
||||
closeModal();
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
document.getElementById('confirmNo').onclick = () => {
|
||||
closeModal();
|
||||
resolve(false);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
const messageDiv = document.getElementById('modalMessage');
|
||||
messageDiv.textContent = message;
|
||||
messageDiv.className = 'message success';
|
||||
messageDiv.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
messageDiv.style.display = 'none';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const messageDiv = document.getElementById('modalMessage');
|
||||
messageDiv.textContent = message;
|
||||
messageDiv.className = 'message error';
|
||||
messageDiv.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
messageDiv.style.display = 'none';
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -284,7 +284,7 @@
|
||||
if (result.success) {
|
||||
showSuccess('✅ Anmeldung erfolgreich! Weiterleitung...');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/generator';
|
||||
window.location.href = '/admin-dashboard';
|
||||
}, 1000);
|
||||
} else {
|
||||
showError(result.message || 'Anmeldung fehlgeschlagen');
|
||||
|
||||
278
routes/api.js
278
routes/api.js
@@ -152,7 +152,7 @@ router.post('/login', async (req, res) => {
|
||||
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT id, username, password_hash FROM adminusers WHERE username = $1 AND is_active = true',
|
||||
'SELECT id, username, password_hash, access_level FROM adminusers WHERE username = $1 AND is_active = true',
|
||||
[username]
|
||||
);
|
||||
|
||||
@@ -176,6 +176,7 @@ router.post('/login', async (req, res) => {
|
||||
// Session setzen
|
||||
req.session.userId = user.id;
|
||||
req.session.username = user.username;
|
||||
req.session.accessLevel = user.access_level;
|
||||
|
||||
// Session speichern
|
||||
req.session.save((err) => {
|
||||
@@ -192,7 +193,8 @@ router.post('/login', async (req, res) => {
|
||||
message: 'Erfolgreich angemeldet',
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username
|
||||
username: user.username,
|
||||
access_level: user.access_level
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1307,4 +1309,276 @@ router.post('/link-by-rfid', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// ADMIN DASHBOARD ROUTES
|
||||
// ============================================================================
|
||||
|
||||
// Middleware für Admin-Authentifizierung
|
||||
function requireAdminAuth(req, res, next) {
|
||||
if (!req.session.userId) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Authentifizierung erforderlich'
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// Middleware für Level 2 Zugriff
|
||||
function requireLevel2Access(req, res, next) {
|
||||
if (!req.session.userId || req.session.accessLevel < 2) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Insufficient access level'
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// Session-Check für Dashboard
|
||||
router.get('/check-session', (req, res) => {
|
||||
if (req.session.userId) {
|
||||
res.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: req.session.userId,
|
||||
username: req.session.username,
|
||||
access_level: req.session.accessLevel || 1
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
message: 'Not authenticated'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Admin Statistiken
|
||||
router.get('/admin-stats', requireAdminAuth, async (req, res) => {
|
||||
try {
|
||||
const playersResult = await pool.query('SELECT COUNT(*) FROM players');
|
||||
const runsResult = await pool.query('SELECT COUNT(*) FROM times');
|
||||
const locationsResult = await pool.query('SELECT COUNT(*) FROM locations');
|
||||
const adminUsersResult = await pool.query('SELECT COUNT(*) FROM adminusers');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
players: parseInt(playersResult.rows[0].count),
|
||||
runs: parseInt(runsResult.rows[0].count),
|
||||
locations: parseInt(locationsResult.rows[0].count),
|
||||
adminUsers: parseInt(adminUsersResult.rows[0].count)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading admin stats:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Fehler beim Laden der Statistiken'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Admin Spieler-Verwaltung
|
||||
router.get('/admin-players', requireAdminAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
p.*,
|
||||
COALESCE(CONCAT(p.firstname, ' ', p.lastname), p.firstname, p.lastname) as full_name,
|
||||
CASE WHEN p.supabase_user_id IS NOT NULL THEN true ELSE false END as has_supabase_link
|
||||
FROM players p
|
||||
ORDER BY p.created_at DESC
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading players:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Fehler beim Laden der Spieler'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/admin-players/:id', requireAdminAuth, async (req, res) => {
|
||||
const playerId = req.params.id;
|
||||
|
||||
try {
|
||||
// Erst alle zugehörigen Zeiten löschen
|
||||
await pool.query('DELETE FROM times WHERE player_id = $1', [playerId]);
|
||||
|
||||
// Dann den Spieler löschen
|
||||
const result = await pool.query('DELETE FROM players WHERE id = $1', [playerId]);
|
||||
|
||||
if (result.rowCount > 0) {
|
||||
res.json({ success: true, message: 'Spieler erfolgreich gelöscht' });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Spieler nicht gefunden' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting player:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Fehler beim Löschen des Spielers'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Admin Läufe-Verwaltung
|
||||
router.get('/admin-runs', requireAdminAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
t.id,
|
||||
t.player_id,
|
||||
t.location_id,
|
||||
t.recorded_time,
|
||||
EXTRACT(EPOCH FROM t.recorded_time) as time_seconds,
|
||||
t.created_at,
|
||||
COALESCE(CONCAT(p.firstname, ' ', p.lastname), p.firstname, p.lastname) as player_name,
|
||||
l.name as location_name
|
||||
FROM times t
|
||||
LEFT JOIN players p ON t.player_id = p.id
|
||||
LEFT JOIN locations l ON t.location_id = l.id
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT 1000
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading runs:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Fehler beim Laden der Läufe'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/admin-runs/:id', requireAdminAuth, async (req, res) => {
|
||||
const runId = req.params.id;
|
||||
|
||||
try {
|
||||
const result = await pool.query('DELETE FROM times WHERE id = $1', [runId]);
|
||||
|
||||
if (result.rowCount > 0) {
|
||||
res.json({ success: true, message: 'Lauf erfolgreich gelöscht' });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Lauf nicht gefunden' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting run:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Fehler beim Löschen des Laufs'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Admin Standort-Verwaltung
|
||||
router.get('/admin-locations', requireAdminAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT * FROM locations ORDER BY name');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading locations:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Fehler beim Laden der Standorte'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/admin-locations/:id', requireAdminAuth, async (req, res) => {
|
||||
const locationId = req.params.id;
|
||||
|
||||
try {
|
||||
// Prüfen ob noch Läufe an diesem Standort existieren
|
||||
const timesResult = await pool.query('SELECT COUNT(*) FROM times WHERE location_id = $1', [locationId]);
|
||||
const timesCount = parseInt(timesResult.rows[0].count);
|
||||
|
||||
if (timesCount > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Standort kann nicht gelöscht werden. Es existieren noch ${timesCount} Läufe an diesem Standort.`
|
||||
});
|
||||
}
|
||||
|
||||
const result = await pool.query('DELETE FROM locations WHERE id = $1', [locationId]);
|
||||
|
||||
if (result.rowCount > 0) {
|
||||
res.json({ success: true, message: 'Standort erfolgreich gelöscht' });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Standort nicht gefunden' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting location:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Fehler beim Löschen des Standorts'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Admin-Benutzer-Verwaltung
|
||||
router.get('/admin-adminusers', requireAdminAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT id, username, access_level, is_active, created_at, last_login
|
||||
FROM adminusers
|
||||
ORDER BY created_at DESC
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading admin users:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Fehler beim Laden der Admin-Benutzer'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/admin-adminusers/:id', requireAdminAuth, async (req, res) => {
|
||||
const userId = req.params.id;
|
||||
|
||||
// Verhindern, dass sich selbst löscht
|
||||
if (parseInt(userId) === req.session.userId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Sie können sich nicht selbst löschen'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await pool.query('DELETE FROM adminusers WHERE id = $1', [userId]);
|
||||
|
||||
if (result.rowCount > 0) {
|
||||
res.json({ success: true, message: 'Admin-Benutzer erfolgreich gelöscht' });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Admin-Benutzer nicht gefunden' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting admin user:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Fehler beim Löschen des Admin-Benutzers'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = { router, requireApiKey };
|
||||
|
||||
18
server.js
18
server.js
@@ -103,11 +103,27 @@ app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||
});
|
||||
|
||||
/**
|
||||
* Admin Dashboard Page
|
||||
* Hauptdashboard für Admin-Benutzer
|
||||
*/
|
||||
app.get('/admin-dashboard', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public', 'admin-dashboard.html'));
|
||||
});
|
||||
|
||||
/**
|
||||
* Admin Generator Page
|
||||
* Geschützte Seite für die Lizenz-Generierung
|
||||
* Geschützte Seite für die Lizenz-Generierung (Level 2 Zugriff erforderlich)
|
||||
*/
|
||||
app.get('/generator', requireWebAuth, (req, res) => {
|
||||
// Prüfe Zugriffslevel für Generator
|
||||
if (req.session.accessLevel < 2) {
|
||||
return res.status(403).send(`
|
||||
<h1>Zugriff verweigert</h1>
|
||||
<p>Sie benötigen Level 2 Zugriff für den Lizenzgenerator.</p>
|
||||
<a href="/admin-dashboard">Zurück zum Dashboard</a>
|
||||
`);
|
||||
}
|
||||
res.sendFile(path.join(__dirname, 'public', 'generator.html'));
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user