middleware('throttle:10,1')->group(function () { Route::get('/install', [InstallerController::class, 'requirements'])->name('install.requirements'); Route::get('/install/database', [InstallerController::class, 'database'])->name('install.database'); Route::post('/install/database', [InstallerController::class, 'storeDatabase'])->name('install.database.store'); Route::get('/install/app', [InstallerController::class, 'app'])->name('install.app'); Route::post('/install/app', [InstallerController::class, 'storeApp'])->name('install.app.store'); Route::get('/install/mail', [InstallerController::class, 'mail'])->name('install.mail'); Route::post('/install/mail', [InstallerController::class, 'storeMail'])->name('install.mail.store'); Route::post('/install/test-mail', [InstallerController::class, 'testMail'])->name('install.test-mail'); Route::get('/install/finalize', [InstallerController::class, 'finalize'])->name('install.finalize'); Route::post('/install/finalize', [InstallerController::class, 'storeFinalize'])->name('install.finalize.store'); Route::get('/install/complete', [InstallerController::class, 'complete'])->name('install.complete'); }); // ------------------------------------------------------- // Öffentliche Seiten // ------------------------------------------------------- Route::get('/', fn () => redirect()->route('login')); Route::get('/impressum', fn () => view('legal.impressum'))->name('impressum'); Route::get('/datenschutz', fn () => view('legal.datenschutz'))->name('datenschutz'); Route::get('/offline', fn () => view('offline'))->name('offline'); // Club-Logo — öffentlich erreichbar für externe Dienste (z.B. Support-Backend) Route::get('/club-logo', function () { // 1. Dynamisches Favicon aus Settings $favicon = \App\Models\Setting::get('app_favicon'); if ($favicon && !str_contains($favicon, '..')) { $path = storage_path('app/public/' . $favicon); $realPath = realpath($path); $allowedBase = realpath(storage_path('app/public')); if ($realPath && $allowedBase && str_starts_with($realPath, $allowedBase . DIRECTORY_SEPARATOR) && file_exists($realPath)) { return response()->file($realPath, [ 'Cache-Control' => 'public, max-age=86400', ]); } } // 2. Fallback: statisches Logo $fallback = public_path('images/logo_woelfe.png'); if (file_exists($fallback)) { return response()->file($fallback, [ 'Cache-Control' => 'public, max-age=86400', ]); } abort(404); })->name('club-logo'); // Sprachumschalter Route::post('/locale', function (\Illuminate\Http\Request $request) { $locale = $request->input('locale'); $supported = \App\Http\Middleware\SetLocaleMiddleware::supportedLocales(); if (in_array($locale, $supported)) { session(['locale' => $locale]); if ($request->user()) { $request->user()->update(['locale' => $locale]); } } return back(); })->name('locale.switch')->middleware('throttle:30,1'); // ------------------------------------------------------- // Auth: Login / Logout / Register // ------------------------------------------------------- Route::middleware('guest')->group(function () { Route::get('/login', [LoginController::class, 'showForm'])->name('login'); Route::post('/login', [LoginController::class, 'login'])->middleware('throttle:login'); Route::get('/register/{token}', [RegisterController::class, 'showForm'])->name('register'); Route::post('/register/{token}', [RegisterController::class, 'register'])->middleware('throttle:registration'); // Passwort zurücksetzen (Self-Service) Route::get('/forgot-password', [ForgotPasswordController::class, 'showForm'])->name('password.request'); Route::post('/forgot-password', [ForgotPasswordController::class, 'sendResetLink'])->name('password.email')->middleware('throttle:login'); Route::get('/reset-password/{token}', [ResetPasswordController::class, 'showResetForm'])->name('password.reset'); Route::post('/reset-password', [ResetPasswordController::class, 'reset'])->name('password.update')->middleware('throttle:login'); }); Route::post('/logout', [LoginController::class, 'logout'])->name('logout')->middleware('auth'); // ------------------------------------------------------- // User-Bereich (eingeloggt + aktiv) // ------------------------------------------------------- Route::middleware(['auth'])->group(function () { Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard'); Route::get('/events', [EventController::class, 'index'])->name('events.index'); Route::get('/events/{event}', [EventController::class, 'show'])->name('events.show'); Route::post('/events/{event}/participants', [ParticipantController::class, 'update'])->name('participants.update')->middleware(['throttle:user-actions', 'dsgvo']); Route::post('/events/{event}/catering', [CateringController::class, 'update'])->name('catering.update')->middleware(['throttle:user-actions', 'dsgvo']); Route::post('/events/{event}/timekeeper', [TimekeeperController::class, 'update'])->name('timekeeper.update')->middleware(['throttle:user-actions', 'dsgvo']); Route::post('/events/{event}/comments', [CommentController::class, 'store'])->name('comments.store')->middleware(['throttle:user-actions', 'dsgvo']); Route::post('/events/{event}/carpool/offer', [CarpoolController::class, 'offer'])->name('carpool.offer')->middleware(['throttle:user-actions', 'dsgvo']); Route::post('/events/{event}/carpool/withdraw', [CarpoolController::class, 'withdraw'])->name('carpool.withdraw')->middleware(['throttle:user-actions', 'dsgvo']); Route::post('/events/{event}/carpool/join', [CarpoolController::class, 'join'])->name('carpool.join')->middleware(['throttle:user-actions', 'dsgvo']); Route::post('/events/{event}/carpool/leave', [CarpoolController::class, 'leave'])->name('carpool.leave')->middleware(['throttle:user-actions', 'dsgvo']); Route::get('/files', [FileController::class, 'index'])->name('files.index'); Route::get('/files/{file}/download', [FileController::class, 'download'])->name('files.download'); Route::get('/files/{file}/preview', [FileController::class, 'preview'])->name('files.preview'); Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::put('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy')->middleware('throttle:user-actions'); Route::delete('/profile/picture', [ProfileController::class, 'removePicture'])->name('profile.remove-picture'); // DSGVO-Einverständniserklärung Route::post('/profile/dsgvo-consent', [ProfileController::class, 'uploadDsgvoConsent'])->name('profile.upload-dsgvo-consent')->middleware('throttle:user-actions'); Route::delete('/profile/dsgvo-consent', [ProfileController::class, 'removeDsgvoConsent'])->name('profile.remove-dsgvo-consent'); Route::get('/profile/dsgvo-consent/download', [ProfileController::class, 'downloadDsgvoConsent'])->name('profile.dsgvo-consent'); }); // ------------------------------------------------------- // Admin-Bereich // ------------------------------------------------------- Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(function () { // --- Fuer alle Admin-Panel-Nutzer (Admin, Coach, ParentRep) --- Route::get('/', [AdminDashboardController::class, 'index'])->name('dashboard'); Route::get('statistics', [StatisticsController::class, 'index'])->name('statistics.index'); Route::get('statistics/player/{player}', [StatisticsController::class, 'playerDetail'])->name('statistics.player-detail'); // Events (Leseansicht fuer alle Admin-Panel-Nutzer) Route::get('events', [AdminEventController::class, 'index'])->name('events.index'); Route::get('events/{event}/edit', [AdminEventController::class, 'edit'])->name('events.edit'); // Geocoding API (intern, AJAX) Route::get('api/geocode', [GeocodingController::class, 'search'])->name('api.geocode')->middleware('throttle:geocoding'); // --- Nur fuer Staff (Admin + Coach) --- Route::middleware('staff')->group(function () { // Events (Mutations nur fuer Staff) Route::get('events/create', [AdminEventController::class, 'create'])->name('events.create'); Route::post('events', [AdminEventController::class, 'store'])->name('events.store'); Route::put('events/{event}', [AdminEventController::class, 'update'])->name('events.update'); Route::delete('events/{event}', [AdminEventController::class, 'destroy'])->name('events.destroy'); Route::patch('events/{event}/participant', [AdminEventController::class, 'updateParticipant'])->name('events.update-participant'); Route::post('events/{event}/stats', [AdminEventController::class, 'updateStats'])->name('events.update-stats'); Route::put('events/{event}/restore', [AdminEventController::class, 'restore'])->name('events.restore'); // Aktivitaetslog (Staff-Level + canViewActivityLog-Pruefung im Controller) Route::get('activity-logs', [ActivityLogController::class, 'index'])->name('activity-logs.index'); Route::post('activity-logs/{log}/revert', [ActivityLogController::class, 'revert'])->name('activity-logs.revert'); // Teams Route::resource('teams', TeamController::class)->except(['show', 'destroy']); Route::patch('teams/{team}/player-team', [TeamController::class, 'updatePlayerTeam'])->name('teams.update-player-team'); // Spieler Route::resource('players', PlayerController::class)->except(['show']); Route::put('players/{id}/restore', [PlayerController::class, 'restore'])->name('players.restore'); Route::put('players/{player}/toggle-active', [PlayerController::class, 'toggleActive'])->name('players.toggle-active'); Route::patch('players/{player}/quick-update', [PlayerController::class, 'quickUpdate'])->name('players.quick-update'); Route::post('players/{player}/assign-parent', [PlayerController::class, 'assignParent'])->name('players.assign-parent'); Route::delete('players/{player}/remove-parent/{user}', [PlayerController::class, 'removeParent'])->name('players.remove-parent'); Route::delete('players/{player}/picture', [PlayerController::class, 'removePicture'])->name('players.remove-picture'); // Benutzer Route::get('users', [UserController::class, 'index'])->name('users.index'); Route::get('users/{user}/edit', [UserController::class, 'edit'])->name('users.edit'); Route::put('users/{user}', [UserController::class, 'update'])->name('users.update'); Route::delete('users/{user}', [UserController::class, 'destroy'])->name('users.destroy'); Route::put('users/{id}/restore', [UserController::class, 'restore'])->name('users.restore'); Route::put('users/{user}/toggle-active', [UserController::class, 'toggleActive'])->name('users.toggle-active'); Route::put('users/{user}/role', [UserController::class, 'updateRole'])->name('users.role'); Route::put('users/{user}/reset-password', [UserController::class, 'resetPassword'])->name('users.reset-password'); Route::delete('users/{user}/picture', [UserController::class, 'removePicture'])->name('users.remove-picture'); Route::put('users/{user}/dsgvo-toggle', [UserController::class, 'toggleDsgvoConsent'])->name('users.dsgvo-toggle'); Route::put('users/{user}/dsgvo-reject', [UserController::class, 'rejectDsgvoConsent'])->name('users.dsgvo-reject'); Route::get('users/{user}/dsgvo-consent', [UserController::class, 'viewDsgvoConsent'])->name('users.view-dsgvo-consent'); // Einladungen Route::resource('invitations', InvitationController::class)->only(['index', 'create', 'store', 'destroy']); // Dateiverwaltung Route::resource('files', AdminFileController::class)->only(['index', 'create', 'store', 'destroy']); // Dateikategorien Route::post('file-categories', [FileCategoryController::class, 'store'])->name('file-categories.store'); Route::put('file-categories/{category}', [FileCategoryController::class, 'update'])->name('file-categories.update'); Route::delete('file-categories/{category}', [FileCategoryController::class, 'destroy'])->name('file-categories.destroy'); // Kommentar-Moderation Route::delete('comments/{comment}', [AdminCommentController::class, 'softDelete'])->name('comments.destroy'); // --- Nur fuer Admin (Route-Level-Schutz, V03) --- Route::middleware('admin-only')->group(function () { // Einstellungen Route::get('settings', [SettingsController::class, 'edit'])->name('settings.edit'); Route::put('settings', [SettingsController::class, 'update'])->name('settings.update'); Route::put('settings/mail', [SettingsController::class, 'updateMail'])->name('settings.update-mail'); Route::post('settings/test-mail', [SettingsController::class, 'testMail'])->name('settings.test-mail'); Route::delete('settings/demo-data', [SettingsController::class, 'destroyDemoData'])->name('settings.destroy-demo-data')->middleware('throttle:5,1'); Route::delete('settings/factory-reset', [SettingsController::class, 'factoryReset'])->name('settings.factory-reset')->middleware('throttle:3,1'); // Bekannte Orte Route::get('locations', [LocationController::class, 'index'])->name('locations.index'); Route::post('locations', [LocationController::class, 'store'])->name('locations.store'); Route::put('locations/{location}', [LocationController::class, 'update'])->name('locations.update'); Route::delete('locations/{location}', [LocationController::class, 'destroy'])->name('locations.destroy'); // Support Route::get('support', [SupportController::class, 'index'])->name('support.index'); Route::post('support', [SupportController::class, 'store'])->name('support.store'); Route::post('support/register', [SupportController::class, 'register'])->name('support.register'); Route::get('support/{ticketId}', [SupportController::class, 'show'])->name('support.show'); Route::post('support/{ticketId}/reply', [SupportController::class, 'reply'])->name('support.reply'); }); // Listenerstellung Route::get('list-generator', [ListGeneratorController::class, 'create'])->name('list-generator.create'); Route::post('list-generator', [ListGeneratorController::class, 'store'])->name('list-generator.store'); }); });