diff --git a/apache-ssl-config.conf b/apache-ssl-config.conf new file mode 100644 index 0000000..79d21c7 --- /dev/null +++ b/apache-ssl-config.conf @@ -0,0 +1,40 @@ +# Apache SSL VirtualHost für NinjaCross +# Datei: /etc/apache2/sites-available/ninjaserver-ssl.conf + + + ServerName ninja.reptilfpv.de + DocumentRoot /var/www/html + + # SSL Configuration + SSLEngine on + SSLCertificateFile /etc/letsencrypt/live/ninja.reptilfpv.de/fullchain.pem + SSLCertificateKeyFile /etc/letsencrypt/live/ninja.reptilfpv.de/privkey.pem + + # Security Headers für Kamera-Zugriff + Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" + Header always set X-Content-Type-Options nosniff + Header always set X-Frame-Options DENY + Header always set Referrer-Policy strict-origin-when-cross-origin + + # Wichtig für Kamera-Zugriff + Header always set Permissions-Policy "camera=self, microphone=()" + + # Reverse Proxy zu Node.js + ProxyPreserveHost On + ProxyPass / http://localhost:3000/ + ProxyPassReverse / http://localhost:3000/ + + # WebSocket Support für Live-Updates + ProxyPass /socket.io/ ws://localhost:3000/socket.io/ + ProxyPassReverse /socket.io/ ws://localhost:3000/socket.io/ + + # Logging + ErrorLog ${APACHE_LOG_DIR}/ninjaserver_ssl_error.log + CustomLog ${APACHE_LOG_DIR}/ninjaserver_ssl_access.log combined + + +# HTTP zu HTTPS Redirect + + ServerName ninja.reptilfpv.de + Redirect permanent / https://ninja.reptilfpv.de/ + diff --git a/public/dashboard.html b/public/dashboard.html index c3c3800..a9453f5 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -5,6 +5,8 @@ SPEEDRUN ARENA - Admin Dashboard + + @@ -243,19 +731,137 @@

Common tasks and shortcuts will be available here. We'll add buttons for creating new records, managing settings, and more.

-
-

�� Settings

-

Configure your account preferences, security settings, and application options. This will be fully functional in the next phase.

+
+

🏷️ RFID Verknüpfung

+

Verknüpfe deine RFID-Karte mit deinem Account, um deine Zeiten automatisch zu tracken.

+
-

📝 Recent Activity

-

View your recent actions and system events. This will show a timeline of your activities once we connect it to the database.

+

📊 Statistiken

+

Hier werden bald detaillierte Statistiken zu deinen Läufen angezeigt - beste Zeiten, Verbesserungen und Vergleiche mit anderen Spielern.

+
+
+ + +
+
+

🏃‍♂️ Meine Zeiten

+

Deine persönlichen Bestzeiten an allen Standorten

+
+ + + + + +
+
+
🔗
+

RFID noch nicht verknüpft

+

Um deine persönlichen Zeiten zu sehen, musst du zuerst deine RFID-Karte mit deinem Account verknüpfen.

