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:
25
app/Http/Middleware/ActiveUserMiddleware.php
Executable file
25
app/Http/Middleware/ActiveUserMiddleware.php
Executable file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ActiveUserMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if ($request->user() && (!$request->user()->is_active || $request->user()->trashed())) {
|
||||
Auth::logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('login')
|
||||
->with('error', __('auth_ui.account_deactivated'));
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
19
app/Http/Middleware/AdminMiddleware.php
Executable file
19
app/Http/Middleware/AdminMiddleware.php
Executable file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AdminMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (!$request->user() || !$request->user()->canAccessAdminPanel()) {
|
||||
abort(403, 'Zugriff verweigert.');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
19
app/Http/Middleware/AdminOnlyMiddleware.php
Normal file
19
app/Http/Middleware/AdminOnlyMiddleware.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AdminOnlyMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (!$request->user() || !$request->user()->isAdmin()) {
|
||||
abort(403, 'Zugriff verweigert.');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
21
app/Http/Middleware/DsgvoConsentMiddleware.php
Normal file
21
app/Http/Middleware/DsgvoConsentMiddleware.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class DsgvoConsentMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if ($user && $user->isDsgvoRestricted()) {
|
||||
return back()->with('error', __('ui.dsgvo_restricted'));
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
81
app/Http/Middleware/InstallerMiddleware.php
Normal file
81
app/Http/Middleware/InstallerMiddleware.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class InstallerMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// Auto-create .env from .env.example if missing (first-time deployment)
|
||||
$envPath = base_path('.env');
|
||||
if (! file_exists($envPath) && file_exists(base_path('.env.example'))) {
|
||||
copy(base_path('.env.example'), $envPath);
|
||||
}
|
||||
|
||||
$isInstalled = file_exists(storage_path('installed'));
|
||||
$isInstallerRoute = $request->is('install') || $request->is('install/*');
|
||||
|
||||
// Not installed + not on installer routes → redirect to installer
|
||||
if (! $isInstalled && ! $isInstallerRoute) {
|
||||
// Allow static assets and health check through
|
||||
if ($request->is('favicon.ico', 'images/*', 'up', 'manifest.json', 'sw.js', 'storage/*')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
return redirect('/install');
|
||||
}
|
||||
|
||||
// Already installed + on installer routes → redirect to login
|
||||
if ($isInstalled && $isInstallerRoute) {
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
// Setup-Token-Schutz: Installer nur mit gültigem Token erreichbar
|
||||
if (! $isInstalled && $isInstallerRoute) {
|
||||
$tokenFile = storage_path('setup-token');
|
||||
|
||||
// Token-Datei beim ersten Zugriff generieren
|
||||
if (! file_exists($tokenFile)) {
|
||||
$token = bin2hex(random_bytes(16));
|
||||
file_put_contents($tokenFile, $token);
|
||||
chmod($tokenFile, 0600);
|
||||
// Nur Token-Hash loggen (Klartext in Datei storage/setup-token)
|
||||
logger()->warning("Installer Setup-Token generiert (SHA256: " . hash('sha256', $token) . ")");
|
||||
logger()->warning("Token befindet sich in: storage/setup-token");
|
||||
}
|
||||
|
||||
$expectedToken = trim(file_get_contents($tokenFile));
|
||||
$providedToken = $request->query('token');
|
||||
// Session ist ggf. noch nicht gestartet (Middleware laeuft vor StartSession)
|
||||
$sessionTokenHash = $request->hasSession() ? $request->session()->get('setup_token_hash') : null;
|
||||
|
||||
if ($providedToken && hash_equals($expectedToken, $providedToken)) {
|
||||
// Token-Hash in Session speichern — kein Klartext in Session (V11)
|
||||
if ($request->hasSession()) {
|
||||
$request->session()->put('setup_token_hash', hash('sha256', $expectedToken));
|
||||
}
|
||||
} elseif ($sessionTokenHash && hash_equals(hash('sha256', $expectedToken), $sessionTokenHash)) {
|
||||
// Gültiges Token via Session-Hash
|
||||
} elseif (! $request->is('install')) {
|
||||
// Nur die Startseite ohne Token erlauben (zeigt Token-Eingabe)
|
||||
abort(403, 'Ungültiges Setup-Token.');
|
||||
}
|
||||
}
|
||||
|
||||
// Force file-based session/cache during installation (DB may not exist yet).
|
||||
// Fixed cookie name prevents session loss when APP_NAME changes in .env mid-install.
|
||||
if (! $isInstalled && $isInstallerRoute) {
|
||||
config([
|
||||
'session.driver' => 'file',
|
||||
'cache.default' => 'file',
|
||||
'session.cookie' => 'handball_installer_session',
|
||||
]);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
61
app/Http/Middleware/SecurityHeadersMiddleware.php
Executable file
61
app/Http/Middleware/SecurityHeadersMiddleware.php
Executable file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SecurityHeadersMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
// Server-Fingerprinting verhindern
|
||||
header_remove('X-Powered-By');
|
||||
$response->headers->remove('X-Powered-By');
|
||||
$response->headers->remove('Server');
|
||||
|
||||
// Content Security Policy — erlaubt CDN-Quellen für Tailwind, Alpine, Quill, Leaflet
|
||||
// 'unsafe-inline' benötigt von: Tailwind CDN (inline Styles), Alpine.js (Event-Handler)
|
||||
// 'unsafe-eval' benötigt von: Tailwind CDN (JIT nutzt new Function())
|
||||
// Entfernung nur möglich durch Wechsel auf self-hosted/kompilierte Assets
|
||||
$cspDirectives = [
|
||||
"default-src 'self'",
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.tailwindcss.com https://cdn.jsdelivr.net https://unpkg.com https://cdn.quilljs.com",
|
||||
"style-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com https://cdn.jsdelivr.net https://unpkg.com https://cdn.quilljs.com",
|
||||
"img-src 'self' data: https://*.tile.openstreetmap.org https://unpkg.com",
|
||||
"font-src 'self' https://cdn.jsdelivr.net https://cdn.quilljs.com",
|
||||
"connect-src 'self' https://photon.komoot.io",
|
||||
"frame-src 'self'",
|
||||
"object-src 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
];
|
||||
|
||||
// upgrade-insecure-requests nur bei HTTPS — bricht sonst lokale HTTP-Entwicklung (Herd/artisan serve)
|
||||
if ($request->secure()) {
|
||||
$cspDirectives[] = "upgrade-insecure-requests";
|
||||
}
|
||||
|
||||
$csp = implode('; ', $cspDirectives);
|
||||
|
||||
$response->headers->set('Content-Security-Policy', $csp);
|
||||
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
||||
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
|
||||
$response->headers->set('X-XSS-Protection', '1; mode=block');
|
||||
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
$response->headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=(self), payment=(), usb=(), bluetooth=(), autoplay=(), magnetometer=(), gyroscope=(), accelerometer=()');
|
||||
|
||||
// Cross-Origin Isolation Headers
|
||||
$response->headers->set('Cross-Origin-Opener-Policy', 'same-origin-allow-popups');
|
||||
|
||||
// HSTS — HTTPS fuer 1 Jahr erzwingen (nur bei HTTPS-Requests aktiv)
|
||||
if ($request->secure()) {
|
||||
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
44
app/Http/Middleware/SetLocaleMiddleware.php
Executable file
44
app/Http/Middleware/SetLocaleMiddleware.php
Executable file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SetLocaleMiddleware
|
||||
{
|
||||
private const SUPPORTED_LOCALES = ['de', 'en', 'pl', 'ru', 'ar', 'tr'];
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$locale = $this->resolveLocale($request);
|
||||
|
||||
app()->setLocale($locale);
|
||||
Carbon::setLocale($locale);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
private function resolveLocale(Request $request): string
|
||||
{
|
||||
// 1. Eingeloggter User → DB-Präferenz
|
||||
if ($request->user() && in_array($request->user()->locale, self::SUPPORTED_LOCALES)) {
|
||||
return $request->user()->locale;
|
||||
}
|
||||
|
||||
// 2. Session (für Gastseiten)
|
||||
if (session()->has('locale') && in_array(session('locale'), self::SUPPORTED_LOCALES)) {
|
||||
return session('locale');
|
||||
}
|
||||
|
||||
// 3. Fallback
|
||||
return 'de';
|
||||
}
|
||||
|
||||
public static function supportedLocales(): array
|
||||
{
|
||||
return self::SUPPORTED_LOCALES;
|
||||
}
|
||||
}
|
||||
19
app/Http/Middleware/StaffMiddleware.php
Normal file
19
app/Http/Middleware/StaffMiddleware.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class StaffMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (!$request->user() || !$request->user()->isStaff()) {
|
||||
abort(403, 'Zugriff verweigert.');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user