// 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 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; // 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 } }); }); } 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' }); } }); module.exports = { router, requireApiKey };