From c3fa0c11e6c870fc43bc1e6650ec64a394fff00b Mon Sep 17 00:00:00 2001 From: Lorenzo Venerandi Date: Sun, 8 Mar 2026 12:43:32 +0100 Subject: [PATCH] feat: update IP stats timestamps to use local time and improve dashboard JavaScript for safer data handling --- src/database.py | 4 +-- src/routes/api.py | 3 +-- src/templates/static/js/dashboard.js | 39 +++++++++++++++++----------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/database.py b/src/database.py index f7b0a59..84eff37 100644 --- a/src/database.py +++ b/src/database.py @@ -2269,8 +2269,8 @@ class DatabaseManager: ip_stats = IpStats( ip=sanitized_ip, total_requests=0, - first_seen=datetime.utcnow(), - last_seen=datetime.utcnow(), + first_seen=datetime.now(), + last_seen=datetime.now(), ) session.add(ip_stats) diff --git a/src/routes/api.py b/src/routes/api.py index 276537f..2338390 100644 --- a/src/routes/api.py +++ b/src/routes/api.py @@ -152,7 +152,6 @@ async def ban_override(request: Request, body: BanOverrideRequest): db = get_db() action_map = {"ban": True, "unban": False, "reset": None} - override_value = action_map.get(body.action) if body.action not in action_map: return JSONResponse( content={"error": "Invalid action. Use: ban, unban, reset"}, @@ -162,7 +161,7 @@ async def ban_override(request: Request, body: BanOverrideRequest): if body.action == "ban": success = db.force_ban_ip(body.ip) else: - success = db.set_ban_override(body.ip, override_value) + success = db.set_ban_override(body.ip, action_map[body.action]) if success: get_app_logger().info(f"Ban override: {body.action} on IP {body.ip}") diff --git a/src/templates/static/js/dashboard.js b/src/templates/static/js/dashboard.js index 41749cb..39e7df9 100644 --- a/src/templates/static/js/dashboard.js +++ b/src/templates/static/js/dashboard.js @@ -252,19 +252,28 @@ document.addEventListener('alpine:init', () => { })); }); +// Helper to access Alpine.js component data +function getAlpineData(selector) { + const container = document.querySelector(selector); + if (!container) return null; + return Alpine.$data ? Alpine.$data(container) : (container._x_dataStack && container._x_dataStack[0]); +} + // 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); - } + const data = getAlpineData('[x-data="dashboardApp()"]'); + if (data && typeof data.openIpInsight === 'function') { + data.openIpInsight(ip); } }; +// Escape HTML to prevent XSS when inserting into innerHTML +function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; +} + // Custom modal system (replaces native confirm/alert) window.krawlModal = { _create(icon, iconClass, message, buttons) { @@ -314,13 +323,14 @@ window.krawlModal = { // Global ban action for IP insight page (auth-gated) window.ipBanAction = async function(ip, action) { // Check if authenticated - const container = document.querySelector('[x-data="dashboardApp()"]'); - const data = container && (Alpine.$data ? Alpine.$data(container) : (container._x_dataStack && container._x_dataStack[0])); + const data = getAlpineData('[x-data="dashboardApp()"]'); if (!data || !data.authenticated) { if (data && typeof data.promptAuth === 'function') data.promptAuth(); return; } - const confirmed = await krawlModal.confirm(`Are you sure you want to ${action} IP ${ip}?`); + const safeIp = escapeHtml(ip); + const safeAction = escapeHtml(action); + const confirmed = await krawlModal.confirm(`Are you sure you want to ${safeAction} IP ${safeIp}?`); if (!confirmed) return; try { const resp = await fetch(`${window.__DASHBOARD_PATH__}/api/ban-override`, { @@ -331,7 +341,7 @@ window.ipBanAction = async function(ip, action) { }); const result = await resp.json().catch(() => ({})); if (resp.ok) { - krawlModal.success(result.message || `${action} successful for ${ip}`); + krawlModal.success(escapeHtml(result.message || `${action} successful for ${ip}`)); const overrides = document.getElementById('overrides-container'); if (overrides) { htmx.ajax('GET', `${window.__DASHBOARD_PATH__}/htmx/ban/overrides?page=1`, { @@ -340,7 +350,7 @@ window.ipBanAction = async function(ip, action) { }); } } else { - krawlModal.error(result.error || `Failed to ${action} IP ${ip}`); + krawlModal.error(escapeHtml(result.error || `Failed to ${action} IP ${ip}`)); } } catch { krawlModal.error('Request failed'); @@ -355,8 +365,7 @@ function updateBanActionVisibility(authenticated) { } // Update visibility after HTMX swaps in new content document.addEventListener('htmx:afterSwap', () => { - const container = document.querySelector('[x-data="dashboardApp()"]'); - const data = container && (Alpine.$data ? Alpine.$data(container) : (container._x_dataStack && container._x_dataStack[0])); + const data = getAlpineData('[x-data="dashboardApp()"]'); if (data) updateBanActionVisibility(data.authenticated); });