diff --git a/modules/pi_control/bootstrap.php b/modules/pi_control/bootstrap.php index abb9804..08c08e1 100644 --- a/modules/pi_control/bootstrap.php +++ b/modules/pi_control/bootstrap.php @@ -41,6 +41,7 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName $hostTable = $table('hosts'); $cmdTable = $table('commands'); $runTable = $table('runs'); + $sessionTable = $table('sessions'); if ($driver === 'pgsql') { $pdo->exec("CREATE TABLE IF NOT EXISTS {$hostTable} ( @@ -71,6 +72,16 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName created_by VARCHAR(120) NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP )"); + $pdo->exec("CREATE TABLE IF NOT EXISTS {$sessionTable} ( + id SERIAL PRIMARY KEY, + token VARCHAR(64) NOT NULL UNIQUE, + host_id INTEGER NOT NULL, + provider VARCHAR(20) NOT NULL DEFAULT 'ttyd', + created_by VARCHAR(120) NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMP NOT NULL, + last_used_at TIMESTAMP NULL + )"); } else { $pdo->exec("CREATE TABLE IF NOT EXISTS {$hostTable} ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -100,6 +111,16 @@ $mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName created_by VARCHAR(120) NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP )"); + $pdo->exec("CREATE TABLE IF NOT EXISTS {$sessionTable} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + token VARCHAR(64) NOT NULL UNIQUE, + host_id INTEGER NOT NULL, + provider VARCHAR(20) NOT NULL DEFAULT 'ttyd', + created_by VARCHAR(120) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + expires_at DATETIME NOT NULL, + last_used_at DATETIME NULL + )"); } // Seed default commands (only when empty) diff --git a/modules/pi_control/module.json b/modules/pi_control/module.json index 2de09be..6e2a378 100644 --- a/modules/pi_control/module.json +++ b/modules/pi_control/module.json @@ -30,7 +30,12 @@ { "name": "db.dbname", "label": "DB Name", "type": "text", "required": false }, { "name": "db.schema", "label": "DB Schema", "type": "text", "required": false }, { "name": "db.user", "label": "DB User", "type": "text", "required": false }, - { "name": "db.password", "label": "DB Passwort", "type": "password", "required": false } + { "name": "db.password", "label": "DB Passwort", "type": "password", "required": false }, + { "name": "ttyd_url", "label": "ttyd URL", "type": "text", "required": false, "help": "z.B. https://staging.nexus.int.kusche.berlin/ttyd" }, + { "name": "wetty_url", "label": "WeTTY URL", "type": "text", "required": false, "help": "z.B. https://staging.nexus.int.kusche.berlin/wetty" }, + { "name": "terminal_default_provider", "label": "Standard-Konsole", "type": "text", "required": false, "help": "ttyd oder wetty" }, + { "name": "terminal_token_ttl", "label": "Token TTL (Minuten)", "type": "number", "required": false, "help": "Gültigkeit der Konsole-Token, z.B. 10" }, + { "name": "terminal_shared_secret", "label": "Terminal Shared Secret", "type": "password", "required": false, "help": "Zusätzliche Absicherung für terminal_info (Header X-Terminal-Secret)" } ] }, "db_defaults": { diff --git a/modules/pi_control/pages/console.php b/modules/pi_control/pages/console.php index 80fbc6f..23db98a 100644 --- a/modules/pi_control/pages/console.php +++ b/modules/pi_control/pages/console.php @@ -5,48 +5,95 @@ $table = fn(string $name) => module_fn('pi_control', 'table', $name); $notice = null; $error = null; +$terminalNotice = null; +$terminalError = null; +$terminalUrl = null; + +$settings = modules()->settings('pi_control'); +$ttydUrl = trim((string)($settings['ttyd_url'] ?? '/ttyd')); +$wettyUrl = trim((string)($settings['wetty_url'] ?? '/wetty')); +$defaultProvider = (string)($settings['terminal_default_provider'] ?? 'ttyd'); +$tokenTtl = (int)($settings['terminal_token_ttl'] ?? 10); +$tokenTtl = $tokenTtl > 0 ? $tokenTtl : 10; $hosts = $pdo->query('SELECT * FROM ' . $table('hosts') . ' ORDER BY name ASC')->fetchAll(PDO::FETCH_ASSOC); $commands = $pdo->query('SELECT * FROM ' . $table('commands') . ' ORDER BY label ASC')->fetchAll(PDO::FETCH_ASSOC); if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $hostId = (int)($_POST['host_id'] ?? 0); - $commandId = (int)($_POST['command_id'] ?? 0); - $rawCommand = trim((string)($_POST['command_text'] ?? '')); + $action = (string)($_POST['action'] ?? ''); - if ($hostId <= 0) { - $error = 'Bitte einen Host wählen.'; - } elseif ($commandId <= 0 && $rawCommand === '') { - $error = 'Bitte einen Befehl wählen oder einen Befehl eingeben.'; - } else { - $selectedCommand = ''; - if ($commandId > 0) { - foreach ($commands as $c) { - if ((int)$c['id'] === $commandId) { - if (!auth_is_admin() && !empty($c['admin_only'])) { - $error = 'Dieser Befehl ist nur für Admins.'; - } else { - $selectedCommand = (string)$c['command']; - } - break; - } - } - } + if ($action === 'open_console') { + $hostId = (int)($_POST['terminal_host_id'] ?? 0); + $provider = (string)($_POST['terminal_provider'] ?? $defaultProvider); + $provider = in_array($provider, ['ttyd', 'wetty'], true) ? $provider : 'ttyd'; - if (!$error) { - $commandText = $selectedCommand !== '' ? $selectedCommand : $rawCommand; + if ($hostId <= 0) { + $terminalError = 'Bitte einen Host wählen.'; + } else { + $driver = (string)$pdo->getAttribute(PDO::ATTR_DRIVER_NAME); + $expiresSql = $driver === 'pgsql' + ? "NOW() + INTERVAL '{$tokenTtl} minutes'" + : "DATETIME('now', '+{$tokenTtl} minutes')"; + + $token = bin2hex(random_bytes(24)); $stmt = $pdo->prepare( - 'INSERT INTO ' . $table('runs') . ' (host_id, command_id, command_text, status, created_by) VALUES (:host_id, :command_id, :command_text, :status, :created_by)' + 'INSERT INTO ' . $table('sessions') . ' (token, host_id, provider, created_by, expires_at) + VALUES (:token, :host_id, :provider, :created_by, ' . $expiresSql . ')' ); $stmt->execute([ + 'token' => $token, 'host_id' => $hostId, - 'command_id' => $commandId > 0 ? $commandId : null, - 'command_text' => $commandText, - 'status' => 'pending', + 'provider' => $provider, 'created_by' => auth_display_name() ?: null, ]); - $notice = 'Befehl wurde erfasst. (Execution-Backend folgt)'; + if ($provider === 'ttyd') { + $sep = str_contains($ttydUrl, '?') ? '&' : '?'; + $terminalUrl = rtrim($ttydUrl, '/') . '/' . $sep . 'arg=' . rawurlencode($token); + } else { + $terminalUrl = $wettyUrl; + $terminalNotice = 'WeTTY nutzt den Standard-Host aus der Container-Konfiguration. Für Host-Auswahl bitte ttyd nutzen.'; + } + } + } else { + $hostId = (int)($_POST['host_id'] ?? 0); + $commandId = (int)($_POST['command_id'] ?? 0); + $rawCommand = trim((string)($_POST['command_text'] ?? '')); + + if ($hostId <= 0) { + $error = 'Bitte einen Host wählen.'; + } elseif ($commandId <= 0 && $rawCommand === '') { + $error = 'Bitte einen Befehl wählen oder einen Befehl eingeben.'; + } else { + $selectedCommand = ''; + if ($commandId > 0) { + foreach ($commands as $c) { + if ((int)$c['id'] === $commandId) { + if (!auth_is_admin() && !empty($c['admin_only'])) { + $error = 'Dieser Befehl ist nur für Admins.'; + } else { + $selectedCommand = (string)$c['command']; + } + break; + } + } + } + + if (!$error) { + $commandText = $selectedCommand !== '' ? $selectedCommand : $rawCommand; + $stmt = $pdo->prepare( + 'INSERT INTO ' . $table('runs') . ' (host_id, command_id, command_text, status, created_by) VALUES (:host_id, :command_id, :command_text, :status, :created_by)' + ); + $stmt->execute([ + 'host_id' => $hostId, + 'command_id' => $commandId > 0 ? $commandId : null, + 'command_text' => $commandText, + 'status' => 'pending', + 'created_by' => auth_display_name() ?: null, + ]); + + $notice = 'Befehl wurde erfasst. (Execution-Backend folgt)'; + } } } } @@ -69,31 +116,84 @@ $runs = $pdo->query('SELECT * FROM ' . $table('runs') . ' ORDER BY id DESC LIMIT