Files
breadcrumb-the-shire/lib/Service/Audit/UserLifecycleAuditService.php
fs 25370a1a55 add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 15:27:35 +01:00

252 lines
7.3 KiB
PHP

<?php
namespace MintyPHP\Service\Audit;
use MintyPHP\Repository\Audit\UserLifecycleAuditRepository;
use MintyPHP\Support\Crypto;
class UserLifecycleAuditService
{
private const RETENTION_DAYS = 365;
private const SNAPSHOT_VERSION = 1;
private const SNAPSHOT_FIELDS = [
'uuid',
'first_name',
'last_name',
'display_name',
'email',
'profile_description',
'job_title',
'phone',
'mobile',
'short_dial',
'address',
'postal_code',
'city',
'region',
'country',
'hire_date',
'locale',
'theme',
'email_verified_at',
'last_login_at',
'last_login_provider',
'primary_tenant_id',
'current_tenant_id',
'created',
'modified',
'active_changed_at',
];
public static function logDeactivate(
string $runUuid,
string $triggerType,
array $policy,
?int $actorUserId,
array $targetUser,
string $status = 'success',
?string $reasonCode = null
): bool {
try {
return UserLifecycleAuditRepository::create(self::buildBaseRow(
$runUuid,
'deactivate',
$triggerType,
$status,
$reasonCode,
$policy,
$actorUserId,
$targetUser
)) !== false;
} catch (\Throwable $exception) {
return false;
}
}
public static function logDeleteWithSnapshot(
string $runUuid,
string $triggerType,
array $policy,
?int $actorUserId,
array $targetUser
): int|false {
try {
$snapshot = self::buildSnapshot($targetUser);
$json = json_encode($snapshot, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($json) || $json === '') {
return false;
}
$row = self::buildBaseRow(
$runUuid,
'delete',
$triggerType,
'success',
null,
$policy,
$actorUserId,
$targetUser
);
$row['snapshot_enc'] = Crypto::encryptString($json);
$row['snapshot_version'] = self::SNAPSHOT_VERSION;
return UserLifecycleAuditRepository::create($row);
} catch (\Throwable $exception) {
return false;
}
}
public static function logDeleteFailure(
string $runUuid,
string $triggerType,
array $policy,
?int $actorUserId,
array $targetUser,
string $reasonCode
): bool {
try {
return UserLifecycleAuditRepository::create(self::buildBaseRow(
$runUuid,
'delete',
$triggerType,
'failed',
$reasonCode,
$policy,
$actorUserId,
$targetUser
)) !== false;
} catch (\Throwable $exception) {
return false;
}
}
public static function logRestore(
string $runUuid,
string $triggerType,
array $policy,
?int $actorUserId,
array $targetUser,
string $status = 'success',
?string $reasonCode = null
): bool {
try {
return UserLifecycleAuditRepository::create(self::buildBaseRow(
$runUuid,
'restore',
$triggerType,
$status,
$reasonCode,
$policy,
$actorUserId,
$targetUser
)) !== false;
} catch (\Throwable $exception) {
return false;
}
}
public static function markDeleteEventRestored(int $auditId, int $restoredByUserId, int $restoredUserId): bool
{
try {
return UserLifecycleAuditRepository::markRestored($auditId, $restoredByUserId, $restoredUserId);
} catch (\Throwable $exception) {
return false;
}
}
public static function listPaged(array $filters): array
{
return UserLifecycleAuditRepository::listPaged($filters);
}
public static function find(int $id): ?array
{
return UserLifecycleAuditRepository::find($id);
}
public static function findDeleteEventForRestore(int $id, bool $forUpdate = false): ?array
{
return UserLifecycleAuditRepository::findDeleteEventForRestore($id, $forUpdate);
}
public static function decryptSnapshot(array $event): ?array
{
$enc = trim((string) ($event['snapshot_enc'] ?? ''));
if ($enc === '') {
return null;
}
try {
$json = Crypto::decryptString($enc);
$decoded = json_decode($json, true);
return is_array($decoded) ? $decoded : null;
} catch (\Throwable $exception) {
return null;
}
}
public static function updateStatus(int $id, string $status, ?string $reasonCode = null): bool
{
try {
return UserLifecycleAuditRepository::updateStatus($id, $status, $reasonCode);
} catch (\Throwable $exception) {
return false;
}
}
public static function purgeExpired(): int
{
return UserLifecycleAuditRepository::purgeOlderThanDays(self::RETENTION_DAYS);
}
private static function buildBaseRow(
string $runUuid,
string $action,
string $triggerType,
string $status,
?string $reasonCode,
array $policy,
?int $actorUserId,
array $targetUser
): array {
$triggerType = strtolower(trim($triggerType));
if (!in_array($triggerType, ['manual', 'cron', 'system'], true)) {
$triggerType = 'system';
}
$status = strtolower(trim($status));
if (!in_array($status, ['success', 'skipped', 'failed'], true)) {
$status = 'failed';
}
return [
'run_uuid' => trim($runUuid),
'action' => $action,
'trigger_type' => $triggerType,
'status' => $status,
'reason_code' => $reasonCode !== null ? trim($reasonCode) : null,
'policy_deactivate_days' => max(0, (int) ($policy['deactivate_days'] ?? 0)),
'policy_delete_days' => max(0, (int) ($policy['delete_days'] ?? 0)),
'actor_user_id' => ($actorUserId ?? 0) > 0 ? (int) $actorUserId : null,
'target_user_id' => ((int) ($targetUser['id'] ?? 0)) > 0 ? (int) $targetUser['id'] : null,
'target_user_uuid' => self::stringOrNull($targetUser['uuid'] ?? null),
'target_user_email' => self::stringOrNull($targetUser['email'] ?? null),
'snapshot_enc' => null,
'snapshot_version' => self::SNAPSHOT_VERSION,
];
}
private static function buildSnapshot(array $targetUser): array
{
$snapshot = [];
foreach (self::SNAPSHOT_FIELDS as $field) {
$snapshot[$field] = $targetUser[$field] ?? null;
}
return $snapshot;
}
private static function stringOrNull(mixed $value): ?string
{
$value = trim((string) $value);
return $value !== '' ? $value : null;
}
}