@@ -2,6 +2,7 @@
declare ( strict_types = 1 );
require_once __DIR__ . '/includes/bootstrap.php' ;
require_once __DIR__ . '/includes/traefik.php' ;
pnpaas_session_start ();
if ( empty ( $_SESSION [ 'pnpaas_user_id' ])) {
@@ -67,6 +68,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$db = pnpaas_db ();
if ( $action === 'request_instance' ) {
$name = trim (( string )( $_POST [ 'name' ] ? ? '' ));
$customDomain = strtolower ( trim (( string )( $_POST [ 'custom_domain' ] ? ? '' )));
$planKey = strtolower ( trim (( string )( $_POST [ 'plan' ] ? ? '' )));
$billingPeriod = ( string )( $_POST [ 'billing_period' ] ? ? '' );
$paymentMethod = ( string )( $_POST [ 'payment_method' ] ? ? 'bank_transfer' );
@@ -78,11 +80,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
$slug = strtolower ( preg_replace ( '/[^a-z0-9]+/i' , '-' , $name ) ? ? 'foundry' );
$containerName = 'pnpaas-' . trim ( $slug , '-' ) . '-' . bin2hex ( random_bytes ( 3 ));
pnpaas_traefik_labels ( $customDomain !== '' ? $customDomain : null , $containerName );
$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.' );
$p = $plans [ $planKey ];
$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 , 'billing_period' => $billingPeriod , 'price_gross_cents' => $p [ $billingPeriod . '_cents' ], 'method' => $paymentMethod , 'container' => $containerName , 'port' => $port , 'storage' => $p [ 'storage' ], 'memory' => $p [ 'memory' ], 'cpu' => $p [ 'cpu' ]]);
$statement = $db -> prepare ( 'INSERT INTO instances (owner_user_id, name, custom_domain, 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, :custom_domain, :plan, :billing_period, :price_gross_cents, "pending", :method, :container, :port, :storage, :memory, :cpu, "pending_payment")' );
$statement -> execute ([ 'owner' => $userId , 'name' => $name , 'custom_domain' => $customDomain !== '' ? $customDomain : null , ' 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.' ;
} elseif ( $action === 'approve_instance' && $isAdmin ) {
$instanceId = ( int )( $_POST [ 'instance_id' ] ? ? 0 );
@@ -91,12 +94,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$instance = $query -> fetch ();
if ( ! $instance ) throw new RuntimeException ( 'Diese Instanzanfrage ist nicht mehr freigabefähig.' );
$db -> prepare ( 'UPDATE instances SET payment_status = "paid", paid_at = CURRENT_TIMESTAMP, status = "provisioning", error_message = NULL WHERE id = :id' ) -> execute ([ 'id' => $instanceId ]);
$endpoint = pnpaas_portainer_endpoint_id ();
$dataPath = '/opt/foundry/instances/' . $instance [ 'container_name' ];
$labels = pnpaas_traefik_labels ( $instance [ 'custom_domain' ] ? ? null , ( string ) $instance [ 'container_name' ]);
$payload = [
'Image' => 'pnpaas/foundryvtt:14.368' ,
'Env' => [ 'FOUNDRY_VERSION=14.368' ],
'ExposedPorts' => [ '30000/tcp' => new stdClass ()],
'Labels' => $labels ,
'HostConfig' => [
'Binds' => [ $dataPath . ':/data' ],
'PortBindings' => [ '30000/tcp' => [[ 'HostPort' => ( string ) $instance [ 'port' ]]]],
@@ -105,6 +109,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'RestartPolicy' => [ 'Name' => 'unless-stopped' ],
],
];
if ( $labels !== []) $payload [ 'NetworkingConfig' ] = [ 'EndpointsConfig' => [ 'traefik-public' => new stdClass ()]];
$created = pnpaas_portainer_request ( '/api/endpoints/' . $endpoint . '/docker/containers/create?name=' . rawurlencode ( $instance [ 'container_name' ]), 'POST' , $payload );
$containerId = ( string )( $created [ 'Id' ] ? ? '' );
if ( $containerId === '' ) throw new RuntimeException ( 'Portainer hat keine Container-ID geliefert.' );
@@ -133,10 +138,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$db -> prepare ( 'UPDATE instances SET status = "provisioning", error_message = NULL WHERE id = :id' ) -> execute ([ 'id' => $instanceId ]);
pnpaas_portainer_request ( '/api/endpoints/' . $endpoint . '/docker/containers/' . $containerId . '?force=true' , 'DELETE' , null );
$dataPath = '/opt/foundry/instances/' . $instance [ 'container_name' ];
$labels = pnpaas_traefik_labels ( $instance [ 'custom_domain' ] ? ? null , ( string ) $instance [ 'container_name' ]);
$payload = [
'Image' => 'pnpaas/foundryvtt:14.368' ,
'Env' => [ 'FOUNDRY_VERSION=14.368' ],
'ExposedPorts' => [ '30000/tcp' => new stdClass ()],
'Labels' => $labels ,
'HostConfig' => [
'Binds' => [ $dataPath . ':/data' ],
'PortBindings' => [ '30000/tcp' => [[ 'HostPort' => ( string ) $instance [ 'port' ]]]],
@@ -145,6 +152,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'RestartPolicy' => [ 'Name' => 'unless-stopped' ],
],
];
if ( $labels !== []) $payload [ 'NetworkingConfig' ] = [ 'EndpointsConfig' => [ 'traefik-public' => new stdClass ()]];
$created = pnpaas_portainer_request ( '/api/endpoints/' . $endpoint . '/docker/containers/create?name=' . rawurlencode ( $instance [ 'container_name' ]), 'POST' , $payload );
$newContainerId = ( string )( $created [ 'Id' ] ? ? '' );
if ( $newContainerId === '' ) throw new RuntimeException ( 'Portainer hat keine neue Container-ID geliefert.' );
@@ -303,7 +311,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 ( $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 >
< 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 >
< 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 = " custom_domain " > Kundeneigene Domain ( optional ) </ label >< input id = " custom_domain " name = " custom_domain " type = " text " maxlength = " 253 " placeholder = " foundry.example.de " autocomplete = " url " >< 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>
< ? 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 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'): ?><?= !empty($instance['custom_domain']) ? dashboard_escape((string)$instance['custom_domain']) : ':' . 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>