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

@@ -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": {

View File

@@ -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'],
];

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 (!$service instanceof ApiAuditService) {
throw new \RuntimeException('ApiResponse resolver returned invalid dependency: ' . ApiAuditService::class);
if (is_object($service) && method_exists($service, 'finish')) {
$service->finish($statusCode, $errorCode);
}
}
} catch (\Throwable) {
// fail-open
}
}
return $service;
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 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<string, string>
*/
public const PAGE_AUDIT_PURGE = [
public const PAGE_JOBS_PURGE = [
'can_purge' => SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE,
];
/**
* @var array<string, string>
*/
public const PAGE_SYSTEM_AUDIT = [
'can_purge_system_audit' => OperationsAuthorizationPolicy::ABILITY_ADMIN_SYSTEM_AUDIT_PURGE,
];
/**
* @var array<string, string>
*/
public const PAGE_USER_LIFECYCLE_VIEW = [
'can_restore' => OperationsAuthorizationPolicy::ABILITY_ADMIN_USERS_LIFECYCLE_RESTORE,
];
}

View File

@@ -0,0 +1,21 @@
<?php
namespace MintyPHP\Service\Audit;
/**
* Core-side contract for recording system audit events.
*
* Implemented by the audit module's SystemAuditService when the module is active.
* Falls back to NullAuditRecorder (no-op) when the audit module is disabled.
*/
interface AuditRecorderInterface
{
/**
* @param array<string, mixed> $context
*/
public function record(
string $eventType,
string $outcome = 'success',
array $context = []
): ?int;
}

View File

