httpClient() ->post('/register', $data); if ($response->successful()) { $result = $response->json(); $this->saveCredentials( $result['installation_id'] ?? '', $result['api_token'] ?? '' ); return $result; } return null; } catch (\Exception $e) { Log::warning('Support API registration failed: ' . $e->getMessage()); return null; } } public function isRegistered(): bool { $data = $this->readInstalled(); return !empty($data['installation_id']) && !empty($data['api_token']); } // ─── License ───────────────────────────────────────── public function validateLicense(string $key): ?array { try { $response = $this->authenticatedClient() ->post('/license/validate', ['license_key' => $key]); if ($response->successful()) { $result = $response->json(); Cache::put('support.license_valid', $result['valid'] ?? false, 86400); return $result; } return null; } catch (\Exception $e) { Log::warning('License validation failed: ' . $e->getMessage()); return null; } } // ─── Updates ───────────────────────────────────────── public function checkForUpdate(bool $force = false): ?array { $cacheKey = 'support.update_check'; if (!$force && Cache::has($cacheKey)) { return Cache::get($cacheKey); } try { $params = [ 'current_version' => config('app.version'), 'app_name' => \App\Models\Setting::get('app_name', config('app.name')), ]; $logoUrl = $this->getLogoUrl(); if ($logoUrl) { $params['logo_url'] = $logoUrl; } $response = $this->authenticatedClient() ->get('/version/check', $params); if ($response->successful()) { $result = $response->json(); Cache::put($cacheKey, $result, 86400); return $result; } return null; } catch (\Exception $e) { Log::warning('Update check failed: ' . $e->getMessage()); return null; } } public function hasUpdate(): bool { $cached = Cache::get('support.update_check'); if (!$cached) { return false; } return version_compare($cached['latest_version'] ?? '0.0.0', config('app.version'), '>'); } // ─── Tickets ───────────────────────────────────────── public function getTickets(): ?array { try { $response = $this->authenticatedClient()->get('/tickets'); if ($response->successful()) { return $response->json(); } return null; } catch (\Exception $e) { Log::warning('Ticket list fetch failed: ' . $e->getMessage()); return null; } } public function getTicket(int $id): ?array { try { $response = $this->authenticatedClient()->get("/tickets/{$id}"); if ($response->successful()) { return $response->json(); } return null; } catch (\Exception $e) { Log::warning("Ticket #{$id} fetch failed: " . $e->getMessage()); return null; } } public function createTicket(array $data): ?array { try { $response = $this->authenticatedClient()->post('/tickets', $data); if ($response->successful()) { return $response->json(); } return null; } catch (\Exception $e) { Log::warning('Ticket creation failed: ' . $e->getMessage()); return null; } } public function replyToTicket(int $id, array $data): ?array { try { $response = $this->authenticatedClient() ->post("/tickets/{$id}/messages", $data); if ($response->successful()) { return $response->json(); } return null; } catch (\Exception $e) { Log::warning("Ticket #{$id} reply failed: " . $e->getMessage()); return null; } } // ─── System Info ───────────────────────────────────── public function getSystemInfo(): array { return [ 'app_version' => config('app.version'), 'php_version' => PHP_VERSION, 'laravel_version' => app()->version(), 'db_driver' => config('database.default'), 'locale' => app()->getLocale(), 'os' => PHP_OS, ]; } public function getLogoUrl(): ?string { $favicon = \App\Models\Setting::get('app_favicon'); if ($favicon) { return rtrim(config('app.url'), '/') . '/storage/' . $favicon; } return null; } // ─── Storage/Installed Access ──────────────────────── public function readInstalled(): array { if ($this->installedData !== null) { return $this->installedData; } $path = storage_path('installed'); if (!file_exists($path)) { return $this->installedData = []; } $data = json_decode(file_get_contents($path), true); return $this->installedData = is_array($data) ? $data : []; } // ─── Private Helpers ───────────────────────────────── private function httpClient(): \Illuminate\Http\Client\PendingRequest { $apiUrl = config('support.api_url'); // SSRF-Schutz: Nur HTTPS und keine privaten IPs (T06) $parsed = parse_url($apiUrl); $scheme = $parsed['scheme'] ?? ''; $host = $parsed['host'] ?? ''; if ($scheme !== 'https') { throw new \RuntimeException('Support API URL must use HTTPS.'); } $ip = gethostbyname($host); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) { throw new \RuntimeException('Support API URL must not resolve to a private/reserved IP.'); } // DNS-Rebinding verhindern: aufgelöste IP direkt verwenden (V07) $resolvedUrl = str_replace($host, $ip, $apiUrl); return Http::baseUrl($resolvedUrl) ->timeout(config('support.timeout', 10)) ->connectTimeout(config('support.connect_timeout', 5)) ->withHeaders(['Accept' => 'application/json', 'Host' => $host]); } private function authenticatedClient(): \Illuminate\Http\Client\PendingRequest { $token = $this->readInstalled()['api_token'] ?? ''; return $this->httpClient()->withToken($token); } private function saveCredentials(string $installationId, string $apiToken): void { $path = storage_path('installed'); $data = $this->readInstalled(); $data['installation_id'] = $installationId; $data['api_token'] = $apiToken; $data['registered_at'] = now()->toIso8601String(); file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT)); chmod($path, 0600); // Reset memoized data $this->installedData = $data; } }