Embed user lifecycle audit into settings page

This commit is contained in:
2026-04-26 18:03:42 +02:00
parent 01f5265eff
commit 144d8410a4
22 changed files with 345 additions and 434 deletions

View File

@@ -31,12 +31,11 @@ return [
['path' => 'admin/import-audit/view', 'target' => 'audit/import-audit/view'],
['path' => 'admin/import-audit/data', 'target' => 'audit/import-audit/data'],
['path' => 'admin/import-audit/purge', 'target' => 'audit/import-audit/purge'],
// User lifecycle audit pages
['path' => 'admin/user-lifecycle-audit', 'target' => 'audit/user-lifecycle-audit/index'],
['path' => 'admin/user-lifecycle-audit/view', 'target' => 'audit/user-lifecycle-audit/view'],
['path' => 'admin/user-lifecycle-audit/data', 'target' => 'audit/user-lifecycle-audit/data'],
['path' => 'admin/user-lifecycle-audit/purge', 'target' => 'audit/user-lifecycle-audit/purge'],
['path' => 'admin/user-lifecycle-audit/restore', 'target' => 'audit/user-lifecycle-audit/restore'],
// User lifecycle audit endpoints embedded in admin/settings/user-lifecycle
['path' => 'admin/settings/user-lifecycle/audit-data', 'target' => 'audit/settings-user-lifecycle/audit-data'],
['path' => 'admin/settings/user-lifecycle/audit-fragment', 'target' => 'audit/settings-user-lifecycle/audit-fragment'],
['path' => 'admin/settings/user-lifecycle/audit-restore', 'target' => 'audit/settings-user-lifecycle/audit-restore'],
['path' => 'admin/settings/user-lifecycle/audit-purge', 'target' => 'audit/settings-user-lifecycle/audit-purge'],
// Frontend telemetry
['path' => 'admin/frontend-telemetry/ingest', 'target' => 'audit/frontend-telemetry/ingest'],
],
@@ -72,13 +71,13 @@ return [
'permission' => 'audit.imports.view',
'order' => 220,
],
],
'settings.user_lifecycle.panel' => [
[
'key' => 'audit-user-lifecycle-audit',
'group' => 'admin-logs',
'label' => 'nav.audit.user_lifecycle',
'path' => 'audit/user-lifecycle-audit',
'key' => 'audit-settings-user-lifecycle-panel',
'template' => 'templates/settings-user-lifecycle-panel.phtml',
'permission' => 'audit.user_lifecycle.view',
'order' => 230,
'order' => 100,
],
],
],

View File

