Files
krawl.es/src/templates/static/js/dashboard.js

277 lines
10 KiB
JavaScript
Raw Normal View History

// Alpine.js Dashboard Application
document.addEventListener('alpine:init', () => {
Alpine.data('dashboardApp', () => ({
// State
tab: 'overview',
dashboardPath: window.__DASHBOARD_PATH__ || '',
// Banlist dropdown
banlistOpen: false,
// Raw request modal
rawModal: { show: false, content: '', logId: null },
// Map state
mapInitialized: false,
// Chart state
chartLoaded: false,
// IP Insight state
insightIp: null,
// Auth state (UI only — actual security enforced server-side via cookie)
authenticated: false,
authModal: { show: false, password: '', error: '', loading: false },
async init() {
// Check if already authenticated (cookie-based)
try {
const resp = await fetch(`${this.dashboardPath}/api/auth/check`, { credentials: 'same-origin' });
if (resp.ok) this.authenticated = true;
} catch {}
// Handle hash-based tab routing
const hash = window.location.hash.slice(1);
if (hash === 'ip-stats' || hash === 'attacks') {
this.switchToAttacks();
}
// ip-insight tab is only accessible via lens buttons, not direct hash navigation
window.addEventListener('hashchange', () => {
const h = window.location.hash.slice(1);
if (h === 'ip-stats' || h === 'attacks') {
this.switchToAttacks();
} else if (h === 'admin') {
if (this.authenticated) this.switchToAdmin();
} else if (h !== 'ip-insight') {
if (this.tab !== 'ip-insight') {
this.switchToOverview();
}
}
});
},
switchToAttacks() {
this.tab = 'attacks';
window.location.hash = '#ip-stats';
// Delay chart initialization to ensure the container is visible
this.$nextTick(() => {
setTimeout(() => {
if (!this.chartLoaded && typeof loadAttackTypesChart === 'function') {
loadAttackTypesChart();
this.chartLoaded = true;
}
}, 200);
});
},
switchToOverview() {
this.tab = 'overview';
window.location.hash = '#overview';
},
switchToAdmin() {
if (!this.authenticated) return;
this.tab = 'admin';
window.location.hash = '#admin';
this.$nextTick(() => {
const container = document.getElementById('admin-htmx-container');
if (container && typeof htmx !== 'undefined') {
htmx.ajax('GET', `${this.dashboardPath}/htmx/admin`, {
target: '#admin-htmx-container',
swap: 'innerHTML'
});
}
});
},
async logout() {
try {
await fetch(`${this.dashboardPath}/api/auth/logout`, {
method: 'POST',
credentials: 'same-origin',
});
} catch {}
this.authenticated = false;
if (this.tab === 'admin') this.switchToOverview();
},
promptAuth() {
this.authModal = { show: true, password: '', error: '', loading: false };
this.$nextTick(() => {
if (this.$refs.authPasswordInput) this.$refs.authPasswordInput.focus();
});
},
closeAuthModal() {
this.authModal.show = false;
this.authModal.password = '';
this.authModal.error = '';
this.authModal.loading = false;
},
async submitAuth() {
const password = this.authModal.password;
if (!password) {
this.authModal.error = 'Please enter a password';
return;
}
this.authModal.error = '';
this.authModal.loading = true;
try {
const msgBuf = new TextEncoder().encode(password);
const hashBuf = await crypto.subtle.digest('SHA-256', msgBuf);
const fingerprint = Array.from(new Uint8Array(hashBuf))
.map(b => b.toString(16).padStart(2, '0')).join('');
const resp = await fetch(`${this.dashboardPath}/api/auth`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ fingerprint }),
});
if (resp.ok) {
this.authenticated = true;
this.closeAuthModal();
this.switchToAdmin();
} else {
const data = await resp.json().catch(() => ({}));
this.authModal.error = data.error || 'Invalid password';
this.authModal.password = '';
this.authModal.loading = false;
if (data.locked && data.retry_after) {
let remaining = data.retry_after;
const interval = setInterval(() => {
remaining--;
if (remaining <= 0) {
clearInterval(interval);
this.authModal.error = '';
} else {
this.authModal.error = `Too many attempts. Try again in ${remaining}s`;
}
}, 1000);
}
}
} catch {
this.authModal.error = 'Authentication failed';
this.authModal.loading = false;
}
},
switchToIpInsight() {
// Only allow switching if an IP is selected
if (!this.insightIp) return;
this.tab = 'ip-insight';
window.location.hash = '#ip-insight';
},
openIpInsight(ip) {
// Set the IP and load the insight content
this.insightIp = ip;
this.tab = 'ip-insight';
window.location.hash = '#ip-insight';
// Load IP insight content via HTMX
this.$nextTick(() => {
const container = document.getElementById('ip-insight-htmx-container');
if (container && typeof htmx !== 'undefined') {
htmx.ajax('GET', `${this.dashboardPath}/htmx/ip-insight/${encodeURIComponent(ip)}`, {
target: '#ip-insight-htmx-container',
swap: 'innerHTML'
});
}
});
},
async viewRawRequest(logId) {
try {
const resp = await fetch(
`${this.dashboardPath}/api/raw-request/${logId}`,
{ cache: 'no-store' }
);
if (resp.status === 404) {
alert('Raw request not available');
return;
}
const data = await resp.json();
this.rawModal.content = data.raw_request || 'No content available';
this.rawModal.logId = logId;
this.rawModal.show = true;
} catch (err) {
alert('Failed to load raw request');
}
},
closeRawModal() {
this.rawModal.show = false;
this.rawModal.content = '';
this.rawModal.logId = null;
},
async copyRawRequest(event) {
if (!this.rawModal.content) return;
const btn = event.currentTarget;
const originalHTML = btn.innerHTML;
const checkIcon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="#3fb950"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/></svg>';
try {
await navigator.clipboard.writeText(this.rawModal.content);
btn.innerHTML = checkIcon;
} catch {
btn.style.color = '#f85149';
}
setTimeout(() => { btn.innerHTML = originalHTML; btn.style.color = ''; }, 1500);
},
downloadRawRequest() {
if (!this.rawModal.content) return;
const blob = new Blob([this.rawModal.content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `raw-request-${this.rawModal.logId || Date.now()}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
},
toggleIpDetail(event) {
const row = event.target.closest('tr');
if (!row) return;
const detailRow = row.nextElementSibling;
if (detailRow && detailRow.classList.contains('ip-stats-row')) {
detailRow.style.display =
detailRow.style.display === 'table-row' ? 'none' : 'table-row';
}
},
}));
});
// Global function for opening IP Insight (used by map popups)
window.openIpInsight = function(ip) {
// Find the Alpine component and call openIpInsight
const container = document.querySelector('[x-data="dashboardApp()"]');
if (container) {
// Try Alpine 3.x API first, then fall back to older API
const data = Alpine.$data ? Alpine.$data(container) : (container._x_dataStack && container._x_dataStack[0]);
if (data && typeof data.openIpInsight === 'function') {
data.openIpInsight(ip);
}
}
};
// Utility function for formatting timestamps (used by map popups)
function formatTimestamp(isoTimestamp) {
if (!isoTimestamp) return 'N/A';
try {
const date = new Date(isoTimestamp);
return date.toLocaleString('en-US', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
});
} catch {
return isoTimestamp;
}
}