Files
fuesse-sexy/server/playerLog.js
Guido 5018ab753a Admin-Panel (/admin): Auth, Spieler-Tracking, Live-Tuning, Bild-Verwaltung
- Passwortgeschütztes Panel unter /admin (Auto-Passwort 20 Zeichen, per CLI abrufbar:
  `node server/admin.js password` / `password reset`), In-Memory-Sessions + Cookie,
  Login-Rate-Limit.
- Spieler-Tracking: pro Score IP (CF/XFF/req.ip), Gerät/Browser (ua-parser-js),
  grober Ort (geoip-lite, offline) -> data/players.json; Aggregation pro Name.
- Live-Tuning: configStore (data/config.json) für Timer + Schwierigkeitskurve; Server
  (makeRound) und Client (/api/config) lesen daraus -> Änderungen ohne Neustart.
- Bild-Verwaltung: Upload/Löschen von Füßen & Deko (multer) + rescanLibrary().
- Panel-Aktionen: Spieler bannen/entbannen, Rangliste heute/alles leeren.
- Fix: .item object-fit:contain -> nicht-quadratische Bilder werden nicht mehr verzerrt.
- trust proxy für korrekte Client-IP hinter Reverse-Proxy/Cloudflare.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 22:19:52 +02:00

123 lines
3.6 KiB
JavaScript

// Spieler-Log: pro Score-Eintrag ein Datensatz mit IP, grobem Ort, Geraet & Browser.
// Persistiert in data/players.json (append, gedeckelt). Nur approximativ (kein Login).
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import UAParser from 'ua-parser-js';
import geoip from 'geoip-lite';
const __dirname = dirname(fileURLToPath(import.meta.url));
const DATA_DIR = join(__dirname, '..', 'data');
const PLAYERS_FILE = join(DATA_DIR, 'players.json');
const MAX_RECORDS = 5000;
function readAll() {
try {
if (!existsSync(PLAYERS_FILE)) return [];
const data = JSON.parse(readFileSync(PLAYERS_FILE, 'utf8'));
return Array.isArray(data) ? data : [];
} catch {
return [];
}
}
function writeAll(list) {
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
writeFileSync(PLAYERS_FILE, JSON.stringify(list, null, 2));
}
// User-Agent -> { device, browser, os }
function parseUA(ua) {
try {
const r = new UAParser(ua || '').getResult();
return {
device: r.device && r.device.type ? cap(r.device.type) : 'Desktop',
browser: [r.browser.name, r.browser.version].filter(Boolean).join(' ') || 'unbekannt',
os: [r.os.name, r.os.version].filter(Boolean).join(' ') || 'unbekannt',
};
} catch {
return { device: '?', browser: '?', os: '?' };
}
}
// IP -> { country, city } (offline, grob)
function geoLookup(ip, cfCountry) {
try {
const g = geoip.lookup(cleanIp(ip));
if (g) return { country: g.country || cfCountry || '', city: g.city || '' };
} catch {}
return { country: cfCountry || '', city: '' };
}
function cleanIp(ip) {
return String(ip || '').replace(/^::ffff:/, '');
}
const cap = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s);
// Einen Spiel-Abschluss protokollieren.
export function logGame({ name, score, ip, ua, cfCountry }) {
const { device, browser, os } = parseUA(ua);
const { country, city } = geoLookup(ip, cfCountry);
const list = readAll();
list.push({
name: String(name || 'Anonym'),
score: Number(score) || 0,
at: new Date().toISOString(),
ip: cleanIp(ip),
device,
browser,
os,
country,
city,
});
writeAll(list.slice(-MAX_RECORDS));
}
// Aggregiert die Log-Eintraege pro Spielername (case-insensitiv).
export function aggregatePlayers() {
const byName = new Map();
for (const r of readAll()) {
const key = String(r.name || '').trim().toLowerCase();
if (!byName.has(key)) {
byName.set(key, {
name: r.name,
games: 0,
bestScore: 0,
firstAt: r.at,
lastAt: r.at,
ips: new Set(),
locations: new Set(),
devices: new Set(),
browsers: new Set(),
oses: new Set(),
});
}
const p = byName.get(key);
p.games += 1;
p.bestScore = Math.max(p.bestScore, r.score || 0);
if (r.at < p.firstAt) p.firstAt = r.at;
if (r.at > p.lastAt) p.lastAt = r.at;
if (r.ip) p.ips.add(r.ip);
const loc = [r.city, r.country].filter(Boolean).join(', ');
if (loc) p.locations.add(loc);
if (r.device) p.devices.add(r.device);
if (r.browser) p.browsers.add(r.browser);
if (r.os) p.oses.add(r.os);
}
return [...byName.values()]
.map((p) => ({
name: p.name,
games: p.games,
bestScore: p.bestScore,
firstAt: p.firstAt,
lastAt: p.lastAt,
ips: [...p.ips],
locations: [...p.locations],
devices: [...p.devices],
browsers: [...p.browsers],
oses: [...p.oses],
}))
.sort((a, b) => new Date(b.lastAt) - new Date(a.lastAt));
}