@kingdiablo/auditor
Version:
A lightweight and customizable audit logger for Node.js apps. Tracks database changes, errors, and user actions with support for external loggers like Winston or Pino.
583 lines (502 loc) • 20.4 kB
JavaScript
// Refactored and improved JavaScript for Audit Log Dashboard
const DEFAULT_COLORS = {
info: '#2196F3',
error: '#F44336',
warning: '#FF9800',
success: '#4CAF50',
signal: '#9C27B0',
system: '#607D8B',
request: '#FF5722',
};
let currentColors = { ...DEFAULT_COLORS };
let logsPerPage = parseInt(localStorage.getItem('logsPerPage'), 10) || 10;
let currentPage = 1;
let filteredLogs = [];
let logData = [];
let logChart;
const DOM = {
themeToggle: document.getElementById('themeToggleCheckbox'),
timeRange: document.getElementById('timeRange'),
chartType: document.getElementById('chartType'),
exportChart: document.getElementById('exportChart'),
logTable: document.getElementById('logTable').querySelector('tbody'),
search: document.getElementById('searchLogs'),
prevPage: document.getElementById('prevPage'),
nextPage: document.getElementById('nextPage'),
pageNumbers: document.getElementById('pageNumbers'),
paginationInfo: document.getElementById('paginationInfo'),
closeSidebar: document.getElementById('closeSidebar'),
colorControls: document.getElementById('colorSettingsPanel'),
modal: document.getElementById('logModal'),
modalBody: document.getElementById('modalBody'),
closeModal: document.querySelector('.close-modal'),
entriesPerPage: document.getElementById('entriesPerPage'),
logTypeFilter: document.getElementById('logTypeFilter'),
startDate: document.getElementById('startDate'),
endDate: document.getElementById('endDate'),
applyCustomRange: document.getElementById('applyCustomRange'),
customRangeContainer: document.getElementById('customRangeContainer')
};
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function generateColor() {
const h = Math.floor(Math.random() * 360);
return `hsl(${h}, 70%, 60%)`;
}
function getColor(type) {
if (!currentColors[type]) currentColors[type] = generateColor();
return currentColors[type];
}
function shadeColor(color, percent) {
const num = parseInt(color.slice(1), 16);
let R = (num >> 16) + percent;
let G = ((num >> 8) & 0x00FF) + percent;
let B = (num & 0x0000FF) + percent;
return `#${(1 << 24 | Math.min(255, R) << 16 | Math.min(255, G) << 8 | Math.min(255, B)).toString(16).slice(1)}`;
}
function formatDate(date, full = false) {
const opt = full ? {
year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit'
} : {
year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
};
return new Date(date).toLocaleDateString(undefined, opt);
}
function toggleTheme() {
const theme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
updateChart();
}
function initTheme() {
const saved = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', saved || (prefersDark ? 'dark' : 'light'));
if (!saved) localStorage.setItem('theme', prefersDark ? 'dark' : 'light');
}
function applyDateFilter() {
const now = new Date();
const range = DOM.timeRange.value;
let start, end;
switch (range) {
case 'today':
start = new Date(now.setHours(0, 0, 0, 0));
end = new Date();
break;
case 'yesterday':
start = new Date(now.setDate(now.getDate() - 1));
start.setHours(0, 0, 0, 0);
end = new Date(now.setHours(23, 59, 59, 999));
break;
case 'week':
start = new Date(now);
start.setDate(start.getDate() - start.getDay());
start.setHours(0, 0, 0, 0);
end = new Date();
break;
case 'month':
start = new Date(now.getFullYear(), now.getMonth(), 1);
end = new Date();
break;
case 'custom':
const s = new Date(DOM.startDate.value);
const e = new Date(DOM.endDate.value);
if (!isNaN(s) && !isNaN(e)) {
start = s;
end = new Date(e.setHours(23, 59, 59, 999));
} else return;
break;
default:
filteredLogs = [...logData];
return;
}
filteredLogs = logData.filter(log => new Date(log.timeStamp) >= start && new Date(log.timeStamp) <= end);
updateChart();
updateTable();
updatePagination();
}
function updateChart() {
const ctx = document.getElementById('logChart').getContext('2d');
const chartType = DOM.chartType.value;
// Get theme vars
const theme = getComputedStyle(document.documentElement);
const textColor = theme.getPropertyValue('--text-color').trim();
const borderColor = theme.getPropertyValue('--border-color').trim();
const cardBg = theme.getPropertyValue('--card-bg').trim();
// Clean chart if exists
if (logChart) logChart.destroy();
const types = [...new Set(filteredLogs.map(log => log.type))]; // ✅ Define this before use
const grouped = {};
filteredLogs.forEach(log => {
const date = new Date(log.timeStamp);
const hour = `${date.getHours().toString().padStart(2, '0')}:00`; // "14:00"
if (!grouped[hour]) grouped[hour] = {};
if (!grouped[hour][log.type]) grouped[hour][log.type] = 0;
grouped[hour][log.type]++;
});
const allHours = Array.from({ length: 24 }, (_, h) => `${h.toString().padStart(2, '0')}:00`);
const logTypes = [...new Set(filteredLogs.map(log => log.type))];
// Prepare datasets
const datasets = logTypes.map(type => {
return {
label: capitalize(type),
data: allHours.map(hour => grouped[hour]?.[type] || 0),
backgroundColor: getColor(type),
borderColor: shadeColor(getColor(type), -20),
fill: chartType !== 'line',
tension: 0.4,
borderWidth: 2,
pointBackgroundColor: getColor(type),
pointBorderColor: borderColor,
};
});
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: textColor,
usePointStyle: true,
boxWidth: 12,
padding: 15
}
},
tooltip: {
backgroundColor: cardBg,
titleColor: textColor,
bodyColor: textColor,
borderColor: borderColor,
borderWidth: 1
}
},
scales: chartType === 'pie' || chartType === 'doughnut' ? {} : {
x: {
ticks: { color: textColor },
grid: { color: borderColor },
title: {
display: true,
text: 'Time',
color: textColor
}
},
y: {
beginAtZero: true,
ticks: { color: textColor },
grid: { color: borderColor },
title: {
display: true,
text: 'Log Count',
color: textColor
}
}
}
};
if (chartType === 'pie' || chartType === 'doughnut') {
const countByType = {};
filteredLogs.forEach(log => {
countByType[log.type] = (countByType[log.type] || 0) + 1;
});
const labels = Object.keys(countByType);
const data = Object.values(countByType);
const colors = labels.map(getColor);
logChart = new Chart(ctx, {
type: chartType,
data: {
labels: labels.map(capitalize),
datasets: [{
data,
backgroundColor: colors,
borderColor: colors.map(c => shadeColor(c, -20)),
borderWidth: 1
}]
},
options
});
} else {
logChart = new Chart(ctx, {
type: chartType,
data: {
labels: allHours,
datasets
},
options
});
}
}
function getContrastYIQ(hexcolor) {
hexcolor = hexcolor.replace('#', '');
const r = parseInt(hexcolor.substr(0, 2), 16);
const g = parseInt(hexcolor.substr(2, 2), 16);
const b = parseInt(hexcolor.substr(4, 2), 16);
const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
return yiq >= 128 ? '#000000' : '#ffffff';
}
function updateLogTableColors() {
document.querySelectorAll('#logTable .badge').forEach(badge => {
const type = badge.textContent.trim().toLowerCase();
const bgColor = currentColors[type] || getColor(type);
badge.style.backgroundColor = bgColor;
badge.style.color = getContrastYIQ(bgColor);
});
}
function updateColorControls(types) {
DOM.colorControls.innerHTML = '';
types.forEach(type => {
const wrapper = document.createElement('div');
wrapper.className = 'color-control';
wrapper.innerHTML = `
<label for="color-${type}" style="color: var(--text-color); text-transform: capitalize;">
${type}
</label>
<div class="color-preview" style="background:${getColor(type)}"></div>
<input type="color" id="color-${type}" value="${getColor(type)}">
`;
const colorInput = wrapper.querySelector('input');
colorInput.addEventListener('input', e => {
currentColors[type] = e.target.value;
updateChart();
updateTable(); // 👈 Updates the badge colors
});
wrapper.querySelector('input').addEventListener('change', e => {
e.stopPropagation(); // ✅ Prevents dropdown from closing or parent actions
currentColors[type] = e.target.value;
localStorage.setItem('customColors', JSON.stringify(currentColors));
updateChart();
updateLogTableColors();
});
DOM.colorControls.appendChild(wrapper);
});
}
function updateTable() {
DOM.logTable.innerHTML = '';
const start = (currentPage - 1) * logsPerPage;
const pageLogs = filteredLogs.slice(start, start + logsPerPage);
if (!pageLogs.length) {
DOM.logTable.innerHTML = '<tr><td colspan="6" style="text-align:center;">No logs found</td></tr>';
return;
}
pageLogs.forEach(log => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${log.id}</td>
<td><span class="badge" style="background-color: ${getColor(log.type)}; color: #fff;">
${log.type.charAt(0).toUpperCase() + log.type.slice(1)}</span></td>
<td>${capitalize(log.action)}</td>
<td>${log?.message?.slice(0, 50)}${log?.message?.length > 50 ? '...' : ''}</td>
<td>${formatDate(log.timeStamp)}</td>
<td><button class="btn view-details" data-id="${log.id}"><i class="fas fa-eye"></i> View</button></td>
`;
DOM.logTable.appendChild(row);
});
document.querySelectorAll('.view-details').forEach(btn => btn.addEventListener('click', showDetails));
updateLogTableColors();
}
function updatePagination() {
const total = Math.ceil(filteredLogs.length / logsPerPage);
const start = (currentPage - 1) * logsPerPage + 1;
const end = Math.min(currentPage * logsPerPage, filteredLogs.length);
DOM.paginationInfo.textContent = `Showing ${start}-${end} of ${filteredLogs.length}`;
DOM.prevPage.disabled = currentPage === 1;
DOM.nextPage.disabled = currentPage === total;
DOM.pageNumbers.innerHTML = '';
const maxPages = 7;
let pages = [];
if (total <= maxPages) {
pages = Array.from({ length: total }, (_, i) => i + 1);
} else {
pages = [1];
if (currentPage > 3) pages.push('...');
const start = Math.max(2, currentPage - 1);
const end = Math.min(total - 1, currentPage + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (currentPage < total - 2) pages.push('...');
pages.push(total);
}
pages.forEach(p => {
const span = document.createElement('span');
span.className = `page-number ${p === currentPage ? 'active' : ''}`;
span.textContent = p;
if (p !== '...') span.addEventListener('click', () => goToPage(p));
DOM.pageNumbers.appendChild(span);
});
}
function goToPage(p) {
currentPage = p;
updateTable();
updatePagination();
}
function showDetails(e) {
const id = e.currentTarget.dataset.id;
const log = logData.find(l => l.id.toString() === id.toString());
DOM.modalBody.innerHTML = `
<div class="log-details-grid">
${Object.entries(log).map(([key, value]) => `
<div class="log-detail-item">
<div class="log-detail-key">${formatKey(key)}</div>
<div class="log-detail-value">${formatValue(value)}</div>
</div>
`).join('')}
</div>
`;
DOM.modal.style.display = 'block';
}
function formatKey(key) {
// Format camelCase or snake_case keys to Title Case
return key
.replace(/_/g, ' ')
.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/\b\w/g, l => l.toUpperCase());
}
function formatValue(value) {
if (typeof value === 'object') {
return `<pre>${JSON.stringify(value, null, 2)}</pre>`;
}
return String(value);
}
async function getAuditLogs() {
try {
const res = await fetch('/audit-log');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
return data.logs;
} catch {
return [
{ id: 1, type: 'info', action: 'login', message: 'User logged in', timeStamp: new Date(), details: { userId: 1 } },
{ id: 2, type: 'error', action: 'api_call', message: 'API failure', timeStamp: new Date(), details: { endpoint: '/api' } },
];
}
}
async function initDashboard() {
const stored = localStorage.getItem('customColors');
let savedColors = {};
try {
savedColors = stored ? JSON.parse(stored) : {};
} catch (e) {
console.warn('Failed to parse saved colors:', e);
}
currentColors = { ...DEFAULT_COLORS, ...savedColors };
initTheme();
logData = await getAuditLogs();
filteredLogs = [...logData];
const types = [...new Set(logData.map(l => l.type))];
populateTypeFilter();
updateColorControls(types);
DOM.logTypeFilter.innerHTML = `<option value="all">All Types</option>${types.map(t => `<option value="${t}">${capitalize(t)}</option>`).join('')}`;
const savedChartType = localStorage.getItem('chartType');
if (savedChartType && DOM.chartType.querySelector(`option[value="${savedChartType}"]`)) {
DOM.chartType.value = savedChartType;
}
updateChart();
updateTable();
updatePagination();
const toggleBtn = document.getElementById('toggleColorSettings');
const panel = document.getElementById('colorSettingsPanel');
toggleBtn.addEventListener('click', () => {
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
});
DOM.themeToggle.addEventListener('click', toggleTheme);
DOM.chartType.addEventListener('change', () => {
const type = DOM.chartType.value;
localStorage.setItem('chartType', type);
updateChart();
});
DOM.prevPage.addEventListener('click', () => goToPage(currentPage - 1));
DOM.nextPage.addEventListener('click', () => goToPage(currentPage + 1));
DOM.search.addEventListener('input', applyFilters);
DOM.logTypeFilter.addEventListener('change', applyFilters);
DOM.timeRange.addEventListener('change', () => {
const showCustom = DOM.timeRange.value === 'custom';
document.getElementById('customRangeContainer').style.display = showCustom ? 'flex' : 'none';
if (!showCustom) applyFilters();
});
DOM.applyCustomRange.addEventListener('click', applyFilters);
// DOM.logTypeFilter.addEventListener('change', applyFilters);
// DOM.search.addEventListener('input', handleSearch);
// DOM.timeRange.addEventListener('change', () => {
// const show = DOM.timeRange.value === 'custom';
// DOM.customRangeContainer.style.display = show ? 'flex' : 'none';
// if (!show) applyDateFilter();
// });
// DOM.applyCustomRange.addEventListener('click', applyDateFilter);
DOM.entriesPerPage.value = logsPerPage;
DOM.entriesPerPage.addEventListener('change', () => {
logsPerPage = +DOM.entriesPerPage.value;
localStorage.setItem('logsPerPage', logsPerPage);
currentPage = 1;
updateTable();
updatePagination();
});
DOM.closeModal.addEventListener('click', () => DOM.modal.style.display = 'none');
window.addEventListener('click', e => { if (e.target === DOM.modal) DOM.modal.style.display = 'none'; });
}
function applyFilters() {
const selectedType = DOM.logTypeFilter.value;
const selectedRange = DOM.timeRange.value;
const searchTerm = DOM.search.value.toLowerCase();
let filtered = [...logData];
// Filter by type
if (selectedType !== 'all') {
filtered = filtered.filter(log => log.type === selectedType);
}
// Filter by time range
const now = new Date();
const today = new Date(now.setHours(0, 0, 0, 0));
filtered = filtered.filter(log => {
const time = new Date(log.timeStamp);
switch (selectedRange) {
case 'today':
return time >= today;
case 'yesterday':
const yest = new Date(today);
yest.setDate(today.getDate() - 1);
return time >= yest && time < today;
case 'week':
const weekStart = new Date(today);
weekStart.setDate(today.getDate() - today.getDay());
return time >= weekStart;
case 'month':
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
return time >= monthStart;
case 'custom':
const start = new Date(DOM.startDate.value);
const end = new Date(DOM.endDate.value);
return time >= start && time <= end;
default:
return true;
}
});
// Search filter
if (searchTerm) {
filtered = filtered.filter(log =>
Object.values(log).some(v => v?.toString().toLowerCase().includes(searchTerm))
);
}
filteredLogs = filtered;
currentPage = 1;
updateChart();
updateTable();
updatePagination();
updateColorControls([...new Set(filteredLogs.map(log => log.type))]);
}
function populateTypeFilter() {
const types = [...new Set(logData.map(log => log.type))];
DOM.logTypeFilter.innerHTML = `<option value="all">All Types</option>`;
types.forEach(type => {
const opt = document.createElement('option');
opt.value = type;
opt.textContent = capitalize(type);
DOM.logTypeFilter.appendChild(opt);
});
}
function handleSearch() {
const term = DOM.search.value.toLowerCase();
filteredLogs = logData.filter(log =>
Object.values(log).some(v => v?.toString().toLowerCase().includes(term))
);
currentPage = 1;
updateTable();
updatePagination();
}
document.addEventListener('DOMContentLoaded', initDashboard);