// routes/api.js const express = require('express'); const { Pool } = require('pg'); const bcrypt = require('bcrypt'); const { start } = require('repl'); const router = express.Router(); // PostgreSQL Pool mit .env Konfiguration const pool = new Pool({ host: process.env.DB_HOST, port: process.env.DB_PORT, database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false }); // Fehlerbehandlung für Pool pool.on('error', (err) => { console.error('PostgreSQL Pool Fehler:', err); }); // Helper function to convert time string to seconds function convertTimeToSeconds(timeStr) { if (!timeStr) return 0; // Handle different time formats if (typeof timeStr === 'string') { // Handle MM:SS.mmm format if (/^\d{1,2}:\d{2}\.\d{3}$/.test(timeStr)) { const [minutes, secondsWithMs] = timeStr.split(':'); const [seconds, milliseconds] = secondsWithMs.split('.'); return parseInt(minutes) * 60 + parseInt(seconds) + parseInt(milliseconds) / 1000; } // Handle HH:MM:SS.mmm format if (/^\d{1,2}:\d{2}:\d{2}\.\d{3}$/.test(timeStr)) { const [hours, minutes, secondsWithMs] = timeStr.split(':'); const [seconds, milliseconds] = secondsWithMs.split('.'); return parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(seconds) + parseInt(milliseconds) / 1000; } // Handle HH:MM:SS format if (/^\d{1,2}:\d{2}:\d{2}$/.test(timeStr)) { const [hours, minutes, seconds] = timeStr.split(':'); return parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(seconds); } // Handle MM:SS format if (/^\d{1,2}:\d{2}$/.test(timeStr)) { const [minutes, seconds] = timeStr.split(':'); return parseInt(minutes) * 60 + parseInt(seconds); } } // If it's already a number (seconds) if (typeof timeStr === 'number') { return timeStr; } return 0; } // Helper function to convert PostgreSQL interval to seconds function convertIntervalToSeconds(interval) { if (!interval) return 0; // PostgreSQL interval format: "HH:MM:SS" or "MM:SS" or just seconds if (typeof interval === 'string') { const parts = interval.split(':'); if (parts.length === 3) { // HH:MM:SS format const [hours, minutes, seconds] = parts; return parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseFloat(seconds); } else if (parts.length === 2) { // MM:SS format const [minutes, seconds] = parts; return parseInt(minutes) * 60 + parseFloat(seconds); } } // If it's already a number (seconds) if (typeof interval === 'number') { return interval; } return 0; } // Middleware für API-Key Authentifizierung async function requireApiKey(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ success: false, message: 'API-Key erforderlich. Verwenden Sie: Authorization: Bearer YOUR_API_KEY' }); } const apiKey = authHeader.substring(7); // "Bearer " entfernen try { // API-Key in der Datenbank validieren const result = await pool.query( `SELECT id, description, standorte, created_at, expires_at, is_active FROM api_tokens WHERE token = $1 AND is_active = true`, [apiKey] ); if (result.rows.length === 0) { return res.status(401).json({ success: false, message: 'Ungültiger oder inaktiver API-Key' }); } const tokenData = result.rows[0]; // Prüfen ob Token abgelaufen ist if (tokenData.expires_at && new Date() > new Date(tokenData.expires_at)) { return res.status(401).json({ success: false, message: 'API-Key ist abgelaufen' }); } // Token-Daten für weitere Verwendung speichern req.apiToken = tokenData; next(); } catch (error) { console.error('Fehler bei API-Key Validierung:', error); res.status(500).json({ success: false, message: 'Fehler bei der API-Key Validierung' }); } } // Login-Route (bleibt für Web-Interface) router.post('/login', async (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ success: false, message: 'Benutzername und Passwort sind erforderlich' }); } try { const result = await pool.query( 'SELECT id, username, password_hash, access_level FROM adminusers WHERE username = $1 AND is_active = true', [username] ); if (result.rows.length === 0) { return res.status(401).json({ success: false, message: 'Ungültige Anmeldedaten' }); } const user = result.rows[0]; const isValidPassword = await bcrypt.compare(password, user.password_hash); if (!isValidPassword) { return res.status(401).json({ success: false, message: 'Ungültige Anmeldedaten' }); } // Session setzen req.session.userId = user.id; req.session.username = user.username; req.session.accessLevel = user.access_level; // Session speichern req.session.save((err) => { if (err) { console.error('Fehler beim Speichern der Session:', err); return res.status(500).json({ success: false, message: 'Fehler beim Speichern der Session' }); } res.json({ success: true, message: 'Erfolgreich angemeldet', user: { id: user.id, username: user.username, access_level: user.access_level } }); }); } catch (error) { console.error('Fehler bei der Anmeldung:', error); res.status(500).json({ success: false, message: 'Interner Serverfehler bei der Anmeldung' }); } }); // Logout-Route (bleibt für Web-Interface) router.post('/logout', (req, res) => { req.session.destroy((err) => { if (err) { return res.status(500).json({ success: false, message: 'Fehler beim Abmelden' }); } res.json({ success: true, message: 'Erfolgreich abgemeldet' }); }); }); // API Endpunkt zum Speichern der Tokens (geschützt mit API-Key) router.post('/save-token', requireApiKey, async (req, res) => { const { token, description, standorte } = req.body; // Validierung if (!token) { return res.status(400).json({ success: false, message: 'Token ist erforderlich' }); } try { // Prüfen ob Token bereits existiert const existingToken = await pool.query( 'SELECT id FROM api_tokens WHERE token = $1', [token] ); if (existingToken.rows.length > 0) { return res.status(409).json({ success: false, message: 'Token existiert bereits in der Datenbank' }); } // Token in Datenbank einfügen const result = await pool.query( `INSERT INTO api_tokens (token, description, standorte, expires_at) VALUES ($1, $2, $3, $4) RETURNING id, created_at`, [ token, description, standorte, new Date(Date.now() + 2 * 365 * 24 * 60 * 60 * 1000) // 2 Jahre gültig ] ); res.json({ success: true, message: 'Token erfolgreich als API-Token gespeichert', data: { id: result.rows[0].id, created_at: result.rows[0].created_at } }); } catch (error) { console.error('Fehler beim Speichern des Tokens:', error); // Spezifische Fehlermeldungen if (error.code === '23505') { // Duplicate key res.status(409).json({ success: false, message: 'Token existiert bereits' }); } else if (error.code === 'ECONNREFUSED') { res.status(503).json({ success: false, message: 'Datenbankverbindung fehlgeschlagen' }); } else { res.status(500).json({ success: false, message: 'Interner Serverfehler beim Speichern des Tokens' }); } } }); // API Endpunkt zum Abrufen aller Tokens (geschützt mit API-Key) router.get('/tokens', requireApiKey, async (req, res) => { try { const result = await pool.query( `SELECT id, token, description, standorte, created_at, expires_at, is_active FROM api_tokens ORDER BY created_at DESC` ); res.json({ success: true, data: result.rows }); } catch (error) { console.error('Fehler beim Abrufen der Tokens:', error); res.status(500).json({ success: false, message: 'Fehler beim Abrufen der Tokens' }); } }); // API Endpunkt zum Validieren eines Tokens (geschützt mit API-Key) router.post('/validate-token', requireApiKey, async (req, res) => { const { token } = req.body; if (!token) { return res.status(400).json({ success: false, message: 'Token ist erforderlich' }); } try { const result = await pool.query( `SELECT id, description, standorte, created_at, expires_at, is_active FROM api_tokens WHERE token = $1 AND is_active = true`, [token] ); if (result.rows.length === 0) { return res.status(401).json({ success: false, message: 'Ungültiger oder inaktiver Token' }); } const tokenData = result.rows[0]; // Prüfen ob Token abgelaufen ist if (tokenData.expires_at && new Date() > new Date(tokenData.expires_at)) { return res.status(401).json({ success: false, message: 'Token ist abgelaufen' }); } res.json({ success: true, message: 'Token ist gültig', data: { id: tokenData.id, description: tokenData.description, standorte: tokenData.standorte, created_at: tokenData.created_at, expires_at: tokenData.expires_at } }); } catch (error) { console.error('Fehler bei Token-Validierung:', error); res.status(500).json({ success: false, message: 'Fehler bei der Token-Validierung' }); } }); // Neue API-Route für Standortverwaltung (geschützt mit API-Key) router.post('/create-location', requireApiKey, async (req, res) => { const { name, lat, lon } = req.body; // Validierung if (!name || lat === undefined || lon === undefined) { return res.status(400).json({ success: false, message: 'Name, Breitengrad und Längengrad sind erforderlich' }); } try { // Prüfen ob Standort bereits existiert const existingLocation = await pool.query( 'SELECT id FROM locations WHERE name = $1', [name] ); if (existingLocation.rows.length > 0) { return res.status(409).json({ success: false, message: 'Standort existiert bereits in der Datenbank' }); } // Standort in Datenbank einfügen const result = await pool.query( `INSERT INTO locations (name, latitude, longitude, created_at) VALUES ($1, $2, $3, $4) RETURNING id, created_at`, [name, lat, lon, new Date()] ); res.json({ success: true, message: 'Standort erfolgreich gespeichert', data: { id: result.rows[0].id, name: name, latitude: lat, longitude: lon, created_at: result.rows[0].created_at } }); } catch (error) { console.error('Fehler beim Speichern des Standorts:', error); // Spezifische Fehlermeldungen if (error.code === '23505') { // Duplicate key res.status(409).json({ success: false, message: 'Standort existiert bereits' }); } else if (error.code === 'ECONNREFUSED') { res.status(503).json({ success: false, message: 'Datenbankverbindung fehlgeschlagen' }); } else { res.status(500).json({ success: false, message: 'Interner Serverfehler beim Speichern des Standorts' }); } } }); // API Endpunkt zum Abrufen aller Standorte (geschützt mit API-Key) router.get('/locations', requireApiKey, async (req, res) => { try { const result = await pool.query( `SELECT id, name, latitude, longitude, time_threshold, created_at FROM locations ORDER BY created_at DESC` ); res.json({ success: true, data: result.rows }); } catch (error) { console.error('Fehler beim Abrufen der Standorte:', error); res.status(500).json({ success: false, message: 'Fehler beim Abrufen der Standorte' }); } }); // API Endpunkt zum Aktualisieren des Zeit-Schwellenwerts für einen Standort router.put('/locations/:id/threshold', requireApiKey, async (req, res) => { const { id } = req.params; const { time_threshold } = req.body; // Validierung if (!time_threshold) { return res.status(400).json({ success: false, message: 'time_threshold ist erforderlich' }); } try { // Prüfen ob Standort existiert const existingLocation = await pool.query( 'SELECT id, name FROM locations WHERE id = $1', [id] ); if (existingLocation.rows.length === 0) { return res.status(404).json({ success: false, message: 'Standort nicht gefunden' }); } // Schwellenwert aktualisieren const result = await pool.query( 'UPDATE locations SET time_threshold = $1 WHERE id = $2 RETURNING id, name, time_threshold', [time_threshold, id] ); res.json({ success: true, message: 'Schwellenwert erfolgreich aktualisiert', data: { id: result.rows[0].id, name: result.rows[0].name, time_threshold: result.rows[0].time_threshold } }); } catch (error) { console.error('Fehler beim Aktualisieren des Schwellenwerts:', error); res.status(500).json({ success: false, message: 'Interner Serverfehler beim Aktualisieren des Schwellenwerts' }); } }); // Neue Route zum Generieren eines API-Keys (nur für authentifizierte Web-Benutzer) router.post('/generate-api-key', async (req, res) => { // Diese Route bleibt für das Web-Interface verfügbar // Hier können Sie einen neuen API-Key generieren try { // Generiere einen zufälligen API-Key const crypto = require('crypto'); const apiKey = crypto.randomBytes(32).toString('hex'); // Speichere den API-Key in der Datenbank const result = await pool.query( `INSERT INTO api_tokens (token, description, standorte, expires_at) VALUES ($1, $2, $3, $4) RETURNING id, created_at`, [ apiKey, req.body.description || 'Generierter API-Key', req.body.standorte || '', new Date(Date.now() + 2 * 365 * 24 * 60 * 60 * 1000) // 2 Jahre gültig ] ); res.json({ success: true, message: 'API-Key erfolgreich generiert', data: { id: result.rows[0].id, apiKey: apiKey, created_at: result.rows[0].created_at } }); } catch (error) { console.error('Fehler beim Generieren des API-Keys:', error); res.status(500).json({ success: false, message: 'Fehler beim Generieren des API-Keys' }); } }); // Web-authenticated endpoints for location management (for frontend use) // These endpoints use session authentication instead of API key authentication // Web-authenticated endpoint for creating locations router.post('/web/create-location', async (req, res) => { // Check if user is authenticated via web session if (!req.session || !req.session.userId) { return res.status(401).json({ success: false, message: 'Nicht angemeldet. Bitte melden Sie sich an.' }); } const { name, lat, lon } = req.body; // Validierung if (!name || lat === undefined || lon === undefined) { return res.status(400).json({ success: false, message: 'Name, Breitengrad und Längengrad sind erforderlich' }); } try { // Prüfen ob Standort bereits existiert const existingLocation = await pool.query( 'SELECT id FROM locations WHERE name = $1', [name] ); if (existingLocation.rows.length > 0) { return res.status(409).json({ success: false, message: 'Standort existiert bereits in der Datenbank' }); } // Standort in Datenbank einfügen const result = await pool.query( `INSERT INTO locations (name, latitude, longitude, created_at) VALUES ($1, $2, $3, $4) RETURNING id, created_at`, [name, lat, lon, new Date()] ); res.json({ success: true, message: 'Standort erfolgreich gespeichert', data: { id: result.rows[0].id, name: name, latitude: lat, longitude: lon, created_at: result.rows[0].created_at } }); } catch (error) { console.error('Fehler beim Speichern des Standorts:', error); // Spezifische Fehlermeldungen if (error.code === '23505') { // Duplicate key res.status(409).json({ success: false, message: 'Standort existiert bereits' }); } else if (error.code === 'ECONNREFUSED') { res.status(503).json({ success: false, message: 'Datenbankverbindung fehlgeschlagen' }); } else { res.status(500).json({ success: false, message: 'Interner Serverfehler beim Speichern des Standorts' }); } } }); // Web-authenticated endpoint for saving tokens router.post('/web/save-token', async (req, res) => { // Check if user is authenticated via web session if (!req.session || !req.session.userId) { return res.status(401).json({ success: false, message: 'Nicht angemeldet. Bitte melden Sie sich an.' }); } const { token, description, standorte } = req.body; if (!token) { return res.status(400).json({ success: false, message: 'Token ist erforderlich' }); } try { // Prüfen ob Token bereits existiert const existingToken = await pool.query( 'SELECT id FROM api_tokens WHERE token = $1', [token] ); if (existingToken.rows.length > 0) { return res.status(409).json({ success: false, message: 'Token existiert bereits in der Datenbank' }); } // Token in Datenbank einfügen const result = await pool.query( `INSERT INTO api_tokens (token, description, standorte, created_at) VALUES ($1, $2, $3, $4) RETURNING id, created_at`, [token, description || 'API-Token', standorte || '', new Date()] ); res.json({ success: true, message: 'Token erfolgreich gespeichert', data: { id: result.rows[0].id, token: token, description: description || 'API-Token', standorte: standorte || '', created_at: result.rows[0].created_at } }); } catch (error) { console.error('Fehler beim Speichern des Tokens:', error); res.status(500).json({ success: false, message: 'Interner Serverfehler beim Speichern des Tokens' }); } }); // API Endpunkt für GetLocations (geschützt mit API-Key) router.get('/get-locations', requireApiKey, async (req, res) => { try { const result = await pool.query('SELECT * FROM "GetLocations"'); res.json({ success: true, data: result.rows }); } catch (error) { console.error('Fehler beim Abrufen der GetLocations:', error); res.status(500).json({ success: false, message: 'Fehler beim Abrufen der GetLocations' }); } }); // API Entpunkt zum erstellen eines neuen Spielers router.post('/create-player', requireApiKey, async (req, res) => { const { firstname, lastname, birthdate, rfiduid } = req.body; // Validierung if (!firstname || !lastname || !birthdate || !rfiduid) { return res.status(400).json({ success: false, message: 'Firstname, Lastname, Birthdate und RFIDUID sind erforderlich' }); } try { // Prüfen ob Spieler bereits existiert const existingPlayer = await pool.query( 'SELECT id FROM players WHERE rfiduid = $1', [rfiduid] ); if (existingPlayer.rows.length > 0) { return res.status(409).json({ success: false, message: 'Spieler existiert bereits in der Datenbank' }); } // Spieler in Datenbank einfügen const result = await pool.query( `INSERT INTO players (firstname, lastname, birthdate, rfiduid, created_at) VALUES ($1, $2, $3, $4, $5) RETURNING id, created_at`, [firstname, lastname, birthdate, rfiduid, new Date()] ); res.json({ success: true, message: 'Spieler erfolgreich gespeichert', data: { id: result.rows[0].id, firstname: firstname, lastname: lastname, birthdate: birthdate, rfiduid: rfiduid, created_at: result.rows[0].created_at } }); } catch (error) { console.error('Fehler beim Speichern des Spielers:', error); res.status(500).json({ success: false, message: 'Interner Serverfehler beim Speichern des Spielers' }); } }); // API Endpunkt zum erstellen einer neuen Zeit mit RFID UID und Location Name router.post('/create-time', requireApiKey, async (req, res) => { const { rfiduid, location_name, recorded_time } = req.body; // Validierung if (!rfiduid || !location_name || !recorded_time) { return res.status(400).json({ success: false, message: 'RFIDUID, Location_name und Recorded_time sind erforderlich' }); } try { // Spieler anhand der RFID UID finden const playerResult = await pool.query( 'SELECT id, firstname, lastname FROM players WHERE rfiduid = $1', [rfiduid] ); if (playerResult.rows.length === 0) { return res.status(404).json({ success: false, message: 'Spieler mit dieser RFID UID nicht gefunden' }); } const player = playerResult.rows[0]; const player_id = player.id; // Location anhand des Namens finden (inklusive time_threshold) const locationResult = await pool.query( 'SELECT id, name, time_threshold FROM locations WHERE name = $1', [location_name] ); if (locationResult.rows.length === 0) { return res.status(404).json({ success: false, message: `Standort '${location_name}' nicht gefunden` }); } const location = locationResult.rows[0]; const location_id = location.id; // Prüfen ob die Zeit über dem Schwellenwert liegt if (location.time_threshold) { // Konvertiere recorded_time zu Sekunden für Vergleich const recordedTimeSeconds = convertTimeToSeconds(recorded_time); const thresholdSeconds = convertIntervalToSeconds(location.time_threshold); if (recordedTimeSeconds < thresholdSeconds) { return res.status(400).json({ success: false, message: `Zeit ${recorded_time} liegt unter dem Schwellenwert von ${location.time_threshold} für diesen Standort`, data: { recorded_time: recorded_time, threshold: location.time_threshold, location_name: location_name } }); } } // Prüfen ob Zeit bereits existiert (optional - kann entfernt werden) const existingTime = await pool.query( 'SELECT id FROM times WHERE player_id = $1 AND location_id = $2 AND recorded_time = $3', [player_id, location_id, recorded_time] ); if (existingTime.rows.length > 0) { return res.status(409).json({ success: false, message: 'Zeit existiert bereits in der Datenbank' }); } // Zeit in Datenbank einfügen const result = await pool.query( `INSERT INTO times (player_id, location_id, recorded_time, created_at) VALUES ($1, $2, $3, $4) RETURNING id, created_at`, [player_id, location_id, recorded_time, new Date()] ); // WebSocket-Event senden für Live-Updates const io = req.app.get('io'); if (io) { // Hole die aktuelle Platzierung für diese Zeit const rankResult = await pool.query( `SELECT COUNT(*) + 1 as rank FROM times t1 WHERE t1.location_id = $1 AND t1.recorded_time < $2`, [location_id, recorded_time] ); const rank = rankResult.rows[0].rank; // Sende WebSocket-Event an alle verbundenen Clients io.emit('newTime', { id: result.rows[0].id, player_name: `${player.firstname} ${player.lastname}`, location_name: location.name, recorded_time: recorded_time, rank: rank, created_at: result.rows[0].created_at }); } res.json({ success: true, message: 'Zeit erfolgreich gespeichert', data: { id: result.rows[0].id, player_id: player_id, player_name: `${player.firstname} ${player.lastname}`, rfiduid: rfiduid, location_id: location_id, location_name: location.name, recorded_time: recorded_time, created_at: result.rows[0].created_at } }); } catch (error) { console.error('Fehler beim Speichern der Zeit:', error); res.status(500).json({ success: false, message: 'Interner Serverfehler beim Speichern der Zeit' }); } }); // API Endpunkt zum Überprüfen eines Benutzers anhand der RFID UID router.post('/users/find', requireApiKey, async (req, res) => { const { uid } = req.body; // Validierung if (!uid) { return res.status(400).json({ success: false, message: 'UID ist erforderlich' }); } try { // Spieler anhand der RFID UID finden const result = await pool.query( 'SELECT rfiduid, firstname, lastname, birthdate FROM players WHERE rfiduid = $1', [uid] ); if (result.rows.length === 0) { // Benutzer nicht gefunden - gibt leere UserData zurück return res.json({ success: true, data: { uid: "", firstname: "", lastname: "", alter: 0, exists: false } }); } const player = result.rows[0]; // Alter aus dem Geburtsdatum berechnen let age = 0; if (player.birthdate) { const today = new Date(); const birthDate = new Date(player.birthdate); age = today.getFullYear() - birthDate.getFullYear(); // Prüfen ob der Geburtstag dieses Jahr noch nicht war const monthDiff = today.getMonth() - birthDate.getMonth(); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { age--; } } res.json({ success: true, data: { uid: player.rfiduid, firstname: player.firstname, lastname: player.lastname, alter: age, exists: true } }); } catch (error) { console.error('Fehler beim Überprüfen des Benutzers:', error); res.status(500).json({ success: false, message: 'Interner Serverfehler beim Überprüfen des Benutzers' }); } }); // ============================================================================ // RFID LINKING & USER MANAGEMENT ENDPOINTS (No API Key required for dashboard) // ============================================================================ // Get all players for RFID linking (no auth required for dashboard) router.get('/players', async (req, res) => { try { const result = await pool.query( `SELECT id, firstname, lastname, birthdate, rfiduid, created_at FROM players ORDER BY created_at DESC` ); res.json(result.rows); } catch (error) { console.error('Fehler beim Abrufen der Spieler:', error); res.status(500).json({ success: false, message: 'Fehler beim Abrufen der Spieler' }); } }); // Create new player with optional Supabase user linking (no auth required for dashboard) router.post('/players', async (req, res) => { const { firstname, lastname, birthdate, rfiduid, supabase_user_id } = req.body; // Validierung if (!firstname || !lastname || !birthdate) { return res.status(400).json({ success: false, message: 'Firstname, Lastname und Birthdate sind erforderlich' }); } try { // Prüfen ob RFID UID bereits existiert (falls angegeben) if (rfiduid) { const existingRfid = await pool.query( 'SELECT id FROM players WHERE rfiduid = $1', [rfiduid] ); if (existingRfid.rows.length > 0) { return res.status(409).json({ success: false, message: 'RFID UID existiert bereits' }); } } // Prüfen ob Supabase User bereits verknüpft ist if (supabase_user_id) { const existingUser = await pool.query( 'SELECT id FROM players WHERE supabase_user_id = $1', [supabase_user_id] ); if (existingUser.rows.length > 0) { return res.status(409).json({ success: false, message: 'Dieser Benutzer ist bereits mit einem Spieler verknüpft' }); } } // Spieler in Datenbank einfügen const result = await pool.query( `INSERT INTO players (firstname, lastname, birthdate, rfiduid, supabase_user_id, created_at) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, created_at`, [firstname, lastname, birthdate, rfiduid, supabase_user_id, new Date()] ); res.json({ success: true, message: 'Spieler erfolgreich erstellt', data: { id: result.rows[0].id, firstname: firstname, lastname: lastname, birthdate: birthdate, rfiduid: rfiduid, supabase_user_id: supabase_user_id, created_at: result.rows[0].created_at } }); } catch (error) { console.error('Fehler beim Erstellen des Spielers:', error); res.status(500).json({ success: false, message: 'Interner Serverfehler beim Erstellen des Spielers' }); } }); // Link existing player to Supabase user (no auth required for dashboard) router.post('/link-player', async (req, res) => { const { player_id, supabase_user_id } = req.body; // Validierung if (!player_id || !supabase_user_id) { return res.status(400).json({ success: false, message: 'Player ID und Supabase User ID sind erforderlich' }); } try { // Prüfen ob Spieler existiert const playerExists = await pool.query( 'SELECT id, firstname, lastname FROM players WHERE id = $1', [player_id] ); if (playerExists.rows.length === 0) { return res.status(404).json({ success: false, message: 'Spieler nicht gefunden' }); } // Prüfen ob Supabase User bereits verknüpft ist const existingLink = await pool.query( 'SELECT id FROM players WHERE supabase_user_id = $1', [supabase_user_id] ); if (existingLink.rows.length > 0) { return res.status(409).json({ success: false, message: 'Dieser Benutzer ist bereits mit einem Spieler verknüpft' }); } // Verknüpfung erstellen const result = await pool.query( 'UPDATE players SET supabase_user_id = $1 WHERE id = $2 RETURNING id, firstname, lastname, rfiduid', [supabase_user_id, player_id] ); res.json({ success: true, message: 'Spieler erfolgreich verknüpft', data: result.rows[0] }); } catch (error) { console.error('Fehler beim Verknüpfen des Spielers:', error); res.status(500).json({ success: false, message: 'Interner Serverfehler beim Verknüpfen des Spielers' }); } }); // Get user times by Supabase user ID (no auth required for dashboard) router.get('/user-times/:supabase_user_id', async (req, res) => { const { supabase_user_id } = req.params; try { // Finde verknüpften Spieler const playerResult = await pool.query( 'SELECT id FROM players WHERE supabase_user_id = $1', [supabase_user_id] ); if (playerResult.rows.length === 0) { return res.json([]); // Noch keine Verknüpfung } const player_id = playerResult.rows[0].id; // Hole alle Zeiten für diesen Spieler mit Location-Namen const timesResult = await pool.query(` SELECT t.id, t.recorded_time, t.created_at, l.name as location_name, l.latitude, l.longitude FROM times t JOIN locations l ON t.location_id = l.id WHERE t.player_id = $1 ORDER BY t.created_at DESC `, [player_id]); res.json(timesResult.rows); } catch (error) { console.error('Fehler beim Abrufen der Benutzerzeiten:', error); res.status(500).json({ success: false, message: 'Fehler beim Abrufen der Benutzerzeiten' }); } }); // Get player info by Supabase user ID (no auth required for dashboard) router.get('/user-player/:supabase_user_id', async (req, res) => { const { supabase_user_id } = req.params; try { const result = await pool.query( 'SELECT id, firstname, lastname, birthdate, rfiduid FROM players WHERE supabase_user_id = $1', [supabase_user_id] ); if (result.rows.length === 0) { return res.status(404).json({ success: false, message: 'Kein verknüpfter Spieler gefunden' }); } res.json({ success: true, data: result.rows[0] }); } catch (error) { console.error('Fehler beim Abrufen der Spielerdaten:', error); res.status(500).json({ success: false, message: 'Fehler beim Abrufen der Spielerdaten' }); } }); // Link user by RFID UID (scanned from QR code) router.post('/link-by-rfid', async (req, res) => { const { rfiduid, supabase_user_id } = req.body; // Validierung if (!rfiduid || !supabase_user_id) { return res.status(400).json({ success: false, message: 'RFID UID und Supabase User ID sind erforderlich' }); } try { // Prüfen ob Spieler mit dieser RFID UID existiert const playerResult = await pool.query( 'SELECT id, firstname, lastname, rfiduid, supabase_user_id FROM players WHERE rfiduid = $1', [rfiduid] ); if (playerResult.rows.length === 0) { return res.status(404).json({ success: false, message: `Kein Spieler mit RFID UID '${rfiduid}' gefunden. Bitte erstelle zuerst einen Spieler mit dieser RFID UID.` }); } const player = playerResult.rows[0]; // Prüfen ob dieser Spieler bereits mit einem anderen Benutzer verknüpft ist if (player.supabase_user_id && player.supabase_user_id !== supabase_user_id) { return res.status(409).json({ success: false, message: 'Dieser Spieler ist bereits mit einem anderen Benutzer verknüpft' }); } // Prüfen ob dieser Benutzer bereits mit einem anderen Spieler verknüpft ist const existingLink = await pool.query( 'SELECT id, firstname, lastname FROM players WHERE supabase_user_id = $1 AND id != $2', [supabase_user_id, player.id] ); if (existingLink.rows.length > 0) { return res.status(409).json({ success: false, message: `Du bist bereits mit dem Spieler '${existingLink.rows[0].firstname} ${existingLink.rows[0].lastname}' verknüpft. Ein Benutzer kann nur mit einem Spieler verknüpft sein.` }); } // Verknüpfung erstellen (falls noch nicht vorhanden) if (!player.supabase_user_id) { await pool.query( 'UPDATE players SET supabase_user_id = $1 WHERE id = $2', [supabase_user_id, player.id] ); } res.json({ success: true, message: 'RFID erfolgreich verknüpft', data: { id: player.id, firstname: player.firstname, lastname: player.lastname, rfiduid: player.rfiduid } }); } catch (error) { console.error('Fehler beim Verknüpfen per RFID UID:', error); res.status(500).json({ success: false, message: 'Interner Serverfehler beim Verknüpfen per RFID UID' }); } }); // ============================================================================ // 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 };