diff --git a/backend/api/index.php b/backend/api/index.php index 9137913..b2f4099 100644 --- a/backend/api/index.php +++ b/backend/api/index.php @@ -1,4 +1,5 @@ 'Unauthorized']); + exit; + } +} + +$method = $_SERVER['REQUEST_METHOD']; $id = $segments[1] ?? null; try { switch ($resource) { + case 'session': + handleSession($method, $db); + break; + case 'settings': + handleSettings($method, $db); + break; case 'teams': handleTeams($method, $id, $db); break; @@ -51,6 +67,110 @@ try { echo json_encode(['error' => $e->getMessage()]); } +function handleSession($method, $db) { + $loggedin = isset($_SESSION['neptune_loggedin']) && $_SESSION['neptune_loggedin'] === true; + if ($loggedin) { + $role = $_SESSION['neptune_role'] ?? 'user'; + $stmt = $db->prepare("SELECT COUNT(*) as c FROM neptune_users WHERE role='admin'"); + $stmt->execute(); + $adminCount = $stmt->fetch()['c']; + echo json_encode([ + 'loggedin' => true, + 'username' => $_SESSION['neptune_username'] ?? 'Unknown', + 'role' => $role, + 'admin_count' => (int)$adminCount + ]); + } else { + echo json_encode(['loggedin' => false]); + } +} + +function handleSettings($method, $db) { + $role = $_SESSION['neptune_role'] ?? 'user'; + if ($method === 'GET') { + if ($role !== 'admin') { + http_response_code(403); + echo json_encode(['error' => 'Admins only']); + return; + } + $users = $db->query("SELECT id, username, user_token, email, role, created_at FROM neptune_users ORDER BY created_at ASC")->fetchAll(); + echo json_encode($users); + } elseif ($method === 'POST') { + if ($role !== 'admin') { + http_response_code(403); + echo json_encode(['error' => 'Admins only']); + return; + } + $data = json_decode(file_get_contents('php://input'), true); + $user_token = $data['user_token'] ?? ''; + if (!$user_token) { + http_response_code(400); + echo json_encode(['error' => 'user_token required']); + return; + } + // Validate the token with Jakach Auth + $check_url = "https://auth.jakach.ch/api/auth/check_auth_key.php?auth_token=" . urlencode($user_token); + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $check_url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + $response = curl_exec($ch); + $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($http !== 200 || !$response) { + http_response_code(400); + echo json_encode(['error' => 'Failed to validate token']); + return; + } + $info = json_decode($response, true); + if (!isset($info['status']) || $info['status'] !== 'success') { + http_response_code(400); + echo json_encode(['error' => 'Invalid token']); + return; + } + + $stmt = $db->prepare("SELECT COUNT(*) as c FROM neptune_users WHERE user_token = ?"); + $stmt->execute([$user_token]); + if ($stmt->fetch()['c'] > 0) { + http_response_code(400); + echo json_encode(['error' => 'User already exists']); + return; + } + + $stmt = $db->prepare("INSERT INTO neptune_users (user_token, username, email, role) VALUES (?, ?, ?, 'user')"); + $stmt->execute([$user_token, $info['username'], $info['email'] ?? '']); + echo json_encode(['status' => 'success', 'msg' => 'User added']); + } elseif ($method === 'DELETE') { + if ($role !== 'admin') { + http_response_code(403); + echo json_encode(['error' => 'Admins only']); + return; + } + $data = json_decode(file_get_contents('php://input'), true); + $id = $data['id'] ?? null; + if (!$id) { + http_response_code(400); + echo json_encode(['error' => 'id required']); + return; + } + // Prevent deleting the last admin + $stmt = $db->prepare("SELECT role FROM neptune_users WHERE id = ?"); + $stmt->execute([$id]); + $user = $stmt->fetch(); + if ($user && $user['role'] === 'admin') { + $adminCount = $db->query("SELECT COUNT(*) as c FROM neptune_users WHERE role='admin'")->fetch()['c']; + if ($adminCount <= 1) { + http_response_code(400); + echo json_encode(['error' => 'Cannot delete the last admin']); + return; + } + } + $db->prepare("DELETE FROM neptune_users WHERE id = ?")->execute([$id]); + echo json_encode(['status' => 'success']); + } +} + function handleTeams($method, $id, $db) { switch ($method) { case 'GET': diff --git a/backend/config/database.php b/backend/config/database.php index 608986d..2dd82ed 100644 --- a/backend/config/database.php +++ b/backend/config/database.php @@ -30,4 +30,12 @@ function migrate($db) { z_index INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"); + $db->exec("CREATE TABLE IF NOT EXISTS neptune_users ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_token VARCHAR(255) NOT NULL UNIQUE, + username VARCHAR(255) NOT NULL, + email VARCHAR(255) DEFAULT '', + role ENUM('admin','user') DEFAULT 'user', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )"); } \ No newline at end of file diff --git a/backend/login.php b/backend/login.php new file mode 100644 index 0000000..8e7db2d --- /dev/null +++ b/backend/login.php @@ -0,0 +1,107 @@ +prepare("SELECT * FROM neptune_users WHERE user_token = ?"); + $stmt->execute([$user_token]); + $user = $stmt->fetch(); + + if ($user) { + $_SESSION['neptune_loggedin'] = true; + $_SESSION['neptune_user_token'] = $user['user_token']; + $_SESSION['neptune_username'] = $user['username']; + $_SESSION['neptune_role'] = $user['role']; + header('Location: /'); + exit; + } else { + // First user becomes admin + $count = $db->query("SELECT COUNT(*) as c FROM neptune_users")->fetch()['c']; + $role = ($count == 0) ? 'admin' : 'user'; + + $stmt = $db->prepare("INSERT INTO neptune_users (user_token, username, email, role) VALUES (?, ?, ?, ?)"); + $stmt->execute([$user_token, $username, $email, $role]); + + $_SESSION['neptune_loggedin'] = true; + $_SESSION['neptune_user_token'] = $user_token; + $_SESSION['neptune_username'] = $username; + $_SESSION['neptune_role'] = $role; + header('Location: /'); + exit; + } + } else { + $error = 'Authentication failed: ' . ($data['msg'] ?? 'unknown error'); + } + } +} +?> + + + + + + Neptune - Login + + + + + +
+ +

Neptune

+

Cybersecurity Incident Journal

+ + +
+ + +
+ + + + Log in with Jakach Auth + + First user automatically becomes admin +
+ + \ No newline at end of file diff --git a/backend/logout.php b/backend/logout.php new file mode 100644 index 0000000..0b7054a --- /dev/null +++ b/backend/logout.php @@ -0,0 +1,6 @@ + { - canvas = document.getElementById('networkCanvas'); - ctx = canvas.getContext('2d'); - resizeCanvas(); + // Check auth first + checkSession().then(() => { + canvas = document.getElementById('networkCanvas'); + ctx = canvas.getContext('2d'); + resizeCanvas(); - loadTeams().then(() => loadEvents()); - loadNetworkData(); + loadTeams().then(() => loadEvents()); + loadNetworkData(); - document.getElementById('saveEvent').addEventListener('click', saveEvent); - document.getElementById('saveNode').addEventListener('click', saveNode); - document.getElementById('saveLink').addEventListener('click', saveLink); - document.getElementById('saveShape').addEventListener('click', saveShape); - document.getElementById('teamFilter').addEventListener('change', renderTimeline); - document.getElementById('searchEvents').addEventListener('input', renderTimeline); - document.getElementById('shapeOpacity').addEventListener('input', (e) => { - document.getElementById('opacityVal').textContent = parseFloat(e.target.value).toFixed(2); - }); + document.getElementById('saveEvent').addEventListener('click', saveEvent); + document.getElementById('saveNode').addEventListener('click', saveNode); + document.getElementById('saveLink').addEventListener('click', saveLink); + document.getElementById('saveShape').addEventListener('click', saveShape); + document.getElementById('addUserBtn').addEventListener('click', addUser); + document.getElementById('teamFilter').addEventListener('change', renderTimeline); + document.getElementById('searchEvents').addEventListener('input', renderTimeline); + document.getElementById('shapeOpacity').addEventListener('input', (e) => { + document.getElementById('opacityVal').textContent = parseFloat(e.target.value).toFixed(2); + }); canvas.addEventListener('mousedown', onMouseDown); canvas.addEventListener('mousemove', onMouseMove); @@ -882,6 +885,76 @@ function esc(s) { return div.innerHTML; } +// ==================== AUTH / SESSION ==================== +let currentUser = null; +let currentRole = null; + +async function checkSession() { + try { + const res = await fetch('/api/session'); + const data = await res.json(); + if (data.loggedin) { + currentUser = data.username; + currentRole = data.role; + document.getElementById('userDisplay').textContent = data.username; + if (data.role === 'admin' || data.admin_count === 0) { + document.getElementById('settingsBtn').classList.remove('d-none'); + } + } else { + window.location.href = '/login.php'; + } + } catch (e) { + window.location.href = '/login.php'; + } +} + +async function loadUsers() { + const list = document.getElementById('userList'); + try { + const users = await apiFetch('settings'); + list.innerHTML = users.map(u => ` +
+
+ ${esc(u.username)} + ${u.role} +
${u.user_token.substring(0, 16)}...
+
+ ${u.role !== 'admin' ? `` : ''} +
+ `).join(''); + } catch (e) { + list.innerHTML = '
Failed to load users
'; + } +} + +async function addUser() { + const token = document.getElementById('addUserToken').value.trim(); + if (!token) return; + try { + const res = await apiFetch('settings', { method: 'POST', body: JSON.stringify({ user_token: token }) }); + if (res.status === 'success') { + document.getElementById('addUserToken').value = ''; + loadUsers(); + } else { + alert(res.error || 'Failed to add user'); + } + } catch (e) { + alert('Failed to add user'); + } +} + +async function removeUser(id) { + if (!confirm('Remove this user?')) return; + try { + await apiFetch('settings', { method: 'DELETE', body: JSON.stringify({ id }) }); + loadUsers(); + } catch (e) { + alert('Failed to remove user'); + } +} + +document.getElementById('settingsModal').addEventListener('show.bs.modal', loadUsers); + if (!CanvasRenderingContext2D.prototype.roundRect) { CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) { if (r > w / 2) r = w / 2; diff --git a/frontend/index.html b/frontend/index.html index f486692..a9cb29b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -33,6 +33,11 @@ +
+ + + +
@@ -329,6 +334,31 @@ + + +