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) + '

' : ''}
-
Comments ${e.comments && e.comments.length ? `(${e.comments.length})` : ''}
+
Comments ${e.comments && e.comments.length ? '(' + e.comments.length + ')' : ''}
${renderComments(e)}
- +
@@ -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
'; }