Merge pull request 'Add per-column filters, smart pagination (25/page), and date range filter to audit logs' (#10) from feature/audit-logs-filters into master

Reviewed-on: #10
Reviewed-by: rafaga21 <rafaelminaya20@hotmail.com>
This commit was merged in pull request #10.
This commit is contained in:
2026-06-07 12:45:09 -04:00
4 changed files with 278 additions and 33 deletions

View File

@@ -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
========================================================================== */

View File

@@ -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,
]);
}

View File

@@ -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'];

View File

@@ -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">&times;</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">&times;</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">&times;</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 !== '' ? '&amp;action=' . urlencode($action) : '')
. ($username !== '' ? '&amp;username=' . urlencode($username) : '')
. ($ipAddress !== '' ? '&amp;ip_address=' . urlencode($ipAddress) : '')
. ($entityType !== '' ? '&amp;entity_type=' . urlencode($entityType) : '')
. ($details !== '' ? '&amp;details=' . urlencode($details) : '')
. ($dateFrom !== '' ? '&amp;date_from=' . urlencode($dateFrom) : '')
. ($dateTo !== '' ? '&amp;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">&laquo; 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 &raquo;</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>