forked from UKSOURCE/cms.hailearning.edu.vn
feat: implement comprehensive audit logging system
This commit is contained in:
699
views/admin/audit-log/index.ejs
Normal file
699
views/admin/audit-log/index.ejs
Normal file
@@ -0,0 +1,699 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
|
||||
<%= title %>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">System activity and change tracking</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-warning" onclick="openCleanupModal()">
|
||||
<i class="fas fa-broom me-2"></i>Cleanup Old Logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<form method="GET" action="/admin/audit-logs" class="row g-3">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Model</label>
|
||||
<select class="form-select" name="model">
|
||||
<option value="">All Models</option>
|
||||
<% uniqueModels.forEach(model => { %>
|
||||
<option value="<%= model %>" <%= query.model === model ? 'selected' : '' %>>
|
||||
<%= model %>
|
||||
</option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Action</label>
|
||||
<select class="form-select" name="action">
|
||||
<option value="">All Actions</option>
|
||||
<% uniqueActions.forEach(action => { %>
|
||||
<option value="<%= action %>" <%= query.action === action ? 'selected' : '' %>>
|
||||
<%= action %>
|
||||
</option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">User</label>
|
||||
<select class="form-select" name="user">
|
||||
<option value="">All Users</option>
|
||||
<% users.forEach(user => { %>
|
||||
<option value="<%= user._id %>" <%= query.user === user._id.toString() ? 'selected' : '' %>>
|
||||
<%= user.username %>
|
||||
</option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">From Date</label>
|
||||
<input type="date" class="form-control" name="dateFrom" value="<%= query.dateFrom || '' %>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">To Date</label>
|
||||
<input type="date" class="form-control" name="dateTo" value="<%= query.dateTo || '' %>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Items per page</label>
|
||||
<select class="form-select" name="limit">
|
||||
<option value="5" <%= (query.limit && query.limit == '5') ? 'selected' : '' %>>5 items</option>
|
||||
<option value="7" <%= (!query.limit || query.limit == '7') ? 'selected' : '' %>>7 items</option>
|
||||
<option value="10" <%= (query.limit && query.limit == '10') ? 'selected' : '' %>>10 items</option>
|
||||
<option value="15" <%= (query.limit && query.limit == '15') ? 'selected' : '' %>>15 items</option>
|
||||
<option value="20" <%= (query.limit && query.limit == '20') ? 'selected' : '' %>>20 items</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label"> </label>
|
||||
<div class="d-flex gap-1">
|
||||
<button type="submit" class="btn flex-fill" style="background-color: var(--primary-color); color: white;">
|
||||
<i class="fas fa-search me-1"></i>Filter
|
||||
</button>
|
||||
<a href="/admin/audit-logs" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audit Logs List -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<% if (auditLogs && auditLogs.length > 0) { %>
|
||||
<!-- Summary Stats -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted">
|
||||
Showing <%= ((pagination.current - 1) * pagination.limit) + 1 %> -
|
||||
<%= Math.min(pagination.current * pagination.limit, pagination.totalCount) %>
|
||||
of <%= pagination.totalCount %> audit logs
|
||||
<% if (pagination.totalCount > pagination.limit) { %>
|
||||
<span class="badge bg-info ms-2">
|
||||
<%= pagination.total %> pages
|
||||
</span>
|
||||
<% } %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="btn-group btn-group-sm me-3" role="group">
|
||||
<input type="radio" class="btn-check" name="viewMode" id="tableView" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-secondary" for="tableView">
|
||||
<i class="fas fa-table"></i> Table
|
||||
</label>
|
||||
<input type="radio" class="btn-check" name="viewMode" id="cardView" autocomplete="off">
|
||||
<label class="btn btn-outline-secondary" for="cardView">
|
||||
<i class="fas fa-th-large"></i> Cards
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table View -->
|
||||
<div id="tableViewContent" class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 140px; color: var(--primary-dark);">Date/Time</th>
|
||||
<th scope="col" style="color: var(--primary-dark);">Model</th>
|
||||
<th scope="col" style="color: var(--primary-dark);">Action</th>
|
||||
<th scope="col" style="color: var(--primary-dark);">User</th>
|
||||
<th scope="col" style="color: var(--primary-dark);">Changes</th>
|
||||
<th scope="col" style="color: var(--primary-dark);">IP Address</th>
|
||||
<th scope="col" style="width: 120px; color: var(--primary-dark);">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% auditLogs.forEach((log, index) => { %>
|
||||
<tr>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
<%= new Date(log.createdAt).toLocaleDateString() %><br>
|
||||
<%= new Date(log.createdAt).toLocaleTimeString() %>
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" style="background-color: var(--primary-color); color: white;">
|
||||
<%= log.model %>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<%
|
||||
let actionStyle = 'background-color: var(--primary-color); color: white;';
|
||||
if (log.action.includes('CREATE')) actionStyle = 'background-color: #28a745; color: white;';
|
||||
else if (log.action.includes('UPDATE')) actionStyle = 'background-color: #ffc107; color: #212529;';
|
||||
else if (log.action.includes('DELETE')) actionStyle = 'background-color: #dc3545; color: white;';
|
||||
%>
|
||||
<span class="badge" style="<%= actionStyle %>">
|
||||
<%= log.action %>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<% if (log.performedBy) { %>
|
||||
<div>
|
||||
<strong style="color: var(--primary-dark);"><%= log.performedBy.username %></strong><br>
|
||||
<small class="text-muted"><%= log.performedBy.email %></small>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<span class="text-muted">System</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (log.changes && log.changes.length > 0) { %>
|
||||
<span class="badge" style="background-color: var(--primary-color); color: white;">
|
||||
<%= log.changes.length %> field<%= log.changes.length > 1 ? 's' : '' %>
|
||||
</span>
|
||||
<div class="mt-1">
|
||||
<% log.changes.slice(0, 3).forEach(change => { %>
|
||||
<small class="text-muted d-block">
|
||||
<strong><%= change.field %></strong>
|
||||
</small>
|
||||
<% }); %>
|
||||
<% if (log.changes.length > 3) { %>
|
||||
<small class="text-muted">
|
||||
+<%= log.changes.length - 3 %> more...
|
||||
</small>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<span class="text-muted">-</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
<%= log.ipAddress %>
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="/admin/audit-logs/<%= log._id %>" class="btn btn-sm" style="background-color: var(--primary-color); color: white;">
|
||||
<i class="fas fa-eye me-1"></i>View
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Card View (Hidden by default) -->
|
||||
<div id="cardViewContent" style="display: none;">
|
||||
<% auditLogs.forEach((log, index) => { %>
|
||||
<div class="audit-card">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center py-2">
|
||||
<span class="badge" style="background-color: var(--primary-color); color: white; font-size: 0.7rem;"><%= log.model %></span>
|
||||
<small class="text-muted" style="font-size: 0.7rem;">
|
||||
<%= new Date(log.createdAt).toLocaleDateString() %>
|
||||
</small>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<%
|
||||
let actionStyle = 'background-color: var(--primary-color); color: white;';
|
||||
if (log.action.includes('CREATE')) actionStyle = 'background-color: #28a745; color: white;';
|
||||
else if (log.action.includes('UPDATE')) actionStyle = 'background-color: #ffc107; color: #212529;';
|
||||
else if (log.action.includes('DELETE')) actionStyle = 'background-color: #dc3545; color: white;';
|
||||
%>
|
||||
<span class="badge" style="<%= actionStyle %>; font-size: 0.65rem;">
|
||||
<%= log.action %>
|
||||
</span>
|
||||
<small class="text-muted" style="font-size: 0.7rem;">
|
||||
<%= new Date(log.createdAt).toLocaleTimeString() %>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<strong style="font-size: 0.8rem;">User:</strong>
|
||||
<% if (log.performedBy) { %>
|
||||
<div style="color: var(--primary-dark); font-size: 0.8rem;"><%= log.performedBy.username %></div>
|
||||
<small class="text-muted" style="font-size: 0.7rem;"><%= log.performedBy.email %></small>
|
||||
<% } else { %>
|
||||
<span class="text-muted" style="font-size: 0.8rem;">System</span>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<% if (log.changes && log.changes.length > 0) { %>
|
||||
<div class="mb-2">
|
||||
<strong style="font-size: 0.8rem;">Changes:</strong>
|
||||
<span class="badge ms-1" style="background-color: var(--primary-color); color: white; font-size: 0.65rem;">
|
||||
<%= log.changes.length %> field<%= log.changes.length > 1 ? 's' : '' %>
|
||||
</span>
|
||||
<div class="mt-1">
|
||||
<% log.changes.slice(0, 2).forEach(change => { %>
|
||||
<small class="text-muted d-block" style="font-size: 0.7rem;">
|
||||
• <%= change.field %>
|
||||
</small>
|
||||
<% }); %>
|
||||
<% if (log.changes.length > 2) { %>
|
||||
<small class="text-muted" style="font-size: 0.7rem;">
|
||||
+<%= log.changes.length - 2 %> more...
|
||||
</small>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="mb-2">
|
||||
<small class="text-muted" style="font-size: 0.7rem;">
|
||||
IP: <%= log.ipAddress %>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer p-2">
|
||||
<a href="/admin/audit-logs/<%= log._id %>" class="btn btn-sm w-100" style="background-color: var(--primary-color); color: white; font-size: 0.8rem;">
|
||||
<i class="fas fa-eye me-1"></i>View Details
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<% if (pagination && pagination.total > 1) { %>
|
||||
<nav aria-label="Audit log pagination" class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
<% if (pagination.current > 1) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=<%= pagination.current - 1 %><%= Object.keys(query).map(key => key !== 'page' ? '&' + key + '=' + encodeURIComponent(query[key]) : '').join('') %>" style="color: var(--primary-dark);">Previous</a>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% for (let i = 1; i <= pagination.total; i++) { %>
|
||||
<% if (i === pagination.current) { %>
|
||||
<li class="page-item active">
|
||||
<span class="page-link" style="background-color: var(--primary-color); border-color: var(--primary-color);"><%= i %></span>
|
||||
</li>
|
||||
<% } else if (i === 1 || i === pagination.total || (i >= pagination.current - 2 && i <= pagination.current + 2)) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=<%= i %><%= Object.keys(query).map(key => key !== 'page' ? '&' + key + '=' + encodeURIComponent(query[key]) : '').join('') %>" style="color: var(--primary-dark);">
|
||||
<%= i %>
|
||||
</a>
|
||||
</li>
|
||||
<% } else if (i === pagination.current - 3 || i === pagination.current + 3) { %>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">...</span>
|
||||
</li>
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
||||
<% if (pagination.current < pagination.total) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=<%= pagination.current + 1 %><%= Object.keys(query).map(key => key !== 'page' ? '&' + key + '=' + encodeURIComponent(query[key]) : '').join('') %>" style="color: var(--primary-dark);">Next</a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
|
||||
<!-- Pagination Info -->
|
||||
<div class="text-center mt-2">
|
||||
<small class="text-muted">
|
||||
Page <%= pagination.current %> of <%= pagination.total %>
|
||||
(showing <%= ((pagination.current - 1) * pagination.limit) + 1 %> -
|
||||
<%= Math.min(pagination.current * pagination.limit, pagination.totalCount) %>
|
||||
of <%= pagination.totalCount %> total items)
|
||||
</small>
|
||||
</div>
|
||||
</nav>
|
||||
<% } %>
|
||||
<% } else { %>
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-clipboard-list text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<h5 class="text-muted mb-3">No Audit Logs Found</h5>
|
||||
<p class="text-muted">No audit logs match your current filters.</p>
|
||||
<a href="/admin/audit-logs" class="btn" style="background-color: var(--primary-color); color: white;">
|
||||
<i class="fas fa-refresh me-1"></i>Clear Filters
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Cleanup Modal -->
|
||||
<div id="cleanupModal" class="custom-modal" style="display: none;">
|
||||
<div class="custom-modal-overlay"></div>
|
||||
<div class="custom-modal-content">
|
||||
<div class="custom-modal-header">
|
||||
<h5 class="custom-modal-title">
|
||||
<i class="fas fa-broom me-2"></i>Cleanup Old Audit Logs
|
||||
</h5>
|
||||
<button type="button" class="custom-modal-close" onclick="closeCleanupModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<form method="POST" action="/admin/audit-logs/cleanup">
|
||||
<div class="custom-modal-body">
|
||||
<p>Delete audit logs older than the specified number of days.</p>
|
||||
<div class="mb-3">
|
||||
<label for="days" class="form-label">Keep logs for (days):</label>
|
||||
<input type="number" class="form-control" id="days" name="days" value="90" min="1" max="365" required>
|
||||
<div class="form-text">Recommended: 90 days for compliance</div>
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<strong>Warning:</strong> This action cannot be undone. Deleted audit logs will be permanently removed.
|
||||
</div>
|
||||
</div>
|
||||
<div class="custom-modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeCleanupModal()">Cancel</button>
|
||||
<button type="submit" class="btn btn-warning">
|
||||
<i class="fas fa-broom me-1"></i>Cleanup Logs
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// View mode toggle
|
||||
const tableViewRadio = document.getElementById('tableView');
|
||||
const cardViewRadio = document.getElementById('cardView');
|
||||
const tableViewContent = document.getElementById('tableViewContent');
|
||||
const cardViewContent = document.getElementById('cardViewContent');
|
||||
|
||||
// Debug: Check if elements exist
|
||||
console.log('Elements found:', {
|
||||
tableViewRadio: !!tableViewRadio,
|
||||
cardViewRadio: !!cardViewRadio,
|
||||
tableViewContent: !!tableViewContent,
|
||||
cardViewContent: !!cardViewContent
|
||||
});
|
||||
|
||||
if (!tableViewRadio || !cardViewRadio || !tableViewContent || !cardViewContent) {
|
||||
console.error('Some view toggle elements are missing!');
|
||||
return;
|
||||
}
|
||||
|
||||
tableViewRadio.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
tableViewContent.classList.remove('hidden');
|
||||
cardViewContent.classList.remove('active');
|
||||
console.log('Table view activated');
|
||||
}
|
||||
});
|
||||
|
||||
cardViewRadio.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
tableViewContent.classList.add('hidden');
|
||||
cardViewContent.classList.add('active');
|
||||
console.log('Card view activated with CSS Grid');
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-submit form when changing items per page
|
||||
const limitSelect = document.querySelector('select[name="limit"]');
|
||||
if (limitSelect) {
|
||||
limitSelect.addEventListener('change', function() {
|
||||
// Reset to page 1 when changing limit
|
||||
const pageInput = document.querySelector('input[name="page"]');
|
||||
if (pageInput) {
|
||||
pageInput.value = 1;
|
||||
} else {
|
||||
// Create hidden input for page
|
||||
const hiddenPageInput = document.createElement('input');
|
||||
hiddenPageInput.type = 'hidden';
|
||||
hiddenPageInput.name = 'page';
|
||||
hiddenPageInput.value = 1;
|
||||
this.form.appendChild(hiddenPageInput);
|
||||
}
|
||||
|
||||
// Submit the form
|
||||
this.form.submit();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function openCleanupModal() {
|
||||
document.getElementById('cleanupModal').style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeCleanupModal() {
|
||||
document.getElementById('cleanupModal').style.display = 'none';
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
// Close modal when clicking overlay
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('custom-modal-overlay')) {
|
||||
closeCleanupModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal with Escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeCleanupModal();
|
||||
}
|
||||
});
|
||||
|
||||
function exportLogs() {
|
||||
// Get current filter parameters
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set('export', 'csv');
|
||||
|
||||
// Create download link
|
||||
const exportUrl = '/admin/audit-logs/export?' + params.toString();
|
||||
window.open(exportUrl, '_blank');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.table th {
|
||||
border-top: none;
|
||||
font-weight: 600;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.btn-group-sm .btn {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.pagination .page-link {
|
||||
color: var(--primary-dark) !important;
|
||||
background-color: #fff !important;
|
||||
border: 1px solid #dee2e6 !important;
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pagination .page-item.active .page-link {
|
||||
background-color: var(--primary-color) !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
color: #fff !important;
|
||||
z-index: 3;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pagination .page-link:hover {
|
||||
color: var(--primary-color) !important;
|
||||
background-color: #f8f9fa !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.pagination .page-link:focus {
|
||||
color: var(--primary-color) !important;
|
||||
background-color: #f8f9fa !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
box-shadow: 0 0 0 0.2rem rgba(var(--primary-color-rgb), 0.25);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.pagination .page-item.disabled .page-link {
|
||||
color: #6c757d !important;
|
||||
background-color: #fff !important;
|
||||
border-color: #dee2e6 !important;
|
||||
}
|
||||
|
||||
/* Ensure pagination text is always visible */
|
||||
.pagination .page-link span,
|
||||
.pagination .page-link {
|
||||
display: inline-block;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
/* Custom Modal Styles */
|
||||
.custom-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1050;
|
||||
}
|
||||
|
||||
.custom-modal-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.custom-modal-content {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.custom-modal-header {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
background-color: #fff3cd;
|
||||
border-radius: 8px 8px 0 0;
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.custom-modal-title {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #856404;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.custom-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.2rem;
|
||||
color: #856404;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.custom-modal-close:hover {
|
||||
background-color: rgba(133, 100, 4, 0.1);
|
||||
}
|
||||
|
||||
.custom-modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.custom-modal-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid #dee2e6;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0 0 8px 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Card View Improvements - Using CSS Grid for better control */
|
||||
#cardViewContent {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#cardViewContent.active {
|
||||
display: grid !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
#cardViewContent .card {
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
width: 100% !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
#cardViewContent .card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
#cardViewContent .card-header {
|
||||
background-color: #f8f9fa !important;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
#cardViewContent .card-footer {
|
||||
background-color: #f8f9fa !important;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
/* Table View */
|
||||
#tableViewContent {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#tableViewContent.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Responsive grid adjustments */
|
||||
@media (max-width: 1400px) {
|
||||
#cardViewContent {
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
#cardViewContent {
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
#cardViewContent {
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#cardViewContent {
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 1rem !important;
|
||||
}
|
||||
|
||||
.custom-modal-content {
|
||||
width: 95%;
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
314
views/admin/audit-log/show.ejs
Normal file
314
views/admin/audit-log/show.ejs
Normal file
@@ -0,0 +1,314 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
|
||||
<%= title %>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Detailed audit log information</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="/admin/audit-logs" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i>Back to Audit Logs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Main Information -->
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0" style="color: var(--primary-dark);">
|
||||
<i class="fas fa-info-circle me-2"></i>Audit Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Model:</label>
|
||||
<span class="badge ms-2" style="background-color: var(--primary-color); color: white;"><%= auditLog.model %></span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Action:</label>
|
||||
<%
|
||||
let actionStyle = 'background-color: var(--primary-color); color: white;';
|
||||
if (auditLog.action.includes('CREATE')) actionStyle = 'background-color: #28a745; color: white;';
|
||||
else if (auditLog.action.includes('UPDATE')) actionStyle = 'background-color: #ffc107; color: #212529;';
|
||||
else if (auditLog.action.includes('DELETE')) actionStyle = 'background-color: #dc3545; color: white;';
|
||||
%>
|
||||
<span class="badge ms-2" style="<%= actionStyle %>"><%= auditLog.action %></span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Document ID:</label>
|
||||
<code class="ms-2" style="background-color: #f8f9fa; color: var(--primary-dark);"><%= auditLog.documentId %></code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Date & Time:</label>
|
||||
<div class="ms-2">
|
||||
<%= new Date(auditLog.createdAt).toLocaleDateString() %>
|
||||
<%= new Date(auditLog.createdAt).toLocaleTimeString() %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">User:</label>
|
||||
<div class="ms-2">
|
||||
<% if (auditLog.performedBy) { %>
|
||||
<strong style="color: var(--primary-dark);"><%= auditLog.performedBy.username %></strong><br>
|
||||
<small class="text-muted"><%= auditLog.performedBy.email %></small>
|
||||
<% } else { %>
|
||||
<span class="text-muted">System</span>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">IP Address:</label>
|
||||
<code class="ms-2" style="background-color: #f8f9fa; color: var(--primary-dark);"><%= auditLog.ipAddress %></code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if (auditLog.userAgent) { %>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">User Agent:</label>
|
||||
<div class="ms-2">
|
||||
<small class="text-muted font-monospace"><%= auditLog.userAgent %></small>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Changes Details -->
|
||||
<% if (auditLog.changes && auditLog.changes.length > 0) { %>
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0" style="color: var(--primary-dark);">
|
||||
<i class="fas fa-edit me-2"></i>Field Changes
|
||||
<span class="badge ms-2" style="background-color: var(--primary-color); color: white;"><%= auditLog.changes.length %></span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if (user && (user.role === 'admin' || user.role === 'superadmin')) { %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 200px; color: var(--primary-dark);">Field</th>
|
||||
<th style="color: var(--primary-dark);">Before</th>
|
||||
<th style="color: var(--primary-dark);">After</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% auditLog.changes.forEach((change, index) => { %>
|
||||
<tr>
|
||||
<td>
|
||||
<strong style="color: var(--primary-dark);"><%= change.field %></strong>
|
||||
</td>
|
||||
<td>
|
||||
<div class="change-value before">
|
||||
<% if (change.before === null || change.before === undefined) { %>
|
||||
<span class="text-muted fst-italic">null</span>
|
||||
<% } else if (typeof change.before === 'object') { %>
|
||||
<pre class="mb-0"><%= JSON.stringify(change.before, null, 2) %></pre>
|
||||
<% } else { %>
|
||||
<%= change.before %>
|
||||
<% } %>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="change-value after">
|
||||
<% if (change.after === null || change.after === undefined) { %>
|
||||
<span class="text-muted fst-italic">null</span>
|
||||
<% } else if (typeof change.after === 'object') { %>
|
||||
<pre class="mb-0"><%= JSON.stringify(change.after, null, 2) %></pre>
|
||||
<% } else { %>
|
||||
<%= change.after %>
|
||||
<% } %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Summary View:</strong> Detailed field values are restricted to administrators.
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="color: var(--primary-dark);">Field</th>
|
||||
<th style="color: var(--primary-dark);">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% auditLog.changes.forEach((change, index) => { %>
|
||||
<tr>
|
||||
<td>
|
||||
<strong style="color: var(--primary-dark);"><%= change.field %></strong>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info">Modified</span>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header">
|
||||
<h6 class="card-title mb-0" style="color: var(--primary-dark);">
|
||||
<i class="fas fa-chart-bar me-2"></i>Summary
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-6">
|
||||
<div class="border-end">
|
||||
<div class="h5 mb-0" style="color: var(--primary-color);">
|
||||
<%= auditLog.changes ? auditLog.changes.length : 0 %>
|
||||
</div>
|
||||
<small class="text-muted">Fields Changed</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="h5 mb-0" style="color: var(--primary-color);">
|
||||
<%= new Date(auditLog.createdAt).toLocaleDateString() === new Date().toLocaleDateString() ? 'Today' : 'Past' %>
|
||||
</div>
|
||||
<small class="text-muted">Timing</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Raw Data (Admin Only - Collapsible) -->
|
||||
<!-- <% if (user && (user.role === 'admin' || user.role === 'superadmin')) { %>
|
||||
<div class="card border-0 shadow-sm mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<button class="btn btn-link p-0 text-decoration-none" type="button" data-bs-toggle="collapse" data-bs-target="#rawDataCollapse" aria-expanded="false" style="color: var(--primary-dark);">
|
||||
<i class="fas fa-code me-2"></i>Raw Data <span class="badge bg-warning text-dark ms-2">Admin Only</span>
|
||||
<i class="fas fa-chevron-down ms-2"></i>
|
||||
</button>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="collapse" id="rawDataCollapse">
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Security Notice:</strong> Raw data contains sensitive information and is only visible to administrators.
|
||||
</div>
|
||||
<div class="row">
|
||||
<% if (auditLog.before) { %>
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-danger">Before:</h6>
|
||||
<pre class="p-3 rounded" style="background-color: #f8f9fa; border: 1px solid #dee2e6; max-height: 400px; overflow-y: auto;"><%= JSON.stringify(auditLog.before, null, 2) %></pre>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if (auditLog.after) { %>
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-success">After:</h6>
|
||||
<pre class="p-3 rounded" style="background-color: #f8f9fa; border: 1px solid #dee2e6; max-height: 400px; overflow-y: auto;"><%= JSON.stringify(auditLog.after, null, 2) %></pre>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<% if (!auditLog.before && !auditLog.after) { %>
|
||||
<div class="text-center text-muted py-3">
|
||||
<i class="fas fa-info-circle me-2"></i>No raw data available for this audit log.
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="card border-0 shadow-sm mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0" style="color: var(--primary-dark);">
|
||||
<i class="fas fa-lock me-2"></i>Raw Data
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-shield-alt me-2"></i>
|
||||
<strong>Access Restricted:</strong> Raw data access is limited to administrators for security reasons.
|
||||
</div>
|
||||
<div class="text-center text-muted py-3">
|
||||
<i class="fas fa-user-shield" style="font-size: 3rem; opacity: 0.3;"></i>
|
||||
<p class="mt-3">Contact your administrator if you need access to detailed raw data.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %> -->
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.change-value {
|
||||
max-width: 300px;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.change-value pre {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.8rem;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.change-value.before {
|
||||
background-color: #fff5f5;
|
||||
border-left: 3px solid #dc3545;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.change-value.after {
|
||||
background-color: #f0fff4;
|
||||
border-left: 3px solid #28a745;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.font-monospace {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
color: var(--primary-dark) !important;
|
||||
}
|
||||
|
||||
.btn-link:hover {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.table th {
|
||||
border-top: none;
|
||||
font-weight: 600;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fa !important;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user