= 100 && $statusCode <= 599) { $where[] = 'api_audit_log.status_code = ?'; $params[] = (string) $statusCode; } } if (in_array($method, ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], true)) { $where[] = 'api_audit_log.method = ?'; $params[] = $method; } if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $createdFrom)) { $where[] = 'api_audit_log.created_at >= ?'; $params[] = $createdFrom . ' 00:00:00'; } if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $createdTo)) { $where[] = 'api_audit_log.created_at <= ?'; $params[] = $createdTo . ' 23:59:59'; } if ($tenantIds !== []) { $where[] = 'api_audit_log.tenant_id in (???)'; $params[] = $tenantIds; } if ($userIds !== []) { $where[] = 'api_audit_log.user_id in (???)'; $params[] = $userIds; } $whereSql = $where ? (' where ' . implode(' and ', $where)) : ''; $fromSql = ' from api_audit_log ' . 'left join users on users.id = api_audit_log.user_id ' . 'left join tenants on tenants.id = api_audit_log.tenant_id '; $total = (int) (DB::selectValue('select count(*)' . $fromSql . $whereSql, ...$params) ?? 0); $rows = DB::select( 'select api_audit_log.id, api_audit_log.request_id, api_audit_log.method, api_audit_log.path, api_audit_log.query_json, api_audit_log.status_code, api_audit_log.duration_ms, api_audit_log.error_code, api_audit_log.user_id, api_audit_log.tenant_id, api_audit_log.api_token_id, api_audit_log.token_tenant_id, api_audit_log.ip, api_audit_log.user_agent, api_audit_log.created_at, users.id, users.uuid, users.display_name, users.email, tenants.id, tenants.uuid, tenants.description ' . $fromSql . $whereSql . sprintf(' order by api_audit_log.`%s` %s limit ? offset ?', $order, $dir), ...array_merge($params, [(string) $limit, (string) $offset]) ); $normalized = []; if (is_array($rows)) { foreach ($rows as $row) { $item = $this->normalizeRow($row); if ($item !== null) { $normalized[] = $item; } } } return [ 'total' => $total, 'rows' => $normalized, ]; } public function listFilterOptions(int $limit = self::FILTER_OPTIONS_LIMIT_MAX): array { $limit = max(1, min(self::FILTER_OPTIONS_LIMIT_MAX, $limit)); $methodRows = DB::select( 'select api_audit_log.method, max(api_audit_log.created_at) as last_used_at from api_audit_log where api_audit_log.method is not null and api_audit_log.method <> \'\' group by api_audit_log.method order by last_used_at desc limit ?', (string) $limit ); $methods = []; if (is_array($methodRows)) { foreach ($methodRows as $row) { $flat = RepositoryArrayHelper::flattenRow($row); $method = strtoupper(trim((string) ($flat['method'] ?? ''))); if ($method === '') { continue; } $methods[] = $method; } } $methods = array_values(array_unique($methods)); $userRows = DB::select( 'select api_audit_log.user_id, max(api_audit_log.created_at) as last_used_at, max(users.id) as user_exists_id, max(users.display_name) as user_display_name, max(users.email) as user_email from api_audit_log left join users on users.id = api_audit_log.user_id where api_audit_log.user_id is not null and api_audit_log.user_id > 0 group by api_audit_log.user_id order by last_used_at desc limit ?', (string) $limit ); $users = []; if (is_array($userRows)) { foreach ($userRows as $row) { $flat = RepositoryArrayHelper::flattenRow($row); $id = (int) ($flat['user_id'] ?? 0); if ($id <= 0) { continue; } $users[] = [ 'id' => $id, 'display_name' => trim((string) ($flat['user_display_name'] ?? '')), 'email' => trim((string) ($flat['user_email'] ?? '')), 'exists' => (int) ($flat['user_exists_id'] ?? 0) > 0, ]; } } $tenantRows = DB::select( 'select api_audit_log.tenant_id, max(api_audit_log.created_at) as last_used_at, max(tenants.id) as tenant_exists_id, max(tenants.description) as tenant_description from api_audit_log left join tenants on tenants.id = api_audit_log.tenant_id where api_audit_log.tenant_id is not null and api_audit_log.tenant_id > 0 group by api_audit_log.tenant_id order by last_used_at desc limit ?', (string) $limit ); $tenants = []; if (is_array($tenantRows)) { foreach ($tenantRows as $row) { $flat = RepositoryArrayHelper::flattenRow($row); $id = (int) ($flat['tenant_id'] ?? 0); if ($id <= 0) { continue; } $tenants[] = [ 'id' => $id, 'description' => trim((string) ($flat['tenant_description'] ?? '')), 'exists' => (int) ($flat['tenant_exists_id'] ?? 0) > 0, ]; } } return [ 'methods' => $methods, 'users' => $users, 'tenants' => $tenants, ]; } public function find(int $id): ?array { $row = DB::selectOne( 'select api_audit_log.id, api_audit_log.request_id, api_audit_log.method, api_audit_log.path, api_audit_log.query_json, api_audit_log.status_code, api_audit_log.duration_ms, api_audit_log.error_code, api_audit_log.user_id, api_audit_log.tenant_id, api_audit_log.api_token_id, api_audit_log.token_tenant_id, api_audit_log.ip, api_audit_log.user_agent, api_audit_log.created_at, users.id, users.uuid, users.display_name, users.email, tenants.id, tenants.uuid, tenants.description from api_audit_log left join users on users.id = api_audit_log.user_id left join tenants on tenants.id = api_audit_log.tenant_id where api_audit_log.id = ? limit 1', (string) $id ); return $this->normalizeRow($row); } public function purgeOlderThanDays(int $days): int { if ($days <= 0) { return 0; } $cutoff = (new \DateTimeImmutable('now', new \DateTimeZone('UTC'))) ->modify('-' . $days . ' days') ->format('Y-m-d H:i:s'); $deleted = DB::delete('delete from api_audit_log where created_at < ?', $cutoff); return is_int($deleted) ? $deleted : 0; } private function normalizeRow(mixed $row): ?array { if (!is_array($row)) { return null; } $log = $row['api_audit_log'] ?? []; if (!is_array($log) || !isset($log['id'])) { return null; } $user = is_array($row['users'] ?? null) ? $row['users'] : []; $tenant = is_array($row['tenants'] ?? null) ? $row['tenants'] : []; $log['user_uuid'] = (string) ($user['uuid'] ?? ''); $log['user_display_name'] = (string) ($user['display_name'] ?? ''); $log['user_email'] = (string) ($user['email'] ?? ''); $log['tenant_uuid'] = (string) ($tenant['uuid'] ?? ''); $log['tenant_description'] = (string) ($tenant['description'] ?? ''); $log['ip_hash'] = trim((string) ($log['ip'] ?? '')); $log['user_agent_hash'] = trim((string) ($log['user_agent'] ?? '')); $log['query_keys'] = $this->extractQueryKeys($log['query_json'] ?? null); return $log; } /** * @return list */ private function extractQueryKeys(mixed $queryJson): array { if (!is_string($queryJson) || trim($queryJson) === '') { return []; } $decoded = json_decode($queryJson, true); if (!is_array($decoded)) { return []; } $keysRaw = $decoded['keys'] ?? null; if (!is_array($keysRaw)) { return []; } $keys = []; foreach ($keysRaw as $value) { $key = trim((string) $value); if ($key === '' || in_array($key, $keys, true)) { continue; } $keys[] = $key; } return $keys; } }