added nice "error 500" pages, fixed core system migration
This commit is contained in:
@@ -3,8 +3,15 @@
|
||||
namespace App\Exceptions;
|
||||
|
||||
use App\Support\ApiError;
|
||||
use Illuminate\Database\Connectors\ConnectionException as DatabaseConnectionException;
|
||||
use Illuminate\Database\QueryException;
|
||||
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 League\Flysystem\FilesystemException;
|
||||
use PDOException;
|
||||
use Throwable;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -74,4 +87,81 @@ class Handler extends ExceptionHandler
|
||||
|
||||
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->timestamp('email_verified_at')->nullable();
|
||||
$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_recovery_codes')->nullable();
|
||||
$table->timestamp('two_factor_confirmed_at')->nullable();
|
||||
$table->string('username', 32)->nullable()->unique()->after('email');
|
||||
$table->string('preferred_locale', 5)->default(config('app.locale', 'en'))->after('role');
|
||||
$table->string('username', 32)->nullable()->unique();
|
||||
$table->string('preferred_locale', 5)->default(config('app.locale', 'en'));
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
$table->string('status')->default('active');
|
||||
@@ -119,4 +119,4 @@ return new class extends Migration
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('users');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -180,6 +180,30 @@ return [
|
||||
'cta_contact' => 'Kontakt aufnehmen',
|
||||
'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' => [
|
||||
'title' => 'Erfolgreich',
|
||||
'verify_email' => 'E-Mail verifizieren',
|
||||
|
||||
@@ -180,6 +180,30 @@ return [
|
||||
'cta_contact' => 'Get in touch',
|
||||
'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' => [
|
||||
'title' => 'Success',
|
||||
'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