Initial commit

This commit is contained in:
root
2026-05-09 17:28:23 +02:00
commit 9d73f82529
5575 changed files with 281989 additions and 0 deletions
+267
View File
@@ -0,0 +1,267 @@
<?php
declare(strict_types=1);
namespace App\Exceptions;
use App\Services\AlertService;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Illuminate\Validation\ValidationException;
use Throwable;
class Handler extends ExceptionHandler
{
#[\Override]
protected $levels = [
//
];
#[\Override]
protected $dontReport = [
//
];
#[\Override]
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
private const string CACHE_KEY_ERROR_COUNT = 'error_count_';
private const int ERROR_COUNT_DURATION = 300;
private const array AUTO_RECOVERABLE_ERRORS = [
'ViteManifestNotFoundException',
'filemtime',
'stat failed for',
'No application encryption key has been specified',
'file does not exist',
'view [',
'Target class [view] does not exist',
'BindingResolutionException',
];
private const int AUTO_RECOVER_COOLDOWN = 60;
#[\Override]
public function register(): void
{
$this->reportable(function (Throwable $e) {
$this->attemptAutoRecovery($e);
$this->handleExceptionAlert($e);
});
$this->renderable(function (Throwable $e) {
$recovered = $this->attemptAutoRecovery($e);
if ($recovered) {
return redirect()->to(request()->url());
}
$this->handleExceptionAlert($e);
});
}
private function attemptAutoRecovery(Throwable $e): bool
{
$message = $e->getMessage() ?: '';
$exceptionClass = $e::class;
$isRecoverable = array_any(self::AUTO_RECOVERABLE_ERRORS, fn ($pattern) => str_contains($exceptionClass, (string) $pattern) || str_contains($message, (string) $pattern));
if (! $isRecoverable) {
return false;
}
$cacheKey = 'auto_recovery_cooldown';
if (Cache::has($cacheKey)) {
return false;
}
Cache::put($cacheKey, true, self::AUTO_RECOVER_COOLDOWN);
Log::warning('Attempting auto-recovery from error', [
'exception' => $exceptionClass,
'message' => $message,
]);
try {
Artisan::call('view:clear');
Artisan::call('cache:clear');
Artisan::call('config:clear');
Artisan::call('route:clear');
Artisan::call('config:cache');
Artisan::call('view:cache');
if (str_contains($exceptionClass, 'ViteManifestNotFoundException') || str_contains($message, 'Vite manifest')) {
$this->rebuildViteManifest();
}
if (function_exists('opcache_reset')) {
@opcache_reset();
}
Log::info('Auto-recovery completed successfully');
return true;
} catch (Throwable $recoveryError) {
Log::error('Auto-recovery failed', [
'original_error' => $message,
'recovery_error' => $recoveryError->getMessage(),
]);
return false;
}
}
private function rebuildViteManifest(): void
{
$manifestPath = public_path('build/manifest.json');
if (! file_exists($manifestPath)) {
Log::warning('Vite manifest missing, attempting rebuild');
$result = Process::timeout(120)->run('npm run build');
if ($result->successful()) {
Log::info('Vite manifest rebuilt successfully');
if (file_exists('/var/www/atomcms/public/build')) {
Process::run('chown -R www-data:www-data /var/www/atomcms/public/build');
Process::run('chmod -R 775 /var/www/atomcms/public/build');
}
} else {
Log::error('Vite manifest rebuild failed', [
'output' => $result->output(),
'error' => $result->errorOutput(),
]);
}
}
}
private function handleExceptionAlert(Throwable $e): void
{
if (! $this->shouldAlertException($e)) {
return;
}
try {
$alertService = app(AlertService::class);
$errorMessage = $e->getMessage() ?: $e::class;
$alertService->sendCriticalError($errorMessage, $e);
$this->trackErrorRate();
Log::channel('emergency')->error('Critical exception reported via alert', [
'exception' => $e::class,
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
} catch (\Exception $alertException) {
Log::error('Failed to send exception alert: ' . $alertException->getMessage());
}
}
private function shouldAlertException(Throwable $e): bool
{
if (! app()->isBooted()) {
return false;
}
if (! (bool) setting('alert_errors_enabled', true)) {
return false;
}
$criticalExceptions = [
QueryException::class,
RconConnectionException::class,
];
foreach ($criticalExceptions as $criticalException) {
if ($e instanceof $criticalException) {
return true;
}
}
if ($this->isHighErrorRate()) {
return true;
}
$minSeverity = setting('alert_min_severity', 'error');
return $this->isSeverityHighEnough($e, $minSeverity);
}
private function trackErrorRate(): void
{
if (! app()->isBooted()) {
return;
}
$key = self::CACHE_KEY_ERROR_COUNT . now()->format('YmdHi');
$count = Cache::get($key, 0);
Cache::put($key, $count + 1, self::ERROR_COUNT_DURATION);
}
private function isHighErrorRate(): bool
{
if (! app()->isBooted()) {
return false;
}
$threshold = (int) setting('alert_error_threshold', 10);
$key = self::CACHE_KEY_ERROR_COUNT . now()->format('YmdHi');
$count = Cache::get($key, 0);
return $count >= $threshold;
}
private function isSeverityHighEnough(Throwable $e, string $minSeverity): bool
{
$severityLevel = [
'info' => 0,
'warning' => 1,
'error' => 2,
'critical' => 3,
];
$exceptionSeverity = $this->determineExceptionSeverity($e);
$minLevel = $severityLevel[$minSeverity] ?? 2;
return $exceptionSeverity >= $minLevel;
}
private function determineExceptionSeverity(Throwable $e): int
{
if ($e instanceof \Error) {
return 3;
}
if ($e instanceof ValidationException) {
return 0;
}
if ($e instanceof AuthenticationException) {
return 1;
}
if ($e instanceof AuthorizationException) {
return 1;
}
return 2;
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Exceptions;
use Exception;
use Throwable;
class MigrationFailedException extends Exception
{
/**
* MigrationFailedException constructor.
*/
public function __construct(string $message = 'Migration failed', int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
/**
* Get the exception message with additional context
*/
public function getDetailedMessage(): string
{
return 'Migration failed: ' . $this->getMessage() . ' (Code: ' . $this->getCode() . ')';
}
/**
* Get the exception code with default
*/
public function getErrorCode(): int
{
return $this->getCode() ?: 500;
}
}
+9
View File
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Exceptions;
use Exception;
class RconConnectionException extends Exception {}