Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
140
status/js/audit-log.js
Normal file
140
status/js/audit-log.js
Normal file
@@ -0,0 +1,140 @@
|
||||
// ========== AUDIT LOG VIEWER ==========
|
||||
(function() {
|
||||
// Inject modal HTML
|
||||
injectModal('audit-modal', `<div id="audit-modal" class="weather-modal">
|
||||
<div class="weather-modal-content" style="min-width: 850px; max-width: 1050px;">
|
||||
<h3>📜 Audit Log</h3>
|
||||
<p class="modal-subtitle">
|
||||
Track all actions performed through the API.
|
||||
</p>
|
||||
|
||||
<div style="display: flex; gap: 12px; margin-bottom: 16px; align-items: center;">
|
||||
<label class="text-muted-sm">Filter:</label>
|
||||
<select id="audit-filter" style="padding: 6px 10px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); color: var(--fg); font-size: 0.85rem;">
|
||||
<option value="">All Actions</option>
|
||||
<option value="service">Services</option>
|
||||
<option value="container">Containers</option>
|
||||
<option value="caddy">Caddy</option>
|
||||
<option value="dns">DNS</option>
|
||||
<option value="backup">Backups</option>
|
||||
<option value="config">Config</option>
|
||||
<option value="auth">Auth</option>
|
||||
</select>
|
||||
<button id="audit-refresh-btn" class="btn-sm">🔄 Refresh</button>
|
||||
<span style="flex: 1;"></span>
|
||||
<button id="audit-clear-btn" style="padding: 6px 12px; font-size: 0.8rem; color: var(--bad-fg); border-color: var(--bad-fg);">🗑️ Clear Log</button>
|
||||
</div>
|
||||
|
||||
<div id="audit-log-container" class="scroll-container">
|
||||
<div class="panel-empty"><span class="brand-spinner"></span> Loading audit log...</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 12px; text-align: center;">
|
||||
<button id="audit-load-more" style="display: none; padding: 6px 16px; font-size: 0.8rem;">Load More</button>
|
||||
</div>
|
||||
|
||||
<div class="weather-modal-buttons modal-footer-bar">
|
||||
<button id="audit-cancel">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`);
|
||||
|
||||
const modal = document.getElementById('audit-modal');
|
||||
const openBtn = document.getElementById('audit-log-btn');
|
||||
const cancelBtn = document.getElementById('audit-cancel');
|
||||
const refreshBtn = document.getElementById('audit-refresh-btn');
|
||||
const clearBtn = document.getElementById('audit-clear-btn');
|
||||
const filterSelect = document.getElementById('audit-filter');
|
||||
const container = document.getElementById('audit-log-container');
|
||||
const loadMoreBtn = document.getElementById('audit-load-more');
|
||||
let currentOffset = 0;
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
async function loadAudit(append) {
|
||||
try {
|
||||
if (!append) {
|
||||
currentOffset = 0;
|
||||
container.innerHTML = '<div class="panel-empty"><span class="brand-spinner"></span> Loading...</div>';
|
||||
}
|
||||
const filter = filterSelect.value;
|
||||
let url = `/api/v1/audit-logs?limit=${PAGE_SIZE}&offset=${currentOffset}`;
|
||||
if (filter) url += `&action=${encodeURIComponent(filter)}`;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
const entries = data.success && data.entries ? data.entries : [];
|
||||
|
||||
if (entries.length === 0 && !append) {
|
||||
container.innerHTML = '<div class="panel-empty"><span class="empty-icon">📜</span>No audit log entries yet. Actions will be logged automatically.</div>';
|
||||
loadMoreBtn.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
if (!append) {
|
||||
html = '<table style="width: 100%; border-collapse: collapse; font-size: 0.82rem;">';
|
||||
html += '<tr style="border-bottom: 1px solid var(--border); color: var(--muted);"><th style="padding: 6px; text-align: left;">When</th><th style="padding: 6px; text-align: left;">IP</th><th style="padding: 6px; text-align: left;">Action</th><th style="padding: 6px; text-align: left;">Resource</th><th style="padding: 6px; text-align: left;">Result</th></tr>';
|
||||
}
|
||||
|
||||
for (const e of entries) {
|
||||
const ok = e.outcome === 'success';
|
||||
html += `<tr style="border-bottom: 1px solid var(--border); cursor: pointer;" class="audit-row">`;
|
||||
html += `<td style="padding: 6px; color: var(--muted);">${timeAgo(e.timestamp)}</td>`;
|
||||
html += `<td style="padding: 6px; font-family: monospace; font-size: 0.78rem;">${e.ip || '-'}</td>`;
|
||||
html += `<td style="padding: 6px; font-weight: 500;">${e.action || '-'}</td>`;
|
||||
html += `<td style="padding: 6px;">${e.resource || '-'}</td>`;
|
||||
html += `<td style="padding: 6px;"><span style="color: ${ok ? 'var(--ok-fg)' : 'var(--bad-fg)'};">${ok ? '✓' : '✗'}</span></td>`;
|
||||
html += '</tr>';
|
||||
if (e.details && Object.keys(e.details).length > 0) {
|
||||
html += `<tr class="audit-detail" style="display: none;"><td colspan="5" style="padding: 6px 6px 10px; font-size: 0.78rem; color: var(--muted);"><pre style="margin: 0; white-space: pre-wrap; font-family: monospace;">${JSON.stringify(e.details, null, 2)}</pre></td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!append) {
|
||||
html += '</table>';
|
||||
container.innerHTML = html;
|
||||
} else {
|
||||
// Append rows to existing table
|
||||
const table = container.querySelector('table');
|
||||
if (table) table.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
currentOffset += entries.length;
|
||||
loadMoreBtn.style.display = entries.length >= PAGE_SIZE ? '' : 'none';
|
||||
|
||||
// Toggle detail rows on click
|
||||
container.querySelectorAll('.audit-row').forEach(row => {
|
||||
if (row.dataset.wired) return;
|
||||
row.dataset.wired = 'true';
|
||||
row.addEventListener('click', () => {
|
||||
const detail = row.nextElementSibling;
|
||||
if (detail && detail.classList.contains('audit-detail')) {
|
||||
detail.style.display = detail.style.display === 'none' ? '' : 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
container.innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
openBtn?.addEventListener('click', () => {
|
||||
modal?.classList.add('show');
|
||||
loadAudit(false);
|
||||
});
|
||||
wireModal(modal, cancelBtn);
|
||||
refreshBtn?.addEventListener('click', () => loadAudit(false));
|
||||
filterSelect?.addEventListener('change', () => loadAudit(false));
|
||||
loadMoreBtn?.addEventListener('click', () => loadAudit(true));
|
||||
|
||||
clearBtn?.addEventListener('click', async () => {
|
||||
if (!confirm('Clear the entire audit log? This cannot be undone.')) return;
|
||||
try {
|
||||
const res = await secureFetch('/api/v1/audit-logs', { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.success) loadAudit(false);
|
||||
else showNotification('Error: ' + (data.error || 'Clear failed'), 'error');
|
||||
} catch (e) {
|
||||
showNotification('Error: ' + e.message, 'error');
|
||||
}
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user