Update web installer (#614)

* update web installer

* make sure we have a user

* save SESSION_SECURE_COOKIE as text so it's written correctly to the .env

* set `SESSION_COOKIE` so session doesn't expire when changing the app name

* Allow enter to go to next step

---------

Co-authored-by: notCharles <charles@pelican.dev>
This commit is contained in:
Boy132 2024-10-12 16:19:52 +02:00 committed by GitHub
parent 3c5da1cd70
commit c0eedc16e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 251 additions and 184 deletions

View File

@ -4,6 +4,7 @@ APP_KEY=
APP_TIMEZONE=UTC APP_TIMEZONE=UTC
APP_URL=http://panel.test APP_URL=http://panel.test
APP_LOCALE=en APP_LOCALE=en
APP_INSTALLED=false
LOG_CHANNEL=daily LOG_CHANNEL=daily
LOG_STACK=single LOG_STACK=single
@ -30,3 +31,4 @@ MAIL_FROM_NAME="Pelican Admin"
SESSION_ENCRYPT=false SESSION_ENCRYPT=false
SESSION_PATH=/ SESSION_PATH=/
SESSION_DOMAIN=null SESSION_DOMAIN=null
SESSION_COOKIE=pelican_session

View File

@ -2,20 +2,14 @@
namespace App\Console\Commands\Environment; namespace App\Console\Commands\Environment;
use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
class AppSettingsCommand extends Command class AppSettingsCommand extends Command
{ {
use EnvironmentWriterTrait;
protected $description = 'Configure basic environment settings for the Panel.'; protected $description = 'Configure basic environment settings for the Panel.';
protected $signature = 'p:environment:setup protected $signature = 'p:environment:setup';
{--url= : The URL that this Panel is running on.}';
protected array $variables = [];
public function handle(): void public function handle(): void
{ {
@ -29,22 +23,5 @@ class AppSettingsCommand extends Command
$this->comment('Generating app key'); $this->comment('Generating app key');
Artisan::call('key:generate'); Artisan::call('key:generate');
} }
$this->variables['APP_TIMEZONE'] = 'UTC';
$this->variables['APP_URL'] = $this->option('url') ?? $this->ask(
'Application URL',
config('app.url', 'https://example.com')
);
// 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';
}
$this->comment('Writing variables to .env file');
$this->writeToEnvironment($this->variables);
$this->info("Setup complete. Vist {$this->variables['APP_URL']}/installer to complete the installation");
} }
} }

View File

