From 8291ef83b44c7185e6e97e1a2ae2c8ab0a979514 Mon Sep 17 00:00:00 2001 From: delicatacurtis Date: Mon, 1 Dec 2025 21:18:52 +0000 Subject: [PATCH 1/4] Add standalone installer UI (public/installer.php), storage helper, and README --- README_INSTALLER.md | 7 +++++++ public/installer.php | 3 +++ storage/installer/create_users.php | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 README_INSTALLER.md create mode 100644 public/installer.php create mode 100644 storage/installer/create_users.php diff --git a/README_INSTALLER.md b/README_INSTALLER.md new file mode 100644 index 00000000..04a5e37f --- /dev/null +++ b/README_INSTALLER.md @@ -0,0 +1,7 @@ +# Installer UI + +This document provides an overview of the installer UI and its usage. + +## Features +- Standalone installer. +- User creation functionality. \ No newline at end of file diff --git a/public/installer.php b/public/installer.php new file mode 100644 index 00000000..58a35568 --- /dev/null +++ b/public/installer.php @@ -0,0 +1,3 @@ + Date: Mon, 1 Dec 2025 21:29:38 +0000 Subject: [PATCH 2/4] Implement standalone installer UI with security checks Added a standalone installer UI with security features and environment variable handling. --- public/installer.php | 67 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/public/installer.php b/public/installer.php index 58a35568..b1a45997 100644 --- a/public/installer.php +++ b/public/installer.php @@ -1,3 +1,66 @@

Installer disabled

Set INSTALLER_ENABLED=true in your .env to enable.

"; + exit; +} + +if (!check_key()) { + // Simple prompt page for key + if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['key'])) { + // redirect with key in query for convenient requests (short-lived) + $key = urlencode($_POST['key']); + $uri = strtok($_SERVER["REQUEST_URI"], '?'); + header("Location: {$uri}?key={$key}"); + exit; + } + echo ' Date: Mon, 1 Dec 2025 21:29:55 +0000 Subject: [PATCH 3/4] Refactor create_users.php for user creation logic Enhance user creation script to handle JSON input and roles. --- storage/installer/create_users.php | 70 ++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/storage/installer/create_users.php b/storage/installer/create_users.php index 406798d8..74b00323 100644 --- a/storage/installer/create_users.php +++ b/storage/installer/create_users.php @@ -1,5 +1,67 @@ +// JSON: { "users": [ { "name":"", "email":"", "password":"", "role":"" }, ... ] } + +$arg = $argv[1] ?? null; +if (!$arg) { + echo "No data provided\n"; + exit(1); +} +$data = json_decode(base64_decode($arg), true); +if (!$data || !isset($data['users'])) { + echo "Invalid payload\n"; + exit(1); +} + +$projectRoot = dirname(__DIR__, 2); +require $projectRoot . '/vendor/autoload.php'; + +$app = require $projectRoot . '/bootstrap/app.php'; +$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class); +$kernel->bootstrap(); + +use Illuminate\Support\Facades\Hash; + +foreach ($data['users'] as $u) { + if (!isset($u['name'],$u['email'],$u['password'])) { + echo "Skipping incomplete user\n"; + continue; + } + try { + // Create user using Eloquent (App\Models\User) + $userClass = '\App\Models\User'; + if (!class_exists($userClass)) { + echo "User model not found\n"; + continue; + } + if (call_user_func([$userClass, 'where'], 'email', $u['email'])->exists()) { + echo "User {$u['email']} already exists, skipping\n"; + continue; + } + $user = $userClass::create([ + 'name' => $u['name'], + 'email' => $u['email'], + 'password' => Hash::make($u['password']), + ]); + // Assign role if Spatie exists and user has assignRole + if (class_exists('\Spatie\Permission\Models\Role') && method_exists($user, 'assignRole') && !empty($u['role'])) { + if (!\Spatie\Permission\Models\Role::where('name', $u['role'])->exists()) { + \Spatie\Permission\Models\Role::create(['name' => $u['role']]); + } + $user->assignRole($u['role']); + echo "Created user {$u['email']} with role {$u['role']}\n"; + } else { + // fallback attempt to set role attribute + if (array_key_exists('role', $user->getAttributes()) && isset($u['role'])) { + $user->role = $u['role']; + $user->save(); + echo "Created user {$u['email']} (role attribute set)\n"; + } else { + echo "Created user {$u['email']} (no role assigned)\n"; + } + } + } catch (Exception $e) { + echo "Failed to create {$u['email']}: " . $e->getMessage() . "\n"; + } +} From 1c1164d97902a1e378fb15dfad8af475a8306879 Mon Sep 17 00:00:00 2001 From: delicatacurtis Date: Mon, 1 Dec 2025 21:55:07 +0000 Subject: [PATCH 4/4] Update installer.php --- public/installer.php | 461 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 460 insertions(+), 1 deletion(-) diff --git a/public/installer.php b/public/installer.php index b1a45997..2996fb1e 100644 --- a/public/installer.php +++ b/public/installer.php @@ -63,4 +63,463 @@ function check_key() { header("Location: {$uri}?key={$key}"); exit; } - echo ''; + echo '

