diff --git a/Dockerfile b/Dockerfile index f843b5f..cab6230 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,41 +1,32 @@ -# /home/docker/pve_dashboard/Dockerfile FROM php:8.2-apache -# Benötigte Pakete installieren (SQLite + OpenSSL für Zertifikate + Cron für den Scheduler) RUN apt-get update && apt-get install -y \ libsqlite3-dev \ openssl \ cron \ && docker-php-ext-install pdo_sqlite -# Apache Rewrite-Modul (für schöne URLs) und SSL-Modul aktivieren RUN a2enmod rewrite ssl -# Generiere ein Self-Signed Zertifikat (Gültig für 10 Jahre) RUN openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ -keyout /etc/ssl/private/apache-selfsigned.key \ -out /etc/ssl/certs/apache-selfsigned.crt \ -subj "/C=DE/ST=Brandenburg/L=Gruenheide/O=PUC/OU=IT/CN=localhost" -# Passe die Default SSL Konfiguration an RUN sed -i 's/ssl-cert-snakeoil.pem/apache-selfsigned.crt/g' /etc/apache2/sites-available/default-ssl.conf \ && sed -i 's/ssl-cert-snakeoil.key/apache-selfsigned.key/g' /etc/apache2/sites-available/default-ssl.conf -# SSL Site in Apache aktivieren RUN a2ensite default-ssl.conf -# === DAS HIER HAT GEFEHLT! === -# Kopiere den PHP/JS Quellcode fest in das Image und setze die Rechte COPY ./src /var/www/html/ RUN chown -R www-data:www-data /var/www/html/ -# ============================= -# Cronjob einrichten (Ruft die cron.php minütlich auf und leitet Output ins Docker-Log um) +# Der goldene Fix für die Datenbankrechte! +RUN mkdir -p /var/www/data && chown -R www-data:www-data /var/www/data + RUN echo "* * * * * root /usr/local/bin/php /var/www/html/cron.php > /proc/1/fd/1 2>/proc/1/fd/2\n" >> /etc/crontab -# Beide Ports freigeben EXPOSE 80 EXPOSE 443 -# Startbefehl: Startet erst den Cron-Daemon im Hintergrund und dann Apache im Vordergrund CMD cron && apache2-foreground \ No newline at end of file diff --git a/README.md b/README.md index 320edbc..c6ffd4c 100644 --- a/README.md +++ b/README.md @@ -38,32 +38,45 @@ Built entirely with native APIs—no slow iframes, no CORS issues. --- -## 📦 Quickstart (Docker) +## 📦 Quickstart (Docker & Portainer) -Deploying the Proxmox Unified Console is incredibly easy using Docker. +Die Installation erfolgt am einfachsten über Docker Compose. Die Applikation bringt einen eigenen Apache-Webserver mit und generiert sich automatisch ein SSL-Zertifikat für HTTPS. -1. **Clone the repository:** - ```bash - git clone [https://github.com/tessmania90/proxmox-unified-console.git](https://github.com/tessmania90/proxmox-unified-console.git) - cd proxmox-unified-console +**1. `docker-compose.yml` anlegen:** +```yaml +version: '3.8' - Start the container: - Bash +services: + pve-dashboard: + image: stessmann/proxmox-unified-console:latest + container_name: puc_dashboard + restart: unless-stopped + ports: + - "8080:80" + - "8443:443" + volumes: + - ./data:/var/www/data + environment: + - TZ=Europe/Berlin - docker compose up -d --build +2. Starten: +Bash - Access the Dashboard: - Open your browser and navigate to: - https://:8443 - (Note: You will need to accept the self-signed certificate warning). +docker compose up -d - Default Login: +3. ⚠️ WICHTIG: Rechte für SQLite anpassen: +Da der Container als Benutzer www-data (UID 33) läuft, der neu erstellte Volume-Ordner auf dem Host aber oft root gehört, muss dem Ordner die Schreibberechtigung erteilt werden. Führe im Verzeichnis der Compose-Datei aus: +Bash - Username: admin +sudo chown -R 33:33 ./data +sudo chmod -R 775 ./data - Password: admin +4. Login: +Rufe https://:8443 in deinem Browser auf (Zertifikatswarnung ignorieren). - ⚠️ IMPORTANT: Please change the default password immediately after your first login! + Benutzername: admin + + Passwort: admin (Bitte direkt nach dem Einloggen oben rechts über das 🔑-Symbol ändern!) 🛠️ Architecture diff --git a/docker-compose.yml b/docker-compose.yml index f4a17e2..fe907ac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,14 @@ -# /home/docker/pve_dashboard/docker-compose.yml version: '3.8' services: pve-dashboard: - build: . - container_name: pve_unified_console + image: stessmann/proxmox-unified-console:latest + container_name: puc_dashboard restart: unless-stopped ports: - # Port 80 bleibt fürs Testing / HAProxy - "8080:80" - # NEU: Port 443 für sicheres NoVNC! (Wir mappen es nach außen z.B. auf 8443) - - "8443:443" + - "8443:443" volumes: - # Dein Source-Code - - ./src:/var/www/html - # Deine Datenbank, sicher ausgelagert - ./data:/var/www/data environment: - - TZ=Europe/Berlin + - TZ=Europe/Berlin \ No newline at end of file diff --git a/src/api.php b/src/api.php index faa1ad7..c6c216c 100644 --- a/src/api.php +++ b/src/api.php @@ -1,13 +1,13 @@ prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)"); $stmt->execute([$userId, $username, $actionName, $target]); } catch (Throwable $e) { - // Fehler im Logbuch ignorieren, damit das Dashboard nicht crasht! error_log("Audit Log Fehler: " . $e->getMessage()); } } if ($action === 'login') { - $username = trim($_POST['username'] ?? ''); $password = $_POST['password'] ?? ''; - $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?"); $stmt->execute([$username]); $user = $stmt->fetch(PDO::FETCH_ASSOC); + $username = trim($_POST['username'] ?? ''); + $password = $_POST['password'] ?? ''; + $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?"); + $stmt->execute([$username]); + $user = $stmt->fetch(PDO::FETCH_ASSOC); + if ($user && password_verify($password, $user['password_hash'])) { - $_SESSION['user_id'] = $user['id']; $_SESSION['username'] = $user['username']; $_SESSION['role'] = $user['role']; + @session_start(); + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['role'] = $user['role']; + @session_write_close(); + $ip = $_SERVER['REMOTE_ADDR'] ?? 'Unknown'; logAudit('User Login', 'IP: ' . $ip); echo json_encode(['success' => true]); - } else echo json_encode(['success' => false, 'error' => 'Login fehlgeschlagen.']); + } else { + echo json_encode(['success' => false, 'error' => 'Login fehlgeschlagen.']); + } exit; } + if ($action === 'logout') { logAudit('User Logout', ''); - session_destroy(); echo json_encode(['success' => true]); exit; + @session_start(); + session_destroy(); + echo json_encode(['success' => true]); + exit; } -if (!isset($_SESSION['user_id'])) { echo json_encode(['success' => false, 'error' => 'Zugriff verweigert.']); exit; } +@session_start(); +if (!isset($_SESSION['user_id'])) { + echo json_encode(['success' => false, 'error' => 'Zugriff verweigert.']); + exit; +} // === PASSWORT ÄNDERN === if ($action === 'change_password') { @@ -68,30 +86,74 @@ if ($action === 'change_password') { $isAdmin = ($_SESSION['role'] ?? '') === 'admin'; @session_write_close(); -function getProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $type = 'pve', $method = "GET", $postData = null, $timeout = 4) { +// === PROXMOX API FETCHER (JETZT MIT TICKET-AUTH FÜR ALLE SYSTEME) === +function getProxmoxData($ip, $tokenId, $tokenSecret, $endpoint, $type = 'pve', $method = "GET", $postData = null, $timeout = 6) { $port = ($type === 'pbs') ? 8007 : 8006; - if ($type === 'pmg' || $type === 'pbs') { - $chAuth = curl_init("https://{$ip}:{$port}/api2/json/access/ticket"); - curl_setopt_array($chAuth, [CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_TIMEOUT => 2, CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query(['username' => $tokenId, 'password' => $tokenSecret])]); - $authRes = json_decode(curl_exec($chAuth), true); curl_close($chAuth); - if(!isset($authRes['data']['ticket'])) return ['data' => []]; - $cookieName = ($type === 'pbs') ? 'PBSAuthCookie' : 'PMGAuthCookie'; - $headers = ["Cookie: {$cookieName}=" . $authRes['data']['ticket'], "CSRFPreventionToken: " . $authRes['data']['CSRFPreventionToken']]; - } else { - $headers = ["Authorization: PVEAPIToken={$tokenId}={$tokenSecret}"]; + + // 1. Ticket holen (Login) + $chAuth = curl_init("https://{$ip}:{$port}/api2/json/access/ticket"); + curl_setopt_array($chAuth, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_TIMEOUT => 4, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query(['username' => $tokenId, 'password' => $tokenSecret]) + ]); + $authRes = json_decode(curl_exec($chAuth), true); + curl_close($chAuth); + + if(!isset($authRes['data']['ticket'])) { + return ['data' => null, 'error' => 'Authentifizierung fehlgeschlagen']; } + // 2. Passendes Cookie für das System wählen + $cookieName = 'PVEAuthCookie'; + if ($type === 'pbs') $cookieName = 'PBSAuthCookie'; + if ($type === 'pmg') $cookieName = 'PMGAuthCookie'; + + $headers = [ + "Cookie: {$cookieName}=" . $authRes['data']['ticket'], + "CSRFPreventionToken: " . $authRes['data']['CSRFPreventionToken'] + ]; + + // 3. Eigentlichen API-Call ausführen $ch = curl_init("https://{$ip}:{$port}{$endpoint}"); - $options = [CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_TIMEOUT => $timeout, CURLOPT_HTTPHEADER => $headers]; - if ($method !== "GET") { $options[CURLOPT_CUSTOMREQUEST] = $method; if ($postData) $options[CURLOPT_POSTFIELDS] = is_array($postData) ? http_build_query($postData) : $postData; } + $options = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_TIMEOUT => $timeout, + CURLOPT_HTTPHEADER => $headers + ]; + + if ($method !== "GET") { + $options[CURLOPT_CUSTOMREQUEST] = $method; + if ($postData) { + $options[CURLOPT_POSTFIELDS] = is_array($postData) ? http_build_query($postData) : $postData; + } + } + curl_setopt_array($ch, $options); - $res = curl_exec($ch); curl_close($ch); return json_decode($res, true); + $res = curl_exec($ch); + curl_close($ch); + + return json_decode($res, true) ?: []; } function checkVmPermission($pdo, $vmid) { - global $isAdmin; if ($isAdmin) return true; - @session_start(); $userId = $_SESSION['user_id'] ?? 0; @session_write_close(); - $stmt = $pdo->prepare("SELECT permissions FROM users WHERE id = ?"); $stmt->execute([$userId]); return in_array($vmid, json_decode($stmt->fetchColumn(), true)['allowed_vms'] ?? []); + global $isAdmin; + if ($isAdmin) return true; + + @session_start(); + $userId = $_SESSION['user_id'] ?? 0; + @session_write_close(); + + $stmt = $pdo->prepare("SELECT permissions FROM users WHERE id = ?"); + $stmt->execute([$userId]); + $perms = json_decode($stmt->fetchColumn() ?: '{}', true); + + return in_array($vmid, $perms['allowed_vms'] ?? []); } if ($action === 'get_audit_logs') { @@ -112,13 +174,22 @@ if ($action === 'get_recent_jobs') { if (isset($tasks['data'])) { foreach ($tasks['data'] as $t) { $t['node_name'] = $node['name']; $allTasks[] = $t; } } } elseif ($node['type'] === 'pmg') { $nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", 'pmg'); - if (isset($nData['data'][0]['node'])) { $tasks = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$nData['data'][0]['node']}/tasks?limit=10", 'pmg'); if (isset($tasks['data'])) { foreach ($tasks['data'] as $t) { $t['node_name'] = $node['name']; $allTasks[] = $t; } } } + if (isset($nData['data'][0]['node'])) { + $tasks = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$nData['data'][0]['node']}/tasks?limit=10", 'pmg'); + if (isset($tasks['data'])) { foreach ($tasks['data'] as $t) { $t['node_name'] = $node['name']; $allTasks[] = $t; } } + } } else { $nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", 'pve'); - if (isset($nData['data'])) { foreach ($nData['data'] as $nInfo) { $tasks = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$nInfo['node']}/tasks?limit=10", 'pve'); if (isset($tasks['data'])) { foreach ($tasks['data'] as $t) { $t['node_name'] = $node['name']; $allTasks[] = $t; } } } } + if (isset($nData['data'])) { + foreach ($nData['data'] as $nInfo) { + $tasks = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$nInfo['node']}/tasks?limit=10", 'pve'); + if (isset($tasks['data'])) { foreach ($tasks['data'] as $t) { $t['node_name'] = $node['name']; $allTasks[] = $t; } } + } + } } } - usort($allTasks, function($a, $b) { return ($b['starttime'] ?? 0) <=> ($a['starttime'] ?? 0); }); echo json_encode(['success' => true, 'data' => array_slice($allTasks, 0, 15)]); exit; + usort($allTasks, function($a, $b) { return ($b['starttime'] ?? 0) <=> ($a['starttime'] ?? 0); }); + echo json_encode(['success' => true, 'data' => array_slice($allTasks, 0, 15)]); exit; } if ($action === 'get_updates') { @@ -129,7 +200,12 @@ if ($action === 'get_updates') { if (isset($apt['data'])) { $c = count($apt['data']); $total += $c; if ($c > 0) $nodesNeed[] = $node['name'] . " (" . $c . ")"; } } else { $nData = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes", 'pve'); - if (isset($nData['data'])) { foreach ($nData['data'] as $nInfo) { $apt = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$nInfo['node']}/apt/update", 'pve'); if (isset($apt['data'])) { $c = count($apt['data']); $total += $c; if ($c > 0) $nodesNeed[] = $nInfo['node'] . " (" . $c . ")"; } } } + if (isset($nData['data'])) { + foreach ($nData['data'] as $nInfo) { + $apt = getProxmoxData($node['ip_address'], $node['token_id'], $node['token_secret'], "/api2/json/nodes/{$nInfo['node']}/apt/update", 'pve'); + if (isset($apt['data'])) { $c = count($apt['data']); $total += $c; if ($c > 0) $nodesNeed[] = $nInfo['node'] . " (" . $c . ")"; } + } + } } } echo json_encode(['success' => true, 'total' => $total, 'details' => implode(', ', $nodesNeed)]); exit; diff --git a/src/app.js b/src/app.js index 802631b..bfea446 100644 --- a/src/app.js +++ b/src/app.js @@ -11,7 +11,10 @@ if (!window.APP.isLoggedIn) { if(setupForm) { setupForm.addEventListener('submit', async function(e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const oTxt = btn.innerText; btn.innerText = 'Verbinde...'; const fd = new FormData(); fd.append('name', document.getElementById('nodeName').value); fd.append('ip', document.getElementById('nodeIp').value); fd.append('user', document.getElementById('nodeUser').value); fd.append('pass', document.getElementById('nodePass').value); fd.append('type', 'pve'); try { const res = await (await fetch('api.php?action=add_node', { method: 'POST', body: fd })).json(); if(res.success) { btn.innerText = 'Erfolgreich!'; setTimeout(() => window.location.reload(), 1000); } else { alert(res.error); btn.innerText = oTxt; } } catch (e) { alert('Netzwerkfehler.'); btn.innerText = oTxt; } }); } } -async function logout() { await fetch('api.php?action=logout'); window.location.reload(); } +async function logout() { + await fetch('api.php?action=logout'); + window.location.href = window.location.pathname + '?t=' + Date.now(); +} if (window.APP.isLoggedIn && window.APP.nodeCount > 0) { diff --git a/src/index.php b/src/index.php index 38d227c..e6a5fb0 100644 --- a/src/index.php +++ b/src/index.php @@ -39,11 +39,9 @@ $nodeCount = $stmt->fetchColumn();
Hallo, - -
@@ -132,6 +130,8 @@ $nodeCount = $stmt->fetchColumn(); - + + + \ No newline at end of file