refactor(audit): extract audit domain into self-contained module

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) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 21:12:49 +01:00
parent 12a837bda9
commit 0c351f6aff
176 changed files with 2157 additions and 834 deletions

View File

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

View File

@@ -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());

View File

@@ -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());

View File

@@ -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),

View File

@@ -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