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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
đ„ 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
+
+
+
+
+
+
+
+
Daten
+
+
+
+
+
+
+
+
+
+
+
+
+
+
×
+
Element hinzufĂŒgen
+
+
+
+
+
+
+
+
×
+
BestÀtigung
+
Sind Sie sicher?
+
+
+
+
+
+
+
+
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'));
});