8 Commits
5 changed files with 52 additions and 16 deletions
+6
View File
@@ -45,3 +45,9 @@ Alle Einträge verwenden ISO-8601-Timestamps in UTC. Mehrere Änderungen desselb
- Root-Layout des Git-Repositories hergestellt; die Anwendung liegt direkt im Repository-Root. - Root-Layout des Git-Repositories hergestellt; die Anwendung liegt direkt im Repository-Root.
- Beispielkonfiguration und Sicherheitsdokumentation bereitgestellt. - Beispielkonfiguration und Sicherheitsdokumentation bereitgestellt.
- Browserinstaller korrigiert: Der initiale Administrator wird jetzt mit allen im `users`-Schema erforderlichen Pflichtfeldern angelegt. - Browserinstaller korrigiert: Der initiale Administrator wird jetzt mit allen im `users`-Schema erforderlichen Pflichtfeldern angelegt.
- Realm-Dashboard korrigiert: Es fragt nur noch die konfigurierte Portainer-Umgebung ab, benötigt keine globale Endpoint-Liste oder Endpoint-Metadaten mehr und verwendet direkt den Docker-Proxy.
- Fehlgeschlagene, bereits bezahlte Provisionierungen können durch Administratoren erneut gestartet werden.
- Administratoren erhalten bei fehlgeschlagenen Instanzaktionen einen bereinigten technischen Diagnosehinweis inklusive cURL-Fehlerdetails bei Verbindungsproblemen.
- Löschaktionen verwenden jetzt den korrekten Docker-API-Endpunkt mit `force=true`.
- Als gelöscht markierte Instanzen werden nicht mehr im Dashboard angezeigt; die Datenbankhistorie bleibt erhalten.
- Tarife zeigen Brutto-Monats- und Jahrespreise; die Jahrespreise werden mit Rabatt gespeichert. Tarif S umfasst jetzt 5 GB Speicher.
+33 -14
View File
@@ -34,12 +34,13 @@ $message = null;
$error = null; $error = null;
$plans = [ $plans = [
's' => ['label' => 'S', 'memory' => 512, 'cpu' => 1.00, 'storage' => 1024], 's' => ['label' => 'S', 'memory' => 512, 'cpu' => 1.00, 'storage' => 5120, 'monthly_cents' => 299, 'yearly_cents' => 2999],
'm' => ['label' => 'M', 'memory' => 2048, 'cpu' => 2.00, 'storage' => 10240], 'm' => ['label' => 'M', 'memory' => 2048, 'cpu' => 2.00, 'storage' => 10240, 'monthly_cents' => 399, 'yearly_cents' => 3999],
'l' => ['label' => 'L', 'memory' => 4096, 'cpu' => 4.00, 'storage' => 25600], 'l' => ['label' => 'L', 'memory' => 4096, 'cpu' => 4.00, 'storage' => 25600, 'monthly_cents' => 499, 'yearly_cents' => 4999],
]; ];
function dashboard_escape(string $value): string { return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); } function dashboard_escape(string $value): string { return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); }
function dashboard_money(int $cents): string { return $cents > 0 ? number_format($cents / 100, 2, ',', '.') . ' €' : ''; }
function dashboard_status(string $status): string { function dashboard_status(string $status): string {
return match ($status) { return match ($status) {
'pending_payment' => 'Zahlung ausstehend', 'pending_payment' => 'Zahlung ausstehend',
@@ -50,6 +51,12 @@ function dashboard_status(string $status): string {
default => ucfirst($status), default => ucfirst($status),
}; };
} }
function dashboard_debug_detail(Throwable $exception): string {
$detail = trim($exception->getMessage());
$detail = preg_replace('/(?:X-API-Key|Authorization|Bearer)\s*:\s*[^\s,;]+/i', '[credential redacted]', $detail) ?? $detail;
$detail = preg_replace('/("?(?:token|password|api[_-]?key|secret)"?\s*[:=]\s*)[^,;\s}]+/i', '$1[redacted]', $detail) ?? $detail;
return substr($detail !== '' ? $detail : 'Keine technische Fehlerbeschreibung vorhanden.', 0, 800);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!pnpaas_valid_csrf((string)($_POST['csrf_token'] ?? ''))) { if (!pnpaas_valid_csrf((string)($_POST['csrf_token'] ?? ''))) {
@@ -61,11 +68,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($action === 'request_instance') { if ($action === 'request_instance') {
$name = trim((string)($_POST['name'] ?? '')); $name = trim((string)($_POST['name'] ?? ''));
$planKey = strtolower(trim((string)($_POST['plan'] ?? ''))); $planKey = strtolower(trim((string)($_POST['plan'] ?? '')));
$billingPeriod = (string)($_POST['billing_period'] ?? '');
$paymentMethod = (string)($_POST['payment_method'] ?? 'bank_transfer'); $paymentMethod = (string)($_POST['payment_method'] ?? 'bank_transfer');
if ($name === '' || !preg_match('/^[A-Za-z0-9][A-Za-z0-9 _.-]{1,119}$/', $name)) { if ($name === '' || !preg_match('/^[A-Za-z0-9][A-Za-z0-9 _.-]{1,119}$/', $name)) {
throw new RuntimeException('Bitte einen gültigen Instanznamen eingeben.'); throw new RuntimeException('Bitte einen gültigen Instanznamen eingeben.');
} }
if (!isset($plans[$planKey]) || !in_array($paymentMethod, ['paypal', 'bank_transfer'], true)) { if (!isset($plans[$planKey]) || !in_array($billingPeriod, ['monthly', 'yearly'], true) || !in_array($paymentMethod, ['paypal', 'bank_transfer'], true)) {
throw new RuntimeException('Tarif oder Zahlungsart ist ungültig.'); throw new RuntimeException('Tarif oder Zahlungsart ist ungültig.');
} }
$slug = strtolower(preg_replace('/[^a-z0-9]+/i', '-', $name) ?? 'foundry'); $slug = strtolower(preg_replace('/[^a-z0-9]+/i', '-', $name) ?? 'foundry');
@@ -73,12 +81,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$port = (int)$db->query('SELECT COALESCE(MAX(port), 29999) + 1 FROM instances')->fetchColumn(); $port = (int)$db->query('SELECT COALESCE(MAX(port), 29999) + 1 FROM instances')->fetchColumn();
if ($port > 60000) throw new RuntimeException('Es sind derzeit keine Ports verfügbar.'); if ($port > 60000) throw new RuntimeException('Es sind derzeit keine Ports verfügbar.');
$p = $plans[$planKey]; $p = $plans[$planKey];
$statement = $db->prepare('INSERT INTO instances (owner_user_id, name, plan, payment_status, payment_method, container_name, port, volume_size_mb, memory_limit_mb, cpu_limit, status) VALUES (:owner, :name, :plan, "pending", :method, :container, :port, :storage, :memory, :cpu, "pending_payment")'); $statement = $db->prepare('INSERT INTO instances (owner_user_id, name, plan, billing_period, price_gross_cents, payment_status, payment_method, container_name, port, volume_size_mb, memory_limit_mb, cpu_limit, status) VALUES (:owner, :name, :plan, :billing_period, :price_gross_cents, "pending", :method, :container, :port, :storage, :memory, :cpu, "pending_payment")');
$statement->execute(['owner' => $userId, 'name' => $name, 'plan' => $planKey, 'method' => $paymentMethod, 'container' => $containerName, 'port' => $port, 'storage' => $p['storage'], 'memory' => $p['memory'], 'cpu' => $p['cpu']]); $statement->execute(['owner' => $userId, 'name' => $name, 'plan' => $planKey, 'billing_period' => $billingPeriod, 'price_gross_cents' => $p[$billingPeriod . '_cents'], 'method' => $paymentMethod, 'container' => $containerName, 'port' => $port, 'storage' => $p['storage'], 'memory' => $p['memory'], 'cpu' => $p['cpu']]);
$message = 'Die Instanzanfrage wurde erfasst und wartet auf die Zahlungsfreigabe.'; $message = 'Die Instanzanfrage wurde erfasst und wartet auf die Zahlungsfreigabe.';
} elseif ($action === 'approve_instance' && $isAdmin) { } elseif ($action === 'approve_instance' && $isAdmin) {
$instanceId = (int)($_POST['instance_id'] ?? 0); $instanceId = (int)($_POST['instance_id'] ?? 0);
$query = $db->prepare('SELECT * FROM instances WHERE id = :id AND status = "pending_payment" LIMIT 1'); $query = $db->prepare('SELECT * FROM instances WHERE id = :id AND ((status = "pending_payment") OR (status = "error" AND payment_status = "paid")) LIMIT 1');
$query->execute(['id' => $instanceId]); $query->execute(['id' => $instanceId]);
$instance = $query->fetch(); $instance = $query->fetch();
if (!$instance) throw new RuntimeException('Diese Instanzanfrage ist nicht mehr freigabefähig.'); if (!$instance) throw new RuntimeException('Diese Instanzanfrage ist nicht mehr freigabefähig.');
@@ -144,7 +152,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$db->prepare('UPDATE instances SET portainer_container_id = :cid, status = "running", error_message = NULL WHERE id = :id')->execute(['cid' => $newContainerId, 'id' => $instanceId]); $db->prepare('UPDATE instances SET portainer_container_id = :cid, status = "running", error_message = NULL WHERE id = :id')->execute(['cid' => $newContainerId, 'id' => $instanceId]);
$message = 'Die Instanz wurde neu bereitgestellt. Die Daten in /data wurden beibehalten.'; $message = 'Die Instanz wurde neu bereitgestellt. Die Daten in /data wurden beibehalten.';
} else { } else {
$apiPath = '/api/endpoints/' . $endpoint . '/docker/containers/' . $containerId . '/' . $operation; $apiPath = $operation === 'delete'
? '/api/endpoints/' . $endpoint . '/docker/containers/' . $containerId . '?force=true'
: '/api/endpoints/' . $endpoint . '/docker/containers/' . $containerId . '/' . $operation;
pnpaas_portainer_request($apiPath, $operation === 'delete' ? 'DELETE' : 'POST', []); pnpaas_portainer_request($apiPath, $operation === 'delete' ? 'DELETE' : 'POST', []);
$newStatus = match ($operation) { $newStatus = match ($operation) {
'start', 'restart' => 'running', 'start', 'restart' => 'running',
@@ -164,8 +174,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (in_array(($action ?? ''), ['approve_instance', 'instance_action'], true) && isset($instanceId)) { if (in_array(($action ?? ''), ['approve_instance', 'instance_action'], true) && isset($instanceId)) {
try { pnpaas_db()->prepare('UPDATE instances SET status = "error", error_message = :error WHERE id = :id')->execute(['error' => 'Die Bereitstellung oder Aktion konnte nicht abgeschlossen werden.', 'id' => $instanceId]); } catch (Throwable) {} try { pnpaas_db()->prepare('UPDATE instances SET status = "error", error_message = :error WHERE id = :id')->execute(['error' => 'Die Bereitstellung oder Aktion konnte nicht abgeschlossen werden.', 'id' => $instanceId]); } catch (Throwable) {}
} }
error_log('PnPaaS instance action error: ' . $exception->getMessage()); $debugDetail = dashboard_debug_detail($exception);
error_log('PnPaaS instance action error: ' . $debugDetail);
$error = 'Die angeforderte Aktion konnte nicht ausgeführt werden. Bitte versuchen Sie es erneut.'; $error = 'Die angeforderte Aktion konnte nicht ausgeführt werden. Bitte versuchen Sie es erneut.';
if ($isAdmin) $error .= ' Technischer Hinweis: ' . $debugDetail;
} }
} }
} }
@@ -180,7 +192,14 @@ $realmError = null;
if ($isAdmin && !$showInstances) { if ($isAdmin && !$showInstances) {
try { try {
$endpoints = pnpaas_portainer_request('/api/endpoints'); $configuredEndpointId = pnpaas_portainer_endpoint_id();
// Die Endpoint-Metadaten sind für eingeschränkte Portainer-Benutzer nicht immer lesbar.
// Die konfigurierte ID reicht aus; der eigentliche Zugriff erfolgt über den Docker-Proxy.
$endpoints = [[
'Id' => $configuredEndpointId,
'Name' => 'Portainer-Umgebung ' . $configuredEndpointId,
'Status' => 1,
]];
foreach ($endpoints as $endpoint) { foreach ($endpoints as $endpoint) {
$endpointId = (int)($endpoint['Id'] ?? 0); $endpointId = (int)($endpoint['Id'] ?? 0);
if ($endpointId < 1) continue; if ($endpointId < 1) continue;
@@ -236,8 +255,8 @@ if ($isAdmin && !$showInstances) {
if ($showInstances) { if ($showInstances) {
$statement = $isAdmin $statement = $isAdmin
? pnpaas_db()->query('SELECT i.*, u.username FROM instances i JOIN users u ON u.id = i.owner_user_id ORDER BY i.created_at DESC') ? pnpaas_db()->query('SELECT i.*, u.username FROM instances i JOIN users u ON u.id = i.owner_user_id WHERE i.status <> "deleted" ORDER BY i.created_at DESC')
: (function () use ($userId) { $s = pnpaas_db()->prepare('SELECT i.*, u.username FROM instances i JOIN users u ON u.id = i.owner_user_id WHERE i.owner_user_id = :owner ORDER BY i.created_at DESC'); $s->execute(['owner' => $userId]); return $s; })(); : (function () use ($userId) { $s = pnpaas_db()->prepare('SELECT i.*, u.username FROM instances i JOIN users u ON u.id = i.owner_user_id WHERE i.owner_user_id = :owner AND i.status <> "deleted" ORDER BY i.created_at DESC'); $s->execute(['owner' => $userId]); return $s; })();
$instances = $statement->fetchAll(); $instances = $statement->fetchAll();
} }
@@ -284,7 +303,7 @@ function dashboard_bytes(int $bytes): string {
<?php if ($message !== null): ?><div class="alert alert-success" role="status"><?= dashboard_escape($message) ?></div><?php endif; ?> <?php if ($message !== null): ?><div class="alert alert-success" role="status"><?= dashboard_escape($message) ?></div><?php endif; ?>
<?php if ($error !== null): ?><div class="alert alert-error" role="alert"><?= dashboard_escape($error) ?></div><?php endif; ?> <?php if ($error !== null): ?><div class="alert alert-error" role="alert"><?= dashboard_escape($error) ?></div><?php endif; ?>
<section class="data-section"><div class="section-heading"><div><p class="eyebrow">Neue Buchung</p><h2>Instanz anfragen</h2></div><span class="status-badge">S · M · L</span></div> <section class="data-section"><div class="section-heading"><div><p class="eyebrow">Neue Buchung</p><h2>Instanz anfragen</h2></div><span class="status-badge">S · M · L</span></div>
<form method="post" class="login-form"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="request_instance"><label for="name">Instanzname</label><input id="name" name="name" required maxlength="120" placeholder="Meine Spielrunde"><label for="plan">Tarif</label><select id="plan" name="plan"><option value="s">S 512 MB RAM · 1 CPU · 1 GB</option><option value="m">M 2 GB RAM · 2 CPU · 10 GB</option><option value="l">L 4 GB RAM · 4 CPU · 25 GB</option></select><label for="payment_method">Zahlungsart</label><select id="payment_method" name="payment_method"><option value="paypal">PayPal</option><option value="bank_transfer">Überweisung</option></select><button type="submit">Instanz anfragen</button></form></section> <form method="post" class="login-form"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="request_instance"><label for="name">Instanzname</label><input id="name" name="name" required maxlength="120" placeholder="Meine Spielrunde"><label for="billing_period">Abrechnung</label><select id="billing_period" name="billing_period"><option value="monthly">Monatlich</option><option value="yearly">Jährlich (Rabatt)</option></select><label for="plan">Tarif</label><select id="plan" name="plan"><option value="s">S 2,99 € monatlich / 29,99 € jährlich · 512 MB RAM · 1 CPU · 5 GB</option><option value="m">M 3,99 € monatlich / 39,99 € jährlich · 2 GB RAM · 2 CPU · 10 GB</option><option value="l">L 4,99 € monatlich / 49,99 € jährlich · 4 GB RAM · 4 CPU · 25 GB</option></select><label for="payment_method">Zahlungsart</label><select id="payment_method" name="payment_method"><option value="paypal">PayPal</option><option value="bank_transfer">Überweisung</option></select><button type="submit">Instanz anfragen</button></form></section>
<section class="data-section"><div class="section-heading"><div><p class="eyebrow">Provisionierung</p><h2>Instanzen</h2></div><?php if ($isAdmin): ?><a href="dashboard.php">← Realms</a><?php endif; ?></div> <section class="data-section"><div class="section-heading"><div><p class="eyebrow">Provisionierung</p><h2>Instanzen</h2></div><?php if ($isAdmin): ?><a href="dashboard.php">← Realms</a><?php endif; ?></div>
<?php if ($instances === []): ?><div class="empty-state">Noch keine Instanzen vorhanden.</div><?php else: ?><div class="table-wrap"><table class="data-table"><thead><tr><th>Name</th><?php if ($isAdmin): ?><th>Benutzer</th><?php endif; ?><th>Tarif</th><th>Zahlung</th><th>Status</th><th>Adresse</th><th>Aktion</th></tr></thead><tbody><?php foreach ($instances as $instance): ?><tr><td data-label="Name"><strong><?= dashboard_escape((string)$instance['name']) ?></strong></td><?php if ($isAdmin): ?><td data-label="Benutzer"><?= dashboard_escape((string)$instance['username']) ?></td><?php endif; ?><td data-label="Tarif"><?= strtoupper(dashboard_escape((string)$instance['plan'])) ?></td><td data-label="Zahlung"><?= dashboard_escape((string)$instance['payment_status']) ?></td><td data-label="Status"><span class="container-state <?= dashboard_escape((string)$instance['status']) ?>"><?= dashboard_escape(dashboard_status((string)$instance['status'])) ?></span><?php if (!empty($instance['error_message'])): ?><br><small><?= dashboard_escape((string)$instance['error_message']) ?></small><?php endif; ?></td><td data-label="Adresse"><?php if ($instance['status'] === 'running'): ?>:<?= dashboard_escape((string)$instance['port']) ?><?php else: ?><?php endif; ?></td><td data-label="Aktion"><?php if ($instance['status'] === 'pending_payment' && $isAdmin): ?><form method="post"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="approve_instance"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit">Zahlung freigeben</button></form><?php elseif (in_array($instance['status'], ['running', 'stopped'], true)): ?><div class="instance-actions"><form method="post" onsubmit="return confirm('Instanz wirklich neu bereitstellen? Die Daten in /data bleiben erhalten.');"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="instance_action"><input type="hidden" name="operation" value="redeploy"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit">Redeploy</button></form><?php if ($instance['status'] === 'running'): ?><form method="post"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="instance_action"><input type="hidden" name="operation" value="stop"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit">Stoppen</button></form><?php else: ?><form method="post"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="instance_action"><input type="hidden" name="operation" value="start"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit">Starten</button></form><?php endif; ?><form method="post"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="instance_action"><input type="hidden" name="operation" value="restart"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit">Neustarten</button></form><?php if ($isAdmin): ?><form method="post" onsubmit="return confirm('Instanz wirklich löschen? Das Datenverzeichnis bleibt erhalten.');"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="instance_action"><input type="hidden" name="operation" value="delete"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit">Löschen</button></form><?php endif; ?></div><?php else: ?><?php endif; ?></td></tr><?php endforeach; ?></tbody></table></div><?php endif; ?></section> <?php if ($instances === []): ?><div class="empty-state">Noch keine Instanzen vorhanden.</div><?php else: ?><div class="table-wrap"><table class="data-table"><thead><tr><th>Name</th><?php if ($isAdmin): ?><th>Benutzer</th><?php endif; ?><th>Tarif</th><th>Preis</th><th>Zahlung</th><th>Status</th><th>Adresse</th><th>Aktion</th></tr></thead><tbody><?php foreach ($instances as $instance): ?><tr><td data-label="Name"><strong><?= dashboard_escape((string)$instance['name']) ?></strong></td><?php if ($isAdmin): ?><td data-label="Benutzer"><?= dashboard_escape((string)$instance['username']) ?></td><?php endif; ?><td data-label="Tarif"><?= strtoupper(dashboard_escape((string)$instance['plan'])) ?></td><td data-label="Preis"><?= dashboard_escape(dashboard_money((int)($instance['price_gross_cents'] ?? 0))) ?> (<?= (string)($instance['billing_period'] ?? 'monthly') === 'yearly' ? 'jährlich' : 'monatlich' ?>)</td><td data-label="Zahlung"><?= dashboard_escape((string)$instance['payment_status']) ?></td><td data-label="Status"><span class="container-state <?= dashboard_escape((string)$instance['status']) ?>"><?= dashboard_escape(dashboard_status((string)$instance['status'])) ?></span><?php if (!empty($instance['error_message'])): ?><br><small><?= dashboard_escape((string)$instance['error_message']) ?></small><?php endif; ?></td><td data-label="Adresse"><?php if ($instance['status'] === 'running'): ?>:<?= dashboard_escape((string)$instance['port']) ?><?php else: ?><?php endif; ?></td><td data-label="Aktion"><?php if (in_array((string)$instance['status'], ['pending_payment', 'error'], true) && $isAdmin && ($instance['status'] === 'pending_payment' || $instance['payment_status'] === 'paid')): ?><form method="post"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="approve_instance"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit"><?= $instance['status'] === 'error' ? 'Erneut provisionieren' : 'Zahlung freigeben' ?></button></form><?php elseif (in_array($instance['status'], ['running', 'stopped'], true)): ?><div class="instance-actions"><form method="post" onsubmit="return confirm('Instanz wirklich neu bereitstellen? Die Daten in /data bleiben erhalten.');"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="instance_action"><input type="hidden" name="operation" value="redeploy"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit">Redeploy</button></form><?php if ($instance['status'] === 'running'): ?><form method="post"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="instance_action"><input type="hidden" name="operation" value="stop"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit">Stoppen</button></form><?php else: ?><form method="post"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="instance_action"><input type="hidden" name="operation" value="start"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit">Starten</button></form><?php endif; ?><form method="post"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="instance_action"><input type="hidden" name="operation" value="restart"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit">Neustarten</button></form><?php if ($isAdmin): ?><form method="post" onsubmit="return confirm('Instanz wirklich löschen? Das Datenverzeichnis bleibt erhalten.');"><input type="hidden" name="csrf_token" value="<?= dashboard_escape($csrf) ?>"><input type="hidden" name="action" value="instance_action"><input type="hidden" name="operation" value="delete"><input type="hidden" name="instance_id" value="<?= (int)$instance['id'] ?>"><button type="submit">Löschen</button></form><?php endif; ?></div><?php else: ?><?php endif; ?></td></tr><?php endforeach; ?></tbody></table></div><?php endif; ?></section>
<?php endif; ?></main></body></html> <?php endif; ?></main></body></html>
@@ -0,0 +1,4 @@
-- Add billing period and gross price snapshot to existing instances.
ALTER TABLE instances
ADD COLUMN IF NOT EXISTS billing_period ENUM('monthly', 'yearly') NOT NULL DEFAULT 'monthly' AFTER plan,
ADD COLUMN IF NOT EXISTS price_gross_cents INT UNSIGNED NOT NULL DEFAULT 0 AFTER billing_period;
+2
View File
@@ -72,6 +72,8 @@ CREATE TABLE IF NOT EXISTS instances (
owner_user_id BIGINT UNSIGNED NOT NULL, owner_user_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(120) NOT NULL, name VARCHAR(120) NOT NULL,
plan ENUM('s', 'm', 'l') NOT NULL DEFAULT 's', plan ENUM('s', 'm', 'l') NOT NULL DEFAULT 's',
billing_period ENUM('monthly', 'yearly') NOT NULL DEFAULT 'monthly',
price_gross_cents INT UNSIGNED NOT NULL DEFAULT 0,
payment_status ENUM('pending', 'paid', 'rejected') NOT NULL DEFAULT 'pending', payment_status ENUM('pending', 'paid', 'rejected') NOT NULL DEFAULT 'pending',
payment_method ENUM('paypal', 'bank_transfer', 'manual') NULL, payment_method ENUM('paypal', 'bank_transfer', 'manual') NULL,
requested_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, requested_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
+7 -2
View File
@@ -195,10 +195,15 @@ function pnpaas_portainer_request(string $path, string $method = 'GET', ?array $
curl_close($handle); curl_close($handle);
if ($body === false || $error !== '') { if ($body === false || $error !== '') {
throw new RuntimeException('Portainer ist nicht erreichbar.'); $detail = trim($error);
$detail = preg_replace('/(?:X-API-Key|Authorization|Bearer)\s*:\s*[^\s,;]+/i', '[credential redacted]', $detail) ?? $detail;
throw new RuntimeException('Portainer ist nicht erreichbar' . ($detail !== '' ? ': ' . substr($detail, 0, 400) : '.'));
} }
if ($status < 200 || ($status >= 300 && $status !== 304)) { if ($status < 200 || ($status >= 300 && $status !== 304)) {
throw new RuntimeException('Portainer hat die Anfrage abgelehnt.'); $detail = trim(preg_replace('/\s+/', ' ', (string)$body) ?? '');
$detail = preg_replace('/("?(?:token|password|api[_-]?key|secret)"?\s*:\s*)"[^"]*"/i', '$1"[redacted]"', $detail) ?? $detail;
$detail = substr($detail, 0, 600);
throw new RuntimeException('Portainer HTTP ' . $status . ($detail !== '' ? ': ' . $detail : '.'));
} }
if ($body === '' || $body === null) { if ($body === '' || $body === null) {
return []; return [];