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:
Rhino
2026-03-02 07:30:37 +01:00
commit 2e24a40d68
9633 changed files with 1300799 additions and 0 deletions

8
routes/console.php Executable file
View File

@@ -0,0 +1,8 @@
<?php
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');

245
routes/web.php Executable file
View File

@@ -0,0 +1,245 @@
<?php
use App\Http\Controllers\Auth\ForgotPasswordController;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\RegisterController;
use App\Http\Controllers\Auth\ResetPasswordController;
use App\Http\Controllers\CateringController;
use App\Http\Controllers\TimekeeperController;
use App\Http\Controllers\CommentController;
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\EventController;
use App\Http\Controllers\InstallerController;
use App\Http\Controllers\ParticipantController;
use App\Http\Controllers\FileController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\Admin\DashboardController as AdminDashboardController;
use App\Http\Controllers\Admin\FileController as AdminFileController;
use App\Http\Controllers\Admin\FileCategoryController;
use App\Http\Controllers\Admin\TeamController;
use App\Http\Controllers\Admin\PlayerController;
use App\Http\Controllers\Admin\UserController;
use App\Http\Controllers\Admin\InvitationController;
use App\Http\Controllers\Admin\EventController as AdminEventController;
use App\Http\Controllers\Admin\CommentController as AdminCommentController;
use App\Http\Controllers\Admin\GeocodingController;
use App\Http\Controllers\Admin\ActivityLogController;
use App\Http\Controllers\Admin\LocationController;
use App\Http\Controllers\Admin\SettingsController;
use App\Http\Controllers\Admin\ListGeneratorController;
use App\Http\Controllers\Admin\StatisticsController;
use App\Http\Controllers\Admin\SupportController;
use Illuminate\Support\Facades\Route;
// -------------------------------------------------------
// Installer (nur wenn noch nicht installiert)
// -------------------------------------------------------
Route::withoutMiddleware([
\App\Http\Middleware\SetLocaleMiddleware::class,
\App\Http\Middleware\ActiveUserMiddleware::class,
])->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::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');
// 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::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');
});
});