@@ -1,32 +0,0 @@
<?php
namespace MintyPHP\Service\Audit;
use MintyPHP\Repository\Audit\ApiAuditLogRepository;
use MintyPHP\Repository\Audit\ApiAuditLogRepositoryInterface;
use MintyPHP\Repository\Audit\SystemAuditLogRepository;
use MintyPHP\Repository\Audit\SystemAuditLogRepositoryInterface;
use MintyPHP\Repository\Audit\UserLifecycleAuditRepository;
use MintyPHP\Repository\Audit\UserLifecycleAuditRepositoryInterface;
class AuditRepositoryFactory
{
private ?ApiAuditLogRepository $apiAuditLogRepository = null;
private ?UserLifecycleAuditRepository $userLifecycleAuditRepository = null;
private ?SystemAuditLogRepository $systemAuditLogRepository = null;
public function createApiAuditLogRepository(): ApiAuditLogRepositoryInterface
{
return $this->apiAuditLogRepository ??= new ApiAuditLogRepository();
}
public function createUserLifecycleAuditRepository(): UserLifecycleAuditRepositoryInterface
{
return $this->userLifecycleAuditRepository ??= new UserLifecycleAuditRepository();
}
public function createSystemAuditLogRepository(): SystemAuditLogRepositoryInterface
{
return $this->systemAuditLogRepository ??= new SystemAuditLogRepository();
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace MintyPHP\Service\Audit;
/**
* Core-side contract for recording import audit events.
*
* Implemented by the audit module's ImportAuditService when active.
* Falls back to NullImportAudit (no-op) when the audit module is disabled.
*/
interface ImportAuditInterface
{
/**
* @param array<int, string> $mappedTargets
*/
public function startRun(
string $profileKey,
array $mappedTargets,
?string $sourceFilename,
int $userId,
?int $currentTenantId
): ?int;
/**
* @param array<string, mixed> $result
*/
public function finishRun(?int $runId, array $result, ?string $forcedStatus = null): void;
}

View File

@@ -0,0 +1,17 @@
<?php
namespace MintyPHP\Service\Audit;
/**
* No-op audit recorder used when the audit module is disabled.
*/
final class NullAuditRecorder implements AuditRecorderInterface
{
public function record(
string $eventType,
string $outcome = 'success',
array $context = []
): ?int {
return null;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace MintyPHP\Service\Audit;
/**
* No-op import audit used when the audit module is disabled.
*/
final class NullImportAudit implements ImportAuditInterface
{
public function startRun(
string $profileKey,
array $mappedTargets,
?string $sourceFilename,
int $userId,
?int $currentTenantId
): ?int {
return null;
}
public function finishRun(?int $runId, array $result, ?string $forcedStatus = null): void
{
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace MintyPHP\Service\Audit;
/**
* No-op user lifecycle audit used when the audit module is disabled.
*/
final class NullUserLifecycleAudit implements UserLifecycleAuditInterface
{
public function logDeactivate(
string $runUuid,
string $triggerType,
array $policy,
?int $actorUserId,
array $targetUser,
string $status = 'success',
?string $reasonCode = null
): bool {
return false;
}
public function logDeleteWithSnapshot(
string $runUuid,
string $triggerType,
array $policy,
?int $actorUserId,
array $targetUser
): int|false {
return false;
}
public function logDeleteFailure(
string $runUuid,
string $triggerType,
array $policy,
?int $actorUserId,
array $targetUser,
string $reasonCode
): bool {
return false;
}
public function logRestore(
string $runUuid,
string $triggerType,
array $policy,
?int $actorUserId,
array $targetUser,
string $status = 'success',
?string $reasonCode = null
): bool {
return false;
}
public function markDeleteEventRestored(int $auditId, int $restoredByUserId, int $restoredUserId): bool
{
return false;
}
public function updateStatus(int $id, string $status, ?string $reasonCode = null): bool
{
return false;
}
public function findDeleteEventForRestore(int $id, bool $forUpdate = false): ?array
{
return null;
}
public function decryptSnapshot(array $event): ?array
{
return null;
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace MintyPHP\Service\Audit;
/**
* Core-side contract for recording user lifecycle audit events (deactivation, deletion, restore).
*
* Implemented by the audit module's UserLifecycleAuditService when active.
* Falls back to NullUserLifecycleAudit (no-op) when the audit module is disabled.
*/
interface UserLifecycleAuditInterface
{
/**
* @param array<string, mixed> $policy
* @param array<string, mixed> $targetUser
*/
public function logDeactivate(
string $runUuid,
string $triggerType,
array $policy,
?int $actorUserId,
array $targetUser,
string $status = 'success',
?string $reasonCode = null
): bool;
/**
* @param array<string, mixed> $policy
* @param array<string, mixed> $targetUser
*/
public function logDeleteWithSnapshot(
string $runUuid,
string $triggerType,
array $policy,
?int $actorUserId,
array $targetUser
): int|false;
/**
* @param array<string, mixed> $policy
* @param array<string, mixed> $targetUser
*/
public function logDeleteFailure(
string $runUuid,
string $triggerType,
array $policy,
?int $actorUserId,
array $targetUser,
string $reasonCode
): bool;
/**
* @param array<string, mixed> $policy
* @param array<string, mixed> $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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<string, ScheduledJobHandlerInterface> */
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();

View File

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

View File

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

View File

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

View File

@@ -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'] ?? ''),

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 ?",

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,55 @@
<?php
namespace MintyPHP\Module\Audit;
use MintyPHP\Service\Access\AuthorizationDecision;
use MintyPHP\Service\Access\AuthorizationPolicyInterface;
use MintyPHP\Service\Access\PermissionService;
final class AuditAuthorizationPolicy implements AuthorizationPolicyInterface
{
public const ABILITY_API_AUDIT_VIEW = 'audit.api.view';
public const ABILITY_SYSTEM_AUDIT_VIEW = 'audit.system.view';
public const ABILITY_SYSTEM_AUDIT_PURGE = 'audit.system.purge';
public const ABILITY_IMPORTS_AUDIT_VIEW = 'audit.imports.view';
public const ABILITY_USER_LIFECYCLE_VIEW = 'audit.user_lifecycle.view';
public const ABILITY_USER_LIFECYCLE_RESTORE = 'audit.user_lifecycle.restore';
private const ABILITY_PERMISSION_MAP = [
self::ABILITY_API_AUDIT_VIEW => '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();
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace MintyPHP\Module\Audit;
use MintyPHP\App\AppContainer;
use MintyPHP\App\Container\ContainerRegistrar;
use MintyPHP\Http\RequestRuntimeInterface;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Module\Audit\Handler\ApiAuditPurgeJobHandler;
use MintyPHP\Module\Audit\Handler\ImportAuditPurgeJobHandler;
use MintyPHP\Module\Audit\Handler\SystemAuditPurgeJobHandler;
use MintyPHP\Module\Audit\Handler\UserLifecycleAuditPurgeJobHandler;
use MintyPHP\Module\Audit\Http\ApiSystemAuditReporter;
use MintyPHP\Module\Audit\Providers\AuditLayoutProvider;
use MintyPHP\Module\Audit\Service\AuditRepositoryFactory;
use MintyPHP\Module\Audit\Service\AuditServicesFactory;
use MintyPHP\Module\Audit\Service\ApiAuditService;
use MintyPHP\Module\Audit\Service\AuditMetadataEnricher;
use MintyPHP\Module\Audit\Service\FrontendTelemetryIngestService;
use MintyPHP\Module\Audit\Service\ImportAuditService;
use MintyPHP\Module\Audit\Service\SystemAuditService;
use MintyPHP\Module\Audit\Service\UserLifecycleAuditService;
use MintyPHP\Repository\User\UserReadRepository;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Audit\AuditRecorderInterface;
use MintyPHP\Service\Audit\ImportAuditInterface;
use MintyPHP\Service\Audit\UserLifecycleAuditInterface;
use MintyPHP\Service\Security\RateLimiterService;
use MintyPHP\Service\Settings\SettingServicesFactory;
use MintyPHP\Service\Settings\SettingsFrontendTelemetryGateway;
final class AuditContainerRegistrar implements ContainerRegistrar
{
public function register(AppContainer $container): void
{
// Repository factory
$container->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());
}
}

View File

@@ -1,6 +1,8 @@
<?php
namespace MintyPHP\Domain\Taxonomy;
namespace MintyPHP\Module\Audit\Domain;
use MintyPHP\Domain\Taxonomy\SupportsStringTaxonomy;
enum ImportAuditStatus: string
{

View File

@@ -1,6 +1,8 @@
<?php
namespace MintyPHP\Domain\Taxonomy;
namespace MintyPHP\Module\Audit\Domain;
use MintyPHP\Domain\Taxonomy\SupportsStringTaxonomy;
enum SystemAuditChannel: string
{

View File

@@ -1,6 +1,8 @@
<?php
namespace MintyPHP\Domain\Taxonomy;
namespace MintyPHP\Module\Audit\Domain;
use MintyPHP\Domain\Taxonomy\SupportsStringTaxonomy;
enum SystemAuditOutcome: string
{

View File

@@ -1,6 +1,8 @@
<?php
namespace MintyPHP\Domain\Taxonomy;
namespace MintyPHP\Module\Audit\Domain;
use MintyPHP\Domain\Taxonomy\SupportsStringTaxonomy;
enum UserLifecycleAction: string
{

View File

@@ -1,6 +1,8 @@
<?php
namespace MintyPHP\Domain\Taxonomy;
namespace MintyPHP\Module\Audit\Domain;
use MintyPHP\Domain\Taxonomy\SupportsStringTaxonomy;
enum UserLifecycleStatus: string
{

View File

@@ -1,6 +1,8 @@
<?php
namespace MintyPHP\Domain\Taxonomy;
namespace MintyPHP\Module\Audit\Domain;
use MintyPHP\Domain\Taxonomy\SupportsStringTaxonomy;
enum UserLifecycleTriggerType: string
{

View File

@@ -0,0 +1,41 @@
<?php
namespace MintyPHP\Module\Audit\Handler;
use MintyPHP\Module\Audit\Service\ApiAuditService;
use MintyPHP\Service\Scheduler\Handler\ScheduledJobHandlerInterface;
class ApiAuditPurgeJobHandler implements ScheduledJobHandlerInterface
{
public function __construct(private readonly ApiAuditService $apiAuditService)
{
}
public function definition(): array
{
return [
'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'],
];
}
public function execute(?int $actorUserId): array
{
$deleted = $this->apiAuditService->purgeExpired();
return [
'status' => 'success',
'error_code' => null,
'error_message' => null,
'result' => ['deleted_count' => $deleted],
];
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace MintyPHP\Module\Audit\Handler;
use MintyPHP\Module\Audit\Service\ImportAuditService;
use MintyPHP\Service\Scheduler\Handler\ScheduledJobHandlerInterface;
class ImportAuditPurgeJobHandler implements ScheduledJobHandlerInterface
{
public function __construct(private readonly ImportAuditService $importAuditService)
{
}
public function definition(): array
{
return [
'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'],
];
}
public function execute(?int $actorUserId): array
{
$deleted = $this->importAuditService->purgeExpired();
return [
'status' => 'success',
'error_code' => null,
'error_message' => null,
'result' => ['deleted_count' => $deleted],
];
}
}

View File

@@ -1,8 +1,9 @@
<?php
namespace MintyPHP\Service\Scheduler\Handler;
namespace MintyPHP\Module\Audit\Handler;
use MintyPHP\Service\Audit\SystemAuditService;
use MintyPHP\Module\Audit\Service\SystemAuditService;
use MintyPHP\Service\Scheduler\Handler\ScheduledJobHandlerInterface;
class SystemAuditPurgeJobHandler implements ScheduledJobHandlerInterface
{
@@ -34,9 +35,7 @@ class SystemAuditPurgeJobHandler implements ScheduledJobHandlerInterface
'status' => 'success',
'error_code' => null,
'error_message' => null,
'result' => [
'deleted_count' => $deleted,
],
'result' => ['deleted_count' => $deleted],
];
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace MintyPHP\Module\Audit\Handler;
use MintyPHP\Module\Audit\Service\UserLifecycleAuditService;
use MintyPHP\Service\Scheduler\Handler\ScheduledJobHandlerInterface;
class UserLifecycleAuditPurgeJobHandler implements ScheduledJobHandlerInterface
{
public function __construct(private readonly UserLifecycleAuditService $userLifecycleAuditService)
{
}
public function definition(): array
{
return [
'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'],
];
}
public function execute(?int $actorUserId): array
{
$deleted = $this->userLifecycleAuditService->purgeExpired();
return [
'status' => 'success',
'error_code' => null,
'error_message' => null,
'result' => ['deleted_count' => $deleted],
];
}
}

View File

@@ -1,9 +1,11 @@
<?php
namespace MintyPHP\Http;
namespace MintyPHP\Module\Audit\Http;
use MintyPHP\Domain\Taxonomy\SystemAuditOutcome;
use MintyPHP\Service\Audit\SystemAuditService;
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\RequestContext;
use MintyPHP\Module\Audit\Domain\SystemAuditOutcome;
use MintyPHP\Module\Audit\Service\SystemAuditService;
class ApiSystemAuditReporter
{

View File

@@ -0,0 +1,14 @@
<?php
namespace MintyPHP\Module\Audit\Providers;
use MintyPHP\App\AppContainer;
use MintyPHP\App\Module\Contracts\LayoutContextProvider;
final class AuditLayoutProvider implements LayoutContextProvider
{
public function provide(array $session, AppContainer $container): array
{
return [];
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Repository\Audit;
namespace MintyPHP\Module\Audit\Repository;
use MintyPHP\DB;
use MintyPHP\Repository\Support\RepoQuery;

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Repository\Audit;
namespace MintyPHP\Module\Audit\Repository;
/** Contract for API request audit log creation, pagination, and retention purge. */
interface ApiAuditLogRepositoryInterface

View File

@@ -1,9 +1,9 @@
<?php
namespace MintyPHP\Repository\Audit;
namespace MintyPHP\Module\Audit\Repository;
use MintyPHP\DB;
use MintyPHP\Domain\Taxonomy\ImportAuditStatus;
use MintyPHP\Module\Audit\Domain\ImportAuditStatus;
use MintyPHP\Repository\Support\RepoQuery;
use MintyPHP\Repository\Support\RepositoryArrayHelper;

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Repository\Audit;
namespace MintyPHP\Module\Audit\Repository;
/** Contract for CSV import run audit trail creation, completion, and purge. */
interface ImportAuditRunRepositoryInterface

View File

@@ -1,10 +1,10 @@
<?php
namespace MintyPHP\Repository\Audit;
namespace MintyPHP\Module\Audit\Repository;
use MintyPHP\DB;
use MintyPHP\Domain\Taxonomy\SystemAuditChannel;
use MintyPHP\Domain\Taxonomy\SystemAuditOutcome;
use MintyPHP\Module\Audit\Domain\SystemAuditChannel;
use MintyPHP\Module\Audit\Domain\SystemAuditOutcome;
use MintyPHP\Repository\Support\RepoQuery;
use MintyPHP\Repository\Support\RepositoryArrayHelper;

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Repository\Audit;
namespace MintyPHP\Module\Audit\Repository;
/** Contract for system-wide security event logging with channel and outcome filtering. */
interface SystemAuditLogRepositoryInterface

View File

@@ -1,11 +1,11 @@
<?php
namespace MintyPHP\Repository\Audit;
namespace MintyPHP\Module\Audit\Repository;
use MintyPHP\DB;
use MintyPHP\Domain\Taxonomy\UserLifecycleAction;
use MintyPHP\Domain\Taxonomy\UserLifecycleStatus;
use MintyPHP\Domain\Taxonomy\UserLifecycleTriggerType;
use MintyPHP\Module\Audit\Domain\UserLifecycleAction;
use MintyPHP\Module\Audit\Domain\UserLifecycleStatus;
use MintyPHP\Module\Audit\Domain\UserLifecycleTriggerType;
use MintyPHP\Repository\Support\RepoQuery;
use MintyPHP\Repository\Support\RepositoryArrayHelper;

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Repository\Audit;
namespace MintyPHP\Module\Audit\Repository;
/** Contract for tracking user deactivation/deletion lifecycle events and restore eligibility. */
interface UserLifecycleAuditRepositoryInterface

View File

@@ -1,11 +1,11 @@
<?php
namespace MintyPHP\Service\Audit;
namespace MintyPHP\Module\Audit\Service;
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\RequestContext;
use MintyPHP\Http\RequestRuntimeInterface;
use MintyPHP\Repository\Audit\ApiAuditLogRepositoryInterface;
use MintyPHP\Module\Audit\Repository\ApiAuditLogRepositoryInterface;
class ApiAuditService
{

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Service\Audit;
namespace MintyPHP\Module\Audit\Service;
use MintyPHP\Repository\User\UserReadRepositoryInterface;

View File

@@ -0,0 +1,40 @@
<?php
namespace MintyPHP\Module\Audit\Service;
use MintyPHP\Module\Audit\Repository\ApiAuditLogRepository;
use MintyPHP\Module\Audit\Repository\ApiAuditLogRepositoryInterface;
use MintyPHP\Module\Audit\Repository\ImportAuditRunRepository;
use MintyPHP\Module\Audit\Repository\ImportAuditRunRepositoryInterface;
use MintyPHP\Module\Audit\Repository\SystemAuditLogRepository;
use MintyPHP\Module\Audit\Repository\SystemAuditLogRepositoryInterface;
use MintyPHP\Module\Audit\Repository\UserLifecycleAuditRepository;
use MintyPHP\Module\Audit\Repository\UserLifecycleAuditRepositoryInterface;
class AuditRepositoryFactory
{
private ?ApiAuditLogRepository $apiAuditLogRepository = null;
private ?ImportAuditRunRepository $importAuditRunRepository = null;
private ?UserLifecycleAuditRepository $userLifecycleAuditRepository = null;
private ?SystemAuditLogRepository $systemAuditLogRepository = null;
public function createApiAuditLogRepository(): ApiAuditLogRepositoryInterface
{
return $this->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();
}
}

View File

@@ -1,12 +1,12 @@
<?php
namespace MintyPHP\Service\Audit;
namespace MintyPHP\Module\Audit\Service;
use MintyPHP\Http\RequestRuntimeInterface;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Repository\Audit\ApiAuditLogRepositoryInterface;
use MintyPHP\Repository\Audit\SystemAuditLogRepositoryInterface;
use MintyPHP\Repository\Audit\UserLifecycleAuditRepositoryInterface;
use MintyPHP\Module\Audit\Repository\ApiAuditLogRepositoryInterface;
use MintyPHP\Module\Audit\Repository\SystemAuditLogRepositoryInterface;
use MintyPHP\Module\Audit\Repository\UserLifecycleAuditRepositoryInterface;
use MintyPHP\Service\Settings\SettingsSystemAuditGateway;
class AuditServicesFactory

View File

@@ -1,10 +1,10 @@
<?php
namespace MintyPHP\Service\Audit;
namespace MintyPHP\Module\Audit\Service;
use DateTimeImmutable;
use DateTimeZone;
use MintyPHP\Domain\Taxonomy\SystemAuditOutcome;
use MintyPHP\Module\Audit\Domain\SystemAuditOutcome;
use MintyPHP\Http\RequestContext;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Service\Security\RateLimiterService;

View File

@@ -1,12 +1,13 @@
<?php
namespace MintyPHP\Service\Audit;
namespace MintyPHP\Module\Audit\Service;
use MintyPHP\Domain\Taxonomy\ImportAuditStatus;
use MintyPHP\Repository\Audit\ImportAuditRunRepositoryInterface;
use MintyPHP\Module\Audit\Domain\ImportAuditStatus;
use MintyPHP\Module\Audit\Repository\ImportAuditRunRepositoryInterface;
use MintyPHP\Repository\Support\RepoQuery;
use MintyPHP\Service\Audit\ImportAuditInterface;
class ImportAuditService
class ImportAuditService implements ImportAuditInterface
{
private const RETENTION_DAYS = 90;

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Service\Audit;
namespace MintyPHP\Module\Audit\Service;
use MintyPHP\Http\RequestContext;

View File

@@ -1,17 +1,18 @@
<?php
namespace MintyPHP\Service\Audit;
namespace MintyPHP\Module\Audit\Service;
use MintyPHP\Domain\Taxonomy\SystemAuditChannel;
use MintyPHP\Domain\Taxonomy\SystemAuditOutcome;
use MintyPHP\Module\Audit\Domain\SystemAuditChannel;
use MintyPHP\Module\Audit\Domain\SystemAuditOutcome;
use MintyPHP\Http\ApiAuth;
use MintyPHP\Http\RequestContext;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\Repository\Audit\SystemAuditLogRepositoryInterface;
use MintyPHP\Module\Audit\Repository\SystemAuditLogRepositoryInterface;
use MintyPHP\Repository\Support\RepoQuery;
use MintyPHP\Service\Audit\AuditRecorderInterface;
use MintyPHP\Service\Settings\SettingsSystemAuditGateway;
class SystemAuditService
class SystemAuditService implements AuditRecorderInterface
{
private const RETENTION_DAYS_MIN = 30;
private const RETENTION_DAYS_MAX = 1095;

View File

@@ -1,14 +1,15 @@
<?php
namespace MintyPHP\Service\Audit;
namespace MintyPHP\Module\Audit\Service;
use MintyPHP\Domain\Taxonomy\UserLifecycleAction;
use MintyPHP\Domain\Taxonomy\UserLifecycleStatus;
use MintyPHP\Domain\Taxonomy\UserLifecycleTriggerType;
use MintyPHP\Repository\Audit\UserLifecycleAuditRepositoryInterface;
use MintyPHP\Module\Audit\Domain\UserLifecycleAction;
use MintyPHP\Module\Audit\Domain\UserLifecycleStatus;
use MintyPHP\Module\Audit\Domain\UserLifecycleTriggerType;
use MintyPHP\Module\Audit\Repository\UserLifecycleAuditRepositoryInterface;
use MintyPHP\Service\Audit\UserLifecycleAuditInterface;
use MintyPHP\Support\Crypto;
class UserLifecycleAuditService
class UserLifecycleAuditService implements UserLifecycleAuditInterface
{
private const RETENTION_DAYS = 365;
private const SNAPSHOT_VERSION = 1;

185
modules/audit/module.php Normal file
View File

@@ -0,0 +1,185 @@
<?php
/**
* Audit module manifest.
*
* Provides four audit streams: system audit, API audit, import audit,
* and user lifecycle audit. Includes frontend telemetry ingestion,
* PII redaction, encrypted snapshots, and retention-based cleanup.
*/
return [
'id' => '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',
];

View File

@@ -1,15 +1,16 @@
<?php
use MintyPHP\Service\Access\OperationsAuthorizationPolicy;
use MintyPHP\Module\Audit\AuditAuthorizationPolicy;
use MintyPHP\Module\Audit\Service\ApiAuditService;
use MintyPHP\Support\Guard;
Guard::requireLogin();
Guard::requireAbilityOrForbidden(OperationsAuthorizationPolicy::ABILITY_ADMIN_API_AUDIT_VIEW);
Guard::requireAbilityOrForbidden(AuditAuthorizationPolicy::ABILITY_API_AUDIT_VIEW);
gridRequireGetRequest();
$filters = gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php');
$result = app(\MintyPHP\Service\Audit\ApiAuditService::class)->listPaged($filters);
$result = app(ApiAuditService::class)->listPaged($filters);
$rows = [];
foreach ($result['rows'] as $row) {

View File

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

View File

@@ -66,4 +66,4 @@ $pageConfig = [
?>
<script src="<?php e(assetVersion('vendor/gridjs/gridjs.umd.js')); ?>"></script>
<script type="application/json" id="page-config-admin-api-audit-index"><?php gridJsonForJs($pageConfig); ?></script>
<script type="module" src="<?php e(assetVersion('js/pages/admin-api-audit-index.js')); ?>"></script>
<script type="module" src="<?php e(assetVersion('modules/audit/js/pages/admin-api-audit-index.js')); ?>"></script>

View File

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

View File

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

View File

@@ -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') {

View File

@@ -1,12 +1,12 @@
<?php
use MintyPHP\Domain\Taxonomy\ImportAuditStatus;
use MintyPHP\Service\Access\OperationsAuthorizationPolicy;
use MintyPHP\Service\Audit\ImportAuditService;
use MintyPHP\Module\Audit\AuditAuthorizationPolicy;
use MintyPHP\Module\Audit\Domain\ImportAuditStatus;
use MintyPHP\Module\Audit\Service\ImportAuditService;
use MintyPHP\Support\Guard;
Guard::requireLogin();
Guard::requireAbilityOrForbidden(OperationsAuthorizationPolicy::ABILITY_ADMIN_IMPORTS_AUDIT_VIEW);
Guard::requireAbilityOrForbidden(AuditAuthorizationPolicy::ABILITY_IMPORTS_AUDIT_VIEW);
gridRequireGetRequest();
$importAuditService = app(ImportAuditService::class);

View File

@@ -1,6 +1,6 @@
<?php
use MintyPHP\Domain\Taxonomy\ImportAuditStatus;
use MintyPHP\Module\Audit\Domain\ImportAuditStatus;
$importAuditStatusItems = array_map(
static fn (ImportAuditStatus $status): array => [

View File

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

View File

@@ -69,4 +69,4 @@ $pageConfig = [
?>
<script src="<?php e(assetVersion('vendor/gridjs/gridjs.umd.js')); ?>"></script>
<script type="application/json" id="page-config-admin-import-audit-index"><?php gridJsonForJs($pageConfig); ?></script>
<script type="module" src="<?php e(assetVersion('js/pages/admin-import-audit-index.js')); ?>"></script>
<script type="module" src="<?php e(assetVersion('modules/audit/js/pages/admin-import-audit-index.js')); ?>"></script>

View File

@@ -1,7 +1,7 @@
<?php
use MintyPHP\Module\Audit\Service\ImportAuditService;
use MintyPHP\Router;
use MintyPHP\Service\Audit\ImportAuditService;
use MintyPHP\Session;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;

View File

@@ -6,10 +6,10 @@ use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
Guard::requireLogin();
Guard::requireAbility(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_ADMIN_IMPORTS_AUDIT_VIEW);
Guard::requireAbility(\MintyPHP\Module\Audit\AuditAuthorizationPolicy::ABILITY_IMPORTS_AUDIT_VIEW);
$runId = (int) ($id ?? 0);
$auditRun = $runId > 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');

View File

@@ -1,13 +1,13 @@
<?php
use MintyPHP\Domain\Taxonomy\SystemAuditChannel;
use MintyPHP\Domain\Taxonomy\SystemAuditOutcome;
use MintyPHP\Service\Access\OperationsAuthorizationPolicy;
use MintyPHP\Service\Audit\SystemAuditService;
use MintyPHP\Module\Audit\AuditAuthorizationPolicy;
use MintyPHP\Module\Audit\Domain\SystemAuditChannel;
use MintyPHP\Module\Audit\Domain\SystemAuditOutcome;
use MintyPHP\Module\Audit\Service\SystemAuditService;
use MintyPHP\Support\Guard;
Guard::requireLogin();
Guard::requireAbilityOrForbidden(OperationsAuthorizationPolicy::ABILITY_ADMIN_SYSTEM_AUDIT_VIEW);
Guard::requireAbilityOrForbidden(AuditAuthorizationPolicy::ABILITY_SYSTEM_AUDIT_VIEW);
gridRequireGetRequest();
$filters = gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php');

View File

@@ -1,7 +1,7 @@
<?php
use MintyPHP\Domain\Taxonomy\SystemAuditChannel;
use MintyPHP\Domain\Taxonomy\SystemAuditOutcome;
use MintyPHP\Module\Audit\Domain\SystemAuditChannel;
use MintyPHP\Module\Audit\Domain\SystemAuditOutcome;
$outcomeItems = array_map(
static fn (SystemAuditOutcome $outcome): array => [

Some files were not shown because too many files have changed in this diff Show More