diff --git a/dev-panel.vincentdevelopment.ca/htdocs/index.php b/dev-panel.vincentdevelopment.ca/htdocs/index.php index fa1c3df..632b817 100644 --- a/dev-panel.vincentdevelopment.ca/htdocs/index.php +++ b/dev-panel.vincentdevelopment.ca/htdocs/index.php @@ -10,6 +10,7 @@ session_start(); while (ob_get_level() > 0) { ob_end_flush(); } + ob_implicit_flush(true); // Send headers early @@ -33,11 +34,9 @@ $themeRemoteOrigin = ''; // e.g. 'git@github.com:your-org/client-theme-repo.git' $dbPath = __DIR__ . '/panel.sqlite'; // ---------- Init DB ---------- - $seedInfo = initDb(); // ---------- Routing / Auth Gate ---------- - $action = isset($_GET['action']) ? $_GET['action'] : 'list'; // Logout handler @@ -52,15 +51,15 @@ requireLogin($action); $user = getCurrentUser(); // ---------- Login Action ---------- - if ($action === 'login') { $loginError = null; + if ($_SERVER['REQUEST_METHOD'] === 'POST') { $username = trim($_POST['username'] ?? ''); $password = $_POST['password'] ?? ''; if ($username !== '' && $password !== '') { - $db = getDb(); + $db = getDb(); $stmt = $db->prepare('SELECT id, username, password_hash, role FROM users WHERE username = ?'); $stmt->execute([$username]); $row = $stmt->fetch(PDO::FETCH_ASSOC); @@ -79,8 +78,8 @@ if ($action === 'login') { $loginError = 'Username and password are required.'; } } - ?> + @@ -133,10 +132,9 @@ if ($action === 'login') { } // ---------- Self password change (processed for any authenticated action) ---------- - -$selfPasswordError = null; +$selfPasswordError = null; $selfPasswordMessage = null; -$passwordDialogOpen = false; +$passwordDialogOpen = false; if ( $user && @@ -157,7 +155,7 @@ if ( } elseif (strlen($newPassword) < 8) { $selfPasswordError = 'New password must be at least 8 characters.'; } else { - $db = getDb(); + $db = getDb(); $stmt = $db->prepare('SELECT password_hash FROM users WHERE id = ?'); $stmt->execute([$user['id']]); $row = $stmt->fetch(PDO::FETCH_ASSOC); @@ -165,17 +163,16 @@ if ( if (!$row || !password_verify($currentPassword, $row['password_hash'])) { $selfPasswordError = 'Current password is incorrect.'; } else { - $hash = password_hash($newPassword, PASSWORD_DEFAULT); + $hash = password_hash($newPassword, PASSWORD_DEFAULT); $update = $db->prepare('UPDATE users SET password_hash = ? WHERE id = ?'); $update->execute([$hash, $user['id']]); $selfPasswordMessage = 'Your password has been updated.'; } } } - // ---------- Page Layout (for authenticated users) ---------- - ?> + @@ -215,623 +212,628 @@ if ( -Invalid domain. Use only letters, numbers, dots and dashes.'; - } else { - // Build WordOps command - $flags = []; - - if ($stackType === 'wp') { - $flags[] = '--wp'; - } elseif ($stackType === 'wpfc') { - $flags[] = '--wpfc'; - } elseif ($stackType === 'plain') { - $flags[] = '--php'; + if (!$projectName) { + $projectName = $domain; } - if ($isMultisite === 'subdir') { - $flags[] = '--wpsubdir'; - } elseif ($isMultisite === 'subdomain') { - $flags[] = '--wpsubdomain'; - } + if (!$domain) { + echo '
Invalid domain. Use only letters, numbers, dots and dashes.
'; + } else { + // Build WordOps command + $flags = []; - $cmd = sprintf( - 'sudo %s site create %s %s', - escapeshellcmd($woPath), - escapeshellarg($domain), - implode(' ', array_map('escapeshellarg', $flags)) - ); - - echo '
'; - echo '

Provisioning site: ' . htmlspecialchars($domain) . '

'; - echo '

WordOps log:

