Add linking of RFID with camera
This commit is contained in:
40
apache-ssl-config.conf
Normal file
40
apache-ssl-config.conf
Normal 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
309
routes/api.js
309
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 };
|
||||
|
||||
@@ -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
8
server.log
Normal 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
89
ssl-setup-guide.md
Normal 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
|
||||
Reference in New Issue
Block a user