- Administration & Rollenmanagement: Neuer Admin-Bereich mit Feature-Toggles und Sichtbarkeitseinstellungen pro Rolle (11 Toggles, 24 Visibility-Settings) - AdministrationController mit eigenem Settings-Tab, aus SettingsController extrahiert - Feature-Toggle-Guards in Controllers (Invitation, File, ListGenerator, Comment) und Views (events/show, events/edit, events/create) - Setting::isFeatureEnabled() und isFeatureVisibleFor() Hilfsmethoden - Wiederkehrende Trainings: Täglich/Wöchentlich/2-Wöchentlich mit Ende per Datum oder Anzahl (max. 52), Vorschau im Formular - Event-Serien: Verknüpfung über event_series_id (UUID), Modal-Dialog beim Speichern und Löschen mit Optionen "nur dieses" / "alle folgenden" - Löschen-Button direkt in der Event-Bearbeitung mit Serien-Dialog - DemoDataSeeder: 4 Trainings als Serie mit gemeinsamer event_series_id - Übersetzungen in allen 6 Sprachen (de, en, pl, ru, ar, tr) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
346 lines
12 KiB
PHP
346 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\ActivityLog;
|
|
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\View\View;
|
|
|
|
class AdministrationController extends Controller
|
|
{
|
|
public function __construct(private HtmlSanitizerService $sanitizer) {}
|
|
|
|
public function index(): View
|
|
{
|
|
if (!auth()->user()->isAdmin()) {
|
|
abort(403);
|
|
}
|
|
|
|
$allSettings = Setting::all()->keyBy('key');
|
|
|
|
// Feature-Toggle-Settings
|
|
$featureSettings = $allSettings->filter(fn ($s) => str_starts_with($s->key, 'feature_'));
|
|
|
|
// Visibility-Settings
|
|
$visibilitySettings = $allSettings->filter(fn ($s) => str_starts_with($s->key, 'visibility_'));
|
|
|
|
// Mail-Konfiguration
|
|
$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'),
|
|
];
|
|
|
|
// Lizenz & Support
|
|
$supportService = app(SupportApiService::class);
|
|
$isRegistered = $supportService->isRegistered();
|
|
$installationId = $isRegistered ? ($supportService->readInstalled()['installation_id'] ?? null) : null;
|
|
$updateInfo = Cache::get('support.update_check');
|
|
|
|
// License-Key
|
|
$licenseKey = $allSettings['license_key']->value ?? '';
|
|
|
|
// Aktivitätslog (letzte 100 Einträge)
|
|
$recentLogs = ActivityLog::with('user')->latest('created_at')->limit(100)->get();
|
|
|
|
return view('admin.administration.index', compact(
|
|
'featureSettings', 'visibilitySettings', 'mailConfig',
|
|
'isRegistered', 'installationId', 'updateInfo', 'licenseKey',
|
|
'recentLogs'
|
|
));
|
|
}
|
|
|
|
public function updateFeatures(Request $request): RedirectResponse
|
|
{
|
|
if (!auth()->user()->isAdmin()) {
|
|
abort(403);
|
|
}
|
|
|
|
$inputSettings = $request->input('settings', []);
|
|
|
|
$oldValues = Setting::whereIn('key', array_keys($inputSettings))->pluck('value', 'key')->toArray();
|
|
|
|
foreach ($inputSettings as $key => $value) {
|
|
// Nur feature_ und visibility_ Keys akzeptieren
|
|
if (!str_starts_with($key, 'feature_') && !str_starts_with($key, 'visibility_')) {
|
|
continue;
|
|
}
|
|
|
|
$setting = Setting::where('key', $key)->first();
|
|
if ($setting) {
|
|
$setting->update(['value' => $value]);
|
|
} else {
|
|
$newSetting = new Setting([
|
|
'label' => $key,
|
|
'type' => 'number',
|
|
'value' => $value,
|
|
]);
|
|
$newSetting->key = $key;
|
|
$newSetting->save();
|
|
}
|
|
}
|
|
|
|
Setting::clearCache();
|
|
|
|
$newValues = Setting::whereIn('key', array_keys($inputSettings))->pluck('value', 'key')->toArray();
|
|
ActivityLog::logWithChanges('updated', __('admin.features_saved'), 'Setting', null, $oldValues, $newValues);
|
|
|
|
return back()->with('success', __('admin.features_saved'));
|
|
}
|
|
|
|
public function updateLicense(Request $request): RedirectResponse
|
|
{
|
|
if (!auth()->user()->isAdmin()) {
|
|
abort(403);
|
|
}
|
|
|
|
$request->validate([
|
|
'license_key' => ['nullable', 'string', 'max:255'],
|
|
]);
|
|
|
|
$oldValue = Setting::get('license_key');
|
|
$newValue = strip_tags($request->input('license_key', ''));
|
|
|
|
Setting::set('license_key', $newValue);
|
|
|
|
if ($newValue && $newValue !== $oldValue) {
|
|
$supportService = app(SupportApiService::class);
|
|
$result = $supportService->validateLicense($newValue);
|
|
if ($result && !($result['valid'] ?? false)) {
|
|
session()->flash('warning', __('admin.license_invalid'));
|
|
}
|
|
}
|
|
|
|
ActivityLog::logWithChanges('updated', __('admin.log_settings_updated'), 'Setting', null,
|
|
['license_key' => $oldValue], ['license_key' => $newValue]);
|
|
|
|
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()]);
|
|
}
|
|
}
|
|
|
|
public function destroyDemoData(Request $request): RedirectResponse
|
|
{
|
|
if (!auth()->user()->isAdmin()) {
|
|
abort(403);
|
|
}
|
|
|
|
$request->validate([
|
|
'password' => ['required', 'current_password'],
|
|
]);
|
|
|
|
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();
|
|
|
|
$files = DB::table('files')->get();
|
|
foreach ($files as $file) {
|
|
Storage::disk('private')->delete($file->path);
|
|
}
|
|
DB::table('files')->delete();
|
|
|
|
$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.administration.index')
|
|
->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'],
|
|
]);
|
|
|
|
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');
|
|
|
|
$driver = DB::getDriverName();
|
|
if ($driver === 'sqlite') {
|
|
DB::statement('PRAGMA foreign_keys = OFF;');
|
|
} else {
|
|
DB::statement('SET FOREIGN_KEY_CHECKS = 0;');
|
|
}
|
|
|
|
$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();
|
|
}
|
|
|
|
if ($driver === 'sqlite') {
|
|
DB::statement('PRAGMA foreign_keys = ON;');
|
|
} else {
|
|
DB::statement('SET FOREIGN_KEY_CHECKS = 1;');
|
|
}
|
|
|
|
$installedFile = storage_path('installed');
|
|
if (file_exists($installedFile)) {
|
|
unlink($installedFile);
|
|
}
|
|
|
|
Artisan::call('cache:clear');
|
|
Artisan::call('config:clear');
|
|
Artisan::call('view:clear');
|
|
Artisan::call('route:clear');
|
|
|
|
auth()->logout();
|
|
request()->session()->invalidate();
|
|
request()->session()->regenerateToken();
|
|
|
|
return redirect()->route('install.requirements');
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|