Files
WebAPP/app/Http/Controllers/Admin/SettingsController.php
Rhino ad60e7a9f9 Spielerpositionen, Statistiken, Fahrgemeinschaften, Spielfeld-Visualisierung
- PlayerPosition Enum (7 Handball-Positionen) mit Label/ShortLabel
- Spielerstatistik pro Spiel (Tore, Würfe, TW-Paraden, Bemerkung)
- Position-Dropdown in Spieler-Editor und Event-Stats-Formular
- Statistik-Seite: TW zuerst, Trennlinie, Feldspieler, Position-Badges
- Spielfeld-SVG mit Ampel-Performance (grün/gelb/rot)
- Anklickbare Spieler im Spielfeld öffnen Detail-Modal
- Fahrgemeinschaften (Anbieten, Zuordnen, Zurückziehen)
- Übersetzungen in allen 6 Sprachen (de, en, pl, ru, ar, tr)
- .gitignore für Laravel hinzugefügt
- Demo-Daten mit Positionen und Statistiken

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 11:47:34 +01:00

431 lines
17 KiB
PHP
Executable File

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\ActivityLog;
use App\Models\FileCategory;
use App\Models\Setting;
use App\Services\HtmlSanitizerService;
use App\Services\SupportApiService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\View\View;
class SettingsController extends Controller
{
public function __construct(private HtmlSanitizerService $sanitizer) {}
public function edit(): View
{
if (!auth()->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.');
}
// Bild-Uploads verarbeiten (vor der normalen Settings-Schleife)
$imageUploads = [
'favicon' => ['setting' => 'app_favicon', 'dir' => 'favicon', 'max' => 512],
'logo_login' => ['setting' => 'app_logo_login', 'dir' => 'logos', 'max' => 1024],
'logo_app' => ['setting' => 'app_logo_app', 'dir' => 'logos', 'max' => 1024],
];
foreach ($imageUploads as $field => $config) {
if ($request->hasFile($field)) {
$request->validate([
$field => 'file|mimes:ico,png,svg,jpg,jpeg,gif,webp|max:' . $config['max'],
]);
$oldFile = Setting::get($config['setting']);
if ($oldFile) {
Storage::disk('public')->delete($oldFile);
}
$file = $request->file($field);
$filename = Str::uuid() . '.' . $file->guessExtension();
$path = $file->storeAs($config['dir'], $filename, 'public');
Setting::set($config['setting'], $path);
} elseif ($request->has("remove_{$field}")) {
$oldFile = Setting::get($config['setting']);
if ($oldFile) {
Storage::disk('public')->delete($oldFile);
}
Setting::set($config['setting'], 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_player_stats')->delete();
DB::table('event_carpool_passengers')->delete();
DB::table('event_carpools')->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('logos');
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_player_stats',
'event_carpool_passengers', 'event_carpools', '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');
}
}