'; - echo '
';
-
-      @ob_flush();
-      @flush();
-
-      list($ok, $woLines) = runCommandStreaming($cmd, function ($line) {
-        echo htmlspecialchars($line) . "\n";
-        @ob_flush();
-        @flush();
-      });
-
-      echo '
'; - - if ($ok) { - // Record site owner in SQLite - $db = getDb(); - $stmt = $db->prepare('INSERT OR REPLACE INTO sites (domain, owner_id, created_at) VALUES (?, ?, datetime("now"))'); - $stmt->execute([$domain, $user['id']]); - - echo '

Site created successfully.

'; - - // Run bootstrap if requested - if ($bootstrapProfile === 'standard') { - $adminUser = $bootstrapAdminUser; - $adminEmail = $bootstrapAdminEmail; - - $bootstrapArgs = [ - '--domain', $domain, - '--project-name', $projectName, - '--admin-user', $adminUser, - '--admin-email', $adminEmail, - ]; - - if ($themeStarterRepo !== '') { - $bootstrapArgs[] = '--theme-starter-repo'; - $bootstrapArgs[] = $themeStarterRepo; - } - if ($themeRemoteOrigin !== '') { - $bootstrapArgs[] = '--theme-remote-origin'; - $bootstrapArgs[] = $themeRemoteOrigin; - } - - $bootstrapCmd = escapeshellcmd($bootstrapScript); - foreach ($bootstrapArgs as $arg) { - $bootstrapCmd .= ' ' . escapeshellarg($arg); - } - - echo '

Bootstrap log:

'; - echo '
';
-
-          @ob_flush();
-          @flush();
-
-          list($bootstrapOk, $bootstrapLines) = runCommandStreaming($bootstrapCmd, function ($line) {
-            echo htmlspecialchars($line) . "\n";
-            @ob_flush();
-            @flush();
-          });
-
-          echo '
'; - - if ($bootstrapOk) { - echo '

Bootstrap completed: standard dev stack applied.

'; - } else { - echo '

Bootstrap failed: see log above.

'; - } + if ($stackType === 'wp') { + $flags[] = '--wp'; + } elseif ($stackType === 'wpfc') { + $flags[] = '--wpfc'; + } elseif ($stackType === 'plain') { + $flags[] = '--php'; } - echo '

Back to site list

'; - } else { - echo '

Site creation failed. See WordOps log above.

'; - echo '

Back to site list

'; - } + if ($isMultisite === 'subdir') { + $flags[] = '--wpsubdir'; + } elseif ($isMultisite === 'subdomain') { + $flags[] = '--wpsubdomain'; + } - echo '
'; - } - } - - ?> - -
-

Create new site

- -
- - - - - - - - - - -
- - Cancel -
-
-
- - Invalid domain.'; - } elseif ( - $_SERVER['REQUEST_METHOD'] === 'POST' && - isset($_POST['confirm']) && - $_POST['confirm'] === 'yes' && - (!isset($_POST['op']) || $_POST['op'] !== 'self_change_password') - ) { - $confirmText = $_POST['confirm_text'] ?? ''; - - if ($confirmText !== $domain) { - echo '
Confirmation text did not match domain.
'; - } else { - // Enforce ownership for dev users - $db = getDb(); - $ownerRow = $db->prepare('SELECT owner_id FROM sites WHERE domain = ?'); - $ownerRow->execute([$domain]); - $ownerId = $ownerRow->fetchColumn(); - - if (!isAdmin() && (int)$ownerId !== (int)$user['id']) { - echo '
You do not have permission to delete this site.
'; - } else { $cmd = sprintf( - 'sudo %s site delete %s --no-prompt', + 'sudo %s site create %s %s', escapeshellcmd($woPath), - escapeshellarg($domain) + escapeshellarg($domain), + implode(' ', array_map('escapeshellarg', $flags)) ); - $output = []; - $status = null; - $ok = runCommand($cmd, $output, $status); - echo '
'; + echo '

Provisioning site: ' . htmlspecialchars($domain) . '

'; + echo '

WordOps log:

