1585 lines
50 KiB
JavaScript
1585 lines
50 KiB
JavaScript
// 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, 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'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Erfolgreich angemeldet',
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
access_level: user.access_level
|
|
}
|
|
});
|
|
});
|
|
|
|
} 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'
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
|
|
// ============================================================================
|
|
// 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'
|
|
});
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// ADMIN DASHBOARD ROUTES
|
|
// ============================================================================
|
|
|
|
// Middleware für Admin-Authentifizierung
|
|
function requireAdminAuth(req, res, next) {
|
|
if (!req.session.userId) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
message: 'Authentifizierung erforderlich'
|
|
});
|
|
}
|
|
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'
|
|
});
|
|
}
|
|
next();
|
|
}
|
|
|
|
// Session-Check für Dashboard
|
|
router.get('/check-session', (req, res) => {
|
|
if (req.session.userId) {
|
|
res.json({
|
|
success: true,
|
|
user: {
|
|
id: req.session.userId,
|
|
username: req.session.username,
|
|
access_level: req.session.accessLevel || 1
|
|
}
|
|
});
|
|
} else {
|
|
res.status(401).json({
|
|
success: false,
|
|
message: 'Not authenticated'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Admin Statistiken
|
|
router.get('/admin-stats', requireAdminAuth, async (req, res) => {
|
|
try {
|
|
const playersResult = await pool.query('SELECT COUNT(*) FROM players');
|
|
const runsResult = await pool.query('SELECT COUNT(*) FROM times');
|
|
const locationsResult = await pool.query('SELECT COUNT(*) FROM locations');
|
|
const adminUsersResult = await pool.query('SELECT COUNT(*) FROM adminusers');
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
players: parseInt(playersResult.rows[0].count),
|
|
runs: parseInt(runsResult.rows[0].count),
|
|
locations: parseInt(locationsResult.rows[0].count),
|
|
adminUsers: parseInt(adminUsersResult.rows[0].count)
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading admin stats:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Fehler beim Laden der Statistiken'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Admin Spieler-Verwaltung
|
|
router.get('/admin-players', requireAdminAuth, async (req, res) => {
|
|
try {
|
|
const result = await pool.query(`
|
|
SELECT
|
|
p.*,
|
|
COALESCE(CONCAT(p.firstname, ' ', p.lastname), p.firstname, p.lastname) as full_name,
|
|
CASE WHEN p.supabase_user_id IS NOT NULL THEN true ELSE false END as has_supabase_link
|
|
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'
|
|
});
|
|
}
|
|
});
|
|
|
|
router.delete('/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 {
|
|
res.status(404).json({ success: false, message: 'Spieler nicht gefunden' });
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting player:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Fehler beim Löschen des Spielers'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Admin Läufe-Verwaltung
|
|
router.get('/admin-runs', requireAdminAuth, async (req, res) => {
|
|
try {
|
|
const result = await pool.query(`
|
|
SELECT
|
|
t.id,
|
|
t.player_id,
|
|
t.location_id,
|
|
t.recorded_time,
|
|
EXTRACT(EPOCH FROM t.recorded_time) as time_seconds,
|
|
t.created_at,
|
|
COALESCE(CONCAT(p.firstname, ' ', p.lastname), p.firstname, p.lastname) as player_name,
|
|
l.name as location_name
|
|
FROM times t
|
|
LEFT JOIN players p ON t.player_id = p.id
|
|
LEFT JOIN locations l ON t.location_id = l.id
|
|
ORDER BY t.created_at DESC
|
|
LIMIT 1000
|
|
`);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.rows
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading runs:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Fehler beim Laden der Läufe'
|
|
});
|
|
}
|
|
});
|
|
|
|
router.delete('/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 {
|
|
res.status(404).json({ success: false, message: 'Lauf nicht gefunden' });
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting run:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Fehler beim Löschen des Laufs'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Admin Standort-Verwaltung
|
|
router.get('/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'
|
|
});
|
|
}
|
|
});
|
|
|
|
router.delete('/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.`
|
|
});
|
|
}
|
|
|
|
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 {
|
|
res.status(404).json({ success: false, message: 'Standort nicht gefunden' });
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting location:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Fehler beim Löschen des Standorts'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Admin-Benutzer-Verwaltung
|
|
router.get('/admin-adminusers', requireAdminAuth, async (req, res) => {
|
|
try {
|
|
const result = await pool.query(`
|
|
SELECT id, username, access_level, is_active, created_at, last_login
|
|
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'
|
|
});
|
|
}
|
|
});
|
|
|
|
router.delete('/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'
|
|
});
|
|
}
|
|
|
|
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 {
|
|
res.status(404).json({ success: false, message: 'Admin-Benutzer nicht gefunden' });
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting admin user:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Fehler beim Löschen des Admin-Benutzers'
|
|
});
|
|
}
|
|
});
|
|
|
|
module.exports = { router, requireApiKey };
|