user()->isAdmin()) { abort(403); } $allSettings = Setting::all()->keyBy('key'); // Event-Default-Keys separieren — immer alle liefern (auch wenn nicht in DB) $eventDefaults = collect(); foreach (['home_game', 'away_game', 'training', 'tournament', 'meeting'] as $type) { foreach (['players', 'catering', 'timekeepers'] as $field) { $key = "default_min_{$field}_{$type}"; $eventDefaults[$key] = $allSettings[$key]->value ?? null; } } // Visibility-Settings separieren $visibilitySettings = $allSettings->filter(fn ($s) => str_starts_with($s->key, 'visibility_')); $settings = $allSettings->filter(fn ($s) => !str_starts_with($s->key, 'default_min_') && !str_starts_with($s->key, 'visibility_') && !str_starts_with($s->key, 'impressum_html_') && !str_starts_with($s->key, 'datenschutz_html_') && !str_starts_with($s->key, 'password_reset_email_') ); $fileCategories = FileCategory::ordered()->withCount('files')->get(); // Verfügbare Sprachen und deren locale-spezifische Settings $availableLocales = ['de', 'en', 'pl', 'ru', 'ar', 'tr']; $localeSettings = []; foreach ($availableLocales as $locale) { $localeSettings[$locale] = [ 'impressum_html' => $allSettings["impressum_html_{$locale}"]->value ?? '', 'datenschutz_html' => $allSettings["datenschutz_html_{$locale}"]->value ?? '', 'password_reset_email' => $allSettings["password_reset_email_{$locale}"]->value ?? '', ]; } // Support-API-Status (nur für Admin-Tab) $supportService = app(SupportApiService::class); $isRegistered = $supportService->isRegistered(); $installationId = $isRegistered ? ($supportService->readInstalled()['installation_id'] ?? null) : null; $updateInfo = Cache::get('support.update_check'); $mailConfig = [ 'mailer' => config('mail.default'), 'host' => config('mail.mailers.smtp.host'), 'port' => config('mail.mailers.smtp.port'), 'username' => config('mail.mailers.smtp.username'), 'password' => config('mail.mailers.smtp.password'), 'encryption' => config('mail.mailers.smtp.scheme', 'tls'), 'from_address' => config('mail.from.address'), 'from_name' => config('mail.from.name'), ]; return view('admin.settings.edit', compact( 'settings', 'eventDefaults', 'fileCategories', 'visibilitySettings', 'isRegistered', 'installationId', 'updateInfo', 'availableLocales', 'localeSettings', 'mailConfig' )); } public function update(Request $request): RedirectResponse { if (!auth()->user()->isAdmin()) { abort(403, 'Nur Admins koennen Einstellungen aendern.'); } // Favicon-Upload verarbeiten (vor der normalen Settings-Schleife) if ($request->hasFile('favicon')) { $request->validate([ 'favicon' => 'file|mimes:ico,png,svg,jpg,jpeg,gif,webp|max:512', ]); // Altes Favicon löschen $oldFavicon = Setting::get('app_favicon'); if ($oldFavicon) { Storage::disk('public')->delete($oldFavicon); } $file = $request->file('favicon'); $filename = Str::uuid() . '.' . $file->guessExtension(); $path = $file->storeAs('favicon', $filename, 'public'); Setting::set('app_favicon', $path); } elseif ($request->has('remove_favicon')) { $oldFavicon = Setting::get('app_favicon'); if ($oldFavicon) { Storage::disk('public')->delete($oldFavicon); } Setting::set('app_favicon', null); } $inputSettings = $request->input('settings', []); // Whitelist: Nur erlaubte Setting-Keys akzeptieren $allowedLocales = ['de', 'en', 'pl', 'ru', 'ar', 'tr']; $allowedPrefixes = ['default_min_', 'visibility_']; $allowedLocaleKeys = []; foreach ($allowedLocales as $loc) { $allowedLocaleKeys[] = "impressum_html_{$loc}"; $allowedLocaleKeys[] = "datenschutz_html_{$loc}"; $allowedLocaleKeys[] = "password_reset_email_{$loc}"; } $oldValues = Setting::whereIn('key', array_keys($inputSettings))->pluck('value', 'key')->toArray(); foreach ($inputSettings as $key => $value) { // Whitelist-Pruefung: Nur bekannte Keys oder erlaubte Prefixe $isExistingSetting = Setting::where('key', $key)->exists(); $isAllowedLocaleKey = in_array($key, $allowedLocaleKeys); $isAllowedPrefix = false; foreach ($allowedPrefixes as $prefix) { if (str_starts_with($key, $prefix)) { $isAllowedPrefix = true; break; } } if (!$isExistingSetting && !$isAllowedLocaleKey && !$isAllowedPrefix) { continue; // Unbekannten Key ignorieren } $setting = Setting::where('key', $key)->first(); if ($setting) { if ($setting->type === 'html' || $setting->type === 'richtext') { $value = $this->sanitizer->sanitize($value ?? ''); } elseif ($setting->type === 'number') { $value = $value !== null && $value !== '' ? (int) $value : null; } else { $value = strip_tags($value ?? ''); } $setting->update(['value' => $value]); } elseif ($isAllowedLocaleKey) { // Locale-suffixed legal/email settings: upsert mit HTML-Sanitisierung $value = $this->sanitizer->sanitize($value ?? ''); $localeSetting = Setting::where('key', $key)->first(); if ($localeSetting) { $localeSetting->update(['value' => $value]); } else { $localeSetting = new Setting(['label' => $key, 'type' => 'html', 'value' => $value]); $localeSetting->key = $key; $localeSetting->save(); } } elseif ($isAllowedPrefix) { // Event-Defaults / Visibility: upsert — anlegen wenn nicht vorhanden $prefixSetting = new Setting([ 'label' => $key, 'type' => 'number', 'value' => $value !== null && $value !== '' ? (int) $value : null, ]); $prefixSetting->key = $key; $prefixSetting->save(); } } Setting::clearCache(); $newValues = Setting::whereIn('key', array_keys($inputSettings))->pluck('value', 'key')->toArray(); ActivityLog::logWithChanges('updated', __('admin.log_settings_updated'), 'Setting', null, $oldValues, $newValues); // License key validation when changed $newLicenseKey = $inputSettings['license_key'] ?? null; $oldLicenseKey = $oldValues['license_key'] ?? null; if ($newLicenseKey && $newLicenseKey !== $oldLicenseKey) { $supportService = app(SupportApiService::class); $result = $supportService->validateLicense($newLicenseKey); if ($result && !($result['valid'] ?? false)) { session()->flash('warning', __('admin.license_invalid')); } } return back()->with('success', __('admin.settings_saved')); } public function updateMail(Request $request): RedirectResponse { if (! auth()->user()->isAdmin()) { abort(403); } $mailer = $request->input('mail_mailer', 'log'); if ($mailer === 'smtp') { $request->validate([ 'mail_host' => 'required|string|max:255', 'mail_port' => 'required|integer|min:1|max:65535', 'mail_username' => 'required|string|max:255', 'mail_password' => 'required|string|max:255', 'mail_from_address' => 'required|email|max:255', 'mail_from_name' => 'nullable|string|max:255', 'mail_encryption' => 'required|in:tls,ssl,none', ]); $encryption = $request->input('mail_encryption'); $this->updateEnvValues([ 'MAIL_MAILER' => 'smtp', 'MAIL_HOST' => $request->input('mail_host'), 'MAIL_PORT' => $request->input('mail_port'), 'MAIL_USERNAME' => $request->input('mail_username'), 'MAIL_PASSWORD' => $request->input('mail_password'), 'MAIL_FROM_ADDRESS' => $request->input('mail_from_address'), 'MAIL_FROM_NAME' => $request->input('mail_from_name', config('app.name')), 'MAIL_SCHEME' => $encryption === 'none' ? '' : $encryption, ]); } else { $this->updateEnvValues([ 'MAIL_MAILER' => 'log', ]); } Artisan::call('config:clear'); return back()->with('success', __('admin.mail_saved'))->withFragment('mail'); } public function testMail(Request $request): \Illuminate\Http\JsonResponse { if (! auth()->user()->isAdmin()) { return response()->json(['success' => false, 'message' => 'Keine Berechtigung.'], 403); } $request->validate([ 'mail_host' => 'required|string|max:255', 'mail_port' => 'required|integer|min:1|max:65535', 'mail_username' => 'required|string|max:255', 'mail_password' => 'required|string|max:255', 'mail_encryption' => 'required|in:tls,ssl,none', ]); try { $encryption = $request->input('mail_encryption'); $tls = ($encryption !== 'none'); $transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport( $request->input('mail_host'), (int) $request->input('mail_port'), $tls, ); $transport->setUsername($request->input('mail_username')); $transport->setPassword($request->input('mail_password')); $transport->start(); $transport->stop(); return response()->json(['success' => true, 'message' => __('admin.mail_test_success')]); } catch (\Throwable $e) { return response()->json(['success' => false, 'message' => $e->getMessage()]); } } private function updateEnvValues(array $values): void { $envPath = base_path('.env'); $envContent = file_get_contents($envPath); foreach ($values as $key => $value) { if ($value === '' || $value === null) { $replacement = "# {$key}="; } else { $quotedValue = str_contains($value, ' ') || str_contains($value, '#') ? '"' . str_replace('"', '\\"', $value) . '"' : $value; $replacement = "{$key}={$quotedValue}"; } if (preg_match("/^#?\s*{$key}=.*/m", $envContent)) { $envContent = preg_replace("/^#?\s*{$key}=.*/m", $replacement, $envContent); } else { $envContent .= "\n{$replacement}"; } } file_put_contents($envPath, $envContent); } public function destroyDemoData(Request $request): RedirectResponse { if (! auth()->user()->isAdmin()) { abort(403); } $request->validate([ 'password' => ['required', 'current_password'], ]); // Löschreihenfolge beachtet FK-Constraints DB::table('activity_logs')->delete(); DB::table('comments')->delete(); DB::table('event_participants')->delete(); DB::table('event_catering')->delete(); DB::table('event_timekeepers')->delete(); DB::table('event_faq')->delete(); DB::table('event_file')->delete(); DB::table('events')->delete(); DB::table('parent_player')->delete(); DB::table('players')->delete(); DB::table('team_user')->delete(); DB::table('team_file')->delete(); DB::table('teams')->delete(); DB::table('invitation_players')->delete(); DB::table('invitations')->delete(); DB::table('locations')->delete(); DB::table('faq')->delete(); DB::table('users')->where('id', '!=', auth()->id())->delete(); // Hochgeladene Dateien aus Storage entfernen + DB-Einträge löschen $files = DB::table('files')->get(); foreach ($files as $file) { Storage::disk('private')->delete($file->path); } DB::table('files')->delete(); // Profilbilder-Ordner leeren (Admin-Bild bleibt via DB erhalten) $adminAvatar = auth()->user()->profile_picture; foreach (Storage::disk('public')->files('avatars') as $avatarFile) { if ($adminAvatar && str_contains($avatarFile, $adminAvatar)) { continue; } Storage::disk('public')->delete($avatarFile); } ActivityLog::log('deleted', __('admin.demo_data_deleted')); return redirect()->route('admin.settings.edit') ->with('success', __('admin.demo_data_deleted')); } public function factoryReset(Request $request): RedirectResponse { if (! auth()->user()->isAdmin()) { abort(403); } $request->validate([ 'password' => ['required', 'current_password'], 'confirmation' => ['required', 'in:RESET-BESTÄTIGT'], ]); // 1. Alle hochgeladenen Dateien entfernen Storage::disk('private')->deleteDirectory('files'); Storage::disk('public')->deleteDirectory('avatars'); Storage::disk('public')->deleteDirectory('favicon'); Storage::disk('public')->deleteDirectory('dsgvo'); // 2. FK-Constraints deaktivieren (DB-agnostisch) $driver = DB::getDriverName(); if ($driver === 'sqlite') { DB::statement('PRAGMA foreign_keys = OFF;'); } else { DB::statement('SET FOREIGN_KEY_CHECKS = 0;'); } // 3. Alle Tabellen leeren $tables = [ 'activity_logs', 'comments', 'event_participants', 'event_catering', 'event_timekeepers', 'event_faq', 'event_file', 'events', 'parent_player', 'players', 'team_user', 'team_file', 'teams', 'invitation_players', 'invitations', 'locations', 'faq', 'files', 'file_categories', 'settings', 'users', 'sessions', 'cache', 'cache_locks', ]; foreach ($tables as $table) { DB::table($table)->delete(); } // 4. FK-Constraints reaktivieren if ($driver === 'sqlite') { DB::statement('PRAGMA foreign_keys = ON;'); } else { DB::statement('SET FOREIGN_KEY_CHECKS = 1;'); } // 5. storage/installed entfernen → Installer-Modus aktivieren $installedFile = storage_path('installed'); if (file_exists($installedFile)) { unlink($installedFile); } // 6. Caches leeren Artisan::call('cache:clear'); Artisan::call('config:clear'); Artisan::call('view:clear'); Artisan::call('route:clear'); // 7. Session invalidieren + Logout auth()->logout(); request()->session()->invalidate(); request()->session()->regenerateToken(); // 8. Redirect zum Installer return redirect()->route('install.requirements'); } }