From 0c351f6afff27e2d7e8c98910ce8295f0cf02e64 Mon Sep 17 00:00:00 2001 From: fs Date: Wed, 25 Mar 2026 21:12:49 +0100 Subject: [PATCH] refactor(audit): extract audit domain into self-contained module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the entire audit subsystem (system audit, API audit, import audit, user lifecycle audit, frontend telemetry) from core into modules/audit/. Core decoupling via interface-based injection: - AuditRecorderInterface replaces SystemAuditService in 10+ core services - UserLifecycleAuditInterface / ImportAuditInterface for specialized flows - NullAuditRecorder fallback when audit module is disabled - ApiBootstrap/ApiResponse use null-safe callable resolvers Module structure (modules/audit/): - Manifest with routes, permissions, scheduler jobs, authorization policy - 9 services, 8 repositories, 6 domain enums, 4 job handlers - 33 page files, 4 JS files, 8 test files, migration scripts, i18n Core cleanup: - OperationsAuthorizationPolicy, UiCapabilityMap, PermissionService surgically cleaned of audit-specific constants - Sidebar template cleared of hardcoded audit navigation - AuditModuleIsolationContractTest ensures no future core→module coupling All quality gates pass: 1346 tests (19,276 assertions), PHPStan level 5 clean, architecture contracts verified. Co-Authored-By: Claude Opus 4.6 (1M context) --- composer.json | 6 +- config/modules.php | 2 +- docs/reference-notifications-module.md | 48 +++- .../Container/Registrars/AccessRegistrar.php | 4 +- .../Registrars/AppServicesRegistrar.php | 28 +- .../Registrars/RepositoryFactoryRegistrar.php | 2 - .../Registrars/ServiceFactoryRegistrar.php | 30 +- .../Registrars/SettingsRegistrar.php | 4 +- lib/App/Module/ModuleEventDispatcher.php | 4 +- lib/App/registerContainer.php | 20 +- lib/Console/Commands/Scheduler/RunCommand.php | 4 +- lib/Http/ApiBootstrap.php | 57 +++- lib/Http/ApiResponse.php | 67 ++--- lib/Repository/Stats/AdminStatsRepository.php | 22 +- lib/Service/Access/AccessGatewayFactory.php | 6 +- lib/Service/Access/AssignableRoleService.php | 4 +- .../Access/OperationsAuthorizationPolicy.php | 18 -- lib/Service/Access/PermissionService.php | 10 +- lib/Service/Access/RoleService.php | 4 +- lib/Service/Access/UiCapabilityMap.php | 22 +- lib/Service/Audit/AuditRecorderInterface.php | 21 ++ lib/Service/Audit/AuditRepositoryFactory.php | 32 --- lib/Service/Audit/ImportAuditInterface.php | 28 ++ lib/Service/Audit/NullAuditRecorder.php | 17 ++ lib/Service/Audit/NullImportAudit.php | 23 ++ lib/Service/Audit/NullUserLifecycleAudit.php | 74 +++++ .../Audit/UserLifecycleAuditInterface.php | 73 +++++ lib/Service/Auth/AuthService.php | 4 +- lib/Service/Auth/AuthServicesFactory.php | 6 +- .../Directory/DirectoryServicesFactory.php | 10 +- .../Import/ImportRepositoryFactory.php | 9 - lib/Service/Import/ImportService.php | 4 +- lib/Service/Import/ImportServicesFactory.php | 18 +- lib/Service/Org/DepartmentService.php | 4 +- .../Scheduler/ScheduledJobRegistry.php | 5 - lib/Service/Scheduler/SchedulerRunService.php | 11 +- .../Scheduler/SchedulerServicesFactory.php | 12 +- lib/Service/Settings/AdminSettingsService.php | 4 +- lib/Service/Tenant/TenantService.php | 11 +- lib/Service/User/UserAccountService.php | 4 +- .../User/UserLifecycleRestoreService.php | 4 +- lib/Service/User/UserLifecycleService.php | 4 +- lib/Service/User/UserServicesFactory.php | 19 +- .../Search/SearchSqlResourceProvider.php | 4 +- lib/Support/helpers/app.php | 16 +- .../db/updates/2026-03-25-audit-tables.sql | 97 +++++++ modules/audit/i18n/default_de.json | 71 +++++ modules/audit/i18n/default_en.json | 71 +++++ .../Module/Audit/AuditAuthorizationPolicy.php | 55 ++++ .../Module/Audit/AuditContainerRegistrar.php | 100 +++++++ .../Audit/Domain}/ImportAuditStatus.php | 4 +- .../Audit/Domain}/SystemAuditChannel.php | 4 +- .../Audit/Domain}/SystemAuditOutcome.php | 4 +- .../Audit/Domain}/UserLifecycleAction.php | 4 +- .../Audit/Domain}/UserLifecycleStatus.php | 4 +- .../Domain}/UserLifecycleTriggerType.php | 4 +- .../Audit/Handler/ApiAuditPurgeJobHandler.php | 41 +++ .../Handler/ImportAuditPurgeJobHandler.php | 41 +++ .../Handler/SystemAuditPurgeJobHandler.php | 9 +- .../UserLifecycleAuditPurgeJobHandler.php | 41 +++ .../Audit}/Http/ApiSystemAuditReporter.php | 8 +- .../Audit/Providers/AuditLayoutProvider.php | 14 + .../Repository}/ApiAuditLogRepository.php | 2 +- .../ApiAuditLogRepositoryInterface.php | 2 +- .../Repository}/ImportAuditRunRepository.php | 4 +- .../ImportAuditRunRepositoryInterface.php | 2 +- .../Repository}/SystemAuditLogRepository.php | 6 +- .../SystemAuditLogRepositoryInterface.php | 2 +- .../UserLifecycleAuditRepository.php | 8 +- .../UserLifecycleAuditRepositoryInterface.php | 2 +- .../Module/Audit/Service}/ApiAuditService.php | 4 +- .../Audit/Service}/AuditMetadataEnricher.php | 2 +- .../Audit/Service/AuditRepositoryFactory.php | 40 +++ .../Audit/Service}/AuditServicesFactory.php | 8 +- .../FrontendTelemetryIngestService.php | 4 +- .../Audit/Service}/ImportAuditService.php | 9 +- .../Service}/SystemAuditRedactionService.php | 2 +- .../Audit/Service}/SystemAuditService.php | 11 +- .../Service}/UserLifecycleAuditService.php | 13 +- modules/audit/module.php | 185 +++++++++++++ .../audit/pages}/admin/api-audit/data().php | 7 +- .../pages}/admin/api-audit/filter-schema.php | 0 .../pages}/admin/api-audit/index($slug).php | 0 .../audit/pages}/admin/api-audit/index().php | 9 +- .../admin/api-audit/index(default).phtml | 2 +- .../audit/pages}/admin/api-audit/purge().php | 2 +- .../pages}/admin/api-audit/view($id).php | 4 +- .../admin/api-audit/view(default).phtml | 0 .../admin/frontend-telemetry/ingest().php | 2 +- .../pages}/admin/import-audit/data().php | 8 +- .../admin/import-audit/filter-schema.php | 2 +- .../admin/import-audit/index($slug).php | 0 .../pages}/admin/import-audit/index().php | 9 +- .../admin/import-audit/index(default).phtml | 2 +- .../pages}/admin/import-audit/purge().php | 2 +- .../pages}/admin/import-audit/view($id).php | 4 +- .../admin/import-audit/view(default).phtml | 0 .../pages}/admin/system-audit/data().php | 10 +- .../admin/system-audit/filter-schema.php | 4 +- .../admin/system-audit/index($slug).php | 0 .../pages}/admin/system-audit/index().php | 10 +- .../admin/system-audit/index(default).phtml | 2 +- .../pages}/admin/system-audit/purge().php | 8 +- .../pages}/admin/system-audit/view($id).php | 6 +- .../admin/system-audit/view(default).phtml | 0 .../admin/user-lifecycle-audit/data().php | 12 +- .../user-lifecycle-audit/filter-schema.php | 6 +- .../admin/user-lifecycle-audit/index().php | 15 +- .../user-lifecycle-audit/index(default).phtml | 2 +- .../admin/user-lifecycle-audit/purge().php | 2 +- .../user-lifecycle-audit/restore($id).php | 2 +- .../admin/user-lifecycle-audit/view($id).php | 10 +- .../user-lifecycle-audit/view(default).phtml | 0 .../Http/ApiSystemAuditReporterTest.php | 6 +- .../Audit/Service}/ApiAuditServiceTest.php | 6 +- .../Service}/AuditMetadataEnricherTest.php | 4 +- .../FrontendTelemetryIngestServiceTest.php | 6 +- .../Audit/Service}/ImportAuditServiceTest.php | 6 +- .../SystemAuditRedactionServiceTest.php | 4 +- .../Audit/Service}/SystemAuditServiceTest.php | 8 +- .../UserLifecycleAuditServiceTest.php | 6 +- .../web}/js/pages/admin-api-audit-index.js | 0 .../web}/js/pages/admin-import-audit-index.js | 0 .../web}/js/pages/admin-system-audit-index.js | 0 .../pages/admin-user-lifecycle-audit-index.js | 0 .../UserActivatedNotificationListener.php | 19 +- ...rAssignmentChangedNotificationListener.php | 31 +-- .../UserCreatedNotificationListener.php | 19 +- .../UserDeactivatedNotificationListener.php | 19 +- .../UserDeletedNotificationListener.php | 19 +- .../Repository/NotificationRepository.php | 2 +- .../Service/NotificationService.php | 258 +++++++++++++++--- .../UserActivatedNotificationListenerTest.php | 22 +- ...ignmentChangedNotificationListenerTest.php | 34 +-- .../UserCreatedNotificationListenerTest.php | 26 +- ...serDeactivatedNotificationListenerTest.php | 16 +- .../UserDeletedNotificationListenerTest.php | 26 +- .../Service/NotificationServiceTest.php | 92 +++++++ .../js/components/app-notification-bell.js | 4 +- pages/admin/scheduled-jobs/index().php | 2 +- pages/admin/stats/index(default).phtml | 6 +- templates/partials/app-main-aside.phtml | 58 +--- .../AuditModuleIsolationContractTest.php | 115 ++++++++ .../AuthzUiActionContractTest.php | 23 +- tests/Architecture/AuthzUiContractSupport.php | 42 +-- .../AuthzUiLayoutContractTest.php | 8 +- .../AuthzUiTemplateContractTest.php | 10 +- .../ContainerFactoryContractTest.php | 2 +- .../FrontendTelemetryContractTest.php | 6 +- tests/Architecture/ListContractFiles.php | 40 +-- .../ListDataEndpointContractTest.php | 8 +- .../ListTitlebarContractFiles.php | 8 +- .../ListUiSharedPartialsContractTest.php | 8 +- .../SecurityLoggingRedactionContractTest.php | 4 +- .../SecurityLoggingRuntimeContractTest.php | 7 +- .../SecurityLoggingTelemetryContractTest.php | 2 +- .../StatusTaxonomyContractFiles.php | 50 ++-- .../StatusTaxonomyUiContractTest.php | 6 +- .../Taxonomy/TaxonomyEnumContractTest.php | 12 +- .../OperationsAuthorizationPolicyTest.php | 14 - .../Service/Access/PermissionServiceTest.php | 6 +- tests/Service/Access/RoleServiceTest.php | 6 +- tests/Service/Auth/AuthServiceTest.php | 16 +- tests/Service/Import/ImportServiceTest.php | 4 +- tests/Service/Org/DepartmentServiceTest.php | 6 +- .../Scheduler/ScheduledJobRegistryTest.php | 2 - .../Scheduler/SchedulerRunServiceTest.php | 6 +- .../AdminSettingsServiceApiLifecycleTest.php | 4 +- .../Settings/AdminSettingsServiceAppTest.php | 4 +- .../AdminSettingsServiceSecurityTest.php | 4 +- .../AdminSettingsServiceSessionPolicyTest.php | 4 +- tests/Service/Tenant/TenantServiceTest.php | 6 +- tests/Service/User/UserAccountServiceTest.php | 18 +- .../User/UserLifecycleRestoreServiceTest.php | 32 +-- .../Service/User/UserLifecycleServiceTest.php | 6 +- .../Unit/Module/ModuleEventDispatcherTest.php | 4 +- 176 files changed, 2157 insertions(+), 834 deletions(-) create mode 100644 lib/Service/Audit/AuditRecorderInterface.php delete mode 100644 lib/Service/Audit/AuditRepositoryFactory.php create mode 100644 lib/Service/Audit/ImportAuditInterface.php create mode 100644 lib/Service/Audit/NullAuditRecorder.php create mode 100644 lib/Service/Audit/NullImportAudit.php create mode 100644 lib/Service/Audit/NullUserLifecycleAudit.php create mode 100644 lib/Service/Audit/UserLifecycleAuditInterface.php create mode 100644 modules/audit/db/updates/2026-03-25-audit-tables.sql create mode 100644 modules/audit/i18n/default_de.json create mode 100644 modules/audit/i18n/default_en.json create mode 100644 modules/audit/lib/Module/Audit/AuditAuthorizationPolicy.php create mode 100644 modules/audit/lib/Module/Audit/AuditContainerRegistrar.php rename {lib/Domain/Taxonomy => modules/audit/lib/Module/Audit/Domain}/ImportAuditStatus.php (88%) rename {lib/Domain/Taxonomy => modules/audit/lib/Module/Audit/Domain}/SystemAuditChannel.php (87%) rename {lib/Domain/Taxonomy => modules/audit/lib/Module/Audit/Domain}/SystemAuditOutcome.php (86%) rename {lib/Domain/Taxonomy => modules/audit/lib/Module/Audit/Domain}/UserLifecycleAction.php (86%) rename {lib/Domain/Taxonomy => modules/audit/lib/Module/Audit/Domain}/UserLifecycleStatus.php (86%) rename {lib/Domain/Taxonomy => modules/audit/lib/Module/Audit/Domain}/UserLifecycleTriggerType.php (86%) create mode 100644 modules/audit/lib/Module/Audit/Handler/ApiAuditPurgeJobHandler.php create mode 100644 modules/audit/lib/Module/Audit/Handler/ImportAuditPurgeJobHandler.php rename {lib/Service/Scheduler => modules/audit/lib/Module/Audit}/Handler/SystemAuditPurgeJobHandler.php (83%) create mode 100644 modules/audit/lib/Module/Audit/Handler/UserLifecycleAuditPurgeJobHandler.php rename {lib => modules/audit/lib/Module/Audit}/Http/ApiSystemAuditReporter.php (96%) create mode 100644 modules/audit/lib/Module/Audit/Providers/AuditLayoutProvider.php rename {lib/Repository/Audit => modules/audit/lib/Module/Audit/Repository}/ApiAuditLogRepository.php (99%) rename {lib/Repository/Audit => modules/audit/lib/Module/Audit/Repository}/ApiAuditLogRepositoryInterface.php (90%) rename {lib/Repository/Audit => modules/audit/lib/Module/Audit/Repository}/ImportAuditRunRepository.php (99%) rename {lib/Repository/Audit => modules/audit/lib/Module/Audit/Repository}/ImportAuditRunRepositoryInterface.php (91%) rename {lib/Repository/Audit => modules/audit/lib/Module/Audit/Repository}/SystemAuditLogRepository.php (98%) rename {lib/Repository/Audit => modules/audit/lib/Module/Audit/Repository}/SystemAuditLogRepositoryInterface.php (90%) rename {lib/Repository/Audit => modules/audit/lib/Module/Audit/Repository}/UserLifecycleAuditRepository.php (98%) rename {lib/Repository/Audit => modules/audit/lib/Module/Audit/Repository}/UserLifecycleAuditRepositoryInterface.php (94%) rename {lib/Service/Audit => modules/audit/lib/Module/Audit/Service}/ApiAuditService.php (98%) rename {lib/Service/Audit => modules/audit/lib/Module/Audit/Service}/AuditMetadataEnricher.php (97%) create mode 100644 modules/audit/lib/Module/Audit/Service/AuditRepositoryFactory.php rename {lib/Service/Audit => modules/audit/lib/Module/Audit/Service}/AuditServicesFactory.php (90%) rename {lib/Service/Audit => modules/audit/lib/Module/Audit/Service}/FrontendTelemetryIngestService.php (99%) rename {lib/Service/Audit => modules/audit/lib/Module/Audit/Service}/ImportAuditService.php (95%) rename {lib/Service/Audit => modules/audit/lib/Module/Audit/Service}/SystemAuditRedactionService.php (99%) rename {lib/Service/Audit => modules/audit/lib/Module/Audit/Service}/SystemAuditService.php (95%) rename {lib/Service/Audit => modules/audit/lib/Module/Audit/Service}/UserLifecycleAuditService.php (94%) create mode 100644 modules/audit/module.php rename {pages => modules/audit/pages}/admin/api-audit/data().php (87%) rename {pages => modules/audit/pages}/admin/api-audit/filter-schema.php (100%) rename {pages => modules/audit/pages}/admin/api-audit/index($slug).php (100%) rename {pages => modules/audit/pages}/admin/api-audit/index().php (94%) rename {pages => modules/audit/pages}/admin/api-audit/index(default).phtml (95%) rename {pages => modules/audit/pages}/admin/api-audit/purge().php (89%) rename {pages => modules/audit/pages}/admin/api-audit/view($id).php (62%) rename {pages => modules/audit/pages}/admin/api-audit/view(default).phtml (100%) rename {pages => modules/audit/pages}/admin/frontend-telemetry/ingest().php (97%) rename {pages => modules/audit/pages}/admin/import-audit/data().php (85%) rename {pages => modules/audit/pages}/admin/import-audit/filter-schema.php (98%) rename {pages => modules/audit/pages}/admin/import-audit/index($slug).php (100%) rename {pages => modules/audit/pages}/admin/import-audit/index().php (92%) rename {pages => modules/audit/pages}/admin/import-audit/index(default).phtml (95%) rename {pages => modules/audit/pages}/admin/import-audit/purge().php (93%) rename {pages => modules/audit/pages}/admin/import-audit/view($id).php (63%) rename {pages => modules/audit/pages}/admin/import-audit/view(default).phtml (100%) rename {pages => modules/audit/pages}/admin/system-audit/data().php (84%) rename {pages => modules/audit/pages}/admin/system-audit/filter-schema.php (97%) rename {pages => modules/audit/pages}/admin/system-audit/index($slug).php (100%) rename {pages => modules/audit/pages}/admin/system-audit/index().php (94%) rename {pages => modules/audit/pages}/admin/system-audit/index(default).phtml (95%) rename {pages => modules/audit/pages}/admin/system-audit/purge().php (77%) rename {pages => modules/audit/pages}/admin/system-audit/view($id).php (70%) rename {pages => modules/audit/pages}/admin/system-audit/view(default).phtml (100%) rename {pages => modules/audit/pages}/admin/user-lifecycle-audit/data().php (77%) rename {pages => modules/audit/pages}/admin/user-lifecycle-audit/filter-schema.php (95%) rename {pages => modules/audit/pages}/admin/user-lifecycle-audit/index().php (93%) rename {pages => modules/audit/pages}/admin/user-lifecycle-audit/index(default).phtml (95%) rename {pages => modules/audit/pages}/admin/user-lifecycle-audit/purge().php (89%) rename {pages => modules/audit/pages}/admin/user-lifecycle-audit/restore($id).php (93%) rename {pages => modules/audit/pages}/admin/user-lifecycle-audit/view($id).php (71%) rename {pages => modules/audit/pages}/admin/user-lifecycle-audit/view(default).phtml (100%) rename {tests => modules/audit/tests/Module/Audit}/Http/ApiSystemAuditReporterTest.php (96%) rename {tests/Service/Audit => modules/audit/tests/Module/Audit/Service}/ApiAuditServiceTest.php (92%) rename {tests/Service/Audit => modules/audit/tests/Module/Audit/Service}/AuditMetadataEnricherTest.php (97%) rename {tests/Service/Audit => modules/audit/tests/Module/Audit/Service}/FrontendTelemetryIngestServiceTest.php (98%) rename {tests/Service/Audit => modules/audit/tests/Module/Audit/Service}/ImportAuditServiceTest.php (94%) rename {tests/Service/Audit => modules/audit/tests/Module/Audit/Service}/SystemAuditRedactionServiceTest.php (92%) rename {tests/Service/Audit => modules/audit/tests/Module/Audit/Service}/SystemAuditServiceTest.php (92%) rename {tests/Service/Audit => modules/audit/tests/Module/Audit/Service}/UserLifecycleAuditServiceTest.php (98%) rename {web => modules/audit/web}/js/pages/admin-api-audit-index.js (100%) rename {web => modules/audit/web}/js/pages/admin-import-audit-index.js (100%) rename {web => modules/audit/web}/js/pages/admin-system-audit-index.js (100%) rename {web => modules/audit/web}/js/pages/admin-user-lifecycle-audit-index.js (100%) create mode 100644 tests/Architecture/AuditModuleIsolationContractTest.php diff --git a/composer.json b/composer.json index 27c5386..98c4ba9 100644 --- a/composer.json +++ b/composer.json @@ -27,12 +27,14 @@ }, "autoload-dev": { "psr-4": { - "MintyPHP\\Tests\\": "tests/" + "MintyPHP\\Tests\\": "tests/", + "MintyPHP\\Tests\\Module\\Audit\\": "modules/audit/tests/Module/Audit/" } }, "autoload": { "psr-4": { - "MintyPHP\\": "lib/" + "MintyPHP\\": "lib/", + "MintyPHP\\Module\\Audit\\": "modules/audit/lib/Module/Audit/" } }, "scripts": { diff --git a/config/modules.php b/config/modules.php index 8c7c003..244f298 100644 --- a/config/modules.php +++ b/config/modules.php @@ -12,5 +12,5 @@ * Each entry must match a directory name under modules/ containing a module.php manifest. */ return [ - 'enabled_modules' => ['addressbook', 'bookmarks', 'notifications'], + 'enabled_modules' => ['audit', 'addressbook', 'bookmarks', 'notifications'], ]; diff --git a/docs/reference-notifications-module.md b/docs/reference-notifications-module.md index 2d22886..a228631 100644 --- a/docs/reference-notifications-module.md +++ b/docs/reference-notifications-module.md @@ -1,6 +1,6 @@ # Notifications Modul (V1) -Letzte Aktualisierung: 2026-03-19 +Letzte Aktualisierung: 2026-03-25 ## Ziel @@ -45,6 +45,41 @@ Kanonische Quelle: `modules/notifications/module.php` 5. `NotificationRepository` persistiert in `user_notifications`. 6. Bell-Endpunkte lesen tenant-scoped Daten und aktualisieren den Session-`unread_count`. +## Producer-Contract (fuer Core + andere Module) + +Stabile Erzeugung erfolgt ueber: + +- `MintyPHP\Module\Notifications\Service\NotificationMessage` +- `MintyPHP\Module\Notifications\Service\NotificationService::createFromMessage(...)` +- `NotificationService::createForTenantUsersFromMessage(...)` +- `NotificationService::createForTenantAdminUsersFromMessage(...)` + +Beispiel (locale-neutral, mit i18n-Key): + +```php +use MintyPHP\Module\Notifications\Service\NotificationMessage; +use MintyPHP\Module\Notifications\Service\NotificationService; + +$message = NotificationMessage::localized( + 'user.created', + 'New user: %s', + [$displayName], + null, + [], + 'admin/users/edit/' . $uuid, + ['user_id' => $userId, 'uuid' => $uuid] +); + +$notificationService->createForTenantAdminUsersFromMessage( + $tenantId, + $message, + $actorUserId, + $allowedAdminSet +); +``` + +Plain/Fallback-Erzeugung bleibt moeglich mit `NotificationMessage::plain(...)`. + ## Datenvertrag Notification Persistierter Kernvertrag: @@ -55,6 +90,16 @@ Persistierter Kernvertrag: - `link` (nullable string) - `data` (nullable JSON) +Erweiterung fuer locale-neutrale Texte: + +- `data.__message.title_key` (string) +- `data.__message.title_params` (array) +- `data.__message.body_key` (string, optional) +- `data.__message.body_params` (array, optional) + +Wenn `__message` vorhanden ist, wird der Anzeigetext beim Lesen in der aktuellen +Empfaenger-Locale gerendert (Fallback auf persistiertes `title`/`body` bleibt erhalten). + Dedupe-Metadaten (intern): - `dedupe_fingerprint` @@ -112,4 +157,3 @@ Dedupe-Metadaten (intern): 2. Actor fuehrt User-Aktion aus. 3. Zweiter Admin sieht Bell-Badge + Listeneintrag. 4. Wiederholung innerhalb 30 Minuten erzeugt kein Duplikat. - diff --git a/lib/App/Container/Registrars/AccessRegistrar.php b/lib/App/Container/Registrars/AccessRegistrar.php index 642aa0b..3825923 100644 --- a/lib/App/Container/Registrars/AccessRegistrar.php +++ b/lib/App/Container/Registrars/AccessRegistrar.php @@ -15,7 +15,7 @@ use MintyPHP\Service\Access\AuthorizationService; use MintyPHP\Service\Access\PermissionService; use MintyPHP\Service\Access\RoleService; use MintyPHP\Service\Access\UiAccessService; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Directory\DirectoryServicesFactory; final class AccessRegistrar implements ContainerRegistrar @@ -40,7 +40,7 @@ final class AccessRegistrar implements ContainerRegistrar $c->get(AccessServicesFactory::class)->createUserRoleRepository(), $c->get(PermissionService::class), $c->get(RoleRepository::class), - $c->get(SystemAuditService::class) + $c->get(AuditRecorderInterface::class) )); } } diff --git a/lib/App/Container/Registrars/AppServicesRegistrar.php b/lib/App/Container/Registrars/AppServicesRegistrar.php index 4b7f0f6..df50018 100644 --- a/lib/App/Container/Registrars/AppServicesRegistrar.php +++ b/lib/App/Container/Registrars/AppServicesRegistrar.php @@ -5,7 +5,6 @@ namespace MintyPHP\App\Container\Registrars; use MintyPHP\App\AppContainer; use MintyPHP\App\Container\ContainerRegistrar; use MintyPHP\App\Module\ModuleRegistry; -use MintyPHP\Http\ApiSystemAuditReporter; use MintyPHP\Http\CookieStore; use MintyPHP\Http\CookieStoreInterface; use MintyPHP\Http\Input\RequestInputFactory; @@ -18,15 +17,7 @@ use MintyPHP\Repository\Search\SearchQueryRepository; use MintyPHP\Repository\Stats\AdminStatsRepository; use MintyPHP\Repository\System\SystemHealthRepository; use MintyPHP\Repository\Tenant\UserTenantRepository; -use MintyPHP\Repository\User\UserReadRepository; use MintyPHP\Service\Access\PermissionService; -use MintyPHP\Service\Audit\ApiAuditService; -use MintyPHP\Service\Audit\AuditMetadataEnricher; -use MintyPHP\Service\Audit\AuditServicesFactory; -use MintyPHP\Service\Audit\FrontendTelemetryIngestService; -use MintyPHP\Service\Audit\ImportAuditService; -use MintyPHP\Service\Audit\SystemAuditService; -use MintyPHP\Service\Audit\UserLifecycleAuditService; use MintyPHP\Service\CustomField\CustomFieldServicesFactory; use MintyPHP\Service\CustomField\TenantCustomFieldService; use MintyPHP\Service\CustomField\UserCustomFieldValueService; @@ -42,7 +33,6 @@ use MintyPHP\Service\Scheduler\SchedulerServicesFactory; use MintyPHP\Service\Search\SearchDataService; use MintyPHP\Service\Security\RateLimiterService; use MintyPHP\Service\Security\SecurityServicesFactory; -use MintyPHP\Service\Settings\SettingsFrontendTelemetryGateway; use MintyPHP\Service\Stats\AdminStatsViewDataService; use MintyPHP\Service\System\SystemHealthService; use MintyPHP\Service\System\SystemInfoService; @@ -71,27 +61,13 @@ final class AppServicesRegistrar implements ContainerRegistrar $container->set(RateLimiterService::class, static fn (AppContainer $c): RateLimiterService => $c->get(SecurityServicesFactory::class)->createRateLimiterService()); $container->set(MailService::class, static fn (AppContainer $c): MailService => $c->get(MailServicesFactory::class)->createMailService()); $container->set(MailLogService::class, static fn (AppContainer $c): MailLogService => $c->get(MailServicesFactory::class)->createMailLogService()); - $container->set(ApiAuditService::class, static fn (AppContainer $c): ApiAuditService => $c->get(AuditServicesFactory::class)->createApiAuditService()); - $container->set(UserLifecycleAuditService::class, static fn (AppContainer $c): UserLifecycleAuditService => $c->get(AuditServicesFactory::class)->createUserLifecycleAuditService()); - $container->set(SystemAuditService::class, static fn (AppContainer $c): SystemAuditService => $c->get(AuditServicesFactory::class)->createSystemAuditService()); - $container->set(FrontendTelemetryIngestService::class, static fn (AppContainer $c): FrontendTelemetryIngestService => new FrontendTelemetryIngestService( - $c->get(SystemAuditService::class), - $c->get(SettingsFrontendTelemetryGateway::class), - $c->get(RateLimiterService::class), - $c->get(SessionStoreInterface::class) - )); - $container->set(ApiSystemAuditReporter::class, static fn (AppContainer $c): ApiSystemAuditReporter => new ApiSystemAuditReporter( - $c->get(SystemAuditService::class) - )); + // Audit services are registered by the audit module's AuditContainerRegistrar. $container->set(ImportService::class, static fn (AppContainer $c): ImportService => $c->get(ImportServicesFactory::class)->createImportService()); - $container->set(ImportAuditService::class, static fn (AppContainer $c): ImportAuditService => $c->get(ImportServicesFactory::class)->createImportAuditService()); $container->set(ScheduledJobService::class, static fn (AppContainer $c): ScheduledJobService => $c->get(SchedulerServicesFactory::class)->createScheduledJobService()); $container->set(SchedulerRunService::class, static fn (AppContainer $c): SchedulerRunService => $c->get(SchedulerServicesFactory::class)->createSchedulerRunService()); $container->set(TenantCustomFieldService::class, static fn (AppContainer $c): TenantCustomFieldService => $c->get(CustomFieldServicesFactory::class)->createTenantCustomFieldService()); $container->set(UserCustomFieldValueService::class, static fn (AppContainer $c): UserCustomFieldValueService => $c->get(CustomFieldServicesFactory::class)->createUserCustomFieldValueService()); - $container->set(AuditMetadataEnricher::class, static fn (AppContainer $c): AuditMetadataEnricher => new AuditMetadataEnricher( - $c->get(UserReadRepository::class) - )); + // AuditMetadataEnricher is registered by the audit module's AuditContainerRegistrar. $container->set(GridUserCountEnricher::class, static fn (): GridUserCountEnricher => new GridUserCountEnricher()); $container->set(IntendedUrlService::class, static fn (): IntendedUrlService => new IntendedUrlService()); $container->set(RequestInputFactory::class, static fn (): RequestInputFactory => new RequestInputFactory()); diff --git a/lib/App/Container/Registrars/RepositoryFactoryRegistrar.php b/lib/App/Container/Registrars/RepositoryFactoryRegistrar.php index 7d5873f..4d3d2c1 100644 --- a/lib/App/Container/Registrars/RepositoryFactoryRegistrar.php +++ b/lib/App/Container/Registrars/RepositoryFactoryRegistrar.php @@ -8,7 +8,6 @@ use MintyPHP\Repository\Search\SearchQueryRepository; use MintyPHP\Repository\Stats\AdminStatsRepository; use MintyPHP\Repository\Support\DatabaseSessionRepository; use MintyPHP\Service\Access\AccessRepositoryFactory; -use MintyPHP\Service\Audit\AuditRepositoryFactory; use MintyPHP\Service\Auth\AuthRepositoryFactory; use MintyPHP\Service\Directory\DirectoryRepositoryFactory; use MintyPHP\Service\Import\ImportRepositoryFactory; @@ -24,7 +23,6 @@ final class RepositoryFactoryRegistrar implements ContainerRegistrar public function register(AppContainer $container): void { $container->set(SettingRepositoryFactory::class, static fn (): SettingRepositoryFactory => new SettingRepositoryFactory()); - $container->set(AuditRepositoryFactory::class, static fn (): AuditRepositoryFactory => new AuditRepositoryFactory()); $container->set(SecurityRepositoryFactory::class, static fn (): SecurityRepositoryFactory => new SecurityRepositoryFactory()); $container->set(MailRepositoryFactory::class, static fn (): MailRepositoryFactory => new MailRepositoryFactory()); $container->set(ImportRepositoryFactory::class, static fn (): ImportRepositoryFactory => new ImportRepositoryFactory()); diff --git a/lib/App/Container/Registrars/ServiceFactoryRegistrar.php b/lib/App/Container/Registrars/ServiceFactoryRegistrar.php index 1ef7a1c..89f0117 100644 --- a/lib/App/Container/Registrars/ServiceFactoryRegistrar.php +++ b/lib/App/Container/Registrars/ServiceFactoryRegistrar.php @@ -18,8 +18,12 @@ use MintyPHP\Service\Access\AccessRepositoryFactory; use MintyPHP\Service\Access\AccessServicesFactory; use MintyPHP\Service\Access\AssignableRoleService; use MintyPHP\Service\Access\PermissionService; -use MintyPHP\Service\Audit\AuditRepositoryFactory; -use MintyPHP\Service\Audit\AuditServicesFactory; +use MintyPHP\Service\Audit\AuditRecorderInterface; +use MintyPHP\Service\Audit\ImportAuditInterface; +use MintyPHP\Service\Audit\NullAuditRecorder; +use MintyPHP\Service\Audit\NullImportAudit; +use MintyPHP\Service\Audit\NullUserLifecycleAudit; +use MintyPHP\Service\Audit\UserLifecycleAuditInterface; use MintyPHP\Service\Auth\AuthGatewayFactory; use MintyPHP\Service\Auth\AuthRepositoryFactory; use MintyPHP\Service\Auth\AuthServicesFactory; @@ -52,12 +56,9 @@ final class ServiceFactoryRegistrar implements ContainerRegistrar $container->set(SettingServicesFactory::class, static fn (AppContainer $c): SettingServicesFactory => new SettingServicesFactory( $c->get(SettingRepositoryFactory::class) )); - $container->set(AuditServicesFactory::class, static fn (AppContainer $c): AuditServicesFactory => new AuditServicesFactory( - $c->get(AuditRepositoryFactory::class), - $c->get(SettingServicesFactory::class)->createSettingsSystemAuditGateway(), - $c->get(RequestRuntimeInterface::class), - $c->get(SessionStoreInterface::class) - )); + // Audit interfaces are NOT registered here — they are set AFTER module registrars + // in registerContainer.php so the audit module can provide real implementations. + // If the audit module is disabled, null implementations are used as fallback. $container->set(SecurityServicesFactory::class, static fn (AppContainer $c): SecurityServicesFactory => new SecurityServicesFactory( $c->get(SecurityRepositoryFactory::class) )); @@ -81,20 +82,20 @@ final class ServiceFactoryRegistrar implements ContainerRegistrar $c->get(AccessServicesFactory::class), $c->get(SettingServicesFactory::class), $c->get(DirectoryServicesFactory::class), - $c->get(ImportRepositoryFactory::class), + $c->get(ImportAuditInterface::class), $c->get(TenantScopeService::class), $c->get(SessionStoreInterface::class) )); $container->set(SchedulerServicesFactory::class, static fn (AppContainer $c): SchedulerServicesFactory => new SchedulerServicesFactory( $c->get(UserServicesFactory::class), - $c->get(AuditServicesFactory::class), + $c->get(AuditRecorderInterface::class), $c->get(DatabaseSessionRepository::class), $c->get(SchedulerRepositoryFactory::class), $c )); $container->set(DirectoryServicesFactory::class, static fn (AppContainer $c): DirectoryServicesFactory => new DirectoryServicesFactory( $c->get(UserServicesFactory::class), - $c->get(AuditServicesFactory::class), + $c->get(AuditRecorderInterface::class), $c->get(DirectoryRepositoryFactory::class), $c->get(DirectoryGatewayFactory::class) )); @@ -111,7 +112,7 @@ final class ServiceFactoryRegistrar implements ContainerRegistrar )); $container->set(AccessGatewayFactory::class, static fn (AppContainer $c): AccessGatewayFactory => new AccessGatewayFactory( $c->get(AccessRepositoryFactory::class), - $c->get(AuditServicesFactory::class), + $c->get(AuditRecorderInterface::class), $c->get(SessionStoreInterface::class) )); $container->set(AccessPolicyFactory::class, static fn (AppContainer $c): AccessPolicyFactory => new AccessPolicyFactory( @@ -127,7 +128,8 @@ final class ServiceFactoryRegistrar implements ContainerRegistrar $c->get(DirectoryRepositoryFactory::class), )); $container->set(UserServicesFactory::class, static fn (AppContainer $c): UserServicesFactory => new UserServicesFactory( - $c->get(AuditServicesFactory::class), + $c->get(AuditRecorderInterface::class), + $c->get(UserLifecycleAuditInterface::class), $c->get(UserRepositoryFactory::class), $c->get(UserGatewayFactory::class), $c->get(DatabaseSessionRepository::class), @@ -144,7 +146,7 @@ final class ServiceFactoryRegistrar implements ContainerRegistrar )); $container->set(AuthServicesFactory::class, static fn (AppContainer $c): AuthServicesFactory => new AuthServicesFactory( $c->get(UserServicesFactory::class), - $c->get(AuditServicesFactory::class), + $c->get(AuditRecorderInterface::class), $c->get(MailServicesFactory::class), $c->get(AuthRepositoryFactory::class), $c->get(AuthGatewayFactory::class), diff --git a/lib/App/Container/Registrars/SettingsRegistrar.php b/lib/App/Container/Registrars/SettingsRegistrar.php index 94fb1c6..5509b42 100644 --- a/lib/App/Container/Registrars/SettingsRegistrar.php +++ b/lib/App/Container/Registrars/SettingsRegistrar.php @@ -7,7 +7,7 @@ use MintyPHP\App\Container\ContainerRegistrar; use MintyPHP\Repository\Auth\ApiTokenRepository; use MintyPHP\Repository\Auth\RememberTokenRepository; use MintyPHP\Service\Access\RoleService; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Branding\BrandingFaviconService; use MintyPHP\Service\Branding\BrandingLogoService; use MintyPHP\Service\Branding\BrandingServicesFactory; @@ -63,7 +63,7 @@ final class SettingsRegistrar implements ContainerRegistrar $c->get(DepartmentService::class), $c->get(RememberTokenRepository::class), $c->get(ApiTokenRepository::class), - $c->get(SystemAuditService::class) + $c->get(AuditRecorderInterface::class) )); $container->set(ThemeConfigGateway::class, static fn (AppContainer $c): ThemeConfigGateway => $c->get(SettingServicesFactory::class)->createThemeConfigGateway()); // Branding services are registered here because they depend on settings gateways diff --git a/lib/App/Module/ModuleEventDispatcher.php b/lib/App/Module/ModuleEventDispatcher.php index d99da40..c7ec86d 100644 --- a/lib/App/Module/ModuleEventDispatcher.php +++ b/lib/App/Module/ModuleEventDispatcher.php @@ -5,7 +5,7 @@ namespace MintyPHP\App\Module; use MintyPHP\App\AppContainer; use MintyPHP\App\Module\Contracts\EventListener; use MintyPHP\Http\RequestContext; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; /** * Fire-and-forget event dispatcher for module lifecycle events. @@ -22,7 +22,7 @@ final class ModuleEventDispatcher public function __construct( private readonly array $listenerMap, private readonly AppContainer $container, - private readonly ?SystemAuditService $systemAuditService = null + private readonly ?AuditRecorderInterface $systemAuditService = null ) { } diff --git a/lib/App/registerContainer.php b/lib/App/registerContainer.php index 281e415..5c2c5ea 100644 --- a/lib/App/registerContainer.php +++ b/lib/App/registerContainer.php @@ -12,7 +12,12 @@ use MintyPHP\App\Container\Registrars\SettingsRegistrar; use MintyPHP\App\Container\Registrars\UserRegistrar; use MintyPHP\App\Module\ModuleEventDispatcher; use MintyPHP\App\Module\ModuleRegistry; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; +use MintyPHP\Service\Audit\ImportAuditInterface; +use MintyPHP\Service\Audit\NullAuditRecorder; +use MintyPHP\Service\Audit\NullImportAudit; +use MintyPHP\Service\Audit\NullUserLifecycleAudit; +use MintyPHP\Service\Audit\UserLifecycleAuditInterface; $container = new AppContainer(); @@ -48,7 +53,7 @@ $container->set(ModuleRegistry::class, static fn (): ModuleRegistry => $moduleRe $container->set(ModuleEventDispatcher::class, static fn () => new ModuleEventDispatcher( $moduleRegistry->getEventListeners(), $container, - $container->has(SystemAuditService::class) ? $container->get(SystemAuditService::class) : null + $container->has(AuditRecorderInterface::class) ? $container->get(AuditRecorderInterface::class) : null )); // Disallow overriding existing core bindings from module registrars. @@ -67,4 +72,15 @@ foreach ($moduleRegistry->getContainerRegistrars() as $registrarClass) { $registrarInstance->register($container); } +// ── Audit interface fallbacks (null implementations when audit module is disabled) ── +if (!$container->has(AuditRecorderInterface::class)) { + $container->set(AuditRecorderInterface::class, static fn (): AuditRecorderInterface => new NullAuditRecorder()); +} +if (!$container->has(UserLifecycleAuditInterface::class)) { + $container->set(UserLifecycleAuditInterface::class, static fn (): UserLifecycleAuditInterface => new NullUserLifecycleAudit()); +} +if (!$container->has(ImportAuditInterface::class)) { + $container->set(ImportAuditInterface::class, static fn (): ImportAuditInterface => new NullImportAudit()); +} + return $container; diff --git a/lib/Console/Commands/Scheduler/RunCommand.php b/lib/Console/Commands/Scheduler/RunCommand.php index c97b5e5..b0eb8bc 100644 --- a/lib/Console/Commands/Scheduler/RunCommand.php +++ b/lib/Console/Commands/Scheduler/RunCommand.php @@ -4,7 +4,7 @@ namespace MintyPHP\Console\Commands\Scheduler; use MintyPHP\Console\Command; use MintyPHP\DB; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Scheduler\SchedulerServicesFactory; final class RunCommand extends Command @@ -44,7 +44,7 @@ final class RunCommand extends Command } } catch (\Throwable $exception) { try { - app(SystemAuditService::class)->record('scheduler.run', 'failed', [ + app(AuditRecorderInterface::class)->record('scheduler.run', 'failed', [ 'error_code' => 'unexpected_error', 'metadata' => [ 'trigger_type' => 'scheduler', diff --git a/lib/Http/ApiBootstrap.php b/lib/Http/ApiBootstrap.php index ed2b534..0b759e1 100644 --- a/lib/Http/ApiBootstrap.php +++ b/lib/Http/ApiBootstrap.php @@ -2,7 +2,6 @@ namespace MintyPHP\Http; -use MintyPHP\Service\Audit\ApiAuditService; use MintyPHP\Service\Security\RateLimiterService; use MintyPHP\Service\Settings\SettingsApiPolicyGateway; @@ -20,20 +19,20 @@ class ApiBootstrap private static bool $initialized = false; private static bool $shutdownRegistered = false; - /** @var (callable(): ApiAuditService)|null */ + /** @var (callable(): object)|null Resolver for API audit service (provided by audit module) */ private static $apiAuditServiceResolver = null; /** @var (callable(): SettingsApiPolicyGateway)|null */ private static $settingsApiPolicyGatewayResolver = null; /** @var (callable(): RateLimiterService)|null */ private static $rateLimiterServiceResolver = null; - /** @var (callable(): ApiSystemAuditReporter)|null */ + /** @var (callable(): object)|null Resolver for API system audit reporter (provided by audit module) */ private static $apiSystemAuditReporterResolver = null; public static function configure( - callable $apiAuditServiceResolver, + ?callable $apiAuditServiceResolver, callable $settingsApiPolicyGatewayResolver, callable $rateLimiterServiceResolver, - callable $apiSystemAuditReporterResolver + ?callable $apiSystemAuditReporterResolver ): void { self::$apiAuditServiceResolver = $apiAuditServiceResolver; self::$settingsApiPolicyGatewayResolver = $settingsApiPolicyGatewayResolver; @@ -66,7 +65,7 @@ class ApiBootstrap header('X-Request-Id: ' . RequestContext::requestHeaderValue()); self::registerShutdownHandler(); - self::apiAuditService()->startRequestContext(); + self::tryStartAudit(); self::startSystemAuditReporter(); self::setCorsHeaders(); @@ -123,7 +122,7 @@ class ApiBootstrap if ($statusCode <= 0) { $statusCode = 200; } - self::apiAuditService()->finish($statusCode); + self::tryFinishAudit($statusCode); self::finishSystemAuditReporter($statusCode); return; } @@ -135,7 +134,7 @@ class ApiBootstrap if ($statusCode <= 0) { $statusCode = 200; } - self::apiAuditService()->finish($statusCode); + self::tryFinishAudit($statusCode); self::finishSystemAuditReporter($statusCode); return; } @@ -144,7 +143,7 @@ class ApiBootstrap if ($statusCode < 400) { $statusCode = 500; } - self::apiAuditService()->finish($statusCode, 'fatal_error'); + self::tryFinishAudit($statusCode, 'fatal_error'); self::finishSystemAuditReporter($statusCode, 'fatal_error'); }); } @@ -252,20 +251,43 @@ class ApiBootstrap return self::resolveDependency(self::$rateLimiterServiceResolver, RateLimiterService::class, 'ApiBootstrap'); } - private static function apiAuditService(): ApiAuditService + private static function tryStartAudit(): void { - return self::resolveDependency(self::$apiAuditServiceResolver, ApiAuditService::class, 'ApiBootstrap'); + try { + if (is_callable(self::$apiAuditServiceResolver)) { + $service = (self::$apiAuditServiceResolver)(); + if (is_object($service) && method_exists($service, 'startRequestContext')) { + $service->startRequestContext(); + } + } + } catch (\Throwable) { + // fail-open + } } - private static function systemAuditReporter(): ApiSystemAuditReporter + private static function tryFinishAudit(int $statusCode, ?string $errorCode = null): void { - return self::resolveDependency(self::$apiSystemAuditReporterResolver, ApiSystemAuditReporter::class, 'ApiBootstrap'); + try { + if (is_callable(self::$apiAuditServiceResolver)) { + $service = (self::$apiAuditServiceResolver)(); + if (is_object($service) && method_exists($service, 'finish')) { + $service->finish($statusCode, $errorCode); + } + } + } catch (\Throwable) { + // fail-open + } } private static function startSystemAuditReporter(): void { try { - self::systemAuditReporter()->start(); + if (is_callable(self::$apiSystemAuditReporterResolver)) { + $service = (self::$apiSystemAuditReporterResolver)(); + if (is_object($service) && method_exists($service, 'start')) { + $service->start(); + } + } } catch (\Throwable) { // fail-open } @@ -274,7 +296,12 @@ class ApiBootstrap private static function finishSystemAuditReporter(int $statusCode, ?string $errorCode = null): void { try { - self::systemAuditReporter()->finish($statusCode, $errorCode); + if (is_callable(self::$apiSystemAuditReporterResolver)) { + $service = (self::$apiSystemAuditReporterResolver)(); + if (is_object($service) && method_exists($service, 'finish')) { + $service->finish($statusCode, $errorCode); + } + } } catch (\Throwable) { // fail-open } diff --git a/lib/Http/ApiResponse.php b/lib/Http/ApiResponse.php index e23cba8..95d5fa7 100644 --- a/lib/Http/ApiResponse.php +++ b/lib/Http/ApiResponse.php @@ -4,21 +4,20 @@ namespace MintyPHP\Http; use MintyPHP\Http\Input\FormErrors; use MintyPHP\Service\Access\AuthorizationService; -use MintyPHP\Service\Audit\ApiAuditService; class ApiResponse { - /** @var (callable(): ApiAuditService)|null */ + /** @var (callable(): object)|null Resolver for API audit service (provided by audit module) */ private static $apiAuditServiceResolver = null; /** @var (callable(): AuthorizationService)|null */ private static $authorizationServiceResolver = null; - /** @var (callable(): ApiSystemAuditReporter)|null */ + /** @var (callable(): object)|null Resolver for API system audit reporter (provided by audit module) */ private static $apiSystemAuditReporterResolver = null; public static function configure( - callable $apiAuditServiceResolver, + ?callable $apiAuditServiceResolver, callable $authorizationServiceResolver, - callable $apiSystemAuditReporterResolver + ?callable $apiSystemAuditReporterResolver ): void { self::$apiAuditServiceResolver = $apiAuditServiceResolver; self::$authorizationServiceResolver = $authorizationServiceResolver; @@ -169,7 +168,7 @@ class ApiResponse if ($body === null) { self::finishSystemAuditReporter($status, $errorCode); - self::apiAuditService()->finish($status, $errorCode); + self::tryFinishAudit($status, $errorCode); die(); } @@ -190,7 +189,7 @@ class ApiResponse header('Content-Type: application/json; charset=utf-8'); self::finishSystemAuditReporter($status, $errorCode); - self::apiAuditService()->finish($status, $errorCode); + self::tryFinishAudit($status, $errorCode); die($json); } @@ -201,7 +200,7 @@ class ApiResponse return trim($requestId); } - $requestId = self::apiAuditService()->currentRequestId(); + $requestId = self::tryGetAuditRequestId(); if (is_string($requestId) && trim($requestId) !== '') { return trim($requestId); } @@ -239,18 +238,33 @@ class ApiResponse return $details; } - private static function apiAuditService(): ApiAuditService + private static function tryFinishAudit(int $statusCode, ?string $errorCode = null): void { - if (!is_callable(self::$apiAuditServiceResolver)) { - throw new \RuntimeException('ApiResponse is not configured for dependency: ' . ApiAuditService::class); + try { + if (is_callable(self::$apiAuditServiceResolver)) { + $service = (self::$apiAuditServiceResolver)(); + if (is_object($service) && method_exists($service, 'finish')) { + $service->finish($statusCode, $errorCode); + } + } + } catch (\Throwable) { + // fail-open } + } - $service = (self::$apiAuditServiceResolver)(); - if (!$service instanceof ApiAuditService) { - throw new \RuntimeException('ApiResponse resolver returned invalid dependency: ' . ApiAuditService::class); + private static function tryGetAuditRequestId(): ?string + { + try { + if (is_callable(self::$apiAuditServiceResolver)) { + $service = (self::$apiAuditServiceResolver)(); + if (is_object($service) && method_exists($service, 'currentRequestId')) { + return $service->currentRequestId(); + } + } + } catch (\Throwable) { + // fail-open } - - return $service; + return null; } private static function authorizationService(): AuthorizationService @@ -267,24 +281,15 @@ class ApiResponse return $service; } - private static function systemAuditReporter(): ApiSystemAuditReporter - { - if (!is_callable(self::$apiSystemAuditReporterResolver)) { - throw new \RuntimeException('ApiResponse is not configured for dependency: ' . ApiSystemAuditReporter::class); - } - - $service = (self::$apiSystemAuditReporterResolver)(); - if (!$service instanceof ApiSystemAuditReporter) { - throw new \RuntimeException('ApiResponse resolver returned invalid dependency: ' . ApiSystemAuditReporter::class); - } - - return $service; - } - private static function finishSystemAuditReporter(int $statusCode, ?string $errorCode = null): void { try { - self::systemAuditReporter()->finish($statusCode, $errorCode); + if (is_callable(self::$apiSystemAuditReporterResolver)) { + $service = (self::$apiSystemAuditReporterResolver)(); + if (is_object($service) && method_exists($service, 'finish')) { + $service->finish($statusCode, $errorCode); + } + } } catch (\Throwable) { // fail-open } diff --git a/lib/Repository/Stats/AdminStatsRepository.php b/lib/Repository/Stats/AdminStatsRepository.php index 507d2dc..b91ed94 100644 --- a/lib/Repository/Stats/AdminStatsRepository.php +++ b/lib/Repository/Stats/AdminStatsRepository.php @@ -3,14 +3,10 @@ namespace MintyPHP\Repository\Stats; use MintyPHP\DB; -use MintyPHP\Domain\Taxonomy\ImportAuditStatus; use MintyPHP\Domain\Taxonomy\MailLogStatus; use MintyPHP\Domain\Taxonomy\ScheduledJobRunStatus; use MintyPHP\Domain\Taxonomy\ScheduledJobTriggerType; -use MintyPHP\Domain\Taxonomy\SystemAuditOutcome; use MintyPHP\Domain\Taxonomy\TenantStatus; -use MintyPHP\Domain\Taxonomy\UserLifecycleAction; -use MintyPHP\Domain\Taxonomy\UserLifecycleStatus; class AdminStatsRepository implements AdminStatsRepositoryInterface { @@ -21,15 +17,15 @@ class AdminStatsRepository implements AdminStatsRepositoryInterface { $tenantActiveStatus = TenantStatus::Active->value; $tenantInactiveStatus = TenantStatus::Inactive->value; - $importStatusRunning = ImportAuditStatus::Running->value; - $importStatusSuccess = ImportAuditStatus::Success->value; - $importStatusPartial = ImportAuditStatus::Partial->value; - $importStatusFailed = ImportAuditStatus::Failed->value; - $systemAuditOutcomeFailed = SystemAuditOutcome::Failed->value; - $systemAuditOutcomeDenied = SystemAuditOutcome::Denied->value; - $lifecycleStatusFailed = UserLifecycleStatus::Failed->value; - $lifecycleStatusSkipped = UserLifecycleStatus::Skipped->value; - $lifecycleActionRestore = UserLifecycleAction::Restore->value; + $importStatusRunning = 'running'; + $importStatusSuccess = 'success'; + $importStatusPartial = 'partial'; + $importStatusFailed = 'failed'; + $systemAuditOutcomeFailed = 'failed'; + $systemAuditOutcomeDenied = 'denied'; + $lifecycleStatusFailed = 'failed'; + $lifecycleStatusSkipped = 'skipped'; + $lifecycleActionRestore = 'restore'; $schedulerTriggerScheduler = ScheduledJobTriggerType::Scheduler->value; $schedulerRunStatusFailed = ScheduledJobRunStatus::Failed->value; $mailStatusFailed = MailLogStatus::Failed->value; diff --git a/lib/Service/Access/AccessGatewayFactory.php b/lib/Service/Access/AccessGatewayFactory.php index 43a20bf..46d1707 100644 --- a/lib/Service/Access/AccessGatewayFactory.php +++ b/lib/Service/Access/AccessGatewayFactory.php @@ -3,7 +3,7 @@ namespace MintyPHP\Service\Access; use MintyPHP\Http\SessionStoreInterface; -use MintyPHP\Service\Audit\AuditServicesFactory; +use MintyPHP\Service\Audit\AuditRecorderInterface; class AccessGatewayFactory { @@ -11,7 +11,7 @@ class AccessGatewayFactory public function __construct( private readonly AccessRepositoryFactory $accessRepositoryFactory, - private readonly AuditServicesFactory $auditServicesFactory, + private readonly AuditRecorderInterface $auditRecorder, private readonly SessionStoreInterface $sessionStore ) { } @@ -22,7 +22,7 @@ class AccessGatewayFactory $this->accessRepositoryFactory->createPermissionRepository(), $this->accessRepositoryFactory->createRolePermissionRepository(), $this->accessRepositoryFactory->createUserRoleRepository(), - $this->auditServicesFactory->createSystemAuditService(), + $this->auditRecorder, $this->sessionStore ); } diff --git a/lib/Service/Access/AssignableRoleService.php b/lib/Service/Access/AssignableRoleService.php index e7f6918..980ed4d 100644 --- a/lib/Service/Access/AssignableRoleService.php +++ b/lib/Service/Access/AssignableRoleService.php @@ -5,7 +5,7 @@ namespace MintyPHP\Service\Access; use MintyPHP\Repository\Access\RoleAssignableRoleRepositoryInterface; use MintyPHP\Repository\Access\RoleRepositoryInterface; use MintyPHP\Repository\Access\UserRoleRepositoryInterface; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; class AssignableRoleService { @@ -14,7 +14,7 @@ class AssignableRoleService private readonly UserRoleRepositoryInterface $userRoleRepository, private readonly PermissionService $permissionService, private readonly RoleRepositoryInterface $roleRepository, - private readonly SystemAuditService $systemAuditService + private readonly AuditRecorderInterface $systemAuditService ) { } diff --git a/lib/Service/Access/OperationsAuthorizationPolicy.php b/lib/Service/Access/OperationsAuthorizationPolicy.php index f3cd5c5..0a529f7 100644 --- a/lib/Service/Access/OperationsAuthorizationPolicy.php +++ b/lib/Service/Access/OperationsAuthorizationPolicy.php @@ -4,13 +4,7 @@ namespace MintyPHP\Service\Access; final class OperationsAuthorizationPolicy implements AuthorizationPolicyInterface { - public const ABILITY_ADMIN_USER_LIFECYCLE_AUDIT_VIEW = 'ops.admin.user_lifecycle_audit.view'; - public const ABILITY_ADMIN_USERS_LIFECYCLE_RESTORE = 'ops.admin.users.lifecycle_restore'; - public const ABILITY_ADMIN_API_AUDIT_VIEW = 'ops.admin.api_audit.view'; - public const ABILITY_ADMIN_SYSTEM_AUDIT_VIEW = 'ops.admin.system_audit.view'; - public const ABILITY_ADMIN_SYSTEM_AUDIT_PURGE = 'ops.admin.system_audit.purge'; public const ABILITY_ADMIN_DOCS_VIEW = 'ops.admin.docs.view'; - public const ABILITY_ADMIN_IMPORTS_AUDIT_VIEW = 'ops.admin.imports_audit.view'; public const ABILITY_ADMIN_IMPORTS_VIEW = 'ops.admin.imports.view'; public const ABILITY_ADMIN_JOBS_RUN_NOW = 'ops.admin.jobs.run_now'; public const ABILITY_ADMIN_JOBS_VIEW = 'ops.admin.jobs.view'; @@ -49,13 +43,7 @@ final class OperationsAuthorizationPolicy implements AuthorizationPolicyInterfac public function supports(string $ability): bool { return in_array($ability, [ - self::ABILITY_ADMIN_USER_LIFECYCLE_AUDIT_VIEW, - self::ABILITY_ADMIN_USERS_LIFECYCLE_RESTORE, - self::ABILITY_ADMIN_API_AUDIT_VIEW, - self::ABILITY_ADMIN_SYSTEM_AUDIT_VIEW, - self::ABILITY_ADMIN_SYSTEM_AUDIT_PURGE, self::ABILITY_ADMIN_DOCS_VIEW, - self::ABILITY_ADMIN_IMPORTS_AUDIT_VIEW, self::ABILITY_ADMIN_IMPORTS_VIEW, self::ABILITY_ADMIN_JOBS_RUN_NOW, self::ABILITY_ADMIN_JOBS_VIEW, @@ -98,13 +86,7 @@ final class OperationsAuthorizationPolicy implements AuthorizationPolicyInterfac return match ($ability) { self::ABILITY_ADMIN_USERS_CREATE_EDIT_CUSTOM_FIELDS => $this->authorizeUsersCreateCustomFields($actorUserId), self::ABILITY_API_TOKENS_SELF_MANAGE => $this->authorizeApiTokensSelfManage($actorUserId), - self::ABILITY_ADMIN_USER_LIFECYCLE_AUDIT_VIEW => $this->allowIfHas($actorUserId, PermissionService::USER_LIFECYCLE_AUDIT_VIEW), - self::ABILITY_ADMIN_USERS_LIFECYCLE_RESTORE => $this->allowIfHas($actorUserId, PermissionService::USERS_LIFECYCLE_RESTORE), - self::ABILITY_ADMIN_API_AUDIT_VIEW => $this->allowIfHas($actorUserId, PermissionService::API_AUDIT_VIEW), - self::ABILITY_ADMIN_SYSTEM_AUDIT_VIEW => $this->allowIfHas($actorUserId, PermissionService::SYSTEM_AUDIT_VIEW), - self::ABILITY_ADMIN_SYSTEM_AUDIT_PURGE => $this->allowIfHas($actorUserId, PermissionService::SYSTEM_AUDIT_PURGE), self::ABILITY_ADMIN_DOCS_VIEW => $this->allowIfHas($actorUserId, PermissionService::DOCS_VIEW), - self::ABILITY_ADMIN_IMPORTS_AUDIT_VIEW => $this->allowIfHas($actorUserId, PermissionService::IMPORTS_AUDIT_VIEW), self::ABILITY_ADMIN_IMPORTS_VIEW => $this->allowIfHas($actorUserId, PermissionService::IMPORTS_VIEW), self::ABILITY_ADMIN_JOBS_RUN_NOW => $this->allowIfHas($actorUserId, PermissionService::JOBS_RUN_NOW), self::ABILITY_ADMIN_JOBS_VIEW => $this->allowIfHas($actorUserId, PermissionService::JOBS_VIEW), diff --git a/lib/Service/Access/PermissionService.php b/lib/Service/Access/PermissionService.php index 2c5261e..628dab5 100644 --- a/lib/Service/Access/PermissionService.php +++ b/lib/Service/Access/PermissionService.php @@ -6,7 +6,7 @@ use MintyPHP\Http\SessionStoreInterface; use MintyPHP\Repository\Access\PermissionRepositoryInterface; use MintyPHP\Repository\Access\RolePermissionRepositoryInterface; use MintyPHP\Repository\Access\UserRoleRepositoryInterface; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; /** * Central RBAC permission service — resolves and caches permission keys for users. @@ -28,7 +28,7 @@ class PermissionService private readonly PermissionRepositoryInterface $permissionRepository, private readonly RolePermissionRepositoryInterface $rolePermissionRepository, private readonly UserRoleRepositoryInterface $userRoleRepository, - private readonly SystemAuditService $systemAuditService, + private readonly AuditRecorderInterface $systemAuditService, private readonly SessionStoreInterface $sessionStore ) { } @@ -65,22 +65,16 @@ class PermissionService public const SETTINGS_UPDATE = 'settings.update'; public const TENANT_SCOPE_GLOBAL = 'tenant.scope.global'; public const IMPORTS_VIEW = 'imports.view'; - public const IMPORTS_AUDIT_VIEW = 'imports.audit.view'; public const JOBS_VIEW = 'jobs.view'; public const JOBS_MANAGE = 'jobs.manage'; public const JOBS_RUN_NOW = 'jobs.run_now'; - public const USER_LIFECYCLE_AUDIT_VIEW = 'user_lifecycle_audit.view'; public const USERS_IMPORT = 'users.import'; public const USERS_IMPORT_ASSIGNMENTS = 'users.import_assignments'; - public const USERS_LIFECYCLE_RESTORE = 'users.lifecycle_restore'; public const CUSTOM_FIELDS_MANAGE = 'custom_fields.manage'; public const CUSTOM_FIELDS_EDIT_VALUES = 'custom_fields.edit_values'; public const API_DOCS_VIEW = 'api_docs.view'; public const DOCS_VIEW = 'docs.view'; public const MAIL_LOG_VIEW = 'mail_log.view'; - public const API_AUDIT_VIEW = 'api_audit.view'; - public const SYSTEM_AUDIT_VIEW = 'system_audit.view'; - public const SYSTEM_AUDIT_PURGE = 'system_audit.purge'; public const STATS_VIEW = 'stats.view'; public const SYSTEM_INFO_VIEW = 'system_info.view'; public const API_TOKENS_MANAGE = 'api_tokens.manage'; diff --git a/lib/Service/Access/RoleService.php b/lib/Service/Access/RoleService.php index 2771335..1ca32d2 100644 --- a/lib/Service/Access/RoleService.php +++ b/lib/Service/Access/RoleService.php @@ -3,7 +3,7 @@ namespace MintyPHP\Service\Access; use MintyPHP\Repository\Access\RoleRepositoryInterface; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Directory\DirectorySettingsGateway; class RoleService @@ -11,7 +11,7 @@ class RoleService public function __construct( private readonly RoleRepositoryInterface $roleRepository, private readonly DirectorySettingsGateway $settingsGateway, - private readonly SystemAuditService $systemAuditService + private readonly AuditRecorderInterface $systemAuditService ) { } diff --git a/lib/Service/Access/UiCapabilityMap.php b/lib/Service/Access/UiCapabilityMap.php index be63e47..e2b33e3 100644 --- a/lib/Service/Access/UiCapabilityMap.php +++ b/lib/Service/Access/UiCapabilityMap.php @@ -24,10 +24,6 @@ final class UiCapabilityMap 'can_view_api_docs' => OperationsAuthorizationPolicy::ABILITY_ADMIN_API_DOCS_VIEW, 'can_view_docs' => OperationsAuthorizationPolicy::ABILITY_ADMIN_DOCS_VIEW, 'can_view_mail_log' => OperationsAuthorizationPolicy::ABILITY_ADMIN_MAIL_LOG_VIEW, - 'can_view_api_audit' => OperationsAuthorizationPolicy::ABILITY_ADMIN_API_AUDIT_VIEW, - 'can_view_system_audit' => OperationsAuthorizationPolicy::ABILITY_ADMIN_SYSTEM_AUDIT_VIEW, - 'can_view_imports_audit' => OperationsAuthorizationPolicy::ABILITY_ADMIN_IMPORTS_AUDIT_VIEW, - 'can_view_user_lifecycle_audit' => OperationsAuthorizationPolicy::ABILITY_ADMIN_USER_LIFECYCLE_AUDIT_VIEW, 'can_view_stats' => OperationsAuthorizationPolicy::ABILITY_ADMIN_STATS_VIEW, 'can_view_system_info' => OperationsAuthorizationPolicy::ABILITY_ADMIN_SYSTEM_INFO_VIEW, ]; @@ -57,29 +53,13 @@ final class UiCapabilityMap 'can_view_mail_log' => OperationsAuthorizationPolicy::ABILITY_ADMIN_MAIL_LOG_VIEW, 'can_view_users' => UserAuthorizationPolicy::ABILITY_ADMIN_USERS_VIEW, 'can_view_tenants' => TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_VIEW, - 'can_view_imports_audit' => OperationsAuthorizationPolicy::ABILITY_ADMIN_IMPORTS_AUDIT_VIEW, - 'can_view_system_audit' => OperationsAuthorizationPolicy::ABILITY_ADMIN_SYSTEM_AUDIT_VIEW, - 'can_view_user_lifecycle_audit' => OperationsAuthorizationPolicy::ABILITY_ADMIN_USER_LIFECYCLE_AUDIT_VIEW, ]; /** * @var array */ - public const PAGE_AUDIT_PURGE = [ + public const PAGE_JOBS_PURGE = [ 'can_purge' => SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE, ]; - /** - * @var array - */ - public const PAGE_SYSTEM_AUDIT = [ - 'can_purge_system_audit' => OperationsAuthorizationPolicy::ABILITY_ADMIN_SYSTEM_AUDIT_PURGE, - ]; - - /** - * @var array - */ - public const PAGE_USER_LIFECYCLE_VIEW = [ - 'can_restore' => OperationsAuthorizationPolicy::ABILITY_ADMIN_USERS_LIFECYCLE_RESTORE, - ]; } diff --git a/lib/Service/Audit/AuditRecorderInterface.php b/lib/Service/Audit/AuditRecorderInterface.php new file mode 100644 index 0000000..e31df61 --- /dev/null +++ b/lib/Service/Audit/AuditRecorderInterface.php @@ -0,0 +1,21 @@ + $context + */ + public function record( + string $eventType, + string $outcome = 'success', + array $context = [] + ): ?int; +} diff --git a/lib/Service/Audit/AuditRepositoryFactory.php b/lib/Service/Audit/AuditRepositoryFactory.php deleted file mode 100644 index d6a4729..0000000 --- a/lib/Service/Audit/AuditRepositoryFactory.php +++ /dev/null @@ -1,32 +0,0 @@ -apiAuditLogRepository ??= new ApiAuditLogRepository(); - } - - public function createUserLifecycleAuditRepository(): UserLifecycleAuditRepositoryInterface - { - return $this->userLifecycleAuditRepository ??= new UserLifecycleAuditRepository(); - } - - public function createSystemAuditLogRepository(): SystemAuditLogRepositoryInterface - { - return $this->systemAuditLogRepository ??= new SystemAuditLogRepository(); - } -} diff --git a/lib/Service/Audit/ImportAuditInterface.php b/lib/Service/Audit/ImportAuditInterface.php new file mode 100644 index 0000000..901ad81 --- /dev/null +++ b/lib/Service/Audit/ImportAuditInterface.php @@ -0,0 +1,28 @@ + $mappedTargets + */ + public function startRun( + string $profileKey, + array $mappedTargets, + ?string $sourceFilename, + int $userId, + ?int $currentTenantId + ): ?int; + + /** + * @param array $result + */ + public function finishRun(?int $runId, array $result, ?string $forcedStatus = null): void; +} diff --git a/lib/Service/Audit/NullAuditRecorder.php b/lib/Service/Audit/NullAuditRecorder.php new file mode 100644 index 0000000..20fc81a --- /dev/null +++ b/lib/Service/Audit/NullAuditRecorder.php @@ -0,0 +1,17 @@ + $policy + * @param array $targetUser + */ + public function logDeactivate( + string $runUuid, + string $triggerType, + array $policy, + ?int $actorUserId, + array $targetUser, + string $status = 'success', + ?string $reasonCode = null + ): bool; + + /** + * @param array $policy + * @param array $targetUser + */ + public function logDeleteWithSnapshot( + string $runUuid, + string $triggerType, + array $policy, + ?int $actorUserId, + array $targetUser + ): int|false; + + /** + * @param array $policy + * @param array $targetUser + */ + public function logDeleteFailure( + string $runUuid, + string $triggerType, + array $policy, + ?int $actorUserId, + array $targetUser, + string $reasonCode + ): bool; + + /** + * @param array $policy + * @param array $targetUser + */ + public function logRestore( + string $runUuid, + string $triggerType, + array $policy, + ?int $actorUserId, + array $targetUser, + string $status = 'success', + ?string $reasonCode = null + ): bool; + + public function markDeleteEventRestored(int $auditId, int $restoredByUserId, int $restoredUserId): bool; + + public function updateStatus(int $id, string $status, ?string $reasonCode = null): bool; + + public function findDeleteEventForRestore(int $id, bool $forUpdate = false): ?array; + + public function decryptSnapshot(array $event): ?array; +} diff --git a/lib/Service/Auth/AuthService.php b/lib/Service/Auth/AuthService.php index e43afd8..f637790 100644 --- a/lib/Service/Auth/AuthService.php +++ b/lib/Service/Auth/AuthService.php @@ -9,7 +9,7 @@ use MintyPHP\Repository\User\UserReadRepositoryInterface; use MintyPHP\Repository\User\UserWriteRepositoryInterface; use MintyPHP\Router; use MintyPHP\Service\Access\PermissionService; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\User\UserAccountService; use MintyPHP\Service\User\UserTenantContextService; use MintyPHP\Session; @@ -42,7 +42,7 @@ class AuthService private readonly PermissionService $permissionService, private readonly TenantSsoService $tenantSsoService, private readonly AuthSessionTenantContextService $authSessionTenantContextService, - private readonly SystemAuditService $systemAuditService, + private readonly AuditRecorderInterface $systemAuditService, private readonly SessionStoreInterface $sessionStore, private readonly ?ModuleEventDispatcher $eventDispatcher = null ) { diff --git a/lib/Service/Auth/AuthServicesFactory.php b/lib/Service/Auth/AuthServicesFactory.php index 3ed79a5..d2277f9 100644 --- a/lib/Service/Auth/AuthServicesFactory.php +++ b/lib/Service/Auth/AuthServicesFactory.php @@ -8,7 +8,7 @@ use MintyPHP\Http\CookieStoreInterface; use MintyPHP\Http\RequestRuntimeInterface; use MintyPHP\Http\SessionStoreInterface; use MintyPHP\Repository\Support\DatabaseSessionRepository; -use MintyPHP\Service\Audit\AuditServicesFactory; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Mail\MailServicesFactory; use MintyPHP\Service\User\UserServicesFactory; @@ -28,7 +28,7 @@ class AuthServicesFactory public function __construct( private readonly UserServicesFactory $userServicesFactory, - private readonly AuditServicesFactory $auditServicesFactory, + private readonly AuditRecorderInterface $auditRecorder, private readonly MailServicesFactory $mailServicesFactory, private readonly AuthRepositoryFactory $authRepositoryFactory, private readonly AuthGatewayFactory $authGatewayFactory, @@ -101,7 +101,7 @@ class AuthServicesFactory $this->authGatewayFactory->createPermissionService(), $this->createTenantSsoService(), $this->createAuthSessionTenantContextService(), - $this->auditServicesFactory->createSystemAuditService(), + $this->auditRecorder, $this->sessionStore, $this->resolveEventDispatcher() ); diff --git a/lib/Service/Directory/DirectoryServicesFactory.php b/lib/Service/Directory/DirectoryServicesFactory.php index 9567a6b..7d967f7 100644 --- a/lib/Service/Directory/DirectoryServicesFactory.php +++ b/lib/Service/Directory/DirectoryServicesFactory.php @@ -6,7 +6,7 @@ use MintyPHP\Repository\Access\RoleRepositoryInterface; use MintyPHP\Repository\Org\DepartmentRepositoryInterface; use MintyPHP\Repository\Tenant\TenantRepositoryInterface; use MintyPHP\Service\Access\RoleService; -use MintyPHP\Service\Audit\AuditServicesFactory; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Org\DepartmentService; use MintyPHP\Service\Tenant\TenantScopeService; use MintyPHP\Service\Tenant\TenantService; @@ -20,7 +20,7 @@ class DirectoryServicesFactory public function __construct( private readonly UserServicesFactory $userServicesFactory, - private readonly AuditServicesFactory $auditServicesFactory, + private readonly AuditRecorderInterface $auditRecorder, private readonly DirectoryRepositoryFactory $directoryRepositoryFactory, private readonly DirectoryGatewayFactory $directoryGatewayFactory ) { @@ -32,7 +32,7 @@ class DirectoryServicesFactory $this->createTenantRepository(), $this->createDepartmentRepository(), $this->createDirectorySettingsGateway(), - $this->auditServicesFactory->createSystemAuditService() + $this->auditRecorder ); } @@ -43,7 +43,7 @@ class DirectoryServicesFactory $this->createDepartmentRepository(), $this->createDirectorySettingsGateway(), $this->directoryGatewayFactory->getTenantScopeService(), - $this->auditServicesFactory->createSystemAuditService() + $this->auditRecorder ); } @@ -52,7 +52,7 @@ class DirectoryServicesFactory return $this->roleService ??= new RoleService( $this->createRoleRepository(), $this->createDirectorySettingsGateway(), - $this->auditServicesFactory->createSystemAuditService() + $this->auditRecorder ); } diff --git a/lib/Service/Import/ImportRepositoryFactory.php b/lib/Service/Import/ImportRepositoryFactory.php index 985edae..32a4999 100644 --- a/lib/Service/Import/ImportRepositoryFactory.php +++ b/lib/Service/Import/ImportRepositoryFactory.php @@ -2,15 +2,6 @@ namespace MintyPHP\Service\Import; -use MintyPHP\Repository\Audit\ImportAuditRunRepository; -use MintyPHP\Repository\Audit\ImportAuditRunRepositoryInterface; - class ImportRepositoryFactory { - private ?ImportAuditRunRepository $importAuditRunRepository = null; - - public function createImportAuditRunRepository(): ImportAuditRunRepositoryInterface - { - return $this->importAuditRunRepository ??= new ImportAuditRunRepository(); - } } diff --git a/lib/Service/Import/ImportService.php b/lib/Service/Import/ImportService.php index 875f006..f9a2da0 100644 --- a/lib/Service/Import/ImportService.php +++ b/lib/Service/Import/ImportService.php @@ -4,7 +4,7 @@ namespace MintyPHP\Service\Import; use MintyPHP\Http\SessionStoreInterface; use MintyPHP\Service\Access\PermissionService; -use MintyPHP\Service\Audit\ImportAuditService; +use MintyPHP\Service\Audit\ImportAuditInterface; use MintyPHP\Service\I18n\TranslatesServiceText; use MintyPHP\Service\Import\Profile\ImportProfileInterface; use MintyPHP\Service\Settings\SettingsDefaultsGateway; @@ -44,7 +44,7 @@ class ImportService public function __construct( private readonly CsvReaderService $csvReaderService, private readonly ImportTempFileService $importTempFileService, - private readonly ImportAuditService $importAuditService, + private readonly ImportAuditInterface $importAuditService, private readonly ImportStateStoreService $importStateStoreService, private readonly array $profiles, private readonly PermissionService $permissionService, diff --git a/lib/Service/Import/ImportServicesFactory.php b/lib/Service/Import/ImportServicesFactory.php index 5f6351d..e388e0a 100644 --- a/lib/Service/Import/ImportServicesFactory.php +++ b/lib/Service/Import/ImportServicesFactory.php @@ -3,10 +3,9 @@ namespace MintyPHP\Service\Import; use MintyPHP\Http\SessionStoreInterface; -use MintyPHP\Repository\Audit\ImportAuditRunRepositoryInterface; use MintyPHP\Service\Access\AccessServicesFactory; use MintyPHP\Service\Access\PermissionService; -use MintyPHP\Service\Audit\ImportAuditService; +use MintyPHP\Service\Audit\ImportAuditInterface; use MintyPHP\Service\Directory\DirectoryServicesFactory; use MintyPHP\Service\Import\Profile\DepartmentImportGateway; use MintyPHP\Service\Import\Profile\DepartmentImportProfile; @@ -22,7 +21,6 @@ class ImportServicesFactory private ?CsvReaderService $csvReaderService = null; private ?ImportTempFileService $importTempFileService = null; private ?ImportStateStoreService $importStateStoreService = null; - private ?ImportAuditService $importAuditService = null; private ?PermissionService $permissionService = null; private ?SettingsDefaultsGateway $settingsDefaultsGateway = null; private ?ImportService $importService = null; @@ -32,7 +30,7 @@ class ImportServicesFactory private readonly AccessServicesFactory $accessServicesFactory, private readonly SettingServicesFactory $settingServicesFactory, private readonly DirectoryServicesFactory $directoryServicesFactory, - private readonly ImportRepositoryFactory $importRepositoryFactory, + private readonly ImportAuditInterface $importAudit, private readonly TenantScopeService $tenantScopeService, private readonly SessionStoreInterface $sessionStore ) { @@ -66,7 +64,7 @@ class ImportServicesFactory return $this->importService = new ImportService( $this->getCsvReaderService(), $this->getImportTempFileService(), - $this->createImportAuditService(), + $this->importAudit, $this->createImportStateStoreService(), $profiles, $this->createPermissionService(), @@ -76,11 +74,6 @@ class ImportServicesFactory ); } - public function createImportAuditService(): ImportAuditService - { - return $this->importAuditService ??= new ImportAuditService($this->getImportAuditRunRepository()); - } - public function createImportStateStoreService(): ImportStateStoreService { return $this->importStateStoreService ??= new ImportStateStoreService( @@ -99,11 +92,6 @@ class ImportServicesFactory return $this->importTempFileService ??= new ImportTempFileService(); } - private function getImportAuditRunRepository(): ImportAuditRunRepositoryInterface - { - return $this->importRepositoryFactory->createImportAuditRunRepository(); - } - private function createPermissionService(): PermissionService { return $this->permissionService ??= $this->accessServicesFactory->createPermissionService(); diff --git a/lib/Service/Org/DepartmentService.php b/lib/Service/Org/DepartmentService.php index 2ecbbfd..e5e77de 100644 --- a/lib/Service/Org/DepartmentService.php +++ b/lib/Service/Org/DepartmentService.php @@ -3,7 +3,7 @@ namespace MintyPHP\Service\Org; use MintyPHP\Repository\Org\DepartmentRepositoryInterface; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Directory\DirectorySettingsGateway; use MintyPHP\Service\Tenant\TenantScopeService; use MintyPHP\Service\User\UserServicesFactory; @@ -15,7 +15,7 @@ class DepartmentService private readonly DepartmentRepositoryInterface $departmentRepository, private readonly DirectorySettingsGateway $settingsGateway, private readonly TenantScopeService $scopeGateway, - private readonly SystemAuditService $systemAuditService + private readonly AuditRecorderInterface $systemAuditService ) { } diff --git a/lib/Service/Scheduler/ScheduledJobRegistry.php b/lib/Service/Scheduler/ScheduledJobRegistry.php index 6448cac..927f3a1 100644 --- a/lib/Service/Scheduler/ScheduledJobRegistry.php +++ b/lib/Service/Scheduler/ScheduledJobRegistry.php @@ -4,9 +4,7 @@ namespace MintyPHP\Service\Scheduler; use MintyPHP\App\AppContainer; use MintyPHP\App\Module\ModuleRegistry; -use MintyPHP\Service\Audit\SystemAuditService; use MintyPHP\Service\Scheduler\Handler\ScheduledJobHandlerInterface; -use MintyPHP\Service\Scheduler\Handler\SystemAuditPurgeJobHandler; use MintyPHP\Service\Scheduler\Handler\UserLifecycleJobHandler; use MintyPHP\Service\User\UserLifecycleService; use RuntimeException; @@ -14,7 +12,6 @@ use RuntimeException; class ScheduledJobRegistry { public const USER_LIFECYCLE_RUN = 'user_lifecycle_run'; - public const SYSTEM_AUDIT_PURGE = 'system_audit_purge'; /** @var array */ private array $handlers; @@ -38,12 +35,10 @@ class ScheduledJobRegistry public function __construct( UserLifecycleService $userLifecycleService, - SystemAuditService $systemAuditService, private readonly AppContainer $container ) { $this->handlers = [ self::USER_LIFECYCLE_RUN => new UserLifecycleJobHandler($userLifecycleService), - self::SYSTEM_AUDIT_PURGE => new SystemAuditPurgeJobHandler($systemAuditService), ]; $this->loadModuleJobs(); diff --git a/lib/Service/Scheduler/SchedulerRunService.php b/lib/Service/Scheduler/SchedulerRunService.php index 1d37a2c..daea0f8 100644 --- a/lib/Service/Scheduler/SchedulerRunService.php +++ b/lib/Service/Scheduler/SchedulerRunService.php @@ -5,13 +5,12 @@ namespace MintyPHP\Service\Scheduler; use MintyPHP\Domain\Taxonomy\ScheduledJobRunStatus; use MintyPHP\Domain\Taxonomy\ScheduledJobTriggerType; use MintyPHP\Domain\Taxonomy\SchedulerRuntimeResult; -use MintyPHP\Domain\Taxonomy\SystemAuditOutcome; use MintyPHP\Repository\Scheduler\ScheduledJobRepositoryInterface; use MintyPHP\Repository\Scheduler\ScheduledJobRunRepositoryInterface; use MintyPHP\Repository\Scheduler\SchedulerRuntimeRepositoryInterface; use MintyPHP\Repository\Support\DatabaseSessionRepository; use MintyPHP\Repository\Support\RepoQuery; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; /** * Orchestrates the execution of scheduled jobs. @@ -33,7 +32,7 @@ class SchedulerRunService private readonly ScheduledJobRegistry $scheduledJobRegistry, private readonly ScheduleCalculator $scheduleCalculator, private readonly DatabaseSessionRepository $databaseSessionRepository, - private readonly SystemAuditService $systemAuditService + private readonly AuditRecorderInterface $systemAuditService ) { } @@ -423,13 +422,13 @@ class SchedulerRunService private function auditOutcomeForRunStatus(string $status): string { if ($status === ScheduledJobRunStatus::Success->value) { - return SystemAuditOutcome::Success->value; + return 'success'; } if ($status === ScheduledJobRunStatus::Failed->value) { - return SystemAuditOutcome::Failed->value; + return 'failed'; } // "skipped" is an operational non-error state (e.g. already running), // not an authorization denial. - return SystemAuditOutcome::Success->value; + return 'success'; } } diff --git a/lib/Service/Scheduler/SchedulerServicesFactory.php b/lib/Service/Scheduler/SchedulerServicesFactory.php index 4378ae2..6420155 100644 --- a/lib/Service/Scheduler/SchedulerServicesFactory.php +++ b/lib/Service/Scheduler/SchedulerServicesFactory.php @@ -7,8 +7,7 @@ use MintyPHP\Repository\Scheduler\ScheduledJobRepositoryInterface; use MintyPHP\Repository\Scheduler\ScheduledJobRunRepositoryInterface; use MintyPHP\Repository\Scheduler\SchedulerRuntimeRepositoryInterface; use MintyPHP\Repository\Support\DatabaseSessionRepository; -use MintyPHP\Service\Audit\AuditServicesFactory; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\User\UserLifecycleService; use MintyPHP\Service\User\UserServicesFactory; @@ -21,7 +20,7 @@ class SchedulerServicesFactory public function __construct( private readonly UserServicesFactory $userServicesFactory, - private readonly AuditServicesFactory $auditServicesFactory, + private readonly AuditRecorderInterface $auditRecorder, private readonly DatabaseSessionRepository $databaseSessionRepository, private readonly SchedulerRepositoryFactory $schedulerRepositoryFactory, private readonly AppContainer $appContainer @@ -48,7 +47,7 @@ class SchedulerServicesFactory $this->getScheduledJobRegistry(), $this->getScheduleCalculator(), $this->getDatabaseSessionRepository(), - $this->getSystemAuditService() + $this->getAuditRecorder() ); } @@ -86,7 +85,6 @@ class SchedulerServicesFactory { return $this->scheduledJobRegistry ??= new ScheduledJobRegistry( $this->getUserLifecycleService(), - $this->getSystemAuditService(), $this->appContainer ); } @@ -96,9 +94,9 @@ class SchedulerServicesFactory return $this->databaseSessionRepository; } - private function getSystemAuditService(): SystemAuditService + private function getAuditRecorder(): AuditRecorderInterface { - return $this->auditServicesFactory->createSystemAuditService(); + return $this->auditRecorder; } } diff --git a/lib/Service/Settings/AdminSettingsService.php b/lib/Service/Settings/AdminSettingsService.php index c2d71ed..13bebd1 100644 --- a/lib/Service/Settings/AdminSettingsService.php +++ b/lib/Service/Settings/AdminSettingsService.php @@ -5,7 +5,7 @@ namespace MintyPHP\Service\Settings; use MintyPHP\Repository\Auth\ApiTokenRepository; use MintyPHP\Repository\Auth\RememberTokenRepository; use MintyPHP\Service\Access\RoleService; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Org\DepartmentService; use MintyPHP\Service\Tenant\TenantService; @@ -29,7 +29,7 @@ class AdminSettingsService private readonly DepartmentService $departmentService, private readonly RememberTokenRepository $rememberTokenRepository, private readonly ApiTokenRepository $apiTokenRepository, - private readonly SystemAuditService $systemAuditService + private readonly AuditRecorderInterface $systemAuditService ) { } diff --git a/lib/Service/Tenant/TenantService.php b/lib/Service/Tenant/TenantService.php index dfc5e69..0b1f2ff 100644 --- a/lib/Service/Tenant/TenantService.php +++ b/lib/Service/Tenant/TenantService.php @@ -2,11 +2,10 @@ namespace MintyPHP\Service\Tenant; -use MintyPHP\Domain\Taxonomy\SystemAuditOutcome; use MintyPHP\Domain\Taxonomy\TenantStatus; use MintyPHP\Repository\Org\DepartmentRepositoryInterface; use MintyPHP\Repository\Tenant\TenantRepositoryInterface; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Directory\DirectorySettingsGateway; class TenantService @@ -15,7 +14,7 @@ class TenantService private readonly TenantRepositoryInterface $tenantRepository, private readonly DepartmentRepositoryInterface $departmentRepository, private readonly DirectorySettingsGateway $settingsGateway, - private readonly SystemAuditService $systemAuditService + private readonly AuditRecorderInterface $systemAuditService ) { } @@ -91,7 +90,7 @@ class TenantService $this->settingsGateway->setDefaultTenantId((int) $createdId); } - $this->systemAuditService->record('admin.tenants.create', SystemAuditOutcome::Success->value, [ + $this->systemAuditService->record('admin.tenants.create', 'success', [ 'actor_user_id' => $currentUserId > 0 ? $currentUserId : null, 'target_type' => 'tenant', 'target_id' => (int) $createdId, @@ -155,7 +154,7 @@ class TenantService return ['ok' => false, 'errors' => [t('Tenant can not be updated')], 'form' => $form]; } - $this->systemAuditService->record('admin.tenants.update', SystemAuditOutcome::Success->value, [ + $this->systemAuditService->record('admin.tenants.update', 'success', [ 'actor_user_id' => $currentUserId > 0 ? $currentUserId : null, 'target_type' => 'tenant', 'target_id' => $tenantId, @@ -194,7 +193,7 @@ class TenantService return ['ok' => false, 'status' => 500, 'error' => 'delete_failed']; } - $this->systemAuditService->record('admin.tenants.delete', SystemAuditOutcome::Success->value, [ + $this->systemAuditService->record('admin.tenants.delete', 'success', [ 'target_type' => 'tenant', 'target_id' => $tenantId, 'target_uuid' => (string) ($tenant['uuid'] ?? ''), diff --git a/lib/Service/User/UserAccountService.php b/lib/Service/User/UserAccountService.php index e335818..a3cc6ff 100644 --- a/lib/Service/User/UserAccountService.php +++ b/lib/Service/User/UserAccountService.php @@ -8,7 +8,7 @@ use MintyPHP\Repository\Support\DatabaseSessionRepository; use MintyPHP\Repository\User\UserListQueryRepositoryInterface; use MintyPHP\Repository\User\UserReadRepositoryInterface; use MintyPHP\Repository\User\UserWriteRepositoryInterface; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Tenant\TenantScopeService; /** @@ -36,7 +36,7 @@ class UserAccountService private readonly UserSettingsGateway $settingsGateway, private readonly TenantScopeService $scopeGateway, private readonly UserDirectoryGateway $directoryGateway, - private readonly SystemAuditService $systemAuditService, + private readonly AuditRecorderInterface $systemAuditService, private readonly DatabaseSessionRepository $databaseSessionRepository, private readonly ?ModuleEventDispatcher $eventDispatcher = null ) { diff --git a/lib/Service/User/UserLifecycleRestoreService.php b/lib/Service/User/UserLifecycleRestoreService.php index 1d9a715..04639bb 100644 --- a/lib/Service/User/UserLifecycleRestoreService.php +++ b/lib/Service/User/UserLifecycleRestoreService.php @@ -6,14 +6,14 @@ use MintyPHP\Repository\Support\DatabaseSessionRepository; use MintyPHP\Repository\Support\RepoQuery; use MintyPHP\Repository\User\UserReadRepositoryInterface; use MintyPHP\Repository\User\UserWriteRepositoryInterface; -use MintyPHP\Service\Audit\UserLifecycleAuditService; +use MintyPHP\Service\Audit\UserLifecycleAuditInterface; class UserLifecycleRestoreService { public function __construct( private readonly UserReadRepositoryInterface $userReadRepository, private readonly UserWriteRepositoryInterface $userWriteRepository, - private readonly UserLifecycleAuditService $userLifecycleAuditService, + private readonly UserLifecycleAuditInterface $userLifecycleAuditService, private readonly DatabaseSessionRepository $databaseSessionRepository, private readonly UserSettingsGateway $userSettingsGateway ) { diff --git a/lib/Service/User/UserLifecycleService.php b/lib/Service/User/UserLifecycleService.php index da00f5f..20bf0fc 100644 --- a/lib/Service/User/UserLifecycleService.php +++ b/lib/Service/User/UserLifecycleService.php @@ -7,7 +7,7 @@ use MintyPHP\Repository\Support\RepoQuery; use MintyPHP\Repository\User\UserReadRepositoryInterface; use MintyPHP\Repository\User\UserWriteRepositoryInterface; use MintyPHP\Service\Access\PermissionService; -use MintyPHP\Service\Audit\UserLifecycleAuditService; +use MintyPHP\Service\Audit\UserLifecycleAuditInterface; class UserLifecycleService { @@ -18,7 +18,7 @@ class UserLifecycleService private readonly UserReadRepositoryInterface $userReadRepository, private readonly UserWriteRepositoryInterface $userWriteRepository, private readonly UserSettingsGateway $settingsGateway, - private readonly UserLifecycleAuditService $userLifecycleAuditService, + private readonly UserLifecycleAuditInterface $userLifecycleAuditService, private readonly DatabaseSessionRepository $databaseSessionRepository ) { } diff --git a/lib/Service/User/UserServicesFactory.php b/lib/Service/User/UserServicesFactory.php index 8711255..cc24a3b 100644 --- a/lib/Service/User/UserServicesFactory.php +++ b/lib/Service/User/UserServicesFactory.php @@ -11,8 +11,8 @@ use MintyPHP\Repository\User\UserListQueryRepositoryInterface; use MintyPHP\Repository\User\UserReadRepositoryInterface; use MintyPHP\Repository\User\UserWriteRepositoryInterface; use MintyPHP\Service\Access\AssignableRoleService; -use MintyPHP\Service\Audit\AuditServicesFactory; -use MintyPHP\Service\Audit\UserLifecycleAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; +use MintyPHP\Service\Audit\UserLifecycleAuditInterface; class UserServicesFactory { @@ -24,13 +24,13 @@ class UserServicesFactory private ?UserAccountService $userAccountService = null; private ?UserLifecycleService $userLifecycleService = null; private ?UserLifecycleRestoreService $userLifecycleRestoreService = null; - private ?UserLifecycleAuditService $userLifecycleAuditService = null; /** * @param \Closure(): AssignableRoleService $assignableRoleServiceFactory Lazy resolver to break circular dependency */ public function __construct( - private readonly AuditServicesFactory $auditServicesFactory, + private readonly AuditRecorderInterface $auditRecorder, + private readonly UserLifecycleAuditInterface $userLifecycleAudit, private readonly UserRepositoryFactory $userRepositoryFactory, private readonly UserGatewayFactory $userGatewayFactory, private readonly DatabaseSessionRepository $databaseSessionRepository, @@ -50,7 +50,7 @@ class UserServicesFactory $this->createUserSettingsGateway(), $this->userGatewayFactory->getTenantScopeService(), $this->createUserDirectoryGateway(), - $this->auditServicesFactory->createSystemAuditService(), + $this->auditRecorder, $this->databaseSessionRepository, $this->eventDispatcher ); @@ -145,7 +145,7 @@ class UserServicesFactory $this->createUserReadRepository(), $this->createUserWriteRepository(), $this->createUserSettingsGateway(), - $this->createUserLifecycleAuditService(), + $this->userLifecycleAudit, $this->databaseSessionRepository ); } @@ -155,14 +155,9 @@ class UserServicesFactory return $this->userLifecycleRestoreService ??= new UserLifecycleRestoreService( $this->createUserReadRepository(), $this->createUserWriteRepository(), - $this->createUserLifecycleAuditService(), + $this->userLifecycleAudit, $this->databaseSessionRepository, $this->createUserSettingsGateway() ); } - - private function createUserLifecycleAuditService(): UserLifecycleAuditService - { - return $this->userLifecycleAuditService ??= $this->auditServicesFactory->createUserLifecycleAuditService(); - } } diff --git a/lib/Support/Search/SearchSqlResourceProvider.php b/lib/Support/Search/SearchSqlResourceProvider.php index 43c94f3..a621cf2 100644 --- a/lib/Support/Search/SearchSqlResourceProvider.php +++ b/lib/Support/Search/SearchSqlResourceProvider.php @@ -113,7 +113,7 @@ class SearchSqlResourceProvider [ 'key' => 'api-audit', 'label' => t('API audit'), - 'permission' => PermissionService::API_AUDIT_VIEW, + 'permission' => 'api_audit.view', 'countSql' => "select count(*) from api_audit_log where (request_id like ? escape '\\\\' or path like ? escape '\\\\' or error_code like ? escape '\\\\' or ip like ? escape '\\\\')", 'countParams' => [$like, $like, $like, $like], 'previewSql' => "select id, request_id, method, path, status_code, created_at from api_audit_log where (request_id like ? escape '\\\\' or path like ? escape '\\\\' or error_code like ? escape '\\\\' or ip like ? escape '\\\\') order by created_at desc limit ?", @@ -124,7 +124,7 @@ class SearchSqlResourceProvider [ 'key' => 'system-audit', 'label' => t('System audit'), - 'permission' => PermissionService::SYSTEM_AUDIT_VIEW, + 'permission' => 'system_audit.view', 'countSql' => "select count(*) from system_audit_log where (request_id like ? escape '\\\\' or event_type like ? escape '\\\\' or error_code like ? escape '\\\\' or path like ? escape '\\\\')", 'countParams' => [$like, $like, $like, $like], 'previewSql' => "select id, request_id, event_type, outcome, created_at from system_audit_log where (request_id like ? escape '\\\\' or event_type like ? escape '\\\\' or error_code like ? escape '\\\\' or path like ? escape '\\\\') order by created_at desc limit ?", diff --git a/lib/Support/helpers/app.php b/lib/Support/helpers/app.php index 542d22e..8910659 100644 --- a/lib/Support/helpers/app.php +++ b/lib/Support/helpers/app.php @@ -41,16 +41,24 @@ function setAppContainer(\MintyPHP\App\AppContainer $container): void ); \MintyPHP\Http\ApiBootstrap::configure( - static fn (): \MintyPHP\Service\Audit\ApiAuditService => $container->get(\MintyPHP\Service\Audit\ApiAuditService::class), + $container->has(\MintyPHP\Module\Audit\Service\ApiAuditService::class) + ? static fn () => $container->get(\MintyPHP\Module\Audit\Service\ApiAuditService::class) + : null, static fn (): \MintyPHP\Service\Settings\SettingsApiPolicyGateway => $container->get(\MintyPHP\Service\Settings\SettingsApiPolicyGateway::class), static fn (): \MintyPHP\Service\Security\RateLimiterService => $container->get(\MintyPHP\Service\Security\RateLimiterService::class), - static fn (): \MintyPHP\Http\ApiSystemAuditReporter => $container->get(\MintyPHP\Http\ApiSystemAuditReporter::class) + $container->has(\MintyPHP\Module\Audit\Http\ApiSystemAuditReporter::class) + ? static fn () => $container->get(\MintyPHP\Module\Audit\Http\ApiSystemAuditReporter::class) + : null ); \MintyPHP\Http\ApiResponse::configure( - static fn (): \MintyPHP\Service\Audit\ApiAuditService => $container->get(\MintyPHP\Service\Audit\ApiAuditService::class), + $container->has(\MintyPHP\Module\Audit\Service\ApiAuditService::class) + ? static fn () => $container->get(\MintyPHP\Module\Audit\Service\ApiAuditService::class) + : null, static fn (): \MintyPHP\Service\Access\AuthorizationService => $container->get(\MintyPHP\Service\Access\AuthorizationService::class), - static fn (): \MintyPHP\Http\ApiSystemAuditReporter => $container->get(\MintyPHP\Http\ApiSystemAuditReporter::class) + $container->has(\MintyPHP\Module\Audit\Http\ApiSystemAuditReporter::class) + ? static fn () => $container->get(\MintyPHP\Module\Audit\Http\ApiSystemAuditReporter::class) + : null ); \MintyPHP\Support\Guard::configure( diff --git a/modules/audit/db/updates/2026-03-25-audit-tables.sql b/modules/audit/db/updates/2026-03-25-audit-tables.sql new file mode 100644 index 0000000..8275c59 --- /dev/null +++ b/modules/audit/db/updates/2026-03-25-audit-tables.sql @@ -0,0 +1,97 @@ +-- Audit module: idempotent migration for existing audit tables. +-- These tables may already exist in deployments that had audit in core. +-- All statements use IF NOT EXISTS to be safe for both fresh installs and upgrades. + +CREATE TABLE IF NOT EXISTS `system_audit_log` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `event_uuid` CHAR(36) NOT NULL, + `request_id` CHAR(36) NULL, + `channel` VARCHAR(20) NOT NULL DEFAULT 'web', + `event_type` VARCHAR(64) NOT NULL, + `outcome` VARCHAR(20) NOT NULL DEFAULT 'success', + `error_code` VARCHAR(100) NULL, + `actor_user_id` INT UNSIGNED NULL, + `actor_tenant_id` INT UNSIGNED NULL, + `target_type` VARCHAR(64) NULL, + `target_id` INT UNSIGNED NULL, + `target_uuid` CHAR(36) NULL, + `method` VARCHAR(8) NULL, + `path` VARCHAR(255) NULL, + `ip_hash` CHAR(64) NULL, + `user_agent_hash` CHAR(64) NULL, + `metadata_json` JSON NULL, + `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_sal_event_type_created` (`event_type`, `created` DESC), + KEY `idx_sal_actor_user_id` (`actor_user_id`), + KEY `idx_sal_created` (`created` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `api_audit_log` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `request_id` CHAR(36) NOT NULL, + `method` VARCHAR(8) NOT NULL, + `path` VARCHAR(255) NOT NULL, + `query_json` JSON NULL, + `status_code` SMALLINT UNSIGNED NOT NULL, + `duration_ms` INT UNSIGNED NULL, + `error_code` VARCHAR(100) NULL, + `user_id` INT UNSIGNED NULL, + `tenant_id` INT UNSIGNED NULL, + `api_token_id` INT UNSIGNED NULL, + `token_tenant_id` INT UNSIGNED NULL, + `ip` VARCHAR(45) NULL, + `user_agent` VARCHAR(255) NULL, + `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_aal_created` (`created` DESC), + KEY `idx_aal_path_created` (`path`(64), `created` DESC), + KEY `idx_aal_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `import_audit_runs` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `run_uuid` CHAR(36) NOT NULL, + `profile_key` VARCHAR(64) NOT NULL, + `status` VARCHAR(20) NOT NULL DEFAULT 'running', + `source_filename` VARCHAR(255) NULL, + `mapped_targets_csv` VARCHAR(500) NULL, + `rows_total` INT UNSIGNED NOT NULL DEFAULT 0, + `created_count` INT UNSIGNED NOT NULL DEFAULT 0, + `skipped_count` INT UNSIGNED NOT NULL DEFAULT 0, + `failed_count` INT UNSIGNED NOT NULL DEFAULT 0, + `error_codes_json` JSON NULL, + `duration_ms` INT UNSIGNED NULL, + `user_id` INT UNSIGNED NULL, + `current_tenant_id` INT UNSIGNED NULL, + `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `finished_at` DATETIME NULL, + PRIMARY KEY (`id`), + KEY `idx_iar_created` (`created` DESC), + KEY `idx_iar_profile_key` (`profile_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `user_lifecycle_audit_log` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `run_uuid` CHAR(36) NOT NULL, + `action` VARCHAR(20) NOT NULL, + `trigger_type` VARCHAR(20) NOT NULL, + `status` VARCHAR(20) NOT NULL DEFAULT 'success', + `reason_code` VARCHAR(100) NULL, + `policy_deactivate_days` INT UNSIGNED NOT NULL DEFAULT 0, + `policy_delete_days` INT UNSIGNED NOT NULL DEFAULT 0, + `actor_user_id` INT UNSIGNED NULL, + `target_user_id` INT UNSIGNED NULL, + `target_user_uuid` CHAR(36) NULL, + `target_user_email` VARCHAR(255) NULL, + `snapshot_enc` MEDIUMTEXT NULL, + `snapshot_version` TINYINT UNSIGNED NOT NULL DEFAULT 1, + `restored_at` DATETIME NULL, + `restored_by_user_id` INT UNSIGNED NULL, + `restored_user_id` INT UNSIGNED NULL, + `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_ulal_action_created` (`action`, `created` DESC), + KEY `idx_ulal_target_user_id` (`target_user_id`), + KEY `idx_ulal_created` (`created` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/modules/audit/i18n/default_de.json b/modules/audit/i18n/default_de.json new file mode 100644 index 0000000..29323da --- /dev/null +++ b/modules/audit/i18n/default_de.json @@ -0,0 +1,71 @@ +{ + "Telemetry helps detect recurring UI issues and failed requests in production.": "Telemetry hilft, wiederkehrende UI-Probleme und fehlgeschlagene Requests in Produktion zu erkennen.", + "perm.user_lifecycle_audit.view": "Benutzer-Lifecycle-Protokolle anzeigen", + "perm.users.lifecycle_restore": "Benutzer aus Lifecycle-Protokoll wiederherstellen", + "User lifecycle": "Benutzer-Lebenszyklus", + "Audit": "Audit", + "API audit": "API-Protokolle", + "API audit logs": "API-Protokolle", + "Purge API audit logs": "API-Protokolle bereinigen", + "Purge entries older than 90 days?": "Einträge älter als 90 Tage bereinigen?", + "%d API audit entries purged": "%d API-Protokoll-Einträge bereinigt", + "API audit entry not found": "API-Protokoll-Eintrag nicht gefunden", + "View API audit entry": "API-Protokoll-Eintrag anzeigen", + "Import audit logs": "Import-Protokolle", + "Can view import audit logs": "Kann Import-Protokolle anzeigen", + "Purge import logs": "Import-Protokolle bereinigen", + "%d import audit entries purged": "%d Import-Protokoll-Einträge bereinigt", + "Import audit entry not found": "Import-Protokoll-Eintrag nicht gefunden", + "View import audit entry": "Import-Protokoll-Eintrag anzeigen", + "User lifecycle policy": "Benutzer-Lifecycle-Regel", + "User lifecycle defines when inactive users are deactivated or deleted.": "Hier wird festgelegt, wann inaktive Benutzer deaktiviert oder gelöscht werden.", + "System audit controls whether security events are logged and how long they are kept.": "Hier wird gesteuert, ob Sicherheitsereignisse protokolliert werden und wie lange sie aufbewahrt werden.", + "Privileged admins are excluded from automatic lifecycle actions": "Privilegierte Admins sind von automatischen Lifecycle-Aktionen ausgenommen", + "Run user lifecycle now?": "Benutzer-Lifecycle jetzt ausführen?", + "Run user lifecycle now": "Benutzer-Lifecycle jetzt ausführen", + "User lifecycle already running": "Benutzer-Lifecycle läuft bereits", + "User lifecycle failed": "Benutzer-Lifecycle fehlgeschlagen", + "User lifecycle completed: %d deactivated, %d deleted": "Benutzer-Lifecycle abgeschlossen: %d deaktiviert, %d gelöscht", + "Purge scheduler logs": "Scheduler-Protokolle bereinigen", + "Purge run logs older than 90 days?": "Laufprotokolle älter als 90 Tage bereinigen?", + "%d scheduler run logs purged": "%d Scheduler-Laufprotokolle bereinigt", + "User lifecycle logs": "Benutzer-Lifecycle-Protokolle", + "View user lifecycle audit entry": "Benutzer-Lifecycle-Eintrag anzeigen", + "Lifecycle audit entry not found": "Benutzer-Lifecycle-Eintrag nicht gefunden", + "Purge user lifecycle logs": "Benutzer-Lifecycle-Protokolle bereinigen", + "Purge entries older than 365 days?": "Einträge älter als 365 Tage bereinigen?", + "%d user lifecycle audit entries purged": "%d Benutzer-Lifecycle-Protokoll-Einträge bereinigt", + "Lifecycle event": "Lifecycle-Ereignis", + "Restore this user from lifecycle snapshot?": "Diesen Benutzer aus dem Lifecycle-Snapshot wiederherstellen?", + "Lifecycle event was already restored": "Lifecycle-Ereignis wurde bereits wiederhergestellt", + "Lifecycle snapshot unavailable": "Lifecycle-Snapshot nicht verfügbar", + "System audit": "System-Protokolle", + "System audit logs": "System-Protokolle", + "View system audit entry": "System-Protokoll-Eintrag anzeigen", + "System audit entry not found": "System-Protokoll-Eintrag nicht gefunden", + "Enable system audit log": "System-Protokolle aktivieren", + "Purge system audit logs": "System-Protokolle bereinigen", + "Purge expired system audit entries?": "Abgelaufene System-Protokoll-Einträge bereinigen?", + "%d system audit entries purged": "%d System-Protokoll-Einträge bereinigt", + "Frontend telemetry": "Frontend-Telemetrie", + "Enable frontend telemetry": "Frontend-Telemetrie aktivieren", + "Allowed telemetry events": "Erlaubte Telemetrie-Ereignisse", + "Telemetry": "Telemetrie", + "Telemetry helps us find UI problems faster.": "Telemetrie hilft uns, UI-Probleme schneller zu finden.", + "The percentage of users who will have telemetry enabled.": "Der Anteil der Nutzer, für die Telemetrie aktiv ist.", + "Advanced telemetry options": "Erweiterte Telemetrie-Optionen", + "setting.system_audit_enabled": "System-Protokolle aktivieren/deaktivieren", + "setting.system_audit_retention_days": "Aufbewahrungsdauer in Tagen für System-Protokoll-Einträge", + "setting.frontend_telemetry_enabled": "Sichere Frontend-UX- und Fehler-Telemetrie aktivieren/deaktivieren", + "setting.frontend_telemetry_sample_rate": "Sampling-Rate zwischen 0 und 1 für akzeptierte Frontend-Telemetrie-Ereignisse", + "setting.frontend_telemetry_allowed_events": "Erlaubte Frontend-Telemetrie-Ereignisgruppen (warn_once, ajax_error)", + "System audit events (24h)": "System-Protokoll-Ereignisse (24h)", + "System audit risks (24h)": "System-Protokoll-Risiken (24h)", + "Top system audit event types (24h)": "Top-System-Protokoll-Ereignistypen (24h)", + "Recent system audit risks (24h)": "Aktuelle System-Protokoll-Risiken (24h)", + "User lifecycle audit": "Benutzer-Lifecycle-Protokoll", + "Lifecycle runs (7d)": "Lifecycle-Läufe (7d)", + "Lifecycle risks (7d)": "Lifecycle-Risiken (7d)", + "Top lifecycle reason codes (7d)": "Top-Lifecycle-Fehlercodes (7d)", + "Recent lifecycle risks (7d)": "Aktuelle Lifecycle-Risiken (7d)" +} diff --git a/modules/audit/i18n/default_en.json b/modules/audit/i18n/default_en.json new file mode 100644 index 0000000..aaaf994 --- /dev/null +++ b/modules/audit/i18n/default_en.json @@ -0,0 +1,71 @@ +{ + "Telemetry helps detect recurring UI issues and failed requests in production.": "Telemetry helps detect recurring UI issues and failed requests in production.", + "perm.user_lifecycle_audit.view": "View user lifecycle logs", + "perm.users.lifecycle_restore": "Restore users from lifecycle audit", + "User lifecycle": "User lifecycle", + "Audit": "Audit", + "API audit": "API logs", + "API audit logs": "API logs", + "Purge API audit logs": "Purge API logs", + "Purge entries older than 90 days?": "Purge entries older than 90 days?", + "%d API audit entries purged": "%d API log entries purged", + "API audit entry not found": "API log entry not found", + "View API audit entry": "View API log entry", + "Import audit logs": "Import audit logs", + "Can view import audit logs": "Can view import audit logs", + "Purge import logs": "Purge import logs", + "%d import audit entries purged": "%d import audit entries purged", + "Import audit entry not found": "Import audit entry not found", + "View import audit entry": "View import audit entry", + "User lifecycle policy": "User lifecycle policy", + "User lifecycle defines when inactive users are deactivated or deleted.": "User lifecycle defines when inactive users are deactivated or deleted.", + "System audit controls whether security events are logged and how long they are kept.": "System audit controls whether security events are logged and how long they are kept.", + "Privileged admins are excluded from automatic lifecycle actions": "Privileged admins are excluded from automatic lifecycle actions", + "Run user lifecycle now?": "Run user lifecycle now?", + "Run user lifecycle now": "Run user lifecycle now", + "User lifecycle already running": "User lifecycle already running", + "User lifecycle failed": "User lifecycle failed", + "User lifecycle completed: %d deactivated, %d deleted": "User lifecycle completed: %d deactivated, %d deleted", + "Purge scheduler logs": "Purge scheduler logs", + "Purge run logs older than 90 days?": "Purge run logs older than 90 days?", + "%d scheduler run logs purged": "%d scheduler run logs purged", + "User lifecycle logs": "User lifecycle logs", + "View user lifecycle audit entry": "View user lifecycle audit entry", + "Lifecycle audit entry not found": "Lifecycle audit entry not found", + "Purge user lifecycle logs": "Purge user lifecycle logs", + "Purge entries older than 365 days?": "Purge entries older than 365 days?", + "%d user lifecycle audit entries purged": "%d user lifecycle audit entries purged", + "Lifecycle event": "Lifecycle event", + "Restore this user from lifecycle snapshot?": "Restore this user from lifecycle snapshot?", + "Lifecycle event was already restored": "Lifecycle event was already restored", + "Lifecycle snapshot unavailable": "Lifecycle snapshot unavailable", + "System audit": "System audit", + "System audit logs": "System audit logs", + "View system audit entry": "View system audit entry", + "System audit entry not found": "System audit entry not found", + "Enable system audit log": "Enable system audit log", + "Purge system audit logs": "Purge system audit logs", + "Purge expired system audit entries?": "Purge expired system audit entries?", + "%d system audit entries purged": "%d system audit entries purged", + "Frontend telemetry": "Frontend telemetry", + "Enable frontend telemetry": "Enable frontend telemetry", + "Allowed telemetry events": "Allowed telemetry events", + "Telemetry": "Telemetry", + "Telemetry helps us find UI problems faster.": "Telemetry helps us find UI problems faster.", + "The percentage of users who will have telemetry enabled.": "The percentage of users who will have telemetry enabled.", + "Advanced telemetry options": "Advanced telemetry options", + "setting.system_audit_enabled": "Enable/disable system audit logging", + "setting.system_audit_retention_days": "Retention in days for system audit entries", + "setting.frontend_telemetry_enabled": "Enable/disable secure frontend UX and error telemetry", + "setting.frontend_telemetry_sample_rate": "Sampling rate between 0 and 1 for accepted frontend telemetry events", + "setting.frontend_telemetry_allowed_events": "Allowed frontend telemetry event groups (warn_once, ajax_error)", + "System audit events (24h)": "System audit events (24h)", + "System audit risks (24h)": "System audit risks (24h)", + "Top system audit event types (24h)": "Top system audit event types (24h)", + "Recent system audit risks (24h)": "Recent system audit risks (24h)", + "User lifecycle audit": "User lifecycle audit", + "Lifecycle runs (7d)": "Lifecycle runs (7d)", + "Lifecycle risks (7d)": "Lifecycle risks (7d)", + "Top lifecycle reason codes (7d)": "Top lifecycle reason codes (7d)", + "Recent lifecycle risks (7d)": "Recent lifecycle risks (7d)" +} diff --git a/modules/audit/lib/Module/Audit/AuditAuthorizationPolicy.php b/modules/audit/lib/Module/Audit/AuditAuthorizationPolicy.php new file mode 100644 index 0000000..af46669 --- /dev/null +++ b/modules/audit/lib/Module/Audit/AuditAuthorizationPolicy.php @@ -0,0 +1,55 @@ + 'api_audit.view', + self::ABILITY_SYSTEM_AUDIT_VIEW => 'system_audit.view', + self::ABILITY_SYSTEM_AUDIT_PURGE => 'system_audit.purge', + self::ABILITY_IMPORTS_AUDIT_VIEW => 'imports.audit.view', + self::ABILITY_USER_LIFECYCLE_VIEW => 'user_lifecycle_audit.view', + self::ABILITY_USER_LIFECYCLE_RESTORE => 'users.lifecycle_restore', + ]; + + public function __construct( + private readonly PermissionService $permissionService + ) { + } + + public function supports(string $ability): bool + { + return isset(self::ABILITY_PERMISSION_MAP[$ability]); + } + + public function authorize(string $ability, array $context = []): AuthorizationDecision + { + $actorUserId = (int) ($context['actor_user_id'] ?? 0); + if ($actorUserId <= 0) { + return AuthorizationDecision::deny(403, 'forbidden'); + } + + $permissionKey = self::ABILITY_PERMISSION_MAP[$ability] ?? null; + if ($permissionKey === null) { + return AuthorizationDecision::deny(403, 'forbidden'); + } + + if (!$this->permissionService->userHas($actorUserId, $permissionKey)) { + return AuthorizationDecision::deny(403, 'forbidden'); + } + + return AuthorizationDecision::allow(); + } +} diff --git a/modules/audit/lib/Module/Audit/AuditContainerRegistrar.php b/modules/audit/lib/Module/Audit/AuditContainerRegistrar.php new file mode 100644 index 0000000..685ad5b --- /dev/null +++ b/modules/audit/lib/Module/Audit/AuditContainerRegistrar.php @@ -0,0 +1,100 @@ +set(AuditRepositoryFactory::class, static fn (): AuditRepositoryFactory => new AuditRepositoryFactory()); + + // Services factory + $container->set(AuditServicesFactory::class, static fn (AppContainer $c): AuditServicesFactory => new AuditServicesFactory( + $c->get(AuditRepositoryFactory::class), + $c->get(SettingServicesFactory::class)->createSettingsSystemAuditGateway(), + $c->get(RequestRuntimeInterface::class), + $c->get(SessionStoreInterface::class) + )); + + // Concrete audit services + $container->set(SystemAuditService::class, static fn (AppContainer $c): SystemAuditService => $c->get(AuditServicesFactory::class)->createSystemAuditService()); + $container->set(ApiAuditService::class, static fn (AppContainer $c): ApiAuditService => $c->get(AuditServicesFactory::class)->createApiAuditService()); + $container->set(UserLifecycleAuditService::class, static fn (AppContainer $c): UserLifecycleAuditService => $c->get(AuditServicesFactory::class)->createUserLifecycleAuditService()); + $container->set(ImportAuditService::class, static fn (AppContainer $c): ImportAuditService => new ImportAuditService( + $c->get(AuditRepositoryFactory::class)->createImportAuditRunRepository() + )); + + // Core interface bindings — override null implementations + $container->set(AuditRecorderInterface::class, static fn (AppContainer $c): AuditRecorderInterface => $c->get(SystemAuditService::class)); + $container->set(UserLifecycleAuditInterface::class, static fn (AppContainer $c): UserLifecycleAuditInterface => $c->get(UserLifecycleAuditService::class)); + $container->set(ImportAuditInterface::class, static fn (AppContainer $c): ImportAuditInterface => $c->get(ImportAuditService::class)); + + // Frontend telemetry + $container->set(FrontendTelemetryIngestService::class, static fn (AppContainer $c): FrontendTelemetryIngestService => new FrontendTelemetryIngestService( + $c->get(SystemAuditService::class), + $c->get(SettingsFrontendTelemetryGateway::class), + $c->get(RateLimiterService::class), + $c->get(SessionStoreInterface::class) + )); + + // API system audit reporter + $container->set(ApiSystemAuditReporter::class, static fn (AppContainer $c): ApiSystemAuditReporter => new ApiSystemAuditReporter( + $c->get(SystemAuditService::class) + )); + + // Metadata enricher + $container->set(AuditMetadataEnricher::class, static fn (AppContainer $c): AuditMetadataEnricher => new AuditMetadataEnricher( + $c->get(UserReadRepository::class) + )); + + // Authorization policy + $container->set(AuditAuthorizationPolicy::class, static fn (AppContainer $c): AuditAuthorizationPolicy => new AuditAuthorizationPolicy( + $c->get(PermissionService::class) + )); + + // Scheduler job handlers + $container->set(SystemAuditPurgeJobHandler::class, static fn (AppContainer $c): SystemAuditPurgeJobHandler => new SystemAuditPurgeJobHandler( + $c->get(SystemAuditService::class) + )); + $container->set(ApiAuditPurgeJobHandler::class, static fn (AppContainer $c): ApiAuditPurgeJobHandler => new ApiAuditPurgeJobHandler( + $c->get(ApiAuditService::class) + )); + $container->set(ImportAuditPurgeJobHandler::class, static fn (AppContainer $c): ImportAuditPurgeJobHandler => new ImportAuditPurgeJobHandler( + $c->get(ImportAuditService::class) + )); + $container->set(UserLifecycleAuditPurgeJobHandler::class, static fn (AppContainer $c): UserLifecycleAuditPurgeJobHandler => new UserLifecycleAuditPurgeJobHandler( + $c->get(UserLifecycleAuditService::class) + )); + + // Layout provider + $container->set(AuditLayoutProvider::class, static fn (AppContainer $c): AuditLayoutProvider => new AuditLayoutProvider()); + } +} diff --git a/lib/Domain/Taxonomy/ImportAuditStatus.php b/modules/audit/lib/Module/Audit/Domain/ImportAuditStatus.php similarity index 88% rename from lib/Domain/Taxonomy/ImportAuditStatus.php rename to modules/audit/lib/Module/Audit/Domain/ImportAuditStatus.php index 1dace8d..5d32fb3 100644 --- a/lib/Domain/Taxonomy/ImportAuditStatus.php +++ b/modules/audit/lib/Module/Audit/Domain/ImportAuditStatus.php @@ -1,6 +1,8 @@ 'API audit log cleanup', + 'description' => 'Purges API audit log entries older than 90 days', + 'default_enabled' => 1, + 'default_timezone' => 'UTC', + 'default_schedule_type' => 'daily', + 'default_schedule_interval' => 1, + 'default_schedule_time' => '03:15', + 'default_schedule_weekdays_csv' => null, + 'default_catchup_once' => 1, + 'allowed_schedule_types' => ['daily', 'weekly'], + ]; + } + + public function execute(?int $actorUserId): array + { + $deleted = $this->apiAuditService->purgeExpired(); + + return [ + 'status' => 'success', + 'error_code' => null, + 'error_message' => null, + 'result' => ['deleted_count' => $deleted], + ]; + } +} diff --git a/modules/audit/lib/Module/Audit/Handler/ImportAuditPurgeJobHandler.php b/modules/audit/lib/Module/Audit/Handler/ImportAuditPurgeJobHandler.php new file mode 100644 index 0000000..f2a3a4a --- /dev/null +++ b/modules/audit/lib/Module/Audit/Handler/ImportAuditPurgeJobHandler.php @@ -0,0 +1,41 @@ + 'Import audit log cleanup', + 'description' => 'Purges import audit run records older than 90 days', + 'default_enabled' => 1, + 'default_timezone' => 'UTC', + 'default_schedule_type' => 'daily', + 'default_schedule_interval' => 1, + 'default_schedule_time' => '03:30', + 'default_schedule_weekdays_csv' => null, + 'default_catchup_once' => 1, + 'allowed_schedule_types' => ['daily', 'weekly'], + ]; + } + + public function execute(?int $actorUserId): array + { + $deleted = $this->importAuditService->purgeExpired(); + + return [ + 'status' => 'success', + 'error_code' => null, + 'error_message' => null, + 'result' => ['deleted_count' => $deleted], + ]; + } +} diff --git a/lib/Service/Scheduler/Handler/SystemAuditPurgeJobHandler.php b/modules/audit/lib/Module/Audit/Handler/SystemAuditPurgeJobHandler.php similarity index 83% rename from lib/Service/Scheduler/Handler/SystemAuditPurgeJobHandler.php rename to modules/audit/lib/Module/Audit/Handler/SystemAuditPurgeJobHandler.php index 0bfcb4d..af46379 100644 --- a/lib/Service/Scheduler/Handler/SystemAuditPurgeJobHandler.php +++ b/modules/audit/lib/Module/Audit/Handler/SystemAuditPurgeJobHandler.php @@ -1,8 +1,9 @@ 'success', 'error_code' => null, 'error_message' => null, - 'result' => [ - 'deleted_count' => $deleted, - ], + 'result' => ['deleted_count' => $deleted], ]; } } diff --git a/modules/audit/lib/Module/Audit/Handler/UserLifecycleAuditPurgeJobHandler.php b/modules/audit/lib/Module/Audit/Handler/UserLifecycleAuditPurgeJobHandler.php new file mode 100644 index 0000000..080172d --- /dev/null +++ b/modules/audit/lib/Module/Audit/Handler/UserLifecycleAuditPurgeJobHandler.php @@ -0,0 +1,41 @@ + 'User lifecycle audit log cleanup', + 'description' => 'Purges user lifecycle audit entries older than 365 days', + 'default_enabled' => 1, + 'default_timezone' => 'UTC', + 'default_schedule_type' => 'daily', + 'default_schedule_interval' => 1, + 'default_schedule_time' => '03:45', + 'default_schedule_weekdays_csv' => null, + 'default_catchup_once' => 1, + 'allowed_schedule_types' => ['daily', 'weekly'], + ]; + } + + public function execute(?int $actorUserId): array + { + $deleted = $this->userLifecycleAuditService->purgeExpired(); + + return [ + 'status' => 'success', + 'error_code' => null, + 'error_message' => null, + 'result' => ['deleted_count' => $deleted], + ]; + } +} diff --git a/lib/Http/ApiSystemAuditReporter.php b/modules/audit/lib/Module/Audit/Http/ApiSystemAuditReporter.php similarity index 96% rename from lib/Http/ApiSystemAuditReporter.php rename to modules/audit/lib/Module/Audit/Http/ApiSystemAuditReporter.php index de666d2..0542089 100644 --- a/lib/Http/ApiSystemAuditReporter.php +++ b/modules/audit/lib/Module/Audit/Http/ApiSystemAuditReporter.php @@ -1,9 +1,11 @@ apiAuditLogRepository ??= new ApiAuditLogRepository(); + } + + public function createUserLifecycleAuditRepository(): UserLifecycleAuditRepositoryInterface + { + return $this->userLifecycleAuditRepository ??= new UserLifecycleAuditRepository(); + } + + public function createSystemAuditLogRepository(): SystemAuditLogRepositoryInterface + { + return $this->systemAuditLogRepository ??= new SystemAuditLogRepository(); + } + + public function createImportAuditRunRepository(): ImportAuditRunRepositoryInterface + { + return $this->importAuditRunRepository ??= new ImportAuditRunRepository(); + } +} diff --git a/lib/Service/Audit/AuditServicesFactory.php b/modules/audit/lib/Module/Audit/Service/AuditServicesFactory.php similarity index 90% rename from lib/Service/Audit/AuditServicesFactory.php rename to modules/audit/lib/Module/Audit/Service/AuditServicesFactory.php index 3bbd018..525b6bd 100644 --- a/lib/Service/Audit/AuditServicesFactory.php +++ b/modules/audit/lib/Module/Audit/Service/AuditServicesFactory.php @@ -1,12 +1,12 @@ 'audit', + 'version' => '1.0.0', + 'enabled_by_default' => true, + 'load_order' => 5, + 'requires' => [], + + 'routes' => [ + // API audit pages + ['path' => 'admin/api-audit', 'target' => 'admin/api-audit/index'], + ['path' => 'admin/api-audit/view', 'target' => 'admin/api-audit/view'], + ['path' => 'admin/api-audit/data', 'target' => 'admin/api-audit/data'], + ['path' => 'admin/api-audit/purge', 'target' => 'admin/api-audit/purge'], + // System audit pages + ['path' => 'admin/system-audit', 'target' => 'admin/system-audit/index'], + ['path' => 'admin/system-audit/view', 'target' => 'admin/system-audit/view'], + ['path' => 'admin/system-audit/data', 'target' => 'admin/system-audit/data'], + ['path' => 'admin/system-audit/purge', 'target' => 'admin/system-audit/purge'], + // Import audit pages + ['path' => 'admin/import-audit', 'target' => 'admin/import-audit/index'], + ['path' => 'admin/import-audit/view', 'target' => 'admin/import-audit/view'], + ['path' => 'admin/import-audit/data', 'target' => 'admin/import-audit/data'], + ['path' => 'admin/import-audit/purge', 'target' => 'admin/import-audit/purge'], + // User lifecycle audit pages + ['path' => 'admin/user-lifecycle-audit', 'target' => 'admin/user-lifecycle-audit/index'], + ['path' => 'admin/user-lifecycle-audit/view', 'target' => 'admin/user-lifecycle-audit/view'], + ['path' => 'admin/user-lifecycle-audit/data', 'target' => 'admin/user-lifecycle-audit/data'], + ['path' => 'admin/user-lifecycle-audit/purge', 'target' => 'admin/user-lifecycle-audit/purge'], + ['path' => 'admin/user-lifecycle-audit/restore', 'target' => 'admin/user-lifecycle-audit/restore'], + // Frontend telemetry + ['path' => 'admin/frontend-telemetry/ingest', 'target' => 'admin/frontend-telemetry/ingest'], + ], + 'public_paths' => [], + + 'container_registrars' => [ + \MintyPHP\Module\Audit\AuditContainerRegistrar::class, + ], + + 'ui_slots' => [ + 'sidebar.admin_nav_item' => [ + [ + 'key' => 'audit-api-audit', + 'group' => 'admin-logs', + 'label' => 'API audit', + 'path' => 'admin/api-audit', + 'permission' => 'audit.api.view', + 'order' => 200, + ], + [ + 'key' => 'audit-system-audit', + 'group' => 'admin-logs', + 'label' => 'System audit logs', + 'path' => 'admin/system-audit', + 'permission' => 'audit.system.view', + 'order' => 210, + ], + [ + 'key' => 'audit-import-audit', + 'group' => 'admin-logs', + 'label' => 'Import logs', + 'path' => 'admin/import-audit', + 'permission' => 'audit.imports.view', + 'order' => 220, + ], + [ + 'key' => 'audit-user-lifecycle-audit', + 'group' => 'admin-logs', + 'label' => 'User lifecycle logs', + 'path' => 'admin/user-lifecycle-audit', + 'permission' => 'audit.user_lifecycle.view', + 'order' => 230, + ], + ], + ], + + 'authorization_policies' => [ + \MintyPHP\Module\Audit\AuditAuthorizationPolicy::class, + ], + + 'permissions' => [ + [ + 'key' => 'audit.api.view', + 'description' => 'View API audit logs', + ], + [ + 'key' => 'audit.system.view', + 'description' => 'View system audit logs', + ], + [ + 'key' => 'audit.system.purge', + 'description' => 'Purge system audit logs', + ], + [ + 'key' => 'audit.imports.view', + 'description' => 'View import audit logs', + ], + [ + 'key' => 'audit.user_lifecycle.view', + 'description' => 'View user lifecycle audit logs', + ], + [ + 'key' => 'audit.user_lifecycle.restore', + 'description' => 'Restore deleted users from lifecycle audit', + ], + ], + + 'scheduler_jobs' => [ + [ + 'job_key' => 'audit_system_purge', + 'handler' => \MintyPHP\Module\Audit\Handler\SystemAuditPurgeJobHandler::class, + 'label' => 'System audit log cleanup', + 'description' => 'Purges system audit log entries older than the configured retention period', + 'default_enabled' => 1, + 'default_timezone' => 'UTC', + 'default_schedule_type' => 'daily', + 'default_schedule_interval' => 1, + 'default_schedule_time' => '03:00', + 'default_schedule_weekdays_csv' => null, + 'default_catchup_once' => 1, + 'allowed_schedule_types' => ['daily', 'weekly'], + ], + [ + 'job_key' => 'audit_api_purge', + 'handler' => \MintyPHP\Module\Audit\Handler\ApiAuditPurgeJobHandler::class, + 'label' => 'API audit log cleanup', + 'description' => 'Purges API audit log entries older than 90 days', + 'default_enabled' => 1, + 'default_timezone' => 'UTC', + 'default_schedule_type' => 'daily', + 'default_schedule_interval' => 1, + 'default_schedule_time' => '03:15', + 'default_schedule_weekdays_csv' => null, + 'default_catchup_once' => 1, + 'allowed_schedule_types' => ['daily', 'weekly'], + ], + [ + 'job_key' => 'audit_import_purge', + 'handler' => \MintyPHP\Module\Audit\Handler\ImportAuditPurgeJobHandler::class, + 'label' => 'Import audit log cleanup', + 'description' => 'Purges import audit run records older than 90 days', + 'default_enabled' => 1, + 'default_timezone' => 'UTC', + 'default_schedule_type' => 'daily', + 'default_schedule_interval' => 1, + 'default_schedule_time' => '03:30', + 'default_schedule_weekdays_csv' => null, + 'default_catchup_once' => 1, + 'allowed_schedule_types' => ['daily', 'weekly'], + ], + [ + 'job_key' => 'audit_user_lifecycle_purge', + 'handler' => \MintyPHP\Module\Audit\Handler\UserLifecycleAuditPurgeJobHandler::class, + 'label' => 'User lifecycle audit log cleanup', + 'description' => 'Purges user lifecycle audit entries older than 365 days', + 'default_enabled' => 1, + 'default_timezone' => 'UTC', + 'default_schedule_type' => 'daily', + 'default_schedule_interval' => 1, + 'default_schedule_time' => '03:45', + 'default_schedule_weekdays_csv' => null, + 'default_catchup_once' => 1, + 'allowed_schedule_types' => ['daily', 'weekly'], + ], + ], + + 'layout_context_providers' => [ + \MintyPHP\Module\Audit\Providers\AuditLayoutProvider::class, + ], + 'session_providers' => [], + + 'event_listeners' => [], + 'search_resources' => [], + 'asset_groups' => [], + 'migrations_path' => 'db/updates', + 'i18n_path' => 'i18n', +]; diff --git a/pages/admin/api-audit/data().php b/modules/audit/pages/admin/api-audit/data().php similarity index 87% rename from pages/admin/api-audit/data().php rename to modules/audit/pages/admin/api-audit/data().php index c689ed8..3e8288f 100644 --- a/pages/admin/api-audit/data().php +++ b/modules/audit/pages/admin/api-audit/data().php @@ -1,15 +1,16 @@ listPaged($filters); +$result = app(ApiAuditService::class)->listPaged($filters); $rows = []; foreach ($result['rows'] as $row) { diff --git a/pages/admin/api-audit/filter-schema.php b/modules/audit/pages/admin/api-audit/filter-schema.php similarity index 100% rename from pages/admin/api-audit/filter-schema.php rename to modules/audit/pages/admin/api-audit/filter-schema.php diff --git a/pages/admin/api-audit/index($slug).php b/modules/audit/pages/admin/api-audit/index($slug).php similarity index 100% rename from pages/admin/api-audit/index($slug).php rename to modules/audit/pages/admin/api-audit/index($slug).php diff --git a/pages/admin/api-audit/index().php b/modules/audit/pages/admin/api-audit/index().php similarity index 94% rename from pages/admin/api-audit/index().php rename to modules/audit/pages/admin/api-audit/index().php index 61f272d..e6ff29d 100644 --- a/pages/admin/api-audit/index().php +++ b/modules/audit/pages/admin/api-audit/index().php @@ -2,17 +2,18 @@ use MintyPHP\Buffer; use MintyPHP\Http\SessionStoreInterface; +use MintyPHP\Module\Audit\AuditAuthorizationPolicy; +use MintyPHP\Module\Audit\Service\ApiAuditService; +use MintyPHP\Service\Access\SettingsAuthorizationPolicy; use MintyPHP\Service\Access\UiAccessService; -use MintyPHP\Service\Access\UiCapabilityMap; -use MintyPHP\Service\Audit\ApiAuditService; use MintyPHP\Support\Guard; $session = app(SessionStoreInterface::class)->all(); Guard::requireLogin(); -Guard::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_ADMIN_API_AUDIT_VIEW); +Guard::requireAbility(AuditAuthorizationPolicy::ABILITY_API_AUDIT_VIEW); $viewAuth['page'] = app(UiAccessService::class)->pageCapabilities( (int) ($session['user']['id'] ?? 0), - UiCapabilityMap::PAGE_AUDIT_PURGE + ['can_purge' => SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE] ); Buffer::set('title', t('API audit logs')); diff --git a/pages/admin/api-audit/index(default).phtml b/modules/audit/pages/admin/api-audit/index(default).phtml similarity index 95% rename from pages/admin/api-audit/index(default).phtml rename to modules/audit/pages/admin/api-audit/index(default).phtml index 2f7b3b7..7ab888f 100644 --- a/pages/admin/api-audit/index(default).phtml +++ b/modules/audit/pages/admin/api-audit/index(default).phtml @@ -66,4 +66,4 @@ $pageConfig = [ ?> - + diff --git a/pages/admin/api-audit/purge().php b/modules/audit/pages/admin/api-audit/purge().php similarity index 89% rename from pages/admin/api-audit/purge().php rename to modules/audit/pages/admin/api-audit/purge().php index 0092ac7..cefc4cb 100644 --- a/pages/admin/api-audit/purge().php +++ b/modules/audit/pages/admin/api-audit/purge().php @@ -18,6 +18,6 @@ if (!Session::checkCsrfToken()) { Router::redirect('admin/api-audit'); } -$deleted = app(\MintyPHP\Service\Audit\ApiAuditService::class)->purgeExpired(); +$deleted = app(\MintyPHP\Module\Audit\Service\ApiAuditService::class)->purgeExpired(); Flash::success(sprintf(t('%d API audit entries purged'), $deleted), 'admin/api-audit', 'api_audit_purged'); Router::redirect('admin/api-audit'); diff --git a/pages/admin/api-audit/view($id).php b/modules/audit/pages/admin/api-audit/view($id).php similarity index 62% rename from pages/admin/api-audit/view($id).php rename to modules/audit/pages/admin/api-audit/view($id).php index 49c6c62..18502e2 100644 --- a/pages/admin/api-audit/view($id).php +++ b/modules/audit/pages/admin/api-audit/view($id).php @@ -6,10 +6,10 @@ use MintyPHP\Support\Flash; use MintyPHP\Support\Guard; Guard::requireLogin(); -Guard::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_ADMIN_API_AUDIT_VIEW); +Guard::requireAbility(\MintyPHP\Module\Audit\AuditAuthorizationPolicy::ABILITY_API_AUDIT_VIEW); $auditId = (int) ($id ?? 0); -$auditLog = $auditId > 0 ? app(\MintyPHP\Service\Audit\ApiAuditService::class)->find($auditId) : null; +$auditLog = $auditId > 0 ? app(\MintyPHP\Module\Audit\Service\ApiAuditService::class)->find($auditId) : null; if (!$auditLog) { Flash::error('API audit entry not found', 'admin/api-audit', 'api_audit_not_found'); Router::redirect('admin/api-audit'); diff --git a/pages/admin/api-audit/view(default).phtml b/modules/audit/pages/admin/api-audit/view(default).phtml similarity index 100% rename from pages/admin/api-audit/view(default).phtml rename to modules/audit/pages/admin/api-audit/view(default).phtml diff --git a/pages/admin/frontend-telemetry/ingest().php b/modules/audit/pages/admin/frontend-telemetry/ingest().php similarity index 97% rename from pages/admin/frontend-telemetry/ingest().php rename to modules/audit/pages/admin/frontend-telemetry/ingest().php index 64b33d5..cb94d32 100644 --- a/pages/admin/frontend-telemetry/ingest().php +++ b/modules/audit/pages/admin/frontend-telemetry/ingest().php @@ -3,7 +3,7 @@ use MintyPHP\Http\RequestContext; use MintyPHP\Http\SessionStoreInterface; use MintyPHP\Router; -use MintyPHP\Service\Audit\FrontendTelemetryIngestService; +use MintyPHP\Module\Audit\Service\FrontendTelemetryIngestService; use MintyPHP\Session; if ((requestInput()->method()) !== 'POST') { diff --git a/pages/admin/import-audit/data().php b/modules/audit/pages/admin/import-audit/data().php similarity index 85% rename from pages/admin/import-audit/data().php rename to modules/audit/pages/admin/import-audit/data().php index 625c955..0f21c69 100644 --- a/pages/admin/import-audit/data().php +++ b/modules/audit/pages/admin/import-audit/data().php @@ -1,12 +1,12 @@ [ diff --git a/pages/admin/import-audit/index($slug).php b/modules/audit/pages/admin/import-audit/index($slug).php similarity index 100% rename from pages/admin/import-audit/index($slug).php rename to modules/audit/pages/admin/import-audit/index($slug).php diff --git a/pages/admin/import-audit/index().php b/modules/audit/pages/admin/import-audit/index().php similarity index 92% rename from pages/admin/import-audit/index().php rename to modules/audit/pages/admin/import-audit/index().php index 36b7395..9be8378 100644 --- a/pages/admin/import-audit/index().php +++ b/modules/audit/pages/admin/import-audit/index().php @@ -2,17 +2,18 @@ use MintyPHP\Buffer; use MintyPHP\Http\SessionStoreInterface; +use MintyPHP\Module\Audit\AuditAuthorizationPolicy; +use MintyPHP\Module\Audit\Service\ImportAuditService; +use MintyPHP\Service\Access\SettingsAuthorizationPolicy; use MintyPHP\Service\Access\UiAccessService; -use MintyPHP\Service\Access\UiCapabilityMap; -use MintyPHP\Service\Audit\ImportAuditService; use MintyPHP\Support\Guard; $session = app(SessionStoreInterface::class)->all(); Guard::requireLogin(); -Guard::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_ADMIN_IMPORTS_AUDIT_VIEW); +Guard::requireAbility(AuditAuthorizationPolicy::ABILITY_IMPORTS_AUDIT_VIEW); $viewAuth['page'] = app(UiAccessService::class)->pageCapabilities( (int) ($session['user']['id'] ?? 0), - UiCapabilityMap::PAGE_AUDIT_PURGE + ['can_purge' => SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE] ); Buffer::set('title', t('Import audit logs')); diff --git a/pages/admin/import-audit/index(default).phtml b/modules/audit/pages/admin/import-audit/index(default).phtml similarity index 95% rename from pages/admin/import-audit/index(default).phtml rename to modules/audit/pages/admin/import-audit/index(default).phtml index 4af116a..7c22e56 100644 --- a/pages/admin/import-audit/index(default).phtml +++ b/modules/audit/pages/admin/import-audit/index(default).phtml @@ -69,4 +69,4 @@ $pageConfig = [ ?> - + diff --git a/pages/admin/import-audit/purge().php b/modules/audit/pages/admin/import-audit/purge().php similarity index 93% rename from pages/admin/import-audit/purge().php rename to modules/audit/pages/admin/import-audit/purge().php index 775ef81..01f2160 100644 --- a/pages/admin/import-audit/purge().php +++ b/modules/audit/pages/admin/import-audit/purge().php @@ -1,7 +1,7 @@ 0 ? app(\MintyPHP\Service\Audit\ImportAuditService::class)->find($runId) : null; +$auditRun = $runId > 0 ? app(\MintyPHP\Module\Audit\Service\ImportAuditService::class)->find($runId) : null; if (!$auditRun) { Flash::error(t('Import audit entry not found'), 'admin/import-audit', 'import_audit_not_found'); Router::redirect('admin/import-audit'); diff --git a/pages/admin/import-audit/view(default).phtml b/modules/audit/pages/admin/import-audit/view(default).phtml similarity index 100% rename from pages/admin/import-audit/view(default).phtml rename to modules/audit/pages/admin/import-audit/view(default).phtml diff --git a/pages/admin/system-audit/data().php b/modules/audit/pages/admin/system-audit/data().php similarity index 84% rename from pages/admin/system-audit/data().php rename to modules/audit/pages/admin/system-audit/data().php index ccf1063..3ff5a6b 100644 --- a/pages/admin/system-audit/data().php +++ b/modules/audit/pages/admin/system-audit/data().php @@ -1,13 +1,13 @@ [ diff --git a/pages/admin/system-audit/index($slug).php b/modules/audit/pages/admin/system-audit/index($slug).php similarity index 100% rename from pages/admin/system-audit/index($slug).php rename to modules/audit/pages/admin/system-audit/index($slug).php diff --git a/pages/admin/system-audit/index().php b/modules/audit/pages/admin/system-audit/index().php similarity index 94% rename from pages/admin/system-audit/index().php rename to modules/audit/pages/admin/system-audit/index().php index e1f0f1a..e789bde 100644 --- a/pages/admin/system-audit/index().php +++ b/modules/audit/pages/admin/system-audit/index().php @@ -2,19 +2,19 @@ use MintyPHP\Buffer; use MintyPHP\Http\SessionStoreInterface; -use MintyPHP\Service\Access\OperationsAuthorizationPolicy; +use MintyPHP\Module\Audit\AuditAuthorizationPolicy; +use MintyPHP\Module\Audit\Service\SystemAuditService; +use MintyPHP\Service\Access\SettingsAuthorizationPolicy; use MintyPHP\Service\Access\UiAccessService; -use MintyPHP\Service\Access\UiCapabilityMap; -use MintyPHP\Service\Audit\SystemAuditService; use MintyPHP\Support\Guard; $session = app(SessionStoreInterface::class)->all(); Guard::requireLogin(); -Guard::requireAbility(OperationsAuthorizationPolicy::ABILITY_ADMIN_SYSTEM_AUDIT_VIEW); +Guard::requireAbility(AuditAuthorizationPolicy::ABILITY_SYSTEM_AUDIT_VIEW); $viewAuth['page'] = app(UiAccessService::class)->pageCapabilities( (int) ($session['user']['id'] ?? 0), - UiCapabilityMap::PAGE_SYSTEM_AUDIT + ['can_purge_system_audit' => AuditAuthorizationPolicy::ABILITY_SYSTEM_AUDIT_PURGE] ); Buffer::set('title', t('System audit logs')); diff --git a/pages/admin/system-audit/index(default).phtml b/modules/audit/pages/admin/system-audit/index(default).phtml similarity index 95% rename from pages/admin/system-audit/index(default).phtml rename to modules/audit/pages/admin/system-audit/index(default).phtml index ce679aa..558b2cb 100644 --- a/pages/admin/system-audit/index(default).phtml +++ b/modules/audit/pages/admin/system-audit/index(default).phtml @@ -66,4 +66,4 @@ $pageConfig = [ ?> - + diff --git a/pages/admin/system-audit/purge().php b/modules/audit/pages/admin/system-audit/purge().php similarity index 77% rename from pages/admin/system-audit/purge().php rename to modules/audit/pages/admin/system-audit/purge().php index 342712f..2dc238c 100644 --- a/pages/admin/system-audit/purge().php +++ b/modules/audit/pages/admin/system-audit/purge().php @@ -1,15 +1,15 @@ method()) !== 'POST') { Router::redirect('admin/system-audit'); diff --git a/pages/admin/system-audit/view($id).php b/modules/audit/pages/admin/system-audit/view($id).php similarity index 70% rename from pages/admin/system-audit/view($id).php rename to modules/audit/pages/admin/system-audit/view($id).php index a5fcf1c..3acdc1e 100644 --- a/pages/admin/system-audit/view($id).php +++ b/modules/audit/pages/admin/system-audit/view($id).php @@ -1,14 +1,14 @@ 0 ? app(SystemAuditService::class)->find($auditId) : null; diff --git a/pages/admin/system-audit/view(default).phtml b/modules/audit/pages/admin/system-audit/view(default).phtml similarity index 100% rename from pages/admin/system-audit/view(default).phtml rename to modules/audit/pages/admin/system-audit/view(default).phtml diff --git a/pages/admin/user-lifecycle-audit/data().php b/modules/audit/pages/admin/user-lifecycle-audit/data().php similarity index 77% rename from pages/admin/user-lifecycle-audit/data().php rename to modules/audit/pages/admin/user-lifecycle-audit/data().php index f28b179..c872d4a 100644 --- a/pages/admin/user-lifecycle-audit/data().php +++ b/modules/audit/pages/admin/user-lifecycle-audit/data().php @@ -1,18 +1,18 @@ listPaged($filters); +$result = app(\MintyPHP\Module\Audit\Service\UserLifecycleAuditService::class)->listPaged($filters); $rows = []; foreach ((array) ($result['rows'] ?? []) as $row) { diff --git a/pages/admin/user-lifecycle-audit/filter-schema.php b/modules/audit/pages/admin/user-lifecycle-audit/filter-schema.php similarity index 95% rename from pages/admin/user-lifecycle-audit/filter-schema.php rename to modules/audit/pages/admin/user-lifecycle-audit/filter-schema.php index a2609ee..0ac4dc7 100644 --- a/pages/admin/user-lifecycle-audit/filter-schema.php +++ b/modules/audit/pages/admin/user-lifecycle-audit/filter-schema.php @@ -1,8 +1,8 @@ [ diff --git a/pages/admin/user-lifecycle-audit/index().php b/modules/audit/pages/admin/user-lifecycle-audit/index().php similarity index 93% rename from pages/admin/user-lifecycle-audit/index().php rename to modules/audit/pages/admin/user-lifecycle-audit/index().php index 751bdd7..30f29a8 100644 --- a/pages/admin/user-lifecycle-audit/index().php +++ b/modules/audit/pages/admin/user-lifecycle-audit/index().php @@ -1,21 +1,22 @@ all(); Guard::requireLogin(); -Guard::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_ADMIN_USER_LIFECYCLE_AUDIT_VIEW); +Guard::requireAbility(AuditAuthorizationPolicy::ABILITY_USER_LIFECYCLE_VIEW); $viewAuth['page'] = app(UiAccessService::class)->pageCapabilities( (int) ($session['user']['id'] ?? 0), - UiCapabilityMap::PAGE_AUDIT_PURGE + ['can_purge' => SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE] ); Buffer::set('title', t('User lifecycle logs')); diff --git a/pages/admin/user-lifecycle-audit/index(default).phtml b/modules/audit/pages/admin/user-lifecycle-audit/index(default).phtml similarity index 95% rename from pages/admin/user-lifecycle-audit/index(default).phtml rename to modules/audit/pages/admin/user-lifecycle-audit/index(default).phtml index 4b22e58..7e7dd5f 100644 --- a/pages/admin/user-lifecycle-audit/index(default).phtml +++ b/modules/audit/pages/admin/user-lifecycle-audit/index(default).phtml @@ -70,4 +70,4 @@ $pageConfig = [ ?> - + diff --git a/pages/admin/user-lifecycle-audit/purge().php b/modules/audit/pages/admin/user-lifecycle-audit/purge().php similarity index 89% rename from pages/admin/user-lifecycle-audit/purge().php rename to modules/audit/pages/admin/user-lifecycle-audit/purge().php index 4a48c40..c6d94b6 100644 --- a/pages/admin/user-lifecycle-audit/purge().php +++ b/modules/audit/pages/admin/user-lifecycle-audit/purge().php @@ -18,7 +18,7 @@ if (!Session::checkCsrfToken()) { Router::redirect('admin/user-lifecycle-audit'); } -$deleted = app(\MintyPHP\Service\Audit\UserLifecycleAuditService::class)->purgeExpired(); +$deleted = app(\MintyPHP\Module\Audit\Service\UserLifecycleAuditService::class)->purgeExpired(); Flash::success( sprintf(t('%d user lifecycle audit entries purged'), $deleted), 'admin/user-lifecycle-audit', diff --git a/pages/admin/user-lifecycle-audit/restore($id).php b/modules/audit/pages/admin/user-lifecycle-audit/restore($id).php similarity index 93% rename from pages/admin/user-lifecycle-audit/restore($id).php rename to modules/audit/pages/admin/user-lifecycle-audit/restore($id).php index f972c30..5bf47e9 100644 --- a/pages/admin/user-lifecycle-audit/restore($id).php +++ b/modules/audit/pages/admin/user-lifecycle-audit/restore($id).php @@ -8,7 +8,7 @@ use MintyPHP\Support\Guard; $session = app(SessionStoreInterface::class)->all(); Guard::requireLogin(); -Guard::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_ADMIN_USERS_LIFECYCLE_RESTORE); +Guard::requireAbility(\MintyPHP\Module\Audit\AuditAuthorizationPolicy::ABILITY_USER_LIFECYCLE_RESTORE); $auditId = (int) ($id ?? 0); $errorBag = formErrors(); diff --git a/pages/admin/user-lifecycle-audit/view($id).php b/modules/audit/pages/admin/user-lifecycle-audit/view($id).php similarity index 71% rename from pages/admin/user-lifecycle-audit/view($id).php rename to modules/audit/pages/admin/user-lifecycle-audit/view($id).php index d11b879..4ba8aac 100644 --- a/pages/admin/user-lifecycle-audit/view($id).php +++ b/modules/audit/pages/admin/user-lifecycle-audit/view($id).php @@ -1,20 +1,20 @@ all(); Guard::requireLogin(); -Guard::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_ADMIN_USER_LIFECYCLE_AUDIT_VIEW); +Guard::requireAbility(AuditAuthorizationPolicy::ABILITY_USER_LIFECYCLE_VIEW); $auditId = (int) ($id ?? 0); -$auditService = app(\MintyPHP\Service\Audit\UserLifecycleAuditService::class); +$auditService = app(\MintyPHP\Module\Audit\Service\UserLifecycleAuditService::class); $auditLog = $auditId > 0 ? $auditService->find($auditId) : null; if (!$auditLog) { Flash::error(t('Lifecycle audit entry not found'), 'admin/user-lifecycle-audit', 'user_lifecycle_audit_not_found'); @@ -27,7 +27,7 @@ if ((string) ($auditLog['action'] ?? '') === UserLifecycleAction::Delete->value) } $viewAuth['page'] = app(UiAccessService::class)->pageCapabilities( (int) ($session['user']['id'] ?? 0), - UiCapabilityMap::PAGE_USER_LIFECYCLE_VIEW + ['can_restore' => AuditAuthorizationPolicy::ABILITY_USER_LIFECYCLE_RESTORE] ); Buffer::set('title', t('View user lifecycle audit entry')); diff --git a/pages/admin/user-lifecycle-audit/view(default).phtml b/modules/audit/pages/admin/user-lifecycle-audit/view(default).phtml similarity index 100% rename from pages/admin/user-lifecycle-audit/view(default).phtml rename to modules/audit/pages/admin/user-lifecycle-audit/view(default).phtml diff --git a/tests/Http/ApiSystemAuditReporterTest.php b/modules/audit/tests/Module/Audit/Http/ApiSystemAuditReporterTest.php similarity index 96% rename from tests/Http/ApiSystemAuditReporterTest.php rename to modules/audit/tests/Module/Audit/Http/ApiSystemAuditReporterTest.php index 51dbd71..4f98b1c 100644 --- a/tests/Http/ApiSystemAuditReporterTest.php +++ b/modules/audit/tests/Module/Audit/Http/ApiSystemAuditReporterTest.php @@ -1,11 +1,11 @@ $userId, 'uuid' => $uuid] + ); foreach ($tenantIds as $tenantId) { - $this->notificationService->createForTenantAdminUsers( + $this->notificationService->createForTenantAdminUsersFromMessage( $tenantId, - 'user.activated', - $title, - null, - $link, - ['user_id' => $userId, 'uuid' => $uuid], + $message, $actorUserId, $adminSet ); diff --git a/modules/notifications/lib/Module/Notifications/Listeners/UserAssignmentChangedNotificationListener.php b/modules/notifications/lib/Module/Notifications/Listeners/UserAssignmentChangedNotificationListener.php index e9aab1a..989f487 100644 --- a/modules/notifications/lib/Module/Notifications/Listeners/UserAssignmentChangedNotificationListener.php +++ b/modules/notifications/lib/Module/Notifications/Listeners/UserAssignmentChangedNotificationListener.php @@ -3,6 +3,7 @@ namespace MintyPHP\Module\Notifications\Listeners; use MintyPHP\App\Module\Contracts\EventListener; +use MintyPHP\Module\Notifications\Service\NotificationMessage; use MintyPHP\Module\Notifications\Service\NotificationService; use MintyPHP\Repository\User\UserReadRepositoryInterface; use MintyPHP\Service\Access\PermissionService; @@ -42,24 +43,26 @@ final class UserAssignmentChangedNotificationListener implements EventListener $tenantIds = $this->sanitizeTenantIds($payload['tenant_ids'] ?? []); $changes = is_array($payload['changes'] ?? null) ? $payload['changes'] : []; - $title = sprintf(t('User assignments updated: %s'), $displayName); - $body = t('Tenant, role, or department assignments were changed.'); $link = $uuid !== '' ? 'admin/users/edit/' . $uuid : null; - $data = [ + $message = NotificationMessage::localized( + 'user.assignment_changed', + 'User assignments updated: %s', + [$displayName], + 'Tenant, role, or department assignments were changed.', + [], + $link, + [ 'user_id' => $userId, 'uuid' => $uuid, 'changes' => $changes, - ]; + ] + ); if ($actorUserId === null || $actorUserId !== $userId) { - $this->notificationService->createForUser( + $this->notificationService->createFromMessage( $userId, null, - 'user.assignment_changed', - $title, - $body, - $link, - $data + $message ); } @@ -76,13 +79,9 @@ final class UserAssignmentChangedNotificationListener implements EventListener $adminSet = array_fill_keys(array_map('intval', $adminUserIds), true); foreach ($tenantIds as $tenantId) { - $this->notificationService->createForTenantAdminUsers( + $this->notificationService->createForTenantAdminUsersFromMessage( $tenantId, - 'user.assignment_changed', - $title, - $body, - $link, - $data, + $message, $actorUserId, $adminSet ); diff --git a/modules/notifications/lib/Module/Notifications/Listeners/UserCreatedNotificationListener.php b/modules/notifications/lib/Module/Notifications/Listeners/UserCreatedNotificationListener.php index ccc0d8a..cec5baa 100644 --- a/modules/notifications/lib/Module/Notifications/Listeners/UserCreatedNotificationListener.php +++ b/modules/notifications/lib/Module/Notifications/Listeners/UserCreatedNotificationListener.php @@ -3,6 +3,7 @@ namespace MintyPHP\Module\Notifications\Listeners; use MintyPHP\App\Module\Contracts\EventListener; +use MintyPHP\Module\Notifications\Service\NotificationMessage; use MintyPHP\Module\Notifications\Service\NotificationService; use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface; use MintyPHP\Repository\User\UserReadRepositoryInterface; @@ -51,17 +52,21 @@ final class UserCreatedNotificationListener implements EventListener } $adminSet = array_flip($adminUserIds); - $title = sprintf(t('New user: %s'), $displayName); $link = 'admin/users/edit/' . $uuid; + $message = NotificationMessage::localized( + 'user.created', + 'New user: %s', + [$displayName], + null, + [], + $link, + ['user_id' => $userId, 'uuid' => $uuid] + ); foreach ($tenantIds as $tenantId) { - $this->notificationService->createForTenantAdminUsers( + $this->notificationService->createForTenantAdminUsersFromMessage( $tenantId, - 'user.created', - $title, - null, - $link, - ['user_id' => $userId, 'uuid' => $uuid], + $message, $actorUserId, $adminSet ); diff --git a/modules/notifications/lib/Module/Notifications/Listeners/UserDeactivatedNotificationListener.php b/modules/notifications/lib/Module/Notifications/Listeners/UserDeactivatedNotificationListener.php index b55b2a4..e8a5f8b 100644 --- a/modules/notifications/lib/Module/Notifications/Listeners/UserDeactivatedNotificationListener.php +++ b/modules/notifications/lib/Module/Notifications/Listeners/UserDeactivatedNotificationListener.php @@ -3,6 +3,7 @@ namespace MintyPHP\Module\Notifications\Listeners; use MintyPHP\App\Module\Contracts\EventListener; +use MintyPHP\Module\Notifications\Service\NotificationMessage; use MintyPHP\Module\Notifications\Service\NotificationService; use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface; use MintyPHP\Repository\User\UserReadRepositoryInterface; @@ -58,17 +59,21 @@ final class UserDeactivatedNotificationListener implements EventListener $adminSet = array_fill_keys(array_map('intval', $adminUserIds), true); $actorUserId = isset($payload['actor_user_id']) ? (int) $payload['actor_user_id'] : null; - $title = sprintf(t('User deactivated: %s'), $displayName); $link = $uuid !== '' ? 'admin/users/edit/' . $uuid : null; + $message = NotificationMessage::localized( + 'user.deactivated', + 'User deactivated: %s', + [$displayName], + null, + [], + $link, + ['user_id' => $userId, 'uuid' => $uuid] + ); foreach ($tenantIds as $tenantId) { - $this->notificationService->createForTenantAdminUsers( + $this->notificationService->createForTenantAdminUsersFromMessage( $tenantId, - 'user.deactivated', - $title, - null, - $link, - ['user_id' => $userId, 'uuid' => $uuid], + $message, $actorUserId, $adminSet ); diff --git a/modules/notifications/lib/Module/Notifications/Listeners/UserDeletedNotificationListener.php b/modules/notifications/lib/Module/Notifications/Listeners/UserDeletedNotificationListener.php index 343d029..b4c7397 100644 --- a/modules/notifications/lib/Module/Notifications/Listeners/UserDeletedNotificationListener.php +++ b/modules/notifications/lib/Module/Notifications/Listeners/UserDeletedNotificationListener.php @@ -3,6 +3,7 @@ namespace MintyPHP\Module\Notifications\Listeners; use MintyPHP\App\Module\Contracts\EventListener; +use MintyPHP\Module\Notifications\Service\NotificationMessage; use MintyPHP\Module\Notifications\Service\NotificationService; use MintyPHP\Repository\User\UserReadRepositoryInterface; use MintyPHP\Service\Access\PermissionService; @@ -42,7 +43,15 @@ final class UserDeletedNotificationListener implements EventListener } $adminSet = array_flip($adminUserIds); - $title = sprintf(t('User deleted: %s'), $displayName); + $message = NotificationMessage::localized( + 'user.deleted', + 'User deleted: %s', + [$displayName], + null, + [], + null, + ['user_id' => $userId] + ); foreach ($tenantIds as $tenantId) { $tenantId = (int) $tenantId; @@ -50,13 +59,9 @@ final class UserDeletedNotificationListener implements EventListener continue; } - $this->notificationService->createForTenantAdminUsers( + $this->notificationService->createForTenantAdminUsersFromMessage( $tenantId, - 'user.deleted', - $title, - null, - null, - ['user_id' => $userId], + $message, $actorUserId, $adminSet ); diff --git a/modules/notifications/lib/Module/Notifications/Repository/NotificationRepository.php b/modules/notifications/lib/Module/Notifications/Repository/NotificationRepository.php index 2aa5f14..853dbfd 100644 --- a/modules/notifications/lib/Module/Notifications/Repository/NotificationRepository.php +++ b/modules/notifications/lib/Module/Notifications/Repository/NotificationRepository.php @@ -49,7 +49,7 @@ class NotificationRepository implements NotificationRepositoryInterface $tenantParams = []; $rows = DB::select( - 'select id, type, title, body, link, is_read, created from user_notifications where recipient_user_id = ?' . + 'select id, type, title, body, link, data, is_read, created from user_notifications where recipient_user_id = ?' . $this->tenantScopeClause($tenantId, true, $tenantParams) . ' order by created desc limit ' . $limit . ' offset ' . $offset, (string) $userId, diff --git a/modules/notifications/lib/Module/Notifications/Service/NotificationService.php b/modules/notifications/lib/Module/Notifications/Service/NotificationService.php index ce5ae3d..3330df6 100644 --- a/modules/notifications/lib/Module/Notifications/Service/NotificationService.php +++ b/modules/notifications/lib/Module/Notifications/Service/NotificationService.php @@ -7,6 +7,7 @@ use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface; class NotificationService { + private const MESSAGE_META_KEY = '__message'; private const DEDUPE_WINDOW_MINUTES = 30; private const DEDUPE_TYPES = [ 'user.created' => true, @@ -29,7 +30,8 @@ class NotificationService return []; } - return $this->notificationRepository->listByUser($userId, $tenantId, $limit, 0); + $notifications = $this->notificationRepository->listByUser($userId, $tenantId, $limit, 0); + return $this->localizeNotifications($notifications); } public function unreadCount(int $userId, ?int $tenantId = null): int @@ -77,23 +79,8 @@ class NotificationService ?string $link, array $data ): int|false { - if ($recipientUserId <= 0 || $type === '' || $title === '') { - return false; - } - - $dedupeMeta = $this->buildDedupeMeta($recipientUserId, $tenantId, $type, $data); - - return $this->notificationRepository->create([ - 'recipient_user_id' => $recipientUserId, - 'tenant_id' => $tenantId, - 'type' => $type, - 'title' => $title, - 'body' => $body, - 'link' => $link, - 'data' => $data ?: null, - 'dedupe_fingerprint' => $dedupeMeta['fingerprint'] ?? null, - 'dedupe_bucket' => $dedupeMeta['bucket'] ?? null, - ]); + $message = NotificationMessage::plain($type, $title, $body, $link, $data); + return $this->createFromMessage($recipientUserId, $tenantId, $message); } /** @@ -110,25 +97,8 @@ class NotificationService array $data, ?int $excludeUserId ): int { - if ($tenantId <= 0 || $type === '' || $title === '') { - return 0; - } - - $userIds = $this->userTenantRepository->listActiveUserIdsByTenantId($tenantId); - $created = 0; - - foreach ($userIds as $userId) { - if ($excludeUserId !== null && $userId === $excludeUserId) { - continue; - } - - $result = $this->createForUser($userId, $tenantId, $type, $title, $body, $link, $data); - if ($result !== false) { - $created++; - } - } - - return $created; + $message = NotificationMessage::plain($type, $title, $body, $link, $data); + return $this->createForTenantUsersFromMessage($tenantId, $message, $excludeUserId); } /** @@ -147,7 +117,69 @@ class NotificationService ?int $excludeUserId, array $allowedUserIds ): int { - if ($tenantId <= 0 || $type === '' || $title === '' || $allowedUserIds === []) { + $message = NotificationMessage::plain($type, $title, $body, $link, $data); + return $this->createForTenantAdminUsersFromMessage($tenantId, $message, $excludeUserId, $allowedUserIds); + } + + public function createFromMessage(int $recipientUserId, ?int $tenantId, NotificationMessage $message): int|false + { + $type = trim($message->type()); + $title = trim($message->title()); + if ($recipientUserId <= 0 || $type === '' || $title === '') { + return false; + } + + $persistedData = $this->buildPersistedData($message); + $dedupeMeta = $this->buildDedupeMeta($recipientUserId, $tenantId, $type, $persistedData); + + return $this->notificationRepository->create([ + 'recipient_user_id' => $recipientUserId, + 'tenant_id' => $tenantId, + 'type' => $type, + 'title' => $title, + 'body' => $message->body(), + 'link' => $message->link(), + 'data' => $persistedData !== [] ? $persistedData : null, + 'dedupe_fingerprint' => $dedupeMeta['fingerprint'] ?? null, + 'dedupe_bucket' => $dedupeMeta['bucket'] ?? null, + ]); + } + + public function createForTenantUsersFromMessage(int $tenantId, NotificationMessage $message, ?int $excludeUserId): int + { + if ($tenantId <= 0 || trim($message->type()) === '' || trim($message->title()) === '') { + return 0; + } + + $userIds = $this->userTenantRepository->listActiveUserIdsByTenantId($tenantId); + $created = 0; + + foreach ($userIds as $userId) { + if ($excludeUserId !== null && $userId === $excludeUserId) { + continue; + } + + $result = $this->createFromMessage($userId, $tenantId, $message); + if ($result !== false) { + $created++; + } + } + + return $created; + } + + /** + * Fan-out using a typed message contract for tenant users filtered by allowed user IDs. + * + * @param array $allowedUserIds Flipped array (user ID as key) for fast lookup + */ + public function createForTenantAdminUsersFromMessage( + int $tenantId, + NotificationMessage $message, + ?int $excludeUserId, + array $allowedUserIds + ): int { + if ($tenantId <= 0 || trim($message->type()) === '' || trim($message->title()) === '' || $allowedUserIds === []) { return 0; } @@ -162,7 +194,7 @@ class NotificationService continue; } - $result = $this->createForUser($userId, $tenantId, $type, $title, $body, $link, $data); + $result = $this->createFromMessage($userId, $tenantId, $message); if ($result !== false) { $created++; } @@ -237,4 +269,152 @@ class NotificationService return ''; } + /** + * @param list> $notifications + * @return list> + */ + private function localizeNotifications(array $notifications): array + { + $localized = []; + foreach ($notifications as $notification) { + $localized[] = $this->localizeNotification($notification); + } + + return $localized; + } + + /** + * @param array $notification + * @return array + */ + private function localizeNotification(array $notification): array + { + $data = $this->decodeNotificationData($notification['data'] ?? null); + $messageMeta = $this->extractMessageMeta($data); + + if ($messageMeta !== []) { + if ($messageMeta['title_key'] !== '') { + $notification['title'] = $this->translateWithParams( + $messageMeta['title_key'], + $messageMeta['title_params'] + ); + } + + if ($messageMeta['body_key'] !== '') { + $notification['body'] = $this->translateWithParams( + $messageMeta['body_key'], + $messageMeta['body_params'] + ); + } + } + + if ($data !== []) { + unset($data[self::MESSAGE_META_KEY]); + if ($data !== []) { + $notification['data'] = $data; + } else { + unset($notification['data']); + } + } else { + unset($notification['data']); + } + + return $notification; + } + + /** + * @param array $data + * @return array{title_key: string, title_params: list, body_key: string, body_params: list}|array{} + */ + private function extractMessageMeta(array $data): array + { + $meta = $data[self::MESSAGE_META_KEY] ?? null; + if (!is_array($meta)) { + return []; + } + + $titleKey = trim((string) ($meta['title_key'] ?? '')); + $bodyKey = trim((string) ($meta['body_key'] ?? '')); + if ($titleKey === '' && $bodyKey === '') { + return []; + } + + return [ + 'title_key' => $titleKey, + 'title_params' => $this->normalizeMessageParams($meta['title_params'] ?? []), + 'body_key' => $bodyKey, + 'body_params' => $this->normalizeMessageParams($meta['body_params'] ?? []), + ]; + } + + /** + * @param list $params + */ + private function translateWithParams(string $key, array $params): string + { + return (string) t($key, ...$params); + } + + /** + * @return list + */ + private function normalizeMessageParams(mixed $params): array + { + if (!is_array($params)) { + return []; + } + + $normalized = []; + foreach ($params as $value) { + if (is_scalar($value) || $value === null) { + $normalized[] = $value; + } + } + + return array_values($normalized); + } + + /** + * @return array + */ + private function decodeNotificationData(mixed $raw): array + { + if (is_array($raw)) { + return $raw; + } + + if (!is_string($raw) || trim($raw) === '') { + return []; + } + + try { + $decoded = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException) { + return []; + } + + return is_array($decoded) ? $decoded : []; + } + + /** + * @return array + */ + private function buildPersistedData(NotificationMessage $message): array + { + $data = $message->data(); + $titleKey = trim((string) ($message->titleKey() ?? '')); + $bodyKey = trim((string) ($message->bodyKey() ?? '')); + + if ($titleKey !== '' || $bodyKey !== '') { + $data[self::MESSAGE_META_KEY] = [ + 'title_key' => $titleKey, + 'title_params' => $this->normalizeMessageParams($message->titleParams()), + 'body_key' => $bodyKey, + 'body_params' => $this->normalizeMessageParams($message->bodyParams()), + ]; + } + + return $data; + } + } diff --git a/modules/notifications/tests/Module/Notifications/Listeners/UserActivatedNotificationListenerTest.php b/modules/notifications/tests/Module/Notifications/Listeners/UserActivatedNotificationListenerTest.php index 0673d4b..7fe2afb 100644 --- a/modules/notifications/tests/Module/Notifications/Listeners/UserActivatedNotificationListenerTest.php +++ b/modules/notifications/tests/Module/Notifications/Listeners/UserActivatedNotificationListenerTest.php @@ -3,6 +3,7 @@ namespace MintyPHP\Tests\Module\Notifications\Listeners; use MintyPHP\Module\Notifications\Listeners\UserActivatedNotificationListener; +use MintyPHP\Module\Notifications\Service\NotificationMessage; use MintyPHP\Module\Notifications\Service\NotificationService; use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface; use MintyPHP\Repository\User\UserReadRepositoryInterface; @@ -42,14 +43,15 @@ final class UserActivatedNotificationListenerTest extends TestCase ->willReturn([20, 30]); $this->notificationService->expects($this->exactly(2)) - ->method('createForTenantAdminUsers') + ->method('createForTenantAdminUsersFromMessage') ->with( $this->callback(static fn (int $tenantId): bool => in_array($tenantId, [1, 2], true)), - 'user.activated', - $this->stringContains('Alice'), - null, - 'admin/users/edit/user-uuid', - ['user_id' => 10, 'uuid' => 'user-uuid'], + $this->callback(function (NotificationMessage $message): bool { + return $message->type() === 'user.activated' + && $message->link() === 'admin/users/edit/user-uuid' + && $message->data() === ['user_id' => 10, 'uuid' => 'user-uuid'] + && $message->titleKey() === 'User activated: %s'; + }), 5, $this->isArray() ); @@ -74,13 +76,9 @@ final class UserActivatedNotificationListenerTest extends TestCase ->willReturn([20]); $this->notificationService->expects($this->once()) - ->method('createForTenantAdminUsers') + ->method('createForTenantAdminUsersFromMessage') ->with( 7, - 'user.activated', - $this->anything(), - null, - 'admin/users/edit/user-uuid', $this->anything(), null, $this->anything() @@ -92,7 +90,7 @@ final class UserActivatedNotificationListenerTest extends TestCase public function testHandleSkipsInvalidPayload(): void { $this->userReadRepository->expects($this->never())->method('find'); - $this->notificationService->expects($this->never())->method('createForTenantAdminUsers'); + $this->notificationService->expects($this->never())->method('createForTenantAdminUsersFromMessage'); $this->listener->handle('user.activated', ['user_id' => 0]); } diff --git a/modules/notifications/tests/Module/Notifications/Listeners/UserAssignmentChangedNotificationListenerTest.php b/modules/notifications/tests/Module/Notifications/Listeners/UserAssignmentChangedNotificationListenerTest.php index d414fb4..9f7e0f5 100644 --- a/modules/notifications/tests/Module/Notifications/Listeners/UserAssignmentChangedNotificationListenerTest.php +++ b/modules/notifications/tests/Module/Notifications/Listeners/UserAssignmentChangedNotificationListenerTest.php @@ -3,6 +3,7 @@ namespace MintyPHP\Tests\Module\Notifications\Listeners; use MintyPHP\Module\Notifications\Listeners\UserAssignmentChangedNotificationListener; +use MintyPHP\Module\Notifications\Service\NotificationMessage; use MintyPHP\Module\Notifications\Service\NotificationService; use MintyPHP\Repository\User\UserReadRepositoryInterface; use MintyPHP\Service\Access\PermissionService; @@ -38,27 +39,24 @@ final class UserAssignmentChangedNotificationListenerTest extends TestCase ->willReturn([20, 30]); $this->notificationService->expects($this->once()) - ->method('createForUser') + ->method('createFromMessage') ->with( 10, null, - 'user.assignment_changed', - $this->stringContains('Alice'), - $this->isString(), - 'admin/users/edit/user-uuid', - $this->isArray() + $this->callback(function (NotificationMessage $message): bool { + return $message->type() === 'user.assignment_changed' + && $message->link() === 'admin/users/edit/user-uuid' + && $message->titleKey() === 'User assignments updated: %s' + && $message->bodyKey() === 'Tenant, role, or department assignments were changed.'; + }) ) ->willReturn(99); $this->notificationService->expects($this->once()) - ->method('createForTenantAdminUsers') + ->method('createForTenantAdminUsersFromMessage') ->with( 1, - 'user.assignment_changed', - $this->stringContains('Alice'), - $this->isString(), - 'admin/users/edit/user-uuid', - $this->isArray(), + $this->isInstanceOf(NotificationMessage::class), 7, $this->isArray() ) @@ -83,15 +81,11 @@ final class UserAssignmentChangedNotificationListenerTest extends TestCase ->method('listPrivilegedUserIdsByPermissionKeys') ->willReturn([20]); - $this->notificationService->expects($this->never())->method('createForUser'); + $this->notificationService->expects($this->never())->method('createFromMessage'); $this->notificationService->expects($this->once()) - ->method('createForTenantAdminUsers') + ->method('createForTenantAdminUsersFromMessage') ->with( 1, - 'user.assignment_changed', - $this->anything(), - $this->anything(), - 'admin/users/edit/user-uuid', $this->anything(), 10, $this->anything() @@ -106,8 +100,8 @@ final class UserAssignmentChangedNotificationListenerTest extends TestCase public function testHandleSkipsInvalidPayload(): void { - $this->notificationService->expects($this->never())->method('createForUser'); - $this->notificationService->expects($this->never())->method('createForTenantAdminUsers'); + $this->notificationService->expects($this->never())->method('createFromMessage'); + $this->notificationService->expects($this->never())->method('createForTenantAdminUsersFromMessage'); $this->listener->handle('user.assignment_changed', ['user_id' => 0]); } diff --git a/modules/notifications/tests/Module/Notifications/Listeners/UserCreatedNotificationListenerTest.php b/modules/notifications/tests/Module/Notifications/Listeners/UserCreatedNotificationListenerTest.php index 084f5ad..fc0ab70 100644 --- a/modules/notifications/tests/Module/Notifications/Listeners/UserCreatedNotificationListenerTest.php +++ b/modules/notifications/tests/Module/Notifications/Listeners/UserCreatedNotificationListenerTest.php @@ -3,6 +3,7 @@ namespace MintyPHP\Tests\Module\Notifications\Listeners; use MintyPHP\Module\Notifications\Listeners\UserCreatedNotificationListener; +use MintyPHP\Module\Notifications\Service\NotificationMessage; use MintyPHP\Module\Notifications\Service\NotificationService; use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface; use MintyPHP\Repository\User\UserReadRepositoryInterface; @@ -39,15 +40,16 @@ class UserCreatedNotificationListenerTest extends TestCase ->willReturn([20, 30]); $this->notifService->expects($this->exactly(2)) - ->method('createForTenantAdminUsers') - ->willReturnCallback(function (int $tenantId, string $type, string $title, ?string $body, ?string $link, array $data, ?int $excludeUserId, array $adminSet): int { + ->method('createForTenantAdminUsersFromMessage') + ->willReturnCallback(function (int $tenantId, NotificationMessage $message, ?int $excludeUserId, array $adminSet): int { $this->assertContains($tenantId, [1, 2]); - $this->assertSame('user.created', $type); - $this->assertStringContainsString('Alice Smith', $title); - $this->assertSame('admin/users/edit/abc-uuid', $link); + $this->assertSame('user.created', $message->type()); + $this->assertStringContainsString('Alice Smith', $message->title()); + $this->assertSame('admin/users/edit/abc-uuid', $message->link()); $this->assertSame(5, $excludeUserId); $this->assertArrayHasKey(20, $adminSet); $this->assertArrayHasKey(30, $adminSet); + $this->assertSame('New user: %s', $message->titleKey()); return 2; }); @@ -68,13 +70,9 @@ class UserCreatedNotificationListenerTest extends TestCase $this->userRepo->expects($this->any())->method('listPrivilegedUserIdsByPermissionKeys')->willReturn([20]); $this->notifService->expects($this->once()) - ->method('createForTenantAdminUsers') + ->method('createForTenantAdminUsersFromMessage') ->with( 1, - 'user.created', - $this->anything(), - null, - 'admin/users/edit/xyz', $this->anything(), 7, // actor excluded $this->anything() @@ -90,7 +88,7 @@ class UserCreatedNotificationListenerTest extends TestCase public function testHandleSkipsInvalidPayload(): void { - $this->notifService->expects($this->never())->method('createForTenantAdminUsers'); + $this->notifService->expects($this->never())->method('createForTenantAdminUsersFromMessage'); $this->listener->handle('user.created', ['uuid' => 'abc']); $this->listener->handle('user.created', ['user_id' => 0, 'uuid' => 'abc']); @@ -100,7 +98,7 @@ class UserCreatedNotificationListenerTest extends TestCase public function testHandleSkipsWhenUserNotFound(): void { $this->userRepo->expects($this->any())->method('find')->with(99)->willReturn(null); - $this->notifService->expects($this->never())->method('createForTenantAdminUsers'); + $this->notifService->expects($this->never())->method('createForTenantAdminUsersFromMessage'); $this->listener->handle('user.created', [ 'user_id' => 99, @@ -116,7 +114,7 @@ class UserCreatedNotificationListenerTest extends TestCase 'display_name' => 'Alice', ]); $this->utRepo->expects($this->any())->method('listTenantIdsByUserId')->with(10)->willReturn([]); - $this->notifService->expects($this->never())->method('createForTenantAdminUsers'); + $this->notifService->expects($this->never())->method('createForTenantAdminUsersFromMessage'); $this->listener->handle('user.created', [ 'user_id' => 10, @@ -134,7 +132,7 @@ class UserCreatedNotificationListenerTest extends TestCase $this->utRepo->expects($this->any())->method('listTenantIdsByUserId')->with(10)->willReturn([1]); $this->userRepo->expects($this->any())->method('listPrivilegedUserIdsByPermissionKeys')->willReturn([]); - $this->notifService->expects($this->never())->method('createForTenantAdminUsers'); + $this->notifService->expects($this->never())->method('createForTenantAdminUsersFromMessage'); $this->listener->handle('user.created', [ 'user_id' => 10, diff --git a/modules/notifications/tests/Module/Notifications/Listeners/UserDeactivatedNotificationListenerTest.php b/modules/notifications/tests/Module/Notifications/Listeners/UserDeactivatedNotificationListenerTest.php index 30af1ab..27a6796 100644 --- a/modules/notifications/tests/Module/Notifications/Listeners/UserDeactivatedNotificationListenerTest.php +++ b/modules/notifications/tests/Module/Notifications/Listeners/UserDeactivatedNotificationListenerTest.php @@ -3,6 +3,7 @@ namespace MintyPHP\Tests\Module\Notifications\Listeners; use MintyPHP\Module\Notifications\Listeners\UserDeactivatedNotificationListener; +use MintyPHP\Module\Notifications\Service\NotificationMessage; use MintyPHP\Module\Notifications\Service\NotificationService; use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface; use MintyPHP\Repository\User\UserReadRepositoryInterface; @@ -42,14 +43,15 @@ final class UserDeactivatedNotificationListenerTest extends TestCase ->willReturn([20, 30]); $this->notificationService->expects($this->exactly(2)) - ->method('createForTenantAdminUsers') + ->method('createForTenantAdminUsersFromMessage') ->with( $this->callback(static fn (int $tenantId): bool => in_array($tenantId, [1, 2], true)), - 'user.deactivated', - $this->stringContains('Alice'), - null, - 'admin/users/edit/user-uuid', - ['user_id' => 10, 'uuid' => 'user-uuid'], + $this->callback(function (NotificationMessage $message): bool { + return $message->type() === 'user.deactivated' + && $message->link() === 'admin/users/edit/user-uuid' + && $message->data() === ['user_id' => 10, 'uuid' => 'user-uuid'] + && $message->titleKey() === 'User deactivated: %s'; + }), 5, $this->isArray() ); @@ -69,7 +71,7 @@ final class UserDeactivatedNotificationListenerTest extends TestCase 'display_name' => 'Alice', ]); $this->userTenantRepository->expects($this->once())->method('listTenantIdsByUserId')->with(10)->willReturn([]); - $this->notificationService->expects($this->never())->method('createForTenantAdminUsers'); + $this->notificationService->expects($this->never())->method('createForTenantAdminUsersFromMessage'); $this->listener->handle('user.deactivated', ['user_id' => 10]); } diff --git a/modules/notifications/tests/Module/Notifications/Listeners/UserDeletedNotificationListenerTest.php b/modules/notifications/tests/Module/Notifications/Listeners/UserDeletedNotificationListenerTest.php index f8efcb5..2a9b619 100644 --- a/modules/notifications/tests/Module/Notifications/Listeners/UserDeletedNotificationListenerTest.php +++ b/modules/notifications/tests/Module/Notifications/Listeners/UserDeletedNotificationListenerTest.php @@ -3,6 +3,7 @@ namespace MintyPHP\Tests\Module\Notifications\Listeners; use MintyPHP\Module\Notifications\Listeners\UserDeletedNotificationListener; +use MintyPHP\Module\Notifications\Service\NotificationMessage; use MintyPHP\Module\Notifications\Service\NotificationService; use MintyPHP\Repository\User\UserReadRepositoryInterface; use MintyPHP\Service\Access\PermissionService; @@ -34,15 +35,16 @@ class UserDeletedNotificationListenerTest extends TestCase ->willReturn([20, 30]); $this->notifService->expects($this->exactly(2)) - ->method('createForTenantAdminUsers') - ->willReturnCallback(function (int $tenantId, string $type, string $title, ?string $body, ?string $link, array $data, ?int $excludeUserId, array $adminSet): int { + ->method('createForTenantAdminUsersFromMessage') + ->willReturnCallback(function (int $tenantId, NotificationMessage $message, ?int $excludeUserId, array $adminSet): int { $this->assertContains($tenantId, [1, 2]); - $this->assertSame('user.deleted', $type); - $this->assertStringContainsString('Alice', $title); - $this->assertNull($link); + $this->assertSame('user.deleted', $message->type()); + $this->assertStringContainsString('Alice', $message->title()); + $this->assertNull($message->link()); $this->assertSame(5, $excludeUserId); $this->assertArrayHasKey(20, $adminSet); $this->assertArrayHasKey(30, $adminSet); + $this->assertSame('User deleted: %s', $message->titleKey()); return 1; }); @@ -57,7 +59,7 @@ class UserDeletedNotificationListenerTest extends TestCase public function testHandleSkipsOnInvalidUserId(): void { $this->notifService->expects($this->never())->method('deleteAllForUser'); - $this->notifService->expects($this->never())->method('createForTenantAdminUsers'); + $this->notifService->expects($this->never())->method('createForTenantAdminUsersFromMessage'); $this->listener->handle('user.deleted', ['user_id' => 0]); $this->listener->handle('user.deleted', []); @@ -69,7 +71,7 @@ class UserDeletedNotificationListenerTest extends TestCase ->method('deleteAllForUser') ->with(10); - $this->notifService->expects($this->never())->method('createForTenantAdminUsers'); + $this->notifService->expects($this->never())->method('createForTenantAdminUsersFromMessage'); $this->listener->handle('user.deleted', [ 'user_id' => 10, @@ -84,7 +86,7 @@ class UserDeletedNotificationListenerTest extends TestCase ->method('deleteAllForUser') ->with(10); - $this->notifService->expects($this->never())->method('createForTenantAdminUsers'); + $this->notifService->expects($this->never())->method('createForTenantAdminUsersFromMessage'); $this->listener->handle('user.deleted', [ 'user_id' => 10, @@ -102,13 +104,9 @@ class UserDeletedNotificationListenerTest extends TestCase ->willReturn([20]); $this->notifService->expects($this->once()) - ->method('createForTenantAdminUsers') + ->method('createForTenantAdminUsersFromMessage') ->with( 1, - 'user.deleted', - $this->anything(), - null, - null, $this->anything(), 7, // actor excluded $this->anything() @@ -131,7 +129,7 @@ class UserDeletedNotificationListenerTest extends TestCase ->method('listPrivilegedUserIdsByPermissionKeys') ->willReturn([]); - $this->notifService->expects($this->never())->method('createForTenantAdminUsers'); + $this->notifService->expects($this->never())->method('createForTenantAdminUsersFromMessage'); $this->listener->handle('user.deleted', [ 'user_id' => 10, diff --git a/modules/notifications/tests/Module/Notifications/Service/NotificationServiceTest.php b/modules/notifications/tests/Module/Notifications/Service/NotificationServiceTest.php index 2d8f74b..0039333 100644 --- a/modules/notifications/tests/Module/Notifications/Service/NotificationServiceTest.php +++ b/modules/notifications/tests/Module/Notifications/Service/NotificationServiceTest.php @@ -3,6 +3,7 @@ namespace MintyPHP\Tests\Module\Notifications\Service; use MintyPHP\Module\Notifications\Repository\NotificationRepositoryInterface; +use MintyPHP\Module\Notifications\Service\NotificationMessage; use MintyPHP\Module\Notifications\Service\NotificationService; use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface; use PHPUnit\Framework\MockObject\MockObject; @@ -282,4 +283,95 @@ class NotificationServiceTest extends TestCase $result = $this->service->createForUser(5, 1, 'system', 'System', null, null, ['user_id' => 33]); $this->assertSame(56, $result); } + + public function testCreateFromMessagePersistsLocalizationMetadata(): void + { + $message = NotificationMessage::localized( + 'user.created', + 'New user: %s', + ['Alice'], + null, + [], + 'admin/users/edit/abc', + ['user_id' => 33] + ); + + $this->notifRepo->expects($this->once()) + ->method('create') + ->with($this->callback(function (array $data): bool { + if (!isset($data['data']['__message']) || !is_array($data['data']['__message'])) { + return false; + } + + return $data['type'] === 'user.created' + && $data['data']['__message']['title_key'] === 'New user: %s' + && ($data['data']['__message']['title_params'] ?? []) === ['Alice'] + && $data['data']['user_id'] === 33; + })) + ->willReturn(57); + + $result = $this->service->createFromMessage(5, 1, $message); + $this->assertSame(57, $result); + } + + public function testCreateForTenantUsersFromMessageCreatesForAllEligibleUsers(): void + { + $message = NotificationMessage::plain('system', 'Hello', null, null, []); + + $this->utRepo->expects($this->once()) + ->method('listActiveUserIdsByTenantId') + ->with(1) + ->willReturn([10, 20, 30]); + + $this->notifRepo->expects($this->exactly(2)) + ->method('create') + ->willReturn(1); + + $result = $this->service->createForTenantUsersFromMessage(1, $message, 20); + $this->assertSame(2, $result); + } + + public function testCreateForTenantAdminUsersFromMessageRejectsEmptyAllowedSet(): void + { + $message = NotificationMessage::plain('system', 'Hello', null, null, []); + + $this->utRepo->expects($this->never())->method('listActiveUserIdsByTenantId'); + $this->notifRepo->expects($this->never())->method('create'); + + $result = $this->service->createForTenantAdminUsersFromMessage(1, $message, null, []); + $this->assertSame(0, $result); + } + + public function testListForUserLocalizesFromStoredMessageMetadata(): void + { + $this->notifRepo->expects($this->once()) + ->method('listByUser') + ->with(3, null, 50, 0) + ->willReturn([ + [ + 'id' => 1, + 'type' => 'user.created', + 'title' => 'fallback', + 'body' => null, + 'link' => null, + 'data' => json_encode([ + 'user_id' => 10, + '__message' => [ + 'title_key' => 'New user: %s', + 'title_params' => ['Alice'], + 'body_key' => '', + 'body_params' => [], + ], + ], JSON_THROW_ON_ERROR), + 'is_read' => 0, + 'created' => '2026-03-19 10:00:00', + ], + ]); + + $result = $this->service->listForUser(3); + + $this->assertCount(1, $result); + $this->assertSame(t('New user: %s', 'Alice'), $result[0]['title']); + $this->assertSame(['user_id' => 10], $result[0]['data']); + } } diff --git a/modules/notifications/web/js/components/app-notification-bell.js b/modules/notifications/web/js/components/app-notification-bell.js index 6b7bc28..ad14dbb 100644 --- a/modules/notifications/web/js/components/app-notification-bell.js +++ b/modules/notifications/web/js/components/app-notification-bell.js @@ -122,8 +122,8 @@ export function initNotificationBell(root = document, config = {}) { `${escapeHtml(timeAgo(n.created))}` + `` + `
` + - (n.is_read === 0 ? `` : '') + - `` + + (n.is_read === 0 ? `` : '') + + `` + `
`; // Keyboard + click navigation for items with a link diff --git a/pages/admin/scheduled-jobs/index().php b/pages/admin/scheduled-jobs/index().php index 23eb868..5c3f4c5 100644 --- a/pages/admin/scheduled-jobs/index().php +++ b/pages/admin/scheduled-jobs/index().php @@ -11,7 +11,7 @@ Guard::requireLogin(); Guard::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_ADMIN_JOBS_VIEW); $viewAuth['page'] = app(UiAccessService::class)->pageCapabilities( (int) ($session['user']['id'] ?? 0), - UiCapabilityMap::PAGE_AUDIT_PURGE + UiCapabilityMap::PAGE_JOBS_PURGE ); $filterSchema = require __DIR__ . '/filter-schema.php'; $filterState = gridParseFilters(requestInput()->queryAll(), gridSchemaQuery($filterSchema)); diff --git a/pages/admin/stats/index(default).phtml b/pages/admin/stats/index(default).phtml index aa1a471..dbdd206 100644 --- a/pages/admin/stats/index(default).phtml +++ b/pages/admin/stats/index(default).phtml @@ -99,9 +99,9 @@ $pageAuth = is_array($viewAuth['page'] ?? null) ? $viewAuth['page'] : []; $canViewMailLog = (bool) ($pageAuth['can_view_mail_log'] ?? false); $canViewUsers = (bool) ($pageAuth['can_view_users'] ?? false); $canViewTenants = (bool) ($pageAuth['can_view_tenants'] ?? false); -$canViewImportAudit = (bool) ($pageAuth['can_view_imports_audit'] ?? false); -$canViewSystemAudit = (bool) ($pageAuth['can_view_system_audit'] ?? false); -$canViewUserLifecycleAudit = (bool) ($pageAuth['can_view_user_lifecycle_audit'] ?? false); +$canViewImportAudit = false; +$canViewSystemAudit = false; +$canViewUserLifecycleAudit = false; $canViewAuditTab = (bool) ($auditStatsAvailable ?? false) && ( ($canViewSystemAudit && (bool) ($systemAuditStatsAvailable ?? false)) || ($canViewUserLifecycleAudit && (bool) ($userLifecycleAuditStatsAvailable ?? false)) diff --git a/templates/partials/app-main-aside.phtml b/templates/partials/app-main-aside.phtml index f466eff..4000694 100644 --- a/templates/partials/app-main-aside.phtml +++ b/templates/partials/app-main-aside.phtml @@ -14,10 +14,6 @@ $canViewJobs = (bool) ($layoutAuth['can_view_jobs'] ?? false); $canViewApiDocs = (bool) ($layoutAuth['can_view_api_docs'] ?? false); $canViewDocs = (bool) ($layoutAuth['can_view_docs'] ?? false); $canViewMailLog = (bool) ($layoutAuth['can_view_mail_log'] ?? false); -$canViewApiAudit = (bool) ($layoutAuth['can_view_api_audit'] ?? false); -$canViewSystemAudit = (bool) ($layoutAuth['can_view_system_audit'] ?? false); -$canViewImportsAudit = (bool) ($layoutAuth['can_view_imports_audit'] ?? false); -$canViewUserLifecycleAudit = (bool) ($layoutAuth['can_view_user_lifecycle_audit'] ?? false); $canViewStats = (bool) ($layoutAuth['can_view_stats'] ?? false); $canViewSystemInfo = (bool) ($layoutAuth['can_view_system_info'] ?? false); $docsDefaultSlug = DocsCatalogService::defaultSlug(); @@ -26,7 +22,7 @@ $hasOrganization = $canViewTenants || $canViewDepartments || $canViewUsers; $hasUsersSection = $canViewRoles || $canViewPermissions; $hasAutomationSection = $canViewImports || $canViewJobs || $canViewApiDocs; $hasMonitoringSection = $canViewStats; -$hasLogsSection = $canViewMailLog || $canViewApiAudit || $canViewSystemAudit || $canViewImportsAudit || $canViewUserLifecycleAudit; +$hasLogsSection = $canViewMailLog; $hasSystemSection = $canViewSettings || $canViewSystemInfo || $canViewDocs; $hasAdminPanel = layoutHasAdminPanel($layoutAuth); @@ -140,34 +136,6 @@ $adminNavGroups = [ 'visible' => $canViewMailLog, 'withTenant' => false, ], - [ - 'label' => t('API audit'), - 'path' => 'admin/api-audit', - 'active' => navActive('admin/api-audit', true), - 'visible' => $canViewApiAudit, - 'withTenant' => false, - ], - [ - 'label' => t('System audit logs'), - 'path' => 'admin/system-audit', - 'active' => navActive('admin/system-audit', true), - 'visible' => $canViewSystemAudit, - 'withTenant' => false, - ], - [ - 'label' => t('Import logs'), - 'path' => 'admin/import-audit', - 'active' => navActive('admin/import-audit', true), - 'visible' => $canViewImportsAudit, - 'withTenant' => false, - ], - [ - 'label' => t('User lifecycle logs'), - 'path' => 'admin/user-lifecycle-audit', - 'active' => navActive('admin/user-lifecycle-audit', true), - 'visible' => $canViewUserLifecycleAudit, - 'withTenant' => false, - ], ], ], [ @@ -457,28 +425,6 @@ $moduleSearchSlots = is_array($moduleSlots['search.resource_item'] ?? null) ? $m
    - - -
  • - > - - 0 - -
      -
    • - - - -
    • - > - - 0 - -
        -
      • - true, @@ -491,8 +437,6 @@ $moduleSearchSlots = is_array($moduleSlots['search.resource_item'] ?? null) ? $m 'docs' => true, 'hotkeys' => true, 'mail-log' => true, - 'api-audit' => true, - 'system-audit' => true, ]; foreach ($moduleSearchSlots as $searchSlot): if (!is_array($searchSlot)) { continue; } diff --git a/tests/Architecture/AuditModuleIsolationContractTest.php b/tests/Architecture/AuditModuleIsolationContractTest.php new file mode 100644 index 0000000..a65e830 --- /dev/null +++ b/tests/Architecture/AuditModuleIsolationContractTest.php @@ -0,0 +1,115 @@ + str_ends_with($f, '.php') + )); + + $allowed = [ + 'AuditRecorderInterface.php', + 'ImportAuditInterface.php', + 'NullAuditRecorder.php', + 'NullImportAudit.php', + 'NullUserLifecycleAudit.php', + 'UserLifecycleAuditInterface.php', + ]; + + sort($files); + sort($allowed); + + self::assertSame( + $allowed, + $files, + 'lib/Service/Audit/ should contain only interfaces and null implementations, found: ' . implode(', ', $files) + ); + } + + public function testCoreRepositoryAuditDirectoryDoesNotExist(): void + { + $root = dirname(__DIR__, 2); + self::assertDirectoryDoesNotExist( + $root . '/lib/Repository/Audit', + 'Audit repository classes should live in modules/audit/lib, not in Core lib/Repository/Audit' + ); + } + + public function testCoreDomainTaxonomyHasNoAuditEnums(): void + { + $dir = dirname(__DIR__, 2) . '/lib/Domain/Taxonomy'; + if (!is_dir($dir)) { + self::addToAssertionCount(1); + return; + } + + $entries = scandir($dir) ?: []; + $auditFiles = array_filter($entries, static fn (string $f): bool => str_contains($f, 'Audit') || str_contains($f, 'Lifecycle')); + self::assertSame( + [], + array_values($auditFiles), + 'Core lib/Domain/Taxonomy/ still contains audit enum files: ' . implode(', ', $auditFiles) + ); + } + + public function testCoreAuditPagesRemoved(): void + { + $root = dirname(__DIR__, 2); + self::assertDirectoryDoesNotExist($root . '/pages/admin/api-audit'); + self::assertDirectoryDoesNotExist($root . '/pages/admin/system-audit'); + self::assertDirectoryDoesNotExist($root . '/pages/admin/import-audit'); + self::assertDirectoryDoesNotExist($root . '/pages/admin/user-lifecycle-audit'); + self::assertDirectoryDoesNotExist($root . '/pages/admin/frontend-telemetry'); + } + + public function testCoreJsHasNoAuditPageScripts(): void + { + $root = dirname(__DIR__, 2); + self::assertFileDoesNotExist($root . '/web/js/pages/admin-api-audit-index.js'); + self::assertFileDoesNotExist($root . '/web/js/pages/admin-system-audit-index.js'); + self::assertFileDoesNotExist($root . '/web/js/pages/admin-import-audit-index.js'); + self::assertFileDoesNotExist($root . '/web/js/pages/admin-user-lifecycle-audit-index.js'); + } + + public function testCoreHttpHasNoConcreteAuditReporter(): void + { + $root = dirname(__DIR__, 2); + self::assertFileDoesNotExist( + $root . '/lib/Http/ApiSystemAuditReporter.php', + 'ApiSystemAuditReporter should live in modules/audit/lib, not in Core lib/Http' + ); + } + + public function testCoreSidebarHasNoHardcodedAuditNavigation(): void + { + $file = dirname(__DIR__, 2) . '/templates/partials/app-main-aside.phtml'; + $content = file_get_contents($file); + self::assertIsString($content); + self::assertStringNotContainsString('can_view_api_audit', $content); + self::assertStringNotContainsString('can_view_system_audit', $content); + self::assertStringNotContainsString('can_view_imports_audit', $content); + self::assertStringNotContainsString('can_view_user_lifecycle_audit', $content); + self::assertStringNotContainsString('admin/api-audit', $content); + self::assertStringNotContainsString('admin/system-audit', $content); + self::assertStringNotContainsString('admin/import-audit', $content); + self::assertStringNotContainsString('admin/user-lifecycle-audit', $content); + } + + public function testCoreSchedulerHandlerRemoved(): void + { + $root = dirname(__DIR__, 2); + self::assertFileDoesNotExist( + $root . '/lib/Service/Scheduler/Handler/SystemAuditPurgeJobHandler.php', + 'SystemAuditPurgeJobHandler should live in modules/audit, not in Core' + ); + } +} diff --git a/tests/Architecture/AuthzUiActionContractTest.php b/tests/Architecture/AuthzUiActionContractTest.php index 56a234e..cd67845 100644 --- a/tests/Architecture/AuthzUiActionContractTest.php +++ b/tests/Architecture/AuthzUiActionContractTest.php @@ -58,12 +58,7 @@ class AuthzUiActionContractTest extends TestCase 'pages/admin/users/index().php' => 'UiCapabilityMap::PAGE_USERS_INDEX', 'pages/admin/users/create().php' => 'UiCapabilityMap::PAGE_USERS_CREATE', 'pages/admin/stats/index().php' => 'UiCapabilityMap::PAGE_STATS_INDEX', - 'pages/admin/api-audit/index().php' => 'UiCapabilityMap::PAGE_AUDIT_PURGE', - 'pages/admin/system-audit/index().php' => 'UiCapabilityMap::PAGE_SYSTEM_AUDIT', - 'pages/admin/import-audit/index().php' => 'UiCapabilityMap::PAGE_AUDIT_PURGE', - 'pages/admin/scheduled-jobs/index().php' => 'UiCapabilityMap::PAGE_AUDIT_PURGE', - 'pages/admin/user-lifecycle-audit/index().php' => 'UiCapabilityMap::PAGE_AUDIT_PURGE', - 'pages/admin/user-lifecycle-audit/view($id).php' => 'UiCapabilityMap::PAGE_USER_LIFECYCLE_VIEW', + 'pages/admin/scheduled-jobs/index().php' => 'UiCapabilityMap::PAGE_JOBS_PURGE', ]; foreach ($actions as $action => $mapConstant) { @@ -72,4 +67,20 @@ class AuthzUiActionContractTest extends TestCase $this->assertStringContainsString($mapConstant, $content, $action); } } + + public function testModuleAuditActionsUseInlinePageCapabilities(): void + { + $actions = [ + 'modules/audit/pages/admin/api-audit/index().php', + 'modules/audit/pages/admin/system-audit/index().php', + 'modules/audit/pages/admin/import-audit/index().php', + 'modules/audit/pages/admin/user-lifecycle-audit/index().php', + 'modules/audit/pages/admin/user-lifecycle-audit/view($id).php', + ]; + + foreach ($actions as $action) { + $content = $this->readProjectFile($action); + $this->assertStringContainsString('->pageCapabilities(', $content, $action); + } + } } diff --git a/tests/Architecture/AuthzUiContractSupport.php b/tests/Architecture/AuthzUiContractSupport.php index f11d649..6abae79 100644 --- a/tests/Architecture/AuthzUiContractSupport.php +++ b/tests/Architecture/AuthzUiContractSupport.php @@ -36,34 +36,34 @@ trait AuthzUiContractSupport 'UiCapabilityMap::PAGE_STATS_INDEX', ], 'api audit index' => [ - 'pages/admin/api-audit/index(default).phtml', - 'pages/admin/api-audit/index().php', - 'UiCapabilityMap::PAGE_AUDIT_PURGE', + 'modules/audit/pages/admin/api-audit/index(default).phtml', + 'modules/audit/pages/admin/api-audit/index().php', + null, ], 'import audit index' => [ - 'pages/admin/import-audit/index(default).phtml', - 'pages/admin/import-audit/index().php', - 'UiCapabilityMap::PAGE_AUDIT_PURGE', + 'modules/audit/pages/admin/import-audit/index(default).phtml', + 'modules/audit/pages/admin/import-audit/index().php', + null, ], 'scheduled jobs index' => [ 'pages/admin/scheduled-jobs/index(default).phtml', 'pages/admin/scheduled-jobs/index().php', - 'UiCapabilityMap::PAGE_AUDIT_PURGE', + 'UiCapabilityMap::PAGE_JOBS_PURGE', ], 'system audit index' => [ - 'pages/admin/system-audit/index(default).phtml', - 'pages/admin/system-audit/index().php', - 'UiCapabilityMap::PAGE_SYSTEM_AUDIT', + 'modules/audit/pages/admin/system-audit/index(default).phtml', + 'modules/audit/pages/admin/system-audit/index().php', + null, ], 'user lifecycle index' => [ - 'pages/admin/user-lifecycle-audit/index(default).phtml', - 'pages/admin/user-lifecycle-audit/index().php', - 'UiCapabilityMap::PAGE_AUDIT_PURGE', + 'modules/audit/pages/admin/user-lifecycle-audit/index(default).phtml', + 'modules/audit/pages/admin/user-lifecycle-audit/index().php', + null, ], 'user lifecycle view' => [ - 'pages/admin/user-lifecycle-audit/view(default).phtml', - 'pages/admin/user-lifecycle-audit/view($id).php', - 'UiCapabilityMap::PAGE_USER_LIFECYCLE_VIEW', + 'modules/audit/pages/admin/user-lifecycle-audit/view(default).phtml', + 'modules/audit/pages/admin/user-lifecycle-audit/view($id).php', + null, ], ]; } @@ -85,6 +85,8 @@ trait AuthzUiContractSupport private function extractViewAuthPageKeys(string $content): array { $keys = []; + + // Match inline array: $viewAuth['page'] = ['key' => ...]; preg_match_all('/\\$viewAuth\\[\'page\'\\]\\s*=\\s*\\[(.*?)\\];/s', $content, $blocks); foreach ($blocks[1] as $block) { preg_match_all('/[\'"]([a-z_]+)[\'"]\\s*=>/', $block, $matches); @@ -93,6 +95,14 @@ trait AuthzUiContractSupport } } + // Match pageCapabilities() inline array: ->pageCapabilities(..., ['key' => ...]) + if (preg_match('/->pageCapabilities\\([^,]+,\\s*\\[(.*?)\\]\\s*\\)/s', $content, $pcMatch)) { + preg_match_all('/[\'"]([a-z_]+)[\'"]\\s*=>/', $pcMatch[1], $matches); + foreach ($matches[1] as $key) { + $keys[] = $key; + } + } + $keys = array_values(array_unique($keys)); sort($keys); return $keys; diff --git a/tests/Architecture/AuthzUiLayoutContractTest.php b/tests/Architecture/AuthzUiLayoutContractTest.php index f7cf3a0..a820812 100644 --- a/tests/Architecture/AuthzUiLayoutContractTest.php +++ b/tests/Architecture/AuthzUiLayoutContractTest.php @@ -40,10 +40,12 @@ class AuthzUiLayoutContractTest extends TestCase $this->assertStringContainsString('UiCapabilityMap::LAYOUT', $content); } - public function testStatsPageCapabilityMapIncludesAuditCapabilities(): void + public function testCoreCapabilityMapDoesNotHardcodeAuditCapabilities(): void { $content = $this->readProjectFile('lib/Service/Access/UiCapabilityMap.php'); - $this->assertStringContainsString("'can_view_system_audit' => OperationsAuthorizationPolicy::ABILITY_ADMIN_SYSTEM_AUDIT_VIEW", $content); - $this->assertStringContainsString("'can_view_user_lifecycle_audit' => OperationsAuthorizationPolicy::ABILITY_ADMIN_USER_LIFECYCLE_AUDIT_VIEW", $content); + $this->assertStringNotContainsString('can_view_system_audit', $content); + $this->assertStringNotContainsString('can_view_api_audit', $content); + $this->assertStringNotContainsString('can_view_user_lifecycle_audit', $content); + $this->assertStringNotContainsString('can_view_imports_audit', $content); } } diff --git a/tests/Architecture/AuthzUiTemplateContractTest.php b/tests/Architecture/AuthzUiTemplateContractTest.php index 056dca4..6e5ff57 100644 --- a/tests/Architecture/AuthzUiTemplateContractTest.php +++ b/tests/Architecture/AuthzUiTemplateContractTest.php @@ -74,13 +74,13 @@ class AuthzUiTemplateContractTest extends TestCase $templates = [ 'templates/partials/app-main-aside.phtml', 'templates/partials/app-main-aside-icon-bar.phtml', - 'pages/admin/api-audit/index(default).phtml', - 'pages/admin/system-audit/index(default).phtml', - 'pages/admin/import-audit/index(default).phtml', + 'modules/audit/pages/admin/api-audit/index(default).phtml', + 'modules/audit/pages/admin/system-audit/index(default).phtml', + 'modules/audit/pages/admin/import-audit/index(default).phtml', 'pages/admin/scheduled-jobs/index(default).phtml', 'pages/admin/stats/index(default).phtml', - 'pages/admin/user-lifecycle-audit/index(default).phtml', - 'pages/admin/user-lifecycle-audit/view(default).phtml', + 'modules/audit/pages/admin/user-lifecycle-audit/index(default).phtml', + 'modules/audit/pages/admin/user-lifecycle-audit/view(default).phtml', 'pages/admin/users/index(default).phtml', 'pages/admin/users/create(default).phtml', 'pages/admin/tenants/edit(default).phtml', diff --git a/tests/Architecture/ContainerFactoryContractTest.php b/tests/Architecture/ContainerFactoryContractTest.php index 2a5d07d..47ddec4 100644 --- a/tests/Architecture/ContainerFactoryContractTest.php +++ b/tests/Architecture/ContainerFactoryContractTest.php @@ -17,7 +17,7 @@ class ContainerFactoryContractTest extends TestCase return [ [\MintyPHP\Service\Access\AccessServicesFactory::class], [\MintyPHP\Module\AddressBook\Service\AddressBookServicesFactory::class], - [\MintyPHP\Service\Audit\AuditServicesFactory::class], + [\MintyPHP\Module\Audit\Service\AuditServicesFactory::class], [\MintyPHP\Service\Auth\AuthServicesFactory::class], [\MintyPHP\Service\Branding\BrandingServicesFactory::class], [\MintyPHP\Service\Directory\DirectoryServicesFactory::class], diff --git a/tests/Architecture/FrontendTelemetryContractTest.php b/tests/Architecture/FrontendTelemetryContractTest.php index f0c0b21..b089f07 100644 --- a/tests/Architecture/FrontendTelemetryContractTest.php +++ b/tests/Architecture/FrontendTelemetryContractTest.php @@ -11,7 +11,7 @@ class FrontendTelemetryContractTest extends TestCase public function testEventTypeMappingsStayInSyncAcrossFrontendAndBackend(): void { $js = $this->readProjectFile('web/js/core/app-telemetry.js'); - $ingest = $this->readProjectFile('lib/Service/Audit/FrontendTelemetryIngestService.php'); + $ingest = $this->readProjectFile('modules/audit/lib/Module/Audit/Service/FrontendTelemetryIngestService.php'); $settings = $this->readProjectFile('lib/Service/Settings/SettingsFrontendTelemetryGateway.php'); $jsMap = $this->parseJsEventKeyToTypeMap($js); @@ -35,7 +35,7 @@ class FrontendTelemetryContractTest extends TestCase public function testAllowedMetaKeysStayInSyncAcrossFrontendAndBackend(): void { $js = $this->readProjectFile('web/js/core/app-telemetry.js'); - $ingest = $this->readProjectFile('lib/Service/Audit/FrontendTelemetryIngestService.php'); + $ingest = $this->readProjectFile('modules/audit/lib/Module/Audit/Service/FrontendTelemetryIngestService.php'); $jsKeys = $this->parseJsStringSet($js, 'ALLOWED_META_KEYS'); $phpKeys = $this->parsePhpStringArrayConstant($ingest, 'ALLOWED_META_KEYS'); @@ -48,7 +48,7 @@ class FrontendTelemetryContractTest extends TestCase public function testLengthLimitsStayInSyncAcrossFrontendAndBackend(): void { $js = $this->readProjectFile('web/js/core/app-telemetry.js'); - $ingest = $this->readProjectFile('lib/Service/Audit/FrontendTelemetryIngestService.php'); + $ingest = $this->readProjectFile('modules/audit/lib/Module/Audit/Service/FrontendTelemetryIngestService.php'); $this->assertSame( $this->parseJsIntConstant($js, 'MAX_MESSAGE_LENGTH'), diff --git a/tests/Architecture/ListContractFiles.php b/tests/Architecture/ListContractFiles.php index 11d7942..09eb4fb 100644 --- a/tests/Architecture/ListContractFiles.php +++ b/tests/Architecture/ListContractFiles.php @@ -53,10 +53,10 @@ trait ListContractFiles 'pages/admin/mail-log/data().php', 'pages/admin/scheduled-jobs/data().php', 'pages/admin/scheduled-jobs/runs-data($id).php', - 'pages/admin/api-audit/data().php', - 'pages/admin/import-audit/data().php', - 'pages/admin/user-lifecycle-audit/data().php', - 'pages/admin/system-audit/data().php', + 'modules/audit/pages/admin/api-audit/data().php', + 'modules/audit/pages/admin/import-audit/data().php', + 'modules/audit/pages/admin/user-lifecycle-audit/data().php', + 'modules/audit/pages/admin/system-audit/data().php', ]; } @@ -76,10 +76,10 @@ trait ListContractFiles 'pages/admin/mail-log/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", 'pages/admin/scheduled-jobs/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", 'pages/admin/scheduled-jobs/runs-data($id).php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/runs-filter-schema.php'", - 'pages/admin/api-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", - 'pages/admin/import-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", - 'pages/admin/user-lifecycle-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", - 'pages/admin/system-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", + 'modules/audit/pages/admin/api-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", + 'modules/audit/pages/admin/import-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", + 'modules/audit/pages/admin/user-lifecycle-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", + 'modules/audit/pages/admin/system-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", ]; } @@ -98,10 +98,10 @@ trait ListContractFiles 'pages/admin/permissions/index(default).phtml', 'pages/admin/mail-log/index(default).phtml', 'pages/admin/scheduled-jobs/index(default).phtml', - 'pages/admin/api-audit/index(default).phtml', - 'pages/admin/import-audit/index(default).phtml', - 'pages/admin/user-lifecycle-audit/index(default).phtml', - 'pages/admin/system-audit/index(default).phtml', + 'modules/audit/pages/admin/api-audit/index(default).phtml', + 'modules/audit/pages/admin/import-audit/index(default).phtml', + 'modules/audit/pages/admin/user-lifecycle-audit/index(default).phtml', + 'modules/audit/pages/admin/system-audit/index(default).phtml', ]; } @@ -120,10 +120,10 @@ trait ListContractFiles 'pages/admin/permissions/index().php', 'pages/admin/mail-log/index().php', 'pages/admin/scheduled-jobs/index().php', - 'pages/admin/api-audit/index().php', - 'pages/admin/import-audit/index().php', - 'pages/admin/user-lifecycle-audit/index().php', - 'pages/admin/system-audit/index().php', + 'modules/audit/pages/admin/api-audit/index().php', + 'modules/audit/pages/admin/import-audit/index().php', + 'modules/audit/pages/admin/user-lifecycle-audit/index().php', + 'modules/audit/pages/admin/system-audit/index().php', ]; } @@ -141,10 +141,10 @@ trait ListContractFiles 'pages/admin/permissions/index(default).phtml', 'pages/admin/mail-log/index(default).phtml', 'pages/admin/scheduled-jobs/index(default).phtml', - 'pages/admin/api-audit/index(default).phtml', - 'pages/admin/import-audit/index(default).phtml', - 'pages/admin/user-lifecycle-audit/index(default).phtml', - 'pages/admin/system-audit/index(default).phtml', + 'modules/audit/pages/admin/api-audit/index(default).phtml', + 'modules/audit/pages/admin/import-audit/index(default).phtml', + 'modules/audit/pages/admin/user-lifecycle-audit/index(default).phtml', + 'modules/audit/pages/admin/system-audit/index(default).phtml', ]; } diff --git a/tests/Architecture/ListDataEndpointContractTest.php b/tests/Architecture/ListDataEndpointContractTest.php index b76c9b5..1140f76 100644 --- a/tests/Architecture/ListDataEndpointContractTest.php +++ b/tests/Architecture/ListDataEndpointContractTest.php @@ -46,10 +46,10 @@ class ListDataEndpointContractTest extends TestCase public function testAuditRepositoriesDoNotUseLegacySingleIdAliasFallbacks(): void { $files = [ - 'lib/Repository/Audit/ApiAuditLogRepository.php', - 'lib/Repository/Audit/ImportAuditRunRepository.php', - 'lib/Repository/Audit/UserLifecycleAuditRepository.php', - 'lib/Repository/Audit/SystemAuditLogRepository.php', + 'modules/audit/lib/Module/Audit/Repository/ApiAuditLogRepository.php', + 'modules/audit/lib/Module/Audit/Repository/ImportAuditRunRepository.php', + 'modules/audit/lib/Module/Audit/Repository/UserLifecycleAuditRepository.php', + 'modules/audit/lib/Module/Audit/Repository/SystemAuditLogRepository.php', ]; foreach ($files as $file) { diff --git a/tests/Architecture/ListTitlebarContractFiles.php b/tests/Architecture/ListTitlebarContractFiles.php index 0e33cc5..fb6cdbf 100644 --- a/tests/Architecture/ListTitlebarContractFiles.php +++ b/tests/Architecture/ListTitlebarContractFiles.php @@ -17,10 +17,10 @@ final class ListTitlebarContractFiles 'pages/admin/permissions/index(default).phtml', 'pages/admin/mail-log/index(default).phtml', 'pages/admin/scheduled-jobs/index(default).phtml', - 'pages/admin/api-audit/index(default).phtml', - 'pages/admin/import-audit/index(default).phtml', - 'pages/admin/user-lifecycle-audit/index(default).phtml', - 'pages/admin/system-audit/index(default).phtml', + 'modules/audit/pages/admin/api-audit/index(default).phtml', + 'modules/audit/pages/admin/import-audit/index(default).phtml', + 'modules/audit/pages/admin/user-lifecycle-audit/index(default).phtml', + 'modules/audit/pages/admin/system-audit/index(default).phtml', 'modules/addressbook/pages/address-book/index(default).phtml', 'pages/search/index(default).phtml', 'pages/help/hotkeys(default).phtml', diff --git a/tests/Architecture/ListUiSharedPartialsContractTest.php b/tests/Architecture/ListUiSharedPartialsContractTest.php index 1b50196..b18efea 100644 --- a/tests/Architecture/ListUiSharedPartialsContractTest.php +++ b/tests/Architecture/ListUiSharedPartialsContractTest.php @@ -26,10 +26,10 @@ class ListUiSharedPartialsContractTest extends TestCase { return [ 'pages/admin/scheduled-jobs/index(default).phtml', - 'pages/admin/api-audit/index(default).phtml', - 'pages/admin/import-audit/index(default).phtml', - 'pages/admin/user-lifecycle-audit/index(default).phtml', - 'pages/admin/system-audit/index(default).phtml', + 'modules/audit/pages/admin/api-audit/index(default).phtml', + 'modules/audit/pages/admin/import-audit/index(default).phtml', + 'modules/audit/pages/admin/user-lifecycle-audit/index(default).phtml', + 'modules/audit/pages/admin/system-audit/index(default).phtml', ]; } diff --git a/tests/Architecture/SecurityLoggingRedactionContractTest.php b/tests/Architecture/SecurityLoggingRedactionContractTest.php index 28fae24..d0865cf 100644 --- a/tests/Architecture/SecurityLoggingRedactionContractTest.php +++ b/tests/Architecture/SecurityLoggingRedactionContractTest.php @@ -10,7 +10,7 @@ final class SecurityLoggingRedactionContractTest extends TestCase public function testSystemAuditRedactionCoversCredentialsTokensAndEmailFields(): void { - $content = $this->readProjectFile('lib/Service/Audit/SystemAuditRedactionService.php'); + $content = $this->readProjectFile('modules/audit/lib/Module/Audit/Service/SystemAuditRedactionService.php'); foreach ([ "'password'", @@ -37,7 +37,7 @@ final class SecurityLoggingRedactionContractTest extends TestCase public function testApiAuditRedactionCoversCredentialsAndAuthorizationFields(): void { - $content = $this->readProjectFile('lib/Service/Audit/ApiAuditService.php'); + $content = $this->readProjectFile('modules/audit/lib/Module/Audit/Service/ApiAuditService.php'); foreach ([ "'token'", diff --git a/tests/Architecture/SecurityLoggingRuntimeContractTest.php b/tests/Architecture/SecurityLoggingRuntimeContractTest.php index dfd9a4f..6c1793a 100644 --- a/tests/Architecture/SecurityLoggingRuntimeContractTest.php +++ b/tests/Architecture/SecurityLoggingRuntimeContractTest.php @@ -12,17 +12,18 @@ final class SecurityLoggingRuntimeContractTest extends TestCase { $violations = array_merge( $this->findPatternMatchesInPhpFiles('lib', '/\berror_log\s*\(/'), - $this->findPatternMatchesInPhpFiles('pages', '/\berror_log\s*\(/') + $this->findPatternMatchesInPhpFiles('pages', '/\berror_log\s*\(/'), + $this->findPatternMatchesInPhpFiles('modules', '/\berror_log\s*\(/') ); sort($violations); self::assertSame( - ['pages/admin/frontend-telemetry/ingest().php'], + ['modules/audit/pages/admin/frontend-telemetry/ingest().php'], $violations, "Unexpected error_log() usage outside the telemetry ingest fallback:\n" . implode("\n", $violations) ); - $content = $this->readProjectFile('pages/admin/frontend-telemetry/ingest().php'); + $content = $this->readProjectFile('modules/audit/pages/admin/frontend-telemetry/ingest().php'); self::assertStringContainsString( "error_log('[frontend-telemetry] ingest record_failed request_id=' . RequestContext::id());", $content diff --git a/tests/Architecture/SecurityLoggingTelemetryContractTest.php b/tests/Architecture/SecurityLoggingTelemetryContractTest.php index 656f67b..9a03f75 100644 --- a/tests/Architecture/SecurityLoggingTelemetryContractTest.php +++ b/tests/Architecture/SecurityLoggingTelemetryContractTest.php @@ -10,7 +10,7 @@ final class SecurityLoggingTelemetryContractTest extends TestCase public function testFrontendTelemetrySanitizesSecretsAndPiiMarkers(): void { - $content = $this->readProjectFile('lib/Service/Audit/FrontendTelemetryIngestService.php'); + $content = $this->readProjectFile('modules/audit/lib/Module/Audit/Service/FrontendTelemetryIngestService.php'); self::assertStringContainsString('[REDACTED]', $content); self::assertStringContainsString('[REDACTED_EMAIL]', $content); diff --git a/tests/Architecture/StatusTaxonomyContractFiles.php b/tests/Architecture/StatusTaxonomyContractFiles.php index 9889698..d7617f0 100644 --- a/tests/Architecture/StatusTaxonomyContractFiles.php +++ b/tests/Architecture/StatusTaxonomyContractFiles.php @@ -10,12 +10,12 @@ final class StatusTaxonomyContractFiles public static function taxonomyEnumFiles(): array { return [ - 'lib/Domain/Taxonomy/SystemAuditOutcome.php', - 'lib/Domain/Taxonomy/SystemAuditChannel.php', - 'lib/Domain/Taxonomy/UserLifecycleAction.php', - 'lib/Domain/Taxonomy/UserLifecycleTriggerType.php', - 'lib/Domain/Taxonomy/UserLifecycleStatus.php', - 'lib/Domain/Taxonomy/ImportAuditStatus.php', + 'modules/audit/lib/Module/Audit/Domain/SystemAuditOutcome.php', + 'modules/audit/lib/Module/Audit/Domain/SystemAuditChannel.php', + 'modules/audit/lib/Module/Audit/Domain/UserLifecycleAction.php', + 'modules/audit/lib/Module/Audit/Domain/UserLifecycleTriggerType.php', + 'modules/audit/lib/Module/Audit/Domain/UserLifecycleStatus.php', + 'modules/audit/lib/Module/Audit/Domain/ImportAuditStatus.php', 'lib/Domain/Taxonomy/ScheduledJobStatus.php', 'lib/Domain/Taxonomy/ScheduledJobRunStatus.php', 'lib/Domain/Taxonomy/ScheduledJobTriggerType.php', @@ -31,16 +31,16 @@ final class StatusTaxonomyContractFiles public static function taxonomySchemaExpectations(): array { return [ - 'pages/admin/system-audit/filter-schema.php' => [ + 'modules/audit/pages/admin/system-audit/filter-schema.php' => [ 'SystemAuditOutcome::values()', 'SystemAuditChannel::values()', ], - 'pages/admin/user-lifecycle-audit/filter-schema.php' => [ + 'modules/audit/pages/admin/user-lifecycle-audit/filter-schema.php' => [ 'gridEnumSanitizer(UserLifecycleAction::class)', 'gridEnumSanitizer(UserLifecycleStatus::class)', 'gridEnumSanitizer(UserLifecycleTriggerType::class)', ], - 'pages/admin/import-audit/filter-schema.php' => [ + 'modules/audit/pages/admin/import-audit/filter-schema.php' => [ 'ImportAuditStatus::values()', ], 'pages/admin/scheduled-jobs/filter-schema.php' => [ @@ -62,9 +62,9 @@ final class StatusTaxonomyContractFiles public static function taxonomyDataEndpointFiles(): array { return [ - 'pages/admin/system-audit/data().php', - 'pages/admin/user-lifecycle-audit/data().php', - 'pages/admin/import-audit/data().php', + 'modules/audit/pages/admin/system-audit/data().php', + 'modules/audit/pages/admin/user-lifecycle-audit/data().php', + 'modules/audit/pages/admin/import-audit/data().php', 'pages/admin/scheduled-jobs/data().php', 'pages/admin/scheduled-jobs/runs-data($id).php', 'pages/admin/mail-log/data().php', @@ -78,13 +78,13 @@ final class StatusTaxonomyContractFiles public static function taxonomyLiteralGuardFiles(): array { return [ - 'lib/Service/Audit/SystemAuditService.php', - 'lib/Repository/Audit/SystemAuditLogRepository.php', - 'lib/Http/ApiSystemAuditReporter.php', - 'lib/Service/Audit/UserLifecycleAuditService.php', - 'lib/Repository/Audit/UserLifecycleAuditRepository.php', - 'lib/Service/Audit/ImportAuditService.php', - 'lib/Repository/Audit/ImportAuditRunRepository.php', + 'modules/audit/lib/Module/Audit/Service/SystemAuditService.php', + 'modules/audit/lib/Module/Audit/Repository/SystemAuditLogRepository.php', + 'modules/audit/lib/Module/Audit/Http/ApiSystemAuditReporter.php', + 'modules/audit/lib/Module/Audit/Service/UserLifecycleAuditService.php', + 'modules/audit/lib/Module/Audit/Repository/UserLifecycleAuditRepository.php', + 'modules/audit/lib/Module/Audit/Service/ImportAuditService.php', + 'modules/audit/lib/Module/Audit/Repository/ImportAuditRunRepository.php', 'lib/Service/Scheduler/SchedulerRunService.php', 'lib/Repository/Scheduler/ScheduledJobRepository.php', 'lib/Repository/Scheduler/ScheduledJobRunRepository.php', @@ -94,16 +94,16 @@ final class StatusTaxonomyContractFiles 'lib/Service/Tenant/TenantService.php', 'lib/Repository/Tenant/TenantRepository.php', 'lib/Repository/Stats/AdminStatsRepository.php', - 'pages/admin/system-audit/data().php', - 'pages/admin/user-lifecycle-audit/data().php', - 'pages/admin/import-audit/data().php', + 'modules/audit/pages/admin/system-audit/data().php', + 'modules/audit/pages/admin/user-lifecycle-audit/data().php', + 'modules/audit/pages/admin/import-audit/data().php', 'pages/admin/scheduled-jobs/data().php', 'pages/admin/scheduled-jobs/runs-data($id).php', 'pages/admin/mail-log/data().php', 'pages/admin/tenants/data().php', - 'pages/admin/system-audit/index().php', - 'pages/admin/user-lifecycle-audit/index().php', - 'pages/admin/import-audit/index().php', + 'modules/audit/pages/admin/system-audit/index().php', + 'modules/audit/pages/admin/user-lifecycle-audit/index().php', + 'modules/audit/pages/admin/import-audit/index().php', 'pages/admin/scheduled-jobs/index().php', 'pages/admin/mail-log/index().php', ]; diff --git a/tests/Architecture/StatusTaxonomyUiContractTest.php b/tests/Architecture/StatusTaxonomyUiContractTest.php index ba86382..015d39d 100644 --- a/tests/Architecture/StatusTaxonomyUiContractTest.php +++ b/tests/Architecture/StatusTaxonomyUiContractTest.php @@ -10,11 +10,11 @@ final class StatusTaxonomyUiContractTest extends TestCase public function testImportAuditIndexTemplateDoesNotUseLegacyStatusMapper(): void { - $content = $this->readProjectFile('pages/admin/import-audit/index(default).phtml'); - $moduleContent = $this->readProjectFile('web/js/pages/admin-import-audit-index.js'); + $content = $this->readProjectFile('modules/audit/pages/admin/import-audit/index(default).phtml'); + $moduleContent = $this->readProjectFile('modules/audit/web/js/pages/admin-import-audit-index.js'); $this->assertStringNotContainsString('const statusLabel = (value) =>', $moduleContent); $this->assertStringNotContainsString("if (key === 'running')", $moduleContent); $this->assertStringContainsString('cell.label || \'-\'', $moduleContent); - $this->assertStringContainsString("assetVersion('js/pages/admin-import-audit-index.js')", $content); + $this->assertStringContainsString("assetVersion('modules/audit/js/pages/admin-import-audit-index.js')", $content); } } diff --git a/tests/Domain/Taxonomy/TaxonomyEnumContractTest.php b/tests/Domain/Taxonomy/TaxonomyEnumContractTest.php index 49088b8..6712144 100644 --- a/tests/Domain/Taxonomy/TaxonomyEnumContractTest.php +++ b/tests/Domain/Taxonomy/TaxonomyEnumContractTest.php @@ -2,18 +2,18 @@ namespace MintyPHP\Tests\Domain\Taxonomy; -use MintyPHP\Domain\Taxonomy\ImportAuditStatus; use MintyPHP\Domain\Taxonomy\MailLogStatus; use MintyPHP\Domain\Taxonomy\ScheduledJobRunStatus; use MintyPHP\Domain\Taxonomy\ScheduledJobStatus; use MintyPHP\Domain\Taxonomy\ScheduledJobTriggerType; use MintyPHP\Domain\Taxonomy\SchedulerRuntimeResult; -use MintyPHP\Domain\Taxonomy\SystemAuditChannel; -use MintyPHP\Domain\Taxonomy\SystemAuditOutcome; use MintyPHP\Domain\Taxonomy\TenantStatus; -use MintyPHP\Domain\Taxonomy\UserLifecycleAction; -use MintyPHP\Domain\Taxonomy\UserLifecycleStatus; -use MintyPHP\Domain\Taxonomy\UserLifecycleTriggerType; +use MintyPHP\Module\Audit\Domain\ImportAuditStatus; +use MintyPHP\Module\Audit\Domain\SystemAuditChannel; +use MintyPHP\Module\Audit\Domain\SystemAuditOutcome; +use MintyPHP\Module\Audit\Domain\UserLifecycleAction; +use MintyPHP\Module\Audit\Domain\UserLifecycleStatus; +use MintyPHP\Module\Audit\Domain\UserLifecycleTriggerType; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; diff --git a/tests/Service/Access/OperationsAuthorizationPolicyTest.php b/tests/Service/Access/OperationsAuthorizationPolicyTest.php index d5ab290..930af89 100644 --- a/tests/Service/Access/OperationsAuthorizationPolicyTest.php +++ b/tests/Service/Access/OperationsAuthorizationPolicyTest.php @@ -110,20 +110,6 @@ class OperationsAuthorizationPolicyTest extends TestCase $this->assertForbiddenDecision($decision); } - public function testSystemAuditPurgeAllowedWithPermission(): void - { - $permissionService = $this->permissionGatewayAllowing([ - 3 => [PermissionService::SYSTEM_AUDIT_PURGE], - ]); - - $policy = new OperationsAuthorizationPolicy($permissionService); - $decision = $policy->authorize(OperationsAuthorizationPolicy::ABILITY_ADMIN_SYSTEM_AUDIT_PURGE, [ - 'actor_user_id' => 3, - ]); - - $this->assertTrue($decision->isAllowed()); - } - // --- authorize: authorizeUsersCreateCustomFields (requires TWO permissions) --- public function testUsersCreateCustomFieldsAllowedWithBothPermissions(): void diff --git a/tests/Service/Access/PermissionServiceTest.php b/tests/Service/Access/PermissionServiceTest.php index c2c4c8e..618b21e 100644 --- a/tests/Service/Access/PermissionServiceTest.php +++ b/tests/Service/Access/PermissionServiceTest.php @@ -7,7 +7,7 @@ use MintyPHP\Repository\Access\PermissionRepositoryInterface; use MintyPHP\Repository\Access\RolePermissionRepositoryInterface; use MintyPHP\Repository\Access\UserRoleRepositoryInterface; use MintyPHP\Service\Access\PermissionService; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -16,7 +16,7 @@ class PermissionServiceTest extends TestCase private PermissionRepositoryInterface&MockObject $permissionRepository; private RolePermissionRepositoryInterface&MockObject $rolePermissionRepository; private UserRoleRepositoryInterface&MockObject $userRoleRepository; - private SystemAuditService&MockObject $systemAuditService; + private AuditRecorderInterface&MockObject $systemAuditService; private SessionStoreInterface&MockObject $sessionStore; private PermissionService $service; @@ -25,7 +25,7 @@ class PermissionServiceTest extends TestCase $this->permissionRepository = $this->createMock(PermissionRepositoryInterface::class); $this->rolePermissionRepository = $this->createMock(RolePermissionRepositoryInterface::class); $this->userRoleRepository = $this->createMock(UserRoleRepositoryInterface::class); - $this->systemAuditService = $this->createMock(SystemAuditService::class); + $this->systemAuditService = $this->createMock(AuditRecorderInterface::class); $this->sessionStore = $this->createMock(SessionStoreInterface::class); $this->service = new PermissionService( diff --git a/tests/Service/Access/RoleServiceTest.php b/tests/Service/Access/RoleServiceTest.php index c50244b..c68cf23 100644 --- a/tests/Service/Access/RoleServiceTest.php +++ b/tests/Service/Access/RoleServiceTest.php @@ -4,7 +4,7 @@ namespace MintyPHP\Tests\Service\Access; use MintyPHP\Repository\Access\RoleRepositoryInterface; use MintyPHP\Service\Access\RoleService; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Directory\DirectorySettingsGateway; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -13,14 +13,14 @@ class RoleServiceTest extends TestCase { private RoleRepositoryInterface&MockObject $roleRepo; private DirectorySettingsGateway&MockObject $settingsGateway; - private SystemAuditService&MockObject $auditService; + private AuditRecorderInterface&MockObject $auditService; private RoleService $service; protected function setUp(): void { $this->roleRepo = $this->createMock(RoleRepositoryInterface::class); $this->settingsGateway = $this->createMock(DirectorySettingsGateway::class); - $this->auditService = $this->createMock(SystemAuditService::class); + $this->auditService = $this->createMock(AuditRecorderInterface::class); $this->service = new RoleService( $this->roleRepo, diff --git a/tests/Service/Auth/AuthServiceTest.php b/tests/Service/Auth/AuthServiceTest.php index 97802b7..688f733 100644 --- a/tests/Service/Auth/AuthServiceTest.php +++ b/tests/Service/Auth/AuthServiceTest.php @@ -6,7 +6,7 @@ use MintyPHP\Http\SessionStoreInterface; use MintyPHP\Repository\User\UserReadRepositoryInterface; use MintyPHP\Repository\User\UserWriteRepositoryInterface; use MintyPHP\Service\Access\PermissionService; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Auth\AuthService; use MintyPHP\Service\Auth\AuthSessionTenantContextService; use MintyPHP\Service\Auth\EmailVerificationService; @@ -265,7 +265,7 @@ class AuthServiceTest extends TestCase $tenantCtxService = $this->createMock(AuthSessionTenantContextService::class); $tenantCtxService->expects($this->once())->method('hydrateForUser')->with(5); - $audit = $this->createMock(SystemAuditService::class); + $audit = $this->createMock(AuditRecorderInterface::class); $audit->expects($this->once())->method('record') ->with('auth.login.success', 'success', $this->anything()); @@ -301,7 +301,7 @@ class AuthServiceTest extends TestCase return $default; }); - $audit = $this->createMock(SystemAuditService::class); + $audit = $this->createMock(AuditRecorderInterface::class); $audit->expects($this->once())->method('record') ->with('auth.login.failed', 'failed', $this->anything()); @@ -360,7 +360,7 @@ class AuthServiceTest extends TestCase 'email_verified_at' => null, ]); - $audit = $this->createMock(SystemAuditService::class); + $audit = $this->createMock(AuditRecorderInterface::class); $audit->expects($this->once())->method('record') ->with('auth.login.failed', 'failed', $this->callback(function (array $ctx): bool { return ($ctx['error_code'] ?? '') === 'email_not_verified'; @@ -398,7 +398,7 @@ class AuthServiceTest extends TestCase $rememberMeService = $this->createMock(RememberMeService::class); $rememberMeService->expects($this->never())->method('forgetCurrentUser'); - $audit = $this->createMock(SystemAuditService::class); + $audit = $this->createMock(AuditRecorderInterface::class); $audit->expects($this->once())->method('record') ->with('auth.session.timeout', 'success', $this->callback(function (array $context): bool { return (int) ($context['actor_user_id'] ?? 0) === 5 @@ -432,7 +432,7 @@ class AuthServiceTest extends TestCase ['current_tenant', [], ['id' => 11]], ]); - $audit = $this->createMock(SystemAuditService::class); + $audit = $this->createMock(AuditRecorderInterface::class); $audit->expects($this->once())->method('record') ->with('auth.logout', 'success', $this->callback(function (array $context): bool { return (int) ($context['actor_user_id'] ?? 0) === 5 @@ -462,7 +462,7 @@ class AuthServiceTest extends TestCase ?PermissionService $permissionService = null, ?TenantSsoService $tenantSsoService = null, ?AuthSessionTenantContextService $authSessionTenantContextService = null, - ?SystemAuditService $systemAuditService = null, + ?AuditRecorderInterface $systemAuditService = null, ?SessionStoreInterface $sessionStore = null ): AuthService { return new AuthService( @@ -475,7 +475,7 @@ class AuthServiceTest extends TestCase $permissionService ?? $this->createMock(PermissionService::class), $tenantSsoService ?? $this->createMock(TenantSsoService::class), $authSessionTenantContextService ?? $this->createMock(AuthSessionTenantContextService::class), - $systemAuditService ?? $this->createMock(SystemAuditService::class), + $systemAuditService ?? $this->createMock(AuditRecorderInterface::class), $sessionStore ?? $this->createMock(SessionStoreInterface::class) ); } diff --git a/tests/Service/Import/ImportServiceTest.php b/tests/Service/Import/ImportServiceTest.php index d98d074..ddebf46 100644 --- a/tests/Service/Import/ImportServiceTest.php +++ b/tests/Service/Import/ImportServiceTest.php @@ -4,7 +4,7 @@ namespace MintyPHP\Tests\Service\Import; use MintyPHP\Http\SessionStoreInterface; use MintyPHP\Service\Access\PermissionService; -use MintyPHP\Service\Audit\ImportAuditService; +use MintyPHP\Service\Audit\ImportAuditInterface; use MintyPHP\Service\Import\CsvReaderService; use MintyPHP\Service\Import\ImportService; use MintyPHP\Service\Import\ImportStateStoreService; @@ -117,7 +117,7 @@ class ImportServiceTest extends TestCase ): ImportService { $csvReader = $csvReader ?? $this->createMock(CsvReaderService::class); $tempFileService = $tempFileService ?? $this->createMock(ImportTempFileService::class); - $auditService = $this->createMock(ImportAuditService::class); + $auditService = $this->createMock(ImportAuditInterface::class); $stateStore = $stateStore ?? $this->createMock(ImportStateStoreService::class); $permissionService = $this->createMock(PermissionService::class); $permissionService->method('userHas')->willReturn(false); diff --git a/tests/Service/Org/DepartmentServiceTest.php b/tests/Service/Org/DepartmentServiceTest.php index 4b74625..f89fc50 100644 --- a/tests/Service/Org/DepartmentServiceTest.php +++ b/tests/Service/Org/DepartmentServiceTest.php @@ -4,7 +4,7 @@ namespace MintyPHP\Tests\Service\Org; use MintyPHP\Repository\Org\DepartmentRepositoryInterface; use MintyPHP\Repository\Org\UserDepartmentRepositoryInterface; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Directory\DirectorySettingsGateway; use MintyPHP\Service\Org\DepartmentService; use MintyPHP\Service\Tenant\TenantScopeService; @@ -19,7 +19,7 @@ class DepartmentServiceTest extends TestCase private DepartmentRepositoryInterface&MockObject $departmentRepository; private DirectorySettingsGateway&MockObject $settingsGateway; private TenantScopeService&MockObject $scopeGateway; - private SystemAuditService&MockObject $systemAuditService; + private AuditRecorderInterface&MockObject $systemAuditService; private DepartmentService $service; protected function setUp(): void @@ -28,7 +28,7 @@ class DepartmentServiceTest extends TestCase $this->departmentRepository = $this->createMock(DepartmentRepositoryInterface::class); $this->settingsGateway = $this->createMock(DirectorySettingsGateway::class); $this->scopeGateway = $this->createMock(TenantScopeService::class); - $this->systemAuditService = $this->createMock(SystemAuditService::class); + $this->systemAuditService = $this->createMock(AuditRecorderInterface::class); $this->service = new DepartmentService( $this->userServicesFactory, diff --git a/tests/Service/Scheduler/ScheduledJobRegistryTest.php b/tests/Service/Scheduler/ScheduledJobRegistryTest.php index 24f1f1f..dd8a2ee 100644 --- a/tests/Service/Scheduler/ScheduledJobRegistryTest.php +++ b/tests/Service/Scheduler/ScheduledJobRegistryTest.php @@ -4,7 +4,6 @@ namespace MintyPHP\Tests\Service\Scheduler; use MintyPHP\App\AppContainer; use MintyPHP\App\Module\ModuleRegistry; -use MintyPHP\Service\Audit\SystemAuditService; use MintyPHP\Service\Scheduler\Handler\ScheduledJobHandlerInterface; use MintyPHP\Service\Scheduler\ScheduledJobRegistry; use MintyPHP\Service\User\UserLifecycleService; @@ -37,7 +36,6 @@ final class ScheduledJobRegistryTest extends TestCase $registry = new ScheduledJobRegistry( $this->createMock(UserLifecycleService::class), - $this->createMock(SystemAuditService::class), $container ); diff --git a/tests/Service/Scheduler/SchedulerRunServiceTest.php b/tests/Service/Scheduler/SchedulerRunServiceTest.php index f841edf..812d704 100644 --- a/tests/Service/Scheduler/SchedulerRunServiceTest.php +++ b/tests/Service/Scheduler/SchedulerRunServiceTest.php @@ -6,7 +6,7 @@ use MintyPHP\Repository\Scheduler\ScheduledJobRepository; use MintyPHP\Repository\Scheduler\ScheduledJobRunRepository; use MintyPHP\Repository\Scheduler\SchedulerRuntimeRepository; use MintyPHP\Repository\Support\DatabaseSessionRepository; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Scheduler\ScheduleCalculator; use MintyPHP\Service\Scheduler\ScheduledJobRegistry; use MintyPHP\Service\Scheduler\ScheduledJobService; @@ -19,7 +19,7 @@ class SchedulerRunServiceTest extends TestCase { $scheduledJobService = $this->createMock(ScheduledJobService::class); $scheduledJobService->expects($this->once())->method('ensureSystemJobs'); - $systemAuditService = $this->createMock(SystemAuditService::class); + $systemAuditService = $this->createMock(AuditRecorderInterface::class); $systemAuditService->expects($this->once()) ->method('record') ->with( @@ -86,7 +86,7 @@ class SchedulerRunServiceTest extends TestCase ->with('scheduled_jobs_runner'); $records = []; - $systemAuditService = $this->createMock(SystemAuditService::class); + $systemAuditService = $this->createMock(AuditRecorderInterface::class); $systemAuditService->expects($this->exactly(2)) ->method('record') ->willReturnCallback(static function (string $eventType, string $outcome, array $context) use (&$records): int { diff --git a/tests/Service/Settings/AdminSettingsServiceApiLifecycleTest.php b/tests/Service/Settings/AdminSettingsServiceApiLifecycleTest.php index 671ff39..0577b2c 100644 --- a/tests/Service/Settings/AdminSettingsServiceApiLifecycleTest.php +++ b/tests/Service/Settings/AdminSettingsServiceApiLifecycleTest.php @@ -5,7 +5,7 @@ namespace MintyPHP\Tests\Service\Settings; use MintyPHP\Repository\Auth\ApiTokenRepository; use MintyPHP\Repository\Auth\RememberTokenRepository; use MintyPHP\Service\Access\RoleService; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Org\DepartmentService; use MintyPHP\Service\Settings\AdminSettingsService; use MintyPHP\Service\Settings\SettingCacheService; @@ -461,7 +461,7 @@ class AdminSettingsServiceApiLifecycleTest extends TestCase $apiTokenRepository = $this->createConfiguredMock(ApiTokenRepository::class, [ 'countActive' => 0, ]); - $systemAuditService = $this->createConfiguredMock(SystemAuditService::class, [ + $systemAuditService = $this->createConfiguredMock(AuditRecorderInterface::class, [ 'record' => null, ]); diff --git a/tests/Service/Settings/AdminSettingsServiceAppTest.php b/tests/Service/Settings/AdminSettingsServiceAppTest.php index 0f7dd1e..a6e02f3 100644 --- a/tests/Service/Settings/AdminSettingsServiceAppTest.php +++ b/tests/Service/Settings/AdminSettingsServiceAppTest.php @@ -5,7 +5,7 @@ namespace MintyPHP\Tests\Service\Settings; use MintyPHP\Repository\Auth\ApiTokenRepository; use MintyPHP\Repository\Auth\RememberTokenRepository; use MintyPHP\Service\Access\RoleService; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Org\DepartmentService; use MintyPHP\Service\Settings\AdminSettingsService; use MintyPHP\Service\Settings\SettingCacheService; @@ -426,7 +426,7 @@ class AdminSettingsServiceAppTest extends TestCase $apiTokenRepository = $apiTokenRepository ?? $this->createConfiguredMock(ApiTokenRepository::class, [ 'countActive' => 0, ]); - $systemAuditService = $this->createConfiguredMock(SystemAuditService::class, [ + $systemAuditService = $this->createConfiguredMock(AuditRecorderInterface::class, [ 'record' => null, ]); diff --git a/tests/Service/Settings/AdminSettingsServiceSecurityTest.php b/tests/Service/Settings/AdminSettingsServiceSecurityTest.php index 3130bc2..b025bc0 100644 --- a/tests/Service/Settings/AdminSettingsServiceSecurityTest.php +++ b/tests/Service/Settings/AdminSettingsServiceSecurityTest.php @@ -5,7 +5,7 @@ namespace MintyPHP\Tests\Service\Settings; use MintyPHP\Repository\Auth\ApiTokenRepository; use MintyPHP\Repository\Auth\RememberTokenRepository; use MintyPHP\Service\Access\RoleService; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Org\DepartmentService; use MintyPHP\Service\Settings\AdminSettingsService; use MintyPHP\Service\Settings\SettingCacheService; @@ -545,7 +545,7 @@ class AdminSettingsServiceSecurityTest extends TestCase $apiTokenRepository = $this->createConfiguredMock(ApiTokenRepository::class, [ 'countActive' => 0, ]); - $systemAuditService = $this->createConfiguredMock(SystemAuditService::class, [ + $systemAuditService = $this->createConfiguredMock(AuditRecorderInterface::class, [ 'record' => null, ]); diff --git a/tests/Service/Settings/AdminSettingsServiceSessionPolicyTest.php b/tests/Service/Settings/AdminSettingsServiceSessionPolicyTest.php index f8ec023..3efd7d3 100644 --- a/tests/Service/Settings/AdminSettingsServiceSessionPolicyTest.php +++ b/tests/Service/Settings/AdminSettingsServiceSessionPolicyTest.php @@ -5,7 +5,7 @@ namespace MintyPHP\Tests\Service\Settings; use MintyPHP\Repository\Auth\ApiTokenRepository; use MintyPHP\Repository\Auth\RememberTokenRepository; use MintyPHP\Service\Access\RoleService; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Org\DepartmentService; use MintyPHP\Service\Settings\AdminSettingsService; use MintyPHP\Service\Settings\SettingCacheService; @@ -344,7 +344,7 @@ class AdminSettingsServiceSessionPolicyTest extends TestCase $apiTokenRepository = $this->createConfiguredMock(ApiTokenRepository::class, [ 'countActive' => 0, ]); - $systemAuditService = $this->createConfiguredMock(SystemAuditService::class, [ + $systemAuditService = $this->createConfiguredMock(AuditRecorderInterface::class, [ 'record' => null, ]); diff --git a/tests/Service/Tenant/TenantServiceTest.php b/tests/Service/Tenant/TenantServiceTest.php index 0baac96..8de984b 100644 --- a/tests/Service/Tenant/TenantServiceTest.php +++ b/tests/Service/Tenant/TenantServiceTest.php @@ -4,7 +4,7 @@ namespace MintyPHP\Tests\Service\Tenant; use MintyPHP\Repository\Org\DepartmentRepositoryInterface; use MintyPHP\Repository\Tenant\TenantRepositoryInterface; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Directory\DirectorySettingsGateway; use MintyPHP\Service\Tenant\TenantService; use PHPUnit\Framework\MockObject\MockObject; @@ -15,7 +15,7 @@ class TenantServiceTest extends TestCase private TenantRepositoryInterface&MockObject $tenantRepository; private DepartmentRepositoryInterface&MockObject $departmentRepository; private DirectorySettingsGateway&MockObject $settingsGateway; - private SystemAuditService&MockObject $systemAuditService; + private AuditRecorderInterface&MockObject $systemAuditService; private TenantService $service; protected function setUp(): void @@ -23,7 +23,7 @@ class TenantServiceTest extends TestCase $this->tenantRepository = $this->createMock(TenantRepositoryInterface::class); $this->departmentRepository = $this->createMock(DepartmentRepositoryInterface::class); $this->settingsGateway = $this->createMock(DirectorySettingsGateway::class); - $this->systemAuditService = $this->createMock(SystemAuditService::class); + $this->systemAuditService = $this->createMock(AuditRecorderInterface::class); $this->service = new TenantService( $this->tenantRepository, diff --git a/tests/Service/User/UserAccountServiceTest.php b/tests/Service/User/UserAccountServiceTest.php index 3bd973a..e6cdf02 100644 --- a/tests/Service/User/UserAccountServiceTest.php +++ b/tests/Service/User/UserAccountServiceTest.php @@ -6,7 +6,7 @@ use MintyPHP\Repository\Support\DatabaseSessionRepository; use MintyPHP\Repository\User\UserListQueryRepositoryInterface; use MintyPHP\Repository\User\UserReadRepositoryInterface; use MintyPHP\Repository\User\UserWriteRepositoryInterface; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use MintyPHP\Service\Tenant\TenantScopeService; use MintyPHP\Service\User\UserAccountService; use MintyPHP\Service\User\UserAssignmentService; @@ -96,7 +96,7 @@ class UserAccountServiceTest extends TestCase ->with(11) ->willReturn(true); - $auditService = $this->createMock(SystemAuditService::class); + $auditService = $this->createMock(AuditRecorderInterface::class); $auditService->expects($this->once()) ->method('record') ->with( @@ -130,7 +130,7 @@ class UserAccountServiceTest extends TestCase ->with(11) ->willReturn(false); - $auditService = $this->createMock(SystemAuditService::class); + $auditService = $this->createMock(AuditRecorderInterface::class); $auditService->expects($this->never())->method('record'); $service = $this->newService($readRepository, $writeRepository, null, null, null, null, null, null, $auditService); @@ -230,7 +230,7 @@ class UserAccountServiceTest extends TestCase $assignmentService = $this->createMock(UserAssignmentService::class); $assignmentService->expects($this->never())->method('bumpAuthzVersion'); - $auditService = $this->createMock(SystemAuditService::class); + $auditService = $this->createMock(AuditRecorderInterface::class); $auditService->expects($this->never())->method('record'); $service = $this->newService($readRepository, $writeRepository, null, $assignmentService, null, null, null, null, $auditService); @@ -257,7 +257,7 @@ class UserAccountServiceTest extends TestCase ->method('bumpAuthzVersion') ->with(33); - $auditService = $this->createMock(SystemAuditService::class); + $auditService = $this->createMock(AuditRecorderInterface::class); $auditService->expects($this->once()) ->method('record') ->with( @@ -293,7 +293,7 @@ class UserAccountServiceTest extends TestCase $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->method('canAccess')->willReturn(true); - $auditService = $this->createMock(SystemAuditService::class); + $auditService = $this->createMock(AuditRecorderInterface::class); $auditService->expects($this->once()) ->method('record') ->with( @@ -398,7 +398,7 @@ class UserAccountServiceTest extends TestCase $assignmentService->expects($this->never())->method('syncRoles'); $assignmentService->expects($this->never())->method('syncDepartments'); - $auditService = $this->createMock(SystemAuditService::class); + $auditService = $this->createMock(AuditRecorderInterface::class); $auditService->expects($this->never())->method('record'); $databaseSessionRepository = $this->createMock(DatabaseSessionRepository::class); @@ -628,7 +628,7 @@ class UserAccountServiceTest extends TestCase ?UserSettingsGateway $settingsGateway = null, ?TenantScopeService $scopeGateway = null, ?UserDirectoryGateway $directoryGateway = null, - ?SystemAuditService $auditService = null, + ?AuditRecorderInterface $auditService = null, ?DatabaseSessionRepository $databaseSessionRepository = null ): UserAccountService { $readRepository = $readRepository ?? $this->createMock(UserReadRepositoryInterface::class); @@ -673,7 +673,7 @@ class UserAccountServiceTest extends TestCase $directoryGateway->method('findTenantByUuid')->willReturn(null); } - $auditService = $auditService ?? $this->createMock(SystemAuditService::class); + $auditService = $auditService ?? $this->createMock(AuditRecorderInterface::class); $databaseSessionRepository = $databaseSessionRepository ?? $this->createMock(DatabaseSessionRepository::class); return new UserAccountService( diff --git a/tests/Service/User/UserLifecycleRestoreServiceTest.php b/tests/Service/User/UserLifecycleRestoreServiceTest.php index 30f40a6..464c20d 100644 --- a/tests/Service/User/UserLifecycleRestoreServiceTest.php +++ b/tests/Service/User/UserLifecycleRestoreServiceTest.php @@ -5,7 +5,7 @@ namespace MintyPHP\Tests\Service\User; use MintyPHP\Repository\Support\DatabaseSessionRepository; use MintyPHP\Repository\User\UserReadRepositoryInterface; use MintyPHP\Repository\User\UserWriteRepositoryInterface; -use MintyPHP\Service\Audit\UserLifecycleAuditService; +use MintyPHP\Service\Audit\UserLifecycleAuditInterface; use MintyPHP\Service\User\UserLifecycleRestoreService; use MintyPHP\Service\User\UserSettingsGateway; use PHPUnit\Framework\TestCase; @@ -32,7 +32,7 @@ class UserLifecycleRestoreServiceTest extends TestCase 'theme' => 'dark', ]; - $auditService = $this->createMock(UserLifecycleAuditService::class); + $auditService = $this->createMock(UserLifecycleAuditInterface::class); $auditService->expects($this->once()) ->method('findDeleteEventForRestore') ->with(1, true) @@ -143,7 +143,7 @@ class UserLifecycleRestoreServiceTest extends TestCase public function testRestoreReturnsAuditEventNotFoundWhenEventIsNull(): void { - $auditService = $this->createMock(UserLifecycleAuditService::class); + $auditService = $this->createMock(UserLifecycleAuditInterface::class); $auditService->expects($this->once()) ->method('findDeleteEventForRestore') ->with(1, true) @@ -169,7 +169,7 @@ class UserLifecycleRestoreServiceTest extends TestCase public function testRestoreReturnsAlreadyRestoredWhenEventHasRestoredAt(): void { - $auditService = $this->createMock(UserLifecycleAuditService::class); + $auditService = $this->createMock(UserLifecycleAuditInterface::class); $auditService->method('findDeleteEventForRestore')->willReturn([ 'id' => 1, 'restored_at' => '2025-01-01 00:00:00', @@ -193,7 +193,7 @@ class UserLifecycleRestoreServiceTest extends TestCase public function testRestoreReturnsSnapshotUnavailableWhenDecryptReturnsNull(): void { - $auditService = $this->createMock(UserLifecycleAuditService::class); + $auditService = $this->createMock(UserLifecycleAuditInterface::class); $auditService->method('findDeleteEventForRestore')->willReturn([ 'id' => 1, 'restored_at' => null, @@ -218,7 +218,7 @@ class UserLifecycleRestoreServiceTest extends TestCase public function testRestoreReturnsSnapshotInvalidWhenUuidIsMissing(): void { - $auditService = $this->createMock(UserLifecycleAuditService::class); + $auditService = $this->createMock(UserLifecycleAuditInterface::class); $auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]); $auditService->method('decryptSnapshot')->willReturn(['uuid' => '', 'email' => 'a@b.com']); @@ -238,7 +238,7 @@ class UserLifecycleRestoreServiceTest extends TestCase public function testRestoreReturnsSnapshotInvalidWhenEmailIsMissing(): void { - $auditService = $this->createMock(UserLifecycleAuditService::class); + $auditService = $this->createMock(UserLifecycleAuditInterface::class); $auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]); $auditService->method('decryptSnapshot')->willReturn(['uuid' => 'u1', 'email' => '']); @@ -260,7 +260,7 @@ class UserLifecycleRestoreServiceTest extends TestCase public function testRestoreReturnsUuidExistsWhenUserWithUuidAlreadyExists(): void { - $auditService = $this->createMock(UserLifecycleAuditService::class); + $auditService = $this->createMock(UserLifecycleAuditInterface::class); $auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]); $auditService->method('decryptSnapshot')->willReturn(['uuid' => 'existing-uuid', 'email' => 'new@example.com']); @@ -287,7 +287,7 @@ class UserLifecycleRestoreServiceTest extends TestCase public function testRestoreReturnsEmailExistsWhenUserWithEmailAlreadyExists(): void { - $auditService = $this->createMock(UserLifecycleAuditService::class); + $auditService = $this->createMock(UserLifecycleAuditInterface::class); $auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]); $auditService->method('decryptSnapshot')->willReturn(['uuid' => 'new-uuid', 'email' => 'existing@example.com']); @@ -315,7 +315,7 @@ class UserLifecycleRestoreServiceTest extends TestCase public function testRestoreReturnsCreateFailedWhenWriteRepoReturnsZero(): void { - $auditService = $this->createMock(UserLifecycleAuditService::class); + $auditService = $this->createMock(UserLifecycleAuditInterface::class); $auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]); $auditService->method('decryptSnapshot')->willReturn(['uuid' => 'u1', 'email' => 'e@x.com']); @@ -348,7 +348,7 @@ class UserLifecycleRestoreServiceTest extends TestCase public function testRestoreReturnsCreateFailedWhenWriteRepoReturnsFalse(): void { - $auditService = $this->createMock(UserLifecycleAuditService::class); + $auditService = $this->createMock(UserLifecycleAuditInterface::class); $auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]); $auditService->method('decryptSnapshot')->willReturn(['uuid' => 'u1', 'email' => 'e@x.com']); @@ -383,7 +383,7 @@ class UserLifecycleRestoreServiceTest extends TestCase public function testRestoreReturnsMarkFailedWhenMarkRestoredReturnsFalse(): void { - $auditService = $this->createMock(UserLifecycleAuditService::class); + $auditService = $this->createMock(UserLifecycleAuditInterface::class); $auditService->method('findDeleteEventForRestore')->willReturn(['id' => 1, 'restored_at' => null]); $auditService->method('decryptSnapshot')->willReturn(['uuid' => 'u1', 'email' => 'e@x.com']); $auditService->expects($this->once()) @@ -423,7 +423,7 @@ class UserLifecycleRestoreServiceTest extends TestCase public function testRestoreRollsBackOnUnexpectedException(): void { - $auditService = $this->createMock(UserLifecycleAuditService::class); + $auditService = $this->createMock(UserLifecycleAuditInterface::class); $auditService->method('findDeleteEventForRestore') ->willThrowException(new \RuntimeException('unexpected')); @@ -445,7 +445,7 @@ class UserLifecycleRestoreServiceTest extends TestCase public function testRestoreRollbackQuietlySwallowsRollbackException(): void { - $auditService = $this->createMock(UserLifecycleAuditService::class); + $auditService = $this->createMock(UserLifecycleAuditInterface::class); $auditService->method('findDeleteEventForRestore') ->willThrowException(new \RuntimeException('db error')); @@ -470,14 +470,14 @@ class UserLifecycleRestoreServiceTest extends TestCase private function newService( ?UserReadRepositoryInterface $userReadRepository = null, ?UserWriteRepositoryInterface $userWriteRepository = null, - ?UserLifecycleAuditService $userLifecycleAuditService = null, + ?UserLifecycleAuditInterface $userLifecycleAuditService = null, ?DatabaseSessionRepository $databaseSessionRepository = null, ?UserSettingsGateway $userSettingsGateway = null, ): UserLifecycleRestoreService { return new UserLifecycleRestoreService( $userReadRepository ?? $this->createMock(UserReadRepositoryInterface::class), $userWriteRepository ?? $this->createMock(UserWriteRepositoryInterface::class), - $userLifecycleAuditService ?? $this->createMock(UserLifecycleAuditService::class), + $userLifecycleAuditService ?? $this->createMock(UserLifecycleAuditInterface::class), $databaseSessionRepository ?? $this->createMock(DatabaseSessionRepository::class), $userSettingsGateway ?? $this->createMock(UserSettingsGateway::class), ); diff --git a/tests/Service/User/UserLifecycleServiceTest.php b/tests/Service/User/UserLifecycleServiceTest.php index 963b560..e3288e4 100644 --- a/tests/Service/User/UserLifecycleServiceTest.php +++ b/tests/Service/User/UserLifecycleServiceTest.php @@ -5,7 +5,7 @@ namespace MintyPHP\Tests\Service\User; use MintyPHP\Repository\Support\DatabaseSessionRepository; use MintyPHP\Repository\User\UserReadRepositoryInterface; use MintyPHP\Repository\User\UserWriteRepositoryInterface; -use MintyPHP\Service\Audit\UserLifecycleAuditService; +use MintyPHP\Service\Audit\UserLifecycleAuditInterface; use MintyPHP\Service\User\UserLifecycleService; use MintyPHP\Service\User\UserSettingsGateway; use PHPUnit\Framework\MockObject\MockObject; @@ -16,7 +16,7 @@ class UserLifecycleServiceTest extends TestCase private UserReadRepositoryInterface&MockObject $userReadRepository; private UserWriteRepositoryInterface&MockObject $userWriteRepository; private UserSettingsGateway&MockObject $settingsGateway; - private UserLifecycleAuditService&MockObject $auditService; + private UserLifecycleAuditInterface&MockObject $auditService; private DatabaseSessionRepository&MockObject $databaseSessionRepository; protected function setUp(): void @@ -24,7 +24,7 @@ class UserLifecycleServiceTest extends TestCase $this->userReadRepository = $this->createMock(UserReadRepositoryInterface::class); $this->userWriteRepository = $this->createMock(UserWriteRepositoryInterface::class); $this->settingsGateway = $this->createMock(UserSettingsGateway::class); - $this->auditService = $this->createMock(UserLifecycleAuditService::class); + $this->auditService = $this->createMock(UserLifecycleAuditInterface::class); $this->databaseSessionRepository = $this->createMock(DatabaseSessionRepository::class); // Default: lock succeeds, release is a no-op diff --git a/tests/Unit/Module/ModuleEventDispatcherTest.php b/tests/Unit/Module/ModuleEventDispatcherTest.php index fd2351e..e64083a 100644 --- a/tests/Unit/Module/ModuleEventDispatcherTest.php +++ b/tests/Unit/Module/ModuleEventDispatcherTest.php @@ -6,7 +6,7 @@ use MintyPHP\App\AppContainer; use MintyPHP\App\Module\Contracts\EventListener; use MintyPHP\App\Module\ModuleEventDispatcher; use MintyPHP\Http\RequestContext; -use MintyPHP\Service\Audit\SystemAuditService; +use MintyPHP\Service\Audit\AuditRecorderInterface; use PHPUnit\Framework\TestCase; final class ModuleEventDispatcherTest extends TestCase @@ -119,7 +119,7 @@ final class ModuleEventDispatcherTest extends TestCase $container = new AppContainer(); $container->set($failingListener::class, static fn () => $failingListener); - $systemAuditService = $this->createMock(SystemAuditService::class); + $systemAuditService = $this->createMock(AuditRecorderInterface::class); $systemAuditService->expects($this->once()) ->method('record') ->with(