Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web Installer #504

Merged
merged 19 commits into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
142 changes: 14 additions & 128 deletions app/Console/Commands/Environment/AppSettingsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,162 +3,48 @@
namespace App\Console\Commands\Environment;

use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
use App\Traits\Commands\EnvironmentWriterTrait;
use Illuminate\Support\Facades\Artisan;

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");
}
}
144 changes: 144 additions & 0 deletions app/Filament/Pages/Installer/PanelInstaller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php

namespace App\Filament\Pages\Installer;

use App\Filament\Pages\Installer\Steps\AdminUserStep;
use App\Filament\Pages\Installer\Steps\DatabaseStep;
use App\Filament\Pages\Installer\Steps\EnvironmentStep;
use App\Filament\Pages\Installer\Steps\RedisStep;
use App\Filament\Pages\Installer\Steps\RequirementsStep;
use App\Services\Users\UserCreationService;
use App\Traits\Commands\EnvironmentWriterTrait;
use Exception;
use Filament\Facades\Filament;
use Filament\Forms\Components\Wizard;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
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;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;

/**
* @property Form $form
*/
class PanelInstaller extends SimplePage implements HasForms
{
use EnvironmentWriterTrait;
use HasUnsavedDataChangesAlert;
use InteractsWithForms;

public $data = [];

protected static string $view = 'filament.pages.installer';

public function getMaxWidth(): MaxWidth|string
{
return MaxWidth::SevenExtraLarge;
}

public function mount()
{
if (is_installed()) {
abort(404);
}

$this->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'
<x-filament::button
type="submit"
size="sm"
>
Finish
</x-filament::button>
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();
}
}
}
31 changes: 31 additions & 0 deletions app/Filament/Pages/Installer/Steps/AdminUserStep.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace App\Filament\Pages\Installer\Steps;

use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;

class AdminUserStep
{
public static function make(): Step
{
return Step::make('user')
->label('Admin User')
->schema([
TextInput::make('user.email')
->label('Admin E-Mail')
->required()
->email()
->default('[email protected]'),
TextInput::make('user.username')
->label('Admin Username')
->required()
->default('admin'),
TextInput::make('user.password')
->label('Admin Password')
->required()
->password()
->revealable(),
]);
}
}
Loading
Loading