Zulassen von Anonymen RFID updates verlinkung der UUID wenn spieler angelegt wurde

This commit is contained in:
2025-09-11 16:33:22 +02:00
parent d2a1bb16ea
commit 28616b3b0c
14 changed files with 2676 additions and 241 deletions

View File

@@ -820,21 +820,6 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => {
}
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(
@@ -881,25 +866,14 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => {
}
// Zeit in Datenbank einfügen
// Zeit mit RFIDUID in die Tabelle einfügen (player_id wird automatisch durch Trigger gesetzt)
const result = await pool.query(
`INSERT INTO times (player_id, location_id, recorded_time, created_at)
`INSERT INTO times (rfiduid, location_id, recorded_time, created_at)
VALUES ($1, $2, $3, $4)
RETURNING id, created_at`,
[player_id, location_id, recorded_time, new Date()]
RETURNING id, player_id, created_at`,
[rfiduid, 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) {
@@ -917,14 +891,13 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => {
// Sende WebSocket-Event an alle verbundenen Clients
io.emit('newTime', {
id: result.rows[0].id,
player_name: `${player.firstname} ${player.lastname}`,
player_id: result.rows[0].player_id,
rfiduid: rfiduid,
location_name: location.name,
recorded_time: recorded_time,
rank: rank,
created_at: result.rows[0].created_at
});
}
res.json({
@@ -932,8 +905,7 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => {
message: 'Zeit erfolgreich gespeichert',
data: {
id: result.rows[0].id,
player_id: player_id,
player_name: `${player.firstname} ${player.lastname}`,
player_id: result.rows[0].player_id,
rfiduid: rfiduid,
location_id: location_id,
location_name: location.name,
@@ -1028,6 +1000,130 @@ router.post('/v1/private/users/find', requireApiKey, async (req, res) => {
// RFID LINKING & USER MANAGEMENT ENDPOINTS (No API Key required for dashboard)
// ============================================================================
// Import blacklist module
const { checkNameAgainstBlacklist, addToBlacklist, removeFromBlacklist, getBlacklist } = require('../config/blacklist-db');
const { checkNameWithLLM, checkNameWithContext, testLLMConnection } = require('../config/llm-blacklist');
// Create new player with RFID and blacklist validation (no auth required for dashboard)
router.post('/v1/public/players/create-with-rfid', async (req, res) => {
const { rfiduid, firstname, lastname, birthdate, supabase_user_id } = req.body;
// Validierung
if (!rfiduid || !firstname || !lastname || !birthdate) {
return res.status(400).json({
success: false,
message: 'RFID UID, Vorname, Nachname und Geburtsdatum sind erforderlich'
});
}
try {
// LLM-basierte Blacklist-Prüfung
const llmCheck = await checkNameWithLLM(firstname, lastname);
if (llmCheck.isBlocked) {
return res.status(400).json({
success: false,
message: `Name nicht zulässig: ${llmCheck.reason}`,
details: llmCheck
});
}
// Fallback: Traditionelle Blacklist-Prüfung (optional)
const blacklistCheck = await checkNameAgainstBlacklist(firstname, lastname);
if (blacklistCheck.isBlocked) {
return res.status(400).json({
success: false,
message: `Name nicht zulässig: ${blacklistCheck.reason}`,
details: {
reason: blacklistCheck.reason,
category: blacklistCheck.category,
matchedTerm: blacklistCheck.matchedTerm
}
});
}
// Prüfen ob RFID UID bereits existiert
const existingRfid = await pool.query(
'SELECT id, firstname, lastname FROM players WHERE rfiduid = $1',
[rfiduid]
);
if (existingRfid.rows.length > 0) {
return res.status(409).json({
success: false,
message: 'RFID UID existiert bereits',
details: {
existingPlayer: {
firstname: existingRfid.rows[0].firstname,
lastname: existingRfid.rows[0].lastname
}
}
});
}
// Prüfen ob Supabase User bereits verknüpft ist (falls angegeben)
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'
});
}
}
// Geburtsdatum validieren
const birthDate = new Date(birthdate);
if (isNaN(birthDate.getTime())) {
return res.status(400).json({
success: false,
message: 'Ungültiges Geburtsdatum'
});
}
// Alter berechnen
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
// Spieler in Datenbank einfügen
const result = await pool.query(
`INSERT INTO players (rfiduid, firstname, lastname, birthdate, supabase_user_id, created_at, show_in_leaderboard)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, rfiduid, firstname, lastname, birthdate, created_at`,
[rfiduid, firstname, lastname, birthdate, supabase_user_id || null, new Date(), true]
);
const newPlayer = result.rows[0];
res.status(201).json({
success: true,
message: 'Spieler erfolgreich mit RFID erstellt',
data: {
id: newPlayer.id,
rfiduid: newPlayer.rfiduid,
firstname: newPlayer.firstname,
lastname: newPlayer.lastname,
birthdate: newPlayer.birthdate,
age: age,
created_at: newPlayer.created_at
}
});
} catch (error) {
console.error('Fehler beim Erstellen des Spielers mit RFID:', error);
res.status(500).json({
success: false,
message: 'Interner Serverfehler beim Erstellen des Spielers'
});
}
});
// Create new player with optional Supabase user linking (no auth required for dashboard)
router.post('/v1/public/players', async (req, res) => {
@@ -1148,10 +1244,25 @@ router.post('/v1/public/link-player', async (req, res) => {
[supabase_user_id, player_id]
);
const player = result.rows[0];
// Alle anonymen Zeiten (mit rfiduid aber ohne player_id) mit diesem Spieler verknüpfen
if (player.rfiduid) {
const updateTimesResult = await pool.query(
'UPDATE times SET player_id = $1 WHERE rfiduid = $2 AND player_id IS NULL',
[player_id, player.rfiduid]
);
console.log(`${updateTimesResult.rowCount} anonyme Zeiten mit Spieler ${player_id} verknüpft`);
}
res.json({
success: true,
message: 'Spieler erfolgreich verknüpft',
data: result.rows[0]
data: {
...player,
linked_times_count: player.rfiduid ? (await pool.query('SELECT COUNT(*) FROM times WHERE rfiduid = $1 AND player_id = $2', [player.rfiduid, player_id])).rows[0].count : 0
}
});
} catch (error) {
@@ -1407,6 +1518,236 @@ router.get('/v1/web/check-session', (req, res) => {
// ADMIN DASHBOARD ROUTES (/api/v1/admin/)
// ============================================================================
// Blacklist-Verwaltung
router.get('/v1/admin/blacklist', requireAdminAuth, async (req, res) => {
try {
const blacklist = await getBlacklist();
res.json({
success: true,
data: blacklist
});
} catch (error) {
console.error('Error loading blacklist:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Laden der Blacklist'
});
}
});
// Blacklist-Eintrag hinzufügen
router.post('/v1/admin/blacklist', requireAdminAuth, async (req, res) => {
const { term, category } = req.body;
if (!term || !category) {
return res.status(400).json({
success: false,
message: 'Begriff und Kategorie sind erforderlich'
});
}
try {
// Get admin user info for created_by field
const adminUser = req.session.username || 'admin';
await addToBlacklist(term, category, adminUser);
res.json({
success: true,
message: 'Begriff erfolgreich zur Blacklist hinzugefügt'
});
} catch (error) {
console.error('Error adding to blacklist:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Hinzufügen zur Blacklist'
});
}
});
// Blacklist-Eintrag entfernen
router.delete('/v1/admin/blacklist', requireAdminAuth, async (req, res) => {
const { term, category } = req.body;
if (!term || !category) {
return res.status(400).json({
success: false,
message: 'Begriff und Kategorie sind erforderlich'
});
}
try {
await removeFromBlacklist(term, category);
res.json({
success: true,
message: 'Begriff erfolgreich aus der Blacklist entfernt'
});
} catch (error) {
console.error('Error removing from blacklist:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Entfernen aus der Blacklist'
});
}
});
// Name gegen Blacklist testen
router.post('/v1/admin/blacklist/test', requireAdminAuth, async (req, res) => {
const { firstname, lastname } = req.body;
if (!firstname || !lastname) {
return res.status(400).json({
success: false,
message: 'Vorname und Nachname sind erforderlich'
});
}
try {
// LLM-Prüfung
const llmResult = await checkNameWithLLM(firstname, lastname);
// Traditionelle Blacklist-Prüfung
const blacklistResult = await checkNameAgainstBlacklist(firstname, lastname);
res.json({
success: true,
data: {
llm: llmResult,
blacklist: blacklistResult,
combined: {
isBlocked: llmResult.isBlocked || blacklistResult.isBlocked,
reason: llmResult.isBlocked ? llmResult.reason : blacklistResult.reason,
source: llmResult.isBlocked ? 'llm' : (blacklistResult.isBlocked ? 'blacklist' : 'none')
}
}
});
} catch (error) {
console.error('Error testing name against blacklist:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Testen des Namens'
});
}
});
// Blacklist-Statistiken
router.get('/v1/admin/blacklist/stats', requireAdminAuth, async (req, res) => {
try {
const result = await pool.query(`
SELECT
category,
COUNT(*) as count,
MAX(created_at) as last_added
FROM blacklist_terms
GROUP BY category
ORDER BY category
`);
const totalResult = await pool.query('SELECT COUNT(*) as total FROM blacklist_terms');
res.json({
success: true,
data: {
categories: result.rows,
total: parseInt(totalResult.rows[0].total)
}
});
} catch (error) {
console.error('Error loading blacklist stats:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Laden der Blacklist-Statistiken'
});
}
});
// LLM-Status prüfen
router.get('/v1/admin/llm/status', requireAdminAuth, async (req, res) => {
try {
const status = await testLLMConnection();
res.json({
success: true,
data: status
});
} catch (error) {
console.error('Error checking LLM status:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Prüfen des LLM-Status'
});
}
});
// LLM-Test mit Kontext
router.post('/v1/admin/llm/test', requireAdminAuth, async (req, res) => {
const { firstname, lastname, context } = req.body;
if (!firstname || !lastname) {
return res.status(400).json({
success: false,
message: 'Vorname und Nachname sind erforderlich'
});
}
try {
const result = await checkNameWithContext(firstname, lastname, context);
res.json({
success: true,
data: result
});
} catch (error) {
console.error('Error testing name with LLM:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Testen mit LLM'
});
}
});
// Levenshtein-Test (für Entwicklung/Debugging)
router.post('/v1/admin/blacklist/levenshtein-test', requireAdminAuth, async (req, res) => {
const { firstname, lastname, threshold } = req.body;
if (!firstname || !lastname) {
return res.status(400).json({
success: false,
message: 'Vorname und Nachname sind erforderlich'
});
}
try {
const { checkWithLevenshtein, THRESHOLDS } = require('../config/levenshtein');
const blacklist = await getBlacklist();
const testThreshold = threshold || 0.3;
const results = {};
// Teste jede Kategorie
for (const [category, terms] of Object.entries(blacklist)) {
const levenshteinResult = checkWithLevenshtein(firstname, lastname, terms, testThreshold);
results[category] = {
hasSimilarTerms: levenshteinResult.hasSimilarTerms,
similarTerms: levenshteinResult.similarTerms,
threshold: testThreshold,
categoryThreshold: THRESHOLDS[category] || 0.3
};
}
res.json({
success: true,
data: {
input: { firstname, lastname },
threshold: testThreshold,
results: results
}
});
} catch (error) {
console.error('Error testing Levenshtein:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Testen der Levenshtein-Distanz'
});
}
});
// Admin Statistiken
router.get('/v1/admin/stats', requireAdminAuth, async (req, res) => {
try {