From 1a377517a6eab88e24bd8a74e754a01cbb53bc6b Mon Sep 17 00:00:00 2001 From: Carsten Graf Date: Wed, 3 Sep 2025 17:13:18 +0200 Subject: [PATCH] AdminDashboard --- public/admin-dashboard.html | 904 ++++++++++++++++++++++++++++++++++++ public/adminlogin.html | 2 +- routes/api.js | 278 ++++++++++- server.js | 18 +- 4 files changed, 1198 insertions(+), 4 deletions(-) create mode 100644 public/admin-dashboard.html diff --git a/public/admin-dashboard.html b/public/admin-dashboard.html new file mode 100644 index 0000000..e8b4219 --- /dev/null +++ b/public/admin-dashboard.html @@ -0,0 +1,904 @@ + + + + + + Admin Dashboard - NinjaCross + + + +
+

đŸ›Ąïž Admin Dashboard

+ +
+ +
+ +
+
+
-
+
Spieler
+
+
+
-
+
LĂ€ufe
+
+
+
-
+
Standorte
+
+
+
-
+
Admin-Benutzer
+
+
+ + +
+ +
+

đŸ‘„ Benutzer-Verwaltung

+

Verwalte Supabase-Benutzer und deren RFID-VerknĂŒpfungen

+ +
+ + +
+

🏃 Spieler-Verwaltung

+

Verwalte Spieler und deren RFID-UIDs

+ +
+ + +
+

⏱ LĂ€ufe-Verwaltung

+

Zeige und lösche LÀufe

+ +
+ + +
+

📍 Standort-Verwaltung

+

Verwalte Standorte und deren Koordinaten

+ +
+ + +
+

🔐 Admin-Benutzer

+

Verwalte Admin-Benutzer und Zugriffsrechte

+ +
+ + +
+

📊 System-Informationen

+

Server-Status und Systemdaten

+ +
+
+ + + +
+ + + + + + + + + diff --git a/public/adminlogin.html b/public/adminlogin.html index e0be0d9..6fc8c04 100644 --- a/public/adminlogin.html +++ b/public/adminlogin.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'); diff --git a/routes/api.js b/routes/api.js index 708ac49..1b64ec4 100644 --- a/routes/api.js +++ b/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 }; diff --git a/server.js b/server.js index 3c7c9e0..c72f623 100644 --- a/server.js +++ b/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(` +

Zugriff verweigert

+

Sie benötigen Level 2 Zugriff fĂŒr den Lizenzgenerator.

+ ZurĂŒck zum Dashboard + `); + } res.sendFile(path.join(__dirname, 'public', 'generator.html')); });