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>
45
public/.htaccess
Executable file
@@ -0,0 +1,45 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Handle X-XSRF-Token Header
|
||||
RewriteCond %{HTTP:x-xsrf-token} .
|
||||
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
|
||||
# Zugriff auf versteckte Dateien/Ordner blockieren (.git, .env, etc.)
|
||||
RewriteRule (^|/)\.(?!well-known) - [F,L]
|
||||
</IfModule>
|
||||
|
||||
# Server-Fingerprinting verhindern
|
||||
<IfModule mod_headers.c>
|
||||
Header unset X-Powered-By
|
||||
Header unset Server
|
||||
</IfModule>
|
||||
|
||||
# Zugriff auf sensible Dateien blockieren
|
||||
<FilesMatch "^\.env|\.git|composer\.(json|lock)$">
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
3
public/.well-known/security.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Contact: mailto:kontakt@rhino.nrw
|
||||
Expires: 2027-01-01T00:00:00.000Z
|
||||
Preferred-Languages: de, en
|
||||
416
public/check.php
Normal file
@@ -0,0 +1,416 @@
|
||||
<?php
|
||||
/**
|
||||
* Eigenstaendige Server-Diagnose (kein Laravel noetig).
|
||||
* Aufruf: https://deine-domain.de/check.php
|
||||
*
|
||||
* WICHTIG: Diese Datei nach erfolgreicher Installation loeschen!
|
||||
*/
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$basePath = __DIR__ . '/..';
|
||||
$results = [];
|
||||
|
||||
// ─── PHP ─────────────────────────────────────────────────────
|
||||
$results[] = [
|
||||
'group' => 'PHP',
|
||||
'name' => 'PHP-Version',
|
||||
'value' => PHP_VERSION,
|
||||
'ok' => version_compare(PHP_VERSION, '8.2.0', '>='),
|
||||
'hint' => 'Mindestens PHP 8.2 erforderlich.',
|
||||
];
|
||||
|
||||
$results[] = [
|
||||
'group' => 'PHP',
|
||||
'name' => 'Server-API (SAPI)',
|
||||
'value' => PHP_SAPI,
|
||||
'ok' => true,
|
||||
'hint' => '',
|
||||
];
|
||||
|
||||
$extensions = ['pdo', 'pdo_sqlite', 'mbstring', 'openssl', 'tokenizer', 'xml', 'ctype', 'fileinfo', 'dom', 'curl'];
|
||||
foreach ($extensions as $ext) {
|
||||
$results[] = [
|
||||
'group' => 'PHP-Extensions',
|
||||
'name' => $ext,
|
||||
'value' => extension_loaded($ext) ? 'Geladen' : 'Fehlt',
|
||||
'ok' => extension_loaded($ext),
|
||||
'hint' => $ext === 'pdo_sqlite' ? 'Nur fuer SQLite noetig' : '',
|
||||
];
|
||||
}
|
||||
|
||||
// Optional
|
||||
$results[] = [
|
||||
'group' => 'PHP-Extensions',
|
||||
'name' => 'pdo_mysql (optional)',
|
||||
'value' => extension_loaded('pdo_mysql') ? 'Geladen' : 'Fehlt',
|
||||
'ok' => true, // optional
|
||||
'hint' => 'Nur fuer MySQL noetig.',
|
||||
];
|
||||
|
||||
// ─── Dateisystem ─────────────────────────────────────────────
|
||||
$dirs = [
|
||||
'storage',
|
||||
'storage/app',
|
||||
'storage/framework',
|
||||
'storage/framework/cache',
|
||||
'storage/framework/cache/data',
|
||||
'storage/framework/sessions',
|
||||
'storage/framework/views',
|
||||
'storage/logs',
|
||||
'bootstrap/cache',
|
||||
'database',
|
||||
];
|
||||
|
||||
foreach ($dirs as $dir) {
|
||||
$full = $basePath . '/' . $dir;
|
||||
$exists = is_dir($full);
|
||||
$writable = $exists && is_writable($full);
|
||||
$perms = $exists ? substr(sprintf('%o', fileperms($full)), -4) : '-';
|
||||
$owner = $exists ? (function_exists('posix_getpwuid') ? posix_getpwuid(fileowner($full))['name'] ?? fileowner($full) : fileowner($full)) : '-';
|
||||
|
||||
$results[] = [
|
||||
'group' => 'Verzeichnisse',
|
||||
'name' => $dir . '/',
|
||||
'value' => ($exists ? 'Existiert' : 'FEHLT') . ' | ' . ($writable ? 'Beschreibbar' : 'NICHT beschreibbar') . ' | ' . $perms . ' | Owner: ' . $owner,
|
||||
'ok' => $writable,
|
||||
'hint' => !$writable ? 'Berechtigungen auf 775 setzen' : '',
|
||||
];
|
||||
}
|
||||
|
||||
// ─── Dateien ─────────────────────────────────────────────────
|
||||
$files = [
|
||||
'.env' => 'Konfigurationsdatei (wird automatisch erstellt)',
|
||||
'.env.example' => 'Vorlage fuer .env',
|
||||
'vendor/autoload.php' => 'Composer-Abhaengigkeiten',
|
||||
'bootstrap/app.php' => 'Laravel-Bootstrap',
|
||||
'storage/installed' => 'Installations-Marker (fehlt bei Erstinstallation)',
|
||||
];
|
||||
|
||||
foreach ($files as $file => $desc) {
|
||||
$full = $basePath . '/' . $file;
|
||||
$exists = file_exists($full);
|
||||
// storage/installed SOLL bei Erstinstallation fehlen
|
||||
$isOptional = ($file === 'storage/installed' || $file === '.env');
|
||||
|
||||
$results[] = [
|
||||
'group' => 'Dateien',
|
||||
'name' => $file,
|
||||
'value' => $exists ? 'Vorhanden' : 'Fehlt',
|
||||
'ok' => $exists || $isOptional,
|
||||
'hint' => $desc,
|
||||
];
|
||||
}
|
||||
|
||||
// ─── Webserver ───────────────────────────────────────────────
|
||||
$results[] = [
|
||||
'group' => 'Webserver',
|
||||
'name' => 'Server-Software',
|
||||
'value' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unbekannt',
|
||||
'ok' => true,
|
||||
'hint' => '',
|
||||
];
|
||||
|
||||
$results[] = [
|
||||
'group' => 'Webserver',
|
||||
'name' => 'Document Root',
|
||||
'value' => $_SERVER['DOCUMENT_ROOT'] ?? 'Unbekannt',
|
||||
'ok' => true,
|
||||
'hint' => 'Muss auf den public/-Ordner zeigen.',
|
||||
];
|
||||
|
||||
$results[] = [
|
||||
'group' => 'Webserver',
|
||||
'name' => 'Script-Pfad',
|
||||
'value' => __FILE__,
|
||||
'ok' => true,
|
||||
'hint' => '',
|
||||
];
|
||||
|
||||
$results[] = [
|
||||
'group' => 'Webserver',
|
||||
'name' => 'PHP-User',
|
||||
'value' => function_exists('posix_getpwuid') ? (posix_getpwuid(posix_geteuid())['name'] ?? posix_geteuid()) : get_current_user(),
|
||||
'ok' => true,
|
||||
'hint' => 'Unter diesem User laeuft PHP.',
|
||||
];
|
||||
|
||||
$results[] = [
|
||||
'group' => 'Webserver',
|
||||
'name' => 'mod_rewrite',
|
||||
'value' => (function_exists('apache_get_modules') && in_array('mod_rewrite', apache_get_modules())) ? 'Aktiv' : 'Nicht pruefbar / Nicht aktiv',
|
||||
'ok' => true, // Can't reliably detect
|
||||
'hint' => 'Wird fuer saubere URLs benoetigt.',
|
||||
];
|
||||
|
||||
// ─── index.php Datei-Check ───────────────────────────────────
|
||||
$indexPath = __DIR__ . '/index.php';
|
||||
$indexExists = file_exists($indexPath);
|
||||
$indexSize = $indexExists ? filesize($indexPath) : 0;
|
||||
$indexMtime = $indexExists ? date('Y-m-d H:i:s', filemtime($indexPath)) : '-';
|
||||
$indexFirstLines = $indexExists ? implode("\n", array_slice(file($indexPath), 0, 10)) : '';
|
||||
$hasTryCatch = $indexExists && strpos(file_get_contents($indexPath), 'Throwable') !== false;
|
||||
|
||||
$results[] = [
|
||||
'group' => 'index.php Pruefung',
|
||||
'name' => 'Dateigroesse',
|
||||
'value' => $indexSize . ' Bytes',
|
||||
'ok' => $indexSize > 1000,
|
||||
'hint' => 'Neue index.php sollte ueber 5000 Bytes sein.',
|
||||
];
|
||||
$results[] = [
|
||||
'group' => 'index.php Pruefung',
|
||||
'name' => 'Letzte Aenderung',
|
||||
'value' => $indexMtime,
|
||||
'ok' => true,
|
||||
'hint' => '',
|
||||
];
|
||||
$results[] = [
|
||||
'group' => 'index.php Pruefung',
|
||||
'name' => 'try-catch vorhanden',
|
||||
'value' => $hasTryCatch ? 'Ja' : 'Nein (ALTE VERSION!)',
|
||||
'ok' => $hasTryCatch,
|
||||
'hint' => 'Bitte die aktuelle index.php erneut hochladen.',
|
||||
];
|
||||
|
||||
// ─── Laravel Boot-Test ───────────────────────────────────────
|
||||
$bootSteps = [];
|
||||
$bootError = null;
|
||||
|
||||
// Schritt 1: Autoloader laden
|
||||
try {
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
$bootSteps[] = ['name' => 'vendor/autoload.php laden', 'ok' => true, 'error' => ''];
|
||||
} catch (\Throwable $e) {
|
||||
$bootSteps[] = ['name' => 'vendor/autoload.php laden', 'ok' => false, 'error' => $e->getMessage()];
|
||||
$bootError = $e;
|
||||
}
|
||||
|
||||
// Schritt 2: Bootstrap laden (nur wenn Schritt 1 ok)
|
||||
if (!$bootError) {
|
||||
try {
|
||||
$app = require_once __DIR__ . '/../bootstrap/app.php';
|
||||
$bootSteps[] = ['name' => 'bootstrap/app.php laden', 'ok' => true, 'error' => ''];
|
||||
} catch (\Throwable $e) {
|
||||
$bootSteps[] = ['name' => 'bootstrap/app.php laden', 'ok' => false, 'error' => $e->getMessage()];
|
||||
$bootError = $e;
|
||||
}
|
||||
}
|
||||
|
||||
// Schritt 3: Kernel erstellen (nur wenn Schritt 2 ok)
|
||||
if (!$bootError && isset($app)) {
|
||||
try {
|
||||
$kernel = $app->make(\Illuminate\Contracts\Http\Kernel::class);
|
||||
$bootSteps[] = ['name' => 'HTTP-Kernel erstellen', 'ok' => true, 'error' => ''];
|
||||
} catch (\Throwable $e) {
|
||||
$bootSteps[] = ['name' => 'HTTP-Kernel erstellen', 'ok' => false, 'error' => $e->getMessage()];
|
||||
$bootError = $e;
|
||||
}
|
||||
}
|
||||
|
||||
// Schritt 4: Request-Handling simulieren (nur wenn Schritt 3 ok)
|
||||
if (!$bootError && isset($app)) {
|
||||
try {
|
||||
// Simuliere einen GET / Request
|
||||
$testRequest = \Illuminate\Http\Request::create('/', 'GET');
|
||||
$response = $app->make(\Illuminate\Contracts\Http\Kernel::class)->handle($testRequest);
|
||||
$statusCode = $response->getStatusCode();
|
||||
$isRedirect = $statusCode >= 300 && $statusCode < 400;
|
||||
$redirectTo = $isRedirect ? $response->headers->get('Location', '') : '';
|
||||
|
||||
$bootSteps[] = [
|
||||
'name' => 'Request-Handling (GET /)',
|
||||
'ok' => true,
|
||||
'error' => 'Status ' . $statusCode . ($isRedirect ? ' → Redirect nach ' . $redirectTo : ''),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
$bootSteps[] = ['name' => 'Request-Handling (GET /)', 'ok' => false, 'error' => $e->getMessage()];
|
||||
$bootError = $e;
|
||||
}
|
||||
}
|
||||
|
||||
// Schritt 5: Auch /install testen (dort laeuft die eigentliche Seite)
|
||||
if (!$bootError && isset($app)) {
|
||||
try {
|
||||
// Log leeren, damit wir nur den Fehler von diesem Request sehen
|
||||
$logFile = __DIR__ . '/../storage/logs/laravel.log';
|
||||
@file_put_contents($logFile, '');
|
||||
|
||||
// Debug aktivieren, damit die Exception geloggt wird
|
||||
config(['app.debug' => true]);
|
||||
|
||||
$installRequest = \Illuminate\Http\Request::create('/install', 'GET');
|
||||
$installResponse = $app->make(\Illuminate\Contracts\Http\Kernel::class)->handle($installRequest);
|
||||
$installStatus = $installResponse->getStatusCode();
|
||||
|
||||
if ($installStatus >= 500) {
|
||||
// Fehler aus dem Log auslesen
|
||||
$logContent = file_exists($logFile) ? file_get_contents($logFile) : '';
|
||||
// Nur die erste Fehlermeldung extrahieren (bis zur ersten Leerzeile)
|
||||
$logLines = explode("\n", $logContent);
|
||||
$errorMsg = '';
|
||||
foreach ($logLines as $line) {
|
||||
if (strlen($errorMsg) > 1500) break;
|
||||
$errorMsg .= $line . "\n";
|
||||
}
|
||||
$errorMsg = trim($errorMsg) ?: '(kein Log-Eintrag gefunden)';
|
||||
|
||||
$bootSteps[] = [
|
||||
'name' => 'Request-Handling (GET /install)',
|
||||
'ok' => false,
|
||||
'error' => 'Status ' . $installStatus . ' — Fehler aus laravel.log: ' . $errorMsg,
|
||||
];
|
||||
} else {
|
||||
$bootSteps[] = [
|
||||
'name' => 'Request-Handling (GET /install)',
|
||||
'ok' => true,
|
||||
'error' => 'Status ' . $installStatus,
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$bootSteps[] = ['name' => 'Request-Handling (GET /install)', 'ok' => false, 'error' => $e->getMessage()];
|
||||
$bootError = $e;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── OPcache ─────────────────────────────────────────────────
|
||||
$opcacheEnabled = function_exists('opcache_get_status') && opcache_get_status() !== false;
|
||||
$results[] = [
|
||||
'group' => 'OPcache',
|
||||
'name' => 'OPcache aktiv',
|
||||
'value' => $opcacheEnabled ? 'Ja' : 'Nein',
|
||||
'ok' => true,
|
||||
'hint' => '',
|
||||
];
|
||||
|
||||
if ($opcacheEnabled) {
|
||||
$status = opcache_get_status();
|
||||
$results[] = [
|
||||
'group' => 'OPcache',
|
||||
'name' => 'validate_timestamps',
|
||||
'value' => ini_get('opcache.validate_timestamps') ? 'Ja' : 'Nein (gefaehrlich!)',
|
||||
'ok' => (bool) ini_get('opcache.validate_timestamps'),
|
||||
'hint' => 'Ohne validate_timestamps werden Dateiupdates nicht erkannt.',
|
||||
];
|
||||
|
||||
// OPcache fuer index.php invalidieren
|
||||
$indexInvalidated = opcache_invalidate(__DIR__ . '/index.php', true);
|
||||
$results[] = [
|
||||
'group' => 'OPcache',
|
||||
'name' => 'index.php Cache invalidiert',
|
||||
'value' => $indexInvalidated ? 'Ja (erfolgreich)' : 'Nein / nicht noetig',
|
||||
'ok' => true,
|
||||
'hint' => '',
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($bootSteps as $step) {
|
||||
$results[] = [
|
||||
'group' => 'Laravel Boot-Test',
|
||||
'name' => $step['name'],
|
||||
'value' => $step['ok'] ? 'OK' : 'FEHLER: ' . $step['error'],
|
||||
'ok' => $step['ok'],
|
||||
'hint' => $step['error'],
|
||||
];
|
||||
}
|
||||
|
||||
if ($bootError) {
|
||||
$results[] = [
|
||||
'group' => 'Laravel Boot-Test',
|
||||
'name' => 'Stack-Trace',
|
||||
'value' => $bootError->getFile() . ':' . $bootError->getLine(),
|
||||
'ok' => false,
|
||||
'hint' => $bootError->getTraceAsString(),
|
||||
];
|
||||
}
|
||||
|
||||
// ─── Zusammenfassung ─────────────────────────────────────────
|
||||
$allOk = true;
|
||||
foreach ($results as $r) {
|
||||
if (!$r['ok']) {
|
||||
$allOk = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── HTML-Ausgabe ────────────────────────────────────────────
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Server-Diagnose</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="min-h-screen bg-gray-100 py-8 px-4">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="text-center mb-6">
|
||||
<h1 class="text-xl font-bold text-gray-900">Server-Diagnose</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Handball WebApp — Systemcheck
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php if ($allOk): ?>
|
||||
<div class="bg-green-50 border border-green-200 rounded-md p-4 mb-6 text-sm text-green-800">
|
||||
Alle Voraussetzungen sind erfuellt. Die Installation sollte funktionieren.
|
||||
<br>Rufe die Hauptseite auf, um den Installer zu starten.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="bg-red-50 border border-red-200 rounded-md p-4 mb-6 text-sm text-red-800">
|
||||
Es gibt Probleme, die behoben werden muessen (rot markiert).
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$currentGroup = '';
|
||||
foreach ($results as $r):
|
||||
if ($r['group'] !== $currentGroup):
|
||||
if ($currentGroup !== '') echo '</tbody></table></div>';
|
||||
$currentGroup = $r['group'];
|
||||
?>
|
||||
<div class="bg-white rounded-lg shadow-md mb-4 overflow-hidden">
|
||||
<h2 class="bg-gray-50 px-4 py-2 text-sm font-semibold text-gray-700 border-b"><?php echo htmlspecialchars($currentGroup); ?></h2>
|
||||
<table class="w-full text-sm">
|
||||
<tbody>
|
||||
<?php endif; ?>
|
||||
<tr class="border-b border-gray-100 <?php echo $r['ok'] ? '' : 'bg-red-50'; ?>">
|
||||
<td class="px-4 py-2 font-mono text-gray-800 whitespace-nowrap"><?php echo htmlspecialchars($r['name']); ?></td>
|
||||
<td class="px-4 py-2 text-gray-600"><?php echo htmlspecialchars($r['value']); ?></td>
|
||||
<td class="px-2 py-2 text-center w-8">
|
||||
<?php if ($r['ok']): ?>
|
||||
<span class="text-green-500">✓</span>
|
||||
<?php else: ?>
|
||||
<span class="text-red-500 font-bold">✗</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php if (!$r['ok'] && $r['hint']): ?>
|
||||
<tr class="bg-red-50 border-b border-red-100">
|
||||
<td colspan="3" class="px-4 py-1 text-xs text-red-600">
|
||||
<pre class="whitespace-pre-wrap break-all">→ <?php echo htmlspecialchars($r['hint']); ?></pre>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</tbody></table>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center gap-4 mt-6">
|
||||
<a href="check.php" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700">
|
||||
Erneut pruefen
|
||||
</a>
|
||||
<?php if ($allOk): ?>
|
||||
<a href="/" class="px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-md hover:bg-green-700">
|
||||
Installation starten
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-xs text-gray-400 mt-6">
|
||||
Diese Datei nach erfolgreicher Installation loeschen (check.php).
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
0
public/favicon.ico
Executable file
BIN
public/images/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/images/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/images/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
public/images/icon-maskable-192x192.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/images/icon-maskable-512x512.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
public/images/logo_sg_woelfe.png
Executable file
|
After Width: | Height: | Size: 629 KiB |
BIN
public/images/logo_woelfe_.png
Executable file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/images/profil_empty.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
304
public/index.php
Executable file
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Vor Installation: Fehler anzeigen statt weisser Seite.
|
||||
// Nach Installation uebernimmt Laravels Error-Handler.
|
||||
if (!file_exists(__DIR__ . '/../storage/installed')) {
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
}
|
||||
|
||||
// ─── Pre-Flight Check (vor Laravel-Bootstrap) ────────────────
|
||||
// Stellt sicher, dass alle Voraussetzungen erfuellt sind, bevor
|
||||
// Laravel bootet. Verhindert den 500er-Fehler bei Erstinstallation.
|
||||
// Nach erfolgreicher Installation (storage/installed existiert)
|
||||
// wird dieser Block komplett uebersprungen.
|
||||
$basePath = __DIR__ . '/..';
|
||||
|
||||
if (!file_exists($basePath . '/storage/installed')) {
|
||||
|
||||
$errors = [];
|
||||
$permissionErrors = [];
|
||||
$phpErrors = [];
|
||||
$extensionErrors = [];
|
||||
$fileErrors = [];
|
||||
|
||||
// 1. PHP-Version pruefen (>= 8.2)
|
||||
if (version_compare(PHP_VERSION, '8.2.0', '<')) {
|
||||
$phpErrors[] = 'PHP 8.2 oder hoeher wird benoetigt. Aktuell installiert: PHP ' . PHP_VERSION;
|
||||
}
|
||||
|
||||
// 2. Pflicht-Extensions pruefen
|
||||
$requiredExtensions = [
|
||||
'pdo' => 'Datenbankanbindung',
|
||||
'mbstring' => 'Zeichenkodierung (Unicode)',
|
||||
'openssl' => 'Verschluesselung',
|
||||
'tokenizer' => 'PHP-Code-Analyse',
|
||||
'xml' => 'XML-Verarbeitung',
|
||||
'ctype' => 'Zeichentyp-Pruefung',
|
||||
'fileinfo' => 'Dateityp-Erkennung',
|
||||
'dom' => 'HTML/XML-Verarbeitung',
|
||||
];
|
||||
foreach ($requiredExtensions as $ext => $desc) {
|
||||
if (!extension_loaded($ext)) {
|
||||
$extensionErrors[] = $ext . ' (' . $desc . ')';
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Verzeichnisse anlegen und Berechtigungen pruefen/reparieren
|
||||
$writableDirs = [
|
||||
'storage',
|
||||
'storage/app',
|
||||
'storage/framework',
|
||||
'storage/framework/cache',
|
||||
'storage/framework/cache/data',
|
||||
'storage/framework/sessions',
|
||||
'storage/framework/views',
|
||||
'storage/logs',
|
||||
'bootstrap/cache',
|
||||
];
|
||||
|
||||
foreach ($writableDirs as $relDir) {
|
||||
$fullPath = $basePath . '/' . $relDir;
|
||||
// Verzeichnis erstellen, falls es nicht existiert
|
||||
if (!is_dir($fullPath)) {
|
||||
@mkdir($fullPath, 0775, true);
|
||||
}
|
||||
// Berechtigungen reparieren, falls nicht beschreibbar
|
||||
if (is_dir($fullPath) && !is_writable($fullPath)) {
|
||||
@chmod($fullPath, 0775);
|
||||
}
|
||||
// Immer noch nicht beschreibbar? → Fehler melden
|
||||
if (!is_dir($fullPath) || !is_writable($fullPath)) {
|
||||
$permissionErrors[] = $relDir;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. .env-Datei erstellen (aus .env.example kopieren)
|
||||
$envPath = $basePath . '/.env';
|
||||
if (!file_exists($envPath) && file_exists($basePath . '/.env.example')) {
|
||||
@copy($basePath . '/.env.example', $envPath);
|
||||
}
|
||||
if (!file_exists($envPath)) {
|
||||
$fileErrors[] = 'Die Konfigurationsdatei .env konnte nicht erstellt werden.';
|
||||
}
|
||||
|
||||
// 5. APP_KEY generieren, falls leer — MUSS vor Laravel-Bootstrap passieren,
|
||||
// weil der EncryptionServiceProvider (eager Index 3) den Key beim Booten braucht,
|
||||
// aber AppServiceProvider (eager Index 14) ihn sonst zu spaet setzen wuerde.
|
||||
if (file_exists($envPath)) {
|
||||
$envContent = file_get_contents($envPath);
|
||||
if (preg_match('/^APP_KEY=\s*$/m', $envContent)) {
|
||||
$key = 'base64:' . base64_encode(random_bytes(32));
|
||||
$envContent = preg_replace('/^APP_KEY=\s*$/m', 'APP_KEY=' . $key, $envContent);
|
||||
file_put_contents($envPath, $envContent);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Veralteten Config-/Routen-Cache loeschen (enthalten absolute Pfade
|
||||
// vom Entwicklungsrechner). services.php und packages.php bleiben —
|
||||
// diese enthalten nur Klassennamen und sind umgebungsunabhaengig.
|
||||
foreach (['config.php', 'routes-v7.php'] as $cacheFile) {
|
||||
$cachePath = $basePath . '/bootstrap/cache/' . $cacheFile;
|
||||
if (file_exists($cachePath)) {
|
||||
@unlink($cachePath);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. vendor/-Ordner pruefen
|
||||
if (!file_exists($basePath . '/vendor/autoload.php')) {
|
||||
$fileErrors[] = 'Der Ordner "vendor/" fehlt oder ist unvollstaendig. Bitte alle Dateien erneut hochladen.';
|
||||
}
|
||||
|
||||
// Alle Fehler sammeln
|
||||
if (!empty($phpErrors)) $errors = array_merge($errors, $phpErrors);
|
||||
if (!empty($extensionErrors)) $errors[] = '__extensions__';
|
||||
if (!empty($permissionErrors)) $errors[] = '__permissions__';
|
||||
if (!empty($fileErrors)) $errors = array_merge($errors, $fileErrors);
|
||||
|
||||
// Bei Fehlern: Eigenstaendige HTML-Seite anzeigen (kein Laravel noetig)
|
||||
if (!empty($errors)) {
|
||||
http_response_code(503);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Installation — Systemcheck</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
</head>
|
||||
<body class="min-h-screen bg-gray-100 flex flex-col">
|
||||
<main class="flex-1 flex items-center justify-center px-4 py-12">
|
||||
<div class="w-full max-w-2xl">
|
||||
<div class="text-center mb-6">
|
||||
<img src="/images/logo_sg_woelfe.png" alt="Logo" class="mx-auto h-20 mb-3" onerror="this.style.display='none'">
|
||||
<h1 class="text-xl font-bold text-gray-900">Installation</h1>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<div class="flex items-start gap-3 mb-4">
|
||||
<svg class="w-6 h-6 text-amber-500 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Systemvoraussetzungen nicht erfuellt</h2>
|
||||
<p class="text-sm text-gray-600 mt-1">
|
||||
Bevor die Installation starten kann, muessen folgende Probleme behoben werden:
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<?php if (!empty($phpErrors)): ?>
|
||||
<div class="bg-red-50 border border-red-200 rounded-md p-4">
|
||||
<h3 class="font-medium text-red-800 text-sm mb-1">PHP-Version</h3>
|
||||
<?php foreach ($phpErrors as $err): ?>
|
||||
<p class="text-sm text-red-700"><?php echo htmlspecialchars($err); ?></p>
|
||||
<?php endforeach; ?>
|
||||
<p class="text-xs text-red-600 mt-2">
|
||||
<strong>Loesung:</strong> Wechsle in deinem Hosting-Panel (z.B. cPanel, Plesk)
|
||||
auf PHP 8.2 oder hoeher. Die Einstellung findest du meistens unter
|
||||
„PHP-Version“ oder „PHP Selector“.
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($extensionErrors)): ?>
|
||||
<div class="bg-red-50 border border-red-200 rounded-md p-4">
|
||||
<h3 class="font-medium text-red-800 text-sm mb-1">Fehlende PHP-Extensions</h3>
|
||||
<p class="text-sm text-red-700">
|
||||
Folgende PHP-Erweiterungen werden benoetigt, sind aber nicht aktiviert:
|
||||
</p>
|
||||
<ul class="mt-1 ml-4 list-disc text-sm text-red-700">
|
||||
<?php foreach ($extensionErrors as $ext): ?>
|
||||
<li><?php echo htmlspecialchars($ext); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<p class="text-xs text-red-600 mt-2">
|
||||
<strong>Loesung:</strong> Aktiviere die fehlenden Extensions in deinem Hosting-Panel
|
||||
unter „PHP-Extensions“ oder „PHP Modules“.
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($permissionErrors)): ?>
|
||||
<div class="bg-red-50 border border-red-200 rounded-md p-4">
|
||||
<h3 class="font-medium text-red-800 text-sm mb-1">Fehlende Schreibberechtigungen</h3>
|
||||
<p class="text-sm text-red-700">
|
||||
Folgende Verzeichnisse muessen beschreibbar sein (Berechtigungen: 775):
|
||||
</p>
|
||||
<ul class="mt-1 ml-4 list-disc text-sm text-red-700 font-mono">
|
||||
<?php foreach ($permissionErrors as $dir): ?>
|
||||
<li><?php echo htmlspecialchars($dir); ?>/</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<div class="mt-3 text-xs text-red-600 space-y-2">
|
||||
<p><strong>Loesung per FTP-Programm</strong> (z.B. FileZilla, WinSCP):</p>
|
||||
<ol class="ml-4 list-decimal space-y-0.5">
|
||||
<li>Verbinde dich mit dem Server per FTP</li>
|
||||
<li>Navigiere zum Installationsordner</li>
|
||||
<li>Rechtsklick auf den Ordner „storage“ → „Dateiberechtigungen“</li>
|
||||
<li>Setze die Berechtigungen auf <strong>775</strong></li>
|
||||
<li>Aktiviere „In Unterverzeichnisse einsteigen“</li>
|
||||
<li>Wiederhole dies fuer den Ordner „bootstrap/cache“</li>
|
||||
</ol>
|
||||
<p class="mt-2"><strong>Loesung per Hosting-Panel</strong> (cPanel / Plesk):</p>
|
||||
<ol class="ml-4 list-decimal space-y-0.5">
|
||||
<li>Oeffne den „Dateimanager“ in deinem Hosting-Panel</li>
|
||||
<li>Navigiere zum Installationsordner</li>
|
||||
<li>Klicke auf „storage“ → „Berechtigungen aendern“</li>
|
||||
<li>Setze die Berechtigungen auf <strong>775</strong> (rekursiv)</li>
|
||||
<li>Wiederhole dies fuer „bootstrap/cache“</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($fileErrors)): ?>
|
||||
<div class="bg-red-50 border border-red-200 rounded-md p-4">
|
||||
<h3 class="font-medium text-red-800 text-sm mb-1">Fehlende Dateien</h3>
|
||||
<?php foreach ($fileErrors as $err): ?>
|
||||
<p class="text-sm text-red-700"><?php echo htmlspecialchars($err); ?></p>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-center">
|
||||
<a href="/"
|
||||
class="px-5 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 transition inline-flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
Erneut pruefen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="text-center py-3 text-xs text-gray-400">
|
||||
Handball WebApp — Systemcheck
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Laravel Bootstrap ───────────────────────────────────────
|
||||
|
||||
// Alles in try-catch wrappen, damit auch Fehler beim Autoloading
|
||||
// sichtbar werden (PHP-FPM unterdrueckt display_errors).
|
||||
try {
|
||||
// Determine if the application is in maintenance mode...
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the request...
|
||||
/** @var \Illuminate\Foundation\Application $app */
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->handleRequest(\Illuminate\Http\Request::capture());
|
||||
} catch (\Throwable $e) {
|
||||
// 503 statt 500: Einige Hoster (ProxyErrorOverride) ersetzen bei 500
|
||||
// die Antwort mit einer eigenen Fehlerseite, die unsere Ausgabe versteckt.
|
||||
http_response_code(503);
|
||||
// Nur vor Installation Fehlerdetails anzeigen
|
||||
if (!file_exists(__DIR__ . '/../storage/installed')) {
|
||||
echo '<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8">';
|
||||
echo '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
|
||||
echo '<title>Startfehler</title>';
|
||||
echo '<script src="https://cdn.tailwindcss.com"></script></head>';
|
||||
echo '<body class="min-h-screen bg-gray-100 flex items-center justify-center p-4">';
|
||||
echo '<div class="max-w-2xl w-full bg-white rounded-lg shadow-md p-6">';
|
||||
echo '<h1 class="text-lg font-bold text-red-700 mb-3">Fehler beim Starten der Anwendung</h1>';
|
||||
echo '<div class="bg-red-50 border border-red-200 rounded p-4 mb-4">';
|
||||
// Urspruenglichen Fehler anzeigen (nicht den kaskadierenden Folgefehler)
|
||||
$rootCause = $e;
|
||||
while ($rootCause->getPrevious() !== null) {
|
||||
$rootCause = $rootCause->getPrevious();
|
||||
}
|
||||
echo '<p class="text-sm font-medium text-red-800">' . htmlspecialchars($rootCause->getMessage()) . '</p>';
|
||||
echo '<p class="text-xs text-red-600 mt-1">Datei: ' . htmlspecialchars($rootCause->getFile()) . ':' . $rootCause->getLine() . '</p>';
|
||||
echo '</div>';
|
||||
if ($rootCause !== $e) {
|
||||
echo '<div class="bg-yellow-50 border border-yellow-200 rounded p-3 mb-3">';
|
||||
echo '<p class="text-xs text-yellow-800">Folgefehler: ' . htmlspecialchars($e->getMessage()) . '</p>';
|
||||
echo '</div>';
|
||||
}
|
||||
echo '<details class="mb-4"><summary class="text-sm text-gray-600 cursor-pointer">Stack-Trace anzeigen</summary>';
|
||||
echo '<pre class="mt-2 text-xs bg-gray-50 p-3 rounded overflow-x-auto max-h-64 overflow-y-auto">';
|
||||
echo htmlspecialchars($rootCause->getTraceAsString());
|
||||
echo '</pre></details>';
|
||||
echo '<a href="/" class="inline-block px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded hover:bg-blue-700">Erneut versuchen</a>';
|
||||
echo '</div></body></html>';
|
||||
}
|
||||
}
|
||||
38
public/manifest.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "SG Wölfe Handball",
|
||||
"short_name": "SG Wölfe",
|
||||
"description": "Spieltermine und Teamorganisation",
|
||||
"start_url": "/dashboard",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait-primary",
|
||||
"theme_color": "#1f2937",
|
||||
"background_color": "#f3f4f6",
|
||||
"lang": "de-DE",
|
||||
"scope": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-maskable-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-maskable-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
2
public/robots.txt
Executable file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
95
public/sw.js
Normal file
@@ -0,0 +1,95 @@
|
||||
// ============================================================
|
||||
// Service Worker – SG Wölfe Handball WebApp
|
||||
// Strategie: Lokale Assets cachen, HTML Network-First
|
||||
// ============================================================
|
||||
|
||||
const CACHE_NAME = 'handball-v3';
|
||||
const OFFLINE_URL = '/offline';
|
||||
|
||||
// Lokale Assets, die beim Install gecached werden
|
||||
// (CDN-Assets wie Tailwind/Alpine werden nicht gecached –
|
||||
// sie kommen extern und sind zu groß/dynamisch)
|
||||
const PRECACHE_ASSETS = [
|
||||
'/offline',
|
||||
'/images/icon-192x192.png',
|
||||
'/images/icon-512x512.png',
|
||||
'/manifest.json',
|
||||
'/images/logo_woelfe.png'
|
||||
];
|
||||
|
||||
// ---- INSTALL ----
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
// Einzeln cachen, damit ein 404 nicht den gesamten Install blockiert
|
||||
return Promise.all(
|
||||
PRECACHE_ASSETS.map((url) =>
|
||||
cache.add(url).catch((err) => {
|
||||
console.warn('SW: Precache fehlgeschlagen für', url, err);
|
||||
})
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
// ---- ACTIVATE ----
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter((name) => name !== CACHE_NAME)
|
||||
.map((name) => caches.delete(name))
|
||||
);
|
||||
})
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
// ---- FETCH ----
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
|
||||
// Nur GET-Requests behandeln (keine Formulare/POST)
|
||||
if (request.method !== 'GET') return;
|
||||
|
||||
// Keine externen Requests behandeln (CDN, Leaflet-Tiles, Nominatim)
|
||||
if (!request.url.startsWith(self.location.origin)) return;
|
||||
|
||||
// Statische Assets: Cache-First
|
||||
if (isStaticAsset(request.url)) {
|
||||
event.respondWith(
|
||||
caches.match(request).then((cached) => {
|
||||
return cached || fetch(request).then((response) => {
|
||||
if (response.ok) {
|
||||
const clone = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
|
||||
}
|
||||
return response;
|
||||
});
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// HTML-Seiten: Network-First mit Offline-Fallback
|
||||
if (request.headers.get('Accept')?.includes('text/html')) {
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then((response) => {
|
||||
return response;
|
||||
})
|
||||
.catch(() => {
|
||||
return caches.match(OFFLINE_URL);
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// ---- HILFSFUNKTIONEN ----
|
||||
function isStaticAsset(url) {
|
||||
return /\.(css|js|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot|webp)(\?.*)?$/.test(url);
|
||||
}
|
||||