Repair 2
All checks were successful
Deploy / deploy-staging (push) Successful in 10s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-06-07 02:52:14 +02:00
parent 72774c05aa
commit a408765e76
5 changed files with 396 additions and 88 deletions

View File

@@ -3,12 +3,22 @@ declare(strict_types=1);
/** /**
* Base database for Nexus core (users, settings, modules). * Base database for Nexus core (users, settings, modules).
* Sync copies the correct file into /config. * Prefer the deployed root config, but fall back to checked-in env defaults.
*/ */
$path = __DIR__ . '/db_settings_basic.php'; $candidates = [
if (!file_exists($path)) { __DIR__ . '/db_settings_basic.php',
throw new RuntimeException('Missing base DB config: expected config/db_settings_basic.php'); __DIR__ . '/staging/db_settings_basic.php',
__DIR__ . '/prod/db_settings_basic.php',
];
foreach ($candidates as $path) {
if (is_file($path)) {
return require $path;
}
} }
return require $path; throw new RuntimeException(
'Missing base DB config. Expected one of: '
. implode(', ', array_map(static fn (string $path): string => basename(dirname($path)) . '/' . basename($path), $candidates))
);

View File

@@ -26,6 +26,7 @@ $pgsql = [
'host' => 'db_nexus', 'host' => 'db_nexus',
'port' => 5432, 'port' => 5432,
'dbname' => 'nexus_live', 'dbname' => 'nexus_live',
'connect_timeout' => 5,
// optional: schema/search_path (commonly "public") // optional: schema/search_path (commonly "public")
'schema' => 'public', 'schema' => 'public',

View File

@@ -26,6 +26,7 @@ $pgsql = [
'host' => 'staging_db_nexus', 'host' => 'staging_db_nexus',
'port' => 5432, 'port' => 5432,
'dbname' => 'nexus_staging', 'dbname' => 'nexus_staging',
'connect_timeout' => 5,
// optional: schema/search_path (commonly "public") // optional: schema/search_path (commonly "public")
'schema' => 'public', 'schema' => 'public',

View File

@@ -1,100 +1,389 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
http_response_code(503); use Modules\MiningChecker\Support\ApiException as MiningApiException;
header('Content-Type: text/html; charset=utf-8'); use Modules\MiningChecker\Support\DebugState as MiningDebugState;
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache'); // boot application (config, autoload, services)
?> require_once __DIR__ . '/../config/fileload.php';
<!DOCTYPE html>
<html lang="de"> $uriPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
<head> $uriPath = preg_replace('~/{2,}~', '/', $uriPath);
<meta charset="utf-8"> $uriPath = trim($uriPath, '/');
<meta name="viewport" content="width=device-width, initial-scale=1"> $projectRoot = dirname(__DIR__);
<title>Nexus Wartungstest</title> $auth = app()->auth();
<style>
:root { // OIDC Auth
color-scheme: light; $publicPaths = [
--bg: #f3efe7; 'auth/login',
--panel: #fffdf8; 'auth/callback',
--text: #1f2933; 'auth/logout',
--muted: #52606d; 'auth/keycloak/login',
--accent: #b85c38; 'auth/keycloak/callback',
--border: #e6dfd2; 'auth/keycloak/logout',
'auth/me',
'module/pi_control/terminal_info',
];
$requiresGlobalAuth = in_array($uriPath, ['settings', 'settings/widgets', 'settings/search-engines', 'settings/apps', 'users', 'modules', 'modules/install', 'modules/sql-import', 'debug', 'exports/database.sql', 'dashboard', 'dashboards', 'integrations', 'page-modules'], true)
|| str_starts_with($uriPath, 'modules/setup/')
|| str_starts_with($uriPath, 'modules/access/');
if (str_starts_with($uriPath, 'page-modules/view/')) {
$requiresGlobalAuth = true;
}
if (defined('APP_AUTH_ENABLED') && APP_AUTH_ENABLED && $requiresGlobalAuth && !in_array($uriPath, $publicPaths, true)) {
$user = auth_user();
if (!$user) {
header('Location: /auth/login', true, 302);
exit;
}
} }
* { // Sicherheitscheck
box-sizing: border-box; if (str_contains($uriPath, '..')) {
http_response_code(400);
exit('Bad request');
} }
body { if ($uriPath === 'auth/keycloak/login') {
margin: 0; $returnTo = (string)($_GET['return_to'] ?? '/');
min-height: 100vh; $auth->login($returnTo);
display: grid;
place-items: center;
padding: 24px;
background:
radial-gradient(circle at top left, rgba(184, 92, 56, 0.12), transparent 30%),
linear-gradient(180deg, var(--bg), #ebe4d8);
color: var(--text);
font-family: "Segoe UI", Arial, sans-serif;
} }
main { if ($uriPath === 'auth/keycloak/callback') {
width: min(680px, 100%); $uriPath = 'auth/callback';
background: var(--panel);
border: 1px solid var(--border);
border-radius: 18px;
padding: 32px;
box-shadow: 0 24px 60px rgba(31, 41, 51, 0.08);
} }
h1 { if ($uriPath === 'auth/keycloak/logout') {
margin: 0 0 12px; $uriPath = 'auth/logout';
font-size: clamp(2rem, 4vw, 3rem);
line-height: 1.05;
} }
p { if ($uriPath === 'auth/me') {
margin: 0 0 14px; header('Content-Type: application/json; charset=utf-8');
color: var(--muted); echo json_encode([
font-size: 1.05rem; 'authenticated' => $auth->isAuthenticated(),
line-height: 1.6; 'user' => $auth->user(),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
} }
.badge { if ($uriPath === 'exports/database.sql') {
display: inline-block; require_admin();
margin-bottom: 18px; $pdo = app()->basePdo() ?: app()->pdo();
padding: 8px 12px; if (!$pdo instanceof PDO) {
border-radius: 999px; http_response_code(500);
background: rgba(184, 92, 56, 0.12); exit('Keine Datenbankverbindung fuer den Export verfuegbar.');
color: var(--accent);
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
font-size: 0.78rem;
} }
.meta { $filename = 'nexus-export-' . gmdate('Ymd-His') . '.sql';
margin-top: 24px; $sql = (new \App\SqlDataExporter())->export($pdo, 'nexus');
padding-top: 18px; header('Content-Type: application/sql; charset=utf-8');
border-top: 1px solid var(--border); header('Content-Disposition: attachment; filename="' . $filename . '"');
font-family: monospace; header('X-Content-Type-Options: nosniff');
font-size: 0.92rem; echo $sql;
color: var(--muted); exit;
}
if (preg_match('~^api/module-auth/([a-zA-Z0-9_-]+)$~', $uriPath, $moduleAuthMatches)) {
$moduleName = $moduleAuthMatches[1];
$moduleMeta = app()->modules()->get($moduleName);
if ($moduleMeta === null) {
http_response_code(404);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => 'module_not_found'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
if (!$auth->isAuthenticated()) {
http_response_code(401);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => 'auth_required'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
if (!auth_is_admin()) {
http_response_code(403);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => 'forbidden'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
echo json_encode(['data' => ($moduleMeta['auth'] ?? ['required' => false, 'users' => [], 'groups' => []])], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
$input = json_decode((string)file_get_contents('php://input'), true);
if (!is_array($input)) {
$input = [];
}
echo json_encode(['data' => app()->modules()->saveAuth($moduleName, $input)], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
http_response_code(405);
echo json_encode(['error' => 'method_not_allowed'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
if ($uriPath === 'api/debug/entries') {
require_admin();
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'data' => [
'enabled' => nexus_debug_enabled(),
'entries' => nexus_debug_entries(),
],
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
if ($uriPath === 'api/debug/clear' && strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST') {
require_admin();
nexus_debug_clear();
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'data' => [
'cleared' => true,
'entries' => [],
],
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
if (preg_match('~^api/mining-checker(?:/(.*))?$~', $uriPath, $apiMatches)) {
$moduleMeta = app()->modules()->get('mining-checker') ?? ['auth' => ['required' => false]];
if (!$auth->canAccessModule($moduleMeta)) {
http_response_code($auth->isAuthenticated() ? 403 : 401);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'error' => $auth->isAuthenticated() ? 'forbidden' : 'auth_required',
'login_url' => '/auth/login',
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
require_once $projectRoot . '/modules/mining-checker/bootstrap.php';
try {
(new Modules\MiningChecker\Api\Router($projectRoot . '/modules/mining-checker'))->handle($apiMatches[1] ?? '');
} catch (MiningApiException $exception) {
$debugTrace = MiningDebugState::export();
http_response_code($exception->statusCode());
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'error' => $exception->getMessage(),
'context' => $exception->context(),
'debug' => $debugTrace !== [] ? $debugTrace : null,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
} catch (Throwable $exception) {
$debugTrace = MiningDebugState::export();
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'error' => 'Unerwarteter Mining-Checker Fehler.',
'context' => ['message' => $exception->getMessage()],
'debug' => $debugTrace !== [] ? $debugTrace : null,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
}
if (preg_match('~^api/fx-rates(?:/(.*))?$~', $uriPath, $apiMatches)) {
$moduleMeta = app()->modules()->get('fx-rates') ?? ['auth' => ['required' => false]];
if (!$auth->canAccessModule($moduleMeta)) {
http_response_code($auth->isAuthenticated() ? 403 : 401);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'error' => $auth->isAuthenticated() ? 'forbidden' : 'auth_required',
'login_url' => '/auth/login',
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
require_once $projectRoot . '/modules/fx-rates/bootstrap.php';
try {
$service = module_fn('fx-rates', 'service');
(new \Modules\FxRates\Api\Router($service))->handle($apiMatches[1] ?? '');
} catch (Throwable $exception) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'error' => 'Unerwarteter FX-Module Fehler.',
'context' => ['message' => $exception->getMessage()],
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
}
if (preg_match('~^module-assets/([a-zA-Z0-9_-]+)/(.*)$~', $uriPath, $assetMatches)) {
$module = $assetMatches[1];
$relativeAssetPath = trim($assetMatches[2], '/');
if ($relativeAssetPath === '' || str_contains($relativeAssetPath, '..')) {
http_response_code(400);
exit('Bad request');
}
$assetFile = $projectRoot . '/modules/' . $module . '/assets/' . $relativeAssetPath;
if (!is_file($assetFile)) {
http_response_code(404);
exit('Asset not found');
}
$extension = strtolower(pathinfo($assetFile, PATHINFO_EXTENSION));
$contentType = match ($extension) {
'css' => 'text/css; charset=utf-8',
'js' => 'application/javascript; charset=utf-8',
'json' => 'application/json; charset=utf-8',
'png' => 'image/png',
'jpg', 'jpeg' => 'image/jpeg',
'webp' => 'image/webp',
'svg' => 'image/svg+xml',
default => 'application/octet-stream',
};
header('Content-Type: ' . $contentType);
readfile($assetFile);
exit;
}
// Basispfad fuer Landingpages
$pagesBase = realpath(__DIR__ . '/../partials/landingpages') ?: (__DIR__ . '/../partials/landingpages');
$page404 = $pagesBase . '/errorpages/404.php';
// Spezialrouten für Module
if (str_starts_with($uriPath, 'modules/install')) {
$target = $pagesBase . '/modules/install.php';
} elseif (str_starts_with($uriPath, 'modules/setup/')) {
$setupPath = trim(substr($uriPath, strlen('modules/setup/')), '/');
$setupParts = $setupPath === '' ? [] : explode('/', $setupPath, 2);
$_GET['module'] = trim((string) ($setupParts[0] ?? ''));
if (isset($setupParts[1]) && trim((string) $setupParts[1]) !== '') {
$_GET['section'] = trim((string) $setupParts[1]);
}
$target = $pagesBase . '/modules/setup.php';
} elseif (str_starts_with($uriPath, 'modules/access/')) {
$_GET['module'] = trim(substr($uriPath, strlen('modules/access/')), '/');
$target = $pagesBase . '/modules/access.php';
} elseif ($uriPath === 'modules/sql-import') {
$target = $pagesBase . '/modules/sql_import.php';
} elseif ($uriPath === 'auth/login') {
$target = $pagesBase . '/auth/login.php';
} elseif ($uriPath === 'auth/callback') {
$target = $pagesBase . '/auth/callback.php';
} elseif ($uriPath === 'auth/logout') {
$target = $pagesBase . '/auth/logout.php';
} elseif ($uriPath === 'settings') {
$target = $pagesBase . '/users/settings.php';
} elseif ($uriPath === 'settings/widgets') {
$target = $pagesBase . '/users/settings_widgets.php';
} elseif ($uriPath === 'settings/search-engines') {
$target = $pagesBase . '/users/settings_search_engines.php';
} elseif ($uriPath === 'settings/apps') {
$target = $pagesBase . '/users/settings_apps.php';
} elseif ($uriPath === 'users') {
$target = $pagesBase . '/users/index.php';
} elseif ($uriPath === 'dashboard') {
$target = $pagesBase . '/dashboard.php';
} elseif ($uriPath === 'dashboards') {
$target = $pagesBase . '/dashboards.php';
} elseif ($uriPath === 'integrations') {
$target = $pagesBase . '/integrations.php';
} elseif ($uriPath === 'page-modules') {
$target = $pagesBase . '/page_modules.php';
} elseif (preg_match('~^page-modules/view/(\d+)$~', $uriPath, $pageModuleMatch)) {
$_GET['id'] = (string) $pageModuleMatch[1];
$target = $pagesBase . '/page_modules_view.php';
} elseif ($uriPath === 'debug') {
$target = $pagesBase . '/retool/debug.php';
} elseif (preg_match('~^module/([a-zA-Z0-9_-]+)(?:/(.+))?$~', $uriPath, $m)) {
$module = $m[1];
$page = isset($m[2]) && $m[2] !== '' ? trim($m[2], '/') : 'index';
$moduleMeta = app()->modules()->get($module);
if ($moduleMeta !== null) {
$auth->requireModuleAccess($moduleMeta);
}
$modulePage = app()->modules()->resolvePage($module, $page);
$moduleBootstrap = $projectRoot . '/modules/' . $module . '/bootstrap.php';
if (is_file($moduleBootstrap)) {
require_once $moduleBootstrap;
}
if ($modulePage) {
$target = $modulePage;
} else {
http_response_code(404);
$target = $page404;
}
} elseif ($uriPath === '' || $uriPath === 'index' || $uriPath === 'index.php') {
$target = $pagesBase . '/index.php';
} else {
$base = $pagesBase . '/' . $uriPath;
// 1) Verzeichnis mit index.php
if (is_dir($base) && is_file($base . '/index.php')) {
$target = $base . '/index.php';
}
// 2) Datei
elseif (is_file($base . '.php')) {
$target = $base . '.php';
}
// 3) 404
elseif (is_file($base)) {
$target = $base;
}
// 3) 404
else {
http_response_code(404);
$target = $page404;
}
}
// ------------------------------------
// Layout-Regel
// ------------------------------------
$skipLayout = false;
$targetReal = realpath($target);
$retoolBase = realpath($pagesBase . '/retool/raw');
// Beispiel: alles unter landingpages/retool/* ohne Layout
if ($targetReal && $retoolBase && str_starts_with($targetReal, $retoolBase)) {
$skipLayout = true;
}
// ------------------------------------
// Ausgabe
// ------------------------------------
// Erst Inhalt laden (ohne Ausgabe), damit Header/Redirects vor HTML funktionieren
ob_start();
try {
require $target;
$content = ob_get_clean();
} catch (\App\ModuleConfigException $e) {
ob_end_clean();
http_response_code(412);
$moduleName = $e->module();
$module = app()->modules()->get($moduleName);
$title = $module['title'] ?? $moduleName;
$setupUrl = '/modules/setup/' . rawurlencode($moduleName);
$content = '<div class="card">' .
'<div class="pill">' . e($title) . '</div>' .
'<h1 style="margin-top:.75rem;">Setup erforderlich</h1>' .
'<p class="muted">' . e($e->getMessage()) . '</p>' .
'<div style="margin-top:1rem;"><a class="nav-link" href="' . e($setupUrl) . '">Zum Setup</a></div>' .
'</div>';
}
// Wenn bereits Header gesendet wurden (z. B. eigener Redirect/Content-Type), Layout überspringen
if (headers_sent()) {
$skipLayout = true;
}
if (!$skipLayout) {
tpl('layout_start', 'structure');
}
echo $content;
if (!$skipLayout) {
tpl('layout_end', 'structure');
} }
</style>
</head>
<body>
<main>
<div class="badge">Wartungstest</div>
<h1>Nexus ist testweise im Wartungsmodus</h1>
<p>Diese Seite wird direkt aus <code>public/index.php</code> ausgeliefert und umgeht die normale App-Initialisierung.</p>
<p>Wenn diese Seite stabil erscheint, liegt das Problem sehr wahrscheinlich in der PHP-Anwendung, in einem Modul oder in deren Abhängigkeiten und nicht in der grundlegenden Webserver-Auslieferung.</p>
<div class="meta">
Testzeit: <?= htmlspecialchars(date('Y-m-d H:i:s T'), ENT_QUOTES, 'UTF-8') ?><br>
Datei: <?= htmlspecialchars(__FILE__, ENT_QUOTES, 'UTF-8') ?>
</div>
</main>
</body>
</html>

View File

@@ -213,14 +213,21 @@ final class Database
$host = (string)($db['host'] ?? 'localhost'); $host = (string)($db['host'] ?? 'localhost');
$port = (int)($db['port'] ?? 5432); $port = (int)($db['port'] ?? 5432);
$connectTimeout = isset($db['connect_timeout']) ? max(1, (int) $db['connect_timeout']) : null;
// Hinweis: charset gehört bei pgsql nicht in den DSN // Hinweis: charset gehört bei pgsql nicht in den DSN
return sprintf( $dsn = sprintf(
'pgsql:host=%s;port=%d;dbname=%s', 'pgsql:host=%s;port=%d;dbname=%s',
$host, $host,
$port, $port,
(string)$db['dbname'] (string)$db['dbname']
); );
if ($connectTimeout !== null) {
$dsn .= ';connect_timeout=' . $connectTimeout;
}
return $dsn;
} }
private static function buildSqliteDsn(array $db): string private static function buildSqliteDsn(array $db): string