forked from UKSOURCE/cms.hailearning.edu.vn
244 lines
12 KiB
Plaintext
244 lines
12 KiB
Plaintext
<!-- Page Header -->
|
||
<div class="page-title-area">
|
||
<div>
|
||
<h1>Audit Logs</h1>
|
||
<p class="subtitle">System activity and change tracking</p>
|
||
</div>
|
||
<button type="button" class="btn btn-outline-warning" onclick="openCleanupModal()">
|
||
<i class="fas fa-broom"></i> Cleanup Old Logs
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Filters -->
|
||
<div class="card border-0 mb-3">
|
||
<div class="card-body" style="padding:1rem 1.25rem;">
|
||
<form method="GET" action="/admin/audit-logs">
|
||
<div class="row g-2 align-items-end">
|
||
<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(u => { %>
|
||
<option value="<%= u._id %>" <%= query.user === u._id.toString() ? 'selected' : '' %>><%= u.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-1">
|
||
<label class="form-label">Per page</label>
|
||
<select class="form-select" name="limit" onchange="this.form.submit()">
|
||
<option value="8" <%= (!query.limit || query.limit == '8') ? 'selected' : '' %>>8</option>
|
||
<option value="15" <%= query.limit == '15' ? 'selected' : '' %>>15</option>
|
||
<option value="25" <%= query.limit == '25' ? 'selected' : '' %>>25</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-1 d-flex gap-1">
|
||
<button type="submit" class="btn btn-primary flex-fill"><i class="fas fa-search"></i></button>
|
||
<a href="/admin/audit-logs" class="btn btn-outline-secondary"><i class="fas fa-times"></i></a>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Table -->
|
||
<div class="card border-0">
|
||
<div class="card-header">
|
||
<h5 class="card-header-title"><i class="fas fa-shield-alt"></i> Activity Log</h5>
|
||
<% if (pagination) { %>
|
||
<span class="badge badge-soft-primary"><%= pagination.totalCount %> records</span>
|
||
<% } %>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
<% if (auditLogs && auditLogs.length > 0) { %>
|
||
<div class="table-responsive">
|
||
<table class="table table-hover align-middle mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:130px">Date / Time</th>
|
||
<th>Model</th>
|
||
<th>Action</th>
|
||
<th>User</th>
|
||
<th>Changes</th>
|
||
<th>IP Address</th>
|
||
<th style="width:80px">Details</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<% auditLogs.forEach(log => { %>
|
||
<tr>
|
||
<td>
|
||
<div style="font-size:0.8rem;font-weight:500"><%= new Date(log.createdAt).toLocaleDateString('en-GB') %></div>
|
||
<div style="font-size:0.75rem;color:var(--text-muted)"><%= new Date(log.createdAt).toLocaleTimeString() %></div>
|
||
</td>
|
||
<td><span class="badge badge-soft-primary"><%= log.model %></span></td>
|
||
<td>
|
||
<%
|
||
let badgeClass = 'badge-soft-primary';
|
||
if (log.action.includes('CREATE')) badgeClass = 'bg-soft-success';
|
||
else if (log.action.includes('UPDATE')) badgeClass = 'bg-soft-warning';
|
||
else if (log.action.includes('DELETE')) badgeClass = 'bg-soft-danger';
|
||
%>
|
||
<span class="badge <%= badgeClass %>"><%= log.action %></span>
|
||
</td>
|
||
<td>
|
||
<% if (log.performedBy) { %>
|
||
<div style="font-weight:500;font-size:0.8125rem"><%= log.performedBy.username %></div>
|
||
<div style="font-size:0.75rem;color:var(--text-muted)"><%= log.performedBy.email %></div>
|
||
<% } else { %>
|
||
<span style="color:var(--text-muted);font-size:0.8125rem">System</span>
|
||
<% } %>
|
||
</td>
|
||
<td>
|
||
<% if (log.changes && log.changes.length > 0) { %>
|
||
<span class="badge badge-soft-accent"><%= log.changes.length %> field<%= log.changes.length > 1 ? 's' : '' %></span>
|
||
<div class="mt-1">
|
||
<% log.changes.slice(0, 2).forEach(change => { %>
|
||
<div style="font-size:0.75rem;color:var(--text-muted)"><%= change.field %></div>
|
||
<% }); %>
|
||
<% if (log.changes.length > 2) { %>
|
||
<div style="font-size:0.75rem;color:var(--text-muted)">+<%= log.changes.length - 2 %> more</div>
|
||
<% } %>
|
||
</div>
|
||
<% } else { %>
|
||
<span style="color:var(--text-muted)">—</span>
|
||
<% } %>
|
||
</td>
|
||
<td style="font-size:0.8rem;color:var(--text-muted)"><%= log.ipAddress %></td>
|
||
<td>
|
||
<a href="/admin/audit-logs/<%= log._id %>" class="btn btn-sm btn-outline-primary btn-icon" title="View">
|
||
<i class="fas fa-eye"></i>
|
||
</a>
|
||
</td>
|
||
</tr>
|
||
<% }); %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Pagination -->
|
||
<% if (pagination && pagination.total > 1) { %>
|
||
<div class="d-flex align-items-center justify-content-between px-3 py-2" style="border-top:1px solid var(--border-light);">
|
||
<small style="color:var(--text-muted)">
|
||
Showing <%= ((pagination.current - 1) * pagination.limit) + 1 %>–<%= Math.min(pagination.current * pagination.limit, pagination.totalCount) %> of <%= pagination.totalCount %>
|
||
</small>
|
||
<nav>
|
||
<ul class="pagination pagination-sm mb-0" style="gap:2px;">
|
||
<% if (pagination.current > 1) { %>
|
||
<li class="page-item">
|
||
<a class="page-link" href="?page=<%= pagination.current - 1 %><%= Object.keys(query).map(k => k !== 'page' ? '&' + k + '=' + encodeURIComponent(query[k]) : '').join('') %>">
|
||
<i class="fas fa-chevron-left" style="font-size:0.7rem"></i>
|
||
</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: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(k => k !== 'page' ? '&' + k + '=' + encodeURIComponent(query[k]) : '').join('') %>"><%= 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(k => k !== 'page' ? '&' + k + '=' + encodeURIComponent(query[k]) : '').join('') %>">
|
||
<i class="fas fa-chevron-right" style="font-size:0.7rem"></i>
|
||
</a>
|
||
</li>
|
||
<% } %>
|
||
</ul>
|
||
</nav>
|
||
</div>
|
||
<% } %>
|
||
|
||
<% } else { %>
|
||
<div class="empty-state">
|
||
<div class="empty-state-icon"><i class="fas fa-shield-alt"></i></div>
|
||
<h5>No audit logs found</h5>
|
||
<p>No activity matches your current filters.</p>
|
||
<a href="/admin/audit-logs" class="btn btn-outline-primary">Clear Filters</a>
|
||
</div>
|
||
<% } %>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Cleanup Modal -->
|
||
<div id="cleanupModal" style="display:none;position:fixed;inset:0;z-index:1050;">
|
||
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.45);" onclick="closeCleanupModal()"></div>
|
||
<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;border-radius:var(--border-radius-lg);box-shadow:var(--shadow-lg);width:90%;max-width:460px;overflow:hidden;">
|
||
<div style="padding:1rem 1.25rem;background:var(--warning-soft);border-bottom:1px solid var(--border-color);display:flex;align-items:center;justify-content:space-between;">
|
||
<span style="font-weight:600;color:var(--warning-color);display:flex;align-items:center;gap:0.5rem;">
|
||
<i class="fas fa-broom"></i> Cleanup Old Audit Logs
|
||
</span>
|
||
<button onclick="closeCleanupModal()" style="background:none;border:none;cursor:pointer;color:var(--warning-color);font-size:1.1rem;"><i class="fas fa-times"></i></button>
|
||
</div>
|
||
<form method="POST" action="/admin/audit-logs/cleanup">
|
||
<div style="padding:1.25rem;">
|
||
<p style="font-size:var(--font-size-sm);color:var(--text-muted);margin-bottom:1rem;">Delete audit logs older than the specified number of days.</p>
|
||
<div class="mb-3">
|
||
<label class="form-label">Keep logs for (days)</label>
|
||
<input type="number" class="form-control" name="days" value="90" min="1" max="365" required>
|
||
<div class="form-text">Recommended: 90 days for compliance</div>
|
||
</div>
|
||
<div class="alert d-flex gap-2 align-items-start" style="background:var(--warning-soft);border:1px solid rgba(217,119,6,0.2);border-radius:var(--border-radius-sm);padding:0.75rem;">
|
||
<i class="fas fa-exclamation-triangle" style="color:var(--warning-color);margin-top:2px;flex-shrink:0;"></i>
|
||
<span style="font-size:var(--font-size-sm);color:var(--warning-color);">This action cannot be undone. Deleted logs will be permanently removed.</span>
|
||
</div>
|
||
</div>
|
||
<div style="padding:0.875rem 1.25rem;border-top:1px solid var(--border-color);display:flex;gap:0.5rem;justify-content:flex-end;background:#fafbfc;">
|
||
<button type="button" class="btn btn-outline-secondary" onclick="closeCleanupModal()">Cancel</button>
|
||
<button type="submit" class="btn btn-warning"><i class="fas fa-broom"></i> Cleanup</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.pagination .page-link {
|
||
color: var(--primary-color);
|
||
border-color: var(--border-color);
|
||
border-radius: var(--border-radius-sm) !important;
|
||
font-size: 0.8125rem;
|
||
padding: 0.3rem 0.6rem;
|
||
}
|
||
.pagination .page-link:hover { background: var(--primary-soft); border-color: var(--primary-color); }
|
||
.pagination .page-item.active .page-link { color: #fff; }
|
||
.pagination .page-item.disabled .page-link { color: var(--text-muted); }
|
||
</style>
|
||
|
||
<script>
|
||
function openCleanupModal() { document.getElementById('cleanupModal').style.display = 'block'; }
|
||
function closeCleanupModal() { document.getElementById('cleanupModal').style.display = 'none'; }
|
||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeCleanupModal(); });
|
||
</script>
|