@ -3,6 +3,7 @@
namespace App\Filament\Pages\Installer; namespace App\Filament\Pages\Installer;
use App\Filament\Pages\Installer\Steps\AdminUserStep; use App\Filament\Pages\Installer\Steps\AdminUserStep;
use App\Filament\Pages\Installer\Steps\CompletedStep;
use App\Filament\Pages\Installer\Steps\DatabaseStep; use App\Filament\Pages\Installer\Steps\DatabaseStep;
use App\Filament\Pages\Installer\Steps\EnvironmentStep; use App\Filament\Pages\Installer\Steps\EnvironmentStep;
use App\Filament\Pages\Installer\Steps\RedisStep; use App\Filament\Pages\Installer\Steps\RedisStep;
@ -12,15 +13,17 @@ use App\Services\Users\UserCreationService;
use App\Traits\CheckMigrationsTrait; use App\Traits\CheckMigrationsTrait;
use App\Traits\EnvironmentWriterTrait; use App\Traits\EnvironmentWriterTrait;
use Exception; use Exception;
use Filament\Facades\Filament;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Wizard; use Filament\Forms\Components\Wizard;
use Filament\Forms\Concerns\InteractsWithForms; use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms; use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Forms\Get; use Filament\Forms\Get;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
use Filament\Pages\SimplePage; use Filament\Pages\SimplePage;
use Filament\Support\Enums\MaxWidth; use Filament\Support\Enums\MaxWidth;
use Filament\Support\Exceptions\Halt;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
@ -32,56 +35,46 @@ class PanelInstaller extends SimplePage implements HasForms
{ {
use CheckMigrationsTrait; use CheckMigrationsTrait;
use EnvironmentWriterTrait; use EnvironmentWriterTrait;
use HasUnsavedDataChangesAlert;
use InteractsWithForms; use InteractsWithForms;
public $data = []; public $data = [];
protected static string $view = 'filament.pages.installer'; protected static string $view = 'filament.pages.installer';
private User $user;
public function getMaxWidth(): MaxWidth|string public function getMaxWidth(): MaxWidth|string
{ {
return MaxWidth::SevenExtraLarge; return MaxWidth::SevenExtraLarge;
} }
public static function show(): bool public static function isInstalled(): bool
{ {
if (User::count() <= 0) { // This defaults to true so existing panels count as "installed"
return true; return env('APP_INSTALLED', true);
}
if (config('panel.client_features.installer.enabled')) {
return true;
}
return false;
} }
public function mount() public function mount()
{ {
abort_unless(self::show(), 404); abort_if(self::isInstalled(), 404);
$this->form->fill(); $this->form->fill();
} }
public function dehydrate(): void
{
Artisan::call('config:clear');
Artisan::call('cache:clear');
}
protected function getFormSchema(): array protected function getFormSchema(): array
{ {
return [ return [
Wizard::make([ Wizard::make([
RequirementsStep::make(), RequirementsStep::make(),
EnvironmentStep::make(), EnvironmentStep::make($this),
DatabaseStep::make(), DatabaseStep::make($this),
RedisStep::make() RedisStep::make($this)
->hidden(fn (Get $get) => $get('env.SESSION_DRIVER') != 'redis' && $get('env.QUEUE_CONNECTION') != 'redis' && $get('env.CACHE_STORE') != 'redis'), ->hidden(fn (Get $get) => $get('env_general.SESSION_DRIVER') != 'redis' && $get('env_general.QUEUE_CONNECTION') != 'redis' && $get('env_general.CACHE_STORE') != 'redis'),
AdminUserStep::make(), AdminUserStep::make($this),
CompletedStep::make(),
]) ])
->persistStepInQueryString() ->persistStepInQueryString()
->nextAction(fn (Action $action) => $action->keyBindings('enter'))
->submitAction(new HtmlString(Blade::render(<<<'BLADE' ->submitAction(new HtmlString(Blade::render(<<<'BLADE'
<x-filament::button <x-filament::button
type="submit" type="submit"
@ -100,61 +93,89 @@ class PanelInstaller extends SimplePage implements HasForms
return 'data'; return 'data';
} }
protected function hasUnsavedDataChangesAlert(): bool
{
return true;
}
public function submit() public function submit()
{ {
try { // Disable installer
$inputs = $this->form->getState(); $this->writeToEnvironment(['APP_INSTALLED' => 'true']);
// Write variables to .env file // Login user
$variables = array_get($inputs, 'env'); $this->user ??= User::all()->filter(fn ($user) => $user->isRootAdmin())->first();
$this->writeToEnvironment($variables); auth()->guard()->login($this->user, true);
// Clear config cache // Redirect to admin panel
Artisan::call('config:clear'); return redirect(Filament::getPanel('admin')->getUrl());
// Run migrations
Artisan::call('migrate', [
'--force' => true,
'--seed' => true,
'--database' => $variables['DB_CONNECTION'],
]);
if (!$this->hasCompletedMigrations()) {
throw new Exception('Migrations didn\'t run successfully. Double check your database configuration.');
} }
// Create first admin user public function writeToEnv(string $key): void
$userData = array_get($inputs, 'user'); {
$userData['root_admin'] = true; try {
$user = app(UserCreationService::class)->handle($userData); $variables = array_get($this->data, $key);
$this->writeToEnvironment($variables);
// Install setup complete
$this->writeToEnvironment(['APP_INSTALLER' => 'false']);
$this->rememberData();
Notification::make()
->title('Successfully Installed')
->success()
->send();
auth()->loginUsingId($user->id);
return redirect('/admin');
} catch (Exception $exception) { } catch (Exception $exception) {
report($exception); report($exception);
Notification::make() Notification::make()
->title('Installation Failed') ->title('Could not write to .env file')
->body($exception->getMessage()) ->body($exception->getMessage())
->danger() ->danger()
->persistent() ->persistent()
->send(); ->send();
throw new Halt('Error while writing .env file');
}
Artisan::call('config:clear');
}
public function runMigrations(string $driver): void
{
try {
Artisan::call('migrate', [
'--force' => true,
'--seed' => true,
'--database' => $driver,
]);
} catch (Exception $exception) {
report($exception);
Notification::make()
->title('Migrations failed')
->body($exception->getMessage())
->danger()
->persistent()
->send();
throw new Halt('Error while running migrations');
}
if (!$this->hasCompletedMigrations()) {
Notification::make()
->title('Migrations failed')
->danger()
->persistent()
->send();
throw new Halt('Migrations failed');
}
}
public function createAdminUser(): void
{
try {
$userData = array_get($this->data, 'user');
$userData['root_admin'] = true;
$this->user = app(UserCreationService::class)->handle($userData);
} catch (Exception $exception) {
report($exception);
Notification::make()
->title('Could not create admin user')
->body($exception->getMessage())
->danger()
->persistent()
->send();
throw new Halt('Error while creating admin user');
} }
} }
} }

View File

@ -2,12 +2,13 @@
namespace App\Filament\Pages\Installer\Steps; namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step; use Filament\Forms\Components\Wizard\Step;
class AdminUserStep class AdminUserStep
{ {
public static function make(): Step public static function make(PanelInstaller $installer): Step
{ {
return Step::make('user') return Step::make('user')
->label('Admin User') ->label('Admin User')
@ -26,6 +27,7 @@ class AdminUserStep
->required() ->required()
->password() ->password()
->revealable(), ->revealable(),
]); ])
->afterValidation(fn () => $installer->createAdminUser());
} }
} }

View File

@ -0,0 +1,34 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class CompletedStep
{
public static function make(): Step
{
return Step::make('complete')
->label('Setup complete')
->schema([
Placeholder::make('')
->content(new HtmlString('The setup is nearly complete!<br>As last step you need to create a new cronjob that runs every minute to process specific tasks, such as session cleanup and scheduled tasks, and also create a queue worker.')),
TextInput::make('crontab')
->label(new HtmlString('Run the following command to setup your crontab. Note that <code>www-data</code> is your webserver user. On some systems this username might be different!'))
->disabled()
->hintAction(CopyAction::make())
->default('(crontab -l -u www-data 2>/dev/null; echo "* * * * * php ' . base_path() . '/artisan schedule:run >> /dev/null 2>&1") | crontab -u www-data -'),
TextInput::make('queueService')
->label(new HtmlString('To setup the queue worker service you simply have to run the following command.'))
->disabled()
->hintAction(CopyAction::make())
->default('sudo php ' . base_path() . '/artisan p:environment:queue-service'),
Placeholder::make('')
->content('After you finished these two last tasks you can click on "Finish" and use your new panel! Have fun!'),
]);
}
}

View File

@ -2,37 +2,38 @@
namespace App\Filament\Pages\Installer\Steps; namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use Exception;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step; use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Get; use Filament\Forms\Get;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Support\Exceptions\Halt; use Filament\Support\Exceptions\Halt;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use PDOException;
class DatabaseStep class DatabaseStep
{ {
public static function make(): Step public static function make(PanelInstaller $installer): Step
{ {
return Step::make('database') return Step::make('database')
->label('Database') ->label('Database')
->columns() ->columns()
->schema([ ->schema([
TextInput::make('env.DB_DATABASE') TextInput::make('env_database.DB_DATABASE')
->label(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name') ->label(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
->columnSpanFull() ->columnSpanFull()
->hintIcon('tabler-question-mark') ->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.') ->hintIconTooltip(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.')
->required() ->required()
->default(fn (Get $get) => env('DB_DATABASE', $get('env.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')), ->default(fn (Get $get) => env('DB_DATABASE', $get('env_general.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')),
TextInput::make('env.DB_HOST') TextInput::make('env_database.DB_HOST')
->label('Database Host') ->label('Database Host')
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark')
->hintIconTooltip('The host of your database. Make sure it is reachable.') ->hintIconTooltip('The host of your database. Make sure it is reachable.')
->required() ->required()
->default(env('DB_HOST', '127.0.0.1')) ->default(env('DB_HOST', '127.0.0.1'))
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'), ->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'),
TextInput::make('env.DB_PORT') TextInput::make('env_database.DB_PORT')
->label('Database Port') ->label('Database Port')
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark')
->hintIconTooltip('The port of your database.') ->hintIconTooltip('The port of your database.')
@ -41,52 +42,68 @@ class DatabaseStep
->minValue(1) ->minValue(1)
->maxValue(65535) ->maxValue(65535)
->default(env('DB_PORT', 3306)) ->default(env('DB_PORT', 3306))
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'), ->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'),
TextInput::make('env.DB_USERNAME') TextInput::make('env_database.DB_USERNAME')
->label('Database Username') ->label('Database Username')
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark')
->hintIconTooltip('The name of your database user.') ->hintIconTooltip('The name of your database user.')
->required() ->required()
->default(env('DB_USERNAME', 'pelican')) ->default(env('DB_USERNAME', 'pelican'))
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'), ->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'),
TextInput::make('env.DB_PASSWORD') TextInput::make('env_database.DB_PASSWORD')
->label('Database Password') ->label('Database Password')
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark')
->hintIconTooltip('The password of your database user. Can be empty.') ->hintIconTooltip('The password of your database user. Can be empty.')
->password() ->password()
->revealable() ->revealable()
->default(env('DB_PASSWORD')) ->default(env('DB_PASSWORD'))
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'), ->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'),
]) ])
->afterValidation(function (Get $get) { ->afterValidation(function (Get $get) use ($installer) {
$driver = $get('env.DB_CONNECTION'); $driver = $get('env_general.DB_CONNECTION');
if ($driver !== 'sqlite') {
if (!self::testConnection($driver, $get('env_database.DB_HOST'), $get('env_database.DB_PORT'), $get('env_database.DB_DATABASE'), $get('env_database.DB_USERNAME'), $get('env_database.DB_PASSWORD'))) {
throw new Halt('Database connection failed');
}
$installer->writeToEnv('env_database');
$installer->runMigrations($driver);
});
}
private static function testConnection(string $driver, $host, $port, $database, $username, $password): bool
{
if ($driver === 'sqlite') {
return true;
}
try { try {
config()->set('database.connections._panel_install_test', [ config()->set('database.connections._panel_install_test', [
'driver' => $driver, 'driver' => $driver,
'host' => $get('env.DB_HOST'), 'host' => $host,
'port' => $get('env.DB_PORT'), 'port' => $port,
'database' => $get('env.DB_DATABASE'), 'database' => $database,
'username' => $get('env.DB_USERNAME'), 'username' => $username,
'password' => $get('env.DB_PASSWORD'), 'password' => $password,
'charset' => 'utf8mb4', 'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci', 'collation' => 'utf8mb4_unicode_ci',
'strict' => true, 'strict' => true,
]); ]);
DB::connection('_panel_install_test')->getPdo(); DB::connection('_panel_install_test')->getPdo();
} catch (PDOException $exception) { } catch (Exception $exception) {
DB::disconnect('_panel_install_test');
Notification::make() Notification::make()
->title('Database connection failed') ->title('Database connection failed')
->body($exception->getMessage()) ->body($exception->getMessage())
->danger() ->danger()
->send(); ->send();
DB::disconnect('_panel_install_test'); return false;
}
throw new Halt('Database connection failed'); return true;
}
}
});
} }
} }

View File

@ -2,14 +2,17 @@
namespace App\Filament\Pages\Installer\Steps; namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use App\Traits\EnvironmentWriterTrait;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons; use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\Wizard\Step; use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Set; use Filament\Forms\Set;
class EnvironmentStep class EnvironmentStep
{ {
use EnvironmentWriterTrait;
public const CACHE_DRIVERS = [ public const CACHE_DRIVERS = [
'file' => 'Filesystem', 'file' => 'Filesystem',
'redis' => 'Redis', 'redis' => 'Redis',
@ -17,14 +20,14 @@ class EnvironmentStep
public const SESSION_DRIVERS = [ public const SESSION_DRIVERS = [
'file' => 'Filesystem', 'file' => 'Filesystem',
'redis' => 'Redis',
'database' => 'Database', 'database' => 'Database',
'cookie' => 'Cookie', 'cookie' => 'Cookie',
'redis' => 'Redis',
]; ];
public const QUEUE_DRIVERS = [ public const QUEUE_DRIVERS = [
'sync' => 'Sync',
'database' => 'Database', 'database' => 'Database',
'sync' => 'Sync',
'redis' => 'Redis', 'redis' => 'Redis',
]; ];
@ -34,30 +37,30 @@ class EnvironmentStep
'mysql' => 'MySQL', 'mysql' => 'MySQL',
]; ];
public static function make(): Step public static function make(PanelInstaller $installer): Step
{ {
return Step::make('environment') return Step::make('environment')
->label('Environment') ->label('Environment')
->columns() ->columns()
->schema([ ->schema([
TextInput::make('env.APP_NAME') TextInput::make('env_general.APP_NAME')
->label('App Name') ->label('App Name')
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark')
->hintIconTooltip('This will be the Name of your Panel.') ->hintIconTooltip('This will be the Name of your Panel.')
->required() ->required()
->default(config('app.name')), ->default(config('app.name')),
TextInput::make('env.APP_URL') TextInput::make('env_general.APP_URL')
->label('App URL') ->label('App URL')
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark')
->hintIconTooltip('This will be the URL you access your Panel from.') ->hintIconTooltip('This will be the URL you access your Panel from.')
->required() ->required()
->default(config('app.url')) ->default(url(''))
->live() ->live()
->afterStateUpdated(fn ($state, Set $set) => $set('env.SESSION_SECURE_COOKIE', str_starts_with($state, 'https://'))), ->afterStateUpdated(fn ($state, Set $set) => $set('env_general.SESSION_SECURE_COOKIE', str_starts_with($state, 'https://') ? 'true' : 'false')),
Toggle::make('env.SESSION_SECURE_COOKIE') TextInput::make('env_general.SESSION_SECURE_COOKIE')
->hidden() ->hidden()
->default(env('SESSION_SECURE_COOKIE')), ->default(str_starts_with(url(''), 'https://') ? 'true' : 'false'),
ToggleButtons::make('env.CACHE_STORE') ToggleButtons::make('env_general.CACHE_STORE')
->label('Cache Driver') ->label('Cache Driver')
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for caching. We recommend "Filesystem".') ->hintIconTooltip('The driver used for caching. We recommend "Filesystem".')
@ -65,7 +68,7 @@ class EnvironmentStep
->inline() ->inline()
->options(self::CACHE_DRIVERS) ->options(self::CACHE_DRIVERS)
->default(config('cache.default', 'file')), ->default(config('cache.default', 'file')),
ToggleButtons::make('env.SESSION_DRIVER') ToggleButtons::make('env_general.SESSION_DRIVER')
->label('Session Driver') ->label('Session Driver')
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".') ->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".')
@ -73,15 +76,15 @@ class EnvironmentStep
->inline() ->inline()
->options(self::SESSION_DRIVERS) ->options(self::SESSION_DRIVERS)
->default(config('session.driver', 'file')), ->default(config('session.driver', 'file')),
ToggleButtons::make('env.QUEUE_CONNECTION') ToggleButtons::make('env_general.QUEUE_CONNECTION')
->label('Queue Driver') ->label('Queue Driver')
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for handling queues. We recommend "Sync" or "Database".') ->hintIconTooltip('The driver used for handling queues. We recommend "Database".')
->required() ->required()
->inline() ->inline()
->options(self::QUEUE_DRIVERS) ->options(self::QUEUE_DRIVERS)
->default(config('queue.default', 'database')), ->default(config('queue.default', 'database')),
ToggleButtons::make('env.DB_CONNECTION') ToggleButtons::make('env_general.DB_CONNECTION')
->label('Database Driver') ->label('Database Driver')
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".') ->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".')
@ -89,6 +92,7 @@ class EnvironmentStep
->inline() ->inline()
->options(self::DATABASE_DRIVERS) ->options(self::DATABASE_DRIVERS)
->default(config('database.default', 'sqlite')), ->default(config('database.default', 'sqlite')),
]); ])
->afterValidation(fn () => $installer->writeToEnv('env_general'));
} }
} }

View File

@ -2,6 +2,8 @@
namespace App\Filament\Pages\Installer\Steps; namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use App\Traits\EnvironmentWriterTrait;
use Exception; use Exception;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step; use Filament\Forms\Components\Wizard\Step;
@ -12,30 +14,32 @@ use Illuminate\Support\Facades\Redis;
class RedisStep class RedisStep
{ {
public static function make(): Step use EnvironmentWriterTrait;
public static function make(PanelInstaller $installer): Step
{ {
return Step::make('redis') return Step::make('redis')
->label('Redis') ->label('Redis')
->columns() ->columns()
->schema([ ->schema([
TextInput::make('env.REDIS_HOST') TextInput::make('env_redis.REDIS_HOST')
->label('Redis Host') ->label('Redis Host')
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark')
->hintIconTooltip('The host of your redis server. Make sure it is reachable.') ->hintIconTooltip('The host of your redis server. Make sure it is reachable.')
->required() ->required()
->default(config('database.redis.default.host')), ->default(config('database.redis.default.host')),
TextInput::make('env.REDIS_PORT') TextInput::make('env_redis.REDIS_PORT')
->label('Redis Port') ->label('Redis Port')
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark')
->hintIconTooltip('The port of your redis server.') ->hintIconTooltip('The port of your redis server.')
->required() ->required()
->default(config('database.redis.default.port')), ->default(config('database.redis.default.port')),
TextInput::make('env.REDIS_USERNAME') TextInput::make('env_redis.REDIS_USERNAME')
->label('Redis Username') ->label('Redis Username')
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark')
->hintIconTooltip('The name of your redis user. Can be empty') ->hintIconTooltip('The name of your redis user. Can be empty')
->default(config('database.redis.default.username')), ->default(config('database.redis.default.username')),
TextInput::make('env.REDIS_PASSWORD') TextInput::make('env_redis.REDIS_PASSWORD')
->label('Redis Password') ->label('Redis Password')
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark')
->hintIconTooltip('The password for your redis user. Can be empty.') ->hintIconTooltip('The password for your redis user. Can be empty.')
@ -43,13 +47,23 @@ class RedisStep
->revealable() ->revealable()
->default(config('database.redis.default.password')), ->default(config('database.redis.default.password')),
]) ])
->afterValidation(function (Get $get) { ->afterValidation(function (Get $get) use ($installer) {
if (!self::testConnection($get('env_redis.REDIS_HOST'), $get('env_redis.REDIS_PORT'), $get('env_redis.REDIS_USERNAME'), $get('env_redis.REDIS_PASSWORD'))) {
throw new Halt('Redis connection failed');
}
$installer->writeToEnv('env_redis');
});
}
private static function testConnection($host, $port, $username, $password): bool
{
try { try {
config()->set('database.redis._panel_install_test', [ config()->set('database.redis._panel_install_test', [
'host' => $get('env.REDIS_HOST'), 'host' => $host,
'username' => $get('env.REDIS_USERNAME'), 'port' => $port,
'password' => $get('env.REDIS_PASSWORD'), 'username' => $username,
'port' => $get('env.REDIS_PORT'), 'password' => $password,
]); ]);
Redis::connection('_panel_install_test')->command('ping'); Redis::connection('_panel_install_test')->command('ping');
@ -60,8 +74,9 @@ class RedisStep
->danger() ->danger()
->send(); ->send();
throw new Halt('Redis connection failed'); return false;
} }
});
return true;
} }
} }

View File

@ -10,18 +10,20 @@ use Filament\Support\Exceptions\Halt;
class RequirementsStep class RequirementsStep
{ {
public const MIN_PHP_VERSION = '8.2';
public static function make(): Step public static function make(): Step
{ {
$correctPhpVersion = version_compare(PHP_VERSION, '8.2.0') >= 0; $correctPhpVersion = version_compare(PHP_VERSION, self::MIN_PHP_VERSION) >= 0;
$fields = [ $fields = [
Section::make('PHP Version') Section::make('PHP Version')
->description('8.2 or newer') ->description(self::MIN_PHP_VERSION . ' or newer')
->icon($correctPhpVersion ? 'tabler-check' : 'tabler-x') ->icon($correctPhpVersion ? 'tabler-check' : 'tabler-x')
->iconColor($correctPhpVersion ? 'success' : 'danger') ->iconColor($correctPhpVersion ? 'success' : 'danger')
->schema([ ->schema([
Placeholder::make('') Placeholder::make('')
->content('Your PHP Version ' . ($correctPhpVersion ? 'is' : 'needs to be') .' 8.2 or newer.'), ->content('Your PHP Version is ' . PHP_VERSION . '.'),
]), ]),
]; ];
@ -80,7 +82,7 @@ class RequirementsStep
->danger() ->danger()
->send(); ->send();
throw new Halt(); throw new Halt('Some requirements are missing');
} }
}); });
} }

