Add linking of RFID with camera

This commit is contained in:
2025-09-03 16:42:18 +02:00
parent 33f3f613e6
commit e4f6218066
6 changed files with 1597 additions and 5 deletions

40
apache-ssl-config.conf Normal file
View File

@@ -0,0 +1,40 @@
# Apache SSL VirtualHost für NinjaCross
# Datei: /etc/apache2/sites-available/ninjaserver-ssl.conf
<VirtualHost *:443>
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
</VirtualHost>
# HTTP zu HTTPS Redirect
<VirtualHost *:80>
ServerName ninja.reptilfpv.de
Redirect permanent / https://ninja.reptilfpv.de/
</VirtualHost>

File diff suppressed because it is too large Load Diff

View File

@@ -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 };

View File

@@ -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
// ============================================================================

8
server.log Normal file
View File

@@ -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

89
ssl-setup-guide.md Normal file
View File

@@ -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