192 lines
9.2 KiB
PHP
192 lines
9.2 KiB
PHP
<?= $this->extend('layouts/main') ?>
|
|
|
|
<?= $this->section('content') ?>
|
|
<header class="cb-page-header cb-page-header--center">
|
|
<div>
|
|
<h1 class="cb-page-title">Audit log</h1>
|
|
<p class="cb-page-subtitle mb-0">Workspace and app activity with filters.</p>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="cb-page-body">
|
|
<?php if (session()->getFlashdata('error')): ?>
|
|
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<div class="cb-form-shell">
|
|
<form method="get" action="<?= base_url('audit') ?>" class="row g-3 align-items-end">
|
|
<?php if ($isSuperadmin): ?>
|
|
<div class="col-12 col-sm-6 col-lg-2">
|
|
<label class="form-label" for="audit-scope">Scope</label>
|
|
<select id="audit-scope" name="scope" class="form-select">
|
|
<option value="active" <?= $scope === 'active' ? 'selected' : '' ?>>Active workspace</option>
|
|
<option value="global" <?= $scope === 'global' ? 'selected' : '' ?>>Global (no workspace)</option>
|
|
<option value="all" <?= $scope === 'all' ? 'selected' : '' ?>>All workspaces</option>
|
|
</select>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="col-12 col-sm-6 col-lg-2">
|
|
<label class="form-label" for="audit-user">User</label>
|
|
<select id="audit-user" name="user_id" class="form-select">
|
|
<option value="0">Any user</option>
|
|
<?php foreach ($memberUsers as $u): ?>
|
|
<option value="<?= (int) $u['id'] ?>" <?= (int) ($filters['user_id'] ?? 0) === (int) $u['id'] ? 'selected' : '' ?>>
|
|
<?= esc($u['name'] ?: $u['email']) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-12 col-md-6 col-lg-2">
|
|
<label class="form-label" for="audit-action">Action contains</label>
|
|
<input id="audit-action" type="text" name="action" class="form-control" value="<?= esc($filters['action'] ?? '') ?>" placeholder="e.g. chart.">
|
|
</div>
|
|
<div class="col-6 col-md-3 col-lg-2">
|
|
<label class="form-label" for="audit-from">From</label>
|
|
<input id="audit-from" type="date" name="date_from" class="form-control" value="<?= esc($filters['date_from'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-6 col-md-3 col-lg-2">
|
|
<label class="form-label" for="audit-to">To</label>
|
|
<input id="audit-to" type="date" name="date_to" class="form-control" value="<?= esc($filters['date_to'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-12 col-lg-auto d-flex flex-wrap gap-2 align-self-end">
|
|
<button type="submit" class="btn btn-primary">Apply</button>
|
|
<a class="btn btn-outline-secondary" href="<?= base_url('audit') ?>">Reset</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="cb-table-shell">
|
|
<div class="table-responsive">
|
|
<table class="table cb-data-table align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>When</th>
|
|
<th>User</th>
|
|
<th>Action</th>
|
|
<th>Resource</th>
|
|
<th>IP</th>
|
|
<th class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if ($logs === []): ?>
|
|
<tr>
|
|
<td colspan="6" class="text-center text-muted py-5">No audit entries match your filters.</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
<?php foreach ($logs as $log): ?>
|
|
<tr>
|
|
<td class="text-nowrap small text-muted"><?= esc($log['created_at'] ?? '') ?></td>
|
|
<td>
|
|
<div class="cb-cell-name"><?= esc($log['user_name'] ?? '—') ?></div>
|
|
<?php if (! empty($log['user_email'])): ?>
|
|
<div class="cb-cell-sub"><?= esc($log['user_email']) ?></div>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><span class="cb-audit-action-code"><?= esc($log['action'] ?? '') ?></span></td>
|
|
<td class="small">
|
|
<?php if (! empty($log['resource_type'])): ?>
|
|
<?= esc($log['resource_type']) ?>
|
|
<?php if ($log['resource_id'] !== null && $log['resource_id'] !== ''): ?>
|
|
#<?= (int) $log['resource_id'] ?>
|
|
<?php endif; ?>
|
|
<?php else: ?>
|
|
—
|
|
<?php endif; ?>
|
|
</td>
|
|
<td class="small text-muted"><?= esc($log['ip_address'] ?? '—') ?></td>
|
|
<td class="text-end">
|
|
<div class="cb-table-actions">
|
|
<button type="button" class="cb-action-btn cb-action-btn--ghost" data-audit-detail="<?= (int) $log['id'] ?>">Details</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="cb-pager-wrap">
|
|
<?= $pager->links('default', 'cb_full') ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="auditDetailModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 class="modal-title h6 mb-0">Audit detail</h2>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<dl class="row small mb-3" id="auditDetailMeta"></dl>
|
|
<div class="row g-2">
|
|
<div class="col-md-6">
|
|
<div class="fw-semibold small mb-1">Previous value</div>
|
|
<pre class="bg-light border rounded p-2 small mb-0" style="max-height:240px;overflow:auto;" id="auditOldJson"></pre>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="fw-semibold small mb-1">New value</div>
|
|
<pre class="bg-light border rounded p-2 small mb-0" style="max-height:240px;overflow:auto;" id="auditNewJson"></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?= $this->endSection() ?>
|
|
|
|
<?= $this->section('scripts') ?>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const modalEl = document.getElementById('auditDetailModal');
|
|
const metaEl = document.getElementById('auditDetailMeta');
|
|
const oldEl = document.getElementById('auditOldJson');
|
|
const newEl = document.getElementById('auditNewJson');
|
|
const modal = modalEl ? new bootstrap.Modal(modalEl) : null;
|
|
const detailUrlBase = <?= json_encode(rtrim(base_url('audit/detail/'), '/') . '/') ?>;
|
|
|
|
document.querySelectorAll('[data-audit-detail]').forEach(function (btn) {
|
|
btn.addEventListener('click', async function () {
|
|
const id = btn.getAttribute('data-audit-detail');
|
|
if (!id || !modal) return;
|
|
oldEl.textContent = '…';
|
|
newEl.textContent = '…';
|
|
metaEl.innerHTML = '';
|
|
modal.show();
|
|
try {
|
|
const res = await fetch(detailUrlBase + id, {
|
|
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
|
|
});
|
|
const data = await res.json();
|
|
if (!data.success) {
|
|
oldEl.textContent = data.message || 'Could not load detail.';
|
|
newEl.textContent = '';
|
|
return;
|
|
}
|
|
const l = data.log;
|
|
metaEl.innerHTML =
|
|
'<dt class="col-sm-3">Action</dt><dd class="col-sm-9"><code>' + escapeHtml(l.action) + '</code></dd>' +
|
|
'<dt class="col-sm-3">Resource</dt><dd class="col-sm-9">' + escapeHtml((l.resource_type || '—') + (l.resource_id != null ? ' #' + l.resource_id : '')) + '</dd>' +
|
|
'<dt class="col-sm-3">User</dt><dd class="col-sm-9">' + escapeHtml((l.user_name || '—') + (l.user_email ? ' · ' + l.user_email : '')) + '</dd>' +
|
|
'<dt class="col-sm-3">Time</dt><dd class="col-sm-9">' + escapeHtml(l.created_at) + '</dd>' +
|
|
'<dt class="col-sm-3">IP / UA</dt><dd class="col-sm-9">' + escapeHtml((l.ip_address || '—') + (l.user_agent ? ' · ' + l.user_agent.substring(0, 120) : '')) + '</dd>';
|
|
oldEl.textContent = l.old_value_json || '(empty)';
|
|
newEl.textContent = l.new_value_json || '(empty)';
|
|
} catch (e) {
|
|
oldEl.textContent = 'Request failed.';
|
|
newEl.textContent = '';
|
|
}
|
|
});
|
|
});
|
|
|
|
function escapeHtml(s) {
|
|
if (!s) return '';
|
|
const d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
});
|
|
</script>
|
|
<?= $this->endSection() ?>
|