@@ -4,31 +4,29 @@ use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Module\Audit\AuditAuthorizationPolicy;
use MintyPHP\Module\Audit\Domain\UserLifecycleAction;
use MintyPHP\Module\Audit\Service\UserLifecycleAuditService;
use MintyPHP\Module\Audit\Support\AuditPageHelper;
use MintyPHP\Service\Access\UiAccessService;
use MintyPHP\Support\Guard;
$session = app(SessionStoreInterface::class)->all();
Guard::requireLogin();
Guard::requireAbilityOrForbidden(AuditAuthorizationPolicy::ABILITY_USER_LIFECYCLE_VIEW);
$auditLog = AuditPageHelper::findOrRedirect(
AuditAuthorizationPolicy::ABILITY_USER_LIFECYCLE_VIEW,
UserLifecycleAuditService::class,
'audit/user-lifecycle-audit',
'Lifecycle audit entry not found',
'user_lifecycle_audit_not_found',
'View user lifecycle audit entry',
$id ?? null
);
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('User lifecycle logs'), 'path' => 'audit/user-lifecycle-audit'],
['label' => t('View')],
];
$auditId = (int) ($id ?? 0);
$auditLog = null;
if ($auditId > 0) {
$auditLog = app(UserLifecycleAuditService::class)->find($auditId);
}
if (!is_array($auditLog)) {
http_response_code(404);
return;
}
$auditService = app(UserLifecycleAuditService::class);
$snapshot = null;
if ((string) ($auditLog['action'] ?? '') === UserLifecycleAction::Delete->value) {
$snapshot = $auditService->decryptSnapshot($auditLog);
}
$viewAuth = is_array($viewAuth ?? null) ? $viewAuth : [];
$viewAuth['page'] = app(UiAccessService::class)->pageCapabilities(
(int) ($session['user']['id'] ?? 0),
['can_restore' => AuditAuthorizationPolicy::ABILITY_USER_LIFECYCLE_RESTORE]

View File

@@ -0,0 +1,177 @@
<?php
use MintyPHP\Module\Audit\Domain\UserLifecycleAction;
use MintyPHP\Module\Audit\Domain\UserLifecycleStatus;
use MintyPHP\Module\Audit\Domain\UserLifecycleTriggerType;
use MintyPHP\Session;
/**
* @var array<string, mixed> $auditLog
* @var ?array<string, mixed> $snapshot
*/
$auditLog = is_array($auditLog ?? null) ? $auditLog : [];
$snapshot = is_array($snapshot ?? null) ? $snapshot : null;
$status = UserLifecycleStatus::normalizeOr((string) ($auditLog['status'] ?? ''), UserLifecycleStatus::Failed);
$statusVariant = $status->badgeVariant();
$action = UserLifecycleAction::normalizeOr((string) ($auditLog['action'] ?? ''), UserLifecycleAction::Deactivate);
$triggerType = UserLifecycleTriggerType::normalizeOr((string) ($auditLog['trigger_type'] ?? ''), UserLifecycleTriggerType::System);
$pageAuth = is_array($viewAuth['page'] ?? null) ? $viewAuth['page'] : [];
$canRestore = (bool) ($pageAuth['can_restore'] ?? false);
$isRestorable = $canRestore
&& $action === UserLifecycleAction::Delete
&& $status === UserLifecycleStatus::Success
&& empty($auditLog['restored_at'])
&& !empty($auditLog['snapshot_enc']);
?>
<article class="app-details-content">
<header>
<h3><?php e(t('View user lifecycle audit entry')); ?></h3>
<p><small><?php e(t('ID')); ?>: <?php e((string) ($auditLog['id'] ?? '-')); ?></small></p>
</header>
<hr>
<details open>
<summary><?php e(t('Lifecycle event')); ?></summary>
<hr>
<div class="grid">
<div>
<small><?php e(t('Created')); ?></small>
<p><?php e(dt((string) ($auditLog['created_at'] ?? '')) ?: '-'); ?></p>
</div>
<div>
<small><?php e(t('Status')); ?></small>
<p><span class="badge" data-variant="<?php e($statusVariant); ?>"><?php e(t($status->labelToken())); ?></span></p>
</div>
</div>
<div class="grid">
<div>
<small><?php e(t('Action')); ?></small>
<p><?php e(t($action->labelToken())); ?></p>
</div>
<div>
<small><?php e(t('Trigger')); ?></small>
<p><?php e(t($triggerType->labelToken())); ?></p>
</div>
</div>
<div class="grid">
<div>
<small><?php e(t('Reason code')); ?></small>
<p><?php e((string) ($auditLog['reason_code'] ?? '') !== '' ? (string) $auditLog['reason_code'] : '-'); ?></p>
</div>
<div>
<small><?php e(t('Run UUID')); ?></small>
<p><?php e((string) ($auditLog['run_uuid'] ?? '') !== '' ? (string) $auditLog['run_uuid'] : '-'); ?></p>
</div>
</div>
</details>
<hr>
<details open>
<summary><?php e(t('Target')); ?></summary>
<hr>
<div class="grid">
<div>
<small><?php e(t('Target UUID')); ?></small>
<p><?php e((string) ($auditLog['target_user_uuid'] ?? '') !== '' ? (string) $auditLog['target_user_uuid'] : '-'); ?></p>
</div>
<div>
<small><?php e(t('Target email')); ?></small>
<p><?php e((string) ($auditLog['target_user_email'] ?? '') !== '' ? (string) $auditLog['target_user_email'] : '-'); ?></p>
</div>
</div>
</details>
<hr>
<details>
<summary><?php e(t('Policy')); ?></summary>
<hr>
<div class="grid">
<div>
<small><?php e(t('Deactivate users after inactivity (days)')); ?></small>
<p><?php e((string) ((int) ($auditLog['policy_deactivate_days'] ?? 0))); ?></p>
</div>
<div>
<small><?php e(t('Delete inactive users after (days)')); ?></small>
<p><?php e((string) ((int) ($auditLog['policy_delete_days'] ?? 0))); ?></p>
</div>
</div>
</details>
<?php if ($snapshot !== null): ?>
<hr>
<details>
<summary><?php e(t('Snapshot summary')); ?></summary>
<hr>
<div class="grid">
<div>
<small><?php e(t('First name')); ?></small>
<p><?php e((string) ($snapshot['first_name'] ?? '') !== '' ? (string) $snapshot['first_name'] : '-'); ?></p>
</div>
<div>
<small><?php e(t('Last name')); ?></small>
<p><?php e((string) ($snapshot['last_name'] ?? '') !== '' ? (string) $snapshot['last_name'] : '-'); ?></p>
</div>
</div>
<div class="grid">
<div>
<small><?php e(t('Email')); ?></small>
<p><?php e((string) ($snapshot['email'] ?? '') !== '' ? (string) $snapshot['email'] : '-'); ?></p>
</div>
<div>
<small><?php e(t('Locale')); ?></small>
<p><?php e((string) ($snapshot['locale'] ?? '') !== '' ? (string) $snapshot['locale'] : '-'); ?></p>
</div>
</div>
<div class="grid">
<div>
<small><?php e(t('Theme')); ?></small>
<p><?php e((string) ($snapshot['theme'] ?? '') !== '' ? (string) $snapshot['theme'] : '-'); ?></p>
</div>
<div>
<small><?php e(t('Snapshot version')); ?></small>
<p><?php e((string) ((int) ($auditLog['snapshot_version'] ?? 1))); ?></p>
</div>
</div>
</details>
<?php endif; ?>
<hr>
<details open>
<summary><?php e(t('Restore status')); ?></summary>
<hr>
<div class="grid">
<div>
<small><?php e(t('Restored at')); ?></small>
<p><?php e(dt((string) ($auditLog['restored_at'] ?? '')) ?: '-'); ?></p>
</div>
<div>
<small><?php e(t('Restored by')); ?></small>
<p><?php e((string) ($auditLog['restored_by_user_display_name'] ?? '') !== '' ? (string) $auditLog['restored_by_user_display_name'] : '-'); ?></p>
</div>
</div>
<div>
<small><?php e(t('Restored user')); ?></small>
<p>
<?php if (!empty($auditLog['restored_user_uuid'])): ?>
<a href="admin/users/edit/<?php e((string) $auditLog['restored_user_uuid']); ?>">
<?php e((string) ($auditLog['restored_user_display_name'] ?? $auditLog['restored_user_uuid'])); ?>
</a>
<?php else: ?>
-
<?php endif; ?>
</p>
</div>
<?php if ($isRestorable): ?>
<hr>
<form method="post" action="<?php e(endpointUrl('admin/settings/user-lifecycle/audit-restore')); ?>/<?php e((string) ($auditLog['id'] ?? 0)); ?>">
<?php Session::getCsrfInput(); ?>
<button type="submit" class="warning" data-confirm-message="<?php e(t('Restore this user from lifecycle snapshot?')); ?>">
<?php e(t('Restore user')); ?>
</button>
</form>
<?php endif; ?>
</details>
</article>

View File

@@ -8,14 +8,14 @@ use MintyPHP\Support\Flash;
AuditPageHelper::requirePurgePost(
SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE,
'audit/user-lifecycle-audit',
'admin/settings/user-lifecycle',
'user_lifecycle_audit'
);
$deleted = app(UserLifecycleAuditService::class)->purgeExpired();
Flash::success(
sprintf(t('%d user lifecycle audit entries purged'), $deleted),
'audit/user-lifecycle-audit',
'admin/settings/user-lifecycle',
'user_lifecycle_audit_purged'
);
Router::redirect('audit/user-lifecycle-audit');
Router::redirect('admin/settings/user-lifecycle');

View File

@@ -14,12 +14,12 @@ $auditId = (int) ($id ?? 0);
$errorBag = formErrors();
if (strtoupper((string) (requestInput()->method())) !== 'POST') {
Router::redirect('audit/user-lifecycle-audit');
Router::redirect('admin/settings/user-lifecycle');
}
if (!Session::checkCsrfToken()) {
$errorBag->addGlobal(t('Form expired, please try again'));
flashFormErrors($errorBag, 'audit/user-lifecycle-audit', 'user_lifecycle_restore');
Router::redirect('audit/user-lifecycle-audit');
flashFormErrors($errorBag, 'admin/settings/user-lifecycle', 'user_lifecycle_restore');
Router::redirect('admin/settings/user-lifecycle');
}
$result = app(\MintyPHP\Service\User\UserLifecycleRestoreService::class)->restoreFromLifecycleAudit(
@@ -40,9 +40,9 @@ if (!($result['ok'] ?? false)) {
} else {
$errorBag->addGlobal(t('User restore failed'));
}
flashFormErrors($errorBag, "audit/user-lifecycle-audit/view/$auditId", 'user_lifecycle_restore');
Router::redirect("audit/user-lifecycle-audit/view/$auditId");
flashFormErrors($errorBag, 'admin/settings/user-lifecycle', 'user_lifecycle_restore');
Router::redirect('admin/settings/user-lifecycle');
}
Flash::success(t('User restored'), "audit/user-lifecycle-audit/view/$auditId", 'user_restored');
Router::redirect("audit/user-lifecycle-audit/view/$auditId");
Flash::success(t('User restored'), 'admin/settings/user-lifecycle', 'user_restored');
Router::redirect('admin/settings/user-lifecycle');

View File

@@ -1,66 +0,0 @@
<?php
$pageAuth = is_array($viewAuth['page'] ?? null) ? $viewAuth['page'] : [];
$canPurge = (bool) ($pageAuth['can_purge'] ?? false);
$toolbarFilterSchema = is_array($toolbarFilterSchema ?? null) ? $toolbarFilterSchema : [];
$searchToolbarFilterSchema = is_array($searchToolbarFilterSchema ?? null) ? $searchToolbarFilterSchema : [];
$drawerToolbarFilterSchema = is_array($drawerToolbarFilterSchema ?? null) ? $drawerToolbarFilterSchema : [];
$toolbarFilterState = is_array($toolbarFilterState ?? null) ? $toolbarFilterState : [];
$toolbarOptionSets = is_array($toolbarOptionSets ?? null) ? $toolbarOptionSets : [];
$filterChipMeta = is_array($filterChipMeta ?? null) ? $filterChipMeta : [];
$clientFilterSchema = is_array($clientFilterSchema ?? null) ? $clientFilterSchema : [];
$searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
?>
<?php
$listTitle = t('User lifecycle logs');
ob_start();
?>
<?php
$listPurgeEnabled = $canPurge;
$listPurgeFormId = 'user-lifecycle-audit-purge-form';
$listPurgeAction = 'audit/user-lifecycle-audit/purge';
$listPurgeConfirmMessage = t('Purge entries older than 365 days?');
$listPurgeButtonLabel = t('Purge user lifecycle logs');
require templatePath('partials/app-list-purge-action.phtml');
?>
<?php
$listTitleActionsHtml = ob_get_clean();
require templatePath('partials/app-list-titlebar.phtml');
?>
<?php
$filterUiNamespace = 'user-lifecycle';
$filterToolbarId = 'user-lifecycle-audit-toolbar';
$filterDrawerToolbarId = 'user-lifecycle-drawer-toolbar';
$filterDrawerTitleId = 'user-lifecycle-filter-drawer-title';
require templatePath('partials/app-list-filters.phtml');
?>
<div class="app-list-table">
<div id="user-lifecycle-audit-grid"></div>
</div>
<?php
$gridLang = json_decode(appBufferValue('grid_lang'), true);
if (!is_array($gridLang)) {
$gridLang = [];
}
$pageConfig = [
'gridSearch' => $searchConfig,
'filterSchema' => $clientFilterSchema,
'filterChipMeta' => $filterChipMeta,
'gridLang' => $gridLang,
'labels' => [
'created' => t('Created'),
'status' => t('Status'),
'action' => t('Action'),
'trigger' => t('Trigger'),
'targetEmail' => t('Target email'),
'targetUuid' => t('Target UUID'),
'reasonCode' => t('Reason code'),
'restoredAt' => t('Restored at'),
],
];
?>
<script src="<?php e(assetVersion('vendor/gridjs/gridjs.umd.js')); ?>"></script>
<script type="application/json" id="page-config-admin-user-lifecycle-audit-index"><?php gridJsonForJs($pageConfig); ?></script>
<script type="module" src="<?php e(assetVersion('modules/audit/js/pages/admin-user-lifecycle-audit-index.js')); ?>"></script>

View File

@@ -1,206 +0,0 @@
<?php
use MintyPHP\Module\Audit\Domain\UserLifecycleAction;
use MintyPHP\Module\Audit\Domain\UserLifecycleStatus;
use MintyPHP\Module\Audit\Domain\UserLifecycleTriggerType;
/**
* @var array $auditLog
* @var ?array $snapshot
*/
use MintyPHP\Session;
$auditLog = $auditLog ?? [];
$snapshot = is_array($snapshot ?? null) ? $snapshot : null;
$status = UserLifecycleStatus::normalizeOr((string) ($auditLog['status'] ?? ''), UserLifecycleStatus::Failed);
$statusVariant = $status->badgeVariant();
$action = UserLifecycleAction::normalizeOr((string) ($auditLog['action'] ?? ''), UserLifecycleAction::Deactivate);
$triggerType = UserLifecycleTriggerType::normalizeOr((string) ($auditLog['trigger_type'] ?? ''), UserLifecycleTriggerType::System);
$pageAuth = is_array($viewAuth['page'] ?? null) ? $viewAuth['page'] : [];
$canRestore = (bool) ($pageAuth['can_restore'] ?? false);
$isRestorable = $canRestore
&& $action === UserLifecycleAction::Delete
&& $status === UserLifecycleStatus::Success
&& empty($auditLog['restored_at'])
&& !empty($auditLog['snapshot_enc']);
?>
<div class="app-details-container">
<section>
<?php
$titlebar = [
'title' => t('View user lifecycle audit entry'),
'backHref' => 'audit/user-lifecycle-audit',
'backTitle' => t('Back'),
];
require templatePath('partials/app-details-titlebar.phtml');
?>
<div class="app-details-content">
<details open>
<summary><?php e(t('Lifecycle event')); ?></summary>
<hr>
<div class="grid">
<div>
<small><?php e(t('Created')); ?></small>
<p><?php e(dt((string) ($auditLog['created_at'] ?? '')) ?: '-'); ?></p>
</div>
<div>
<small><?php e(t('Status')); ?></small>
<p><span class="badge" data-variant="<?php e($statusVariant); ?>"><?php e(t($status->labelToken())); ?></span></p>
</div>
</div>
<div class="grid">
<div>
<small><?php e(t('Action')); ?></small>
<p><?php e(t($action->labelToken())); ?></p>
</div>
<div>
<small><?php e(t('Trigger')); ?></small>
<p><?php e(t($triggerType->labelToken())); ?></p>
</div>
</div>
<div class="grid">
<div>
<small><?php e(t('Reason code')); ?></small>
<p><?php e((string) ($auditLog['reason_code'] ?? '') !== '' ? (string) $auditLog['reason_code'] : '-'); ?></p>
</div>
<div>
<small><?php e(t('Run UUID')); ?></small>
<p><?php e((string) ($auditLog['run_uuid'] ?? '') !== '' ? (string) $auditLog['run_uuid'] : '-'); ?></p>
</div>
</div>
</details>
<hr>
<details open>
<summary><?php e(t('Target')); ?></summary>
<hr>
<div class="grid">
<div>
<small><?php e(t('Target UUID')); ?></small>
<p><?php e((string) ($auditLog['target_user_uuid'] ?? '') !== '' ? (string) $auditLog['target_user_uuid'] : '-'); ?></p>
</div>
<div>
<small><?php e(t('Target email')); ?></small>
<p><?php e((string) ($auditLog['target_user_email'] ?? '') !== '' ? (string) $auditLog['target_user_email'] : '-'); ?></p>
</div>
</div>
</details>
<hr>
<details>
<summary><?php e(t('Policy')); ?></summary>
<hr>
<div class="grid">
<div>
<small><?php e(t('Deactivate users after inactivity (days)')); ?></small>
<p><?php e((string) ((int) ($auditLog['policy_deactivate_days'] ?? 0))); ?></p>
</div>
<div>
<small><?php e(t('Delete inactive users after (days)')); ?></small>
<p><?php e((string) ((int) ($auditLog['policy_delete_days'] ?? 0))); ?></p>
</div>
</div>
</details>
<?php if ($snapshot !== null): ?>
<hr>
<details>
<summary><?php e(t('Snapshot summary')); ?></summary>
<hr>
<div class="grid">
<div>
<small><?php e(t('First name')); ?></small>
<p><?php e((string) ($snapshot['first_name'] ?? '') !== '' ? (string) $snapshot['first_name'] : '-'); ?></p>
</div>
<div>
<small><?php e(t('Last name')); ?></small>
<p><?php e((string) ($snapshot['last_name'] ?? '') !== '' ? (string) $snapshot['last_name'] : '-'); ?></p>
</div>
</div>
<div class="grid">
<div>
<small><?php e(t('Email')); ?></small>
<p><?php e((string) ($snapshot['email'] ?? '') !== '' ? (string) $snapshot['email'] : '-'); ?></p>
</div>
<div>
<small><?php e(t('Locale')); ?></small>
<p><?php e((string) ($snapshot['locale'] ?? '') !== '' ? (string) $snapshot['locale'] : '-'); ?></p>
</div>
</div>
<div class="grid">
<div>
<small><?php e(t('Theme')); ?></small>
<p><?php e((string) ($snapshot['theme'] ?? '') !== '' ? (string) $snapshot['theme'] : '-'); ?></p>
</div>
<div>
<small><?php e(t('Snapshot version')); ?></small>
<p><?php e((string) ((int) ($auditLog['snapshot_version'] ?? 1))); ?></p>
</div>
</div>
</details>
<?php endif; ?>
<hr>
<details open>
<summary><?php e(t('Restore status')); ?></summary>
<hr>
<div class="grid">
<div>
<small><?php e(t('Restored at')); ?></small>
<p><?php e(dt((string) ($auditLog['restored_at'] ?? '')) ?: '-'); ?></p>
</div>
<div>
<small><?php e(t('Restored by')); ?></small>
<p><?php e((string) ($auditLog['restored_by_user_display_name'] ?? '') !== '' ? (string) $auditLog['restored_by_user_display_name'] : '-'); ?></p>
</div>
</div>
<div>
<small><?php e(t('Restored user')); ?></small>
<p>
<?php if (!empty($auditLog['restored_user_uuid'])): ?>
<a href="admin/users/edit/<?php e((string) $auditLog['restored_user_uuid']); ?>">
<?php e((string) ($auditLog['restored_user_display_name'] ?? $auditLog['restored_user_uuid'])); ?>
</a>
<?php else: ?>
-
<?php endif; ?>
</p>
</div>
<?php if ($isRestorable): ?>
<hr>
<form method="post" action="audit/user-lifecycle-audit/restore/<?php e((string) ($auditLog['id'] ?? 0)); ?>">
<?php Session::getCsrfInput(); ?>
<button type="submit" class="warning" data-confirm-message="<?php e(t('Restore this user from lifecycle snapshot?')); ?>">
<?php e(t('Restore user')); ?>
</button>
</form>
<?php endif; ?>
</details>
</div>
</section>
<aside id="app-details-aside-section">
<div class="app-details-aside-section">
<hgroup>
<strong><?php e(t('User lifecycle logs')); ?></strong>
<p><small><?php e((string) ($auditLog['run_uuid'] ?? '') !== '' ? (string) $auditLog['run_uuid'] : '-'); ?></small></p>
</hgroup>
<hr>
<div>
<small><?php e(t('ID')); ?></small>
<p><span class="badge" data-variant="neutral"><?php e((string) ($auditLog['id'] ?? '-')); ?></span></p>
</div>
<div>
<small><?php e(t('Status')); ?></small>
<p><span class="badge" data-variant="<?php e($statusVariant); ?>"><?php e(t($status->labelToken())); ?></span></p>
</div>
<div>
<small><?php e(t('Action')); ?></small>
<p><?php e(t($action->labelToken())); ?></p>
</div>
</div>
</aside>
</div>

View File

@@ -1,32 +1,12 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Module\Audit\AuditAuthorizationPolicy;
use MintyPHP\Module\Audit\Domain\UserLifecycleAction;
use MintyPHP\Module\Audit\Domain\UserLifecycleStatus;
use MintyPHP\Module\Audit\Domain\UserLifecycleTriggerType;
use MintyPHP\Module\Audit\Service\UserLifecycleAuditService;
use MintyPHP\Service\Access\SettingsAuthorizationPolicy;
use MintyPHP\Service\Access\UiAccessService;
use MintyPHP\Support\Guard;
$session = app(SessionStoreInterface::class)->all();
Guard::requireLogin();
Guard::requireAbility(AuditAuthorizationPolicy::ABILITY_USER_LIFECYCLE_VIEW);
$viewAuth['page'] = app(UiAccessService::class)->pageCapabilities(
(int) ($session['user']['id'] ?? 0),
['can_purge' => SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE]
);
$canPurge = (bool) ($canUpdateSettings ?? false);
Buffer::set('title', t('User lifecycle logs'));
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('User lifecycle logs')],
];
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$filterSchema = require __DIR__ . '/filter-schema.php';
$filterSchema = require __DIR__ . '/../pages/audit/settings-user-lifecycle/filter-schema.php';
$filterState = gridParseFilters(requestInput()->queryAll(), [
...gridSchemaQuery($filterSchema),
'actions' => [
@@ -49,8 +29,6 @@ $filterState = gridParseFilters(requestInput()->queryAll(), [
],
'actor_user_ids' => ['type' => 'csv_ids', 'max' => 200, 'return' => 'array'],
]);
$filterOptions = app(UserLifecycleAuditService::class)->filterOptions(200);
$activeActions = is_array($filterState['actions'] ?? null) ? $filterState['actions'] : [];
$activeStatuses = is_array($filterState['statuses'] ?? null) ? $filterState['statuses'] : [];
$activeTriggerTypes = is_array($filterState['trigger_types'] ?? null) ? $filterState['trigger_types'] : [];
@@ -76,16 +54,6 @@ foreach (UserLifecycleAction::values() as $action) {
'description' => $actionLabelMap[$action] ?? $action,
];
}
foreach ((array) ($filterOptions['actions'] ?? []) as $action) {
$action = strtolower(trim((string) $action));
if ($action === '') {
continue;
}
$lifecycleActionItems[$action] = [
'id' => $action,
'description' => $actionLabelMap[$action] ?? $action,
];
}
$lifecycleActionItems = array_values($lifecycleActionItems);
$lifecycleStatusItems = [];
@@ -95,16 +63,6 @@ foreach (UserLifecycleStatus::values() as $status) {
'description' => $statusLabelMap[$status] ?? $status,
];
}
foreach ((array) ($filterOptions['statuses'] ?? []) as $status) {
$status = strtolower(trim((string) $status));
if ($status === '') {
continue;
}
$lifecycleStatusItems[$status] = [
'id' => $status,
'description' => $statusLabelMap[$status] ?? $status,
];
}
$lifecycleStatusItems = array_values($lifecycleStatusItems);
$lifecycleTriggerItems = [];
@@ -114,48 +72,14 @@ foreach (UserLifecycleTriggerType::values() as $triggerType) {
'description' => $triggerLabelMap[$triggerType] ?? $triggerType,
];
}
foreach ((array) ($filterOptions['trigger_types'] ?? []) as $triggerType) {
$triggerType = strtolower(trim((string) $triggerType));
if ($triggerType === '') {
continue;
}
$lifecycleTriggerItems[$triggerType] = [
'id' => $triggerType,
'description' => $triggerLabelMap[$triggerType] ?? $triggerType,
];
}
$lifecycleTriggerItems = array_values($lifecycleTriggerItems);
$lifecycleActorItems = [];
foreach ((array) ($filterOptions['actors'] ?? []) as $actorOption) {
if (!is_array($actorOption)) {
continue;
}
$actorId = (int) ($actorOption['id'] ?? 0);
if ($actorId <= 0) {
continue;
}
$actorDisplayName = trim((string) ($actorOption['display_name'] ?? ''));
$actorEmail = trim((string) ($actorOption['email'] ?? ''));
$actorExists = (bool) ($actorOption['exists'] ?? false);
$label = $actorDisplayName !== '' ? $actorDisplayName : $actorEmail;
if ($label === '') {
$label = $actorExists
? sprintf(t('User #%d'), $actorId)
: sprintf(t('User #%d (deleted)'), $actorId);
}
$id = (string) $actorId;
$lifecycleActorItems[$id] = [
'id' => $id,
'description' => $label,
];
}
foreach ($activeActorUserIds as $actorId) {
if (!isset($lifecycleActorItems[$actorId])) {
$lifecycleActorItems[$actorId] = [
'id' => $actorId,
'description' => sprintf(t('User #%d (deleted)'), (int) $actorId),
'description' => sprintf(t('User #%d'), (int) $actorId),
];
}
}
@@ -182,11 +106,14 @@ $listFilterContext = gridBuildListFilterContext($filterSchema, [
'toolbar_state_overrides' => $toolbarFilterState,
'toolbar_option_sets' => $toolbarOptionSets,
]);
$toolbarFilterSchema = $listFilterContext['toolbarFilterSchema'];
$toolbarFilterState = $listFilterContext['toolbarFilterState'];
$searchToolbarFilterSchema = $listFilterContext['searchToolbarFilterSchema'];
$drawerToolbarFilterSchema = $listFilterContext['drawerToolbarFilterSchema'];
$toolbarOptionSets = $listFilterContext['toolbarOptionSets'];
$toolbarFilterSchema = is_array($listFilterContext['toolbarFilterSchema'] ?? null) ? $listFilterContext['toolbarFilterSchema'] : [];
$searchToolbarFilterSchema = is_array($listFilterContext['searchToolbarFilterSchema'] ?? null) ? $listFilterContext['searchToolbarFilterSchema'] : [];
$drawerToolbarFilterSchema = is_array($listFilterContext['drawerToolbarFilterSchema'] ?? null) ? $listFilterContext['drawerToolbarFilterSchema'] : [];
$toolbarFilterState = is_array($listFilterContext['toolbarFilterState'] ?? null) ? $listFilterContext['toolbarFilterState'] : [];
$toolbarOptionSets = is_array($listFilterContext['toolbarOptionSets'] ?? null) ? $listFilterContext['toolbarOptionSets'] : [];
$clientFilterSchema = is_array($listFilterContext['clientFilterSchema'] ?? null) ? $listFilterContext['clientFilterSchema'] : [];
$searchConfig = $listFilterContext['searchConfig'] ?? null;
$filterChipMeta = [
'search' => [
'label' => t('Search'),
@@ -219,5 +146,50 @@ $filterChipMeta = [
'to_param' => 'created_to',
],
];
$clientFilterSchema = $listFilterContext['clientFilterSchema'];
$searchConfig = $listFilterContext['searchConfig'];
$listTitle = t('User lifecycle logs');
ob_start();
$listPurgeEnabled = $canPurge;
$listPurgeFormId = 'settings-user-lifecycle-audit-purge-form';
$listPurgeAction = endpointUrl('admin/settings/user-lifecycle/audit-purge');
$listPurgeConfirmMessage = t('Purge entries older than 365 days?');
$listPurgeButtonLabel = t('Purge user lifecycle logs');
require templatePath('partials/app-list-purge-action.phtml');
$listTitleActionsHtml = ob_get_clean();
$filterUiNamespace = 'settings-user-lifecycle';
$filterToolbarId = 'settings-user-lifecycle-audit-toolbar';
$filterDrawerToolbarId = 'settings-user-lifecycle-audit-drawer-toolbar';
$filterDrawerTitleId = 'settings-user-lifecycle-audit-filter-drawer-title';
$pageConfig = [
'dataUrl' => endpointUrl('admin/settings/user-lifecycle/audit-data'),
'gridSearch' => $searchConfig,
'filterSchema' => $clientFilterSchema,
'filterChipMeta' => $filterChipMeta,
'gridLang' => gridLang(),
'labels' => [
'created' => t('Created'),
'status' => t('Status'),
'action' => t('Action'),
'trigger' => t('Trigger'),
'targetEmail' => t('Target email'),
'targetUuid' => t('Target UUID'),
'reasonCode' => t('Reason code'),
'restoredAt' => t('Restored at'),
],
];
?>
<section class="app-settings-user-lifecycle-audit" data-app-component="settings-user-lifecycle-audit">
<?php require templatePath('partials/app-list-titlebar.phtml'); ?>
<?php require templatePath('partials/app-list-filters.phtml'); ?>
<div class="app-list-table">
<div id="settings-user-lifecycle-audit-grid"></div>
</div>
<script src="<?php e(assetVersion('vendor/gridjs/gridjs.umd.js')); ?>"></script>
<script type="application/json" id="page-config-settings-user-lifecycle-audit-panel"><?php gridJsonForJs($pageConfig); ?></script>
<script type="module" src="<?php e(assetVersion('modules/audit/js/pages/settings-user-lifecycle-audit-panel.js')); ?>"></script>
</section>

View File

@@ -1,12 +1,14 @@
import { createListPageModule } from '/js/core/app-list-page-module.js';
import { initDetailDrawer } from '/js/components/app-detail-drawer.js';
createListPageModule({
configId: 'admin-user-lifecycle-audit-index',
moduleId: 'admin-user-lifecycle-audit-index',
configId: 'settings-user-lifecycle-audit-panel',
moduleId: 'settings-user-lifecycle-audit-panel',
missingGridMessage: 'User lifecycle audit grid init failed: Grid.js missing',
initErrorMessage: 'User lifecycle audit grid init failed',
setup: ({ config, gridjs, appBase, initListPage }) => {
const labels = config.labels ?? {};
const dataUrl = String(config.dataUrl || 'audit/settings-user-lifecycle/audit-data');
const gridSearch = config.gridSearch ?? null;
const filterSchema = config.filterSchema ?? [];
const filterChipMeta = config.filterChipMeta ?? [];
@@ -14,8 +16,8 @@ createListPageModule({
const { gridConfig } = initListPage({
grid: {
gridjs,
container: '#user-lifecycle-audit-grid',
dataUrl: 'audit/user-lifecycle-audit/data',
container: '#settings-user-lifecycle-audit-grid',
dataUrl,
appBase,
columns: [
{ name: labels.created || 'Created', sort: true },
@@ -29,7 +31,18 @@ createListPageModule({
return gridjs.html(`<span class="badge" data-variant="${cell.variant || 'neutral'}">${cell.label || '-'}</span>`);
},
},
{ name: labels.action || 'Action', sort: true },
{
name: labels.action || 'Action',
sort: true,
formatter: (cell, row) => {
const id = row?.cells?.[8]?.data;
const label = String(cell || '-');
if (!id) {
return gridjs.html(label);
}
return gridjs.html(`<a href="#" class="app-grid-link-cell" data-lifecycle-drawer-trigger>${label}</a>`);
},
},
{ name: labels.trigger || 'Trigger', sort: true },
{ name: labels.targetEmail || 'Target email', sort: false },
{ name: labels.targetUuid || 'Target UUID', sort: false },
@@ -51,14 +64,17 @@ createListPageModule({
row.restored_at || '-',
row.id,
]),
rowInteraction: { linkColumn: 2 },
rowInteraction: { linkColumn: false },
search: gridSearch,
filterSchema,
urlSync: true,
rowDataset: (row) => ({
'audit-id': row?.cells?.[8]?.data ? String(row.cells[8].data) : '',
}),
rowDblClick: {
getUrl: (rowData) => {
const id = rowData?.cells?.[8]?.data;
return id ? new URL(`audit/user-lifecycle-audit/view/${id}`, appBase).toString() : '';
return id ? new URL(`admin/settings/user-lifecycle#lifecycle-audit/${id}`, appBase).toString() : '';
},
},
},
@@ -69,6 +85,15 @@ createListPageModule({
},
}) || {};
initDetailDrawer({
gridConfig,
triggerSelector: '[data-lifecycle-drawer-trigger]',
rowUuidAttr: 'audit-id',
fetchUrl: (id) => new URL(`audit/settings-user-lifecycle/audit-fragment/${id}`, appBase).toString(),
fullUrl: (id) => new URL(`admin/settings/user-lifecycle#lifecycle-audit/${id}`, appBase).toString(),
hashPrefix: 'lifecycle-audit',
});
return gridConfig;
},
});