diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..b932e56
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,7 @@
+DEV_MODE=true
+CLIENT_ID=
+REDIRECT_URI=http://localhost:8080/auth/callback.php
+AUTHORIZE_URI=https://www.fflogs.com/oauth/authorize
+TOKEN_URI=https://www.fflogs.com/oauth/token
+GRAPHQL_URI=https://www.fflogs.com/api/v2/user
+ADMIN_USER_IDS=
diff --git a/admin.php b/admin.php
new file mode 100644
index 0000000..5376a27
--- /dev/null
+++ b/admin.php
@@ -0,0 +1,199 @@
+&1';
+ exec($cmd, $lines, $code);
+ return ['code' => $code, 'output' => trim(implode("\n", $lines))];
+}
+
+function git_output(array $args): string {
+ return git_run($args)['output'];
+}
+
+function git_local_branches(): array {
+ $out = git_output(['branch', '--format=%(refname:short)']);
+ $branches = array_values(array_filter(array_map('trim', explode("\n", $out)), fn($v) => $v !== ''));
+ sort($branches, SORT_NATURAL | SORT_FLAG_CASE);
+ return $branches;
+}
+
+$message = null;
+$result = null;
+
+if ($allowed && $_SERVER['REQUEST_METHOD'] === 'POST') {
+ if (!hash_equals($csrf, $_POST['csrf'] ?? '')) {
+ http_response_code(400);
+ $message = 'Invalid CSRF token.';
+ } else {
+ $action = $_POST['action'] ?? '';
+ if ($action === 'fetch') {
+ $result = git_run(['fetch', '--prune']);
+ $message = 'git fetch --prune';
+ } elseif ($action === 'pull') {
+ $result = git_run(['pull', '--ff-only']);
+ $message = 'git pull --ff-only';
+ } elseif ($action === 'switch') {
+ $branch = trim((string)($_POST['branch'] ?? ''));
+ $branches = git_local_branches();
+ if (!in_array($branch, $branches, true)) {
+ $result = ['code' => 1, 'output' => 'Unknown or non-local branch: ' . $branch];
+ } else {
+ $result = git_run(['switch', $branch]);
+ $message = 'git switch ' . $branch;
+ }
+ }
+ }
+}
+
+$currentBranch = '';
+$status = '';
+$branches = [];
+$lastCommit = '';
+if ($allowed) {
+ $currentBranch = git_output(['rev-parse', '--abbrev-ref', 'HEAD']);
+ $status = git_output(['status', '-sb']);
+ $branches = git_local_branches();
+ $lastCommit = git_output(['log', '-1', '--pretty=%h %s (%cr)']);
+}
+?>
+
+
+
+
+
+ Admin · FFLogs Report Viewer
+
+
+
+
+
+
+
+
+
+
+
+
+
User nicht verfügbar
+
Ich konnte deine FFLogs User-ID nicht laden. = admin_h($_SESSION['fflogs_user_error'] ?? '') ?>
+
+
+
+
+
+
Kein Zugriff
+
+ Deine FFLogs User-ID ist = admin_h($user['id']) ?>.
+ Füge diese ID in deiner .env zu ADMIN_USER_IDS hinzu, um diese Seite freizuschalten.
+
+
+
+
+
+
+
Git Admin
+
+ Branch: = admin_h($currentBranch) ?>
+ Last commit: = admin_h($lastCommit) ?>
+
+
+
+
+
+
+
+
Status
+
= admin_h($status ?: 'Clean') ?>
+
+
+
+
+
+
= admin_h($message ?? 'Output') ?> · Exit = (int)$result['code'] ?>
+
= admin_h($result['output'] ?: '(no output)') ?>
+
+
+
+
+
+
+
diff --git a/auth/callback.php b/auth/callback.php
index b96897d..5dd9af0 100644
--- a/auth/callback.php
+++ b/auth/callback.php
@@ -67,6 +67,7 @@ if ($curlError || $status !== 200 || empty($data['access_token'])) {
$_SESSION['access_token'] = $data['access_token'];
$_SESSION['token_expires'] = time() + ($data['expires_in'] ?? 3600);
+$_SESSION['fflogs_user'] = fetch_current_fflogs_user($data['access_token']);
header('Location: ' . $returnPath);
exit;
diff --git a/config.php b/config.php
index 29ee653..8fd386f 100644
--- a/config.php
+++ b/config.php
@@ -38,6 +38,11 @@ function env_bool(string $key, bool $default = false): bool {
return in_array($value, ['1', 'true', 'yes', 'on'], true);
}
+function env_list(string $key): array {
+ $value = env_value($key, '');
+ return array_values(array_filter(array_map('trim', explode(',', $value)), fn($v) => $v !== ''));
+}
+
load_env_file(__DIR__ . '/.env');
define('DEV_MODE', env_bool('DEV_MODE'));
@@ -46,6 +51,7 @@ define('REDIRECT_URI', env_value('REDIRECT_URI'));
define('AUTHORIZE_URI', env_value('AUTHORIZE_URI'));
define('TOKEN_URI', env_value('TOKEN_URI'));
define('GRAPHQL_URI', env_value('GRAPHQL_URI'));
+define('ADMIN_USER_IDS', env_list('ADMIN_USER_IDS'));
function session_start_safe(): void {
if (session_status() === PHP_SESSION_NONE) {
@@ -94,3 +100,90 @@ function current_return_path(): string {
function auth_start_href(?string $returnPath = null): string {
return 'auth/start.php?return=' . rawurlencode($returnPath ?? current_return_path());
}
+
+function fflogs_graphql_user_query(string $query, string $token): array {
+ $payload = json_encode(['query' => $query]);
+ if ($payload === false) {
+ return ['error' => 'Could not encode GraphQL payload'];
+ }
+
+ if (function_exists('curl_init')) {
+ $ch = curl_init(GRAPHQL_URI);
+ curl_setopt_array($ch, [
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => $payload,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_HTTPHEADER => [
+ 'Content-Type: application/json',
+ 'Authorization: Bearer ' . $token,
+ ],
+ CURLOPT_SSL_VERIFYPEER => !DEV_MODE,
+ ]);
+ $body = curl_exec($ch);
+ $err = curl_error($ch);
+ $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+ if ($err) return ['error' => $err, 'status' => $code];
+ return ['body' => $body, 'status' => $code];
+ }
+
+ $context = stream_context_create([
+ 'http' => [
+ 'method' => 'POST',
+ 'content' => $payload,
+ 'header' => implode("\r\n", [
+ 'Content-Type: application/json',
+ 'Authorization: Bearer ' . $token,
+ ]),
+ 'timeout' => 30,
+ ],
+ 'ssl' => [
+ 'verify_peer' => !DEV_MODE,
+ 'verify_peer_name' => !DEV_MODE,
+ ],
+ ]);
+ $body = file_get_contents(GRAPHQL_URI, false, $context);
+ if ($body === false) return ['error' => 'GraphQL request failed'];
+ return ['body' => $body, 'status' => 200];
+}
+
+function fetch_current_fflogs_user(string $token): ?array {
+ $result = fflogs_graphql_user_query('query { userData { currentUser { id name } } }', $token);
+ if (!empty($result['error'])) {
+ $_SESSION['fflogs_user_error'] = $result['error'];
+ return null;
+ }
+
+ $data = json_decode((string)($result['body'] ?? ''), true);
+ $user = $data['data']['userData']['currentUser'] ?? null;
+ if (!is_array($user) || empty($user['id'])) {
+ $_SESSION['fflogs_user_error'] = $data['errors'][0]['message'] ?? 'Could not read current FFLogs user';
+ return null;
+ }
+
+ return [
+ 'id' => (string)$user['id'],
+ 'name' => (string)($user['name'] ?? ''),
+ ];
+}
+
+function current_fflogs_user(bool $forceRefresh = false): ?array {
+ if (!$forceRefresh && !empty($_SESSION['fflogs_user']) && is_array($_SESSION['fflogs_user'])) {
+ return $_SESSION['fflogs_user'];
+ }
+ if (empty($_SESSION['access_token']) || (($_SESSION['token_expires'] ?? 0) <= time())) {
+ return null;
+ }
+
+ $user = fetch_current_fflogs_user($_SESSION['access_token']);
+ if ($user !== null) {
+ $_SESSION['fflogs_user'] = $user;
+ unset($_SESSION['fflogs_user_error']);
+ }
+ return $user;
+}
+
+function is_admin_user(): bool {
+ $user = current_fflogs_user();
+ return $user !== null && in_array((string)$user['id'], ADMIN_USER_IDS, true);
+}
diff --git a/css/layout.css b/css/layout.css
index 416ca74..866a347 100644
--- a/css/layout.css
+++ b/css/layout.css
@@ -100,12 +100,29 @@
/* ── Topbar user badge ───────────────────────────────────────────────────────── */
.topbar-user {
- margin-left: auto;
font-size: 13px;
color: var(--t2);
white-space: nowrap;
}
+.topbar-actions {
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.topbar-link {
+ color: var(--gold);
+ font-size: 13px;
+ text-decoration: none;
+ border: 1px solid var(--borderem);
+ border-radius: var(--r);
+ padding: 5px 10px;
+ background: var(--bg2);
+}
+.topbar-link:hover { background: var(--bg3); }
+
/* ── Login Overlay ───────────────────────────────────────────────────────────── */
#login-overlay {
position: fixed;
diff --git a/templates/topbar.php b/templates/topbar.php
index f6376c4..b9ecde8 100644
--- a/templates/topbar.php
+++ b/templates/topbar.php
@@ -5,5 +5,16 @@
- Token gültig bis: = date('Y-m-d H:i:s', $_SESSION['token_expires']) ?>
+
+
+
+
Admin
+
+
+
+ = htmlspecialchars($fflogsUser['name'] ?: 'FFLogs User') ?> #= htmlspecialchars($fflogsUser['id']) ?> ·
+
+ Token gültig bis: = date('Y-m-d H:i:s', $_SESSION['token_expires']) ?>
+
+