'; + echo '
';
+
+        @ob_flush();
+        @flush();
+
+        list($ok, $woLines) = runCommandStreaming($cmd, function ($line) {
+          echo htmlspecialchars($line) . "\n";
+          @ob_flush();
+          @flush();
+        });
+
+        echo '
'; if ($ok) { - // Remove from metadata - $stmt = $db->prepare('DELETE FROM sites WHERE domain = ?'); - $stmt->execute([$domain]); + // Record site owner in SQLite + $db = getDb(); + $stmt = $db->prepare('INSERT OR REPLACE INTO sites (domain, owner_id, created_at) VALUES (?, ?, datetime("now"))'); + $stmt->execute([$domain, $user['id']]); - echo 'Site deleted: ' . htmlspecialchars($domain) . '
'; + echo '

Site created successfully.

'; + + // Run bootstrap if requested + if ($bootstrapProfile === 'standard') { + $adminUser = $bootstrapAdminUser; + $adminEmail = $bootstrapAdminEmail; + + $bootstrapArgs = [ + '--domain', $domain, + '--project-name', $projectName, + '--admin-user', $adminUser, + '--admin-email', $adminEmail, + ]; + + if ($themeStarterRepo !== '') { + $bootstrapArgs[] = '--theme-starter-repo'; + $bootstrapArgs[] = $themeStarterRepo; + } + + if ($themeRemoteOrigin !== '') { + $bootstrapArgs[] = '--theme-remote-origin'; + $bootstrapArgs[] = $themeRemoteOrigin; + } + + $bootstrapCmd = escapeshellcmd($bootstrapScript); + + foreach ($bootstrapArgs as $arg) { + $bootstrapCmd .= ' ' . escapeshellarg($arg); + } + + echo '

Bootstrap log:

'; + echo '
';
+
+            @ob_flush();
+            @flush();
+
+            list($bootstrapOk, $bootstrapLines) = runCommandStreaming($bootstrapCmd, function ($line) {
+              echo htmlspecialchars($line) . "\n";
+              @ob_flush();
+              @flush();
+            });
+
+            echo '
'; + + if ($bootstrapOk) { + echo '

Bootstrap completed: standard dev stack applied.

'; + } else { + echo '

Bootstrap failed: see log above.

'; + } + } + + echo '

Back to site list

'; } else { - echo 'Failed to delete site. Check the log below.'; + echo '

Site creation failed. See WordOps log above.

'; + echo '

Back to site list

'; } - echo '
' . htmlspecialchars(implode("\n", $output)) . '
'; - echo '

Back to site list

'; echo '
'; } } - } else { ?> +
-

Delete site

-

You're about to delete .

-

This will remove the vhost and files. Databases are handled by WordOps according to its defaults.

+

Create new site

- + + + + + + +
- + Cancel
- prepare('SELECT COUNT(*) FROM users WHERE username = ?'); - $stmt->execute([$username]); - if ((int)$stmt->fetchColumn() > 0) { - $userError = 'Username already exists.'; - } else { - $hash = password_hash($password, PASSWORD_DEFAULT); - $stmt = $db->prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)'); - $stmt->execute([$username, $hash, $role]); - $userMessage = 'User "' . htmlspecialchars($username) . '" created successfully.'; - } - } - } elseif ($op === 'reset_password') { - $userId = (int)($_POST['user_id'] ?? 0); - if ($userId > 0) { - $stmt = $db->prepare('SELECT username FROM users WHERE id = ?'); - $stmt->execute([$userId]); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - - if (!$row) { - $userError = 'User not found.'; - } else { - $newPass = generatePassword(16); - $hash = password_hash($newPass, PASSWORD_DEFAULT); - $update = $db->prepare('UPDATE users SET password_hash = ? WHERE id = ?'); - $update->execute([$hash, $userId]); - - $generatedPasswordInfo = [ - 'username' => $row['username'], - 'password' => $newPass, - ]; - $userMessage = 'Password reset for user "' . htmlspecialchars($row['username']) . '".'; - } - } else { - $userError = 'Invalid user ID.'; - } - } - } - - // Fetch users for listing - $stmt = $db->query('SELECT id, username, role FROM users ORDER BY username ASC'); - $users = $stmt->fetchAll(PDO::FETCH_ASSOC); - - ?> -
-

User Management

' . $userMessage . '

'; - } - if ($userError) { - echo '

' . htmlspecialchars($userError) . '

