From d5537dc2bdd6946f971e515289f1e7c625dffbe8 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Fri, 19 Jul 2024 09:04:16 +0200 Subject: [PATCH 01/17] simplify setup command --- .../Environment/AppSettingsCommand.php | 142 ++------------ .../Environment/DatabaseSettingsCommand.php | 185 ------------------ 2 files changed, 14 insertions(+), 313 deletions(-) delete mode 100644 app/Console/Commands/Environment/DatabaseSettingsCommand.php diff --git a/app/Console/Commands/Environment/AppSettingsCommand.php b/app/Console/Commands/Environment/AppSettingsCommand.php index 4f2e931196..b3ee96e081 100644 --- a/app/Console/Commands/Environment/AppSettingsCommand.php +++ b/app/Console/Commands/Environment/AppSettingsCommand.php @@ -3,7 +3,6 @@ namespace App\Console\Commands\Environment; use Illuminate\Console\Command; -use Illuminate\Contracts\Console\Kernel; use App\Traits\Commands\EnvironmentWriterTrait; use Illuminate\Support\Facades\Artisan; @@ -11,154 +10,41 @@ class AppSettingsCommand extends Command { use EnvironmentWriterTrait; - public const CACHE_DRIVERS = [ - 'file' => 'Filesystem (recommended)', - 'redis' => 'Redis', - ]; - - public const SESSION_DRIVERS = [ - 'file' => 'Filesystem (recommended)', - 'redis' => 'Redis', - 'database' => 'Database', - 'cookie' => 'Cookie', - ]; - - public const QUEUE_DRIVERS = [ - 'database' => 'Database (recommended)', - 'redis' => 'Redis', - 'sync' => 'Synchronous', - ]; - protected $description = 'Configure basic environment settings for the Panel.'; protected $signature = 'p:environment:setup - {--url= : The URL that this Panel is running on.} - {--cache= : The cache driver backend to use.} - {--session= : The session driver backend to use.} - {--queue= : The queue driver backend to use.} - {--redis-host= : Redis host to use for connections.} - {--redis-pass= : Password used to connect to redis.} - {--redis-port= : Port to connect to redis over.} - {--settings-ui= : Enable or disable the settings UI.}'; + {--url= : The URL that this Panel is running on.}'; protected array $variables = []; - /** - * AppSettingsCommand constructor. - */ - public function __construct(private Kernel $console) - { - parent::__construct(); - } - - /** - * Handle command execution. - * - * @throws \App\Exceptions\PanelException - */ - public function handle(): int + public function handle(): void { - $this->variables['APP_TIMEZONE'] = 'UTC'; - - $this->output->comment(__('commands.appsettings.comment.url')); - $this->variables['APP_URL'] = $this->option('url') ?? $this->ask( - 'Application URL', - config('app.url', 'https://example.com') - ); - - $selected = config('cache.default', 'file'); - $this->variables['CACHE_STORE'] = $this->option('cache') ?? $this->choice( - 'Cache Driver', - self::CACHE_DRIVERS, - array_key_exists($selected, self::CACHE_DRIVERS) ? $selected : null - ); - - $selected = config('session.driver', 'file'); - $this->variables['SESSION_DRIVER'] = $this->option('session') ?? $this->choice( - 'Session Driver', - self::SESSION_DRIVERS, - array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null - ); - - $selected = config('queue.default', 'database'); - $this->variables['QUEUE_CONNECTION'] = $this->option('queue') ?? $this->choice( - 'Queue Driver', - self::QUEUE_DRIVERS, - array_key_exists($selected, self::QUEUE_DRIVERS) ? $selected : null - ); - - if (!is_null($this->option('settings-ui'))) { - $this->variables['APP_ENVIRONMENT_ONLY'] = $this->option('settings-ui') == 'true' ? 'false' : 'true'; - } else { - $this->variables['APP_ENVIRONMENT_ONLY'] = $this->confirm(__('commands.appsettings.comment.settings_ui'), true) ? 'false' : 'true'; - } - - // Make sure session cookies are set as "secure" when using HTTPS - if (str_starts_with($this->variables['APP_URL'], 'https://')) { - $this->variables['SESSION_SECURE_COOKIE'] = 'true'; - } - - $redisUsed = count(collect($this->variables)->filter(function ($item) { - return $item === 'redis'; - })) !== 0; - - if ($redisUsed) { - $this->requestRedisSettings(); - } - $path = base_path('.env'); if (!file_exists($path)) { + $this->comment('Copying example .env file'); copy($path . '.example', $path); } - $this->writeToEnvironment($this->variables); - if (!config('app.key')) { + $this->comment('Generating app key'); Artisan::call('key:generate'); } - if ($this->variables['QUEUE_CONNECTION'] !== 'sync') { - $this->call('p:environment:queue-service', [ - '--use-redis' => $redisUsed, - ]); - } - - $this->info($this->console->output()); - - return 0; - } + $this->variables['APP_TIMEZONE'] = 'UTC'; - /** - * Request redis connection details and verify them. - */ - private function requestRedisSettings(): void - { - $this->output->note(__('commands.appsettings.redis.note')); - $this->variables['REDIS_HOST'] = $this->option('redis-host') ?? $this->ask( - 'Redis Host', - config('database.redis.default.host') + $this->variables['APP_URL'] = $this->option('url') ?? $this->ask( + 'Application URL', + config('app.url', 'https://example.com') ); - $askForRedisPassword = true; - if (!empty(config('database.redis.default.password'))) { - $this->variables['REDIS_PASSWORD'] = config('database.redis.default.password'); - $askForRedisPassword = $this->confirm('It seems a password is already defined for Redis, would you like to change it?'); - } - - if ($askForRedisPassword) { - $this->output->comment(__('commands.appsettings.redis.comment')); - $this->variables['REDIS_PASSWORD'] = $this->option('redis-pass') ?? $this->output->askHidden( - 'Redis Password' - ); + // Make sure session cookies are set as "secure" when using HTTPS + if (str_starts_with($this->variables['APP_URL'], 'https://')) { + $this->variables['SESSION_SECURE_COOKIE'] = 'true'; } - if (empty($this->variables['REDIS_PASSWORD'])) { - $this->variables['REDIS_PASSWORD'] = 'null'; - } + $this->comment('Writing variables to .env file'); + $this->writeToEnvironment($this->variables); - $this->variables['REDIS_PORT'] = $this->option('redis-port') ?? $this->ask( - 'Redis Port', - config('database.redis.default.port') - ); + $this->info("Setup complete. Vist {$this->variables['APP_URL']}/installer to complete the installation"); } } diff --git a/app/Console/Commands/Environment/DatabaseSettingsCommand.php b/app/Console/Commands/Environment/DatabaseSettingsCommand.php deleted file mode 100644 index f05f9bbc4b..0000000000 --- a/app/Console/Commands/Environment/DatabaseSettingsCommand.php +++ /dev/null @@ -1,185 +0,0 @@ - 'SQLite (recommended)', - 'mariadb' => 'MariaDB', - 'mysql' => 'MySQL', - ]; - - protected $description = 'Configure database settings for the Panel.'; - - protected $signature = 'p:environment:database - {--driver= : The database driver backend to use.} - {--database= : The database to use.} - {--host= : The connection address for the MySQL/ MariaDB server.} - {--port= : The connection port for the MySQL/ MariaDB server.} - {--username= : Username to use when connecting to the MySQL/ MariaDB server.} - {--password= : Password to use for the MySQL/ MariaDB database.}'; - - protected array $variables = []; - - /** - * DatabaseSettingsCommand constructor. - */ - public function __construct(private DatabaseManager $database, private Kernel $console) - { - parent::__construct(); - } - - /** - * Handle command execution. - */ - public function handle(): int - { - $selected = config('database.default', 'sqlite'); - $this->variables['DB_CONNECTION'] = $this->option('driver') ?? $this->choice( - 'Database Driver', - self::DATABASE_DRIVERS, - array_key_exists($selected, self::DATABASE_DRIVERS) ? $selected : null - ); - - if ($this->variables['DB_CONNECTION'] === 'mysql') { - $this->output->note(__('commands.database_settings.DB_HOST_note')); - $this->variables['DB_HOST'] = $this->option('host') ?? $this->ask( - 'Database Host', - config('database.connections.mysql.host', '127.0.0.1') - ); - - $this->variables['DB_PORT'] = $this->option('port') ?? $this->ask( - 'Database Port', - config('database.connections.mysql.port', 3306) - ); - - $this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask( - 'Database Name', - config('database.connections.mysql.database', 'panel') - ); - - $this->output->note(__('commands.database_settings.DB_USERNAME_note')); - $this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask( - 'Database Username', - config('database.connections.mysql.username', 'pelican') - ); - - $askForMySQLPassword = true; - if (!empty(config('database.connections.mysql.password')) && $this->input->isInteractive()) { - $this->variables['DB_PASSWORD'] = config('database.connections.mysql.password'); - $askForMySQLPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note')); - } - - if ($askForMySQLPassword) { - $this->variables['DB_PASSWORD'] = $this->option('password') ?? $this->secret('Database Password'); - } - - try { - // Test connection - config()->set('database.connections._panel_command_test', [ - 'driver' => 'mysql', - 'host' => $this->variables['DB_HOST'], - 'port' => $this->variables['DB_PORT'], - 'database' => $this->variables['DB_DATABASE'], - 'username' => $this->variables['DB_USERNAME'], - 'password' => $this->variables['DB_PASSWORD'], - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'strict' => true, - ]); - - $this->database->connection('_panel_command_test')->getPdo(); - } catch (\PDOException $exception) { - $this->output->error(sprintf('Unable to connect to the MySQL server using the provided credentials. The error returned was "%s".', $exception->getMessage())); - $this->output->error(__('commands.database_settings.DB_error_2')); - - if ($this->confirm(__('commands.database_settings.go_back'))) { - $this->database->disconnect('_panel_command_test'); - - return $this->handle(); - } - - return 1; - } - } elseif ($this->variables['DB_CONNECTION'] === 'mariadb') { - $this->output->note(__('commands.database_settings.DB_HOST_note')); - $this->variables['DB_HOST'] = $this->option('host') ?? $this->ask( - 'Database Host', - config('database.connections.mariadb.host', '127.0.0.1') - ); - - $this->variables['DB_PORT'] = $this->option('port') ?? $this->ask( - 'Database Port', - config('database.connections.mariadb.port', 3306) - ); - - $this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask( - 'Database Name', - config('database.connections.mariadb.database', 'panel') - ); - - $this->output->note(__('commands.database_settings.DB_USERNAME_note')); - $this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask( - 'Database Username', - config('database.connections.mariadb.username', 'pelican') - ); - - $askForMariaDBPassword = true; - if (!empty(config('database.connections.mariadb.password')) && $this->input->isInteractive()) { - $this->variables['DB_PASSWORD'] = config('database.connections.mariadb.password'); - $askForMariaDBPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note')); - } - - if ($askForMariaDBPassword) { - $this->variables['DB_PASSWORD'] = $this->option('password') ?? $this->secret('Database Password'); - } - - try { - // Test connection - config()->set('database.connections._panel_command_test', [ - 'driver' => 'mariadb', - 'host' => $this->variables['DB_HOST'], - 'port' => $this->variables['DB_PORT'], - 'database' => $this->variables['DB_DATABASE'], - 'username' => $this->variables['DB_USERNAME'], - 'password' => $this->variables['DB_PASSWORD'], - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'strict' => true, - ]); - - $this->database->connection('_panel_command_test')->getPdo(); - } catch (\PDOException $exception) { - $this->output->error(sprintf('Unable to connect to the MariaDB server using the provided credentials. The error returned was "%s".', $exception->getMessage())); - $this->output->error(__('commands.database_settings.DB_error_2')); - - if ($this->confirm(__('commands.database_settings.go_back'))) { - $this->database->disconnect('_panel_command_test'); - - return $this->handle(); - } - - return 1; - } - } elseif ($this->variables['DB_CONNECTION'] === 'sqlite') { - $this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask( - 'Database Path', - env('DB_DATABASE', 'database.sqlite') - ); - } - - $this->writeToEnvironment($this->variables); - - $this->info($this->console->output()); - - return 0; - } -} From f84aef175bab4d42ea6152ea2aa15a5bb631664e Mon Sep 17 00:00:00 2001 From: Boy132 Date: Fri, 19 Jul 2024 09:06:08 +0200 Subject: [PATCH 02/17] add installer page --- .../Pages/Installer/PanelInstaller.php | 135 ++++++++++++++++++ .../Pages/Installer/Steps/AdminUserStep.php | 31 ++++ .../Pages/Installer/Steps/DatabaseStep.php | 88 ++++++++++++ .../Pages/Installer/Steps/EnvironmentStep.php | 87 +++++++++++ .../Pages/Installer/Steps/RedisStep.php | 33 +++++ .../Installer/Steps/RequirementsStep.php | 76 ++++++++++ .../views/filament/pages/installer.blade.php | 5 + 7 files changed, 455 insertions(+) create mode 100644 app/Filament/Pages/Installer/PanelInstaller.php create mode 100644 app/Filament/Pages/Installer/Steps/AdminUserStep.php create mode 100644 app/Filament/Pages/Installer/Steps/DatabaseStep.php create mode 100644 app/Filament/Pages/Installer/Steps/EnvironmentStep.php create mode 100644 app/Filament/Pages/Installer/Steps/RedisStep.php create mode 100644 app/Filament/Pages/Installer/Steps/RequirementsStep.php create mode 100644 resources/views/filament/pages/installer.blade.php diff --git a/app/Filament/Pages/Installer/PanelInstaller.php b/app/Filament/Pages/Installer/PanelInstaller.php new file mode 100644 index 0000000000..d55cf8eb34 --- /dev/null +++ b/app/Filament/Pages/Installer/PanelInstaller.php @@ -0,0 +1,135 @@ +form->fill(); + } + + public function dehydrate(): void + { + Artisan::call('config:clear'); + Artisan::call('cache:clear'); + } + + protected function getFormSchema(): array + { + return [ + Wizard::make([ + RequirementsStep::make(), + EnvironmentStep::make(), + DatabaseStep::make(), + RedisStep::make() + ->hidden(fn (Get $get) => $get('env.SESSION_DRIVER') != 'redis' && $get('env.QUEUE_CONNECTION') != 'redis' && $get('env.CACHE_STORE') != 'redis'), + AdminUserStep::make(), + ]) + ->persistStepInQueryString() + ->submitAction(new HtmlString(Blade::render(<<<'BLADE' + + Finish + + BLADE))), + ]; + } + + protected function getFormStatePath(): ?string + { + return 'data'; + } + + public function submit() + { + try { + $inputs = $this->form->getState(); + + // Write variables to .env file + $variables = array_get($inputs, 'env'); + $this->writeToEnvironment($variables); + + $redisUsed = count(collect($variables)->filter(function ($item) { + return $item === 'redis'; + })) !== 0; + + // Create queue worker service (if needed) + if ($variables['QUEUE_CONNECTION'] !== 'sync') { + Artisan::call('p:environment:queue-service', [ + '--use-redis' => $redisUsed, + '--overwrite' => true, + ]); + } + + // Run migrations + Artisan::call('migrate', [ + '--force' => true, + '--seed' => true, + ]); + + // Create first admin user + $userData = array_get($inputs, 'user'); + $userData['root_admin'] = true; + app(UserCreationService::class)->handle($userData); + + // Install setup complete + file_put_contents(storage_path('installed'), 'installed'); + + Notification::make() + ->title('Successfully Installed') + ->success() + ->send(); + } catch (Exception $exception) { + Notification::make() + ->title('Installation Failed') + ->body($exception->getMessage()) + ->danger() + ->send(); + } + + return redirect()->intended(Filament::getUrl()); + } +} diff --git a/app/Filament/Pages/Installer/Steps/AdminUserStep.php b/app/Filament/Pages/Installer/Steps/AdminUserStep.php new file mode 100644 index 0000000000..68ebb6510e --- /dev/null +++ b/app/Filament/Pages/Installer/Steps/AdminUserStep.php @@ -0,0 +1,31 @@ +label('Admin User') + ->schema([ + TextInput::make('user.email') + ->label('Admin E-Mail') + ->required() + ->email() + ->default('admin@example.com'), + TextInput::make('user.username') + ->label('Admin Username') + ->required() + ->default('admin'), + TextInput::make('user.password') + ->label('Admin Password') + ->required() + ->password() + ->revealable(), + ]); + } +} diff --git a/app/Filament/Pages/Installer/Steps/DatabaseStep.php b/app/Filament/Pages/Installer/Steps/DatabaseStep.php new file mode 100644 index 0000000000..f16b8c6356 --- /dev/null +++ b/app/Filament/Pages/Installer/Steps/DatabaseStep.php @@ -0,0 +1,88 @@ +label('Database') + ->schema([ + TextInput::make('env.DB_DATABASE') + ->label(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name') + ->hint(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.') + ->required() + ->default(fn (Get $get) => env('DB_DATABASE', $get('env.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')), + TextInput::make('env.DB_HOST') + ->label('Database Host') + ->hint('The host of your database. Make sure it is reachable.') + ->required() + ->default(env('DB_HOST', '127.0.0.1')) + ->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'), + TextInput::make('env.DB_PORT') + ->label('Database Port') + ->hint('The port of your database.') + ->required() + ->numeric() + ->minValue(1) + ->maxValue(65535) + ->default(env('DB_PORT', 3306)) + ->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'), + TextInput::make('env.DB_USERNAME') + ->label('Database Username') + ->hint('The name of your database user.') + ->required() + ->default(env('DB_USERNAME', 'pelican')) + ->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'), + TextInput::make('env.DB_PASSWORD') + ->label('Database Password') + ->hint('The password of your database user. Can be empty.') + ->password() + ->revealable() + ->default(env('DB_PASSWORD')) + ->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'), + ]) + ->afterValidation(function (Get $get) { + $driver = $get('env.DB_CONNECTION'); + if ($driver !== 'sqlite') { + /** @var DatabaseManager $database */ + $database = app(DatabaseManager::class); + + try { + config()->set('database.connections._panel_install_test', [ + 'driver' => $driver, + 'host' => $get('env.DB_HOST'), + 'port' => $get('env.DB_PORT'), + 'database' => $get('env.DB_DATABASE'), + 'username' => $get('env.DB_USERNAME'), + 'password' => $get('env.DB_PASSWORD'), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'strict' => true, + ]); + + $database->connection('_panel_install_test')->getPdo(); + } catch (PDOException $exception) { + Notification::make() + ->title('Database connection failed') + ->body($exception->getMessage()) + ->danger() + ->send(); + + $database->disconnect('_panel_install_test'); + + throw new Halt('Database connection failed'); + } + } + }); + } +} diff --git a/app/Filament/Pages/Installer/Steps/EnvironmentStep.php b/app/Filament/Pages/Installer/Steps/EnvironmentStep.php new file mode 100644 index 0000000000..ddfcdfd321 --- /dev/null +++ b/app/Filament/Pages/Installer/Steps/EnvironmentStep.php @@ -0,0 +1,87 @@ + 'Filesystem', + 'redis' => 'Redis', + ]; + + public const SESSION_DRIVERS = [ + 'file' => 'Filesystem', + 'redis' => 'Redis', + 'database' => 'Database', + 'cookie' => 'Cookie', + ]; + + public const QUEUE_DRIVERS = [ + 'database' => 'Database', + 'redis' => 'Redis', + 'sync' => 'Synchronous', + ]; + + public const DATABASE_DRIVERS = [ + 'sqlite' => 'SQLite', + 'mariadb' => 'MariaDB', + 'mysql' => 'MySQL', + ]; + + public static function make(): Step + { + return Step::make('environment') + ->label('Environment') + ->schema([ + TextInput::make('env.APP_NAME') + ->label('App Name') + ->hint('This will be the Name of your Panel.') + ->required() + ->default(config('app.name')), + TextInput::make('env.APP_URL') + ->label('App URL') + ->hint('This will be the URL you access your Panel from.') + ->required() + ->default(config('app.url')) + ->live() + ->afterStateUpdated(fn ($state, Set $set) => $set('env.SESSION_SECURE_COOKIE', str_starts_with($state, 'https://'))), + Toggle::make('env.SESSION_SECURE_COOKIE') + ->hidden() + ->default(env('SESSION_SECURE_COOKIE')), + ToggleButtons::make('env.CACHE_STORE') + ->label('Cache Driver') + ->hint('The driver used for caching. We recommend "Filesystem".') + ->required() + ->grouped() + ->options(self::CACHE_DRIVERS) + ->default(config('cache.default', 'file')), + ToggleButtons::make('env.SESSION_DRIVER') + ->label('Session Driver') + ->hint('The driver used for storing sessions. We recommend "Filesystem" or "Database".') + ->required() + ->grouped() + ->options(self::SESSION_DRIVERS) + ->default(config('session.driver', 'file')), + ToggleButtons::make('env.QUEUE_CONNECTION') + ->label('Queue Driver') + ->hint('The driver used for handling queues. We recommend "Database".') + ->required() + ->grouped() + ->options(self::QUEUE_DRIVERS) + ->default(config('queue.default', 'database')), + ToggleButtons::make('env.DB_CONNECTION') + ->label('Database Driver') + ->hint('The driver used for the panel database. We recommend "SQLite".') + ->required() + ->grouped() + ->options(self::DATABASE_DRIVERS) + ->default(config('database.default', 'sqlite')), + ]); + } +} diff --git a/app/Filament/Pages/Installer/Steps/RedisStep.php b/app/Filament/Pages/Installer/Steps/RedisStep.php new file mode 100644 index 0000000000..5e88b56d80 --- /dev/null +++ b/app/Filament/Pages/Installer/Steps/RedisStep.php @@ -0,0 +1,33 @@ +label('Redis') + ->schema([ + TextInput::make('env.REDIS_HOST') + ->label('Redis Host') + ->hint('The host of your redis server. Make sure it is reachable.') + ->required() + ->default(config('database.redis.default.host')), + TextInput::make('env.REDIS_PORT') + ->label('Redis Port') + ->hint('The port of your redis server.') + ->required() + ->default(config('database.redis.default.port')), + TextInput::make('env.REDIS_PASSWORD') + ->label('Redis Password') + ->hint('The password for your redis server. Can be empty.') + ->password() + ->revealable() + ->default(config('database.redis.default.password')), + ]); + } +} diff --git a/app/Filament/Pages/Installer/Steps/RequirementsStep.php b/app/Filament/Pages/Installer/Steps/RequirementsStep.php new file mode 100644 index 0000000000..35b475c7e0 --- /dev/null +++ b/app/Filament/Pages/Installer/Steps/RequirementsStep.php @@ -0,0 +1,76 @@ += 0; + + $fields = [ + self::makeToggle('php_version') + ->label('PHP Version 8.2/ 8.3') + ->default($phpVersion) + ->validationMessages([ + 'accepted' => 'Your PHP Version needs to be 8.2 or 8.3.', + ]), + ]; + + $phpExtensions = [ + 'GD' => extension_loaded('gd'), + 'MySQL' => extension_loaded('pdo_mysql'), + 'mbstring' => extension_loaded('mbstring'), + 'BCMath' => extension_loaded('bcmath'), + 'XML' => extension_loaded('xml'), + 'cURL' => extension_loaded('curl'), + 'Zip' => extension_loaded('zip'), + 'intl' => extension_loaded('intl'), + 'SQLite3' => extension_loaded('pdo_sqlite'), + 'FPM' => extension_loaded('fpm'), + ]; + + foreach ($phpExtensions as $extension => $loaded) { + $fields[] = self::makeToggle('ext_' . strtolower($extension)) + ->label($extension . ' Extension') + ->default($loaded) + ->validationMessages([ + 'accepted' => 'The ' . $extension . ' extension needs to be installed and enabled.', + ]); + } + + $folderPermissions = [ + 'Storage' => substr(sprintf('%o', fileperms(base_path('storage/'))), -4), + 'Cache' => substr(sprintf('%o', fileperms(base_path('bootstrap/cache/'))), -4), + ]; + + foreach ($folderPermissions as $folder => $permission) { + $correct = $permission >= 755; + $fields[] = self::makeToggle('folder_' . strtolower($folder)) + ->label($folder . ' Folder writeable') + ->default($correct) + ->validationMessages([ + 'accepted' => 'The ' . $folder . ' needs to be writable. (755)', + ]); + } + + return Step::make('requirements') + ->label('Server Requirements') + ->schema($fields); + } + + private static function makeToggle(string $name): Toggle + { + return Toggle::make($name) + ->required() + ->accepted() + ->disabled() + ->onIcon('tabler-check') + ->offIcon('tabler-x') + ->onColor('success') + ->offColor('danger'); + } +} diff --git a/resources/views/filament/pages/installer.blade.php b/resources/views/filament/pages/installer.blade.php new file mode 100644 index 0000000000..4963a2dc5b --- /dev/null +++ b/resources/views/filament/pages/installer.blade.php @@ -0,0 +1,5 @@ + + + {{ $this->form }} + + \ No newline at end of file From d4bb2591eed8c720def2aeaca36e49f84bc1c01b Mon Sep 17 00:00:00 2001 From: Boy132 Date: Fri, 19 Jul 2024 09:06:17 +0200 Subject: [PATCH 03/17] add route for installer --- routes/base.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/routes/base.php b/routes/base.php index 6fbc41ae81..69e1c2f9a7 100644 --- a/routes/base.php +++ b/routes/base.php @@ -1,5 +1,6 @@ withoutMiddleware(['auth', RequireTwoFactorAuthentication::class]) ->where('namespace', '.*'); +Route::get('installer', PanelInstaller::class)->name('installer') + ->withoutMiddleware(['auth', RequireTwoFactorAuthentication::class]); + Route::get('/{react}', [Base\IndexController::class, 'index']) ->where('react', '^(?!(\/)?(api|auth|admin|daemon|legacy)).+'); From 0ba15cbb397ba7627cd6e89564ed8567daabae16 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Fri, 19 Jul 2024 09:06:25 +0200 Subject: [PATCH 04/17] adjust gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8a0a2ef56f..49ae84459c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /public/storage /storage/*.key /storage/clockwork/* +/storage/installed /vendor *.DS_Store* .env From 43aa18ebd0412b118b894ec93e1b7cc7c802db79 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Fri, 19 Jul 2024 09:06:43 +0200 Subject: [PATCH 05/17] set colors globally --- app/Providers/AppServiceProvider.php | 11 +++++++++++ app/Providers/Filament/AdminPanelProvider.php | 10 ---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 7d5ba0c44b..c16586c15e 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -10,6 +10,8 @@ use Dedoc\Scramble\Scramble; use Dedoc\Scramble\Support\Generator\OpenApi; use Dedoc\Scramble\Support\Generator\SecurityScheme; +use Filament\Support\Colors\Color; +use Filament\Support\Facades\FilamentColor; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Pagination\Paginator; use Illuminate\Support\Facades\Broadcast; @@ -80,6 +82,15 @@ public function boot(): void Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) { $event->extendSocialite('discord', \SocialiteProviders\Discord\Provider::class); }); + + FilamentColor::register([ + 'danger' => Color::Red, + 'gray' => Color::Zinc, + 'info' => Color::Sky, + 'primary' => Color::Blue, + 'success' => Color::Green, + 'warning' => Color::Amber, + ]); } /** diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 923a3a77d3..8c8f6355e2 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -9,7 +9,6 @@ use Filament\Http\Middleware\DispatchServingFilamentEvent; use Filament\Panel; use Filament\PanelProvider; -use Filament\Support\Colors\Color; use Filament\Support\Facades\FilamentAsset; use Filament\Widgets; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; @@ -44,15 +43,6 @@ public function panel(Panel $panel): Panel ->brandLogo(config('app.logo')) ->brandLogoHeight('2rem') ->profile(EditProfile::class, false) - ->colors([ - 'danger' => Color::Red, - 'gray' => Color::Zinc, - 'info' => Color::Sky, - 'primary' => Color::Blue, - 'success' => Color::Green, - 'warning' => Color::Amber, - 'blurple' => Color::hex('#5865F2'), - ]) ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages') ->discoverClusters(in: app_path('Filament/Clusters'), for: 'App\\Filament\\Clusters') From ff245665e7fe68414356deb87b331a6326087ac3 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 23 Jul 2024 09:53:03 +0200 Subject: [PATCH 06/17] add "unsaved data changes" alert --- app/Filament/Pages/Installer/PanelInstaller.php | 9 +++++++++ resources/views/filament/pages/installer.blade.php | 2 ++ 2 files changed, 11 insertions(+) diff --git a/app/Filament/Pages/Installer/PanelInstaller.php b/app/Filament/Pages/Installer/PanelInstaller.php index d55cf8eb34..1b4d946224 100644 --- a/app/Filament/Pages/Installer/PanelInstaller.php +++ b/app/Filament/Pages/Installer/PanelInstaller.php @@ -17,6 +17,7 @@ use Filament\Forms\Form; use Filament\Forms\Get; use Filament\Notifications\Notification; +use Filament\Pages\Concerns\HasUnsavedDataChangesAlert; use Filament\Pages\SimplePage; use Filament\Support\Enums\MaxWidth; use Illuminate\Support\Facades\Artisan; @@ -29,6 +30,7 @@ class PanelInstaller extends SimplePage implements HasForms { use EnvironmentWriterTrait; + use HasUnsavedDataChangesAlert; use InteractsWithForms; public $data = []; @@ -83,6 +85,11 @@ protected function getFormStatePath(): ?string return 'data'; } + protected function hasUnsavedDataChangesAlert(): bool + { + return true; + } + public function submit() { try { @@ -118,6 +125,8 @@ public function submit() // Install setup complete file_put_contents(storage_path('installed'), 'installed'); + $this->rememberData(); + Notification::make() ->title('Successfully Installed') ->success() diff --git a/resources/views/filament/pages/installer.blade.php b/resources/views/filament/pages/installer.blade.php index 4963a2dc5b..1977991c2c 100644 --- a/resources/views/filament/pages/installer.blade.php +++ b/resources/views/filament/pages/installer.blade.php @@ -2,4 +2,6 @@ {{ $this->form }} + + \ No newline at end of file From ba73afe64a1b388eeaf024f9da57dc5b72525ace Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 23 Jul 2024 09:53:26 +0200 Subject: [PATCH 07/17] add helper method to check if panel is installed --- app/Filament/Pages/Installer/PanelInstaller.php | 2 +- app/helpers.php | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/Filament/Pages/Installer/PanelInstaller.php b/app/Filament/Pages/Installer/PanelInstaller.php index 1b4d946224..fe9a997de8 100644 --- a/app/Filament/Pages/Installer/PanelInstaller.php +++ b/app/Filament/Pages/Installer/PanelInstaller.php @@ -44,7 +44,7 @@ public function getMaxWidth(): MaxWidth|string public function mount() { - if (file_exists(storage_path('installed'))) { + if (is_installed()) { abort(404); } diff --git a/app/helpers.php b/app/helpers.php index c2aa5cd749..8b23f89f8f 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -40,3 +40,10 @@ function object_get_strict(object $object, ?string $key, mixed $default = null): return $object; } } + +if (!function_exists('is_installed')) { + function is_installed(): bool + { + return file_exists(storage_path('installed')); + } +} From 6740abbfb320b8c9f81c9a160ba577b7f495371c Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 23 Jul 2024 10:06:06 +0200 Subject: [PATCH 08/17] make nicer --- .../Pages/Installer/Steps/DatabaseStep.php | 17 ++++++++++++----- .../Pages/Installer/Steps/EnvironmentStep.php | 19 +++++++++++++------ .../Pages/Installer/Steps/RedisStep.php | 16 +++++++++++++--- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/app/Filament/Pages/Installer/Steps/DatabaseStep.php b/app/Filament/Pages/Installer/Steps/DatabaseStep.php index f16b8c6356..94a7952e2f 100644 --- a/app/Filament/Pages/Installer/Steps/DatabaseStep.php +++ b/app/Filament/Pages/Installer/Steps/DatabaseStep.php @@ -16,21 +16,26 @@ public static function make(): Step { return Step::make('database') ->label('Database') + ->columns() ->schema([ TextInput::make('env.DB_DATABASE') ->label(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name') - ->hint(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.') + ->columnSpanFull() + ->hintIcon('tabler-question-mark') + ->hintIconTooltip(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.') ->required() ->default(fn (Get $get) => env('DB_DATABASE', $get('env.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')), TextInput::make('env.DB_HOST') ->label('Database Host') - ->hint('The host of your database. Make sure it is reachable.') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The host of your database. Make sure it is reachable.') ->required() ->default(env('DB_HOST', '127.0.0.1')) ->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'), TextInput::make('env.DB_PORT') ->label('Database Port') - ->hint('The port of your database.') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The port of your database.') ->required() ->numeric() ->minValue(1) @@ -39,13 +44,15 @@ public static function make(): Step ->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'), TextInput::make('env.DB_USERNAME') ->label('Database Username') - ->hint('The name of your database user.') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The name of your database user.') ->required() ->default(env('DB_USERNAME', 'pelican')) ->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'), TextInput::make('env.DB_PASSWORD') ->label('Database Password') - ->hint('The password of your database user. Can be empty.') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The password of your database user. Can be empty.') ->password() ->revealable() ->default(env('DB_PASSWORD')) diff --git a/app/Filament/Pages/Installer/Steps/EnvironmentStep.php b/app/Filament/Pages/Installer/Steps/EnvironmentStep.php index ddfcdfd321..2227201c89 100644 --- a/app/Filament/Pages/Installer/Steps/EnvironmentStep.php +++ b/app/Filament/Pages/Installer/Steps/EnvironmentStep.php @@ -38,15 +38,18 @@ public static function make(): Step { return Step::make('environment') ->label('Environment') + ->columns() ->schema([ TextInput::make('env.APP_NAME') ->label('App Name') - ->hint('This will be the Name of your Panel.') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('This will be the Name of your Panel.') ->required() ->default(config('app.name')), TextInput::make('env.APP_URL') ->label('App URL') - ->hint('This will be the URL you access your Panel from.') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('This will be the URL you access your Panel from.') ->required() ->default(config('app.url')) ->live() @@ -56,28 +59,32 @@ public static function make(): Step ->default(env('SESSION_SECURE_COOKIE')), ToggleButtons::make('env.CACHE_STORE') ->label('Cache Driver') - ->hint('The driver used for caching. We recommend "Filesystem".') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The driver used for caching. We recommend "Filesystem".') ->required() ->grouped() ->options(self::CACHE_DRIVERS) ->default(config('cache.default', 'file')), ToggleButtons::make('env.SESSION_DRIVER') ->label('Session Driver') - ->hint('The driver used for storing sessions. We recommend "Filesystem" or "Database".') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".') ->required() ->grouped() ->options(self::SESSION_DRIVERS) ->default(config('session.driver', 'file')), ToggleButtons::make('env.QUEUE_CONNECTION') ->label('Queue Driver') - ->hint('The driver used for handling queues. We recommend "Database".') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The driver used for handling queues. We recommend "Database".') ->required() ->grouped() ->options(self::QUEUE_DRIVERS) ->default(config('queue.default', 'database')), ToggleButtons::make('env.DB_CONNECTION') ->label('Database Driver') - ->hint('The driver used for the panel database. We recommend "SQLite".') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".') ->required() ->grouped() ->options(self::DATABASE_DRIVERS) diff --git a/app/Filament/Pages/Installer/Steps/RedisStep.php b/app/Filament/Pages/Installer/Steps/RedisStep.php index 5e88b56d80..a65f263c44 100644 --- a/app/Filament/Pages/Installer/Steps/RedisStep.php +++ b/app/Filament/Pages/Installer/Steps/RedisStep.php @@ -11,20 +11,30 @@ public static function make(): Step { return Step::make('redis') ->label('Redis') + ->columns() ->schema([ TextInput::make('env.REDIS_HOST') ->label('Redis Host') - ->hint('The host of your redis server. Make sure it is reachable.') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The host of your redis server. Make sure it is reachable.') ->required() ->default(config('database.redis.default.host')), TextInput::make('env.REDIS_PORT') ->label('Redis Port') - ->hint('The port of your redis server.') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The port of your redis server.') ->required() ->default(config('database.redis.default.port')), + TextInput::make('env.REDIS_USERNAME') + ->label('Redis Username') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The name of your redis user.') + ->required() + ->default(config('database.redis.default.username')), TextInput::make('env.REDIS_PASSWORD') ->label('Redis Password') - ->hint('The password for your redis server. Can be empty.') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The password for your redis user. Can be empty.') ->password() ->revealable() ->default(config('database.redis.default.password')), From bef2364c13f5c3187a9cf726228477ba10f4b87c Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 23 Jul 2024 10:12:44 +0200 Subject: [PATCH 09/17] redis username isn't required --- app/Filament/Pages/Installer/Steps/RedisStep.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Filament/Pages/Installer/Steps/RedisStep.php b/app/Filament/Pages/Installer/Steps/RedisStep.php index a65f263c44..04a4b1b72f 100644 --- a/app/Filament/Pages/Installer/Steps/RedisStep.php +++ b/app/Filament/Pages/Installer/Steps/RedisStep.php @@ -28,8 +28,7 @@ public static function make(): Step TextInput::make('env.REDIS_USERNAME') ->label('Redis Username') ->hintIcon('tabler-question-mark') - ->hintIconTooltip('The name of your redis user.') - ->required() + ->hintIconTooltip('The name of your redis user. Can be empty') ->default(config('database.redis.default.username')), TextInput::make('env.REDIS_PASSWORD') ->label('Redis Password') From 7e2f8a36b34ca7148c8eac37ad26b17e854bf482 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 23 Jul 2024 10:19:50 +0200 Subject: [PATCH 10/17] bring back db settings command --- .../Environment/DatabaseSettingsCommand.php | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 app/Console/Commands/Environment/DatabaseSettingsCommand.php diff --git a/app/Console/Commands/Environment/DatabaseSettingsCommand.php b/app/Console/Commands/Environment/DatabaseSettingsCommand.php new file mode 100644 index 0000000000..f05f9bbc4b --- /dev/null +++ b/app/Console/Commands/Environment/DatabaseSettingsCommand.php @@ -0,0 +1,185 @@ + 'SQLite (recommended)', + 'mariadb' => 'MariaDB', + 'mysql' => 'MySQL', + ]; + + protected $description = 'Configure database settings for the Panel.'; + + protected $signature = 'p:environment:database + {--driver= : The database driver backend to use.} + {--database= : The database to use.} + {--host= : The connection address for the MySQL/ MariaDB server.} + {--port= : The connection port for the MySQL/ MariaDB server.} + {--username= : Username to use when connecting to the MySQL/ MariaDB server.} + {--password= : Password to use for the MySQL/ MariaDB database.}'; + + protected array $variables = []; + + /** + * DatabaseSettingsCommand constructor. + */ + public function __construct(private DatabaseManager $database, private Kernel $console) + { + parent::__construct(); + } + + /** + * Handle command execution. + */ + public function handle(): int + { + $selected = config('database.default', 'sqlite'); + $this->variables['DB_CONNECTION'] = $this->option('driver') ?? $this->choice( + 'Database Driver', + self::DATABASE_DRIVERS, + array_key_exists($selected, self::DATABASE_DRIVERS) ? $selected : null + ); + + if ($this->variables['DB_CONNECTION'] === 'mysql') { + $this->output->note(__('commands.database_settings.DB_HOST_note')); + $this->variables['DB_HOST'] = $this->option('host') ?? $this->ask( + 'Database Host', + config('database.connections.mysql.host', '127.0.0.1') + ); + + $this->variables['DB_PORT'] = $this->option('port') ?? $this->ask( + 'Database Port', + config('database.connections.mysql.port', 3306) + ); + + $this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask( + 'Database Name', + config('database.connections.mysql.database', 'panel') + ); + + $this->output->note(__('commands.database_settings.DB_USERNAME_note')); + $this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask( + 'Database Username', + config('database.connections.mysql.username', 'pelican') + ); + + $askForMySQLPassword = true; + if (!empty(config('database.connections.mysql.password')) && $this->input->isInteractive()) { + $this->variables['DB_PASSWORD'] = config('database.connections.mysql.password'); + $askForMySQLPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note')); + } + + if ($askForMySQLPassword) { + $this->variables['DB_PASSWORD'] = $this->option('password') ?? $this->secret('Database Password'); + } + + try { + // Test connection + config()->set('database.connections._panel_command_test', [ + 'driver' => 'mysql', + 'host' => $this->variables['DB_HOST'], + 'port' => $this->variables['DB_PORT'], + 'database' => $this->variables['DB_DATABASE'], + 'username' => $this->variables['DB_USERNAME'], + 'password' => $this->variables['DB_PASSWORD'], + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'strict' => true, + ]); + + $this->database->connection('_panel_command_test')->getPdo(); + } catch (\PDOException $exception) { + $this->output->error(sprintf('Unable to connect to the MySQL server using the provided credentials. The error returned was "%s".', $exception->getMessage())); + $this->output->error(__('commands.database_settings.DB_error_2')); + + if ($this->confirm(__('commands.database_settings.go_back'))) { + $this->database->disconnect('_panel_command_test'); + + return $this->handle(); + } + + return 1; + } + } elseif ($this->variables['DB_CONNECTION'] === 'mariadb') { + $this->output->note(__('commands.database_settings.DB_HOST_note')); + $this->variables['DB_HOST'] = $this->option('host') ?? $this->ask( + 'Database Host', + config('database.connections.mariadb.host', '127.0.0.1') + ); + + $this->variables['DB_PORT'] = $this->option('port') ?? $this->ask( + 'Database Port', + config('database.connections.mariadb.port', 3306) + ); + + $this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask( + 'Database Name', + config('database.connections.mariadb.database', 'panel') + ); + + $this->output->note(__('commands.database_settings.DB_USERNAME_note')); + $this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask( + 'Database Username', + config('database.connections.mariadb.username', 'pelican') + ); + + $askForMariaDBPassword = true; + if (!empty(config('database.connections.mariadb.password')) && $this->input->isInteractive()) { + $this->variables['DB_PASSWORD'] = config('database.connections.mariadb.password'); + $askForMariaDBPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note')); + } + + if ($askForMariaDBPassword) { + $this->variables['DB_PASSWORD'] = $this->option('password') ?? $this->secret('Database Password'); + } + + try { + // Test connection + config()->set('database.connections._panel_command_test', [ + 'driver' => 'mariadb', + 'host' => $this->variables['DB_HOST'], + 'port' => $this->variables['DB_PORT'], + 'database' => $this->variables['DB_DATABASE'], + 'username' => $this->variables['DB_USERNAME'], + 'password' => $this->variables['DB_PASSWORD'], + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'strict' => true, + ]); + + $this->database->connection('_panel_command_test')->getPdo(); + } catch (\PDOException $exception) { + $this->output->error(sprintf('Unable to connect to the MariaDB server using the provided credentials. The error returned was "%s".', $exception->getMessage())); + $this->output->error(__('commands.database_settings.DB_error_2')); + + if ($this->confirm(__('commands.database_settings.go_back'))) { + $this->database->disconnect('_panel_command_test'); + + return $this->handle(); + } + + return 1; + } + } elseif ($this->variables['DB_CONNECTION'] === 'sqlite') { + $this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask( + 'Database Path', + env('DB_DATABASE', 'database.sqlite') + ); + } + + $this->writeToEnvironment($this->variables); + + $this->info($this->console->output()); + + return 0; + } +} From 8556c488349ce1a5b80d2f5cd34c8d7091d30639 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 23 Jul 2024 10:24:22 +0200 Subject: [PATCH 11/17] store current date in "installed" file --- app/Filament/Pages/Installer/PanelInstaller.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Filament/Pages/Installer/PanelInstaller.php b/app/Filament/Pages/Installer/PanelInstaller.php index fe9a997de8..7edd032457 100644 --- a/app/Filament/Pages/Installer/PanelInstaller.php +++ b/app/Filament/Pages/Installer/PanelInstaller.php @@ -9,6 +9,7 @@ use App\Filament\Pages\Installer\Steps\RequirementsStep; use App\Services\Users\UserCreationService; use App\Traits\Commands\EnvironmentWriterTrait; +use Carbon\Carbon; use Exception; use Filament\Facades\Filament; use Filament\Forms\Components\Wizard; @@ -123,7 +124,7 @@ public function submit() app(UserCreationService::class)->handle($userData); // Install setup complete - file_put_contents(storage_path('installed'), 'installed'); + file_put_contents(storage_path('installed'), Carbon::now()->toDateTimeString()); $this->rememberData(); From c61f811864d16be6efcbb182c37e152553904aec Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 23 Jul 2024 10:31:54 +0200 Subject: [PATCH 12/17] only redirect if install was successfull --- app/Filament/Pages/Installer/PanelInstaller.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Filament/Pages/Installer/PanelInstaller.php b/app/Filament/Pages/Installer/PanelInstaller.php index 7edd032457..d538fac8ea 100644 --- a/app/Filament/Pages/Installer/PanelInstaller.php +++ b/app/Filament/Pages/Installer/PanelInstaller.php @@ -132,6 +132,8 @@ public function submit() ->title('Successfully Installed') ->success() ->send(); + + redirect()->intended(Filament::getUrl()); } catch (Exception $exception) { Notification::make() ->title('Installation Failed') @@ -139,7 +141,5 @@ public function submit() ->danger() ->send(); } - - return redirect()->intended(Filament::getUrl()); } } From d94d799bce8c58e48996473de34ae65d684df214 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 23 Jul 2024 10:55:15 +0200 Subject: [PATCH 13/17] remove fpm requirement --- app/Filament/Pages/Installer/Steps/RequirementsStep.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Filament/Pages/Installer/Steps/RequirementsStep.php b/app/Filament/Pages/Installer/Steps/RequirementsStep.php index 35b475c7e0..fb1b739e64 100644 --- a/app/Filament/Pages/Installer/Steps/RequirementsStep.php +++ b/app/Filament/Pages/Installer/Steps/RequirementsStep.php @@ -30,7 +30,6 @@ public static function make(): Step 'Zip' => extension_loaded('zip'), 'intl' => extension_loaded('intl'), 'SQLite3' => extension_loaded('pdo_sqlite'), - 'FPM' => extension_loaded('fpm'), ]; foreach ($phpExtensions as $extension => $loaded) { From 072eb5f048bbad7925ee3afcc7d870e21926fa16 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 23 Jul 2024 11:12:42 +0200 Subject: [PATCH 14/17] change "installed" marker to env variable --- .env.example | 1 + .gitignore | 1 - app/Filament/Pages/Installer/PanelInstaller.php | 3 +-- app/helpers.php | 3 ++- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index ae71e78e69..db52d2976f 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,7 @@ APP_TIMEZONE=UTC APP_URL=http://panel.test APP_LOCALE=en APP_ENVIRONMENT_ONLY=true +APP_INSTALLED=false LOG_CHANNEL=daily LOG_STACK=single diff --git a/.gitignore b/.gitignore index 49ae84459c..8a0a2ef56f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ /public/storage /storage/*.key /storage/clockwork/* -/storage/installed /vendor *.DS_Store* .env diff --git a/app/Filament/Pages/Installer/PanelInstaller.php b/app/Filament/Pages/Installer/PanelInstaller.php index d538fac8ea..222793a646 100644 --- a/app/Filament/Pages/Installer/PanelInstaller.php +++ b/app/Filament/Pages/Installer/PanelInstaller.php @@ -9,7 +9,6 @@ use App\Filament\Pages\Installer\Steps\RequirementsStep; use App\Services\Users\UserCreationService; use App\Traits\Commands\EnvironmentWriterTrait; -use Carbon\Carbon; use Exception; use Filament\Facades\Filament; use Filament\Forms\Components\Wizard; @@ -124,7 +123,7 @@ public function submit() app(UserCreationService::class)->handle($userData); // Install setup complete - file_put_contents(storage_path('installed'), Carbon::now()->toDateTimeString()); + $this->writeToEnvironment(['APP_INSTALLED' => true]); $this->rememberData(); diff --git a/app/helpers.php b/app/helpers.php index 8b23f89f8f..4f3e87021f 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -44,6 +44,7 @@ function object_get_strict(object $object, ?string $key, mixed $default = null): if (!function_exists('is_installed')) { function is_installed(): bool { - return file_exists(storage_path('installed')); + // This defaults to true so existing panels count as "installed" + return env('APP_INSTALLED', true); } } From e9953287672ebfe332ea9dc0ca963cee7254bd9f Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 23 Jul 2024 13:00:18 +0200 Subject: [PATCH 15/17] improve requirements step --- .../Installer/Steps/RequirementsStep.php | 98 +++++++++++-------- 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/app/Filament/Pages/Installer/Steps/RequirementsStep.php b/app/Filament/Pages/Installer/Steps/RequirementsStep.php index fb1b739e64..483e4ca1bc 100644 --- a/app/Filament/Pages/Installer/Steps/RequirementsStep.php +++ b/app/Filament/Pages/Installer/Steps/RequirementsStep.php @@ -2,74 +2,86 @@ namespace App\Filament\Pages\Installer\Steps; -use Filament\Forms\Components\Toggle; +use Filament\Forms\Components\Placeholder; +use Filament\Forms\Components\Section; use Filament\Forms\Components\Wizard\Step; +use Filament\Notifications\Notification; +use Filament\Support\Exceptions\Halt; class RequirementsStep { public static function make(): Step { - $phpVersion = version_compare(PHP_VERSION, '8.2.0') >= 0; + $correctPhpVersion = version_compare(PHP_VERSION, '8.2.0') >= 0; $fields = [ - self::makeToggle('php_version') - ->label('PHP Version 8.2/ 8.3') - ->default($phpVersion) - ->validationMessages([ - 'accepted' => 'Your PHP Version needs to be 8.2 or 8.3.', + Section::make('PHP Version') + ->description('8.2 or newer') + ->icon($correctPhpVersion ? 'tabler-check' : 'tabler-x') + ->iconColor($correctPhpVersion ? 'success' : 'danger') + ->schema([ + Placeholder::make('') + ->content('Your PHP Version ' . ($correctPhpVersion ? 'is' : 'needs to be') .' 8.2 or newer.'), ]), ]; $phpExtensions = [ - 'GD' => extension_loaded('gd'), - 'MySQL' => extension_loaded('pdo_mysql'), - 'mbstring' => extension_loaded('mbstring'), 'BCMath' => extension_loaded('bcmath'), - 'XML' => extension_loaded('xml'), 'cURL' => extension_loaded('curl'), - 'Zip' => extension_loaded('zip'), + 'GD' => extension_loaded('gd'), 'intl' => extension_loaded('intl'), + 'mbstring' => extension_loaded('mbstring'), + 'MySQL' => extension_loaded('pdo_mysql'), 'SQLite3' => extension_loaded('pdo_sqlite'), + 'XML' => extension_loaded('xml'), + 'Zip' => extension_loaded('zip'), ]; + $allExtensionsInstalled = !in_array(false, $phpExtensions); - foreach ($phpExtensions as $extension => $loaded) { - $fields[] = self::makeToggle('ext_' . strtolower($extension)) - ->label($extension . ' Extension') - ->default($loaded) - ->validationMessages([ - 'accepted' => 'The ' . $extension . ' extension needs to be installed and enabled.', - ]); - } + $fields[] = Section::make('PHP Extensions') + ->description(implode(', ', array_keys($phpExtensions))) + ->icon($allExtensionsInstalled ? 'tabler-check' : 'tabler-x') + ->iconColor($allExtensionsInstalled ? 'success' : 'danger') + ->schema([ + Placeholder::make('') + ->content('All needed PHP Extensions are installed.') + ->visible($allExtensionsInstalled), + Placeholder::make('') + ->content('The following PHP Extensions are missing: ' . implode(', ', array_keys($phpExtensions, false))) + ->visible(!$allExtensionsInstalled), + ]); $folderPermissions = [ - 'Storage' => substr(sprintf('%o', fileperms(base_path('storage/'))), -4), - 'Cache' => substr(sprintf('%o', fileperms(base_path('bootstrap/cache/'))), -4), + 'Storage' => substr(sprintf('%o', fileperms(base_path('storage/'))), -4) >= 755, + 'Cache' => substr(sprintf('%o', fileperms(base_path('bootstrap/cache/'))), -4) >= 755, ]; + $correctFolderPermissions = !in_array(false, $folderPermissions); - foreach ($folderPermissions as $folder => $permission) { - $correct = $permission >= 755; - $fields[] = self::makeToggle('folder_' . strtolower($folder)) - ->label($folder . ' Folder writeable') - ->default($correct) - ->validationMessages([ - 'accepted' => 'The ' . $folder . ' needs to be writable. (755)', - ]); - } + $fields[] = Section::make('Folder Permissions') + ->description(implode(', ', array_keys($folderPermissions))) + ->icon($correctFolderPermissions ? 'tabler-check' : 'tabler-x') + ->iconColor($correctFolderPermissions ? 'success' : 'danger') + ->schema([ + Placeholder::make('') + ->content('All Folders have the correct permissions.') + ->visible($correctFolderPermissions), + Placeholder::make('') + ->content('The following Folders have wrong permissions: ' . implode(', ', array_keys($folderPermissions, false))) + ->visible(!$correctFolderPermissions), + ]); return Step::make('requirements') ->label('Server Requirements') - ->schema($fields); - } + ->schema($fields) + ->afterValidation(function () use ($correctPhpVersion, $allExtensionsInstalled, $correctFolderPermissions) { + if (!$correctPhpVersion || !$allExtensionsInstalled || !$correctFolderPermissions) { + Notification::make() + ->title('Some requirements are missing!') + ->danger() + ->send(); - private static function makeToggle(string $name): Toggle - { - return Toggle::make($name) - ->required() - ->accepted() - ->disabled() - ->onIcon('tabler-check') - ->offIcon('tabler-x') - ->onColor('success') - ->offColor('danger'); + throw new Halt(); + } + }); } } From afe442b5223c78398e3f39cdfe05435d876533c5 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Wed, 31 Jul 2024 09:09:20 +0200 Subject: [PATCH 16/17] add commands to change cache, queue or session drivers respectively --- .../Environment/CacheSettingsCommand.php | 68 ++++++++++++++++++ .../Environment/QueueSettingsCommand.php | 66 ++++++++++++++++++ .../Environment/SessionSettingsCommand.php | 69 +++++++++++++++++++ .../Commands/RequestRedisSettingsTrait.php | 37 ++++++++++ 4 files changed, 240 insertions(+) create mode 100644 app/Console/Commands/Environment/CacheSettingsCommand.php create mode 100644 app/Console/Commands/Environment/QueueSettingsCommand.php create mode 100644 app/Console/Commands/Environment/SessionSettingsCommand.php create mode 100644 app/Traits/Commands/RequestRedisSettingsTrait.php diff --git a/app/Console/Commands/Environment/CacheSettingsCommand.php b/app/Console/Commands/Environment/CacheSettingsCommand.php new file mode 100644 index 0000000000..4870e1bc97 --- /dev/null +++ b/app/Console/Commands/Environment/CacheSettingsCommand.php @@ -0,0 +1,68 @@ + 'Filesystem (default)', + 'database' => 'Database', + 'redis' => 'Redis', + ]; + + protected $description = 'Configure cache settings for the Panel.'; + + protected $signature = 'p:environment:cache + {--driver= : The cache driver backend to use.} + {--redis-host= : Redis host to use for connections.} + {--redis-pass= : Password used to connect to redis.} + {--redis-port= : Port to connect to redis over.}'; + + protected array $variables = []; + + /** + * CacheSettingsCommand constructor. + */ + public function __construct(private Kernel $console) + { + parent::__construct(); + } + + /** + * Handle command execution. + */ + public function handle(): int + { + $selected = config('cache.default', 'file'); + $this->variables['CACHE_STORE'] = $this->option('driver') ?? $this->choice( + 'Cache Driver', + self::CACHE_DRIVERS, + array_key_exists($selected, self::CACHE_DRIVERS) ? $selected : null + ); + + if ($this->variables['CACHE_STORE'] === 'redis') { + $this->requestRedisSettings(); + + if (config('queue.default') !== 'sync') { + $this->call('p:environment:queue-service', [ + '--use-redis' => true, + '--overwrite' => true, + ]); + } + } + + $this->writeToEnvironment($this->variables); + + $this->info($this->console->output()); + + return 0; + } +} diff --git a/app/Console/Commands/Environment/QueueSettingsCommand.php b/app/Console/Commands/Environment/QueueSettingsCommand.php new file mode 100644 index 0000000000..3d48a31239 --- /dev/null +++ b/app/Console/Commands/Environment/QueueSettingsCommand.php @@ -0,0 +1,66 @@ + 'Database (default)', + 'redis' => 'Redis', + 'sync' => 'Synchronous', + ]; + + protected $description = 'Configure queue settings for the Panel.'; + + protected $signature = 'p:environment:queue + {--driver= : The queue driver backend to use.} + {--redis-host= : Redis host to use for connections.} + {--redis-pass= : Password used to connect to redis.} + {--redis-port= : Port to connect to redis over.}'; + + protected array $variables = []; + + /** + * QueueSettingsCommand constructor. + */ + public function __construct(private Kernel $console) + { + parent::__construct(); + } + + /** + * Handle command execution. + */ + public function handle(): int + { + $selected = config('queue.default', 'database'); + $this->variables['QUEUE_CONNECTION'] = $this->option('driver') ?? $this->choice( + 'Queue Driver', + self::QUEUE_DRIVERS, + array_key_exists($selected, self::QUEUE_DRIVERS) ? $selected : null + ); + + if ($this->variables['QUEUE_CONNECTION'] === 'redis') { + $this->requestRedisSettings(); + + $this->call('p:environment:queue-service', [ + '--use-redis' => true, + '--overwrite' => true, + ]); + } + + $this->writeToEnvironment($this->variables); + + $this->info($this->console->output()); + + return 0; + } +} diff --git a/app/Console/Commands/Environment/SessionSettingsCommand.php b/app/Console/Commands/Environment/SessionSettingsCommand.php new file mode 100644 index 0000000000..9a4081c51a --- /dev/null +++ b/app/Console/Commands/Environment/SessionSettingsCommand.php @@ -0,0 +1,69 @@ + 'Filesystem (default)', + 'redis' => 'Redis', + 'database' => 'Database', + 'cookie' => 'Cookie', + ]; + + protected $description = 'Configure session settings for the Panel.'; + + protected $signature = 'p:environment:session + {--driver= : The session driver backend to use.} + {--redis-host= : Redis host to use for connections.} + {--redis-pass= : Password used to connect to redis.} + {--redis-port= : Port to connect to redis over.}'; + + protected array $variables = []; + + /** + * SessionSettingsCommand constructor. + */ + public function __construct(private Kernel $console) + { + parent::__construct(); + } + + /** + * Handle command execution. + */ + public function handle(): int + { + $selected = config('session.driver', 'file'); + $this->variables['SESSION_DRIVER'] = $this->option('driver') ?? $this->choice( + 'Session Driver', + self::SESSION_DRIVERS, + array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null + ); + + if ($this->variables['SESSION_DRIVER'] === 'redis') { + $this->requestRedisSettings(); + + if (config('queue.default') !== 'sync') { + $this->call('p:environment:queue-service', [ + '--use-redis' => true, + '--overwrite' => true, + ]); + } + } + + $this->writeToEnvironment($this->variables); + + $this->info($this->console->output()); + + return 0; + } +} diff --git a/app/Traits/Commands/RequestRedisSettingsTrait.php b/app/Traits/Commands/RequestRedisSettingsTrait.php new file mode 100644 index 0000000000..527915da6d --- /dev/null +++ b/app/Traits/Commands/RequestRedisSettingsTrait.php @@ -0,0 +1,37 @@ +output->note(__('commands.appsettings.redis.note')); + $this->variables['REDIS_HOST'] = $this->option('redis-host') ?? $this->ask( + 'Redis Host', + config('database.redis.default.host') + ); + + $askForRedisPassword = true; + if (!empty(config('database.redis.default.password'))) { + $this->variables['REDIS_PASSWORD'] = config('database.redis.default.password'); + $askForRedisPassword = $this->confirm('It seems a password is already defined for Redis, would you like to change it?'); + } + + if ($askForRedisPassword) { + $this->output->comment(__('commands.appsettings.redis.comment')); + $this->variables['REDIS_PASSWORD'] = $this->option('redis-pass') ?? $this->output->askHidden( + 'Redis Password' + ); + } + + if (empty($this->variables['REDIS_PASSWORD'])) { + $this->variables['REDIS_PASSWORD'] = 'null'; + } + + $this->variables['REDIS_PORT'] = $this->option('redis-port') ?? $this->ask( + 'Redis Port', + config('database.redis.default.port') + ); + } +} From daaf56aacfafea0f84bcaace5c2a2d8fea5c590c Mon Sep 17 00:00:00 2001 From: Boy132 Date: Wed, 31 Jul 2024 09:12:50 +0200 Subject: [PATCH 17/17] removed `grouped` for better mobile view --- app/Filament/Pages/Installer/Steps/EnvironmentStep.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Filament/Pages/Installer/Steps/EnvironmentStep.php b/app/Filament/Pages/Installer/Steps/EnvironmentStep.php index 2227201c89..d9cc5eafaa 100644 --- a/app/Filament/Pages/Installer/Steps/EnvironmentStep.php +++ b/app/Filament/Pages/Installer/Steps/EnvironmentStep.php @@ -62,7 +62,7 @@ public static function make(): Step ->hintIcon('tabler-question-mark') ->hintIconTooltip('The driver used for caching. We recommend "Filesystem".') ->required() - ->grouped() + ->inline() ->options(self::CACHE_DRIVERS) ->default(config('cache.default', 'file')), ToggleButtons::make('env.SESSION_DRIVER') @@ -70,7 +70,7 @@ public static function make(): Step ->hintIcon('tabler-question-mark') ->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".') ->required() - ->grouped() + ->inline() ->options(self::SESSION_DRIVERS) ->default(config('session.driver', 'file')), ToggleButtons::make('env.QUEUE_CONNECTION') @@ -78,7 +78,7 @@ public static function make(): Step ->hintIcon('tabler-question-mark') ->hintIconTooltip('The driver used for handling queues. We recommend "Database".') ->required() - ->grouped() + ->inline() ->options(self::QUEUE_DRIVERS) ->default(config('queue.default', 'database')), ToggleButtons::make('env.DB_CONNECTION') @@ -86,7 +86,7 @@ public static function make(): Step ->hintIcon('tabler-question-mark') ->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".') ->required() - ->grouped() + ->inline() ->options(self::DATABASE_DRIVERS) ->default(config('database.default', 'sqlite')), ]);