Files
Cs2-Mapveto/server/src/adminRoutes.js
T
mregosar bfe315d70c Admin: Pausieren und Fortsetzen des laufenden Matches
Neue Admin-Aktionen server/pause und server/resume senden
mp_pause_match bzw. mp_unpause_match an den CS2-Server. Nur bei
konfiguriertem Server verfügbar.
2026-10-26 00:48:25 +01:00

121 lines
4.6 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('server/pause', (v) => manager.setPaused(v, true));
action('server/resume', (v) => manager.setPaused(v, false));
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;
}