Files
Cs2-Mapveto/server/src/adminRoutes.js
T
mregosar 3bfe087b7d Rollback: Runden aus CS2-Sicherungen zurückspielen (nur Admin)
Beim Konfigurieren aktiviert CS2 Mapveto die Rundensicherung des Servers
(mp_backup_round_auto, Präfix mit Match-ID). Im Admin-Modus lassen sich
die Sicherungen der laufenden Map auflisten und zurückspielen. Der
Live-Stand wird auf die Runden bis zur Sicherung gekürzt. Organisatoren
haben keinen Zugang zu den Rollback-Routen.

Sicherheit: Dateinamen werden gegen Match-ID und Muster geprüft, damit
keine fremden Dateien oder Befehle in die RCON-Zeile gelangen.
2026-10-25 23:24:49 +01:00

119 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import express from 'express';
import { HttpError } from './matchService.js';
import { createRateLimiter, randomToken, safeEqual } from './security.js';
const SESSION_HOURS = 12;
function bearer(req) {
const header = req.get('authorization') ?? '';
return header.startsWith('Bearer ') ? header.slice(7).trim() : null;
}
/**
* Admin-Modus unter /api/admin. Anmeldung mit ADMIN_PASSWORD, danach ein zufälliger Sitzungs-Token
* (nur im Speicher, verfällt nach 12 Stunden oder beim Neustart). Admin-Aktionen nutzen die bestehenden
* Organisator-Funktionen mit einem Admin-Zugang, die Berechtigungen bleiben also an einer Stelle.
*/
export function createAdminRouter({ service, manager, config }) {
const router = express.Router();
const sessions = new Map(); // Token -> Ablaufzeit (ms)
const loginLimit = createRateLimiter({ windowMs: 60_000, max: 5 });
const wrap = (fn) => async (req, res, next) => {
try {
const result = await fn(req, res);
res.json(result ?? { ok: true });
} catch (err) {
next(err);
}
};
const requireEnabled = (req, res, next) => {
if (config.adminPassword) return next();
return next(new HttpError(404, 'Admin-Modus ist nicht eingerichtet (ADMIN_PASSWORD fehlt)', 'admin_disabled'));
};
const requireAdmin = (req, res, next) => {
const token = bearer(req);
const expires = token && sessions.get(token);
if (expires && expires > Date.now()) return next();
if (token) sessions.delete(token);
return next(new HttpError(401, 'Admin-Anmeldung erforderlich', 'unauthorized'));
};
// Admin-Zugang für eine Match-Aktion: wie ein Organisator, bei Bedarf als Captain eines Teams
const adminViewer = (matchId, extra = {}) => ({
matchId,
role: 'organizer',
isOrganizer: true,
isCaptain: false,
isSpectator: false,
playerId: null,
team: null,
...extra,
});
router.use(requireEnabled);
router.post('/login', (req, res, next) => {
if (!loginLimit(`admin:${req.ip}`)) {
return next(new HttpError(429, 'Zu viele Anmeldeversuche – bitte eine Minute warten', 'rate_limited'));
}
if (!safeEqual(String(req.body?.password ?? ''), config.adminPassword)) {
return next(new HttpError(401, 'Falsches Passwort', 'unauthorized'));
}
const token = randomToken();
const expiresAt = Date.now() + SESSION_HOURS * 3_600_000;
sessions.set(token, expiresAt);
return res.json({ token, expiresAt });
});
router.post('/logout', (req, res) => {
const token = bearer(req);
if (token) sessions.delete(token);
res.json({ ok: true });
});
router.get('/session', requireAdmin, (req, res) => res.json({ ok: true }));
// Alle Matches mit allen Details (Codes, Verbindungsdaten, Spieler, Veto, Serie, Ereignisse)
router.get('/matches', requireAdmin, wrap(() => {
const ids = service.db.prepare('SELECT id FROM matches ORDER BY created_at DESC LIMIT 100').all();
return { matches: ids.map(({ id }) => service.snapshot(id, adminViewer(id))) };
}));
const action = (path, fn) => router.post(`/matches/:id/${path}`, requireAdmin, wrap((req) => {
service.getMatch(req.params.id);
return fn(adminViewer(req.params.id), req.body ?? {}, req);
}));
action('finish', (v) => service.close(v, 'finished'));
action('cancel', (v) => service.close(v, 'cancelled'));
action('server/stop', (v) => manager.stopByOrganizer(v));
action('server/retry', (v) => manager.retry(v));
action('server/warmup-end', (v) => manager.endWarmup(v));
action('series/next', (v) => manager.loadNextMap(v));
action('veto/start', (v) => service.startVeto(v));
action('veto/action', (v, b) => service.vetoAction(
adminViewer(v.matchId, { team: b.team, isCaptain: true, role: 'captain' }),
{ stepIndex: b.stepIndex, map: b.map, side: b.side },
));
action('kick', (v, b) => service.kick(v, b.playerId));
action('codes/regenerate', (v, b) => service.regenerateCode(v, b.target));
// Rollback: nur für Admins. Diese Routen gibt es für Organisatoren nicht.
router.get('/matches/:id/rollback-points', requireAdmin, wrap(async (req) => {
service.getMatch(req.params.id);
return { points: await manager.rollbackPoints(adminViewer(req.params.id)) };
}));
action('rollback', (v, b) => manager.rollback(v, b.file));
// Match endgültig löschen (Server wird vorher freigegeben)
router.delete('/matches/:id', requireAdmin, wrap(async (req) => {
await manager.deleteMatch(req.params.id);
return { ok: true };
}));
return router;
}