+ = h($size['label']) ?> VM
+-
+
- = h((string)$size['vcpus']) ?> vCPU= $size['vcpus'] > 1 ? 's' : '' ?> +
- = h((string)$size['ram_gb']) ?> GB RAM +
- = h((string)$size['disk_gb']) ?> GB Disk +
diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..1529c33 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +config.local.php +.env +.e2e-cookies.txt +.my.cnf.* +.my.output.* +.mysql.* +.pi/ +.ralph/ +AGENTS.md +context.md +analyze-phpsite.sh +test-pi-combo.py +test-pi-rpc.py +setup_database.sql.backup diff --git a/README.md b/README.md old mode 100644 new mode 100755 index f3e0f01..eba2b49 --- a/README.md +++ b/README.md @@ -1 +1,71 @@ -# phpsite \ No newline at end of file +# phpsite + +VM request webshop. Users browse VM configurations (Small / Medium / Big), pick one, fill in details, and submit a request. Completed requests are registered in NetBox as virtual machines. + +Tech stack: plain PHP, MariaDB, Nginx + PHP-FPM, optional NetBox integration. + +## File layout + +``` +├── index.php # Shop home — product cards for each VM size +├── create.php # Order form (pre-filled with chosen size) +├── requests.php # My Requests — history table with pagination +├── send.php # POST handler: validate, insert, call NetBox +├── cancel.php # Cancel a queued request +├── login.php # Sign-in page (database auth) +├── logout.php # Session destroy + redirect +├── config.php # Shared helpers: db(), require_login(), h(), CSRF +├── sizes.php # VM size definitions + allowed domains +├── style.css # All CSS (Inter font, dark-mode, webshop styles) +├── health.php # Health check endpoint (returns JSON) +├── setup_database.sql # MariaDB schema (users + vm_requests tables) +├── deploy-local.sh # One-shot local deployment script +├── test-e2e.sh # End-to-end smoke test +├── vm_images/ # Auto-generated product-card images +├── netbox_integration/ # NetBox API client for virtualization VMs +│ ├── config.php # Load .env token, build client config +│ └── netbox_api.php # NetBoxClient class (createVirtualMachine, etc.) +└── .gitignore +``` + +## Deploy locally + +```sh +./deploy-local.sh +``` + +Creates the MariaDB database/user, writes `config.local.php`, imports the schema, +seeds an admin user, and enables Nginx + PHP-FPM. + +**Default local login:** `admin` / `admin123` + +## Health check + +```sh +curl http://127.0.0.1/health.php +``` + +Returns JSON with `"status": "ok"` and `"database": "connected"` on success. + +## End-to-end test + +```sh +./test-e2e.sh +PHPSITE_URL=http://127.0.0.1:8000 ./test-e2e.sh +``` + +Checks PHP syntax, signs in, submits a VM request, and verifies it appears. + +## NetBox integration + +When a request is submitted, `send.php` calls NetBox (`/virtualization/virtual-machines/`) +to create a virtual machine entry. The VM is tagged with the chosen domain and attached to the +first available cluster. + +To enable: + +1. Put your NetBox API token in `.env` (or set `NETBOX_API_TOKEN` env var) +2. Ensure a cluster exists in NetBox (the app looks for `proxmox-cluster`) +3. Ensure tags exist for each domain (`domain1`, `domain2`, `domain3`) + +If NetBox is unreachable, the request stays as `queued` — no data is lost. diff --git a/cancel.php b/cancel.php new file mode 100644 index 0000000..acb502e --- /dev/null +++ b/cancel.php @@ -0,0 +1,48 @@ +prepare('SELECT id, status FROM vm_requests WHERE id = ?'); +$stmt->execute([$id]); +$request = $stmt->fetch(); + +if (!$request) { + header('Location: /requests.php?error=Request+not+found.'); + exit; +} + +if ($request['status'] !== 'queued') { + header('Location: /requests.php?error=Only+queued+requests+can+be+cancelled.'); + exit; +} + +try { + $stmt = db()->prepare('UPDATE vm_requests SET status = ? WHERE id = ?'); + $stmt->execute(['cancelled', $id]); +} catch (PDOException $e) { + header('Location: /requests.php?error=Failed+to+cancel+request.'); + exit; +} + +header('Location: /requests.php?cancelled=1'); +exit; diff --git a/config.php b/config.php new file mode 100755 index 0000000..965dd4a --- /dev/null +++ b/config.php @@ -0,0 +1,89 @@ + $config['db_host'] ?? getenv('PHPSITE_DB_HOST') ?: '127.0.0.1', + 'name' => $config['db_name'] ?? getenv('PHPSITE_DB_NAME') ?: 'phpsite', + 'user' => $config['db_user'] ?? getenv('PHPSITE_DB_USER') ?: 'phpsite_app', + 'pass' => $config['db_pass'] ?? getenv('PHPSITE_DB_PASS') ?: '', +]; + +// Include NetBox integration +require_once __DIR__ . '/netbox_integration/config.php'; + +function db(): PDO +{ + static $pdo = null; + global $dbConfig; + + if ($pdo instanceof PDO) { + return $pdo; + } + + $dsn = sprintf( + 'mysql:host=%s;dbname=%s;charset=utf8mb4', + $dbConfig['host'], + $dbConfig['name'] + ); + + $pdo = new PDO($dsn, $dbConfig['user'], $dbConfig['pass'], [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]); + + return $pdo; +} + +function start_app_session(): void +{ + if (session_status() === PHP_SESSION_ACTIVE) { + return; + } + + session_set_cookie_params([ + 'lifetime' => 0, + 'path' => '/', + 'httponly' => true, + 'samesite' => 'Lax', + ]); + session_start(); +} + +function h(?string $value): string +{ + return htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); +} + +function require_login(): void +{ + start_app_session(); + + if (empty($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) { + header('Location: /login.php'); + exit; + } +} + +function csrf_token(): string +{ + start_app_session(); + + if (empty($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); + } + + return $_SESSION['csrf_token']; +} + +function verify_csrf(?string $token): bool +{ + start_app_session(); + + return is_string($token) + && isset($_SESSION['csrf_token']) + && hash_equals($_SESSION['csrf_token'], $token); +} diff --git a/create.php b/create.php new file mode 100644 index 0000000..becebae --- /dev/null +++ b/create.php @@ -0,0 +1,245 @@ + + + +
+ + +
+