forked from xziino/ff14-mitigator
fflogs-ids > admin
This commit is contained in:
parent
89d1ac0df1
commit
0f4d5a98d4
7
.env.example
Normal file
7
.env.example
Normal file
@ -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=
|
||||||
199
admin.php
Normal file
199
admin.php
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
session_start_safe();
|
||||||
|
|
||||||
|
if (empty($_SESSION['access_token']) || (($_SESSION['token_expires'] ?? 0) <= time())) {
|
||||||
|
header('Location: ' . auth_start_href(current_return_path()));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = current_fflogs_user();
|
||||||
|
if (!$user) {
|
||||||
|
http_response_code(502);
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowed = $user && is_admin_user();
|
||||||
|
if (!$allowed && $user) {
|
||||||
|
http_response_code(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($_SESSION['admin_csrf'])) {
|
||||||
|
$_SESSION['admin_csrf'] = bin2hex(random_bytes(16));
|
||||||
|
}
|
||||||
|
$csrf = $_SESSION['admin_csrf'];
|
||||||
|
|
||||||
|
function admin_h(?string $value): string {
|
||||||
|
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function git_run(array $args): array {
|
||||||
|
$cmd = 'git -C ' . escapeshellarg(__DIR__);
|
||||||
|
foreach ($args as $arg) {
|
||||||
|
$cmd .= ' ' . escapeshellarg($arg);
|
||||||
|
}
|
||||||
|
$cmd .= ' 2>&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)']);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Admin · FFLogs Report Viewer</title>
|
||||||
|
<link rel="stylesheet" href="css/base.css">
|
||||||
|
<link rel="stylesheet" href="css/layout.css">
|
||||||
|
<link rel="stylesheet" href="css/components.css">
|
||||||
|
<style>
|
||||||
|
.admin-shell { max-width: 980px; margin: 24px auto; width: min(92%, 980px); }
|
||||||
|
.admin-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
|
||||||
|
.admin-actions { display: flex; gap: 10px; flex-wrap: wrap; align-items: end; }
|
||||||
|
.admin-pre {
|
||||||
|
background: var(--bg2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--r);
|
||||||
|
padding: 14px;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
color: var(--t2);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.admin-meta { color: var(--t2); line-height: 1.8; font-size: 14px; }
|
||||||
|
.admin-deny { max-width: 720px; margin: 24px auto; width: min(92%, 720px); }
|
||||||
|
@media (max-width: 800px) { .admin-grid { grid-template-columns: 1fr; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<header id="topbar">
|
||||||
|
<a class="logo" href="index.php" style="text-decoration:none">REPORT VIEWER <span>Admin</span></a>
|
||||||
|
<div class="topbar-actions">
|
||||||
|
<a class="topbar-link" href="index.php">Back</a>
|
||||||
|
<div class="topbar-user">
|
||||||
|
<?= $user ? admin_h($user['name'] ?: 'FFLogs User') . ' #' . admin_h($user['id']) : 'FFLogs user unavailable' ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<?php if (!$user): ?>
|
||||||
|
<main class="admin-deny">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">User nicht verfügbar</div>
|
||||||
|
<p class="admin-meta">Ich konnte deine FFLogs User-ID nicht laden. <?= admin_h($_SESSION['fflogs_user_error'] ?? '') ?></p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<?php elseif (!$allowed): ?>
|
||||||
|
<main class="admin-deny">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">Kein Zugriff</div>
|
||||||
|
<p class="admin-meta">
|
||||||
|
Deine FFLogs User-ID ist <strong><?= admin_h($user['id']) ?></strong>.
|
||||||
|
Füge diese ID in deiner <code>.env</code> zu <code>ADMIN_USER_IDS</code> hinzu, um diese Seite freizuschalten.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<?php else: ?>
|
||||||
|
<main class="admin-shell">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">Git Admin</div>
|
||||||
|
<div class="admin-meta">
|
||||||
|
Branch: <strong><?= admin_h($currentBranch) ?></strong><br>
|
||||||
|
Last commit: <?= admin_h($lastCommit) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-grid section-gap">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">Aktionen</div>
|
||||||
|
<form method="post" class="admin-actions">
|
||||||
|
<input type="hidden" name="csrf" value="<?= admin_h($csrf) ?>">
|
||||||
|
<button class="btn" name="action" value="fetch" type="submit">Fetch</button>
|
||||||
|
<button class="btn btn-gold" name="action" value="pull" type="submit">Pull</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="post" class="admin-actions section-gap">
|
||||||
|
<input type="hidden" name="csrf" value="<?= admin_h($csrf) ?>">
|
||||||
|
<input type="hidden" name="action" value="switch">
|
||||||
|
<div class="fg">
|
||||||
|
<label for="branch">Branch</label>
|
||||||
|
<select id="branch" name="branch">
|
||||||
|
<?php foreach ($branches as $branch): ?>
|
||||||
|
<option value="<?= admin_h($branch) ?>" <?= $branch === $currentBranch ? 'selected' : '' ?>>
|
||||||
|
<?= admin_h($branch) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="btn" type="submit">Switch</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">Status</div>
|
||||||
|
<pre class="admin-pre"><?= admin_h($status ?: 'Clean') ?></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($result): ?>
|
||||||
|
<div class="card section-gap">
|
||||||
|
<div class="card-title"><?= admin_h($message ?? 'Output') ?> · Exit <?= (int)$result['code'] ?></div>
|
||||||
|
<pre class="admin-pre"><?= admin_h($result['output'] ?: '(no output)') ?></pre>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</main>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -67,6 +67,7 @@ if ($curlError || $status !== 200 || empty($data['access_token'])) {
|
|||||||
|
|
||||||
$_SESSION['access_token'] = $data['access_token'];
|
$_SESSION['access_token'] = $data['access_token'];
|
||||||
$_SESSION['token_expires'] = time() + ($data['expires_in'] ?? 3600);
|
$_SESSION['token_expires'] = time() + ($data['expires_in'] ?? 3600);
|
||||||
|
$_SESSION['fflogs_user'] = fetch_current_fflogs_user($data['access_token']);
|
||||||
|
|
||||||
header('Location: ' . $returnPath);
|
header('Location: ' . $returnPath);
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
93
config.php
93
config.php
@ -38,6 +38,11 @@ function env_bool(string $key, bool $default = false): bool {
|
|||||||
return in_array($value, ['1', 'true', 'yes', 'on'], true);
|
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');
|
load_env_file(__DIR__ . '/.env');
|
||||||
|
|
||||||
define('DEV_MODE', env_bool('DEV_MODE'));
|
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('AUTHORIZE_URI', env_value('AUTHORIZE_URI'));
|
||||||
define('TOKEN_URI', env_value('TOKEN_URI'));
|
define('TOKEN_URI', env_value('TOKEN_URI'));
|
||||||
define('GRAPHQL_URI', env_value('GRAPHQL_URI'));
|
define('GRAPHQL_URI', env_value('GRAPHQL_URI'));
|
||||||
|
define('ADMIN_USER_IDS', env_list('ADMIN_USER_IDS'));
|
||||||
|
|
||||||
function session_start_safe(): void {
|
function session_start_safe(): void {
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
@ -94,3 +100,90 @@ function current_return_path(): string {
|
|||||||
function auth_start_href(?string $returnPath = null): string {
|
function auth_start_href(?string $returnPath = null): string {
|
||||||
return 'auth/start.php?return=' . rawurlencode($returnPath ?? current_return_path());
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@ -100,12 +100,29 @@
|
|||||||
|
|
||||||
/* ── Topbar user badge ───────────────────────────────────────────────────────── */
|
/* ── Topbar user badge ───────────────────────────────────────────────────────── */
|
||||||
.topbar-user {
|
.topbar-user {
|
||||||
margin-left: auto;
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--t2);
|
color: var(--t2);
|
||||||
white-space: nowrap;
|
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 ───────────────────────────────────────────────────────────── */
|
||||||
#login-overlay {
|
#login-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@ -5,5 +5,16 @@
|
|||||||
<button class="tab" data-tab="analysis">⚖ Analyse</button>
|
<button class="tab" data-tab="analysis">⚖ Analyse</button>
|
||||||
<button class="tab" data-tab="planner">☰ Planer</button>
|
<button class="tab" data-tab="planner">☰ Planer</button>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="topbar-user">Token gültig bis: <?= date('Y-m-d H:i:s', $_SESSION['token_expires']) ?></div>
|
<?php $fflogsUser = current_fflogs_user(); ?>
|
||||||
|
<div class="topbar-actions">
|
||||||
|
<?php if (is_admin_user()): ?>
|
||||||
|
<a class="topbar-link" href="admin.php">Admin</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="topbar-user">
|
||||||
|
<?php if ($fflogsUser): ?>
|
||||||
|
<?= htmlspecialchars($fflogsUser['name'] ?: 'FFLogs User') ?> #<?= htmlspecialchars($fflogsUser['id']) ?> ·
|
||||||
|
<?php endif; ?>
|
||||||
|
Token gültig bis: <?= date('Y-m-d H:i:s', $_SESSION['token_expires']) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user