diff --git a/intern/statistik/index.php b/intern/statistik/index.php
index 65ff087..05b1622 100644
--- a/intern/statistik/index.php
+++ b/intern/statistik/index.php
@@ -37,6 +37,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['username'], $_POST['p
if ($userOk && $passwordOk) {
session_regenerate_id(true);
$_SESSION['authenticated'] = true;
+ $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
header('Location: ./');
exit;
}
@@ -94,6 +95,51 @@ if (empty($_SESSION['authenticated'])):
exit;
endif;
+if (empty($_SESSION['csrf_token'])) $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
+$promoFlash = $_SESSION['promo_flash'] ?? null;
+unset($_SESSION['promo_flash']);
+$promoError = '';
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['promo_action'])) {
+ $providedCsrf = (string) ($_POST['csrf_token'] ?? '');
+ if (!hash_equals((string) $_SESSION['csrf_token'], $providedCsrf)) {
+ $promoError = 'Die Sitzung ist abgelaufen. Bitte lade die Seite neu und versuche es erneut.';
+ } else {
+ $reward = (string) ($_POST['reward'] ?? '');
+ $maxUses = filter_var($_POST['max_uses'] ?? '1', FILTER_VALIDATE_INT);
+ $note = trim((string) ($_POST['note'] ?? ''));
+ $noteLength = function_exists('mb_strlen') ? mb_strlen($note, 'UTF-8') : strlen($note);
+ $httpKey = trim((string) ($config['nakama_http_key'] ?? ''));
+ $rpcBase = preg_replace('~/private_stats$~', '/create_unlock_code', (string) ($config['nakama_rpc_url'] ?? ''));
+ if (!in_array($reward, ['all_towers', 'all_maps', 'all'], true)) {
+ $promoError = 'Bitte wähle einen gültigen Freischaltumfang.';
+ } elseif ($maxUses === false || $maxUses < 1 || $maxUses > 50000) {
+ $promoError = 'Die Anzahl der Einlösungen muss zwischen 1 und 50.000 liegen.';
+ } elseif ($noteLength > 160) {
+ $promoError = 'Die Bemerkung darf höchstens 160 Zeichen lang sein.';
+ } elseif ($httpKey === '' || !is_string($rpcBase) || $rpcBase === (string) ($config['nakama_rpc_url'] ?? '')) {
+ $promoError = 'Die interne Codeverwaltung ist noch nicht mit Nakama verbunden.';
+ } else {
+ $url = $rpcBase . '?http_key=' . rawurlencode($httpKey) . '&unwrap';
+ $response = fetchJson($url, 'POST', json_encode(['reward' => $reward, 'max_uses' => $maxUses, 'note' => $note], JSON_THROW_ON_ERROR));
+ if (is_array($response) && isset($response['payload']) && is_string($response['payload'])) {
+ $decodedPayload = json_decode($response['payload'], true);
+ if (is_array($decodedPayload)) $response = $decodedPayload;
+ }
+ if (is_array($response) && !empty($response['accepted']) && !empty($response['code'])) {
+ $_SESSION['promo_flash'] = [
+ 'code' => (string) $response['code'],
+ 'reward' => (string) ($response['reward'] ?? $reward),
+ 'max_uses' => (int) ($response['max_uses'] ?? $maxUses),
+ 'note' => (string) ($response['note'] ?? $note),
+ ];
+ header('Location: ./');
+ exit;
+ }
+ $promoError = 'Der Server konnte keinen Code erstellen. Prüfe die Verbindung und versuche es erneut.';
+ }
+ }
+}
+
$releases = fetchJson((string) $config['gitea_releases_api']) ?? [];
$downloadTotal = 0;
$latestDownloads = 0;
@@ -131,7 +177,48 @@ $players = (int) ($playerStats['total_players'] ?? 0);
$today = gmdate('Y-m-d');
$activeToday = (int) (($playerStats['daily_active'] ?? [])[$today] ?? 0);
$updatedAt = isset($playerStats['updated_at']) ? date('d.m.Y H:i', intdiv((int) $playerStats['updated_at'], 1000)) : 'Noch keine Daten';
+
+$promoCodes = null;
+$promoListError = '';
+if ($httpKey !== '') {
+ $listRpcUrl = preg_replace('~/private_stats$~', '/list_unlock_codes', (string) ($config['nakama_rpc_url'] ?? ''));
+ if (is_string($listRpcUrl) && $listRpcUrl !== (string) ($config['nakama_rpc_url'] ?? '')) {
+ $listUrl = $listRpcUrl . '?http_key=' . rawurlencode($httpKey) . '&unwrap';
+ $listResponse = fetchJson($listUrl, 'POST', '{}');
+ if (is_array($listResponse) && isset($listResponse['payload']) && is_string($listResponse['payload'])) {
+ $decodedPayload = json_decode($listResponse['payload'], true);
+ if (is_array($decodedPayload)) $listResponse = $decodedPayload;
+ }
+ if (is_array($listResponse) && isset($listResponse['codes']) && is_array($listResponse['codes'])) {
+ $promoCodes = $listResponse['codes'];
+ } else {
+ $promoListError = 'Die Code-Übersicht konnte gerade nicht geladen werden.';
+ }
+ } else {
+ $promoListError = 'Die interne Codeverwaltung ist noch nicht mit Nakama verbunden.';
+ }
+} else {
+ $promoListError = 'Die interne Codeverwaltung ist noch nicht mit Nakama verbunden.';
+}
?>
-
Void Bastion – Interne Statistik= $downloadTotal ?>Downloads insgesamt= $latestDownloads ?>Downloads aktuelles Release= $players ?>Registrierte Piloten= $activeToday ?>Heute aktive Piloten= $platformDownloads['windows'] ?>Windows-Downloads= $platformDownloads['android'] ?>Android-Downloads= e($latestRelease) ?>
Die Downloadwerte stammen direkt aus den Gitea-Release-Anhängen und werden alle fünf Minuten neu geladen.
Die private Nakama-Statistik ist noch nicht mit dem HTTP-Schlüssel verbunden.
Letzte Spieleraktualisierung: = e($updatedAt) ?>
Diese Seite ist nicht verlinkt, von Suchmaschinen ausgeschlossen und verlangt eine Anmeldung. Es werden ausschließlich zusammengefasste Werte dargestellt.
+
+Void Bastion – Interne Statistik
+= $downloadTotal ?>Downloads insgesamt= $latestDownloads ?>Downloads aktuelles Release= $players ?>Registrierte Piloten= $activeToday ?>Heute aktive Piloten= $platformDownloads['windows'] ?>Windows-Downloads= $platformDownloads['android'] ?>Android-Downloads
+= e($latestRelease) ?>
Die Downloadwerte stammen direkt aus den Gitea-Release-Anhängen und werden alle fünf Minuten neu geladen.
Die private Nakama-Statistik ist noch nicht mit dem HTTP-Schlüssel verbunden.
Letzte Spieleraktualisierung: = e($updatedAt) ?>
Diese Seite ist nicht verlinkt, von Suchmaschinen ausgeschlossen und verlangt eine Anmeldung. Es werden ausschließlich zusammengefasste Werte dargestellt.
+Freischaltcodes erstellen
Codes und Bemerkungen werden ausschließlich in der geschützten Serverablage verwaltet. Der Klartext ist nur in diesem angemeldeten Bereich sichtbar.
+Code erstellt:= e((string) $promoFlash['code']) ?>= e((string) $promoFlash['reward']) ?> · offen · maximal = (int) $promoFlash['max_uses'] ?> Einlösungen · = e((string) $promoFlash['note']) ?>
+= e($promoError) ?>
+
+= e($promoListError) ?>
Code-Übersicht
Noch keine Freischaltcodes vorhanden.
| Code | Freischaltung | Status | Einlösungen | Erstellt | Bemerkung |
= $maxUses ? 'redeemed' : ($uses > 0 ? 'partial' : 'open');
+$statusLabel = $status === 'redeemed' ? 'Eingelöst' : ($status === 'partial' ? 'Teilweise eingelöst' : 'Offen');
+$createdAt = (int) ($promoCode['created_at'] ?? 0);
+$createdLabel = $createdAt > 0 ? date('d.m.Y H:i', intdiv($createdAt, 1000)) : '—';
+$codeLabel = (string) ($promoCode['code'] ?? '');
+if ($codeLabel === '') $codeLabel = 'Alter Code – Klartext nicht gespeichert';
+?>= e($codeLabel) ?> | = e((string) ($promoCode['reward'] ?? '')) ?> | = e($statusLabel) ?> | = $uses ?> / = $maxUses ?> | = e($createdLabel) ?> | = e((string) (($promoCode['note'] ?? '') !== '' ? $promoCode['note'] : '—')) ?> |
+