UNPKG

request-src

Version:

A lightweight server-side middleware for real-time monitoring and storage of HTTP requests.

441 lines (360 loc) 15.5 kB
// ✅ Get the base route dynamically & clean it let dashboardRoute = window.location.pathname.replace(/\/$/, ""); // Remove trailing slash if any // Sorting variables let currentSortColumn = null; let currentSortOrder = "asc"; // Pagination variables let currentPage = 1; const limit = 100; document.addEventListener("DOMContentLoaded", () => { currentSortColumn = localStorage.getItem("sortColumn") ? parseInt(localStorage.getItem("sortColumn")) : null; currentSortOrder = localStorage.getItem("sortOrder") || "asc"; fetchLogs(); setupToggles(); }); // ✅ Fetch graph data when switching to graph mode document.getElementById("toggleView").addEventListener("change", function () { let table = document.getElementById("logTable"); let page = document.getElementById("pageButtons"); let graph = document.getElementById("logChart"); let graphOptions = document.getElementById("graphOptions"); let anonToggler = document.getElementById("container"); let header = document.getElementById("header"); let settings = document.getElementById("settings"); let body = document.body; // ✅ Use body to apply mode-based styles if (this.checked) { table.style.display = "none"; page.style.display = "none"; graph.style.display = "block"; graphOptions.style.display = "block"; anonToggler.style.display = "none"; header.style.padding = "0"; header.style.margin = "0"; header.style.width = "100%"; settings.style.backgroundColor = "#d8315b"; // ✅ Apply graph mode styling body.classList.add("graph-mode"); // ✅ Refresh chart data fetchGraphData(document.getElementById("groupBy").value); } else { table.style.display = "table"; page.style.display = "block"; graph.style.display = "none"; graphOptions.style.display = "none"; anonToggler.style.display = "flex"; header.style.width = "90%"; settings.style.backgroundColor = "#3e92cc"; // ✅ Remove graph mode styling body.classList.remove("graph-mode"); } }); async function fetchLogs() { return new Promise(async (resolve) => { let filters = { limit: 50, page: currentPage, sortColumn: localStorage.getItem("sortColumn") || null, sortOrder: localStorage.getItem("sortOrder") || "desc" }; let query = new URLSearchParams(filters).toString(); let response = await fetch(`${dashboardRoute}/logs?${query}`); if (!response.ok) return; let data = await response.json(); let tbody = document.querySelector("#logTable tbody"); tbody.innerHTML = ""; // ✅ Clear only current page data // ✅ Apply sorting before rendering if (filters.sortColumn !== null) { let columnIndex = parseInt(filters.sortColumn); let sortOrder = filters.sortOrder; data.data.sort((a, b) => { let valA = a[Object.keys(a)[columnIndex]]; let valB = b[Object.keys(b)[columnIndex]]; if (valA === undefined || valA === null) valA = ""; if (valB === undefined || valB === null) valB = ""; let numA = parseFloat(valA); let numB = parseFloat(valB); if (!isNaN(numA) && !isNaN(numB)) { return sortOrder === "asc" ? numA - numB : numB - numA; } valA = String(valA).toLowerCase(); valB = String(valB).toLowerCase(); return sortOrder === "asc" ? valA.localeCompare(valB) : valB.localeCompare(valA); }); updateSortIcons(); } // ✅ Assign colors to request types (shared with graph) data.data.forEach((log, index) => { let localTime = convertUTCtoLocal(log.timestamp, false); let rowColor = index % 2 === 0 ? "white" : "#f7f6fe"; let reqType = log.req_type || "Unknown"; // ✅ Reuse assignedColors from graph if (!assignedColors[reqType]) { assignedColors[reqType] = colorPalette[Object.keys(assignedColors).length % colorPalette.length]; } // ✅ Generate a lighter version of the color let baseColor = assignedColors[reqType]; let lightColor = `${baseColor}30`; // Adds 30% transparency let row = `<tr style="background-color: ${rowColor};"> <td>${localTime}</td> <td>${log.ip}</td> <td>${log.city || "Unknown"}</td> <td>${log.region || "Unknown"}</td> <td>${log.country || "Unknown"}</td> <td>${parseUserAgent(log.user_agent)}</td> <td> <span class="req-badge" style="background-color: ${lightColor}; color: ${baseColor};">${reqType}</span> </td> </tr>`; tbody.innerHTML += row; }); document.getElementById("currentPageDisplay").innerText = `Page: ${currentPage}`; resolve(); }); } // ✅ Color palette for assigning colors to request types const colorPalette = [ "#FF5733", "#33FF57", "#3357FF", "#FF33A1", "#A133FF", "#33FFF5", "#FFC300", "#FF5733", "#C70039", "#900C3F", "#581845", "#28A745", "#17A2B8", "#DC3545", "#FFC107" ]; let assignedColors = {}; let lastId = 0; async function fetchGraphData() { return new Promise(async (resolve) => { let timeRange = document.getElementById("timeRange").value; let groupBy = document.getElementById("groupBy").value; if (window.currentGroupBy !== groupBy) { lastId = 0; window.currentGroupBy = groupBy; } let response = await fetch(`${dashboardRoute}/chart-data?lastId=${lastId}&timeRange=${timeRange}&groupBy=${groupBy}`); let result = await response.json(); let noDataMessage = document.getElementById("noDataMessage"); let graph = document.getElementById("logChart"); // ✅ Handle No Data Scenario if (!result || result.data.length === 0) { noDataMessage.style.display = "block"; // Show "No Data" message graph.style.display = "none"; // Hide the graph resolve(); // Still resolve the promise return; } else { noDataMessage.style.display = "none"; // Hide the message graph.style.display = "block"; // Show the graph } lastId = result.lastId; let groupedData = {}; let colorIndex = 0; result.data.forEach(log => { let localTime = convertUTCtoLocal(log.time, true); let key = log[groupBy] || "Unknown"; if (!groupedData[key]) groupedData[key] = {}; if (!groupedData[key][localTime]) groupedData[key][localTime] = 0; groupedData[key][localTime] += log.count; if (!assignedColors[key]) { assignedColors[key] = colorPalette[colorIndex % colorPalette.length]; colorIndex++; } }); let datasets = Object.keys(groupedData).map(key => ({ label: key, data: Object.entries(groupedData[key]).map(([time, count]) => ({ x: new Date(time), y: count })), fill: false, borderColor: assignedColors[key] })); let canvas = document.getElementById("logChart"); let ctx = canvas.getContext("2d"); // ✅ Dynamically set the time unit based on `timeRange` let timeUnit; if (timeRange === "hour") timeUnit = "hour"; else if (timeRange === "day") timeUnit = "day"; else if (timeRange === "week") timeUnit = "week"; else if (timeRange === "month") timeUnit = "month"; else if (timeRange === "quarter") timeUnit = "month"; else timeUnit = "day"; if (window.chartInstance) { datasets.forEach(newDataset => { let existingDataset = window.chartInstance.data.datasets.find(ds => ds.label === newDataset.label); if (existingDataset) { existingDataset.data = newDataset.data; } else { window.chartInstance.data.datasets.push(newDataset); } }); window.chartInstance.data.datasets = window.chartInstance.data.datasets.filter(ds => datasets.some(newDs => newDs.label === ds.label) ); window.chartInstance.options.scales.x.time.unit = timeUnit; window.chartInstance.options.scales.y.title.text = `Count by ${groupBy}`; window.chartInstance.update(); } else { window.chartInstance = new Chart(ctx, { type: "line", data: { datasets }, options: { responsive: true, maintainAspectRatio: true, animation: false, plugins: { legend: { display: true } }, scales: { x: { type: "time", time: { unit: timeUnit } }, y: { title: { display: true, text: `Count by ${groupBy}` } } } } }); } resolve(); }); } /** * ✅ Convert UTC timestamp to local time and remove seconds */ function convertUTCtoLocal(utcDateString, forChart = false) { let utcDate = new Date(utcDateString); if (isNaN(utcDate)) return null; // Prevent invalid dates let userOffset = utcDate.getTimezoneOffset() * 60000; let localDate = new Date(utcDate.getTime() - userOffset); // ✅ If used for a graph, return a Date object (needed for Chart.js) if (forChart) return localDate; // ✅ If used for a table, return a formatted string (MM/DD/YYYY HH:mm AM/PM) return localDate.toLocaleString("en-US", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: true }); } /** * ✅ Extracts relevant info from User-Agent, including Insomnia & Postman */ function parseUserAgent(uaString) { if (!uaString) return "Unknown"; let browser = "Unknown"; let platform = "Unknown"; let version = ""; // ✅ Detect API Clients if (uaString.includes("curl")) { browser = "curl"; version = uaString.match(/curl\/([\d.]+)/)?.[1] || ""; return `${browser} ${version}`.trim(); } if (uaString.includes("Wget")) { browser = "Wget"; version = uaString.match(/Wget\/([\d.]+)/)?.[1] || ""; return `${browser} ${version}`.trim(); } if (uaString.includes("HTTPie")) { browser = "HTTPie"; version = uaString.match(/HTTPie\/([\d.]+)/)?.[1] || ""; return `${browser} ${version}`.trim(); } if (uaString.includes("python-requests")) { browser = "python-requests"; version = uaString.match(/python-requests\/([\d.]+)/)?.[1] || ""; return `${browser} ${version}`.trim(); } if (uaString.includes("PostmanRuntime")) return "Postman"; if (uaString.includes("insomnia")) return "Insomnia"; // ✅ Detect Browsers if (uaString.includes("Edg")) { browser = "Edge"; version = uaString.match(/Edg\/([\d.]+)/)?.[1] || ""; } else if (uaString.includes("Chrome")) { browser = "Chrome"; version = uaString.match(/Chrome\/([\d.]+)/)?.[1] || ""; } else if (uaString.includes("Firefox")) { browser = "Firefox"; version = uaString.match(/Firefox\/([\d.]+)/)?.[1] || ""; } else if (uaString.includes("Safari") && !uaString.includes("Chrome")) { browser = "Safari"; version = uaString.match(/Version\/([\d.]+)/)?.[1] || ""; } else if (uaString.includes("MSIE") || uaString.includes("Trident")) { browser = "IE"; version = uaString.match(/(MSIE |rv:)([\d.]+)/)?.[2] || ""; } // ✅ Detect Platforms if (uaString.includes("Windows")) platform = "Windows"; else if (uaString.includes("Mac OS X")) platform = "Mac"; else if (uaString.includes("Linux")) platform = "Linux"; else if (uaString.includes("Android")) platform = "Android"; else if (uaString.includes("iPhone") || uaString.includes("iPad")) platform = "iOS"; // return `${browser} ${version} (${platform})`.trim(); //this is if you want to show version return `${browser} (${platform})`.trim(); } async function setupToggles() { let response = await fetch(`${dashboardRoute}/config`); let config = await response.json(); document.getElementById("toggleAnonymize").checked = config.anonymize; } document.getElementById("toggleAnonymize").addEventListener("change", function () { let newConfig = { anonymize: this.checked }; fetch(`${dashboardRoute}/update-config`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newConfig) }) .then(response => { if (response.ok) { console.log("✅ Anonymization setting updated successfully!"); } else { console.error("❌ Failed to update anonymization setting."); } }); }); function nextPage() { currentPage++; fetchLogs(); } function prevPage() { if (currentPage > 1) { currentPage--; fetchLogs(); } } function fetchLogsWithSorting(columnIndex) { if (currentSortColumn === columnIndex) { currentSortOrder = currentSortOrder === "asc" ? "desc" : "asc"; } else { currentSortOrder = "asc"; } currentSortColumn = columnIndex; localStorage.setItem("sortColumn", columnIndex); localStorage.setItem("sortOrder", currentSortOrder); fetchLogs(); } function updateSortIcons() { for (let i = 0; i < 7; i++) { let icon = document.getElementById(`sortIcon${i}`); if (icon) icon.innerText = ""; // Reset all to default } if (currentSortColumn !== null) { let icon = document.getElementById(`sortIcon${currentSortColumn}`); if (icon) { icon.innerText = currentSortOrder === "asc" ? "▲" : "▼"; } } } // ✅ Automatically refresh only the active views setInterval(() => { let table = document.getElementById("logTable"); let graph = document.getElementById("logChart"); let refreshIcon = document.getElementById("refreshIcon"); // ✅ Change ⟳ to a spinning version when refreshing refreshIcon.classList.add("refreshing"); let fetchPromises = []; if (table.style.display !== "none") { fetchPromises.push(fetchLogs()); } else if (graph.style.display !== "none") { fetchPromises.push(fetchGraphData()); } // ✅ Revert back to static ⟳ after refresh completes Promise.all(fetchPromises).then(() => { setTimeout(() => { refreshIcon.classList.remove("refreshing"); }, 500); }); }, 5000);