Installer - Authentication

'; + echo '
'; + echo ''; + exit; +} + +// Helper to run a shell command and return output and exit code +function run_cmd($cmd, &$output = null) { + // Use proc_open so we can capture stdout/stderr. Avoid shell injection by leaving caller to escape. + $descriptorspec = [ + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $process = @proc_open($cmd, $descriptorspec, $pipes, getcwd()); + if (!is_resource($process)) { + $output = "Failed to start process for command: {$cmd}"; + return 255; + } + $out = stream_get_contents($pipes[1]); + fclose($pipes[1]); + $err = stream_get_contents($pipes[2]); + fclose($pipes[2]); + $status = proc_close($process); + $output = trim($out . PHP_EOL . $err); + return $status; +} + +$action = $_REQUEST['action'] ?? null; + +if ($action) { + header('Content-Type: application/json'); + + // Simple allowlist for actions + $allowed = [ + 'composer_install', + 'php_key_generate', + 'migrate_seed', + 'npm_install', + 'npm_build', + 'save_settings', + 'create_users', + 'status', + ]; + if (!in_array($action, $allowed, true)) { + echo json_encode(['ok' => false, 'message' => 'Invalid action']); + exit; + } + + // Make sure path calculations are consistent + $projectRoot = realpath(__DIR__ . '/..'); + chdir($projectRoot); + + try { + if ($action === 'status') { + $composerInstalled = file_exists($projectRoot . '/vendor/autoload.php'); + echo json_encode(['ok' => true, 'composer_installed' => $composerInstalled]); + exit; + } + + if ($action === 'composer_install') { + $composer = getenv('COMPOSER_BINARY') ?: 'composer'; + $cmd = escapeshellcmd($composer) . ' install --no-interaction --no-progress'; + $out = null; + $code = run_cmd($cmd, $out); + echo json_encode(['ok' => $code === 0, 'exit' => $code, 'output' => $out]); + exit; + } + + if ($action === 'php_key_generate') { + // php artisan key:generate requires vendor; fail gracefully if vendor missing + if (!file_exists($projectRoot . '/vendor/autoload.php')) { + echo json_encode(['ok' => false, 'message' => 'vendor not installed. Run composer first.']); + exit; + } + $php = getenv('PHP_BINARY') ?: 'php'; + $cmd = escapeshellcmd($php) . ' artisan key:generate --force'; + $out = null; + $code = run_cmd($cmd, $out); + echo json_encode(['ok' => $code === 0, 'exit' => $code, 'output' => $out]); + exit; + } + + if ($action === 'migrate_seed') { + if (!file_exists($projectRoot . '/vendor/autoload.php')) { + echo json_encode(['ok' => false, 'message' => 'vendor not installed. Run composer first.']); + exit; + } + $php = getenv('PHP_BINARY') ?: 'php'; + $cmd = escapeshellcmd($php) . ' artisan migrate --force --seed'; + $out = null; + $code = run_cmd($cmd, $out); + echo json_encode(['ok' => $code === 0, 'exit' => $code, 'output' => $out]); + exit; + } + + if ($action === 'npm_install') { + $npm = getenv('NPM_BINARY') ?: 'npm'; + $cmd = escapeshellcmd($npm) . ' install --no-audit --no-fund'; + $out = null; + $code = run_cmd($cmd, $out); + echo json_encode(['ok' => $code === 0, 'exit' => $code, 'output' => $out]); + exit; + } + + if ($action === 'npm_build') { + $npm = getenv('NPM_BINARY') ?: 'npm'; + $cmd = escapeshellcmd($npm) . ' run build'; + $out = null; + $code = run_cmd($cmd, $out); + echo json_encode(['ok' => $code === 0, 'exit' => $code, 'output' => $out]); + exit; + } + + if ($action === 'save_settings') { + // Accept JSON body + $body = json_decode(file_get_contents('php://input'), true) ?? $_POST; + $appName = $body['app_name'] ?? null; + $appUrl = $body['app_url'] ?? null; + $adminEmail = $body['admin_email'] ?? null; + + $envPath = $projectRoot . '/.env'; + if (!file_exists($envPath)) { + // create + file_put_contents($envPath, ''); + } + $env = file_get_contents($envPath); + $replacements = [ + 'APP_NAME' => $appName, + 'APP_URL' => $appUrl, + 'ADMIN_EMAIL' => $adminEmail, + ]; + foreach ($replacements as $k => $v) { + if ($v === null || $v === '') continue; + $escaped = (strpos($v, ' ') !== false) ? '"' . addcslashes($v, "\"") . '"' : $v; + if (preg_match("/^{$k}=.*/m", $env)) { + $env = preg_replace("/^{$k}=.*/m", "{$k}={$escaped}", $env); + } else { + $env .= PHP_EOL . "{$k}={$escaped}"; + } + } + file_put_contents($envPath, $env); + // try clearing config caches (works only if vendor installed) + if (file_exists($projectRoot . '/vendor/autoload.php')) { + $php = getenv('PHP_BINARY') ?: 'php'; + run_cmd(escapeshellcmd($php) . ' artisan config:clear', $o1); + } + echo json_encode(['ok' => true, 'message' => '.env updated']); + exit; + } + + if ($action === 'create_users') { + $body = json_decode(file_get_contents('php://input'), true) ?? $_POST; + $users = $body['users'] ?? null; + if (!$users) { + echo json_encode(['ok' => false, 'message' => 'No users provided']); + exit; + } + if (!file_exists($projectRoot . '/vendor/autoload.php')) { + echo json_encode(['ok' => false, 'message' => 'vendor not installed. Run composer first.']); + exit; + } + // Ensure storage/installer exists + $scriptDir = $projectRoot . '/storage/installer'; + if (!is_dir($scriptDir)) { + @mkdir($scriptDir, 0755, true); + } + $scriptPath = $scriptDir . '/create_users.php'; + // Write helper script (idempotent) + $helper = <<<'PHP' + +// JSON: { "users": [ { "name":"", "email":"", "password":"", "role":"" }, ... ] } + +$arg = $argv[1] ?? null; +if (!$arg) { + echo "No data provided\n"; + exit(1); +} +$data = json_decode(base64_decode($arg), true); +if (!$data || !isset($data['users'])) { + echo "Invalid payload\n"; + exit(1); +} + +$projectRoot = dirname(__DIR__, 2); +require $projectRoot . '/vendor/autoload.php'; + +$app = require $projectRoot . '/bootstrap/app.php'; +$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class); +$kernel->bootstrap(); + +use Illuminate\Support\Facades\Hash; + +foreach ($data['users'] as $u) { + if (!isset($u['name'],$u['email'],$u['password'])) { + echo "Skipping incomplete user\n"; + continue; + } + try { + // Create user using Eloquent (App\Models\User) + $userClass = '\\App\\Models\\User'; + if (!class_exists($userClass)) { + echo "User model not found\n"; + continue; + } + if (call_user_func([$userClass, 'where'], 'email', $u['email'])->exists()) { + echo "User {$u['email']} already exists, skipping\n"; + continue; + } + $user = $userClass::create([ + 'name' => $u['name'], + 'email' => $u['email'], + 'password' => Hash::make($u['password']), + ]); + // Assign role if Spatie exists and user has assignRole + if (class_exists('\\Spatie\\Permission\\Models\\Role') && method_exists($user, 'assignRole') && !empty($u['role'])) { + if (!\\Spatie\\Permission\\Models\\Role::where('name', $u['role'])->exists()) { + \\Spatie\\Permission\\Models\\Role::create(['name' => $u['role']]); + } + $user->assignRole($u['role']); + echo "Created user {$u['email']} with role {$u['role']}\n"; + } else { + // fallback attempt to set role attribute + if (array_key_exists('role', $user->getAttributes()) && isset($u['role'])) { + $user->role = $u['role']; + $user->save(); + echo "Created user {$u['email']} (role attribute set)\n"; + } else { + echo "Created user {$u['email']} (no role assigned)\n"; + } + } + } catch (Exception $e) { + echo "Failed to create {$u['email']}: " . $e->getMessage() . "\n"; + } +} +PHP; + file_put_contents($scriptPath, $helper); + + // Encode user payload to base64 to safely pass via CLI + $payload = ['users' => $users]; + $b64 = base64_encode(json_encode($payload)); + $php = getenv('PHP_BINARY') ?: 'php'; + $cmd = escapeshellcmd($php) . ' ' . escapeshellarg($scriptPath) . ' ' . escapeshellarg($b64); + $out = null; + $code = run_cmd($cmd, $out); + echo json_encode(['ok' => $code === 0, 'exit' => $code, 'output' => $out]); + exit; + } + + echo json_encode(['ok' => false, 'message' => 'Unhandled action']); + exit; + + } catch (Throwable $th) { + echo json_encode(['ok' => false, 'message' => $th->getMessage()]); + exit; + } +} + +// No action: render UI +$app_name = htmlspecialchars(dotenv_get('APP_NAME') ?: 'Laravel App', ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8'); +$installer_key_configured = (bool)dotenv_get('INSTALLER_KEY'); +$example_key = substr(bin2hex(random_bytes(8)),0,16); +?> + + + + + Installer - <?= $app_name ?> + + + + +
+

Standalone Installer

+

This installer runs common setup tasks. Make sure this is disabled (INSTALLER_ENABLED=false or remove file) after use.

+ + +
+ Installer Key is configured. Requests must include ?key=YOUR_KEY or send key in POST/JSON. +
+ +
+ No INSTALLER_KEY set. For safety set INSTALLER_KEY in your .env. Example: INSTALLER_KEY= +
+ + +
+
+
+
+ Steps +
+ + + +
+
+ + +
+
+ +
+ Settings +
+ + + + + + +
+ +
+
+
+ +
+ Create Users +
+
+ + + + +
+
+
+ + +
+
+ +
+ Console Output +

+          
+
+
+ +
+
+

Quick Status

+
Checking...
+
+
+ - Commands are executed on the server using CLI binaries found in PATH or as set in .env (COMPOSER_BINARY, PHP_BINARY, NPM_BINARY).
+ - Composer must be run first. Artisan commands require vendor installed.
+ - After finishing, disable the installer by setting INSTALLER_ENABLED=false. +
+
+
+
+ +
+ + +
+
+ + + +