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/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..222793a646
--- /dev/null
+++ b/app/Filament/Pages/Installer/PanelInstaller.php
@@ -0,0 +1,144 @@
+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);
+
+ $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
+ $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/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/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/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)).+');