diff --git a/.env.example b/.env.example index 6a128b13df..84ff1d4324 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,7 @@ APP_KEY= APP_TIMEZONE=UTC APP_URL=http://panel.test APP_LOCALE=en +APP_INSTALLED=false LOG_CHANNEL=daily LOG_STACK=single diff --git a/app/Console/Commands/Environment/AppSettingsCommand.php b/app/Console/Commands/Environment/AppSettingsCommand.php index a480e9b6f5..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,147 +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.}'; + {--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 - ); - - // 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/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/QueueWorkerServiceCommand.php b/app/Console/Commands/Environment/QueueWorkerServiceCommand.php index 607f2ae9bc..dbf1aa7354 100644 --- a/app/Console/Commands/Environment/QueueWorkerServiceCommand.php +++ b/app/Console/Commands/Environment/QueueWorkerServiceCommand.php @@ -14,7 +14,6 @@ class QueueWorkerServiceCommand extends Command {--service-name= : Name of the queue worker service.} {--user= : The user that PHP runs under.} {--group= : The group that PHP runs under.} - {--use-redis : Whether redis is used.} {--overwrite : Force overwrite if the service file already exists.}'; public function handle(): void @@ -32,7 +31,8 @@ public function handle(): void $user = $this->option('user') ?? $this->ask('Webserver User', 'www-data'); $group = $this->option('group') ?? $this->ask('Webserver Group', 'www-data'); - $afterRedis = $this->option('use-redis') ? ' + $redisUsed = config('queue.default') === 'redis' || config('session.driver') === 'redis' || config('cache.default') === 'redis'; + $afterRedis = $redisUsed ? ' After=redis-server.service' : ''; $basePath = base_path(); 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/Filament/Pages/Installer/PanelInstaller.php b/app/Filament/Pages/Installer/PanelInstaller.php new file mode 100644 index 0000000000..00b182af08 --- /dev/null +++ b/app/Filament/Pages/Installer/PanelInstaller.php @@ -0,0 +1,132 @@ +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'; + } + + protected function hasUnsavedDataChangesAlert(): bool + { + return true; + } + + public function submit() + { + try { + $inputs = $this->form->getState(); + + // Write variables to .env file + $variables = array_get($inputs, 'env'); + $this->writeToEnvironment($variables); + + // 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 + $this->writeToEnvironment(['APP_INSTALLED' => true]); + + $this->rememberData(); + + Notification::make() + ->title('Successfully Installed') + ->success() + ->send(); + + redirect()->intended(Filament::getUrl()); + } catch (Exception $exception) { + Notification::make() + ->title('Installation Failed') + ->body($exception->getMessage()) + ->danger() + ->send(); + } + } +} 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..94a7952e2f --- /dev/null +++ b/app/Filament/Pages/Installer/Steps/DatabaseStep.php @@ -0,0 +1,95 @@ +label('Database') + ->columns() + ->schema([ + TextInput::make('env.DB_DATABASE') + ->label(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name') + ->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') + ->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') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('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') + ->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') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('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..d9cc5eafaa --- /dev/null +++ b/app/Filament/Pages/Installer/Steps/EnvironmentStep.php @@ -0,0 +1,94 @@ + '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') + ->columns() + ->schema([ + TextInput::make('env.APP_NAME') + ->label('App Name') + ->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') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('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') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The driver used for caching. We recommend "Filesystem".') + ->required() + ->inline() + ->options(self::CACHE_DRIVERS) + ->default(config('cache.default', 'file')), + ToggleButtons::make('env.SESSION_DRIVER') + ->label('Session Driver') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".') + ->required() + ->inline() + ->options(self::SESSION_DRIVERS) + ->default(config('session.driver', 'file')), + ToggleButtons::make('env.QUEUE_CONNECTION') + ->label('Queue Driver') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The driver used for handling queues. We recommend "Database".') + ->required() + ->inline() + ->options(self::QUEUE_DRIVERS) + ->default(config('queue.default', 'database')), + ToggleButtons::make('env.DB_CONNECTION') + ->label('Database Driver') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".') + ->required() + ->inline() + ->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..04a4b1b72f --- /dev/null +++ b/app/Filament/Pages/Installer/Steps/RedisStep.php @@ -0,0 +1,42 @@ +label('Redis') + ->columns() + ->schema([ + TextInput::make('env.REDIS_HOST') + ->label('Redis Host') + ->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') + ->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. Can be empty') + ->default(config('database.redis.default.username')), + TextInput::make('env.REDIS_PASSWORD') + ->label('Redis Password') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The password for your redis user. 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..483e4ca1bc --- /dev/null +++ b/app/Filament/Pages/Installer/Steps/RequirementsStep.php @@ -0,0 +1,87 @@ += 0; + + $fields = [ + 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 = [ + 'BCMath' => extension_loaded('bcmath'), + 'cURL' => extension_loaded('curl'), + '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); + + $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) >= 755, + 'Cache' => substr(sprintf('%o', fileperms(base_path('bootstrap/cache/'))), -4) >= 755, + ]; + $correctFolderPermissions = !in_array(false, $folderPermissions); + + $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) + ->afterValidation(function () use ($correctPhpVersion, $allExtensionsInstalled, $correctFolderPermissions) { + if (!$correctPhpVersion || !$allExtensionsInstalled || !$correctFolderPermissions) { + Notification::make() + ->title('Some requirements are missing!') + ->danger() + ->send(); + + throw new Halt(); + } + }); + } +} diff --git a/app/Filament/Pages/Settings.php b/app/Filament/Pages/Settings.php index 4edddf310e..3edaf89dfe 100644 --- a/app/Filament/Pages/Settings.php +++ b/app/Filament/Pages/Settings.php @@ -98,7 +98,7 @@ private function generalSettings(): array ->hintIcon('tabler-question-mark') ->hintIconTooltip('Favicons should be placed in the public folder, located in the root panel directory.') ->required() - ->default(env('APP_FAVICON', './pelican.ico')), + ->default(env('APP_FAVICON', '/pelican.ico')), Toggle::make('APP_DEBUG') ->label('Enable Debug Mode?') ->inline(false) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 384e4501d1..19a43bef47 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') diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index d9d0f7fe6d..43b8c97364 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -51,12 +51,12 @@ protected function returnFormat(Server $server): array 'invocation' => $server->startup, 'skip_egg_scripts' => $server->skip_scripts, 'build' => [ - 'memory_limit' => config('panel.use_binary_prefix') ? $server->memory : $server->memory / 1.048576, - 'swap' => config('panel.use_binary_prefix') ? $server->swap : $server->swap / 1.048576, + 'memory_limit' => (int) round(config('panel.use_binary_prefix') ? $server->memory : $server->memory / 1.048576), + 'swap' => (int) round(config('panel.use_binary_prefix') ? $server->swap : $server->swap / 1.048576), 'io_weight' => $server->io, 'cpu_limit' => $server->cpu, 'threads' => $server->threads, - 'disk_space' => config('panel.use_binary_prefix') ? $server->disk : $server->disk / 1.048576, + 'disk_space' => (int) round(config('panel.use_binary_prefix') ? $server->disk : $server->disk / 1.048576), 'oom_killer' => $server->oom_killer, ], 'container' => [ 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') + ); + } +} diff --git a/app/helpers.php b/app/helpers.php index c2aa5cd749..4f3e87021f 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -40,3 +40,11 @@ function object_get_strict(object $object, ?string $key, mixed $default = null): return $object; } } + +if (!function_exists('is_installed')) { + function is_installed(): bool + { + // This defaults to true so existing panels count as "installed" + return env('APP_INSTALLED', true); + } +} diff --git a/config/app.php b/config/app.php index f9a4acc850..1a89ba65ff 100644 --- a/config/app.php +++ b/config/app.php @@ -5,7 +5,7 @@ return [ 'name' => env('APP_NAME', 'Pelican'), - 'favicon' => env('APP_FAVICON', './pelican.ico'), + 'favicon' => env('APP_FAVICON', '/pelican.ico'), 'version' => 'canary', diff --git a/resources/views/filament/pages/installer.blade.php b/resources/views/filament/pages/installer.blade.php new file mode 100644 index 0000000000..1977991c2c --- /dev/null +++ b/resources/views/filament/pages/installer.blade.php @@ -0,0 +1,7 @@ + + + {{ $this->form }} + + + + \ No newline at end of file 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)).+');