adding auth

This commit is contained in:
2026-05-06 11:56:38 +02:00
parent 1de68361a9
commit 86f3d13629
8 changed files with 431 additions and 59 deletions
+3 -3
View File
@@ -1,8 +1,8 @@
FROM php:8.3-fpm-alpine
RUN apk add --no-cache linux-headers \
&& docker-php-ext-install pcntl sockets || \
docker-php-ext-install pcntl
RUN apk add --no-cache linux-headers curl-dev \
&& docker-php-ext-install curl pcntl sockets || \
docker-php-ext-install curl pcntl
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
+3 -4
View File
@@ -1,10 +1,9 @@
FROM php:8.3-cli-alpine
RUN apk add --no-cache linux-headers git
RUN apk add --no-cache linux-headers curl-dev git
RUN apk add --no-cache --repository https://dl-cdn.alpinelinux.org/alpine/v3.20/main linux-headers \
&& docker-php-ext-install pcntl sockets || \
docker-php-ext-install pcntl
RUN docker-php-ext-install curl pcntl sockets 2>/dev/null || \
docker-php-ext-install curl pcntl
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
+151 -19
View File
@@ -30,22 +30,49 @@ body { font-size: .875rem; }
.toast-container { z-index: 1060; }
#detailModal .modal-body { max-height: 70vh; overflow-y: auto; }
pre.raw-line { background: var(--bs-tertiary-bg); padding: .75rem; border-radius: .375rem; font-size: .8rem; white-space: pre-wrap; word-break: break-all; max-height: 200px; overflow-y: auto; }
.login-container { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.login-card { max-width: 420px; width: 100%; }
.login-container .navbar-brand { font-size: 1.5rem; }
.login-footer { position: fixed; bottom: 1rem; left: 0; right: 0; text-align: center; font-size: .8rem; color: var(--bs-secondary-color); }
@media (max-width: 768px) { .sidebar { display: none; } .main { margin-left: 0; } }
</style>
</head>
<body>
<div id="appLogin" class="login-container">
<div class="login-card">
<div class="text-center mb-4">
<h3 class="navbar-brand"><i class="bi bi-terminal-plus"></i> Jakach Logging</h3>
<p class="text-secondary">Log analysis & alerting system</p>
</div>
<div class="card">
<div class="card-body p-4 text-center">
<i class="bi bi-shield-lock" style="font-size:3rem;display:block;margin-bottom:1rem"></i>
<h5>Sign in required</h5>
<p class="text-secondary mb-4">Authenticate with your Jakach account to access the system.</p>
<a id="loginBtn" class="btn btn-primary btn-lg w-100" href="#">
<i class="bi bi-box-arrow-in-right me-2"></i>Log in using Jakach Login
</a>
</div>
</div>
<div class="login-footer">Powered by <a href="https://auth.jakach.ch" class="text-secondary">Jakach Auth</a></div>
</div>
</div>
<div id="appMain" style="display:none">
<nav class="navbar navbar-expand navbar-dark bg-dark fixed-top">
<div class="container-fluid">
<a class="navbar-brand" href="#"><i class="bi bi-terminal-plus"></i>Jakach Logging</a>
<div class="d-flex align-items-center gap-2 ms-auto">
<span class="badge bg-danger d-none" id="criticalBadge">0</span>
<span class="badge bg-warning text-dark d-none" id="warningBadge">0</span>
<span class="text-secondary small me-2 d-none d-md-inline" id="userDisplay"></span>
<button class="btn btn-outline-secondary btn-sm" id="refreshBtn" title="Refresh"><i class="bi bi-arrow-clockwise"></i></button>
<div class="form-check form-switch ms-2">
<input class="form-check-input" type="checkbox" id="autoRefresh" checked>
<label class="form-check-label" for="autoRefresh" style="font-size:.8rem">Auto</label>
</div>
<button class="btn btn-outline-danger btn-sm" id="logoutBtn" title="Logout"><i class="bi bi-box-arrow-right"></i></button>
</div>
</div>
</nav>
@@ -74,7 +101,7 @@ pre.raw-line { background: var(--bs-tertiary-bg); padding: .75rem; border-radius
<div class="col-lg-8">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-bell me-1"></i>Recent Alerts</span>
<span><i class="bi bi-bell me-1"></i>Recent Critical Alerts</span>
<a href="#" data-page="alerts" class="btn btn-outline-secondary btn-sm">View all</a>
</div>
<div class="card-body p-0">
@@ -130,7 +157,6 @@ pre.raw-line { background: var(--bs-tertiary-bg); padding: .75rem; border-radius
</div>
<div class="card-footer d-flex justify-content-between align-items-center py-2">
<small class="text-secondary" id="alertsCount">0 alerts</small>
<nav><ul class="pagination pagination-sm mb-0" id="alertsPagination"></ul></nav>
</div>
</div>
</div>
@@ -182,24 +208,48 @@ pre.raw-line { background: var(--bs-tertiary-bg); padding: .75rem; border-radius
</div>
<div class="row g-3">
<div class="col-md-6">
<div class="card">
<div class="card mb-3">
<div class="card-header"><i class="bi bi-info-circle me-1"></i>System Info</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-4">Health</dt><dd class="col-sm-8"><span id="sysHealth" class="badge bg-secondary">checking...</span></dd>
<dt class="col-sm-4">Auth Server</dt><dd class="col-sm-8"><a href="https://auth.jakach.ch" target="_blank">auth.jakach.ch</a></dd>
<dt class="col-sm-4">Logged in as</dt><dd class="col-sm-8" id="settingsUser"></dd>
<dt class="col-sm-4">DB Path</dt><dd class="col-sm-8"><code>/app/data/logging.db</code></dd>
<dt class="col-sm-4">Worker</dt><dd class="col-sm-8"><code>php bin/consume</code></dd>
</dl>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header"><i class="bi bi-book me-1"></i>Quick Reference</div>
<div class="card-body">
<p class="mb-1"><strong>File sources</strong> — path to a log file on the worker container</p>
<p class="mb-1"><strong>TCP/UDP sources</strong><code>tcp://0.0.0.0:9514</code> or <code>udp://0.0.0.0:9514</code></p>
<p class="mb-0"><strong>Rules</strong> use PHP regex patterns, e.g. <code>/error/i</code></p>
<p class="mb-1"><strong>TCP/UDP sources</strong><code>tcp://0.0.0.0:9514</code></p>
<p class="mb-0"><strong>Rules</strong> — PHP regex patterns, e.g. <code>/error/i</code></p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3">
<div class="card-header"><i class="bi bi-shield-check me-1"></i>Security</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Allowed User Tokens</label>
<p class="small text-secondary">Only these Jakach user tokens can access this system. Leave empty to allow any authenticated Jakach user.</p>
<textarea class="form-control font-monospace" id="allowedTokensInput" rows="4" placeholder="One user_token per line"></textarea>
</div>
<button class="btn btn-primary btn-sm" id="saveTokensBtn"><i class="bi bi-floppy"></i> Save</button>
<small id="tokenSaveStatus" class="ms-2"></small>
</div>
</div>
<div class="card">
<div class="card-header"><i class="bi bi-info-circle me-1"></i>How to get your user token</div>
<div class="card-body small">
<ol class="mb-0 ps-3">
<li>Log in at <a href="https://auth.jakach.ch" target="_blank">auth.jakach.ch</a></li>
<li>Your <code>user_token</code> is shown in your profile</li>
<li>Paste it above to grant access</li>
</ol>
</div>
</div>
</div>
@@ -208,6 +258,7 @@ pre.raw-line { background: var(--bs-tertiary-bg); padding: .75rem; border-radius
</div>
</div>
</div>
<!-- Alert Detail Modal -->
<div class="modal fade" id="detailModal" tabindex="-1">
@@ -299,10 +350,45 @@ pre.raw-line { background: var(--bs-tertiary-bg); padding: .75rem; border-radius
<script>
const API = window.location.origin;
let state = { alerts: [], sources: [], rules: [], counts: [], alertPage: 0, alertPageSize: 50 };
let state = { alerts: [], sources: [], rules: [], counts: [], user: null };
let autoRefreshInterval = null;
let currentAlertId = null;
// --- Auth ---
async function checkAuth() {
try {
const res = await api('/auth/me');
if (res.user) {
state.user = res.user;
document.getElementById('userDisplay').textContent = res.user.username;
document.getElementById('settingsUser').textContent = res.user.username + ' (' + res.user.user_token.substring(0, 12) + '...)';
document.getElementById('appLogin').style.display = 'none';
document.getElementById('appMain').style.display = '';
initApp();
return true;
}
} catch (e) {}
showLogin();
return false;
}
function showLogin() {
document.getElementById('appLogin').style.display = '';
document.getElementById('appMain').style.display = 'none';
const loginUrl = window.location.origin + '/oauth.php?redirect=' + encodeURIComponent(window.location.href);
document.getElementById('loginBtn').href = 'https://auth.jakach.ch/?send_to=' + encodeURIComponent(loginUrl);
}
async function logout() {
try {
await api('/auth/logout', { method: 'POST' });
} catch (e) {}
state.user = null;
showLogin();
}
document.getElementById('logoutBtn').addEventListener('click', logout);
// --- Navigation ---
document.querySelectorAll('[data-page]').forEach(el => {
el.addEventListener('click', e => {
@@ -335,8 +421,15 @@ async function api(path, opts = {}) {
headers: { 'Accept': 'application/json', ...(opts.body ? { 'Content-Type': 'application/json' } : {}) },
...opts,
});
if (!res.ok) throw new Error(await res.text());
return res.json();
const data = await res.json();
if (!res.ok) {
if (res.status === 401 && data.login_url) {
window.location.href = data.login_url;
return;
}
throw new Error(data.error || 'Request failed');
}
return data;
}
function toast(msg, type = 'success') {
@@ -405,13 +498,12 @@ async function loadDashboard() {
} else {
tbody.innerHTML = alerts.map(a => `<tr class="alert-row" onclick="showAlert(${a.id})">
<td>${severityBadge(a.severity)}</td>
<td>${a.rule_name}</td>
<td>${esc(a.rule_name)}</td>
<td class="log-line">${esc(a.message)}</td>
<td class="text-secondary" style="white-space:nowrap">${timeAgo(a.created_at)}</td>
</tr>`).join('');
}
// Mini bar chart using CSS
const chartEl = document.getElementById('chartContainer');
const severityCounts = { critical: 0, warning: 0, info: 0 };
counts.forEach(c => { if (severityCounts[c.severity] !== undefined) severityCounts[c.severity] += parseInt(c.count); });
@@ -430,7 +522,7 @@ async function loadDashboard() {
async function loadAlerts() {
const severity = document.getElementById('filterSeverity').value;
const status = document.getElementById('filterStatus').value;
const params = new URLSearchParams({ limit: state.alertPageSize, offset: state.alertPage * state.alertPageSize });
const params = new URLSearchParams({ limit: 100, offset: 0 });
if (severity) params.set('severity', severity);
if (status) params.set('status', status);
@@ -464,7 +556,7 @@ function showAlert(id) {
document.getElementById('detailBody').innerHTML = `
<dl class="row mb-0">
<dt class="col-sm-3">ID</dt><dd class="col-sm-9">#${a.id}</dd>
<dt class="col-sm-3">Rule</dt><dd class="col-sm-9">${esc(a.rule_name)} (ID ${a.rule_id})</dd>
<dt class="col-sm-3">Rule</dt><dd class="col-sm-9">${esc(a.rule_name)}</dd>
<dt class="col-sm-3">Severity</dt><dd class="col-sm-9">${severityBadge(a.severity)}</dd>
<dt class="col-sm-3">Status</dt><dd class="col-sm-9">${statusBadge(a.status)}</dd>
<dt class="col-sm-3">Source</dt><dd class="col-sm-9">${esc(a.source_name || '—')}</dd>
@@ -489,8 +581,8 @@ async function ackAlert(id) {
} catch (e) { toast('Failed to acknowledge', 'danger'); }
}
document.getElementById('filterSeverity').addEventListener('change', () => { state.alertPage = 0; loadAlerts(); });
document.getElementById('filterStatus').addEventListener('change', () => { state.alertPage = 0; loadAlerts(); });
document.getElementById('filterSeverity').addEventListener('change', loadAlerts);
document.getElementById('filterStatus').addEventListener('change', loadAlerts);
document.getElementById('refreshAlertsBtn').addEventListener('click', loadAlerts);
// --- SOURCES ---
@@ -583,6 +675,7 @@ document.getElementById('ruleForm').addEventListener('submit', async e => {
} catch (err) { toast('Failed to add rule', 'danger'); }
});
// --- SETTINGS ---
async function loadSettings() {
try {
await api('/health');
@@ -592,8 +685,37 @@ async function loadSettings() {
document.getElementById('sysHealth').textContent = 'Unreachable';
document.getElementById('sysHealth').className = 'badge bg-danger';
}
try {
const res = await api('/config/allowed_tokens');
const tokens = res.tokens || [];
document.getElementById('allowedTokensInput').value = tokens.join('\n');
} catch (e) { console.error('load tokens error', e); }
}
document.getElementById('saveTokensBtn').addEventListener('click', async () => {
const raw = document.getElementById('allowedTokensInput').value;
const tokens = raw.split('\n').map(t => t.trim()).filter(t => t.length > 0);
const statusEl = document.getElementById('tokenSaveStatus');
statusEl.textContent = 'Saving...';
statusEl.className = 'ms-2 text-secondary';
try {
await api('/config/allowed_tokens', {
method: 'PUT',
body: JSON.stringify({ tokens }),
});
statusEl.textContent = 'Saved';
statusEl.className = 'ms-2 text-success';
toast('Allowed tokens updated');
setTimeout(() => { statusEl.textContent = ''; }, 3000);
} catch (e) {
statusEl.textContent = 'Failed';
statusEl.className = 'ms-2 text-danger';
toast('Failed to save tokens', 'danger');
}
});
// --- Helpers ---
function esc(s) {
const d = document.createElement('div');
d.textContent = s || '';
@@ -624,9 +746,19 @@ document.getElementById('refreshBtn').addEventListener('click', () => {
if (active) loadPage(active.id.replace('page-', ''));
});
// --- Bootstrap ---
startAutoRefresh();
loadDashboard();
// --- Init ---
function initApp() {
startAutoRefresh();
loadDashboard();
document.querySelectorAll('[data-page]').forEach(el => {
el.addEventListener('click', e => {
e.preventDefault();
showPage(el.dataset.page);
});
});
}
checkAuth();
</script>
</body>
</html>
+74
View File
@@ -0,0 +1,74 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use Jakach\Logging\Storage\Database;
session_set_cookie_params([
'lifetime' => 86400 * 7,
'path' => '/',
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
$authToken = $_GET['auth'] ?? '';
if (!$authToken) {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode(['error' => 'Missing auth token']);
exit;
}
$checkUrl = 'https://auth.jakach.ch/api/auth/check_auth_key.php?auth_token=' . urlencode($authToken);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $checkUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || !$response) {
header('Content-Type: application/json');
http_response_code(502);
echo json_encode(['error' => 'Auth server unreachable']);
exit;
}
$data = json_decode($response, true);
if (!isset($data['status']) || $data['status'] !== 'success') {
header('Content-Type: application/json');
http_response_code(401);
echo json_encode(['error' => 'Authentication failed', 'msg' => $data['msg'] ?? 'unknown']);
exit;
}
$_SESSION['loggedin'] = true;
$_SESSION['username'] = $data['username'] ?? 'unknown';
$_SESSION['id'] = $data['id'] ?? '';
$_SESSION['email'] = $data['email'] ?? '';
$_SESSION['telegram_id'] = $data['telegram_id'] ?? '';
$_SESSION['user_token'] = $data['user_token'] ?? '';
if (!headers_sent()) {
$db = new Database();
$repo = new \Jakach\Logging\Storage\Repository($db);
$allowedTokens = $repo->getAllowedUserTokens();
if (!empty($allowedTokens) && !in_array($_SESSION['user_token'], $allowedTokens, true)) {
$_SESSION = [];
session_destroy();
header('Content-Type: application/json');
http_response_code(403);
echo json_encode(['error' => 'Your account is not authorized to access this system']);
exit;
}
}
$redirect = $_GET['redirect'] ?? '/';
header('Location: ' . $redirect);
exit;
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace Jakach\Logging\Api;
use Jakach\Logging\Storage\Repository;
class AuthMiddleware
{
private Repository $repo;
public function __construct(Repository $repo)
{
$this->repo = $repo;
}
public function requireAuth(): ?array
{
if (session_status() === PHP_SESSION_NONE) {
session_set_cookie_params([
'lifetime' => 86400 * 7,
'path' => '/',
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
if (empty($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
return null;
}
$allowedTokens = $this->repo->getAllowedUserTokens();
if (!empty($allowedTokens)) {
$userToken = $_SESSION['user_token'] ?? '';
if (!in_array($userToken, $allowedTokens, true)) {
return null;
}
}
return [
'username' => $_SESSION['username'] ?? 'unknown',
'user_token' => $_SESSION['user_token'] ?? '',
'email' => $_SESSION['email'] ?? '',
];
}
}
+125 -33
View File
@@ -8,11 +8,13 @@ use Jakach\Logging\Storage\{Database, Repository};
class Router
{
private Repository $repo;
private AuthMiddleware $auth;
public function __construct()
{
$db = new Database();
$this->repo = new Repository($db);
$this->auth = new AuthMiddleware($this->repo);
}
public function handle(): void
@@ -20,51 +22,123 @@ class Router
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$path = rtrim($path, '/');
$path = $path ?: '/';
header('Content-Type: application/json');
try {
$result = match (true) {
$path === '/sources' && $method === 'GET' => $this->repo->getSources(),
$path === '/sources' && $method === 'POST' => $this->createSource(),
$path === '/rules' && $method === 'GET' => $this->repo->getRules(),
$path === '/rules' && $method === 'POST' => $this->createRule(),
$path === '/alerts' && $method === 'GET' => $this->getAlerts(),
preg_match('#^/alerts/(\d+)/ack$#', $path, $m) && $method === 'POST' => $this->ackAlert((int) $m[1]),
preg_match('#^/alerts/counts$#', $path) && $method === 'GET' => $this->repo->getAlertCounts(),
$path === '/health' && $method === 'GET' => ['status' => 'ok'],
default => throw new \RuntimeException('Not found', 404),
};
if ($result instanceof \UnitEnum || is_scalar($result)) {
$result = ['data' => $result];
} elseif (is_array($result) && !empty($result) && $result[array_key_first($result)] instanceof \UnitEnum) {
$result = ['data' => $result];
} elseif (is_array($result)) {
$needsWrap = false;
foreach ($result as $key => $val) {
if (is_object($val) && method_exists($val, 'toArray')) {
$result[$key] = $val->toArray();
} else {
$needsWrap = true;
}
$publicPaths = ['/health', '/oauth', '/auth/me', '/auth/logout'];
$isPublic = false;
foreach ($publicPaths as $pp) {
if ($path === $pp || str_starts_with($path, $pp . '/')) {
$isPublic = true;
break;
}
if ($needsWrap || empty($result)) {
$result = ['data' => $result];
}
} elseif (is_object($result) && method_exists($result, 'toArray')) {
$result = ['data' => $result->toArray()];
}
http_response_code(200);
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
if (!$isPublic) {
$user = $this->auth->requireAuth();
if (!$user) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized', 'login_url' => $this->loginUrl()]);
return;
}
}
$result = match (true) {
$path === '/health' && $method === 'GET' => ['status' => 'ok'],
$path === '/auth/me' && $method === 'GET' => $this->getMe(),
$path === '/auth/logout' && $method === 'POST' => $this->logout(),
$path === '/sources' && $method === 'GET' => $this->repo->getSources(),
$path === '/sources' && $method === 'POST' => $this->createSource(),
preg_match('#^/sources/(\d+)$#', $path, $m) && $method === 'DELETE'
=> $this->deleteEntity('source', (int) $m[1]),
$path === '/rules' && $method === 'GET' => $this->repo->getRules(),
$path === '/rules' && $method === 'POST' => $this->createRule(),
preg_match('#^/rules/(\d+)$#', $path, $m) && $method === 'DELETE'
=> $this->deleteEntity('rule', (int) $m[1]),
$path === '/alerts' && $method === 'GET' => $this->getAlerts(),
preg_match('#^/alerts/(\d+)/ack$#', $path, $m) && $method === 'POST'
=> $this->ackAlert((int) $m[1]),
preg_match('#^/alerts/counts$#', $path) && $method === 'GET'
=> $this->repo->getAlertCounts(),
$path === '/config/allowed_tokens' && $method === 'GET'
=> ['tokens' => $this->repo->getAllowedUserTokens()],
$path === '/config/allowed_tokens' && $method === 'PUT'
=> $this->updateAllowedTokens(),
default => throw new \RuntimeException('Not found', 404),
};
$this->respond(200, $result);
} catch (\RuntimeException $e) {
http_response_code($e->getCode() ?: 500);
echo json_encode(['error' => $e->getMessage()]);
$this->respond($e->getCode() ?: 500, ['error' => $e->getMessage()]);
}
}
private function respond(int $code, mixed $result): void
{
http_response_code($code);
if (is_array($result)) {
$hasObjects = false;
foreach ($result as $key => $val) {
if (is_object($val) && method_exists($val, 'toArray')) {
$result[$key] = $val->toArray();
$hasObjects = true;
}
}
if ($hasObjects) {
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
return;
}
if (array_is_list($result) && (empty($result) || !isset($result['data']))) {
$result = ['data' => $result];
}
} elseif (is_object($result) && method_exists($result, 'toArray')) {
$result = ['data' => $result->toArray()];
}
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
private function loginUrl(): string
{
$redirect = 'https://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . '/oauth.php';
return 'https://auth.jakach.ch/?send_to=' . urlencode($redirect);
}
private function getMe(): array
{
$user = $this->auth->requireAuth();
if (!$user) {
http_response_code(401);
return ['error' => 'Not logged in', 'login_url' => $this->loginUrl()];
}
return ['user' => $user];
}
private function logout(): array
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$_SESSION = [];
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
session_destroy();
return ['status' => 'logged_out'];
}
private function createSource(): array
{
$body = json_decode(file_get_contents('php://input'), true);
@@ -88,6 +162,16 @@ class Router
);
}
private function deleteEntity(string $type, int $id): array
{
if ($type === 'source') {
$this->repo->deleteSource($id);
} else {
$this->repo->deleteRule($id);
}
return ['status' => 'deleted', 'id' => $id];
}
private function getAlerts(): array
{
$limit = (int) ($_GET['limit'] ?? 100);
@@ -102,4 +186,12 @@ class Router
$this->repo->updateAlertStatus($id, AlertStatus::Acknowledged);
return ['status' => 'acknowledged', 'id' => $id];
}
private function updateAllowedTokens(): array
{
$body = json_decode(file_get_contents('php://input'), true);
$tokens = $body['tokens'] ?? [];
$this->repo->setAllowedUserTokens($tokens);
return ['status' => 'saved', 'tokens' => $this->repo->getAllowedUserTokens()];
}
}
+7
View File
@@ -83,5 +83,12 @@ class Database
PRIMARY KEY (rule_id, window_start)
)
");
$this->pdo->exec("
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
");
}
}
+22
View File
@@ -145,6 +145,28 @@ class Repository
)->fetchAll();
}
// --- Config ---
public function getAllowedUserTokens(): array
{
$stmt = $this->db->pdo()->prepare("SELECT value FROM config WHERE key = ?");
$stmt->execute(['allowed_user_tokens']);
$row = $stmt->fetch();
if (!$row || empty($row['value'])) {
return [];
}
return json_decode($row['value'], true) ?? [];
}
public function setAllowedUserTokens(array $tokens): void
{
$stmt = $this->db->pdo()->prepare(
"INSERT INTO config (key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value"
);
$stmt->execute(['allowed_user_tokens', json_encode(array_values($tokens))]);
}
// --- Rate Limiting ---
public function checkRateLimit(int $ruleId, int $windowSeconds): bool