View File

@ -9,7 +9,6 @@ use Illuminate\Http\Request;
use App\Models\User; use App\Models\User;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use App\Facades\Activity; use App\Facades\Activity;
use Illuminate\View\View;
class LoginController extends AbstractLoginController class LoginController extends AbstractLoginController
{ {
@ -20,7 +19,7 @@ class LoginController extends AbstractLoginController
*/ */
public function index() public function index()
{ {
if (PanelInstaller::show()) { if (!PanelInstaller::isInstalled()) {
return redirect('/installer'); return redirect('/installer');
} }

View File

@ -93,10 +93,6 @@ return [
'range_start' => env('PANEL_CLIENT_ALLOCATIONS_RANGE_START'), 'range_start' => env('PANEL_CLIENT_ALLOCATIONS_RANGE_START'),
'range_end' => env('PANEL_CLIENT_ALLOCATIONS_RANGE_END'), 'range_end' => env('PANEL_CLIENT_ALLOCATIONS_RANGE_END'),
], ],
'installer' => [
'enabled' => env('APP_INSTALLER', false),
],
], ],
/* /*

View File

@ -2,6 +2,4 @@
<x-filament-panels::form wire:submit="submit"> <x-filament-panels::form wire:submit="submit">
{{ $this->form }} {{ $this->form }}
</x-filament-panels::form> </x-filament-panels::form>
<x-filament-panels::page.unsaved-data-changes-alert />
</x-filament-panels::page.simple> </x-filament-panels::page.simple>