Add per-column filters, smart pagination (25/page), and date range filter to audit logs #10
@@ -2033,6 +2033,79 @@ textarea.form-input {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.filter-row th {
|
||||
padding: 0.5rem 0.4rem !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.filter-row .form-input,
|
||||
.filter-row .form-select {
|
||||
width: 100%;
|
||||
padding: 0.35rem 0.5rem;
|
||||
font-size: 0.78rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.filter-date-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.filter-date-group .form-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0.35rem 0.3rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.filter-date-sep {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filter-input-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-input-wrap .form-input {
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.filter-clear {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.filter-clear:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.page-ellipsis {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
color: var(--text-muted) !important;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Security Status
|
||||
========================================================================== */
|
||||
|
||||
@@ -168,19 +168,34 @@ class AdminController
|
||||
|
||||
$page = (int) ($_GET['page'] ?? 1);
|
||||
$action = $_GET['action'] ?? '';
|
||||
$userId = $_GET['user_id'] ?? '';
|
||||
$username = $_GET['username'] ?? '';
|
||||
$ipAddress = $_GET['ip_address'] ?? '';
|
||||
$entityType = $_GET['entity_type'] ?? '';
|
||||
$details = $_GET['details'] ?? '';
|
||||
$dateFrom = $_GET['date_from'] ?? '';
|
||||
$dateTo = $_GET['date_to'] ?? '';
|
||||
|
||||
$filters = [];
|
||||
if ($action) $filters['action'] = $action;
|
||||
if ($userId) $filters['user_id'] = (int) $userId;
|
||||
if ($action !== '') $filters['action'] = $action;
|
||||
if ($username !== '') $filters['username'] = $username;
|
||||
if ($ipAddress !== '') $filters['ip_address'] = $ipAddress;
|
||||
if ($entityType !== '') $filters['entity_type'] = $entityType;
|
||||
if ($details !== '') $filters['details'] = $details;
|
||||
if ($dateFrom !== '') $filters['date_from'] = $dateFrom;
|
||||
if ($dateTo !== '') $filters['date_to'] = $dateTo;
|
||||
|
||||
$logs = $this->auditService->getAuditLogs($page, 50, $filters);
|
||||
$logs = $this->auditService->getAuditLogs($page, 25, $filters);
|
||||
|
||||
$this->view->display('admin.audit', [
|
||||
'title' => 'Audit Logs - ServerManager',
|
||||
'logs' => $logs,
|
||||
'action' => $action,
|
||||
'userId' => $userId,
|
||||
'username' => $username,
|
||||
'ipAddress' => $ipAddress,
|
||||
'entityType' => $entityType,
|
||||
'details' => $details,
|
||||
'dateFrom' => $dateFrom,
|
||||
'dateTo' => $dateTo,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -157,11 +157,27 @@ class AuditService
|
||||
$params[] = $filters['user_id'];
|
||||
}
|
||||
|
||||
if (!empty($filters['username'])) {
|
||||
$where[] = '(al.username LIKE ? OR u.username LIKE ?)';
|
||||
$params[] = '%' . $filters['username'] . '%';
|
||||
$params[] = '%' . $filters['username'] . '%';
|
||||
}
|
||||
|
||||
if (!empty($filters['entity_type'])) {
|
||||
$where[] = 'al.entity_type = ?';
|
||||
$params[] = $filters['entity_type'];
|
||||
}
|
||||
|
||||
if (!empty($filters['ip_address'])) {
|
||||
$where[] = 'al.ip_address LIKE ?';
|
||||
$params[] = '%' . $filters['ip_address'] . '%';
|
||||
}
|
||||
|
||||
if (!empty($filters['details'])) {
|
||||
$where[] = 'al.details LIKE ?';
|
||||
$params[] = '%' . $filters['details'] . '%';
|
||||
}
|
||||
|
||||
if (!empty($filters['date_from'])) {
|
||||
$where[] = 'al.created_at >= ?';
|
||||
$params[] = $filters['date_from'];
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
<?php
|
||||
$logs = $logs ?? [];
|
||||
$action = $action ?? '';
|
||||
$userId = $userId ?? '';
|
||||
$username = $username ?? '';
|
||||
$ipAddress = $ipAddress ?? '';
|
||||
$entityType = $entityType ?? '';
|
||||
$details = $details ?? '';
|
||||
$dateFrom = $dateFrom ?? '';
|
||||
$dateTo = $dateTo ?? '';
|
||||
|
||||
function selected(string $value, string $compare): string {
|
||||
return $value === $compare ? 'selected' : '';
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Audit Logs</h1>
|
||||
<p class="page-subtitle">Comprehensive activity tracking and security monitoring</p>
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Audit Logs</h1>
|
||||
<p class="page-subtitle">Comprehensive activity tracking and security monitoring</p>
|
||||
</div>
|
||||
<div class="page-header-right">
|
||||
<button class="btn btn-sm" onclick="clearFilters()"><i class="fas fa-times"></i> Clear Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-filters">
|
||||
<select id="actionFilter" class="form-select" onchange="applyFilters()">
|
||||
<option value="">All Actions</option>
|
||||
<option value="login_success" <?= $action === 'login_success' ? 'selected' : '' ?>>Login Success</option>
|
||||
<option value="login_failed" <?= $action === 'login_failed' ? 'selected' : '' ?>>Login Failed</option>
|
||||
<option value="server_created" <?= $action === 'server_created' ? 'selected' : '' ?>>Server Created</option>
|
||||
<option value="server_deleted" <?= $action === 'server_deleted' ? 'selected' : '' ?>>Server Deleted</option>
|
||||
<option value="credential_access" <?= $action === 'credential_access' ? 'selected' : '' ?>>Credential Access</option>
|
||||
<option value="command_executed" <?= $action === 'command_executed' ? 'selected' : '' ?>>Command Executed</option>
|
||||
<option value="user_created" <?= $action === 'user_created' ? 'selected' : '' ?>>User Created</option>
|
||||
<option value="user_deleted" <?= $action === 'user_deleted' ? 'selected' : '' ?>>User Deleted</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
@@ -37,6 +36,65 @@ $userId = $userId ?? '';
|
||||
<th>Details</th>
|
||||
<th>IP Address</th>
|
||||
</tr>
|
||||
<tr class="filter-row">
|
||||
<th>
|
||||
<div class="filter-date-group">
|
||||
<input type="date" id="f_date_from" class="form-input" placeholder="From" value="<?= htmlspecialchars($dateFrom) ?>">
|
||||
<span class="filter-date-sep">—</span>
|
||||
<input type="date" id="f_date_to" class="form-input" placeholder="To" value="<?= htmlspecialchars($dateTo) ?>">
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="filter-input-wrap">
|
||||
<input type="text" id="f_username" class="form-input filter-input" placeholder="User..." value="<?= htmlspecialchars($username) ?>">
|
||||
<?php if ($username !== ''): ?>
|
||||
<button class="filter-clear" onclick="clearField('f_username')" title="Clear">×</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<select id="f_action" class="form-select">
|
||||
<option value="">All Actions</option>
|
||||
<option value="login_success" <?= selected($action, 'login_success') ?>>Login Success</option>
|
||||
<option value="login_failed" <?= selected($action, 'login_failed') ?>>Login Failed</option>
|
||||
<option value="server_created" <?= selected($action, 'server_created') ?>>Server Created</option>
|
||||
<option value="server_deleted" <?= selected($action, 'server_deleted') ?>>Server Deleted</option>
|
||||
<option value="credential_access" <?= selected($action, 'credential_access') ?>>Credential Access</option>
|
||||
<option value="command_executed" <?= selected($action, 'command_executed') ?>>Command Executed</option>
|
||||
<option value="user_created" <?= selected($action, 'user_created') ?>>User Created</option>
|
||||
<option value="user_deleted" <?= selected($action, 'user_deleted') ?>>User Deleted</option>
|
||||
<option value="logout" <?= selected($action, 'logout') ?>>Logout</option>
|
||||
<option value="user_registered" <?= selected($action, 'user_registered') ?>>User Registered</option>
|
||||
<option value="profile_updated" <?= selected($action, 'profile_updated') ?>>Profile Updated</option>
|
||||
<option value="server_action" <?= selected($action, 'server_action') ?>>Server Action</option>
|
||||
<option value="user_updated" <?= selected($action, 'user_updated') ?>>User Updated</option>
|
||||
</select>
|
||||
</th>
|
||||
<th>
|
||||
<select id="f_entity_type" class="form-select">
|
||||
<option value="">All Entities</option>
|
||||
<option value="user" <?= selected($entityType, 'user') ?>>User</option>
|
||||
<option value="server" <?= selected($entityType, 'server') ?>>Server</option>
|
||||
<option value="server_credentials" <?= selected($entityType, 'server_credentials') ?>>Credentials</option>
|
||||
</select>
|
||||
</th>
|
||||
<th>
|
||||
<div class="filter-input-wrap">
|
||||
<input type="text" id="f_details" class="form-input filter-input" placeholder="Details..." value="<?= htmlspecialchars($details) ?>">
|
||||
<?php if ($details !== ''): ?>
|
||||
<button class="filter-clear" onclick="clearField('f_details')" title="Clear">×</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="filter-input-wrap">
|
||||
<input type="text" id="f_ip_address" class="form-input filter-input" placeholder="IP..." value="<?= htmlspecialchars($ipAddress) ?>">
|
||||
<?php if ($ipAddress !== ''): ?>
|
||||
<button class="filter-clear" onclick="clearField('f_ip_address')" title="Clear">×</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($logs['data'] as $log): ?>
|
||||
@@ -69,22 +127,105 @@ $userId = $userId ?? '';
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if ($logs['total_pages'] > 1): ?>
|
||||
<?php
|
||||
$totalPages = $logs['total_pages'] ?? 0;
|
||||
$currentPage = $logs['page'] ?? 1;
|
||||
if ($totalPages > 1):
|
||||
$window = 3;
|
||||
$qs = function (int $p) use ($action, $username, $ipAddress, $entityType, $details, $dateFrom, $dateTo) {
|
||||
return '?page=' . $p
|
||||
. ($action !== '' ? '&action=' . urlencode($action) : '')
|
||||
. ($username !== '' ? '&username=' . urlencode($username) : '')
|
||||
. ($ipAddress !== '' ? '&ip_address=' . urlencode($ipAddress) : '')
|
||||
. ($entityType !== '' ? '&entity_type=' . urlencode($entityType) : '')
|
||||
. ($details !== '' ? '&details=' . urlencode($details) : '')
|
||||
. ($dateFrom !== '' ? '&date_from=' . urlencode($dateFrom) : '')
|
||||
. ($dateTo !== '' ? '&date_to=' . urlencode($dateTo) : '');
|
||||
};
|
||||
?>
|
||||
<div class="pagination">
|
||||
<?php for ($i = 1; $i <= $logs['total_pages']; $i++): ?>
|
||||
<a href="?page=<?= $i ?>&action=<?= urlencode($action) ?>&user_id=<?= urlencode($userId) ?>"
|
||||
class="page-link <?= $i === ($logs['page'] ?? 1) ? 'active' : '' ?>">
|
||||
<?= $i ?>
|
||||
</a>
|
||||
<?php endfor; ?>
|
||||
<?php if ($currentPage > 1): ?>
|
||||
<a href="<?= $qs($currentPage - 1) ?>" class="page-link">« Previous</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$showPages = [];
|
||||
$showPages[] = 1;
|
||||
|
||||
if ($currentPage > $window + 2) {
|
||||
$showPages[] = '...';
|
||||
}
|
||||
|
||||
$start = max(2, $currentPage - $window);
|
||||
$end = min($totalPages - 1, $currentPage + $window);
|
||||
for ($i = $start; $i <= $end; $i++) {
|
||||
$showPages[] = $i;
|
||||
}
|
||||
|
||||
if ($currentPage < $totalPages - $window - 1) {
|
||||
$showPages[] = '...';
|
||||
}
|
||||
|
||||
if ($totalPages > 1) {
|
||||
$showPages[] = $totalPages;
|
||||
}
|
||||
|
||||
foreach ($showPages as $p):
|
||||
?>
|
||||
<?php if ($p === '...'): ?>
|
||||
<span class="page-link page-ellipsis">…</span>
|
||||
<?php else: ?>
|
||||
<a href="<?= $qs($p) ?>" class="page-link <?= $p === $currentPage ? 'active' : '' ?>"><?= $p ?></a>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if ($currentPage < $totalPages): ?>
|
||||
<a href="<?= $qs($currentPage + 1) ?>" class="page-link">Next »</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function applyFilters() {
|
||||
const action = document.getElementById('actionFilter').value;
|
||||
window.location.href = '/admin/audit?action=' + encodeURIComponent(action);
|
||||
function buildQuery() {
|
||||
const params = [];
|
||||
const fields = [
|
||||
{ key: 'action', id: 'f_action' },
|
||||
{ key: 'username', id: 'f_username' },
|
||||
{ key: 'ip_address', id: 'f_ip_address' },
|
||||
{ key: 'entity_type', id: 'f_entity_type' },
|
||||
{ key: 'details', id: 'f_details' },
|
||||
{ key: 'date_from', id: 'f_date_from' },
|
||||
{ key: 'date_to', id: 'f_date_to' },
|
||||
];
|
||||
fields.forEach(function (f) {
|
||||
const el = document.getElementById(f.id);
|
||||
if (el && el.value) {
|
||||
params.push(encodeURIComponent(f.key) + '=' + encodeURIComponent(el.value));
|
||||
}
|
||||
});
|
||||
return params.length ? '/admin/audit?' + params.join('&') : '/admin/audit';
|
||||
}
|
||||
|
||||
document.addEventListener('change', function (e) {
|
||||
if (e.target.matches('#f_date_from, #f_date_to, #f_action, #f_entity_type')) {
|
||||
window.location.href = buildQuery();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter' && e.target.matches('#f_username, #f_details, #f_ip_address')) {
|
||||
window.location.href = buildQuery();
|
||||
}
|
||||
});
|
||||
|
||||
function clearFilters() {
|
||||
window.location.href = '/admin/audit';
|
||||
}
|
||||
|
||||
function clearField(id) {
|
||||
document.getElementById(id).value = '';
|
||||
window.location.href = buildQuery();
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user