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:
2025-11-05 12:48:25 +01:00
parent 7c52065aae
commit 95147ff23e
215 changed files with 29490 additions and 368 deletions

View File

@@ -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');
}
}