refactor(deployment): Remove WireGuard VPN dependency and restore public service access
Remove WireGuard integration from production deployment to simplify infrastructure: - Remove docker-compose-direct-access.yml (VPN-bound services) - Remove VPN-only middlewares from Grafana, Prometheus, Portainer - Remove WireGuard middleware definitions from Traefik - Remove WireGuard IPs (10.8.0.0/24) from Traefik forwarded headers All monitoring services now publicly accessible via subdomains: - grafana.michaelschiemer.de (with Grafana native auth) - prometheus.michaelschiemer.de (with Basic Auth) - portainer.michaelschiemer.de (with Portainer native auth) All services use Let's Encrypt SSL certificates via Traefik.
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Renderers;
|
||||
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
||||
use App\Framework\Http\Response;
|
||||
use App\Framework\Http\Status;
|
||||
|
||||
/**
|
||||
* HTTP Response factory for API and HTML error pages
|
||||
*
|
||||
* Extracts Response generation logic from ErrorKernel for reuse
|
||||
* in middleware recovery patterns.
|
||||
*/
|
||||
final readonly class ResponseErrorRenderer
|
||||
{
|
||||
public function __construct(
|
||||
private bool $isDebugMode = false
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create HTTP Response from exception
|
||||
*
|
||||
* @param \Throwable $exception Exception to render
|
||||
* @param ExceptionContextProvider|null $contextProvider Optional context provider
|
||||
* @return Response HTTP Response object
|
||||
*/
|
||||
public function createResponse(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider = null
|
||||
): Response {
|
||||
// Determine if API or HTML response needed
|
||||
$isApiRequest = $this->isApiRequest();
|
||||
|
||||
if ($isApiRequest) {
|
||||
return $this->createApiResponse($exception, $contextProvider);
|
||||
}
|
||||
|
||||
return $this->createHtmlResponse($exception, $contextProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create JSON API error response
|
||||
*/
|
||||
private function createApiResponse(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider
|
||||
): Response {
|
||||
$statusCode = $this->getHttpStatusCode($exception);
|
||||
|
||||
$errorData = [
|
||||
'error' => [
|
||||
'message' => $this->isDebugMode
|
||||
? $exception->getMessage()
|
||||
: 'An error occurred while processing your request.',
|
||||
'type' => $this->isDebugMode ? get_class($exception) : 'ServerError',
|
||||
'code' => $exception->getCode(),
|
||||
]
|
||||
];
|
||||
|
||||
// Add debug information if enabled
|
||||
if ($this->isDebugMode) {
|
||||
$errorData['error']['file'] = $exception->getFile();
|
||||
$errorData['error']['line'] = $exception->getLine();
|
||||
$errorData['error']['trace'] = $this->formatStackTrace($exception);
|
||||
|
||||
// Add context from WeakMap if available
|
||||
if ($contextProvider !== null) {
|
||||
$context = $contextProvider->get($exception);
|
||||
if ($context !== null) {
|
||||
$errorData['context'] = [
|
||||
'operation' => $context->operation,
|
||||
'component' => $context->component,
|
||||
'request_id' => $context->requestId,
|
||||
'occurred_at' => $context->occurredAt?->format('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$body = json_encode($errorData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
return new Response(
|
||||
status: Status::from($statusCode),
|
||||
body: $body,
|
||||
headers: [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create HTML error page response
|
||||
*/
|
||||
private function createHtmlResponse(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider
|
||||
): Response {
|
||||
$statusCode = $this->getHttpStatusCode($exception);
|
||||
|
||||
$html = $this->generateErrorHtml(
|
||||
$exception,
|
||||
$contextProvider,
|
||||
$statusCode
|
||||
);
|
||||
|
||||
return new Response(
|
||||
status: Status::from($statusCode),
|
||||
body: $html,
|
||||
headers: [
|
||||
'Content-Type' => 'text/html; charset=utf-8',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HTML error page
|
||||
*/
|
||||
private function generateErrorHtml(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider,
|
||||
int $statusCode
|
||||
): string {
|
||||
$title = $this->getErrorTitle($statusCode);
|
||||
$message = $this->isDebugMode
|
||||
? $exception->getMessage()
|
||||
: 'An error occurred while processing your request.';
|
||||
|
||||
$debugInfo = '';
|
||||
if ($this->isDebugMode) {
|
||||
$debugInfo = $this->generateDebugSection($exception, $contextProvider);
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{$title}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.error-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #d32f2f;
|
||||
margin-top: 0;
|
||||
}
|
||||
.error-message {
|
||||
background: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.debug-info {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
margin-top: 2rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.debug-info pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.context-item {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.context-label {
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<h1>{$title}</h1>
|
||||
<div class="error-message">
|
||||
<p>{$message}</p>
|
||||
</div>
|
||||
{$debugInfo}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate debug information section
|
||||
*/
|
||||
private function generateDebugSection(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider
|
||||
): string {
|
||||
$exceptionClass = get_class($exception);
|
||||
$file = $exception->getFile();
|
||||
$line = $exception->getLine();
|
||||
$trace = $this->formatStackTrace($exception);
|
||||
|
||||
$contextHtml = '';
|
||||
if ($contextProvider !== null) {
|
||||
$context = $contextProvider->get($exception);
|
||||
if ($context !== null) {
|
||||
$contextHtml = <<<HTML
|
||||
<div class="context-item">
|
||||
<span class="context-label">Operation:</span> {$context->operation}
|
||||
</div>
|
||||
<div class="context-item">
|
||||
<span class="context-label">Component:</span> {$context->component}
|
||||
</div>
|
||||
<div class="context-item">
|
||||
<span class="context-label">Request ID:</span> {$context->requestId}
|
||||
</div>
|
||||
<div class="context-item">
|
||||
<span class="context-label">Occurred At:</span> {$context->occurredAt?->format('Y-m-d H:i:s')}
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<div class="debug-info">
|
||||
<h3>Debug Information</h3>
|
||||
<div class="context-item">
|
||||
<span class="context-label">Exception:</span> {$exceptionClass}
|
||||
</div>
|
||||
<div class="context-item">
|
||||
<span class="context-label">File:</span> {$file}:{$line}
|
||||
</div>
|
||||
{$contextHtml}
|
||||
<h4>Stack Trace:</h4>
|
||||
<pre>{$trace}</pre>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if current request is API request
|
||||
*/
|
||||
private function isApiRequest(): bool
|
||||
{
|
||||
// Check for JSON Accept header
|
||||
$acceptHeader = $_SERVER['HTTP_ACCEPT'] ?? '';
|
||||
if (str_contains($acceptHeader, 'application/json')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for API path prefix
|
||||
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
if (str_starts_with($requestUri, '/api/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for AJAX requests
|
||||
$requestedWith = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? '';
|
||||
if (strtolower($requestedWith) === 'xmlhttprequest') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTTP status code from exception
|
||||
*/
|
||||
private function getHttpStatusCode(\Throwable $exception): int
|
||||
{
|
||||
// Use exception code if it's a valid HTTP status code
|
||||
$code = $exception->getCode();
|
||||
if ($code >= 400 && $code < 600) {
|
||||
return $code;
|
||||
}
|
||||
|
||||
// Map common exceptions to status codes
|
||||
return match (true) {
|
||||
$exception instanceof \InvalidArgumentException => 400,
|
||||
$exception instanceof \RuntimeException => 500,
|
||||
$exception instanceof \LogicException => 500,
|
||||
default => 500,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-friendly error title from status code
|
||||
*/
|
||||
private function getErrorTitle(int $statusCode): string
|
||||
{
|
||||
return match ($statusCode) {
|
||||
400 => 'Bad Request',
|
||||
401 => 'Unauthorized',
|
||||
403 => 'Forbidden',
|
||||
404 => 'Not Found',
|
||||
405 => 'Method Not Allowed',
|
||||
429 => 'Too Many Requests',
|
||||
500 => 'Internal Server Error',
|
||||
502 => 'Bad Gateway',
|
||||
503 => 'Service Unavailable',
|
||||
default => "Error {$statusCode}",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format stack trace for display
|
||||
*/
|
||||
private function formatStackTrace(\Throwable $exception): string
|
||||
{
|
||||
$trace = $exception->getTraceAsString();
|
||||
|
||||
// Limit trace depth in production
|
||||
if (!$this->isDebugMode) {
|
||||
$lines = explode("\n", $trace);
|
||||
$trace = implode("\n", array_slice($lines, 0, 5));
|
||||
}
|
||||
|
||||
return htmlspecialchars($trace, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user