Zulassen von Anonymen RFID updates verlinkung der UUID wenn spieler angelegt wurde
This commit is contained in:
413
routes/api.js
413
routes/api.js
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user