diff --git a/frontend/assets/js/app.js b/frontend/assets/js/app.js
index cb16acb..acc4bc0 100644
--- a/frontend/assets/js/app.js
+++ b/frontend/assets/js/app.js
@@ -29,7 +29,77 @@ let copyBuffer = null;
let editingNodeId = null;
let editingShapeId = null;
+// ==================== 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') document.getElementById('settingsBtn').classList.remove('d-none');
+ document.getElementById('loginOverlay').style.display = 'none';
+ return;
+ }
+ } catch (_) {}
+ 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';
+ window.history.replaceState({}, '', '/');
+ loadTeams().then(() => loadEvents());
+ loadNetworkData();
+ } else {
+ errEl.textContent = data.error || 'Login failed';
+ errEl.style.display = 'block';
+ }
+ } catch (e) {
+ 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';
+}
+
+// Init
document.addEventListener('DOMContentLoaded', () => {
+ const params = new URLSearchParams(window.location.search);
+ const authToken = params.get('auth');
+ if (authToken) {
+ document.getElementById('loginOverlay').style.display = 'flex';
+ const btn = document.querySelector('#loginOverlay .btn');
+ if (btn) btn.textContent = 'Authenticating...';
+ performLogin(authToken);
+ }
+
checkSession().then(() => {
canvas = document.getElementById('networkCanvas');
ctx = canvas.getContext('2d');
@@ -38,6 +108,10 @@ document.addEventListener('DOMContentLoaded', () => {
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);
+ });
document.getElementById('saveEvent').addEventListener('click', saveEvent);
document.getElementById('saveNode').addEventListener('click', saveNode);
document.getElementById('saveLink').addEventListener('click', saveLink);
@@ -88,7 +162,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
if (e.key === 'Escape') {
- selectedNodeId = null;
+ selectedNodeId = null; selectedNodeIds = [];
selectedShapeId = null;
renderNetwork();
renderNodeList();
@@ -120,7 +194,6 @@ async function apiFetch(path, options = {}) {
return res.json();
}
-// ==================== TEAMS ====================
async function loadTeams() {
teams = await apiFetch('teams');
const sel = document.getElementById('eventTeam');
@@ -133,7 +206,6 @@ async function loadTeams() {
});
}
-// ==================== EVENTS ====================
async function loadEvents() {
events = await apiFetch('events');
renderTimeline();
@@ -152,7 +224,7 @@ function renderTimeline() {
);
if (!filtered.length) {
- container.innerHTML = `
No events yet. Create your first incident entry!
`;
+ container.innerHTML = 'No events yet. Create your first incident entry!
';
return;
}
@@ -172,12 +244,12 @@ function renderTimeline() {
${date}
${esc(e.title)}
- ${e.description ? `${esc(e.description)}
` : ''}
+ ${e.description ? '' + esc(e.description) + '
' : ''}
@@ -249,48 +321,47 @@ async function loadNetworkData() {
}
function populateNodeSelects() {
- const html = nodes.map(n => ``).join('');
+ const html = nodes.map(n => '').join('');
document.getElementById('linkSource').innerHTML = html;
document.getElementById('linkTarget').innerHTML = html;
}
function renderNodeList() {
const list = document.getElementById('nodeList');
+ const iconMap = { host:'fa-desktop', server:'fa-server', router:'fa-route', firewall:'fa-shield-halved', switch:'fa-network-wired', cloud:'fa-cloud', endpoint:'fa-laptop', other:'fa-circle' };
list.innerHTML = nodes.map(n => {
- const iconMap = { host:'fa-desktop', server:'fa-server', router:'fa-route', firewall:'fa-shield-halved', switch:'fa-network-wired', cloud:'fa-cloud', endpoint:'fa-laptop', other:'fa-circle' };
- return `
-
-
-
-
-
-
${esc(n.label)}
-
${n.ip_address || '—'} · ${n.node_type}
-
-
-
`;
+ return '' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' + esc(n.label) + '' +
+ '
' + (n.ip_address || '—') + ' · ' + n.node_type + '
' +
+ '
' +
+ '
' +
+ '
';
}).join('');
}
function renderShapeList() {
const list = document.getElementById('shapeList');
- list.innerHTML = shapes.map(s => `
-
-
-
-
- ${esc(s.label) || (s.shape_type === 'rectangle' ? 'Box' : 'Ellipse')}
-
-
- ${s.shape_type}
-
-
-
-
- `).join('');
+ list.innerHTML = shapes.map(s => {
+ return '' +
+ '
' +
+ '
' +
+ '' +
+ '' + esc(s.label || (s.shape_type === 'rectangle' ? 'Box' : 'Ellipse')) + '' +
+ '
' +
+ '
' +
+ '' + s.shape_type + '' +
+ '' +
+ '
' +
+ '
' +
+ '
';
+ }).join('');
}
-function selectNode(id, add = false) {
+function selectNode(id, add) {
if (add) {
const idx = selectedNodeIds.indexOf(id);
if (idx >= 0) { selectedNodeIds.splice(idx, 1); if (selectedNodeIds.length === 0) selectedNodeId = null; else selectedNodeId = selectedNodeIds[0]; }
@@ -302,20 +373,19 @@ function selectNode(id, add = false) {
selectedShapeId = null;
const n = nodes.find(x => x.id == id);
if (n) {
- document.getElementById('nodeDetails').innerHTML = `
-
-
${esc(n.label)}
-
IP: ${n.ip_address || '—'}
-
Type: ${n.node_type}
-
Status: ${n.status}
-
Group: ${n.group_name}
- ${selectedNodeIds.length > 1 ? `
+${selectedNodeIds.length - 1} more selected` : ''}
-
-
-
-
-
- `;
+ document.getElementById('nodeDetails').innerHTML =
+ '' +
+ '
' + esc(n.label) + '
' +
+ '
IP: ' + (n.ip_address || '—') + '
' +
+ '
Type: ' + n.node_type + '
' +
+ '
Status: ' + n.status + '
' +
+ '
Group: ' + n.group_name + '
' +
+ (selectedNodeIds.length > 1 ? '
+' + (selectedNodeIds.length - 1) + ' more selected' : '') +
+ '
' +
+ '' +
+ '' +
+ '
' +
+ '
';
}
renderNodeList();
renderShapeList();
@@ -334,11 +404,11 @@ function selectShape(id) {
async function deleteSelectedNodes() {
const ids = [...selectedNodeIds];
if (!ids.length) return;
- const ok = await showConfirm(`Delete ${ids.length} node(s) and their connections?`);
+ const ok = await showConfirm('Delete ' + ids.length + ' node(s) and their connections?');
if (!ok) return;
selectedNodeId = null;
selectedNodeIds = [];
- for (const id of ids) await apiFetch(`nodes/${id}`, { method: 'DELETE' });
+ for (const id of ids) await apiFetch('nodes/' + id, { method: 'DELETE' });
loadNetworkData();
}
@@ -347,20 +417,20 @@ async function deleteSelectedShape(id) {
const ok = await showConfirm('Delete this shape?');
if (!ok) return;
selectedShapeId = null;
- await apiFetch(`shapes/${id}`, { method: 'DELETE' });
+ await apiFetch('shapes/' + id, { method: 'DELETE' });
loadNetworkData();
}
function copyNode(id) {
const n = nodes.find(x => x.id == id);
if (!n) return;
- copyBuffer = { type: 'node', data: { ...n } };
+ copyBuffer = { type: 'node', data: Object.assign({}, n) };
}
function copyShape(id) {
const s = shapes.find(x => x.id == id);
if (!s) return;
- copyBuffer = { type: 'shape', data: { ...s } };
+ copyBuffer = { type: 'shape', data: Object.assign({}, s) };
}
async function pasteItem() {
@@ -368,36 +438,23 @@ async function pasteItem() {
const offset = 30;
if (copyBuffer.type === 'node') {
const d = copyBuffer.data;
- await apiFetch('nodes', {
- method: 'POST',
- body: JSON.stringify({
- label: d.label + ' (copy)',
- ip_address: d.ip_address,
- node_type: d.node_type,
- status: d.status,
- group_name: d.group_name,
- pos_x: (parseFloat(d.pos_x) || 100) + offset,
- pos_y: (parseFloat(d.pos_y) || 100) + offset
- })
- });
+ await apiFetch('nodes', { method: 'POST', body: JSON.stringify({
+ label: d.label + ' (copy)', ip_address: d.ip_address,
+ node_type: d.node_type, status: d.status, group_name: d.group_name,
+ pos_x: (parseFloat(d.pos_x) || 100) + offset,
+ pos_y: (parseFloat(d.pos_y) || 100) + offset
+ })});
loadNetworkData();
} else if (copyBuffer.type === 'shape') {
const d = copyBuffer.data;
- await apiFetch('shapes', {
- method: 'POST',
- body: JSON.stringify({
- label: d.label + ' (copy)',
- shape_type: d.shape_type,
- pos_x: (parseFloat(d.pos_x) || 100) + offset,
- pos_y: (parseFloat(d.pos_y) || 100) + offset,
- width: d.width || 200,
- height: d.height || 150,
- color: d.color || '#1e3a5f',
- border_color: d.border_color || '#3b82f6',
- opacity: parseFloat(d.opacity) || 0.15,
- z_index: nextShapeZ++
- })
- });
+ await apiFetch('shapes', { method: 'POST', body: JSON.stringify({
+ label: d.label + ' (copy)', shape_type: d.shape_type,
+ pos_x: (parseFloat(d.pos_x) || 100) + offset,
+ pos_y: (parseFloat(d.pos_y) || 100) + offset,
+ width: d.width || 200, height: d.height || 150,
+ color: d.color || '#1e3a5f', border_color: d.border_color || '#3b82f6',
+ opacity: parseFloat(d.opacity) || 0.15, z_index: nextShapeZ++
+ })});
loadNetworkData();
}
}
@@ -430,14 +487,10 @@ async function saveNode() {
};
if (!data.label) return alert('Label required');
if (editingNodeId) {
- const updates = {
- label: data.label,
- ip_address: data.ip_address,
- node_type: data.node_type,
- status: data.status,
- group_name: data.group_name
- };
- await apiFetch(`nodes/${editingNodeId}`, { method: 'PUT', body: JSON.stringify(updates) });
+ await apiFetch('nodes/' + editingNodeId, { method: 'PUT', body: JSON.stringify({
+ label: data.label, ip_address: data.ip_address,
+ node_type: data.node_type, status: data.status, group_name: data.group_name
+ })});
editingNodeId = null;
} else {
await apiFetch('nodes', { method: 'POST', body: JSON.stringify(data) });
@@ -503,13 +556,12 @@ async function saveShape() {
if (editingShapeId) {
const s = shapes.find(x => x.id == editingShapeId);
data.pos_x = s.pos_x; data.pos_y = s.pos_y; data.width = s.width; data.height = s.height; data.z_index = s.z_index;
- await apiFetch(`shapes/${editingShapeId}`, { method: 'PUT', body: JSON.stringify(data) });
+ await apiFetch('shapes/' + editingShapeId, { method: 'PUT', body: JSON.stringify(data) });
editingShapeId = null;
} else {
data.pos_x = canvas.width / 2 - 100 - panX;
data.pos_y = canvas.height / 2 - 75 - panY;
- data.width = 200;
- data.height = 150;
+ data.width = 200; data.height = 150;
data.z_index = nextShapeZ++;
await apiFetch('shapes', { method: 'POST', body: JSON.stringify(data) });
}
@@ -525,7 +577,6 @@ function getNodeColorVal(type) {
return c[type] || '#6b7280';
}
-// ==================== CANVAS RENDERING ====================
const NODE_FA_ICONS = {
host: { icon: '\uf108', color: '#3b82f6' },
server: { icon: '\uf233', color: '#8b5cf6' },
@@ -540,20 +591,16 @@ const NODE_FA_ICONS = {
function buildCanvasGraph() {
canvasNodes = nodes.map(n => {
const fa = NODE_FA_ICONS[n.node_type] || NODE_FA_ICONS.other;
- return {
- id: n.id, label: n.label, ip: n.ip_address,
- type: n.node_type, status: n.status, group: n.group_name,
+ return { id: n.id, label: n.label, ip: n.ip_address, type: n.node_type,
+ status: n.status, group: n.group_name,
x: parseFloat(n.pos_x) || 100, y: parseFloat(n.pos_y) || 100,
- icon: fa.icon, color: fa.color, w: 36, h: 36
- };
+ icon: fa.icon, color: fa.color, w: 36, h: 36 };
});
-
canvasLinks = links.map(l => ({
source: canvasNodes.find(n => n.id == l.source_id),
target: canvasNodes.find(n => n.id == l.target_id),
type: l.link_type, label: l.label
})).filter(l => l.source && l.target);
-
canvasShapes = shapes.map(s => ({
id: s.id, label: s.label, type: s.shape_type,
x: parseFloat(s.pos_x), y: parseFloat(s.pos_y),
@@ -567,10 +614,7 @@ function renderNetwork() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(panX, panY);
-
- // Use canvasShapes / canvasNodes / canvasLinks arrays directly (no rebuild)
canvasShapes.sort((a, b) => a.z - b.z).forEach(drawShape);
-
canvasLinks.forEach(l => {
ctx.beginPath();
ctx.moveTo(l.source.x, l.source.y);
@@ -589,10 +633,7 @@ function renderNetwork() {
ctx.fillText(l.label, (l.source.x + l.target.x) / 2, (l.source.y + l.target.y) / 2 - 8);
}
});
-
canvasNodes.forEach(drawCanvasNode);
-
- // Selection rectangle
if (selectRect && (selectRect.w > 0 || selectRect.h > 0)) {
ctx.strokeStyle = '#3b82f6';
ctx.lineWidth = 1.5;
@@ -602,7 +643,6 @@ function renderNetwork() {
ctx.fillRect(selectRect.x, selectRect.y, selectRect.w, selectRect.h);
ctx.setLineDash([]);
}
-
ctx.restore();
}
@@ -610,7 +650,6 @@ function drawShape(s) {
ctx.save();
const sel = selectedShapeId == s.id;
ctx.globalAlpha = s.opacity;
-
if (s.type === 'ellipse') {
ctx.beginPath();
ctx.ellipse(s.x + s.w / 2, s.y + s.h / 2, s.w / 2, s.h / 2, 0, 0, Math.PI * 2);
@@ -626,7 +665,6 @@ function drawShape(s) {
ctx.setLineDash([5, 3]);
ctx.stroke();
ctx.setLineDash([]);
-
if (sel) {
getShapeHandles(s).forEach(h => {
ctx.beginPath();
@@ -638,7 +676,6 @@ function drawShape(s) {
ctx.stroke();
});
}
-
ctx.fillStyle = '#94a3b8';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
@@ -649,9 +686,7 @@ function drawShape(s) {
function drawCanvasNode(n) {
const sel = selectedNodeIds.includes(n.id);
ctx.save();
-
if (sel) { ctx.shadowColor = n.color; ctx.shadowBlur = 18; }
-
ctx.beginPath();
ctx.arc(n.x, n.y, 22, 0, Math.PI * 2);
ctx.fillStyle = n.color + '18';
@@ -660,8 +695,6 @@ function drawCanvasNode(n) {
ctx.lineWidth = sel ? 2.5 : 1.5;
ctx.stroke();
ctx.shadowBlur = 0;
-
- // Draw icon using Font Awesome
ctx.save();
ctx.font = '900 22px "Font Awesome 6 Free", "FontAwesome", "Font Awesome 5 Free"';
ctx.textAlign = 'center';
@@ -669,17 +702,12 @@ function drawCanvasNode(n) {
ctx.fillStyle = n.color;
ctx.fillText(n.icon, n.x, n.y);
ctx.restore();
-
- // Status dot
ctx.beginPath();
ctx.arc(n.x + 17, n.y - 17, 5, 0, Math.PI * 2);
const sc = { online: '#22c55e', offline: '#6b7280', unknown: '#9ca3af', compromised: '#ef4444', monitoring: '#eab308' };
ctx.fillStyle = sc[n.status] || '#9ca3af';
ctx.fill();
- if (n.status === 'compromised') {
- ctx.strokeStyle = '#ef4444'; ctx.lineWidth = 2; ctx.stroke();
- }
-
+ if (n.status === 'compromised') { ctx.strokeStyle = '#ef4444'; ctx.lineWidth = 2; ctx.stroke(); }
ctx.fillStyle = '#e2e8f0';
ctx.font = sel ? 'bold 11px sans-serif' : '10px sans-serif';
ctx.textAlign = 'center';
@@ -701,7 +729,6 @@ function getShapeHandles(s) {
];
}
-// ==================== CANVAS EVENTS ====================
function getCanvasNodeAt(mx, my) {
return canvasNodes.find(n => Math.hypot(mx - n.x, my - n.y) < 28);
}
@@ -728,7 +755,6 @@ function onMouseDown(e) {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left - panX;
const my = e.clientY - rect.top - panY;
-
const resize = getShapeResizeHandleAt(mx, my);
if (resize) {
dragType = 'resize'; dragTarget = resize.shape;
@@ -736,31 +762,20 @@ function onMouseDown(e) {
dragOrig = { x: resize.shape.x, y: resize.shape.y, w: resize.shape.w, h: resize.shape.h, cx: resize.cx, cy: resize.cy };
return;
}
-
const node = getCanvasNodeAt(mx, my);
if (node) {
- if (e.shiftKey) {
- selectNode(node.id, true);
- // Start multi-drag for all selected
- dragType = 'node'; dragTarget = node;
- dragOffX = mx - node.x; dragOffY = my - node.y;
- } else {
- if (!selectedNodeIds.includes(node.id)) {
- selectNode(node.id);
- }
- dragType = 'node'; dragTarget = node;
- dragOffX = mx - node.x; dragOffY = my - node.y;
- }
+ if (e.shiftKey) { selectNode(node.id, true); }
+ else if (!selectedNodeIds.includes(node.id)) { selectNode(node.id); }
+ dragType = 'node'; dragTarget = node;
+ dragOffX = mx - node.x; dragOffY = my - node.y;
return;
}
-
const shape = getShapeAt(mx, my);
if (shape) {
selectedNodeId = null; selectedNodeIds = []; selectedShapeId = shape.id;
dragType = 'shape'; dragTarget = shape;
dragOffX = mx - shape.x; dragOffY = my - shape.y;
renderNodeList(); renderShapeList();
- // Re-check resize handles now that shape is selected
const resizeNow = getShapeResizeHandleAt(mx, my);
if (resizeNow) {
dragType = 'resize'; dragTarget = resizeNow.shape;
@@ -769,7 +784,6 @@ function onMouseDown(e) {
}
return;
}
-
selectedNodeId = null; selectedNodeIds = []; selectedShapeId = null;
renderNodeList(); renderShapeList();
dragType = 'select';
@@ -779,38 +793,28 @@ function onMouseDown(e) {
function onMouseMove(e) {
const rect = canvas.getBoundingClientRect();
-
if (dragType === 'node' && dragTarget) {
const dx = e.clientX - rect.left - panX - dragOffX - dragTarget.x;
const dy = e.clientY - rect.top - panY - dragOffY - dragTarget.y;
- // Move all selected nodes by delta
- for (const c of canvasNodes) {
- if (selectedNodeIds.includes(c.id)) { c.x += dx; c.y += dy; }
- }
- renderNetwork();
- return;
+ for (const c of canvasNodes) { if (selectedNodeIds.includes(c.id)) { c.x += dx; c.y += dy; } }
+ renderNetwork(); return;
}
if (dragType === 'shape' && dragTarget) {
dragTarget.x = e.clientX - rect.left - panX - dragOffX;
dragTarget.y = e.clientY - rect.top - panY - dragOffY;
- renderNetwork();
- return;
+ renderNetwork(); return;
}
if (dragType === 'resize' && dragTarget) {
const dx = e.clientX - rect.left - panX - dragOffX;
const dy = e.clientY - rect.top - panY - dragOffY;
- const s = dragTarget;
- const o = dragOrig;
+ const s = dragTarget; const o = dragOrig;
let nx = o.x, ny = o.y, nw = o.w, nh = o.h;
- if (o.cx === 0) { nx = o.x + dx; nw = o.w - dx; }
- else { nw = o.w + dx; }
- if (o.cy === 0) { ny = o.y + dy; nh = o.h - dy; }
- else { nh = o.h + dy; }
+ if (o.cx === 0) { nx = o.x + dx; nw = o.w - dx; } else { nw = o.w + dx; }
+ if (o.cy === 0) { ny = o.y + dy; nh = o.h - dy; } else { nh = o.h + dy; }
if (nw < 50) { if (o.cx === 0) nx = o.x + o.w - 50; nw = 50; }
if (nh < 50) { if (o.cy === 0) ny = o.y + o.h - 50; nh = 50; }
s.x = nx; s.y = ny; s.w = nw; s.h = nh;
- renderNetwork();
- return;
+ renderNetwork(); return;
}
if (dragType === 'select') {
const mx = e.clientX - rect.left - panX;
@@ -819,15 +823,12 @@ function onMouseMove(e) {
selectRect.y = Math.min(selectStartY, my);
selectRect.w = Math.abs(mx - selectStartX);
selectRect.h = Math.abs(my - selectStartY);
- renderNetwork();
- return;
+ renderNetwork(); return;
}
if (isPanning) {
panX = e.clientX - panStartX; panY = e.clientY - panStartY;
- renderNetwork();
- return;
+ renderNetwork(); return;
}
-
const mx = e.clientX - rect.left - panX;
const my = e.clientY - rect.top - panY;
if (getShapeResizeHandleAt(mx, my)) canvas.style.cursor = 'nwse-resize';
@@ -837,13 +838,9 @@ function onMouseMove(e) {
function onMouseUp(e) {
if (dragType === 'node') {
- for (const c of canvasNodes) {
- if (selectedNodeIds.includes(c.id)) {
- apiFetch(`nodes/${c.id}`, { method: 'PUT', body: JSON.stringify({ pos_x: c.x, pos_y: c.y }) });
- }
- }
+ for (const c of canvasNodes) { if (selectedNodeIds.includes(c.id)) apiFetch('nodes/' + c.id, { method: 'PUT', body: JSON.stringify({ pos_x: c.x, pos_y: c.y }) }); }
} else if (dragTarget && (dragType === 'shape' || dragType === 'resize')) {
- apiFetch(`shapes/${dragTarget.id}`, { method: 'PUT', body: JSON.stringify({ pos_x: dragTarget.x, pos_y: dragTarget.y, width: dragTarget.w, height: dragTarget.h }) });
+ apiFetch('shapes/' + dragTarget.id, { method: 'PUT', body: JSON.stringify({ pos_x: dragTarget.x, pos_y: dragTarget.y, width: dragTarget.w, height: dragTarget.h }) });
} else if (dragType === 'select') {
selectRect = null;
const rect = canvas.getBoundingClientRect();
@@ -854,9 +851,7 @@ function onMouseUp(e) {
const rw = Math.abs(mx - selectStartX);
const rh = Math.abs(my - selectStartY);
if (rw > 5 || rh > 5) {
- const found = canvasNodes.filter(n =>
- n.x >= rx && n.x <= rx + rw && n.y >= ry && n.y <= ry + rh
- );
+ const found = canvasNodes.filter(n => n.x >= rx && n.x <= rx + rw && n.y >= ry && n.y <= ry + rh);
selectedNodeIds = found.map(n => n.id);
selectedNodeId = found.length > 0 ? found[0].id : null;
selectedShapeId = null;
@@ -886,110 +881,18 @@ 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') {
- 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 {
- errEl.textContent = data.error || 'Login failed';
- errEl.style.display = 'block';
- }
- } catch (e) {
- 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 {
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('');
+ list.innerHTML = users.map(u => {
+ return '' +
+ '
' + esc(u.username) + '' +
+ '
' + u.role + '' +
+ '
' + u.user_token.substring(0, 16) + '...
' +
+ (u.role !== 'admin' ? '
' : '') +
+ '
';
+ }).join('');
} catch (e) {
list.innerHTML = 'Failed to load users
';
}