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>
This commit is contained in:
98
server/adminAuth.js
Normal file
98
server/adminAuth.js
Normal file
@@ -0,0 +1,98 @@
|
||||
// Admin-Authentifizierung: automatisch generiertes Passwort (20 Zeichen), persistent in
|
||||
// data/admin.json (Klartext, chmod 600 -> per CLI abrufbar). Sessions liegen im Speicher.
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { randomBytes, randomInt, timingSafeEqual } from 'node:crypto';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const DATA_DIR = join(__dirname, '..', 'data');
|
||||
const ADMIN_FILE = join(DATA_DIR, 'admin.json');
|
||||
|
||||
const LOWER = 'abcdefghijkmnopqrstuvwxyz';
|
||||
const UPPER = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
|
||||
const DIGITS = '23456789';
|
||||
const SPECIAL = '!@#$%&*()-_=+?';
|
||||
const ALL = LOWER + UPPER + DIGITS + SPECIAL;
|
||||
const PW_LEN = 20;
|
||||
|
||||
const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 Tage
|
||||
const sessions = new Map(); // token -> expiresAt
|
||||
|
||||
function generatePassword() {
|
||||
// je 1 Zeichen aus jeder Klasse garantieren, Rest zufaellig, dann mischen
|
||||
const pick = (set) => set[randomInt(set.length)];
|
||||
const chars = [pick(LOWER), pick(UPPER), pick(DIGITS), pick(SPECIAL)];
|
||||
while (chars.length < PW_LEN) chars.push(pick(ALL));
|
||||
for (let i = chars.length - 1; i > 0; i--) {
|
||||
const j = randomInt(i + 1);
|
||||
[chars[i], chars[j]] = [chars[j], chars[i]];
|
||||
}
|
||||
return chars.join('');
|
||||
}
|
||||
|
||||
function write(data) {
|
||||
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
|
||||
writeFileSync(ADMIN_FILE, JSON.stringify(data, null, 2));
|
||||
try {
|
||||
chmodSync(ADMIN_FILE, 0o600);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Liest die Admin-Daten; legt beim ersten Mal Passwort + Secret an.
|
||||
function ensure() {
|
||||
if (existsSync(ADMIN_FILE)) {
|
||||
try {
|
||||
const data = JSON.parse(readFileSync(ADMIN_FILE, 'utf8'));
|
||||
if (data && data.password) return data;
|
||||
} catch {}
|
||||
}
|
||||
const data = { password: generatePassword(), secret: randomBytes(32).toString('hex') };
|
||||
write(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getPassword() {
|
||||
return ensure().password;
|
||||
}
|
||||
|
||||
export function resetPassword() {
|
||||
const data = ensure();
|
||||
data.password = generatePassword();
|
||||
write(data);
|
||||
return data.password;
|
||||
}
|
||||
|
||||
export function verifyPassword(input) {
|
||||
const real = ensure().password;
|
||||
const a = Buffer.from(String(input || ''));
|
||||
const b = Buffer.from(real);
|
||||
if (a.length !== b.length) return false;
|
||||
return timingSafeEqual(a, b);
|
||||
}
|
||||
|
||||
// --- Sessions (In-Memory) ---
|
||||
export function createSession() {
|
||||
const token = randomBytes(32).toString('hex');
|
||||
sessions.set(token, Date.now() + SESSION_TTL_MS);
|
||||
return token;
|
||||
}
|
||||
|
||||
export function validSession(token) {
|
||||
if (!token) return false;
|
||||
const exp = sessions.get(token);
|
||||
if (!exp) return false;
|
||||
if (Date.now() > exp) {
|
||||
sessions.delete(token);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function destroySession(token) {
|
||||
if (token) sessions.delete(token);
|
||||
}
|
||||
|
||||
export const SESSION_COOKIE = 'admin_session';
|
||||
export const SESSION_MAX_AGE = SESSION_TTL_MS;
|
||||
Reference in New Issue
Block a user