added nice "error 500" pages, fixed core system migration
This commit is contained in:
@@ -3,8 +3,15 @@
|
|||||||
namespace App\Exceptions;
|
namespace App\Exceptions;
|
||||||
|
|
||||||
use App\Support\ApiError;
|
use App\Support\ApiError;
|
||||||
|
use Illuminate\Database\Connectors\ConnectionException as DatabaseConnectionException;
|
||||||
|
use Illuminate\Database\QueryException;
|
||||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||||
|
use Illuminate\Http\Client\ConnectionException as HttpClientConnectionException;
|
||||||
|
use Illuminate\Queue\InvalidQueueException;
|
||||||
|
use Illuminate\Queue\MaxAttemptsExceededException;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use League\Flysystem\FilesystemException;
|
||||||
|
use PDOException;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class Handler extends ExceptionHandler
|
class Handler extends ExceptionHandler
|
||||||
@@ -63,6 +70,12 @@ class Handler extends ExceptionHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! $request->expectsJson() && ! $request->inertia()) {
|
||||||
|
if ($hintKey = $this->resolveServerErrorHint($e)) {
|
||||||
|
$request->attributes->set('serverErrorHint', __($hintKey, [], app()->getLocale()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return parent::render($request, $e);
|
return parent::render($request, $e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,4 +87,81 @@ class Handler extends ExceptionHandler
|
|||||||
|
|
||||||
return 'Your request could not be processed. Please verify the details and try again.';
|
return 'Your request could not be processed. Please verify the details and try again.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolveServerErrorHint(Throwable $throwable): ?string
|
||||||
|
{
|
||||||
|
$status = $this->isHttpException($throwable)
|
||||||
|
? $this->toHttpException($throwable)->getStatusCode()
|
||||||
|
: 500;
|
||||||
|
|
||||||
|
if ($status < 500) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->exceptionChainContains($throwable, [
|
||||||
|
QueryException::class,
|
||||||
|
DatabaseConnectionException::class,
|
||||||
|
PDOException::class,
|
||||||
|
'Doctrine\\DBAL\\Exception',
|
||||||
|
])) {
|
||||||
|
return 'marketing.server_error.hints.database';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->exceptionChainContains($throwable, [
|
||||||
|
FilesystemException::class,
|
||||||
|
'League\\Flysystem\\UnableToWriteFile',
|
||||||
|
'League\\Flysystem\\UnableToCreateDirectory',
|
||||||
|
'League\\Flysystem\\UnableToCheckExistence',
|
||||||
|
])) {
|
||||||
|
return 'marketing.server_error.hints.storage';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->exceptionChainContains($throwable, [
|
||||||
|
InvalidQueueException::class,
|
||||||
|
MaxAttemptsExceededException::class,
|
||||||
|
'RedisException',
|
||||||
|
'Predis\\Connection\\ConnectionException',
|
||||||
|
])) {
|
||||||
|
return 'marketing.server_error.hints.queue';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->exceptionChainContains($throwable, [
|
||||||
|
HttpClientConnectionException::class,
|
||||||
|
'GuzzleHttp\\Exception\\ConnectException',
|
||||||
|
'Psr\\Http\\Client\\NetworkExceptionInterface',
|
||||||
|
'Symfony\\Component\\HttpClient\\Exception\\TransportExceptionInterface',
|
||||||
|
])) {
|
||||||
|
return 'marketing.server_error.hints.network';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'marketing.server_error.hints.generic';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function exceptionChainContains(Throwable $throwable, array $classNames): bool
|
||||||
|
{
|
||||||
|
do {
|
||||||
|
foreach ($classNames as $className) {
|
||||||
|
if ($this->throwableIsInstanceOf($throwable, $className)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$throwable = $throwable->getPrevious();
|
||||||
|
} while ($throwable);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function throwableIsInstanceOf(Throwable $throwable, string $className): bool
|
||||||
|
{
|
||||||
|
if ($className === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! class_exists($className) && ! interface_exists($className)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $throwable instanceof $className;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ return new class extends Migration
|
|||||||
$table->string('email')->unique();
|
$table->string('email')->unique();
|
||||||
$table->timestamp('email_verified_at')->nullable();
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
$table->string('password');
|
$table->string('password');
|
||||||
$table->string('role')->default('super_admin')->after('password');
|
$table->string('role')->default('super_admin');
|
||||||
$table->text('two_factor_secret')->nullable();
|
$table->text('two_factor_secret')->nullable();
|
||||||
$table->text('two_factor_recovery_codes')->nullable();
|
$table->text('two_factor_recovery_codes')->nullable();
|
||||||
$table->timestamp('two_factor_confirmed_at')->nullable();
|
$table->timestamp('two_factor_confirmed_at')->nullable();
|
||||||
$table->string('username', 32)->nullable()->unique()->after('email');
|
$table->string('username', 32)->nullable()->unique();
|
||||||
$table->string('preferred_locale', 5)->default(config('app.locale', 'en'))->after('role');
|
$table->string('preferred_locale', 5)->default(config('app.locale', 'en'));
|
||||||
$table->rememberToken();
|
$table->rememberToken();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
$table->string('status')->default('active');
|
$table->string('status')->default('active');
|
||||||
@@ -119,4 +119,4 @@ return new class extends Migration
|
|||||||
Schema::dropIfExists('password_reset_tokens');
|
Schema::dropIfExists('password_reset_tokens');
|
||||||
Schema::dropIfExists('users');
|
Schema::dropIfExists('users');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -180,6 +180,30 @@ return [
|
|||||||
'cta_contact' => 'Kontakt aufnehmen',
|
'cta_contact' => 'Kontakt aufnehmen',
|
||||||
'requested_path_label' => 'Angefragter Pfad',
|
'requested_path_label' => 'Angefragter Pfad',
|
||||||
],
|
],
|
||||||
|
'server_error' => [
|
||||||
|
'title' => 'Unerwarteter Fehler',
|
||||||
|
'subtitle' => 'Unser Team wurde bereits informiert.',
|
||||||
|
'description' => 'Während wir den Fehler beheben, kannst du die folgenden Schritte ausprobieren oder zu einem sicheren Bereich der App zurückkehren.',
|
||||||
|
'tip_heading' => 'Das kannst du tun',
|
||||||
|
'tips' => [
|
||||||
|
'Lade die Seite nach einer kurzen Pause neu.',
|
||||||
|
'Wechsle zur Startseite oder den Paketen und stöbere weiter.',
|
||||||
|
'Melde dich bei uns, wenn der Fehler bestehen bleibt oder du dringend Hilfe brauchst.',
|
||||||
|
],
|
||||||
|
'tip_fallback' => 'Bitte versuche es gleich noch einmal.',
|
||||||
|
'status_label' => 'Statuscode',
|
||||||
|
'hint_label' => 'Was ist passiert?',
|
||||||
|
'cta_home' => 'Zur Startseite',
|
||||||
|
'cta_packages' => 'Pakete entdecken',
|
||||||
|
'cta_contact' => 'Support kontaktieren',
|
||||||
|
'hints' => [
|
||||||
|
'database' => 'Wo ist die Datenbank? Wir erreichen sie gerade nicht – vermutlich ein Verbindungs- oder Zugriffsproblem.',
|
||||||
|
'storage' => 'Der Upload-Speicher ist vorübergehend nicht erreichbar oder schreibgeschützt. Versuche es gleich noch einmal.',
|
||||||
|
'queue' => 'Unsere Hintergrundprozesse (Queue/Redis) sind kurz offline. Wir starten sie neu.',
|
||||||
|
'network' => 'Wir können einen externen Dienst nicht erreichen. Möglich ist eine Firewall oder ein Ausfall beim Provider.',
|
||||||
|
'generic' => 'Es ist ein unerwarteter Fehler aufgetreten. Wir kümmern uns bereits darum.',
|
||||||
|
],
|
||||||
|
],
|
||||||
'success' => [
|
'success' => [
|
||||||
'title' => 'Erfolgreich',
|
'title' => 'Erfolgreich',
|
||||||
'verify_email' => 'E-Mail verifizieren',
|
'verify_email' => 'E-Mail verifizieren',
|
||||||
|
|||||||
@@ -180,6 +180,30 @@ return [
|
|||||||
'cta_contact' => 'Get in touch',
|
'cta_contact' => 'Get in touch',
|
||||||
'requested_path_label' => 'Requested path',
|
'requested_path_label' => 'Requested path',
|
||||||
],
|
],
|
||||||
|
'server_error' => [
|
||||||
|
'title' => 'We ran into an issue',
|
||||||
|
'subtitle' => 'Our team has already been notified.',
|
||||||
|
'description' => 'While we work on the fix, you can follow the tips below or head back to a safe spot in the app.',
|
||||||
|
'tip_heading' => 'In the meantime',
|
||||||
|
'tips' => [
|
||||||
|
'Reload the page after a short break.',
|
||||||
|
'Visit the homepage or packages overview to keep browsing.',
|
||||||
|
'Contact us if the issue persists or you need urgent help.',
|
||||||
|
],
|
||||||
|
'tip_fallback' => 'Please try again in a moment.',
|
||||||
|
'status_label' => 'Status code',
|
||||||
|
'hint_label' => 'What happened?',
|
||||||
|
'cta_home' => 'Back to homepage',
|
||||||
|
'cta_packages' => 'Explore packages',
|
||||||
|
'cta_contact' => 'Contact support',
|
||||||
|
'hints' => [
|
||||||
|
'database' => 'Where’s the database? We can’t reach it right now – it’s usually a credentials or network issue.',
|
||||||
|
'storage' => 'Uploads storage is temporarily unavailable or read-only. Nothing was lost; please retry shortly.',
|
||||||
|
'queue' => 'Our background workers (queue/Redis) are offline for a moment. We are restarting them.',
|
||||||
|
'network' => 'We can’t connect to an upstream service. It might be a firewall or provider outage.',
|
||||||
|
'generic' => 'We hit an unexpected error and are already investigating.',
|
||||||
|
],
|
||||||
|
],
|
||||||
'success' => [
|
'success' => [
|
||||||
'title' => 'Success',
|
'title' => 'Success',
|
||||||
'verify_email' => 'Verify Email',
|
'verify_email' => 'Verify Email',
|
||||||
|
|||||||
2
resources/views/errors/500.blade.php
Normal file
2
resources/views/errors/500.blade.php
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@php($statusCode = 500)
|
||||||
|
@include('errors.server', ['statusCode' => $statusCode])
|
||||||
2
resources/views/errors/503.blade.php
Normal file
2
resources/views/errors/503.blade.php
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@php($statusCode = 503)
|
||||||
|
@include('errors.server', ['statusCode' => $statusCode])
|
||||||
109
resources/views/errors/server.blade.php
Normal file
109
resources/views/errors/server.blade.php
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{{ __('marketing.server_error.title', [], app()->getLocale()) }} · Fotospiel</title>
|
||||||
|
@vite(['resources/css/app.css'])
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen bg-gradient-to-br from-slate-900 via-gray-900 to-black text-white antialiased">
|
||||||
|
@php
|
||||||
|
$statusCode = $statusCode ?? null;
|
||||||
|
if (! $statusCode && isset($exception) && method_exists($exception, 'getStatusCode')) {
|
||||||
|
$statusCode = $exception->getStatusCode();
|
||||||
|
}
|
||||||
|
$statusCode = is_numeric($statusCode) ? (int) $statusCode : 500;
|
||||||
|
if ($statusCode < 500 || $statusCode >= 600) {
|
||||||
|
$statusCode = 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tips = trans('marketing.server_error.tips');
|
||||||
|
if (! is_array($tips) || empty($tips)) {
|
||||||
|
$tips = [__(
|
||||||
|
'marketing.server_error.tip_fallback',
|
||||||
|
[],
|
||||||
|
app()->getLocale()
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
$hintMessage = $hint ?? request()->attributes->get('serverErrorHint');
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<main class="relative mx-auto flex min-h-screen max-w-5xl flex-col items-center justify-center px-6 py-16 text-center">
|
||||||
|
<div class="pointer-events-none absolute inset-0">
|
||||||
|
<div class="absolute -left-32 -top-32 h-96 w-96 rounded-full bg-amber-500/30 blur-3xl"></div>
|
||||||
|
<div class="absolute right-0 top-1/3 h-80 w-80 rounded-full bg-rose-500/20 blur-3xl"></div>
|
||||||
|
<div class="absolute bottom-0 left-1/2 h-72 w-72 -translate-x-1/2 rounded-full bg-blue-500/20 blur-3xl"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="relative z-10 w-full space-y-8 rounded-3xl border border-white/10 bg-white/5 p-10 shadow-2xl shadow-black/40 backdrop-blur">
|
||||||
|
<div class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-4 py-2 text-xs font-semibold uppercase tracking-widest text-amber-200 shadow-lg shadow-amber-500/30">
|
||||||
|
{{ $statusCode }} · {{ __('marketing.server_error.title') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="text-balance text-4xl font-bold leading-tight text-white sm:text-5xl md:text-6xl">
|
||||||
|
{{ __('marketing.server_error.subtitle') }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p class="text-sm text-white/60">
|
||||||
|
{{ __('marketing.server_error.status_label') }}:
|
||||||
|
<span class="font-mono">{{ $statusCode }}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mx-auto max-w-2xl text-lg text-white/70 sm:text-xl">
|
||||||
|
{{ __('marketing.server_error.description') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-6 rounded-3xl border border-white/10 bg-white/10 p-8 text-left shadow-lg shadow-black/30 backdrop-blur md:flex-row md:gap-8">
|
||||||
|
<div class="md:w-1/3">
|
||||||
|
<h2 class="text-lg font-semibold uppercase tracking-widest text-amber-200">
|
||||||
|
{{ __('marketing.server_error.tip_heading') }}
|
||||||
|
</h2>
|
||||||
|
<div class="mt-4 h-1 w-16 rounded-full bg-amber-400"></div>
|
||||||
|
</div>
|
||||||
|
<ul class="space-y-3 text-base text-white/80 md:w-2/3">
|
||||||
|
@foreach($tips as $index => $tip)
|
||||||
|
<li class="flex items-start gap-3">
|
||||||
|
<span class="mt-1 flex h-6 w-6 flex-none items-center justify-center rounded-full bg-white/10 text-xs font-semibold text-amber-200">
|
||||||
|
{{ $index + 1 }}
|
||||||
|
</span>
|
||||||
|
<span>{{ $tip }}</span>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(! empty($hintMessage))
|
||||||
|
<div class="rounded-3xl border border-amber-300/40 bg-amber-500/10 p-6 text-left text-white/80 shadow-inner shadow-amber-900/30">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-widest text-amber-200">
|
||||||
|
{{ __('marketing.server_error.hint_label') }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-3 text-base text-white">
|
||||||
|
{{ $hintMessage }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3 sm:flex-row sm:justify-center">
|
||||||
|
<a
|
||||||
|
href="{{ route('marketing.home', ['locale' => app()->getLocale()]) }}"
|
||||||
|
class="inline-flex items-center justify-center rounded-full bg-white px-6 py-3 font-medium text-gray-900 shadow-lg shadow-amber-500/30 transition hover:-translate-y-1 hover:bg-amber-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
|
||||||
|
>
|
||||||
|
{{ __('marketing.server_error.cta_home') }}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="{{ route('packages', ['locale' => app()->getLocale()]) }}"
|
||||||
|
class="inline-flex items-center justify-center rounded-full border border-white/40 px-6 py-3 font-medium text-white transition hover:-translate-y-1 hover:border-white hover:bg-white/10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
|
||||||
|
>
|
||||||
|
{{ __('marketing.server_error.cta_packages') }}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="{{ app()->getLocale() === 'en' ? route('marketing.contact', ['locale' => app()->getLocale()]) : route('kontakt', ['locale' => app()->getLocale()]) }}"
|
||||||
|
class="inline-flex items-center justify-center rounded-full border border-transparent bg-gradient-to-r from-amber-500 to-rose-500 px-6 py-3 font-medium text-white shadow-lg shadow-rose-500/30 transition hover:-translate-y-1 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
|
||||||
|
>
|
||||||
|
{{ __('marketing.server_error.cta_contact') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user