+ + +
+
+ + +
+ + + + + diff --git a/routes/api.js b/routes/api.js index 369412f..708ac49 100644 --- a/routes/api.js +++ b/routes/api.js @@ -998,4 +998,313 @@ router.post('/users/find', requireApiKey, async (req, res) => { +// ============================================================================ +// RFID LINKING & USER MANAGEMENT ENDPOINTS (No API Key required for dashboard) +// ============================================================================ + +// Get all players for RFID linking (no auth required for dashboard) +router.get('/players', async (req, res) => { + try { + const result = await pool.query( + `SELECT id, firstname, lastname, birthdate, rfiduid, created_at + FROM players + ORDER BY created_at DESC` + ); + + res.json(result.rows); + + } catch (error) { + console.error('Fehler beim Abrufen der Spieler:', error); + res.status(500).json({ + success: false, + message: 'Fehler beim Abrufen der Spieler' + }); + } +}); + +// Create new player with optional Supabase user linking (no auth required for dashboard) +router.post('/players', async (req, res) => { + const { firstname, lastname, birthdate, rfiduid, supabase_user_id } = req.body; + + // Validierung + if (!firstname || !lastname || !birthdate) { + return res.status(400).json({ + success: false, + message: 'Firstname, Lastname und Birthdate sind erforderlich' + }); + } + + try { + // Prüfen ob RFID UID bereits existiert (falls angegeben) + if (rfiduid) { + const existingRfid = await pool.query( + 'SELECT id FROM players WHERE rfiduid = $1', + [rfiduid] + ); + + if (existingRfid.rows.length > 0) { + return res.status(409).json({ + success: false, + message: 'RFID UID existiert bereits' + }); + } + } + + // Prüfen ob Supabase User bereits verknüpft ist + if (supabase_user_id) { + const existingUser = await pool.query( + 'SELECT id FROM players WHERE supabase_user_id = $1', + [supabase_user_id] + ); + + if (existingUser.rows.length > 0) { + return res.status(409).json({ + success: false, + message: 'Dieser Benutzer ist bereits mit einem Spieler verknüpft' + }); + } + } + + // Spieler in Datenbank einfügen + const result = await pool.query( + `INSERT INTO players (firstname, lastname, birthdate, rfiduid, supabase_user_id, created_at) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id, created_at`, + [firstname, lastname, birthdate, rfiduid, supabase_user_id, new Date()] + ); + + res.json({ + success: true, + message: 'Spieler erfolgreich erstellt', + data: { + id: result.rows[0].id, + firstname: firstname, + lastname: lastname, + birthdate: birthdate, + rfiduid: rfiduid, + supabase_user_id: supabase_user_id, + created_at: result.rows[0].created_at + } + }); + + } catch (error) { + console.error('Fehler beim Erstellen des Spielers:', error); + res.status(500).json({ + success: false, + message: 'Interner Serverfehler beim Erstellen des Spielers' + }); + } +}); + +// Link existing player to Supabase user (no auth required for dashboard) +router.post('/link-player', async (req, res) => { + const { player_id, supabase_user_id } = req.body; + + // Validierung + if (!player_id || !supabase_user_id) { + return res.status(400).json({ + success: false, + message: 'Player ID und Supabase User ID sind erforderlich' + }); + } + + try { + // Prüfen ob Spieler existiert + const playerExists = await pool.query( + 'SELECT id, firstname, lastname FROM players WHERE id = $1', + [player_id] + ); + + if (playerExists.rows.length === 0) { + return res.status(404).json({ + success: false, + message: 'Spieler nicht gefunden' + }); + } + + // Prüfen ob Supabase User bereits verknüpft ist + const existingLink = await pool.query( + 'SELECT id FROM players WHERE supabase_user_id = $1', + [supabase_user_id] + ); + + if (existingLink.rows.length > 0) { + return res.status(409).json({ + success: false, + message: 'Dieser Benutzer ist bereits mit einem Spieler verknüpft' + }); + } + + // Verknüpfung erstellen + const result = await pool.query( + 'UPDATE players SET supabase_user_id = $1 WHERE id = $2 RETURNING id, firstname, lastname, rfiduid', + [supabase_user_id, player_id] + ); + + res.json({ + success: true, + message: 'Spieler erfolgreich verknüpft', + data: result.rows[0] + }); + + } catch (error) { + console.error('Fehler beim Verknüpfen des Spielers:', error); + res.status(500).json({ + success: false, + message: 'Interner Serverfehler beim Verknüpfen des Spielers' + }); + } +}); + +// Get user times by Supabase user ID (no auth required for dashboard) +router.get('/user-times/:supabase_user_id', async (req, res) => { + const { supabase_user_id } = req.params; + + try { + // Finde verknüpften Spieler + const playerResult = await pool.query( + 'SELECT id FROM players WHERE supabase_user_id = $1', + [supabase_user_id] + ); + + if (playerResult.rows.length === 0) { + return res.json([]); // Noch keine Verknüpfung + } + + const player_id = playerResult.rows[0].id; + + // Hole alle Zeiten für diesen Spieler mit Location-Namen + const timesResult = await pool.query(` + SELECT + t.id, + t.recorded_time, + t.created_at, + l.name as location_name, + l.latitude, + l.longitude + FROM times t + JOIN locations l ON t.location_id = l.id + WHERE t.player_id = $1 + ORDER BY t.created_at DESC + `, [player_id]); + + res.json(timesResult.rows); + + } catch (error) { + console.error('Fehler beim Abrufen der Benutzerzeiten:', error); + res.status(500).json({ + success: false, + message: 'Fehler beim Abrufen der Benutzerzeiten' + }); + } +}); + +// Get player info by Supabase user ID (no auth required for dashboard) +router.get('/user-player/:supabase_user_id', async (req, res) => { + const { supabase_user_id } = req.params; + + try { + const result = await pool.query( + 'SELECT id, firstname, lastname, birthdate, rfiduid FROM players WHERE supabase_user_id = $1', + [supabase_user_id] + ); + + if (result.rows.length === 0) { + return res.status(404).json({ + success: false, + message: 'Kein verknüpfter Spieler gefunden' + }); + } + + res.json({ + success: true, + data: result.rows[0] + }); + + } catch (error) { + console.error('Fehler beim Abrufen der Spielerdaten:', error); + res.status(500).json({ + success: false, + message: 'Fehler beim Abrufen der Spielerdaten' + }); + } +}); + +// Link user by RFID UID (scanned from QR code) +router.post('/link-by-rfid', async (req, res) => { + const { rfiduid, supabase_user_id } = req.body; + + // Validierung + if (!rfiduid || !supabase_user_id) { + return res.status(400).json({ + success: false, + message: 'RFID UID und Supabase User ID sind erforderlich' + }); + } + + try { + // Prüfen ob Spieler mit dieser RFID UID existiert + const playerResult = await pool.query( + 'SELECT id, firstname, lastname, rfiduid, supabase_user_id FROM players WHERE rfiduid = $1', + [rfiduid] + ); + + if (playerResult.rows.length === 0) { + return res.status(404).json({ + success: false, + message: `Kein Spieler mit RFID UID '${rfiduid}' gefunden. Bitte erstelle zuerst einen Spieler mit dieser RFID UID.` + }); + } + + const player = playerResult.rows[0]; + + // Prüfen ob dieser Spieler bereits mit einem anderen Benutzer verknüpft ist + if (player.supabase_user_id && player.supabase_user_id !== supabase_user_id) { + return res.status(409).json({ + success: false, + message: 'Dieser Spieler ist bereits mit einem anderen Benutzer verknüpft' + }); + } + + // Prüfen ob dieser Benutzer bereits mit einem anderen Spieler verknüpft ist + const existingLink = await pool.query( + 'SELECT id, firstname, lastname FROM players WHERE supabase_user_id = $1 AND id != $2', + [supabase_user_id, player.id] + ); + + if (existingLink.rows.length > 0) { + return res.status(409).json({ + success: false, + message: `Du bist bereits mit dem Spieler '${existingLink.rows[0].firstname} ${existingLink.rows[0].lastname}' verknüpft. Ein Benutzer kann nur mit einem Spieler verknüpft sein.` + }); + } + + // Verknüpfung erstellen (falls noch nicht vorhanden) + if (!player.supabase_user_id) { + await pool.query( + 'UPDATE players SET supabase_user_id = $1 WHERE id = $2', + [supabase_user_id, player.id] + ); + } + + res.json({ + success: true, + message: 'RFID erfolgreich verknüpft', + data: { + id: player.id, + firstname: player.firstname, + lastname: player.lastname, + rfiduid: player.rfiduid + } + }); + + } catch (error) { + console.error('Fehler beim Verknüpfen per RFID UID:', error); + res.status(500).json({ + success: false, + message: 'Interner Serverfehler beim Verknüpfen per RFID UID' + }); + } +}); + module.exports = { router, requireApiKey }; diff --git a/server.js b/server.js index f5c4a17..3c7c9e0 100644 --- a/server.js +++ b/server.js @@ -141,6 +141,14 @@ app.get('/reset-password.html', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'reset-password.html')); }); +/** + * Admin Login Page + * Lizenzgenerator Login-Seite für Admin-Benutzer + */ +app.get('/adminlogin.html', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'adminlogin.html')); +}); + // ============================================================================ // STATIC FILE SERVING // ============================================================================ diff --git a/server.log b/server.log new file mode 100644 index 0000000..65a1dd3 --- /dev/null +++ b/server.log @@ -0,0 +1,8 @@ +nohup: ignoring input +🚀 Server läuft auf http://ninja.reptilfpv.de:3000 +📊 Datenbank: localhost:5432/ninjacross +🔐 API-Key Authentifizierung aktiviert +🔌 WebSocket-Server aktiviert +📁 Static files: /public +🌐 Public API: /public-api +🔑 Private API: /api diff --git a/ssl-setup-guide.md b/ssl-setup-guide.md new file mode 100644 index 0000000..e39aea8 --- /dev/null +++ b/ssl-setup-guide.md @@ -0,0 +1,89 @@ +# SSL Setup für NinjaCross Server + +## 1. Let's Encrypt SSL Zertifikat installieren + +```bash +# Certbot installieren (Ubuntu/Debian) +sudo apt update +sudo apt install certbot python3-certbot-apache + +# SSL Zertifikat anfordern +sudo certbot certonly --standalone -d ninja.reptilfpv.de + +# Automatische Erneuerung einrichten +sudo crontab -e +# Füge hinzu: 0 12 * * * /usr/bin/certbot renew --quiet +``` + +## 2. Apache Module aktivieren + +```bash +# Notwendige Apache Module aktivieren +sudo a2enmod ssl +sudo a2enmod proxy +sudo a2enmod proxy_http +sudo a2enmod proxy_wstunnel +sudo a2enmod headers +sudo a2enmod rewrite + +# Apache neustarten +sudo systemctl restart apache2 +``` + +## 3. VirtualHost konfigurieren + +```bash +# SSL Konfiguration kopieren +sudo cp apache-ssl-config.conf /etc/apache2/sites-available/ninjaserver-ssl.conf + +# Site aktivieren +sudo a2ensite ninjaserver-ssl.conf + +# Standard-Site deaktivieren (optional) +sudo a2dissite 000-default.conf + +# Apache Konfiguration testen +sudo apache2ctl configtest + +# Apache neuladen +sudo systemctl reload apache2 +``` + +## 4. Node.js Server anpassen (optional) + +Ihr Node.js Server läuft weiterhin auf Port 3000 (localhost only). +Optional können Sie den Server auf localhost binden: + +```javascript +// In server.js +server.listen(port, 'localhost', () => { + console.log(`🚀 Server läuft auf http://localhost:${port}`); + console.log(`🔒 HTTPS verfügbar über Apache Proxy`); +}); +``` + +## 5. Firewall anpassen + +```bash +# HTTPS Port öffnen +sudo ufw allow 443/tcp +sudo ufw allow 80/tcp + +# Port 3000 nur für localhost (Sicherheit) +sudo ufw deny 3000 +``` + +## 6. Testen + +1. Öffne https://ninja.reptilfpv.de +2. Teste QR-Scanner → Kamera sollte funktionieren +3. Prüfe SSL-Rating: https://www.ssllabs.com/ssltest/ + +## Vorteile dieser Lösung: + +✅ **Automatische SSL-Erneuerung** mit Let's Encrypt +✅ **Hohe Performance** durch Apache SSL-Terminierung +✅ **Security Headers** für zusätzlichen Schutz +✅ **WebSocket Support** für Live-Updates +✅ **HTTP → HTTPS Redirect** automatisch +✅ **Kamera-Zugriff** funktioniert in allen Browsern