- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks - Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists - Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services - Add Microsoft OIDC SSO, API token management, and user lifecycle features - Add swagger-ui vendor integration and OpenAPI spec - Add production Docker setup and bin/ scripts - Update composer dependencies, config, templates, and frontend assets throughout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
268 lines
7.9 KiB
PHTML
268 lines
7.9 KiB
PHTML
<?php
|
|
|
|
use MintyPHP\Session;
|
|
|
|
$canPurgeApiAudit = can('settings.update');
|
|
|
|
?>
|
|
<?php
|
|
$breadcrumbs = [
|
|
['label' => t('Home'), 'path' => 'admin'],
|
|
['label' => t('API audit logs')],
|
|
];
|
|
require templatePath('partials/app-breadcrumb.phtml');
|
|
?>
|
|
<div class="app-list-titlebar">
|
|
<h1><?php e(t('API audit logs')); ?></h1>
|
|
<div class="app-list-titlebar-actions">
|
|
<?php if ($canPurgeApiAudit): ?>
|
|
<form id="api-audit-purge-form" method="post" action="admin/api-audit/purge">
|
|
<?php Session::getCsrfInput(); ?>
|
|
</form>
|
|
<button
|
|
type="submit"
|
|
form="api-audit-purge-form"
|
|
class="outline secondary"
|
|
onclick="return confirm('<?php e(t('Purge entries older than 90 days?')); ?>');"
|
|
>
|
|
<i class="bi bi-trash"></i> <?php e(t('Purge API audit logs')); ?>
|
|
</button>
|
|
<?php endif; ?>
|
|
<button
|
|
class="outline secondary"
|
|
type="button"
|
|
data-toolbar-toggle
|
|
data-toolbar-target="#api-audit-toolbar"
|
|
data-toolbar-text-show="<?php e(t('Show filters')); ?>"
|
|
data-toolbar-text-hide="<?php e(t('Hide filters')); ?>"
|
|
>
|
|
<i class="bi bi-funnel-fill"></i> <span data-toolbar-label><?php e(t('Hide filters')); ?></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="app-list-toolbar" id="api-audit-toolbar">
|
|
<label class="app-field">
|
|
<span><?php e(t('Search')); ?></span>
|
|
<input type="search" id="api-audit-search" placeholder="<?php e(t('Search...')); ?>">
|
|
</label>
|
|
<label class="app-field">
|
|
<span><?php e(t('Status')); ?></span>
|
|
<select id="api-audit-status-filter">
|
|
<option value=""><?php e(t('All statuses')); ?></option>
|
|
<option value="2xx">2xx</option>
|
|
<option value="4xx">4xx</option>
|
|
<option value="5xx">5xx</option>
|
|
<option value="401">401</option>
|
|
<option value="403">403</option>
|
|
<option value="404">404</option>
|
|
<option value="422">422</option>
|
|
<option value="500">500</option>
|
|
</select>
|
|
</label>
|
|
<label class="app-field">
|
|
<span><?php e(t('Method')); ?></span>
|
|
<select id="api-audit-method-filter">
|
|
<option value=""><?php e(t('All methods')); ?></option>
|
|
<option value="GET">GET</option>
|
|
<option value="POST">POST</option>
|
|
<option value="PUT">PUT</option>
|
|
<option value="PATCH">PATCH</option>
|
|
<option value="DELETE">DELETE</option>
|
|
</select>
|
|
</label>
|
|
<label class="app-field">
|
|
<span><?php e(t('Created from')); ?></span>
|
|
<input type="date" id="api-audit-created-from">
|
|
</label>
|
|
<label class="app-field">
|
|
<span><?php e(t('Created to')); ?></span>
|
|
<input type="date" id="api-audit-created-to">
|
|
</label>
|
|
<label class="app-field">
|
|
<span><?php e(t('Tenant ID')); ?></span>
|
|
<input type="number" id="api-audit-tenant-id" min="1" step="1">
|
|
</label>
|
|
<label class="app-field">
|
|
<span><?php e(t('User ID')); ?></span>
|
|
<input type="number" id="api-audit-user-id" min="1" step="1">
|
|
</label>
|
|
</div>
|
|
<div class="app-list-table">
|
|
<div id="api-audit-grid"></div>
|
|
</div>
|
|
|
|
<script src="<?php e(asset('vendor/gridjs/gridjs.umd.js')); ?>"></script>
|
|
<script type="module">
|
|
import { createServerGrid } from "<?php e(assetVersion('js/core/app-grid-factory.js')); ?>";
|
|
import { getAppBase } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
|
|
|
|
const appBase = getAppBase();
|
|
|
|
const initApiAuditGrid = () => {
|
|
const gridConfig = createServerGrid({
|
|
gridjs: window.gridjs,
|
|
container: '#api-audit-grid',
|
|
dataUrl: 'admin/api-audit/data',
|
|
appBase,
|
|
columns: [
|
|
{
|
|
name: "<?php e(t('Created')); ?>",
|
|
sort: true
|
|
},
|
|
{
|
|
name: "<?php e(t('Status code')); ?>",
|
|
sort: true,
|
|
formatter: (cell) => {
|
|
if (!cell || typeof cell !== 'object') {
|
|
return gridjs.html('');
|
|
}
|
|
return gridjs.html(`<span class="badge" data-variant="${cell.variant || 'neutral'}">${cell.label || ''}</span>`);
|
|
}
|
|
},
|
|
{
|
|
name: "<?php e(t('Method')); ?>",
|
|
sort: true,
|
|
formatter: (cell) => {
|
|
const method = String(cell || '').toUpperCase();
|
|
return gridjs.html(`<span class="badge" data-variant="neutral">${method}</span>`);
|
|
}
|
|
},
|
|
{
|
|
name: "<?php e(t('Path')); ?>",
|
|
sort: true
|
|
},
|
|
{
|
|
name: "<?php e(t('Duration (ms)')); ?>",
|
|
sort: true,
|
|
formatter: (cell) => (cell > 0 ? `${cell}` : '-')
|
|
},
|
|
{
|
|
name: "<?php e(t('User')); ?>",
|
|
sort: false,
|
|
formatter: (cell) => {
|
|
if (!cell || typeof cell !== 'object') {
|
|
return gridjs.html('-');
|
|
}
|
|
const label = cell.label || '-';
|
|
if (!cell.uuid) {
|
|
return gridjs.html(label);
|
|
}
|
|
const url = new URL(`admin/users/edit/${cell.uuid}`, appBase).toString();
|
|
return gridjs.html(`<a href="${url}">${label}</a>`);
|
|
}
|
|
},
|
|
{
|
|
name: "<?php e(t('Tenant')); ?>",
|
|
sort: false,
|
|
formatter: (cell) => {
|
|
if (!cell || typeof cell !== 'object') {
|
|
return gridjs.html('-');
|
|
}
|
|
const label = cell.label || '-';
|
|
if (!cell.uuid) {
|
|
return gridjs.html(label);
|
|
}
|
|
const url = new URL(`admin/tenants/edit/${cell.uuid}`, appBase).toString();
|
|
return gridjs.html(`<a href="${url}">${label}</a>`);
|
|
}
|
|
},
|
|
{
|
|
name: "<?php e(t('Error code')); ?>",
|
|
sort: false,
|
|
formatter: (cell) => cell ? gridjs.html(`<code>${String(cell)}</code>`) : gridjs.html('-')
|
|
},
|
|
{
|
|
name: "<?php e(t('IP')); ?>",
|
|
sort: false
|
|
},
|
|
{
|
|
name: 'ID',
|
|
hidden: true
|
|
}
|
|
],
|
|
sortColumns: ['created_at', 'status_code', 'method', 'path', 'duration_ms', null, null, null, null, null],
|
|
paginationLimit: 10,
|
|
language: <?php MintyPHP\Buffer::get('grid_lang'); ?>,
|
|
mapData: (data) => data.data.map((row) => [
|
|
row.created_at,
|
|
{
|
|
variant: row.status_badge,
|
|
label: String(row.status_code || '')
|
|
},
|
|
row.method,
|
|
row.path,
|
|
row.duration_ms,
|
|
{
|
|
uuid: row.user_uuid || '',
|
|
label: row.user_label || '-'
|
|
},
|
|
{
|
|
uuid: row.tenant_uuid || '',
|
|
label: row.tenant_label || '-'
|
|
},
|
|
row.error_code || '',
|
|
row.ip || '',
|
|
row.id
|
|
]),
|
|
actions: {
|
|
enabled: false
|
|
},
|
|
search: {
|
|
input: '#api-audit-search',
|
|
param: 'search',
|
|
debounce: 250
|
|
},
|
|
filters: [
|
|
{
|
|
input: '#api-audit-status-filter',
|
|
param: 'status'
|
|
},
|
|
{
|
|
input: '#api-audit-method-filter',
|
|
param: 'method'
|
|
},
|
|
{
|
|
input: '#api-audit-created-from',
|
|
param: 'created_from'
|
|
},
|
|
{
|
|
input: '#api-audit-created-to',
|
|
param: 'created_to'
|
|
},
|
|
{
|
|
input: '#api-audit-tenant-id',
|
|
param: 'tenant_id',
|
|
normalize: (value) => {
|
|
const n = Number(value);
|
|
return Number.isInteger(n) && n > 0 ? String(n) : '';
|
|
}
|
|
},
|
|
{
|
|
input: '#api-audit-user-id',
|
|
param: 'user_id',
|
|
normalize: (value) => {
|
|
const n = Number(value);
|
|
return Number.isInteger(n) && n > 0 ? String(n) : '';
|
|
}
|
|
}
|
|
],
|
|
urlSync: true,
|
|
rowDblClick: {
|
|
getUrl: (rowData) => {
|
|
const id = rowData?.cells?.[9]?.data;
|
|
return id ? new URL(`admin/api-audit/view/${id}`, appBase).toString() : '';
|
|
}
|
|
}
|
|
});
|
|
|
|
if (!gridConfig || !gridConfig.grid) {
|
|
console.warn('API audit grid init failed');
|
|
return null;
|
|
}
|
|
|
|
return gridConfig;
|
|
};
|
|
|
|
initApiAuditGrid();
|
|
</script>
|