- Fix: Notifiable-Trait zum User-Model hinzugefuegt (behebt notify()-500er) - Installer: SMTP-Verbindungstest mit EsmtpTransport + Ueberspringen-Link - Admin: Neuer E-Mail-Tab mit SMTP-Konfiguration + Verbindungstest - Admin: Lazy Quill-Initialisierung (nur sichtbare Locale wird geladen) - Uebersetzungen: 17 neue Mail-Keys in allen 6 Sprachen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
260 lines
8.2 KiB
PHP
260 lines
8.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class SupportApiService
|
|
{
|
|
private ?array $installedData = null;
|
|
|
|
// ─── Registration ────────────────────────────────────
|
|
|
|
public function register(array $data): ?array
|
|
{
|
|
try {
|
|
$response = $this->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;
|
|
}
|
|
}
|