auth(); // OIDC Auth $publicPaths = [ 'auth/login', 'auth/callback', 'auth/logout', 'auth/keycloak/login', 'auth/keycloak/callback', 'auth/keycloak/logout', 'auth/me', 'module/pi_control/terminal_info', ]; $requiresGlobalAuth = in_array($uriPath, ['settings', 'users', 'modules', 'modules/install', 'modules/sql-import', 'debug', 'exports/database.sql'], true) || str_starts_with($uriPath, 'modules/setup/') || str_starts_with($uriPath, 'modules/access/'); 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 if (str_contains($uriPath, '..')) { http_response_code(400); exit('Bad request'); } if ($uriPath === 'auth/keycloak/login') { $returnTo = (string)($_GET['return_to'] ?? '/'); $auth->login($returnTo); } if ($uriPath === 'auth/keycloak/callback') { $uriPath = 'auth/callback'; } if ($uriPath === 'auth/keycloak/logout') { $uriPath = 'auth/logout'; } if ($uriPath === 'auth/me') { header('Content-Type: application/json; charset=utf-8'); echo json_encode([ 'authenticated' => $auth->isAuthenticated(), 'user' => $auth->user(), ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } if ($uriPath === 'exports/database.sql') { require_admin(); $pdo = app()->basePdo() ?: app()->pdo(); if (!$pdo instanceof PDO) { http_response_code(500); exit('Keine Datenbankverbindung fuer den Export verfuegbar.'); } $filename = 'nexus-export-' . gmdate('Ymd-His') . '.sql'; $sql = (new \App\SqlDataExporter())->export($pdo, 'nexus'); header('Content-Type: application/sql; charset=utf-8'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('X-Content-Type-Options: nosniff'); echo $sql; 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 (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'; app()->modules()->runDueIntervalTasks('fx-rates'); app()->modules()->runDueCronTasks('fx-rates'); 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/')) { $_GET['module'] = trim(substr($uriPath, strlen('modules/setup/')), '/'); $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 === 'users') { $target = $pagesBase . '/users/index.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) { app()->modules()->runDueIntervalTasks($module); app()->modules()->runDueCronTasks($module); $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 = '
' . '
' . e($title) . '
' . '

Setup erforderlich

' . '

' . e($e->getMessage()) . '

' . '
Zum Setup
' . '
'; } // 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'); }