Files
Ninjaserver/routes/api.js
Carsten Graf 2becf784bd 🔧 Fix settings modal: Load current user preferences
- Added show_in_leaderboard to user-player API response
- Improved loadSettings() function with better error handling
- Added console logging for debugging
- Settings modal now shows current user preference instead of always 'off'
- Fixed dependency on currentPlayerId (now uses currentUser.id directly)
2025-09-08 19:18:35 +02:00

3003 lines
97 KiB
JavaScript

// routes/api.js
const express = require('express');
const { Pool } = require('pg');
const bcrypt = require('bcrypt');
const { start } = require('repl');
const pushService = require('../push-service');
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;
// Handle PostgreSQL interval object format
if (typeof interval === 'object' && interval.seconds !== undefined) {
return parseFloat(interval.seconds);
}
// 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'
});
}
}
// ============================================================================
// PUBLIC API ROUTES (/api/v1/public/)
// ============================================================================
// Login-Route (bleibt für Web-Interface)
router.post('/v1/public/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('/v1/public/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'
});
});
});
// ============================================================================
// PRIVATE API ROUTES (/api/v1/private/)
// ============================================================================
// API Endpunkt zum Speichern der Tokens (geschützt mit API-Key)
router.post('/v1/private/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('/v1/private/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('/v1/private/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('/v1/private/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('/v1/private/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('/v1/private/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'
});
}
});
// ============================================================================
// WEB-AUTHENTICATED ROUTES (/api/v1/web/)
// ============================================================================
// Neue Route zum Generieren eines API-Keys (nur für authentifizierte Web-Benutzer)
router.post('/v1/web/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('/v1/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('/v1/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('/v1/private/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('/v1/private/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('/v1/private/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) {
// Convert threshold to readable format
const thresholdMinutes = Math.floor(thresholdSeconds / 60);
const thresholdSecs = Math.floor(thresholdSeconds % 60);
const thresholdMs = Math.floor((thresholdSeconds % 1) * 1000);
const thresholdDisplay = thresholdMinutes > 0
? `${thresholdMinutes}:${thresholdSecs.toString().padStart(2, '0')}.${thresholdMs.toString().padStart(3, '0')}`
: `${thresholdSecs}.${thresholdMs.toString().padStart(3, '0')}`;
return res.status(400).json({
success: false,
message: `Zeit ${recorded_time} liegt unter dem Schwellenwert von ${thresholdDisplay} für diesen Standort`,
data: {
recorded_time: recorded_time,
threshold: location.time_threshold,
threshold_display: thresholdDisplay,
location_name: location_name
}
});
}
}
// 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()]
);
// Achievement-Überprüfung nach Zeit-Eingabe
try {
await pool.query('SELECT check_immediate_achievements($1)', [player_id]);
console.log(`✅ Achievement-Check für Spieler ${player_id} ausgeführt`);
} catch (achievementError) {
console.error('Fehler bei Achievement-Check:', achievementError);
// Achievement-Fehler sollen die Zeit-Eingabe nicht blockieren
}
// 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('/v1/private/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)
// ============================================================================
// Create new player with optional Supabase user linking (no auth required for dashboard)
router.post('/v1/public/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('/v1/public/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('/v1/public/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({
success: true,
data: timesResult.rows
});
} catch (error) {
console.error('Fehler beim Abrufen der Benutzerzeiten:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Abrufen der Benutzerzeiten'
});
}
});
/**
* @swagger
* /api/v1/public/user-player/{supabase_user_id}:
* get:
* summary: Spieler-Info anhand der Supabase User ID abrufen
* description: Ruft Spieler-Informationen für das Dashboard ab
* tags: [Public API]
* parameters:
* - in: path
* name: supabase_user_id
* required: true
* schema:
* type: string
* format: uuid
* description: Supabase User ID
* responses:
* 200:
* description: Spieler-Informationen
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* $ref: '#/components/schemas/Player'
* 404:
* description: Spieler nicht gefunden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server-Fehler
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// Get player info by Supabase user ID (no auth required for dashboard)
router.get('/v1/public/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, show_in_leaderboard 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('/v1/public/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('/v1/web/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 DASHBOARD ROUTES (/api/v1/admin/)
// ============================================================================
// Admin Statistiken
router.get('/v1/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('/v1/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('/v1/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('/v1/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'
});
}
});
// GET einzelner Lauf
router.get('/v1/admin/runs/:id', requireAdminAuth, async (req, res) => {
try {
const { id } = req.params;
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
WHERE t.id = $1
`, [id]);
if (result.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Lauf nicht gefunden'
});
}
res.json({
success: true,
data: result.rows[0]
});
} catch (error) {
console.error('Error loading run:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Laden des Laufs'
});
}
});
router.delete('/v1/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('/v1/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('/v1/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('/v1/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('/v1/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'
});
}
});
// ============================================================================
// PAGE VIEWS TRACKING
// ============================================================================
// Track page view
router.post('/v1/public/track-page-view', async (req, res) => {
try {
const { page, userAgent, ipAddress, referer } = req.body;
await pool.query(`
INSERT INTO page_views (page, user_agent, ip_address, referer)
VALUES ($1, $2, $3, $4)
`, [page, userAgent, ipAddress, referer]);
res.json({ success: true });
} catch (error) {
console.error('Error tracking page view:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Tracking der Seitenaufrufe'
});
}
});
// ============================================================================
// LEADERBOARD ROUTES (moved from public.js)
// ============================================================================
// Public endpoint für Standorte (keine Authentifizierung erforderlich)
router.get('/v1/public/locations', 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 Standorte'
});
}
});
/**
* @swagger
* /api/v1/public/times:
* get:
* summary: Alle Zeiten mit Standort-Informationen abrufen
* description: Ruft alle aufgezeichneten Zeiten mit Standort-Details ab
* tags: [Public API]
* parameters:
* - in: query
* name: location
* schema:
* type: string
* description: Filter nach Standort-ID
* - in: query
* name: limit
* schema:
* type: integer
* default: 100
* description: Maximale Anzahl der Ergebnisse
* responses:
* 200:
* description: Liste aller Zeiten
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: array
* items:
* type: object
* properties:
* id:
* type: string
* format: uuid
* player_id:
* type: string
* format: uuid
* location_id:
* type: string
* format: uuid
* recorded_time:
* type: object
* properties:
* seconds:
* type: number
* minutes:
* type: number
* milliseconds:
* type: number
* created_at:
* type: string
* format: date-time
* location_name:
* type: string
* latitude:
* type: number
* longitude:
* type: number
* 500:
* description: Server-Fehler
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// Public route to get times for location with parameter
router.get('/v1/public/times', async (req, res) => {
const { location } = req.query;
try {
// First, let's check if the view exists and has data
const viewCheck = await pool.query('SELECT COUNT(*) as count FROM "GetTimesWithPlayerAndLocation"');
// Check what location names are available
const availableLocations = await pool.query('SELECT DISTINCT location_name FROM "GetTimesWithPlayerAndLocation"');
// Now search for the specific location
const result = await pool.query('SELECT * FROM "GetTimesWithPlayerAndLocation" WHERE location_name = $1', [location]);
res.json({
success: true,
data: result.rows,
debug: {
searchedFor: location,
totalRecords: viewCheck.rows[0].count,
availableLocations: availableLocations.rows.map(r => r.location_name),
foundRecords: result.rows.length
}
});
} catch (error) {
console.error('❌ Fehler beim Abrufen der Zeiten:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Abrufen der Zeiten',
error: error.message
});
}
});
// Public route to get all times with player and location details for leaderboard
router.get('/v1/public/times-with-details', async (req, res) => {
try {
const { location, period } = req.query;
// Build WHERE clause for location filter
let locationFilter = '';
if (location && location !== 'all') {
locationFilter = `AND l.name ILIKE '%${location}%'`;
}
// Build WHERE clause for date filter using PostgreSQL timezone functions
let dateFilter = '';
if (period === 'today') {
// Today in local timezone (UTC+2)
dateFilter = `AND DATE(t.created_at AT TIME ZONE 'UTC' AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE`;
} else if (period === 'week') {
// This week starting from Monday in local timezone
dateFilter = `AND DATE(t.created_at AT TIME ZONE 'UTC' AT TIME ZONE 'Europe/Berlin') >= DATE_TRUNC('week', CURRENT_DATE)`;
} else if (period === 'month') {
// This month starting from 1st in local timezone
dateFilter = `AND DATE(t.created_at AT TIME ZONE 'UTC' AT TIME ZONE 'Europe/Berlin') >= DATE_TRUNC('month', CURRENT_DATE)`;
}
// Get all times with player and location details, ordered by time (fastest first)
// Only show times from players who have opted into leaderboard visibility
const result = await pool.query(`
SELECT
t.id,
EXTRACT(EPOCH FROM t.recorded_time) as recorded_time_seconds,
t.created_at,
json_build_object(
'id', p.id,
'firstname', p.firstname,
'lastname', p.lastname,
'rfiduid', p.rfiduid
) as player,
json_build_object(
'id', l.id,
'name', l.name,
'latitude', l.latitude,
'longitude', l.longitude
) as location
FROM times t
LEFT JOIN players p ON t.player_id = p.id
LEFT JOIN locations l ON t.location_id = l.id
WHERE 1=1 ${locationFilter} ${dateFilter}
AND p.show_in_leaderboard = true
ORDER BY t.recorded_time ASC
LIMIT 50
`);
// Convert seconds to minutes:seconds.milliseconds format
const formattedResults = result.rows.map(row => {
const totalSeconds = parseFloat(row.recorded_time_seconds);
const minutes = Math.floor(totalSeconds / 60);
const seconds = Math.floor(totalSeconds % 60);
const milliseconds = Math.floor((totalSeconds % 1) * 1000);
return {
...row,
recorded_time: {
minutes: minutes,
seconds: seconds,
milliseconds: milliseconds
}
};
});
res.json(formattedResults);
} catch (error) {
console.error('❌ Fehler beim Abrufen der Zeiten mit Details:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Abrufen der Zeiten mit Details',
error: error.message
});
}
});
// Get page statistics
router.get('/v1/admin/page-stats', requireAdminAuth, async (req, res) => {
try {
// Page views for today, this week, this month
const today = new Date();
const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate());
const startOfWeek = new Date(today);
startOfWeek.setDate(today.getDate() - today.getDay());
startOfWeek.setHours(0, 0, 0, 0);
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
// Today's page views
const todayViews = await pool.query(`
SELECT page, COUNT(*) as count
FROM page_views
WHERE created_at >= $1
GROUP BY page
ORDER BY count DESC
`, [startOfDay]);
// This week's page views
const weekViews = await pool.query(`
SELECT page, COUNT(*) as count
FROM page_views
WHERE created_at >= $1
GROUP BY page
ORDER BY count DESC
`, [startOfWeek]);
// This month's page views
const monthViews = await pool.query(`
SELECT page, COUNT(*) as count
FROM page_views
WHERE created_at >= $1
GROUP BY page
ORDER BY count DESC
`, [startOfMonth]);
// Total page views
const totalViews = await pool.query(`
SELECT page, COUNT(*) as count
FROM page_views
GROUP BY page
ORDER BY count DESC
`);
// Player/Supabase link statistics
const linkStats = await pool.query(`
SELECT
COUNT(*) as total_players,
COUNT(CASE WHEN supabase_user_id IS NOT NULL THEN 1 END) as linked_players,
CAST(
ROUND(
(COUNT(CASE WHEN supabase_user_id IS NOT NULL THEN 1 END)::numeric / COUNT(*)) * 100, 2
) AS DECIMAL(5,2)
) as link_percentage
FROM players
`);
res.json({
success: true,
data: {
today: todayViews.rows,
week: weekViews.rows,
month: monthViews.rows,
total: totalViews.rows,
linkStats: linkStats.rows[0]
}
});
} catch (error) {
console.error('Error loading page statistics:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Laden der Seitenstatistiken'
});
}
});
// ============================================================================
// POST/PUT ROUTES FÜR CRUD-OPERATIONEN
// ============================================================================
// Admin Spieler - POST (Hinzufügen)
router.post('/v1/admin/players', requireAdminAuth, async (req, res) => {
try {
const { full_name, rfiduid, supabase_user_id } = req.body;
// Name in firstname und lastname aufteilen
const nameParts = full_name ? full_name.trim().split(' ') : [];
const firstname = nameParts[0] || '';
const lastname = nameParts.slice(1).join(' ') || '';
const result = await pool.query(
`INSERT INTO players (firstname, lastname, rfiduid, supabase_user_id, created_at)
VALUES ($1, $2, $3, $4, NOW())
RETURNING *`,
[firstname, lastname, rfiduid || null, supabase_user_id || null]
);
res.json({
success: true,
message: 'Spieler erfolgreich hinzugefügt',
data: result.rows[0]
});
} catch (error) {
console.error('Error creating player:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Hinzufügen des Spielers'
});
}
});
// Admin Spieler - PUT (Bearbeiten)
router.put('/v1/admin/players/:id', requireAdminAuth, async (req, res) => {
try {
const playerId = req.params.id;
const { full_name, rfiduid, supabase_user_id } = req.body;
// Name in firstname und lastname aufteilen
const nameParts = full_name ? full_name.trim().split(' ') : [];
const firstname = nameParts[0] || '';
const lastname = nameParts.slice(1).join(' ') || '';
const result = await pool.query(
`UPDATE players
SET firstname = $1, lastname = $2, rfiduid = $3, supabase_user_id = $4
WHERE id = $5
RETURNING *`,
[firstname, lastname, rfiduid || null, supabase_user_id || null, playerId]
);
if (result.rowCount > 0) {
res.json({
success: true,
message: 'Spieler erfolgreich aktualisiert',
data: result.rows[0]
});
} else {
res.status(404).json({ success: false, message: 'Spieler nicht gefunden' });
}
} catch (error) {
console.error('Error updating player:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Aktualisieren des Spielers'
});
}
});
// Admin Standorte - POST (Hinzufügen)
router.post('/v1/admin/locations', requireAdminAuth, async (req, res) => {
try {
const { name, latitude, longitude, time_threshold } = req.body;
const result = await pool.query(
`INSERT INTO locations (name, latitude, longitude, time_threshold, created_at)
VALUES ($1, $2, $3, $4, NOW())
RETURNING *`,
[name, latitude, longitude, time_threshold || null]
);
res.json({
success: true,
message: 'Standort erfolgreich hinzugefügt',
data: result.rows[0]
});
} catch (error) {
console.error('Error creating location:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Hinzufügen des Standorts'
});
}
});
// Admin Standorte - PUT (Bearbeiten)
router.put('/v1/admin/locations/:id', requireAdminAuth, async (req, res) => {
try {
const locationId = req.params.id;
const { name, latitude, longitude, time_threshold } = req.body;
const result = await pool.query(
`UPDATE locations
SET name = $1, latitude = $2, longitude = $3, time_threshold = $4
WHERE id = $5
RETURNING *`,
[name, latitude, longitude, time_threshold || null, locationId]
);
if (result.rowCount > 0) {
res.json({
success: true,
message: 'Standort erfolgreich aktualisiert',
data: result.rows[0]
});
} else {
res.status(404).json({ success: false, message: 'Standort nicht gefunden' });
}
} catch (error) {
console.error('Error updating location:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Aktualisieren des Standorts'
});
}
});
// Admin Läufe - POST (Hinzufügen)
router.post('/v1/admin/runs', requireAdminAuth, async (req, res) => {
try {
const { player_id, location_id, time_seconds } = req.body;
// Prüfen ob Location existiert und Threshold abrufen
const locationResult = await pool.query(
'SELECT id, name, time_threshold FROM locations WHERE id = $1',
[location_id]
);
if (locationResult.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Standort nicht gefunden'
});
}
const location = locationResult.rows[0];
// Prüfen ob die Zeit über dem Schwellenwert liegt
if (location.time_threshold) {
const thresholdSeconds = convertIntervalToSeconds(location.time_threshold);
if (time_seconds < thresholdSeconds) {
return res.status(400).json({
success: false,
message: `Zeit ${time_seconds}s liegt unter dem Schwellenwert von ${location.time_threshold} für diesen Standort`,
data: {
recorded_time: `${time_seconds}s`,
threshold: location.time_threshold,
location_name: location.name
}
});
}
}
// Zeit in INTERVAL konvertieren
const timeInterval = `${time_seconds} seconds`;
const result = await pool.query(
`INSERT INTO times (player_id, location_id, recorded_time, created_at)
VALUES ($1, $2, $3, NOW())
RETURNING *`,
[player_id, location_id, timeInterval]
);
// Achievement-Überprüfung nach Zeit-Eingabe
try {
await pool.query('SELECT check_immediate_achievements($1)', [player_id]);
console.log(`✅ Achievement-Check für Spieler ${player_id} ausgeführt`);
} catch (achievementError) {
console.error('Fehler bei Achievement-Check:', achievementError);
// Achievement-Fehler sollen die Zeit-Eingabe nicht blockieren
}
res.json({
success: true,
message: 'Lauf erfolgreich hinzugefügt',
data: result.rows[0]
});
} catch (error) {
console.error('Error creating run:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Hinzufügen des Laufs'
});
}
});
// Admin Läufe - PUT (Bearbeiten)
router.put('/v1/admin/runs/:id', requireAdminAuth, async (req, res) => {
try {
const runId = req.params.id;
const { player_id, location_id, time_seconds } = req.body;
// Zeit in INTERVAL konvertieren
const timeInterval = `${time_seconds} seconds`;
const result = await pool.query(
`UPDATE times
SET player_id = $1, location_id = $2, recorded_time = $3
WHERE id = $4
RETURNING *`,
[player_id, location_id, timeInterval, runId]
);
if (result.rowCount > 0) {
res.json({
success: true,
message: 'Lauf erfolgreich aktualisiert',
data: result.rows[0]
});
} else {
res.status(404).json({ success: false, message: 'Lauf nicht gefunden' });
}
} catch (error) {
console.error('Error updating run:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Aktualisieren des Laufs'
});
}
});
// Admin-Benutzer - POST (Hinzufügen)
router.post('/v1/admin/adminusers', requireAdminAuth, async (req, res) => {
try {
const { username, password, access_level } = req.body;
// Passwort hashen
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
const result = await pool.query(
`INSERT INTO adminusers (username, password_hash, access_level, is_active, created_at)
VALUES ($1, $2, $3, true, NOW())
RETURNING id, username, access_level, is_active, created_at`,
[username, hashedPassword, access_level]
);
res.json({
success: true,
message: 'Admin-Benutzer erfolgreich hinzugefügt',
data: result.rows[0]
});
} catch (error) {
console.error('Error creating admin user:', error);
if (error.code === '23505') { // Unique constraint violation
res.status(400).json({
success: false,
message: 'Benutzername bereits vergeben'
});
} else {
res.status(500).json({
success: false,
message: 'Fehler beim Hinzufügen des Admin-Benutzers'
});
}
}
});
// Admin-Benutzer - PUT (Bearbeiten)
router.put('/v1/admin/adminusers/:id', requireAdminAuth, async (req, res) => {
try {
const userId = req.params.id;
const { username, password, access_level } = req.body;
// Verhindern, dass sich selbst bearbeitet
if (parseInt(userId) === req.session.userId) {
return res.status(400).json({
success: false,
message: 'Sie können sich nicht selbst bearbeiten'
});
}
let query, params;
if (password) {
// Passwort hashen
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
query = `UPDATE adminusers
SET username = $1, password_hash = $2, access_level = $3
WHERE id = $4
RETURNING id, username, access_level, is_active, created_at`;
params = [username, hashedPassword, access_level, userId];
} else {
query = `UPDATE adminusers
SET username = $1, access_level = $2
WHERE id = $3
RETURNING id, username, access_level, is_active, created_at`;
params = [username, access_level, userId];
}
const result = await pool.query(query, params);
if (result.rowCount > 0) {
res.json({
success: true,
message: 'Admin-Benutzer erfolgreich aktualisiert',
data: result.rows[0]
});
} else {
res.status(404).json({ success: false, message: 'Admin-Benutzer nicht gefunden' });
}
} catch (error) {
console.error('Error updating admin user:', error);
if (error.code === '23505') { // Unique constraint violation
res.status(400).json({
success: false,
message: 'Benutzername bereits vergeben'
});
} else {
res.status(500).json({
success: false,
message: 'Fehler beim Aktualisieren des Admin-Benutzers'
});
}
}
});
// ==================== BEST TIMES ENDPOINTS ====================
/**
* @swagger
* /api/v1/public/best-times:
* get:
* summary: Beste Zeiten abrufen
* description: Ruft die besten Zeiten des Tages, der Woche und des Monats ab
* tags: [Times]
* responses:
* 200:
* description: Beste Zeiten erfolgreich abgerufen
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: object
* properties:
* daily:
* type: object
* properties:
* player_id:
* type: string
* format: uuid
* player_name:
* type: string
* best_time:
* type: string
* format: time
* weekly:
* type: object
* properties:
* player_id:
* type: string
* format: uuid
* player_name:
* type: string
* best_time:
* type: string
* format: time
* monthly:
* type: object
* properties:
* player_id:
* type: string
* format: uuid
* player_name:
* type: string
* best_time:
* type: string
* format: time
* 500:
* description: Server-Fehler
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// Get best times (daily, weekly, monthly)
router.get('/v1/public/best-times', async (req, res) => {
try {
const currentDate = new Date();
const today = currentDate.toISOString().split('T')[0];
const weekStart = new Date(currentDate.setDate(currentDate.getDate() - currentDate.getDay()));
const monthStart = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
// Get daily best
const dailyResult = await pool.query(`
WITH daily_best AS (
SELECT
t.player_id,
MIN(t.recorded_time) as best_time,
CONCAT(p.firstname, ' ', p.lastname) as player_name
FROM times t
JOIN players p ON t.player_id = p.id
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = $1
AND p.show_in_leaderboard = true
GROUP BY t.player_id, p.firstname, p.lastname
)
SELECT
player_id,
player_name,
best_time
FROM daily_best
WHERE best_time = (SELECT MIN(best_time) FROM daily_best)
LIMIT 1
`, [today]);
// Get weekly best
const weeklyResult = await pool.query(`
WITH weekly_best AS (
SELECT
t.player_id,
MIN(t.recorded_time) as best_time,
CONCAT(p.firstname, ' ', p.lastname) as player_name
FROM times t
JOIN players p ON t.player_id = p.id
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $2
AND p.show_in_leaderboard = true
GROUP BY t.player_id, p.firstname, p.lastname
)
SELECT
player_id,
player_name,
best_time
FROM weekly_best
WHERE best_time = (SELECT MIN(best_time) FROM weekly_best)
LIMIT 1
`, [weekStart.toISOString().split('T')[0], today]);
// Get monthly best
const monthlyResult = await pool.query(`
WITH monthly_best AS (
SELECT
t.player_id,
MIN(t.recorded_time) as best_time,
CONCAT(p.firstname, ' ', p.lastname) as player_name
FROM times t
JOIN players p ON t.player_id = p.id
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $2
AND p.show_in_leaderboard = true
GROUP BY t.player_id, p.firstname, p.lastname
)
SELECT
player_id,
player_name,
best_time
FROM monthly_best
WHERE best_time = (SELECT MIN(best_time) FROM monthly_best)
LIMIT 1
`, [monthStart.toISOString().split('T')[0], today]);
res.json({
success: true,
data: {
daily: dailyResult.rows[0] || null,
weekly: weeklyResult.rows[0] || null,
monthly: monthlyResult.rows[0] || null
}
});
} catch (error) {
console.error('Error fetching best times:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Abrufen der Bestzeiten',
error: error.message
});
}
});
// ==================== PUSH NOTIFICATION ENDPOINTS ====================
// Subscribe to push notifications
router.post('/v1/public/subscribe', async (req, res) => {
try {
const { endpoint, keys } = req.body;
const userId = req.session.userId || 'anonymous';
// Generate a UUID for anonymous users or use existing UUID
let playerId;
if (userId === 'anonymous') {
// Generate a random UUID for anonymous users
const { v4: uuidv4 } = require('uuid');
playerId = uuidv4();
} else {
playerId = userId;
}
// Store subscription in database
await pool.query(`
INSERT INTO player_subscriptions (player_id, endpoint, p256dh, auth, created_at)
VALUES ($1, $2, $3, $4, NOW())
ON CONFLICT (player_id)
DO UPDATE SET
endpoint = EXCLUDED.endpoint,
p256dh = EXCLUDED.p256dh,
auth = EXCLUDED.auth,
updated_at = NOW()
`, [
playerId,
endpoint,
keys.p256dh,
keys.auth
]);
// Also store in push service for immediate use
const subscription = {
endpoint: endpoint,
keys: {
p256dh: keys.p256dh,
auth: keys.auth
}
};
pushService.subscribe(playerId, subscription);
res.json({
success: true,
message: 'Push subscription erfolgreich gespeichert'
});
} catch (error) {
console.error('Error storing push subscription:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Speichern der Push Subscription',
error: error.message
});
}
});
// Test push notification endpoint
router.post('/v1/public/test-push', async (req, res) => {
try {
const { userId, message } = req.body;
const payload = {
title: '🧪 Test Notification',
body: message || 'Das ist eine Test-Push-Notification!',
icon: '/pictures/icon-192.png',
badge: '/pictures/icon-192.png',
data: {
type: 'test',
timestamp: Date.now()
}
};
const success = await pushService.sendToUser(userId || 'anonymous', payload);
res.json({
success: success,
message: success ? 'Test-Push gesendet!' : 'Keine Subscription gefunden'
});
} catch (error) {
console.error('Error sending test push:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Senden der Test-Push'
});
}
});
// Get push subscription status
router.get('/v1/public/push-status', async (req, res) => {
try {
const userId = req.session.userId || 'anonymous';
// For anonymous users, we can't check specific subscription
// but we can show general stats
const hasSubscription = userId !== 'anonymous' ? pushService.subscriptions.has(userId) : false;
res.json({
success: true,
data: {
hasSubscription: hasSubscription,
totalSubscriptions: pushService.getSubscriptionCount(),
subscribedUsers: pushService.getSubscribedUsers(),
isAnonymous: userId === 'anonymous'
}
});
} catch (error) {
console.error('Error getting push status:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Abrufen des Push-Status'
});
}
});
// ==================== ACHIEVEMENT ENDPOINTS ====================
/**
* @swagger
* /api/achievements:
* get:
* summary: Alle verfügbaren Achievements abrufen
* description: Ruft alle aktiven Achievements im System ab
* tags: [Achievements]
* responses:
* 200:
* description: Liste aller Achievements
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: array
* items:
* $ref: '#/components/schemas/Achievement'
* 500:
* description: Server-Fehler
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// Get all achievements
router.get('/achievements', async (req, res) => {
try {
const result = await pool.query(`
SELECT id, name, description, category, icon, points, is_active
FROM achievements
WHERE is_active = true
ORDER BY category, points DESC
`);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('Error fetching achievements:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Laden der Achievements'
});
}
});
/**
* @swagger
* /api/achievements/player/{playerId}:
* get:
* summary: Achievements eines Spielers abrufen
* description: Ruft alle Achievements für einen bestimmten Spieler ab
* tags: [Achievements]
* parameters:
* - in: path
* name: playerId
* required: true
* schema:
* type: string
* format: uuid
* description: Eindeutige Spieler-ID
* responses:
* 200:
* description: Liste der Spieler-Achievements
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: array
* items:
* allOf:
* - $ref: '#/components/schemas/Achievement'
* - $ref: '#/components/schemas/PlayerAchievement'
* 500:
* description: Server-Fehler
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// Get player achievements
router.get('/achievements/player/:playerId', async (req, res) => {
try {
// Set no-cache headers
res.set({
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
});
const { playerId } = req.params;
const result = await pool.query(`
SELECT
a.id,
a.name,
a.description,
a.category,
a.icon,
a.points,
COALESCE(pa.progress, 0) as progress,
COALESCE(pa.is_completed, false) as is_completed,
pa.earned_at
FROM achievements a
LEFT JOIN player_achievements pa ON a.id = pa.achievement_id AND pa.player_id = $1
WHERE a.is_active = true
ORDER BY
pa.is_completed DESC,
a.category,
a.points DESC
`, [playerId]);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('Error fetching player achievements:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Laden der Spieler-Achievements'
});
}
});
// Get player achievement statistics
router.get('/achievements/player/:playerId/stats', async (req, res) => {
try {
// Set no-cache headers
res.set({
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
});
const { playerId } = req.params;
// Get total available achievements
const totalResult = await pool.query(`
SELECT COUNT(*) as total_achievements
FROM achievements
WHERE is_active = true
`);
// Get player's earned achievements
const playerResult = await pool.query(`
SELECT
COUNT(CASE WHEN pa.is_completed = true THEN 1 END) as completed_achievements,
SUM(CASE WHEN pa.is_completed = true THEN a.points ELSE 0 END) as total_points,
COUNT(CASE WHEN pa.is_completed = true AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE THEN 1 END) as achievements_today
FROM achievements a
LEFT JOIN player_achievements pa ON a.id = pa.achievement_id AND pa.player_id = $1
WHERE a.is_active = true
`, [playerId]);
const totalStats = totalResult.rows[0];
const playerStats = playerResult.rows[0];
const totalAchievements = parseInt(totalStats.total_achievements);
const completedAchievements = parseInt(playerStats.completed_achievements) || 0;
const completionPercentage = totalAchievements > 0 ? Math.round((completedAchievements / totalAchievements) * 100) : 0;
res.json({
success: true,
data: {
total_achievements: totalAchievements,
completed_achievements: completedAchievements,
total_points: parseInt(playerStats.total_points) || 0,
achievements_today: parseInt(playerStats.achievements_today) || 0,
completion_percentage: completionPercentage
}
});
} catch (error) {
console.error('Error fetching player achievement stats:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Laden der Achievement-Statistiken'
});
}
});
// Check achievements for a specific player
router.post('/achievements/check/:playerId', async (req, res) => {
try {
const { playerId } = req.params;
// Verify player exists
const playerCheck = await pool.query('SELECT id FROM players WHERE id = $1', [playerId]);
if (playerCheck.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Spieler nicht gefunden'
});
}
// Run achievement check
await pool.query('SELECT check_all_achievements($1)', [playerId]);
// Get newly earned achievements
const newAchievements = await pool.query(`
SELECT a.name, a.description, a.icon, a.points
FROM player_achievements pa
INNER JOIN achievements a ON pa.achievement_id = a.id
WHERE pa.player_id = $1
AND pa.is_completed = true
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
ORDER BY pa.earned_at DESC
`, [playerId]);
res.json({
success: true,
message: 'Achievement-Check abgeschlossen',
data: {
new_achievements: newAchievements.rows,
count: newAchievements.rows.length
}
});
} catch (error) {
console.error('Error checking achievements:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Überprüfen der Achievements'
});
}
});
// Run daily achievement check for all players
router.post('/achievements/daily-check', async (req, res) => {
try {
// This endpoint runs the daily achievement check
const { runDailyAchievements } = require('../scripts/daily_achievements');
await runDailyAchievements();
res.json({
success: true,
message: 'Tägliche Achievement-Überprüfung abgeschlossen'
});
} catch (error) {
console.error('Error running daily achievement check:', error);
res.status(500).json({
success: false,
message: 'Fehler bei der täglichen Achievement-Überprüfung'
});
}
});
// Run best-time achievement check (for manual testing or cron jobs)
router.post('/achievements/best-time-check', async (req, res) => {
try {
// This endpoint runs the best-time achievement check
const { runBestTimeAchievements } = require('../scripts/best_time_achievements');
await runBestTimeAchievements();
res.json({
success: true,
message: 'Best-Time Achievement-Überprüfung abgeschlossen'
});
} catch (error) {
console.error('Error running best-time achievement check:', error);
res.status(500).json({
success: false,
message: 'Fehler bei der Best-Time Achievement-Überprüfung'
});
}
});
// Get achievement leaderboard
router.get('/achievements/leaderboard', async (req, res) => {
try {
const { limit = 10 } = req.query;
const result = await pool.query(`
SELECT
p.firstname,
p.lastname,
COUNT(pa.id) as completed_achievements,
SUM(a.points) as total_points
FROM players p
INNER JOIN player_achievements pa ON p.id = pa.player_id
INNER JOIN achievements a ON pa.achievement_id = a.id
WHERE pa.is_completed = true
GROUP BY p.id, p.firstname, p.lastname
ORDER BY total_points DESC, completed_achievements DESC
LIMIT $1
`, [parseInt(limit)]);
res.json({
success: true,
data: result.rows.map((row, index) => ({
rank: index + 1,
name: `${row.firstname} ${row.lastname}`,
completed_achievements: parseInt(row.completed_achievements),
total_points: parseInt(row.total_points)
}))
});
} catch (error) {
console.error('Error fetching achievement leaderboard:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Laden der Bestenliste'
});
}
});
// Update player settings (privacy settings) - Public endpoint for authenticated users
router.post('/v1/public/update-player-settings', async (req, res) => {
try {
const { player_id, show_in_leaderboard } = req.body;
if (!player_id) {
return res.status(400).json({
success: false,
message: 'Player ID ist erforderlich'
});
}
// Update player settings
const updateQuery = `
UPDATE players
SET show_in_leaderboard = $1, updated_at = NOW()
WHERE id = $2
RETURNING id, firstname, lastname, show_in_leaderboard
`;
const result = await pool.query(updateQuery, [show_in_leaderboard || false, player_id]);
if (result.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Spieler nicht gefunden'
});
}
res.json({
success: true,
message: 'Einstellungen erfolgreich aktualisiert',
data: result.rows[0]
});
} catch (error) {
console.error('Error updating player settings:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Aktualisieren der Einstellungen'
});
}
});
// Update player settings (privacy settings) - Private endpoint with API key
router.post('/v1/private/update-player-settings', requireApiKey, async (req, res) => {
try {
const { player_id, show_in_leaderboard } = req.body;
if (!player_id) {
return res.status(400).json({
success: false,
message: 'Player ID ist erforderlich'
});
}
// Update player settings
const updateQuery = `
UPDATE players
SET show_in_leaderboard = $1, updated_at = NOW()
WHERE id = $2
RETURNING id, firstname, lastname, show_in_leaderboard
`;
const result = await pool.query(updateQuery, [show_in_leaderboard || false, player_id]);
if (result.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Spieler nicht gefunden'
});
}
res.json({
success: true,
message: 'Einstellungen erfolgreich aktualisiert',
data: result.rows[0]
});
} catch (error) {
console.error('Error updating player settings:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Aktualisieren der Einstellungen'
});
}
});
module.exports = { router, requireApiKey };