adding auth
This commit is contained in:
+151
-19
@@ -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>
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user