diff --git a/backend/api/index.php b/backend/api/index.php
index b2f4099..d4d0386 100644
--- a/backend/api/index.php
+++ b/backend/api/index.php
@@ -40,6 +40,12 @@ try {
case 'settings':
handleSettings($method, $db);
break;
+ case 'login':
+ handleLogin($method, $db);
+ break;
+ case 'logout':
+ handleLogout();
+ break;
case 'teams':
handleTeams($method, $id, $db);
break;
@@ -69,6 +75,10 @@ try {
function handleSession($method, $db) {
$loggedin = isset($_SESSION['neptune_loggedin']) && $_SESSION['neptune_loggedin'] === true;
+ if (!$loggedin && $method === 'GET') {
+ echo json_encode(['loggedin' => false]);
+ return;
+ }
if ($loggedin) {
$role = $_SESSION['neptune_role'] ?? 'user';
$stmt = $db->prepare("SELECT COUNT(*) as c FROM neptune_users WHERE role='admin'");
@@ -85,6 +95,80 @@ function handleSession($method, $db) {
}
}
+function handleLogin($method, $db) {
+ if ($method !== 'POST') {
+ http_response_code(405);
+ echo json_encode(['error' => 'POST required']);
+ return;
+ }
+ $data = json_decode(file_get_contents('php://input'), true);
+ $auth_token = $data['auth_token'] ?? '';
+ if (!$auth_token) {
+ http_response_code(400);
+ echo json_encode(['error' => 'auth_token required']);
+ return;
+ }
+
+ $check_url = "https://auth.jakach.ch/api/auth/check_auth_key.php?auth_token=" . urlencode($auth_token);
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $check_url);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 15);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
+ $response = curl_exec($ch);
+ $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($http_code !== 200 || !$response) {
+ http_response_code(502);
+ echo json_encode(['error' => 'Failed to contact auth server']);
+ return;
+ }
+
+ $info = json_decode($response, true);
+ if (!isset($info['status']) || $info['status'] !== 'success') {
+ http_response_code(401);
+ echo json_encode(['error' => $info['msg'] ?? 'Auth failed']);
+ return;
+ }
+
+ $user_token = $info['user_token'];
+ $username = $info['username'];
+ $email = $info['email'] ?? '';
+
+ $stmt = $db->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'];
+ } else {
+ $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;
+ }
+
+ echo json_encode([
+ 'status' => 'success',
+ 'username' => $_SESSION['neptune_username'],
+ 'role' => $_SESSION['neptune_role']
+ ]);
+}
+
+function handleLogout() {
+ $_SESSION = array();
+ session_destroy();
+ echo json_encode(['status' => 'success']);
+}
+
function handleSettings($method, $db) {
$role = $_SESSION['neptune_role'] ?? 'user';
if ($method === 'GET') {
diff --git a/backend/login.php b/backend/login.php
deleted file mode 100644
index 0b37970..0000000
--- a/backend/login.php
+++ /dev/null
@@ -1,114 +0,0 @@
-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
-
-
-
= htmlspecialchars($error) ?>
-
-
-
= htmlspecialchars($success) ?>
-
-
-
- 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
deleted file mode 100644
index 0b7054a..0000000
--- a/backend/logout.php
+++ /dev/null
@@ -1,6 +0,0 @@
- {
document.getElementById('saveLink').addEventListener('click', saveLink);
document.getElementById('saveShape').addEventListener('click', saveShape);
document.getElementById('addUserBtn').addEventListener('click', addUser);
+ document.getElementById('logoutBtn').addEventListener('click', logout);
document.getElementById('teamFilter').addEventListener('change', renderTimeline);
document.getElementById('searchEvents').addEventListener('input', renderTimeline);
document.getElementById('shapeOpacity').addEventListener('input', (e) => {
@@ -892,43 +893,89 @@ let currentRole = null;
async function checkSession() {
try {
const res = await fetch('/api/session');
- if (res.redirected || !res.ok) {
- window.location.replace('/login.php');
- return;
- }
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) {
+ if (data.role === 'admin') {
document.getElementById('settingsBtn').classList.remove('d-none');
}
+ document.getElementById('loginOverlay').style.display = 'none';
+ return;
+ }
+ } catch (_) {}
+ // Show login overlay
+ document.getElementById('loginOverlay').style.display = 'flex';
+}
+
+async function performLogin(authToken) {
+ const errEl = document.getElementById('loginError');
+ const sucEl = document.getElementById('loginSuccess');
+ errEl.style.display = 'none';
+ sucEl.style.display = 'none';
+ try {
+ const res = await fetch('/api/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ auth_token: authToken })
+ });
+ const data = await res.json();
+ if (data.status === 'success') {
+ currentUser = data.username;
+ currentRole = data.role;
+ document.getElementById('userDisplay').textContent = data.username;
+ if (data.role === 'admin') document.getElementById('settingsBtn').classList.remove('d-none');
+ document.getElementById('loginOverlay').style.display = 'none';
+ // Clean URL
+ window.history.replaceState({}, '', '/');
+ // Reload data
+ loadTeams().then(() => loadEvents());
+ loadNetworkData();
} else {
- window.location.replace('/login.php');
+ errEl.textContent = data.error || 'Login failed';
+ errEl.style.display = 'block';
}
} catch (e) {
- // Retry once after a brief delay in case of transient network issue
- setTimeout(async () => {
- try {
- const res = await fetch('/api/session');
- if (!res.ok || res.redirected) throw new Error();
- 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');
- }
- return;
- }
- } catch (_) {}
- window.location.replace('/login.php');
- }, 500);
+ errEl.textContent = 'Connection error';
+ errEl.style.display = 'block';
}
}
+async function logout() {
+ await apiFetch('logout', { method: 'POST' });
+ currentUser = null;
+ currentRole = null;
+ document.getElementById('settingsBtn').classList.add('d-none');
+ document.getElementById('userDisplay').textContent = '';
+ document.getElementById('loginOverlay').style.display = 'flex';
+}
+
+// Check for auth token in URL on page load
+document.addEventListener('DOMContentLoaded', () => {
+ const params = new URLSearchParams(window.location.search);
+ const authToken = params.get('auth');
+ if (authToken) {
+ // Show a loading state on overlay
+ document.getElementById('loginOverlay').style.display = 'flex';
+ document.querySelector('#loginOverlay .btn').textContent = 'Authenticating...';
+ performLogin(authToken);
+ }
+
+ checkSession().then(() => {
+ // Init canvas and load data
+ canvas = document.getElementById('networkCanvas');
+ ctx = canvas.getContext('2d');
+ resizeCanvas();
+
+ loadTeams().then(() => loadEvents());
+ loadNetworkData();
+
+ document.getElementById('loginBtn').addEventListener('click', () => {
+ const callbackUrl = window.location.origin + '/?auth_callback=1';
+ window.location.href = 'https://auth.jakach.ch/?send_to=' + encodeURIComponent(callbackUrl);
+ });
+
async function loadUsers() {
const list = document.getElementById('userList');
try {
diff --git a/frontend/index.html b/frontend/index.html
index a9cb29b..2dd365d 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -36,7 +36,7 @@
@@ -375,6 +375,21 @@
+
+
+
+
+
Neptune
+
Cybersecurity Incident Journal
+
+
+
+
First user automatically becomes admin
+
+
+