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
+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;