diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 4e4bbbb..e0c744f 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -984,12 +984,11 @@ async function saveSettings() { const showInLeaderboard = document.getElementById('showInLeaderboard').checked; - // Update player settings - const response = await fetch(`/api/v1/private/update-player-settings`, { + // Update player settings using public endpoint (no API key needed) + const response = await fetch(`/api/v1/public/update-player-settings`, { method: 'POST', headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem('apiKey')}` + 'Content-Type': 'application/json' }, body: JSON.stringify({ player_id: currentPlayerId, diff --git a/routes/api.js b/routes/api.js index e72d571..2549ac6 100644 --- a/routes/api.js +++ b/routes/api.js @@ -24,7 +24,7 @@ pool.on('error', (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 @@ -33,44 +33,44 @@ function convertTimeToSeconds(timeStr) { 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; - + // Handle PostgreSQL interval object format if (typeof interval === 'object' && interval.seconds !== undefined) { return parseFloat(interval.seconds); } - + // PostgreSQL interval format: "HH:MM:SS" or "MM:SS" or just seconds if (typeof interval === 'string') { const parts = interval.split(':'); @@ -84,28 +84,28 @@ function convertIntervalToSeconds(interval) { 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' + 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( @@ -114,16 +114,16 @@ async function requireApiKey(req, res, next) { 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({ @@ -131,16 +131,16 @@ async function requireApiKey(req, res, next) { 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' + res.status(500).json({ + success: false, + message: 'Fehler bei der API-Key Validierung' }); } } @@ -152,52 +152,52 @@ async function requireApiKey(req, res, next) { // Login-Route (bleibt für Web-Interface) router.post('/v1/public/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' + 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' + return res.status(500).json({ + success: false, + message: 'Fehler beim Speichern der Session' }); } - + res.json({ success: true, message: 'Erfolgreich angemeldet', @@ -208,12 +208,12 @@ router.post('/v1/public/login', async (req, res) => { } }); }); - + } catch (error) { console.error('Fehler bei der Anmeldung:', error); - res.status(500).json({ - success: false, - message: 'Interner Serverfehler bei der Anmeldung' + res.status(500).json({ + success: false, + message: 'Interner Serverfehler bei der Anmeldung' }); } }); @@ -222,14 +222,14 @@ router.post('/v1/public/login', async (req, res) => { router.post('/v1/public/logout', (req, res) => { req.session.destroy((err) => { if (err) { - return res.status(500).json({ - success: false, - message: 'Fehler beim Abmelden' + return res.status(500).json({ + success: false, + message: 'Fehler beim Abmelden' }); } - res.json({ - success: true, - message: 'Erfolgreich abgemeldet' + res.json({ + success: true, + message: 'Erfolgreich abgemeldet' }); }); }); @@ -241,70 +241,70 @@ router.post('/v1/public/logout', (req, res) => { // API Endpunkt zum Speichern der Tokens (geschützt mit API-Key) router.post('/v1/private/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' + 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, + token, + description, standorte, new Date(Date.now() + 2 * 365 * 24 * 60 * 60 * 1000) // 2 Jahre gültig ] ); - - - res.json({ - success: true, + + + 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' + res.status(409).json({ + success: false, + message: 'Token existiert bereits' }); } else if (error.code === 'ECONNREFUSED') { - res.status(503).json({ - success: false, - message: 'Datenbankverbindung fehlgeschlagen' + res.status(503).json({ + success: false, + message: 'Datenbankverbindung fehlgeschlagen' }); } else { - res.status(500).json({ - success: false, + res.status(500).json({ + success: false, message: 'Interner Serverfehler beim Speichern des Tokens' }); } @@ -319,17 +319,17 @@ router.get('/v1/private/tokens', requireApiKey, async (req, res) => { 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' + res.status(500).json({ + success: false, + message: 'Fehler beim Abrufen der Tokens' }); } }); @@ -337,14 +337,14 @@ router.get('/v1/private/tokens', requireApiKey, async (req, res) => { // API Endpunkt zum Validieren eines Tokens (geschützt mit API-Key) router.post('/v1/private/validate-token', requireApiKey, async (req, res) => { const { token } = req.body; - + if (!token) { - return res.status(400).json({ - success: false, - message: 'Token ist erforderlich' + 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 @@ -352,16 +352,16 @@ router.post('/v1/private/validate-token', requireApiKey, async (req, res) => { 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({ @@ -369,7 +369,7 @@ router.post('/v1/private/validate-token', requireApiKey, async (req, res) => { message: 'Token ist abgelaufen' }); } - + res.json({ success: true, message: 'Token ist gültig', @@ -381,12 +381,12 @@ router.post('/v1/private/validate-token', requireApiKey, async (req, res) => { 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' + res.status(500).json({ + success: false, + message: 'Fehler bei der Token-Validierung' }); } }); @@ -394,29 +394,29 @@ router.post('/v1/private/validate-token', requireApiKey, async (req, res) => { // Neue API-Route für Standortverwaltung (geschützt mit API-Key) router.post('/v1/private/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' + 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) @@ -424,11 +424,11 @@ router.post('/v1/private/create-location', requireApiKey, async (req, res) => { RETURNING id, created_at`, [name, lat, lon, new Date()] ); - - - res.json({ - success: true, + + + res.json({ + success: true, message: 'Standort erfolgreich gespeichert', data: { id: result.rows[0].id, @@ -438,24 +438,24 @@ router.post('/v1/private/create-location', requireApiKey, async (req, res) => { 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' + res.status(409).json({ + success: false, + message: 'Standort existiert bereits' }); } else if (error.code === 'ECONNREFUSED') { - res.status(503).json({ - success: false, - message: 'Datenbankverbindung fehlgeschlagen' + res.status(503).json({ + success: false, + message: 'Datenbankverbindung fehlgeschlagen' }); } else { - res.status(500).json({ - success: false, + res.status(500).json({ + success: false, message: 'Interner Serverfehler beim Speichern des Standorts' }); } @@ -470,17 +470,17 @@ router.get('/v1/private/locations', requireApiKey, async (req, res) => { 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' + res.status(500).json({ + success: false, + message: 'Fehler beim Abrufen der Standorte' }); } }); @@ -489,39 +489,39 @@ router.get('/v1/private/locations', requireApiKey, async (req, res) => { router.put('/v1/private/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' + 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, + + + res.json({ + success: true, message: 'Schwellenwert erfolgreich aktualisiert', data: { id: result.rows[0].id, @@ -529,12 +529,12 @@ router.put('/v1/private/locations/:id/threshold', requireApiKey, async (req, res 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' + res.status(500).json({ + success: false, + message: 'Interner Serverfehler beim Aktualisieren des Schwellenwerts' }); } }); @@ -551,7 +551,7 @@ router.post('/v1/web/generate-api-key', async (req, res) => { // 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) @@ -564,7 +564,7 @@ router.post('/v1/web/generate-api-key', async (req, res) => { new Date(Date.now() + 2 * 365 * 24 * 60 * 60 * 1000) // 2 Jahre gültig ] ); - + res.json({ success: true, message: 'API-Key erfolgreich generiert', @@ -574,12 +574,12 @@ router.post('/v1/web/generate-api-key', async (req, res) => { 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' + res.status(500).json({ + success: false, + message: 'Fehler beim Generieren des API-Keys' }); } }); @@ -591,36 +591,36 @@ router.post('/v1/web/generate-api-key', async (req, res) => { router.post('/v1/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.' + 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' + 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) @@ -628,11 +628,11 @@ router.post('/v1/web/create-location', async (req, res) => { RETURNING id, created_at`, [name, lat, lon, new Date()] ); - - - res.json({ - success: true, + + + res.json({ + success: true, message: 'Standort erfolgreich gespeichert', data: { id: result.rows[0].id, @@ -642,24 +642,24 @@ router.post('/v1/web/create-location', async (req, res) => { 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' + res.status(409).json({ + success: false, + message: 'Standort existiert bereits' }); } else if (error.code === 'ECONNREFUSED') { - res.status(503).json({ - success: false, - message: 'Datenbankverbindung fehlgeschlagen' + res.status(503).json({ + success: false, + message: 'Datenbankverbindung fehlgeschlagen' }); } else { - res.status(500).json({ - success: false, + res.status(500).json({ + success: false, message: 'Interner Serverfehler beim Speichern des Standorts' }); } @@ -670,35 +670,35 @@ router.post('/v1/web/create-location', async (req, res) => { router.post('/v1/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.' + 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' + 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) @@ -706,11 +706,11 @@ router.post('/v1/web/save-token', async (req, res) => { RETURNING id, created_at`, [token, description || 'API-Token', standorte || '', new Date()] ); - - - res.json({ - success: true, + + + res.json({ + success: true, message: 'Token erfolgreich gespeichert', data: { id: result.rows[0].id, @@ -720,11 +720,11 @@ router.post('/v1/web/save-token', async (req, res) => { created_at: result.rows[0].created_at } }); - + } catch (error) { console.error('Fehler beim Speichern des Tokens:', error); - res.status(500).json({ - success: false, + res.status(500).json({ + success: false, message: 'Interner Serverfehler beim Speichern des Tokens' }); } @@ -734,17 +734,17 @@ router.post('/v1/web/save-token', async (req, res) => { router.get('/v1/private/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' + res.status(500).json({ + success: false, + message: 'Fehler beim Abrufen der GetLocations' }); } }); @@ -755,26 +755,26 @@ router.post('/v1/private/create-player', requireApiKey, async (req, res) => { // Validierung if (!firstname || !lastname || !birthdate || !rfiduid) { - return res.status(400).json({ - success: false, - message: 'Firstname, Lastname, Birthdate und RFIDUID sind erforderlich' + 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) @@ -782,11 +782,11 @@ router.post('/v1/private/create-player', requireApiKey, async (req, res) => { RETURNING id, created_at`, [firstname, lastname, birthdate, rfiduid, new Date()] ); - - - res.json({ - success: true, + + + res.json({ + success: true, message: 'Spieler erfolgreich gespeichert', data: { id: result.rows[0].id, @@ -797,12 +797,12 @@ router.post('/v1/private/create-player', requireApiKey, async (req, res) => { 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' + res.status(500).json({ + success: false, + message: 'Interner Serverfehler beim Speichern des Spielers' }); } }); @@ -810,63 +810,63 @@ router.post('/v1/private/create-player', requireApiKey, async (req, res) => { // API Endpunkt zum erstellen einer neuen Zeit mit RFID UID und Location Name router.post('/v1/private/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' + 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) { // Convert threshold to readable format const thresholdMinutes = Math.floor(thresholdSeconds / 60); const thresholdSecs = Math.floor(thresholdSeconds % 60); const thresholdMs = Math.floor((thresholdSeconds % 1) * 1000); - const thresholdDisplay = thresholdMinutes > 0 + const thresholdDisplay = thresholdMinutes > 0 ? `${thresholdMinutes}:${thresholdSecs.toString().padStart(2, '0')}.${thresholdMs.toString().padStart(3, '0')}` : `${thresholdSecs}.${thresholdMs.toString().padStart(3, '0')}`; - + return res.status(400).json({ success: false, message: `Zeit ${recorded_time} liegt unter dem Schwellenwert von ${thresholdDisplay} für diesen Standort`, @@ -879,8 +879,8 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => { }); } } - - + + // Zeit in Datenbank einfügen const result = await pool.query( `INSERT INTO times (player_id, location_id, recorded_time, created_at) @@ -888,7 +888,7 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => { RETURNING id, created_at`, [player_id, location_id, recorded_time, new Date()] ); - + // Achievement-Überprüfung nach Zeit-Eingabe try { await pool.query('SELECT check_immediate_achievements($1)', [player_id]); @@ -897,9 +897,9 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => { console.error('Fehler bei Achievement-Check:', achievementError); // Achievement-Fehler sollen die Zeit-Eingabe nicht blockieren } - - + + // WebSocket-Event senden für Live-Updates const io = req.app.get('io'); if (io) { @@ -911,9 +911,9 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => { 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, @@ -923,12 +923,12 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => { rank: rank, created_at: result.rows[0].created_at }); - + } - - res.json({ - success: true, + + res.json({ + success: true, message: 'Zeit erfolgreich gespeichert', data: { id: result.rows[0].id, @@ -941,12 +941,12 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => { 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' + res.status(500).json({ + success: false, + message: 'Interner Serverfehler beim Speichern der Zeit' }); } }); @@ -954,26 +954,26 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => { // API Endpunkt zum Überprüfen eines Benutzers anhand der RFID UID router.post('/v1/private/users/find', requireApiKey, async (req, res) => { const { uid } = req.body; - + // Validierung if (!uid) { - return res.status(400).json({ - success: false, - message: 'UID ist erforderlich' + 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, + return res.json({ + success: true, data: { uid: "", firstname: "", @@ -983,27 +983,27 @@ router.post('/v1/private/users/find', requireApiKey, async (req, res) => { } }); } - + 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, + + + res.json({ + success: true, data: { uid: player.rfiduid, firstname: player.firstname, @@ -1012,12 +1012,12 @@ router.post('/v1/private/users/find', requireApiKey, async (req, res) => { 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' + res.status(500).json({ + success: false, + message: 'Interner Serverfehler beim Überprüfen des Benutzers' }); } }); @@ -1032,15 +1032,15 @@ router.post('/v1/private/users/find', requireApiKey, async (req, res) => { // Create new player with optional Supabase user linking (no auth required for dashboard) router.post('/v1/public/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' + 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) { @@ -1048,7 +1048,7 @@ router.post('/v1/public/players', async (req, res) => { 'SELECT id FROM players WHERE rfiduid = $1', [rfiduid] ); - + if (existingRfid.rows.length > 0) { return res.status(409).json({ success: false, @@ -1056,14 +1056,14 @@ router.post('/v1/public/players', async (req, res) => { }); } } - + // 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, @@ -1071,7 +1071,7 @@ router.post('/v1/public/players', async (req, res) => { }); } } - + // Spieler in Datenbank einfügen const result = await pool.query( `INSERT INTO players (firstname, lastname, birthdate, rfiduid, supabase_user_id, created_at) @@ -1079,9 +1079,9 @@ router.post('/v1/public/players', async (req, res) => { RETURNING id, created_at`, [firstname, lastname, birthdate, rfiduid, supabase_user_id, new Date()] ); - - res.json({ - success: true, + + res.json({ + success: true, message: 'Spieler erfolgreich erstellt', data: { id: result.rows[0].id, @@ -1093,12 +1093,12 @@ router.post('/v1/public/players', async (req, res) => { 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' + res.status(500).json({ + success: false, + message: 'Interner Serverfehler beim Erstellen des Spielers' }); } }); @@ -1106,59 +1106,59 @@ router.post('/v1/public/players', async (req, res) => { // Link existing player to Supabase user (no auth required for dashboard) router.post('/v1/public/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' + 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, + + 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' + res.status(500).json({ + success: false, + message: 'Interner Serverfehler beim Verknüpfen des Spielers' }); } }); @@ -1166,20 +1166,20 @@ router.post('/v1/public/link-player', async (req, res) => { // Get user times by Supabase user ID (no auth required for dashboard) router.get('/v1/public/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 @@ -1194,17 +1194,17 @@ router.get('/v1/public/user-times/:supabase_user_id', async (req, res) => { WHERE t.player_id = $1 ORDER BY t.created_at DESC `, [player_id]); - + res.json({ success: true, data: timesResult.rows }); - + } catch (error) { console.error('Fehler beim Abrufen der Benutzerzeiten:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Abrufen der Benutzerzeiten' + res.status(500).json({ + success: false, + message: 'Fehler beim Abrufen der Benutzerzeiten' }); } }); @@ -1253,30 +1253,30 @@ router.get('/v1/public/user-times/:supabase_user_id', async (req, res) => { // Get player info by Supabase user ID (no auth required for dashboard) router.get('/v1/public/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, + + 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' + res.status(500).json({ + success: false, + message: 'Fehler beim Abrufen der Spielerdaten' }); } }); @@ -1284,31 +1284,31 @@ router.get('/v1/public/user-player/:supabase_user_id', async (req, res) => { // Link user by RFID UID (scanned from QR code) router.post('/v1/public/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' + 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({ @@ -1316,20 +1316,20 @@ router.post('/v1/public/link-by-rfid', async (req, res) => { 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( @@ -1337,9 +1337,9 @@ router.post('/v1/public/link-by-rfid', async (req, res) => { [supabase_user_id, player.id] ); } - - res.json({ - success: true, + + res.json({ + success: true, message: 'RFID erfolgreich verknüpft', data: { id: player.id, @@ -1348,12 +1348,12 @@ router.post('/v1/public/link-by-rfid', async (req, res) => { 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' + res.status(500).json({ + success: false, + message: 'Interner Serverfehler beim Verknüpfen per RFID UID' }); } }); @@ -1365,9 +1365,9 @@ router.post('/v1/public/link-by-rfid', async (req, res) => { // Middleware für Admin-Authentifizierung function requireAdminAuth(req, res, next) { if (!req.session.userId) { - return res.status(401).json({ - success: false, - message: 'Authentifizierung erforderlich' + return res.status(401).json({ + success: false, + message: 'Authentifizierung erforderlich' }); } next(); @@ -1376,9 +1376,9 @@ function requireAdminAuth(req, res, 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' + return res.status(403).json({ + success: false, + message: 'Insufficient access level' }); } next(); @@ -1426,9 +1426,9 @@ router.get('/v1/admin/stats', requireAdminAuth, async (req, res) => { }); } catch (error) { console.error('Error loading admin stats:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Laden der Statistiken' + res.status(500).json({ + success: false, + message: 'Fehler beim Laden der Statistiken' }); } }); @@ -1444,30 +1444,30 @@ router.get('/v1/admin/players', requireAdminAuth, async (req, res) => { 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' + res.status(500).json({ + success: false, + message: 'Fehler beim Laden der Spieler' }); } }); router.delete('/v1/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 { @@ -1475,9 +1475,9 @@ router.delete('/v1/admin/players/:id', requireAdminAuth, async (req, res) => { } } catch (error) { console.error('Error deleting player:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Löschen des Spielers' + res.status(500).json({ + success: false, + message: 'Fehler beim Löschen des Spielers' }); } }); @@ -1501,7 +1501,7 @@ router.get('/v1/admin/runs', requireAdminAuth, async (req, res) => { ORDER BY t.created_at DESC LIMIT 1000 `); - + res.json({ success: true, data: result.rows @@ -1519,7 +1519,7 @@ router.get('/v1/admin/runs', requireAdminAuth, async (req, res) => { router.get('/v1/admin/runs/:id', requireAdminAuth, async (req, res) => { try { const { id } = req.params; - + const result = await pool.query(` SELECT t.id, @@ -1535,14 +1535,14 @@ router.get('/v1/admin/runs/:id', requireAdminAuth, async (req, res) => { LEFT JOIN locations l ON t.location_id = l.id WHERE t.id = $1 `, [id]); - + if (result.rows.length === 0) { return res.status(404).json({ success: false, message: 'Lauf nicht gefunden' }); } - + res.json({ success: true, data: result.rows[0] @@ -1558,10 +1558,10 @@ router.get('/v1/admin/runs/:id', requireAdminAuth, async (req, res) => { router.delete('/v1/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 { @@ -1569,9 +1569,9 @@ router.delete('/v1/admin/runs/:id', requireAdminAuth, async (req, res) => { } } catch (error) { console.error('Error deleting run:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Löschen des Laufs' + res.status(500).json({ + success: false, + message: 'Fehler beim Löschen des Laufs' }); } }); @@ -1580,37 +1580,37 @@ router.delete('/v1/admin/runs/:id', requireAdminAuth, async (req, res) => { router.get('/v1/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' + res.status(500).json({ + success: false, + message: 'Fehler beim Laden der Standorte' }); } }); router.delete('/v1/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.` + 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 { @@ -1618,9 +1618,9 @@ router.delete('/v1/admin/locations/:id', requireAdminAuth, async (req, res) => { } } catch (error) { console.error('Error deleting location:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Löschen des Standorts' + res.status(500).json({ + success: false, + message: 'Fehler beim Löschen des Standorts' }); } }); @@ -1633,34 +1633,34 @@ router.get('/v1/admin/adminusers', requireAdminAuth, async (req, res) => { 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' + res.status(500).json({ + success: false, + message: 'Fehler beim Laden der Admin-Benutzer' }); } }); router.delete('/v1/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' + 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 { @@ -1668,9 +1668,9 @@ router.delete('/v1/admin/adminusers/:id', requireAdminAuth, async (req, res) => } } catch (error) { console.error('Error deleting admin user:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Löschen des Admin-Benutzers' + res.status(500).json({ + success: false, + message: 'Fehler beim Löschen des Admin-Benutzers' }); } }); @@ -1683,18 +1683,18 @@ router.delete('/v1/admin/adminusers/:id', requireAdminAuth, async (req, res) => router.post('/v1/public/track-page-view', async (req, res) => { try { const { page, userAgent, ipAddress, referer } = req.body; - + await pool.query(` INSERT INTO page_views (page, user_agent, ip_address, referer) VALUES ($1, $2, $3, $4) `, [page, userAgent, ipAddress, referer]); - + res.json({ success: true }); } catch (error) { console.error('Error tracking page view:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Tracking der Seitenaufrufe' + res.status(500).json({ + success: false, + message: 'Fehler beim Tracking der Seitenaufrufe' }); } }); @@ -1707,17 +1707,17 @@ router.post('/v1/public/track-page-view', async (req, res) => { router.get('/v1/public/locations', 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 Standorte' + res.status(500).json({ + success: false, + message: 'Fehler beim Abrufen der Standorte' }); } }); @@ -1794,17 +1794,17 @@ router.get('/v1/public/locations', async (req, res) => { // Public route to get times for location with parameter router.get('/v1/public/times', async (req, res) => { const { location } = req.query; - + try { // First, let's check if the view exists and has data const viewCheck = await pool.query('SELECT COUNT(*) as count FROM "GetTimesWithPlayerAndLocation"'); - + // Check what location names are available const availableLocations = await pool.query('SELECT DISTINCT location_name FROM "GetTimesWithPlayerAndLocation"'); - + // Now search for the specific location const result = await pool.query('SELECT * FROM "GetTimesWithPlayerAndLocation" WHERE location_name = $1', [location]); - + res.json({ success: true, data: result.rows, @@ -1815,7 +1815,7 @@ router.get('/v1/public/times', async (req, res) => { foundRecords: result.rows.length } }); - + } catch (error) { console.error('❌ Fehler beim Abrufen der Zeiten:', error); res.status(500).json({ @@ -1830,13 +1830,13 @@ router.get('/v1/public/times', async (req, res) => { router.get('/v1/public/times-with-details', async (req, res) => { try { const { location, period } = req.query; - + // Build WHERE clause for location filter let locationFilter = ''; if (location && location !== 'all') { locationFilter = `AND l.name ILIKE '%${location}%'`; } - + // Build WHERE clause for date filter using PostgreSQL timezone functions let dateFilter = ''; if (period === 'today') { @@ -1849,7 +1849,7 @@ router.get('/v1/public/times-with-details', async (req, res) => { // This month starting from 1st in local timezone dateFilter = `AND DATE(t.created_at AT TIME ZONE 'UTC' AT TIME ZONE 'Europe/Berlin') >= DATE_TRUNC('month', CURRENT_DATE)`; } - + // Get all times with player and location details, ordered by time (fastest first) // Only show times from players who have opted into leaderboard visibility const result = await pool.query(` @@ -1877,14 +1877,14 @@ router.get('/v1/public/times-with-details', async (req, res) => { ORDER BY t.recorded_time ASC LIMIT 50 `); - + // Convert seconds to minutes:seconds.milliseconds format const formattedResults = result.rows.map(row => { const totalSeconds = parseFloat(row.recorded_time_seconds); const minutes = Math.floor(totalSeconds / 60); const seconds = Math.floor(totalSeconds % 60); const milliseconds = Math.floor((totalSeconds % 1) * 1000); - + return { ...row, recorded_time: { @@ -1894,9 +1894,9 @@ router.get('/v1/public/times-with-details', async (req, res) => { } }; }); - + res.json(formattedResults); - + } catch (error) { console.error('❌ Fehler beim Abrufen der Zeiten mit Details:', error); res.status(500).json({ @@ -1917,7 +1917,7 @@ router.get('/v1/admin/page-stats', requireAdminAuth, async (req, res) => { startOfWeek.setDate(today.getDate() - today.getDay()); startOfWeek.setHours(0, 0, 0, 0); const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); - + // Today's page views const todayViews = await pool.query(` SELECT page, COUNT(*) as count @@ -1926,7 +1926,7 @@ router.get('/v1/admin/page-stats', requireAdminAuth, async (req, res) => { GROUP BY page ORDER BY count DESC `, [startOfDay]); - + // This week's page views const weekViews = await pool.query(` SELECT page, COUNT(*) as count @@ -1935,7 +1935,7 @@ router.get('/v1/admin/page-stats', requireAdminAuth, async (req, res) => { GROUP BY page ORDER BY count DESC `, [startOfWeek]); - + // This month's page views const monthViews = await pool.query(` SELECT page, COUNT(*) as count @@ -1944,7 +1944,7 @@ router.get('/v1/admin/page-stats', requireAdminAuth, async (req, res) => { GROUP BY page ORDER BY count DESC `, [startOfMonth]); - + // Total page views const totalViews = await pool.query(` SELECT page, COUNT(*) as count @@ -1952,7 +1952,7 @@ router.get('/v1/admin/page-stats', requireAdminAuth, async (req, res) => { GROUP BY page ORDER BY count DESC `); - + // Player/Supabase link statistics const linkStats = await pool.query(` SELECT @@ -1965,7 +1965,7 @@ router.get('/v1/admin/page-stats', requireAdminAuth, async (req, res) => { ) as link_percentage FROM players `); - + res.json({ success: true, data: { @@ -1978,9 +1978,9 @@ router.get('/v1/admin/page-stats', requireAdminAuth, async (req, res) => { }); } catch (error) { console.error('Error loading page statistics:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Laden der Seitenstatistiken' + res.status(500).json({ + success: false, + message: 'Fehler beim Laden der Seitenstatistiken' }); } }); @@ -1993,19 +1993,19 @@ router.get('/v1/admin/page-stats', requireAdminAuth, async (req, res) => { router.post('/v1/admin/players', requireAdminAuth, async (req, res) => { try { const { full_name, rfiduid, supabase_user_id } = req.body; - + // Name in firstname und lastname aufteilen const nameParts = full_name ? full_name.trim().split(' ') : []; const firstname = nameParts[0] || ''; const lastname = nameParts.slice(1).join(' ') || ''; - + const result = await pool.query( `INSERT INTO players (firstname, lastname, rfiduid, supabase_user_id, created_at) VALUES ($1, $2, $3, $4, NOW()) RETURNING *`, [firstname, lastname, rfiduid || null, supabase_user_id || null] ); - + res.json({ success: true, message: 'Spieler erfolgreich hinzugefügt', @@ -2013,9 +2013,9 @@ router.post('/v1/admin/players', requireAdminAuth, async (req, res) => { }); } catch (error) { console.error('Error creating player:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Hinzufügen des Spielers' + res.status(500).json({ + success: false, + message: 'Fehler beim Hinzufügen des Spielers' }); } }); @@ -2025,12 +2025,12 @@ router.put('/v1/admin/players/:id', requireAdminAuth, async (req, res) => { try { const playerId = req.params.id; const { full_name, rfiduid, supabase_user_id } = req.body; - + // Name in firstname und lastname aufteilen const nameParts = full_name ? full_name.trim().split(' ') : []; const firstname = nameParts[0] || ''; const lastname = nameParts.slice(1).join(' ') || ''; - + const result = await pool.query( `UPDATE players SET firstname = $1, lastname = $2, rfiduid = $3, supabase_user_id = $4 @@ -2038,7 +2038,7 @@ router.put('/v1/admin/players/:id', requireAdminAuth, async (req, res) => { RETURNING *`, [firstname, lastname, rfiduid || null, supabase_user_id || null, playerId] ); - + if (result.rowCount > 0) { res.json({ success: true, @@ -2050,9 +2050,9 @@ router.put('/v1/admin/players/:id', requireAdminAuth, async (req, res) => { } } catch (error) { console.error('Error updating player:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Aktualisieren des Spielers' + res.status(500).json({ + success: false, + message: 'Fehler beim Aktualisieren des Spielers' }); } }); @@ -2061,14 +2061,14 @@ router.put('/v1/admin/players/:id', requireAdminAuth, async (req, res) => { router.post('/v1/admin/locations', requireAdminAuth, async (req, res) => { try { const { name, latitude, longitude, time_threshold } = req.body; - + const result = await pool.query( `INSERT INTO locations (name, latitude, longitude, time_threshold, created_at) VALUES ($1, $2, $3, $4, NOW()) RETURNING *`, [name, latitude, longitude, time_threshold || null] ); - + res.json({ success: true, message: 'Standort erfolgreich hinzugefügt', @@ -2076,9 +2076,9 @@ router.post('/v1/admin/locations', requireAdminAuth, async (req, res) => { }); } catch (error) { console.error('Error creating location:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Hinzufügen des Standorts' + res.status(500).json({ + success: false, + message: 'Fehler beim Hinzufügen des Standorts' }); } }); @@ -2088,7 +2088,7 @@ router.put('/v1/admin/locations/:id', requireAdminAuth, async (req, res) => { try { const locationId = req.params.id; const { name, latitude, longitude, time_threshold } = req.body; - + const result = await pool.query( `UPDATE locations SET name = $1, latitude = $2, longitude = $3, time_threshold = $4 @@ -2096,7 +2096,7 @@ router.put('/v1/admin/locations/:id', requireAdminAuth, async (req, res) => { RETURNING *`, [name, latitude, longitude, time_threshold || null, locationId] ); - + if (result.rowCount > 0) { res.json({ success: true, @@ -2108,9 +2108,9 @@ router.put('/v1/admin/locations/:id', requireAdminAuth, async (req, res) => { } } catch (error) { console.error('Error updating location:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Aktualisieren des Standorts' + res.status(500).json({ + success: false, + message: 'Fehler beim Aktualisieren des Standorts' }); } }); @@ -2119,26 +2119,26 @@ router.put('/v1/admin/locations/:id', requireAdminAuth, async (req, res) => { router.post('/v1/admin/runs', requireAdminAuth, async (req, res) => { try { const { player_id, location_id, time_seconds } = req.body; - + // Prüfen ob Location existiert und Threshold abrufen const locationResult = await pool.query( 'SELECT id, name, time_threshold FROM locations WHERE id = $1', [location_id] ); - + if (locationResult.rows.length === 0) { return res.status(404).json({ success: false, message: 'Standort nicht gefunden' }); } - + const location = locationResult.rows[0]; - + // Prüfen ob die Zeit über dem Schwellenwert liegt if (location.time_threshold) { const thresholdSeconds = convertIntervalToSeconds(location.time_threshold); - + if (time_seconds < thresholdSeconds) { return res.status(400).json({ success: false, @@ -2151,17 +2151,17 @@ router.post('/v1/admin/runs', requireAdminAuth, async (req, res) => { }); } } - + // Zeit in INTERVAL konvertieren const timeInterval = `${time_seconds} seconds`; - + const result = await pool.query( `INSERT INTO times (player_id, location_id, recorded_time, created_at) VALUES ($1, $2, $3, NOW()) RETURNING *`, [player_id, location_id, timeInterval] ); - + // Achievement-Überprüfung nach Zeit-Eingabe try { await pool.query('SELECT check_immediate_achievements($1)', [player_id]); @@ -2170,7 +2170,7 @@ router.post('/v1/admin/runs', requireAdminAuth, async (req, res) => { console.error('Fehler bei Achievement-Check:', achievementError); // Achievement-Fehler sollen die Zeit-Eingabe nicht blockieren } - + res.json({ success: true, message: 'Lauf erfolgreich hinzugefügt', @@ -2178,9 +2178,9 @@ router.post('/v1/admin/runs', requireAdminAuth, async (req, res) => { }); } catch (error) { console.error('Error creating run:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Hinzufügen des Laufs' + res.status(500).json({ + success: false, + message: 'Fehler beim Hinzufügen des Laufs' }); } }); @@ -2190,10 +2190,10 @@ router.put('/v1/admin/runs/:id', requireAdminAuth, async (req, res) => { try { const runId = req.params.id; const { player_id, location_id, time_seconds } = req.body; - + // Zeit in INTERVAL konvertieren const timeInterval = `${time_seconds} seconds`; - + const result = await pool.query( `UPDATE times SET player_id = $1, location_id = $2, recorded_time = $3 @@ -2201,7 +2201,7 @@ router.put('/v1/admin/runs/:id', requireAdminAuth, async (req, res) => { RETURNING *`, [player_id, location_id, timeInterval, runId] ); - + if (result.rowCount > 0) { res.json({ success: true, @@ -2213,9 +2213,9 @@ router.put('/v1/admin/runs/:id', requireAdminAuth, async (req, res) => { } } catch (error) { console.error('Error updating run:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Aktualisieren des Laufs' + res.status(500).json({ + success: false, + message: 'Fehler beim Aktualisieren des Laufs' }); } }); @@ -2224,18 +2224,18 @@ router.put('/v1/admin/runs/:id', requireAdminAuth, async (req, res) => { router.post('/v1/admin/adminusers', requireAdminAuth, async (req, res) => { try { const { username, password, access_level } = req.body; - + // Passwort hashen const saltRounds = 10; const hashedPassword = await bcrypt.hash(password, saltRounds); - + const result = await pool.query( `INSERT INTO adminusers (username, password_hash, access_level, is_active, created_at) VALUES ($1, $2, $3, true, NOW()) RETURNING id, username, access_level, is_active, created_at`, [username, hashedPassword, access_level] ); - + res.json({ success: true, message: 'Admin-Benutzer erfolgreich hinzugefügt', @@ -2244,14 +2244,14 @@ router.post('/v1/admin/adminusers', requireAdminAuth, async (req, res) => { } catch (error) { console.error('Error creating admin user:', error); if (error.code === '23505') { // Unique constraint violation - res.status(400).json({ - success: false, - message: 'Benutzername bereits vergeben' + res.status(400).json({ + success: false, + message: 'Benutzername bereits vergeben' }); } else { - res.status(500).json({ - success: false, - message: 'Fehler beim Hinzufügen des Admin-Benutzers' + res.status(500).json({ + success: false, + message: 'Fehler beim Hinzufügen des Admin-Benutzers' }); } } @@ -2262,22 +2262,22 @@ router.put('/v1/admin/adminusers/:id', requireAdminAuth, async (req, res) => { try { const userId = req.params.id; const { username, password, access_level } = req.body; - + // Verhindern, dass sich selbst bearbeitet if (parseInt(userId) === req.session.userId) { - return res.status(400).json({ - success: false, - message: 'Sie können sich nicht selbst bearbeiten' + return res.status(400).json({ + success: false, + message: 'Sie können sich nicht selbst bearbeiten' }); } - + let query, params; - + if (password) { // Passwort hashen const saltRounds = 10; const hashedPassword = await bcrypt.hash(password, saltRounds); - + query = `UPDATE adminusers SET username = $1, password_hash = $2, access_level = $3 WHERE id = $4 @@ -2290,9 +2290,9 @@ router.put('/v1/admin/adminusers/:id', requireAdminAuth, async (req, res) => { RETURNING id, username, access_level, is_active, created_at`; params = [username, access_level, userId]; } - + const result = await pool.query(query, params); - + if (result.rowCount > 0) { res.json({ success: true, @@ -2305,14 +2305,14 @@ router.put('/v1/admin/adminusers/:id', requireAdminAuth, async (req, res) => { } catch (error) { console.error('Error updating admin user:', error); if (error.code === '23505') { // Unique constraint violation - res.status(400).json({ - success: false, - message: 'Benutzername bereits vergeben' + res.status(400).json({ + success: false, + message: 'Benutzername bereits vergeben' }); } else { - res.status(500).json({ - success: false, - message: 'Fehler beim Aktualisieren des Admin-Benutzers' + res.status(500).json({ + success: false, + message: 'Fehler beim Aktualisieren des Admin-Benutzers' }); } } @@ -2388,7 +2388,7 @@ router.get('/v1/public/best-times', async (req, res) => { const today = currentDate.toISOString().split('T')[0]; const weekStart = new Date(currentDate.setDate(currentDate.getDate() - currentDate.getDay())); const monthStart = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1); - + // Get daily best const dailyResult = await pool.query(` WITH daily_best AS ( @@ -2410,7 +2410,7 @@ router.get('/v1/public/best-times', async (req, res) => { WHERE best_time = (SELECT MIN(best_time) FROM daily_best) LIMIT 1 `, [today]); - + // Get weekly best const weeklyResult = await pool.query(` WITH weekly_best AS ( @@ -2433,7 +2433,7 @@ router.get('/v1/public/best-times', async (req, res) => { WHERE best_time = (SELECT MIN(best_time) FROM weekly_best) LIMIT 1 `, [weekStart.toISOString().split('T')[0], today]); - + // Get monthly best const monthlyResult = await pool.query(` WITH monthly_best AS ( @@ -2456,7 +2456,7 @@ router.get('/v1/public/best-times', async (req, res) => { WHERE best_time = (SELECT MIN(best_time) FROM monthly_best) LIMIT 1 `, [monthStart.toISOString().split('T')[0], today]); - + res.json({ success: true, data: { @@ -2482,7 +2482,7 @@ router.post('/v1/public/subscribe', async (req, res) => { try { const { endpoint, keys } = req.body; const userId = req.session.userId || 'anonymous'; - + // Generate a UUID for anonymous users or use existing UUID let playerId; if (userId === 'anonymous') { @@ -2492,7 +2492,7 @@ router.post('/v1/public/subscribe', async (req, res) => { } else { playerId = userId; } - + // Store subscription in database await pool.query(` INSERT INTO player_subscriptions (player_id, endpoint, p256dh, auth, created_at) @@ -2519,7 +2519,7 @@ router.post('/v1/public/subscribe', async (req, res) => { } }; pushService.subscribe(playerId, subscription); - + res.json({ success: true, message: 'Push subscription erfolgreich gespeichert' @@ -2538,7 +2538,7 @@ router.post('/v1/public/subscribe', async (req, res) => { router.post('/v1/public/test-push', async (req, res) => { try { const { userId, message } = req.body; - + const payload = { title: '🧪 Test Notification', body: message || 'Das ist eine Test-Push-Notification!', @@ -2551,7 +2551,7 @@ router.post('/v1/public/test-push', async (req, res) => { }; const success = await pushService.sendToUser(userId || 'anonymous', payload); - + res.json({ success: success, message: success ? 'Test-Push gesendet!' : 'Keine Subscription gefunden' @@ -2569,11 +2569,11 @@ router.post('/v1/public/test-push', async (req, res) => { router.get('/v1/public/push-status', async (req, res) => { try { const userId = req.session.userId || 'anonymous'; - + // For anonymous users, we can't check specific subscription // but we can show general stats const hasSubscription = userId !== 'anonymous' ? pushService.subscriptions.has(userId) : false; - + res.json({ success: true, data: { @@ -2632,16 +2632,16 @@ router.get('/achievements', async (req, res) => { WHERE is_active = true ORDER BY category, points DESC `); - + res.json({ success: true, data: result.rows }); } catch (error) { console.error('Error fetching achievements:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Laden der Achievements' + res.status(500).json({ + success: false, + message: 'Fehler beim Laden der Achievements' }); } }); @@ -2694,9 +2694,9 @@ router.get('/achievements/player/:playerId', async (req, res) => { 'Pragma': 'no-cache', 'Expires': '0' }); - + const { playerId } = req.params; - + const result = await pool.query(` SELECT a.id, @@ -2716,16 +2716,16 @@ router.get('/achievements/player/:playerId', async (req, res) => { a.category, a.points DESC `, [playerId]); - + res.json({ success: true, data: result.rows }); } catch (error) { console.error('Error fetching player achievements:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Laden der Spieler-Achievements' + res.status(500).json({ + success: false, + message: 'Fehler beim Laden der Spieler-Achievements' }); } }); @@ -2739,16 +2739,16 @@ router.get('/achievements/player/:playerId/stats', async (req, res) => { 'Pragma': 'no-cache', 'Expires': '0' }); - + const { playerId } = req.params; - + // Get total available achievements const totalResult = await pool.query(` SELECT COUNT(*) as total_achievements FROM achievements WHERE is_active = true `); - + // Get player's earned achievements const playerResult = await pool.query(` SELECT @@ -2759,14 +2759,14 @@ router.get('/achievements/player/:playerId/stats', async (req, res) => { LEFT JOIN player_achievements pa ON a.id = pa.achievement_id AND pa.player_id = $1 WHERE a.is_active = true `, [playerId]); - + const totalStats = totalResult.rows[0]; const playerStats = playerResult.rows[0]; - + const totalAchievements = parseInt(totalStats.total_achievements); const completedAchievements = parseInt(playerStats.completed_achievements) || 0; const completionPercentage = totalAchievements > 0 ? Math.round((completedAchievements / totalAchievements) * 100) : 0; - + res.json({ success: true, data: { @@ -2779,9 +2779,9 @@ router.get('/achievements/player/:playerId/stats', async (req, res) => { }); } catch (error) { console.error('Error fetching player achievement stats:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Laden der Achievement-Statistiken' + res.status(500).json({ + success: false, + message: 'Fehler beim Laden der Achievement-Statistiken' }); } }); @@ -2790,19 +2790,19 @@ router.get('/achievements/player/:playerId/stats', async (req, res) => { router.post('/achievements/check/:playerId', async (req, res) => { try { const { playerId } = req.params; - + // Verify player exists const playerCheck = await pool.query('SELECT id FROM players WHERE id = $1', [playerId]); if (playerCheck.rows.length === 0) { - return res.status(404).json({ - success: false, - message: 'Spieler nicht gefunden' + return res.status(404).json({ + success: false, + message: 'Spieler nicht gefunden' }); } - + // Run achievement check await pool.query('SELECT check_all_achievements($1)', [playerId]); - + // Get newly earned achievements const newAchievements = await pool.query(` SELECT a.name, a.description, a.icon, a.points @@ -2813,7 +2813,7 @@ router.post('/achievements/check/:playerId', async (req, res) => { AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE ORDER BY pa.earned_at DESC `, [playerId]); - + res.json({ success: true, message: 'Achievement-Check abgeschlossen', @@ -2824,9 +2824,9 @@ router.post('/achievements/check/:playerId', async (req, res) => { }); } catch (error) { console.error('Error checking achievements:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Überprüfen der Achievements' + res.status(500).json({ + success: false, + message: 'Fehler beim Überprüfen der Achievements' }); } }); @@ -2836,18 +2836,18 @@ router.post('/achievements/daily-check', async (req, res) => { try { // This endpoint runs the daily achievement check const { runDailyAchievements } = require('../scripts/daily_achievements'); - + await runDailyAchievements(); - + res.json({ success: true, message: 'Tägliche Achievement-Überprüfung abgeschlossen' }); } catch (error) { console.error('Error running daily achievement check:', error); - res.status(500).json({ - success: false, - message: 'Fehler bei der täglichen Achievement-Überprüfung' + res.status(500).json({ + success: false, + message: 'Fehler bei der täglichen Achievement-Überprüfung' }); } }); @@ -2857,18 +2857,18 @@ router.post('/achievements/best-time-check', async (req, res) => { try { // This endpoint runs the best-time achievement check const { runBestTimeAchievements } = require('../scripts/best_time_achievements'); - + await runBestTimeAchievements(); - + res.json({ success: true, message: 'Best-Time Achievement-Überprüfung abgeschlossen' }); } catch (error) { console.error('Error running best-time achievement check:', error); - res.status(500).json({ - success: false, - message: 'Fehler bei der Best-Time Achievement-Überprüfung' + res.status(500).json({ + success: false, + message: 'Fehler bei der Best-Time Achievement-Überprüfung' }); } }); @@ -2877,7 +2877,7 @@ router.post('/achievements/best-time-check', async (req, res) => { router.get('/achievements/leaderboard', async (req, res) => { try { const { limit = 10 } = req.query; - + const result = await pool.query(` SELECT p.firstname, @@ -2892,7 +2892,7 @@ router.get('/achievements/leaderboard', async (req, res) => { ORDER BY total_points DESC, completed_achievements DESC LIMIT $1 `, [parseInt(limit)]); - + res.json({ success: true, data: result.rows.map((row, index) => ({ @@ -2904,14 +2904,58 @@ router.get('/achievements/leaderboard', async (req, res) => { }); } catch (error) { console.error('Error fetching achievement leaderboard:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Laden der Bestenliste' + res.status(500).json({ + success: false, + message: 'Fehler beim Laden der Bestenliste' }); } }); -// Update player settings (privacy settings) +// Update player settings (privacy settings) - Public endpoint for authenticated users +router.post('/v1/public/update-player-settings', async (req, res) => { + try { + const { player_id, show_in_leaderboard } = req.body; + + if (!player_id) { + return res.status(400).json({ + success: false, + message: 'Player ID ist erforderlich' + }); + } + + // Update player settings + const updateQuery = ` + UPDATE players + SET show_in_leaderboard = $1, updated_at = NOW() + WHERE id = $2 + RETURNING id, firstname, lastname, show_in_leaderboard + `; + + const result = await pool.query(updateQuery, [show_in_leaderboard || false, player_id]); + + if (result.rows.length === 0) { + return res.status(404).json({ + success: false, + message: 'Spieler nicht gefunden' + }); + } + + res.json({ + success: true, + message: 'Einstellungen erfolgreich aktualisiert', + data: result.rows[0] + }); + + } catch (error) { + console.error('Error updating player settings:', error); + res.status(500).json({ + success: false, + message: 'Fehler beim Aktualisieren der Einstellungen' + }); + } +}); + +// Update player settings (privacy settings) - Private endpoint with API key router.post('/v1/private/update-player-settings', requireApiKey, async (req, res) => { try { const { player_id, show_in_leaderboard } = req.body;