Stand: SMTP-Test, Admin-Mail-Tab, Notifiable-Fix, Lazy-Quill
- 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>
This commit is contained in:
259
app/Services/SupportApiService.php
Normal file
259
app/Services/SupportApiService.php
Normal file
@@ -0,0 +1,259 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user