'; - } - if ($generatedPasswordInfo) { - echo '

New password for ' . htmlspecialchars($generatedPasswordInfo['username']) . ': '; - echo '' . htmlspecialchars($generatedPasswordInfo['password']) . '

'; - } - ?> + } elseif ($action === 'delete' && isset($_GET['domain'])) { + $domain = sanitizeDomain($_GET['domain']); -

Existing users

- -

No users found.

- - - - - - - - - - - - - - - - - - -
UsernameRoleActions
-
- - - -
-
- + if (!$domain) { + echo '
Invalid domain.
'; + } elseif ( + $_SERVER['REQUEST_METHOD'] === 'POST' && + isset($_POST['confirm']) && + $_POST['confirm'] === 'yes' && + (!isset($_POST['op']) || $_POST['op'] !== 'self_change_password') + ) { + $confirmText = $_POST['confirm_text'] ?? ''; -

Create new user

-
- + if ($confirmText !== $domain) { + echo '
Confirmation text did not match domain.
'; + } else { + // Enforce ownership for dev users + $db = getDb(); + $ownerRow = $db->prepare('SELECT owner_id FROM sites WHERE domain = ?'); + $ownerRow->execute([$domain]); + $ownerId = $ownerRow->fetchColumn(); - + if (!isAdmin() && (int)$ownerId !== (int)$user['id']) { + echo '
You do not have permission to delete this site.
'; + } else { + $cmd = sprintf( + 'sudo %s site delete %s --no-prompt', + escapeshellcmd($woPath), + escapeshellarg($domain) + ); - + $output = []; + $status = null; + $ok = runCommand($cmd, $output, $status); - + echo '
'; - + if ($ok) { + // Remove from metadata + $stmt = $db->prepare('DELETE FROM sites WHERE domain = ?'); + $stmt->execute([$domain]); -
- -
- -
- Site deleted: ' . htmlspecialchars($domain) . '
'; + } else { + echo 'Failed to delete site. Check the log below.'; + } -} else { - // Default: site list - $cmd = sprintf('sudo %s site list', escapeshellcmd($woPath)); - $output = []; - $status = null; - $ok = runCommand($cmd, $output, $status); - - echo '
'; - echo '

Sites

'; - - if (!$ok) { - echo 'Could not retrieve site list.'; - echo '
' . htmlspecialchars(implode("\n", $output)) . '
'; - } else { - $db = getDb(); - - // Map of domain => [owner_id, username, role] - $meta = []; - $stmt = $db->query(' - SELECT s.domain, s.owner_id, u.username, u.role - FROM sites s - LEFT JOIN users u ON s.owner_id = u.id - '); - while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { - $meta[$row['domain']] = $row; - } - - $sites = []; - foreach ($output as $line) { - $line = trim($line); - - // Skip header / separator lines and the panel site itself - if ( - $line === '' || - strpos($line, 'Site') !== false || - strpos($line, 'site') !== false || - strpos($line, '---') !== false || - $line === 'dev-panel.local' - ) { - continue; - } - - // Take the first column as the domain - $parts = preg_split('/\s+/', $line); - if (!$parts || !isset($parts[0])) { - continue; - } - - $domain = sanitizeDomain($parts[0]); - if (!$domain) { - continue; - } - - $ownerInfo = $meta[$domain] ?? null; - - // Role-based filtering: dev sees only their own; admin sees all - if (!isAdmin()) { - if (!$ownerInfo || (int)$ownerInfo['owner_id'] !== (int)$user['id']) { - continue; + echo '
' . htmlspecialchars(implode("\n", $output)) . '
'; + echo '

Back to site list

'; + echo '
'; } } - - $sites[] = [ - 'domain' => $domain, - 'owner' => $ownerInfo['username'] ?? null, - 'ownerRole' => $ownerInfo['role'] ?? null, - 'ownerId' => $ownerInfo['owner_id'] ?? null, - ]; - } - - if (empty($sites) || (count($sites) === 1 && strPos($sites[0]['domain'], 'dev-panel') !== false)) { - echo '

No sites found for your account yet. Create your first site.

'; } else { - echo ''; } + + echo '
'; } + ?> - echo ''; -} + +