diff --git a/app/Console/Commands/Environment/DatabaseSettingsCommand.php b/app/Console/Commands/Environment/DatabaseSettingsCommand.php index 97f9815f30..b7784e1f83 100644 --- a/app/Console/Commands/Environment/DatabaseSettingsCommand.php +++ b/app/Console/Commands/Environment/DatabaseSettingsCommand.php @@ -2,31 +2,26 @@ namespace App\Console\Commands\Environment; +use App\Enums\DatabaseDriver; use App\Traits\EnvironmentWriterTrait; use Illuminate\Console\Command; use Illuminate\Contracts\Console\Kernel; use Illuminate\Database\DatabaseManager; +use Illuminate\Support\Facades\DB; class DatabaseSettingsCommand extends Command { use EnvironmentWriterTrait; - public const DATABASE_DRIVERS = [ - 'sqlite' => 'SQLite (recommended)', - 'mariadb' => 'MariaDB', - 'mysql' => 'MySQL', - 'pgsql' => 'PostgreSQL', - ]; - protected $description = 'Configure database settings for the Panel.'; protected $signature = 'p:environment:database {--driver= : The database driver backend to use.} {--database= : The database to use.} - {--host= : The connection address for the MySQL/ MariaDB server.} - {--port= : The connection port for the MySQL/ MariaDB server.} - {--username= : Username to use when connecting to the MySQL/ MariaDB server.} - {--password= : Password to use for the MySQL/ MariaDB database.}'; + {--host= : The connection address for the database server.} + {--port= : The connection port for the database server.} + {--username= : Username to use when connecting to the database server.} + {--password= : Password to use for the database server.}'; protected array $variables = []; @@ -50,198 +45,81 @@ public function handle(): int return 1; } + $driverList = DatabaseDriver::getFriendlyNameArray(DatabaseDriver::Sqlite); $selected = config('database.default', 'sqlite'); $this->variables['DB_CONNECTION'] = $this->option('driver') ?? $this->choice( 'Database Driver', - self::DATABASE_DRIVERS, - array_key_exists($selected, self::DATABASE_DRIVERS) ? $selected : null + $driverList, + array_key_exists($selected, $driverList) ? $selected : null ); - - if ($this->variables['DB_CONNECTION'] === 'mysql') { - $this->output->note(__('commands.database_settings.DB_HOST_note')); - $this->variables['DB_HOST'] = $this->option('host') ?? $this->ask( - 'Database Host', - config('database.connections.mysql.host', '127.0.0.1') - ); - - $this->variables['DB_PORT'] = $this->option('port') ?? $this->ask( - 'Database Port', - config('database.connections.mysql.port', 3306) - ); - - $this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask( - 'Database Name', - config('database.connections.mysql.database', 'panel') - ); - - $this->output->note(__('commands.database_settings.DB_USERNAME_note')); - $this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask( - 'Database Username', - config('database.connections.mysql.username', 'pelican') - ); - - $askForMySQLPassword = true; - if (!empty(config('database.connections.mysql.password')) && $this->input->isInteractive()) { - $this->variables['DB_PASSWORD'] = config('database.connections.mysql.password'); - $askForMySQLPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note')); - } - - if ($askForMySQLPassword) { - $this->variables['DB_PASSWORD'] = $this->option('password') ?? $this->secret('Database Password'); - } - - try { - // Test connection - config()->set('database.connections._panel_command_test', [ - 'driver' => 'mysql', - 'host' => $this->variables['DB_HOST'], - 'port' => $this->variables['DB_PORT'], - 'database' => $this->variables['DB_DATABASE'], - 'username' => $this->variables['DB_USERNAME'], - 'password' => $this->variables['DB_PASSWORD'], - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'strict' => true, - ]); - - $this->database->connection('_panel_command_test')->getPdo(); - } catch (\PDOException $exception) { - $this->output->error(sprintf('Unable to connect to the MySQL server using the provided credentials. The error returned was "%s".', $exception->getMessage())); - $this->output->error(__('commands.database_settings.DB_error_2')); - - if ($this->confirm(__('commands.database_settings.go_back'))) { - $this->database->disconnect('_panel_command_test'); - - return $this->handle(); + $driver = DatabaseDriver::from($this->variables['DB_CONNECTION']); + + switch ($driver) { + case DatabaseDriver::Sqlite: + $this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask( + 'Database Path', + env('DB_DATABASE', 'database.sqlite') + ); + break; + case DatabaseDriver::Mariadb: + case DatabaseDriver::Mysql: + case DatabaseDriver::Postgresql: + $this->output->note(__('commands.database_settings.DB_HOST_note')); + $this->variables['DB_HOST'] = $this->option('host') ?? $this->ask( + 'Database Host', + $driver->getDefaultOption('host', true) + ); + + $this->variables['DB_PORT'] = $this->option('port') ?? $this->ask( + 'Database Port', + $driver->getDefaultOption('port', true) + ); + + $this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask( + 'Database Name', + $driver->getDefaultOption('database', true) + ); + + $this->output->note(__('commands.database_settings.DB_USERNAME_note')); + $this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask( + 'Database Username', + $driver->getDefaultOption('username', true) + ); + + $askForPassword = true; + if (!empty($driver->getDefaultOption('password', true)) && $this->input->isInteractive()) { + $this->variables['DB_PASSWORD'] = $driver->getDefaultOption('password', true); + $askForPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note')); } - return 1; - } - } elseif ($this->variables['DB_CONNECTION'] === 'mariadb') { - $this->output->note(__('commands.database_settings.DB_HOST_note')); - $this->variables['DB_HOST'] = $this->option('host') ?? $this->ask( - 'Database Host', - config('database.connections.mariadb.host', '127.0.0.1') - ); - - $this->variables['DB_PORT'] = $this->option('port') ?? $this->ask( - 'Database Port', - config('database.connections.mariadb.port', 3306) - ); - - $this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask( - 'Database Name', - config('database.connections.mariadb.database', 'panel') - ); - - $this->output->note(__('commands.database_settings.DB_USERNAME_note')); - $this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask( - 'Database Username', - config('database.connections.mariadb.username', 'pelican') - ); - - $askForMariaDBPassword = true; - if (!empty(config('database.connections.mariadb.password')) && $this->input->isInteractive()) { - $this->variables['DB_PASSWORD'] = config('database.connections.mariadb.password'); - $askForMariaDBPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note')); - } - - if ($askForMariaDBPassword) { - $this->variables['DB_PASSWORD'] = $this->option('password') ?? $this->secret('Database Password'); - } - - try { - // Test connection - config()->set('database.connections._panel_command_test', [ - 'driver' => 'mariadb', - 'host' => $this->variables['DB_HOST'], - 'port' => $this->variables['DB_PORT'], - 'database' => $this->variables['DB_DATABASE'], - 'username' => $this->variables['DB_USERNAME'], - 'password' => $this->variables['DB_PASSWORD'], - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'strict' => true, - ]); - - $this->database->connection('_panel_command_test')->getPdo(); - } catch (\PDOException $exception) { - $this->output->error(sprintf('Unable to connect to the MariaDB server using the provided credentials. The error returned was "%s".', $exception->getMessage())); - $this->output->error(__('commands.database_settings.DB_error_2')); - - if ($this->confirm(__('commands.database_settings.go_back'))) { - $this->database->disconnect('_panel_command_test'); - - return $this->handle(); + if ($askForPassword) { + $this->variables['DB_PASSWORD'] = $this->option('password') ?? $this->secret('Database Password'); } - return 1; - } - } elseif ($this->variables['DB_CONNECTION'] === 'sqlite') { - $this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask( - 'Database Path', - env('DB_DATABASE', 'database.sqlite') - ); - } elseif ($this->variables['DB_CONNECTION'] === 'pgsql') { - $this->output->note(__('commands.database_settings.DB_HOST_note')); - $this->variables['DB_HOST'] = $this->option('host') ?? $this->ask( - 'Database Host', - config('database.connections.pgsql.host', '127.0.0.1') - ); - - $this->variables['DB_PORT'] = $this->option('port') ?? $this->ask( - 'Database Port', - config('database.connections.pgsql.port', 5432) - ); - - $this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask( - 'Database Name', - config('database.connections.pgsql.database', 'panel') - ); - - $this->output->note(__('commands.database_settings.DB_USERNAME_note')); - $this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask( - 'Database Username', - config('database.connections.pgsql.username', 'pelican') - ); - - $askForPGSQLPassword = true; - if (!empty(config('database.connections.pgsql.password')) && $this->input->isInteractive()) { - $this->variables['DB_PASSWORD'] = config('database.connections.pgsql.password'); - $askForPGSQLPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note')); - } - - if ($askForPGSQLPassword) { - $this->variables['DB_PASSWORD'] = $this->option('password') ?? $this->secret('Database Password'); - } - - try { - // Test connection - config()->set('database.connections._panel_command_test', [ - 'driver' => 'pgsql', - 'host' => $this->variables['DB_HOST'], - 'port' => $this->variables['DB_PORT'], - 'database' => $this->variables['DB_DATABASE'], - 'username' => $this->variables['DB_USERNAME'], - 'password' => $this->variables['DB_PASSWORD'], - 'charset' => 'UTF8', - 'collation' => 'en_US.UTF-8', - 'strict' => true, - ]); - - $this->database->connection('_panel_command_test')->getPdo(); - } catch (\PDOException $exception) { - $this->output->error(sprintf('Unable to connect to the PostgreSQL server using the provided credentials. The error returned was "%s".', $exception->getMessage())); - $this->output->error(__('commands.database_settings.DB_error_2')); - - if ($this->confirm(__('commands.database_settings.go_back'))) { - $this->database->disconnect('_panel_command_test'); - - return $this->handle(); + try { + // Test connection + DB::build([ + 'driver' => $driver->value, + 'host' => $this->variables['DB_HOST'], + 'port' => $this->variables['DB_PORT'], + 'database' => $this->variables['DB_DATABASE'], + 'username' => $this->variables['DB_USERNAME'], + 'password' => $this->variables['DB_PASSWORD'], + 'charset' => $driver->getDefaultOption('charset', true), + 'collation' => $driver->getDefaultOption('collation', true), + 'strict' => true, + ])->beginTransaction(); + } catch (\PDOException $exception) { + $this->output->error(sprintf('Unable to connect to the %s server using the provided credentials. The error returned was "%s".', $driver->getFriendlyName(), $exception->getMessage())); + $this->output->error(__('commands.database_settings.DB_error_2')); + + if ($this->confirm(__('commands.database_settings.go_back'))) { + return $this->handle(); + } + + return 1; } - - return 1; - } + break; } $this->writeToEnvironment($this->variables); diff --git a/app/Enums/DatabaseDriver.php b/app/Enums/DatabaseDriver.php new file mode 100644 index 0000000000..f50d21f294 --- /dev/null +++ b/app/Enums/DatabaseDriver.php @@ -0,0 +1,124 @@ + 'MariaDB', + self::Mysql => 'MySQL', + self::Postgresql => 'PostgreSQL', + self::Sqlite => 'SQLite', + }; + } + + public function getJDBCDriver(): string + { + return match($this) { + self::Mariadb, self::Mysql => 'mysql', + self::Postgresql => 'postgresql', + self::Sqlite => 'sqlite', + }; + } + + public function isRemote(): bool + { + return match($this) { + self::Mariadb, self::Mysql, self::Postgresql => true, + self::Sqlite => false, + }; + } + + public function getDefaultOption(string $key, bool $useEnv = false): mixed + { + $defaultValue = (match($this) { + self::Mariadb => [ + 'driver' => 'mariadb', + 'url' => '', + 'host' => '127.0.0.1', + 'port' => 3306, + 'database' => 'panel', + 'username' => 'pelican', + 'password' => '', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => false, + 'engine' => null, + 'options' => [], + 'test_database' => 'mysql', + ], + self::Mysql => [ + 'driver' => 'mysql', + 'url' => '', + 'host' => '127.0.0.1', + 'port' => 3306, + 'database' => 'panel', + 'username' => 'pelican', + 'password' => '', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => false, + 'engine' => null, + 'options' => [], + 'test_database' => 'mysql', + ], + self::Postgresql => [ + 'driver' => 'pgsql', + 'url' => '', + 'host' => '127.0.0.1', + 'port' => 5432, + 'database' => 'panel', + 'username' => 'pelican', + 'password' => '', + 'charset' => 'UTF8', + 'prefix' => '', + 'prefix_indexes' => true, + 'sslmode' => 'prefer', + 'test_database' => 'postgres', + ], + self::Sqlite => [ + 'driver' => 'sqlite', + 'url' => '', + 'database' => 'database.sqlite', + 'prefix' => '', + 'foreign_key_constraints' => true, + ], + })[$key] ?? null; + return $useEnv ? config()->get(sprintf('database.connections.%s.%s', $this->value, $key), $defaultValue) : $defaultValue; + } + + /** + * Returns a string to string mapping of internal driver names to friendly names. + * The recommended driver, if specified, will have (recommended) added to its name. + */ + public static function getFriendlyNameArray(self|null $recommended = null): array + { + $values = array_map(fn (self $value) => sprintf($value === $recommended ? '%s (recommended)' : '%s', $value->getFriendlyName()), self::cases()); + return array_combine(array_column(self::cases(), 'value'), $values); + } + + /** + * Returns a string to string mapping of internal driver names to friendly names. + * The recommended driver, if specified, will have (recommended) added to its name. + * Only returns databases connected to remotely. Not SQLite. + */ + public static function getFriendlyNameArrayRemote(self|null $recommended = null): array + { + $remoteDrivers = array_filter(self::cases(), fn (self $value) => $value->isRemote()); + $values = array_map(fn (self $value) => sprintf($value === $recommended ? '%s (recommended)' : '%s', $value->getFriendlyName()), $remoteDrivers); + return array_combine(array_column($remoteDrivers, 'value'), $values); + } +} diff --git a/app/Extensions/DynamicDatabaseConnection.php b/app/Extensions/DynamicDatabaseConnection.php index 524d9720b3..4f7f86da56 100644 --- a/app/Extensions/DynamicDatabaseConnection.php +++ b/app/Extensions/DynamicDatabaseConnection.php @@ -6,24 +6,6 @@ class DynamicDatabaseConnection { - public const DB_DEFAULTS = [ - 'mysql' => [ - 'DB_CHARSET' => 'utf8', - 'DB_COLLATION' => 'utf8_unicode_ci', - 'DEFAULT_DB' => 'mysql', - ], - 'mariadb' => [ - 'DB_CHARSET' => 'utf8', - 'DB_COLLATION' => 'utf8_unicode_ci', - 'DEFAULT_DB' => 'mysql', - ], - 'pgsql' => [ - 'DB_CHARSET' => 'utf8', - 'DB_COLLATION' => 'en_US', - 'DEFAULT_DB' => 'postgres', - ], - ]; - /** * Adds a dynamic database connection entry to the runtime config. */ @@ -34,14 +16,14 @@ public function set(string $connection, DatabaseHost|int $host, string $database } config()->set('database.connections.' . $connection, [ - 'driver' => $host->driver, + 'driver' => $host->driver->value, 'host' => $host->host, 'port' => $host->port, - 'database' => $database !== '' ? self::DB_DEFAULTS[$host->driver]['DEFAULT_DB'] : $database, + 'database' => $database !== '' ? $database : $host->driver->getDefaultOption('test_database'), 'username' => $host->username, 'password' => $host->password, - 'charset' => self::DB_DEFAULTS[$host->driver]['DB_CHARSET'], - 'collation' => self::DB_DEFAULTS[$host->driver]['DB_COLLATION'], + 'charset' => $host->driver->getDefaultOption('charset'), + 'collation' => $host->driver->getDefaultOption('collation'), ]); } } diff --git a/app/Filament/Admin/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php index 76c976a8fc..7c843ec249 100644 --- a/app/Filament/Admin/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php +++ b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php @@ -2,6 +2,7 @@ namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages; +use App\Enums\DatabaseDriver; use App\Filament\Admin\Resources\DatabaseHostResource; use App\Services\Databases\Hosts\HostCreationService; use Filament\Forms; @@ -10,6 +11,7 @@ use Filament\Forms\Components\TextInput; use Filament\Forms\Components\ToggleButtons; use Filament\Forms\Form; +use Filament\Forms\Set; use Filament\Notifications\Notification; use Filament\Resources\Pages\CreateRecord; use Filament\Support\Exceptions\Halt; @@ -44,22 +46,23 @@ public function form(Form $form): Form ToggleButtons::make('driver') ->label('Database Driver') ->inline() - ->options([ - 'mariadb' => 'MariaDB', - 'mysql' => 'MySQL', - 'pgsql' => 'PostgreSQL', - ]) - ->default('mariadb'), + ->options(DatabaseDriver::getFriendlyNameArrayRemote()) + ->default('mariadb') + ->live() + ->afterStateUpdated(function ($state, Set $set) { + $driver = DatabaseDriver::from($state); + $set('port', $driver->getDefaultOption('port')); + }), TextInput::make('host') ->columnSpan(2) - ->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.') + ->helperText('The IP address or Domain name that should be used when attempting to connect to this database host from this Panel to create new databases.') ->required() ->live(onBlur: true) ->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state)) ->maxLength(255), TextInput::make('port') ->columnSpan(1) - ->helperText('The port that MySQL is running on for this host.') + ->helperText('The port that the database is running on for this host.') ->required() ->numeric() ->default(3306) diff --git a/app/Filament/Admin/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php index 92aac1dfe7..ed2116ccf2 100644 --- a/app/Filament/Admin/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php +++ b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php @@ -43,14 +43,14 @@ public function form(Form $form): Form ->schema([ TextInput::make('host') ->columnSpan(2) - ->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.') + ->helperText('The IP address or Domain name that should be used when attempting to connect to this database host from this Panel to create new databases.') ->required() ->live(onBlur: true) ->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state)) ->maxLength(255), TextInput::make('port') ->columnSpan(1) - ->helperText('The port that MySQL is running on for this host.') + ->helperText('The port that database is running on for this host.') ->required() ->numeric() ->minValue(0) diff --git a/app/Livewire/Installer/Steps/DatabaseStep.php b/app/Livewire/Installer/Steps/DatabaseStep.php index bba2091316..5061a99c8b 100644 --- a/app/Livewire/Installer/Steps/DatabaseStep.php +++ b/app/Livewire/Installer/Steps/DatabaseStep.php @@ -2,6 +2,7 @@ namespace App\Livewire\Installer\Steps; +use App\Enums\DatabaseDriver; use App\Livewire\Installer\PanelInstaller; use Exception; use Filament\Forms\Components\TextInput; @@ -15,13 +16,6 @@ class DatabaseStep { - public const DATABASE_DRIVERS = [ - 'sqlite' => 'SQLite', - 'mariadb' => 'MariaDB', - 'mysql' => 'MySQL', - 'pgsql' => 'PostgreSQL', - ]; - public static function make(PanelInstaller $installer): Step { return Step::make('database') @@ -34,28 +28,23 @@ public static function make(PanelInstaller $installer): Step ->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".') ->required() ->inline() - ->options(self::DATABASE_DRIVERS) + ->options(DatabaseDriver::getFriendlyNameArray(DatabaseDriver::Sqlite)) ->default(config('database.default')) ->live() ->afterStateUpdated(function ($state, Set $set, Get $get) { - $set('env_database.DB_DATABASE', $state === 'sqlite' ? 'database.sqlite' : 'panel'); + $driver = DatabaseDriver::from($state); + $set('env_database.DB_DATABASE', $driver === DatabaseDriver::Sqlite ? 'database.sqlite' : 'panel'); - switch ($state) { - case 'sqlite': + switch ($driver) { + case DatabaseDriver::Sqlite: $set('env_database.DB_HOST', null); $set('env_database.DB_PORT', null); $set('env_database.DB_USERNAME', null); $set('env_database.DB_PASSWORD', null); break; - case 'mariadb': - case 'mysql': - $set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1'); - $set('env_database.DB_PORT', $get('env_database.DB_PORT') ?? '3306'); - $set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican'); - break; - case 'pgsql': - $set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1'); - $set('env_database.DB_PORT', $get('env_database.DB_PORT') ?? '5432'); + default: + $set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? $driver->getDefaultOption('host')); + $set('env_database.DB_PORT', $get('env_database.DB_PORT') ?? $driver->getDefaultOption('port')); $set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican'); break; } @@ -76,7 +65,7 @@ public static function make(PanelInstaller $installer): Step ->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'), TextInput::make('env_database.DB_PORT') ->label('Database Port') - ->placeholder('3306') + ->placeholder(fn (Get $get) => DatabaseDriver::from($get('env_database.DB_CONNECTION'))->getDefaultOption('port', '3306')) ->hintIcon('tabler-question-mark') ->hintIconTooltip('The port of your database.') ->numeric() @@ -100,7 +89,7 @@ public static function make(PanelInstaller $installer): Step ->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'), ]) ->afterValidation(function (Get $get) use ($installer) { - $driver = $get('env_database.DB_CONNECTION'); + $driver = DatabaseDriver::from($get('env_database.DB_CONNECTION')); 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'); @@ -110,70 +99,31 @@ public static function make(PanelInstaller $installer): Step }); } - private static function testConnection(string $driver, ?string $host, null|string|int $port, ?string $database, ?string $username, ?string $password): bool + private static function testConnection(DatabaseDriver $driver, ?string $host, null|string|int $port, ?string $database, ?string $username, ?string $password): bool { - switch ($driver) { - case 'sqlite': - return true; - - case 'mariadb': - case 'mysql': - try { - config()->set('database.connections._panel_install_test', [ - 'driver' => $driver, - 'host' => $host, - 'port' => $port, - 'database' => $database, - 'username' => $username, - 'password' => $password, - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'strict' => true, - ]); - - DB::connection('_panel_install_test')->getPdo(); - } catch (Exception $exception) { - DB::disconnect('_panel_install_test'); - - Notification::make() - ->title('Database connection failed') - ->body($exception->getMessage()) - ->danger() - ->send(); - - return false; - } - break; - - case 'pgsql': - try { - config()->set('database.connections._panel_install_test', [ - 'driver' => $driver, - 'host' => $host, - 'port' => $port, - 'database' => $database, - 'username' => $username, - 'password' => $password, - 'charset' => 'UTF8', - 'collation' => 'en_US.UTF-8', - 'strict' => true, - ]); - - DB::connection('_panel_install_test')->getPdo(); - } catch (Exception $exception) { - DB::disconnect('_panel_install_test'); - - Notification::make() - ->title('Database connection failed') - ->body($exception->getMessage()) - ->danger() - ->send(); - - return false; - } - break; + if ($driver === DatabaseDriver::Sqlite) { + return true; + } + try { + DB::build([ + 'driver' => $driver->value, + 'host' => $host, + 'port' => $port, + 'database' => $database !== '' ? $database : $driver->getDefaultOption('database'), + 'username' => $username, + 'password' => $password, + 'charset' => $driver->getDefaultOption('charset'), + 'collation' => $driver->getDefaultOption('collation'), + ])->beginTransaction(); + } catch (Exception $exception) { + Notification::make() + ->title('Database connection failed') + ->body($exception->getMessage()) + ->danger() + ->send(); + + return false; } - return true; } } diff --git a/app/Models/Database.php b/app/Models/Database.php index 0c70c614e0..a8c2cd5a19 100644 --- a/app/Models/Database.php +++ b/app/Models/Database.php @@ -2,7 +2,9 @@ namespace App\Models; +use App\Enums\DatabaseDriver; use BadMethodCallException; +use Illuminate\Database\ConnectionInterface; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Facades\DB; @@ -32,13 +34,7 @@ class Database extends Model public const DEFAULT_CONNECTION_NAME = 'dynamic'; - public const DATABASE_SETUP_CONNECTION_NAME = '_panel_setup_database'; - - public const JDBC_DRIVER_MAPPING = [ - 'mysql' => 'mysql', - 'mariadb' => 'mariadb', - 'pgsql' => 'postgresql', - ]; + public const DEFAULT_CONNECTION_NAME_USER_DB = '_panel_setup_database'; /** * The table associated with the model. @@ -107,7 +103,7 @@ public function server(): BelongsTo protected function jdbc(): Attribute { return Attribute::make( - get: fn () => 'jdbc:' . self::JDBC_DRIVER_MAPPING[$this->host->driver] . '://' . $this->username . ':' . urlencode($this->password) . '@' . $this->host->host . ':' . $this->host->port . '/' . $this->database, + get: fn () => 'jdbc:' . $this->host->driver->getJDBCDriver() . '://' . $this->username . ':' . urlencode($this->password) . '@' . $this->host->host . ':' . $this->host->port . '/' . $this->database, ); } @@ -121,19 +117,18 @@ private function run(string $statement): bool /** * Setup a temporary database connection. - * Needed for PostgreSQL connections. */ - private function setConfigDatabase(string $database): void + private function getDatabaseConnection(string $database): ConnectionInterface { - config()->set('database.connections.' . self::DATABASE_SETUP_CONNECTION_NAME, [ - 'driver' => $this->host->driver, + return DB::build([ + 'driver' => $this->host->driver->value, 'host' => $this->host->host, 'port' => $this->host->port, 'database' => $database, 'username' => $this->host->username, 'password' => $this->host->password, - 'charset' => 'UTF8', - 'collation' => 'en_US.UTF-8', + 'charset' => $this->host->driver->getDefaultOption('charset', true), + 'collation' => $this->host->driver->getDefaultOption('collation', true), 'strict' => true, ]); } @@ -143,15 +138,10 @@ private function setConfigDatabase(string $database): void */ public function createDatabase(string $database): bool { - switch ($this->host->driver) { - case 'mysql': - case 'mariadb': - return $this->run(sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $database)); - case 'pgsql': - return $this->run(sprintf('CREATE DATABASE "%s"', $database)); - } - - return false; + return match ($this->host->driver) { + DatabaseDriver::Mysql, DatabaseDriver::Mariadb => $this->run(sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $database)), + DatabaseDriver::Postgresql => $this->run(sprintf('CREATE DATABASE "%s"', $database)), + }; } /** @@ -162,8 +152,8 @@ public function createUser(string $database, string $username, string $remote, s $args = []; $command = ''; switch ($this->host->driver) { - case 'mysql': - case 'mariadb': + case DatabaseDriver::Mysql: + case DatabaseDriver::Mariadb: $args = [$username, $remote, $password]; $command = 'CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\''; @@ -173,21 +163,18 @@ public function createUser(string $database, string $username, string $remote, s } return $this->run(sprintf($command, ...$args)); - case 'pgsql': - try { - $this->setConfigDatabase($database); - $args = [$username, $password]; - $command = 'CREATE USER "%s" WITH PASSWORD \'%s\''; + case DatabaseDriver::Postgresql: + $args = [$username, $password]; + $command = 'CREATE USER "%s" WITH PASSWORD \'%s\''; - if (!empty($max_connections)) { - $args[] = $max_connections; - $command .= ' CONNECTION LIMIT %s'; - } - - return DB::connection(self::DATABASE_SETUP_CONNECTION_NAME)->statement(sprintf($command, ...$args)); - } finally { - DB::disconnect(self::DATABASE_SETUP_CONNECTION_NAME); + if (!empty($max_connections)) { + $args[] = $max_connections; + $command .= ' CONNECTION LIMIT %s'; } + + return $this->getDatabaseConnection($database)->statement(sprintf($command, ...$args)); + default: + throw new BadMethodCallException(sprintf('Not implemented for driver %s', $this->host->driver)); } return false; @@ -199,18 +186,10 @@ public function createUser(string $database, string $username, string $remote, s */ public function updateUserPassword(string $database, string $username, string $remote, string $password): bool { - switch ($this->host->driver) { - case 'pgsql': - try { - $this->setConfigDatabase($database); - - return DB::connection(self::DATABASE_SETUP_CONNECTION_NAME)->statement(sprintf('ALTER USER "%s" WITH PASSWORD \'%s\'', $username, $password)); - } finally { - DB::disconnect(self::DATABASE_SETUP_CONNECTION_NAME); - } - default: - throw new BadMethodCallException('updateUserPassword only implemented for PostgreSQL'); - } + return match ($this->host->driver) { + DatabaseDriver::Postgresql => $this->getDatabaseConnection($database)->statement(sprintf('ALTER USER "%s" WITH PASSWORD \'%s\'', $username, $password)), + default => throw new BadMethodCallException(sprintf('Not implemented for driver %s', $this->host->driver)), + }; } /** @@ -219,32 +198,24 @@ public function updateUserPassword(string $database, string $username, string $r public function assignUserToDatabase(string $database, string $username, string $remote): bool { switch ($this->host->driver) { - case 'mysql': - case 'mariadb': + case DatabaseDriver::Mysql: + case DatabaseDriver::Mariadb: return $this->run(sprintf( 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, REFERENCES, INDEX, LOCK TABLES, CREATE ROUTINE, ALTER ROUTINE, EXECUTE, CREATE TEMPORARY TABLES, CREATE VIEW, SHOW VIEW, EVENT, TRIGGER ON `%s`.* TO `%s`@`%s`', $database, $username, $remote )); - case 'pgsql': - try { - $this->setConfigDatabase($database); - $success = DB::connection(self::DATABASE_SETUP_CONNECTION_NAME)->statement(sprintf('REVOKE CONNECT ON DATABASE "%s" FROM public', $database)); - $success = DB::connection(self::DATABASE_SETUP_CONNECTION_NAME)->statement(sprintf('GRANT CONNECT ON DATABASE "%s" TO "%s"', $database, $username)); - $success = DB::connection(self::DATABASE_SETUP_CONNECTION_NAME)->statement('DROP SCHEMA public'); - $success = $success && DB::connection(self::DATABASE_SETUP_CONNECTION_NAME)->statement(sprintf( - 'CREATE SCHEMA AUTHORIZATION "%s"', - $username - )); + case DatabaseDriver::Postgresql: + $conn = $this->getDatabaseConnection($database); - return $success; - } finally { - DB::disconnect(self::DATABASE_SETUP_CONNECTION_NAME); - } + return $conn->statement(sprintf('REVOKE CONNECT ON DATABASE "%s" FROM public', $database)) + && $conn->statement(sprintf('GRANT CONNECT ON DATABASE "%s" TO "%s"', $database, $username)) + && $conn->statement('DROP SCHEMA public') + && $conn->statement(sprintf('CREATE SCHEMA AUTHORIZATION "%s"', $username)); + default: + throw new BadMethodCallException(sprintf('Not implemented for driver %s', $this->host->driver)); } - - return false; } /** @@ -252,13 +223,10 @@ public function assignUserToDatabase(string $database, string $username, string */ public function flush(): bool { - switch ($this->host->driver) { - case 'mysql': - case 'mariadb': - return $this->run('FLUSH PRIVILEGES'); - } - - return true; + return match ($this->host->driver) { + DatabaseDriver::Mysql, DatabaseDriver::Mariadb => $this->run('FLUSH PRIVILEGES'), + default => true, + }; } /** @@ -266,18 +234,11 @@ public function flush(): bool */ public function dropDatabase(string $database): bool { - switch ($this->host->driver) { - case 'mysql': - case 'mariadb': - return $this->run(sprintf('DROP DATABASE IF EXISTS `%s`', $database)); - case 'pgsql': - $success = $this->run(sprintf('SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = \'%s\' AND pid <> pg_backend_pid()', $database)); - $success = $success && $this->run(sprintf('DROP DATABASE IF EXISTS "%s"', $database)); - - return $success; - } - - return false; + return match ($this->host->driver) { + DatabaseDriver::Mysql, DatabaseDriver::Mariadb => $this->run(sprintf('DROP DATABASE IF EXISTS `%s`', $database)), + DatabaseDriver::Postgresql => $this->run(sprintf('SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = \'%s\' AND pid <> pg_backend_pid()', $database)) + && $this->run(sprintf('DROP DATABASE IF EXISTS "%s"', $database)), + }; } /** @@ -285,14 +246,9 @@ public function dropDatabase(string $database): bool */ public function dropUser(string $username, string $remote): bool { - switch ($this->host->driver) { - case 'mysql': - case 'mariadb': - return $this->run(sprintf('DROP USER IF EXISTS `%s`@`%s`', $username, $remote)); - case 'pgsql': - return $this->run(sprintf('DROP USER IF EXISTS "%s"', $username)); - } - - return false; + return match ($this->host->driver) { + DatabaseDriver::Mysql, DatabaseDriver::Mariadb => $this->run(sprintf('DROP USER IF EXISTS `%s`@`%s`', $username, $remote)), + DatabaseDriver::Postgresql => $this->run(sprintf('DROP USER IF EXISTS "%s"', $username)), + }; } } diff --git a/app/Models/DatabaseHost.php b/app/Models/DatabaseHost.php index 22a9d081f0..fc6d28a256 100644 --- a/app/Models/DatabaseHost.php +++ b/app/Models/DatabaseHost.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Enums\DatabaseDriver; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -20,7 +21,7 @@ * @property int|null $nodes_count * @property \Illuminate\Database\Eloquent\Collection|\App\Models\Database[] $databases * @property int|null $databases_count - * @property string $driver + * @property DatabaseDriver $driver */ class DatabaseHost extends Model { @@ -75,6 +76,7 @@ protected function casts(): array 'password' => 'encrypted', 'created_at' => 'immutable_datetime', 'updated_at' => 'immutable_datetime', + 'driver' => DatabaseDriver::class, ]; } diff --git a/app/Services/Databases/DatabasePasswordService.php b/app/Services/Databases/DatabasePasswordService.php index 7ec01d784e..683c43bbb0 100644 --- a/app/Services/Databases/DatabasePasswordService.php +++ b/app/Services/Databases/DatabasePasswordService.php @@ -2,6 +2,7 @@ namespace App\Services\Databases; +use App\Enums\DatabaseDriver; use App\Models\Database; use App\Helpers\Utilities; use Illuminate\Database\ConnectionInterface; @@ -36,14 +37,14 @@ public function handle(Database|int $database): void ]); switch ($database->host->driver) { - case 'mysql': - case 'mariadb': + case DatabaseDriver::Mysql: + case DatabaseDriver::Mariadb: $database->dropUser($database->username, $database->remote); $database->createUser($database->database, $database->username, $database->remote, $password, $database->max_connections); $database->assignUserToDatabase($database->database, $database->username, $database->remote); $database->flush(); break; - case 'pgsql': + case DatabaseDriver::Postgresql: $database->updateUserPassword($database->database, $database->username, $database->remote, $password); break; } diff --git a/config/database.php b/config/database.php index 58bc48cfae..f05118d312 100644 --- a/config/database.php +++ b/config/database.php @@ -1,6 +1,8 @@ getDefaultOption('database')); $datapasePath = database_path($database); if (str($database)->startsWith('/')) { $databasePath = $database; @@ -12,67 +14,68 @@ 'connections' => [ 'sqlite' => [ - 'driver' => 'sqlite', - 'url' => env('DB_URL'), + 'driver' => DatabaseDriver::Sqlite->value, + 'url' => env('DB_URL', DatabaseDriver::Sqlite->getDefaultOption('url')), 'database' => $datapasePath, - 'prefix' => '', - 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'prefix' => DatabaseDriver::Sqlite->getDefaultOption('prefix'), + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', DatabaseDriver::Sqlite->getDefaultOption('foreign_key_constraints')), ], 'mysql' => [ - 'driver' => 'mysql', - 'url' => env('DB_URL'), - 'host' => env('DB_HOST', '127.0.0.1'), - 'port' => env('DB_PORT', '3306'), - 'database' => env('DB_DATABASE', 'panel'), - 'username' => env('DB_USERNAME', 'pelican'), - 'password' => env('DB_PASSWORD', ''), - 'unix_socket' => env('DB_SOCKET', ''), - 'charset' => env('DB_CHARSET', 'utf8mb4'), - 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), - 'prefix' => env('DB_PREFIX', ''), - 'prefix_indexes' => true, - 'strict' => env('DB_STRICT_MODE', false), - 'engine' => null, + 'driver' => DatabaseDriver::Mysql->value, + 'url' => env('DB_URL', DatabaseDriver::Mysql->getDefaultOption('url')), + 'host' => env('DB_HOST', DatabaseDriver::Mysql->getDefaultOption('host')), + 'port' => env('DB_PORT', DatabaseDriver::Mysql->getDefaultOption('port')), + 'database' => env('DB_DATABASE', DatabaseDriver::Mysql->getDefaultOption('database')), + 'username' => env('DB_USERNAME', DatabaseDriver::Mysql->getDefaultOption('username')), + 'password' => env('DB_PASSWORD', DatabaseDriver::Mysql->getDefaultOption('password')), + 'unix_socket' => env('DB_SOCKET', DatabaseDriver::Mysql->getDefaultOption('unix_socket')), + 'charset' => env('DB_CHARSET', DatabaseDriver::Mysql->getDefaultOption('charset')), + 'collation' => env('DB_COLLATION', DatabaseDriver::Mysql->getDefaultOption('collation')), + 'prefix' => env('DB_PREFIX', DatabaseDriver::Mysql->getDefaultOption('prefix')), + 'prefix_indexes' => DatabaseDriver::Mysql->getDefaultOption('prefix_indexes'), + 'strict' => env('DB_STRICT_MODE', DatabaseDriver::Mysql->getDefaultOption('strict')), + 'engine' => DatabaseDriver::Mysql->getDefaultOption('engine'), 'options' => extension_loaded('pdo_mysql') ? array_filter([ PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), ]) : [], + 'test_database' => DatabaseDriver::Mysql->getDefaultOption('test_database'), ], 'mariadb' => [ - 'driver' => 'mariadb', - 'url' => env('DB_URL'), - 'host' => env('DB_HOST', '127.0.0.1'), - 'port' => env('DB_PORT', '3306'), - 'database' => env('DB_DATABASE', 'panel'), - 'username' => env('DB_USERNAME', 'pelican'), - 'password' => env('DB_PASSWORD', ''), - 'unix_socket' => env('DB_SOCKET', ''), - 'charset' => env('DB_CHARSET', 'utf8mb4'), - 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), - 'prefix' => env('DB_PREFIX', ''), - 'prefix_indexes' => true, - 'strict' => env('DB_STRICT_MODE', false), - 'engine' => null, + 'driver' => DatabaseDriver::Mariadb->value, + 'url' => env('DB_URL', DatabaseDriver::Mariadb->getDefaultOption('url')), + 'host' => env('DB_HOST', DatabaseDriver::Mariadb->getDefaultOption('host')), + 'port' => env('DB_PORT', DatabaseDriver::Mariadb->getDefaultOption('port')), + 'database' => env('DB_DATABASE', DatabaseDriver::Mariadb->getDefaultOption('database')), + 'username' => env('DB_USERNAME', DatabaseDriver::Mariadb->getDefaultOption('username')), + 'password' => env('DB_PASSWORD', DatabaseDriver::Mariadb->getDefaultOption('password')), + 'unix_socket' => env('DB_SOCKET', DatabaseDriver::Mariadb->getDefaultOption('unix_socket')), + 'charset' => env('DB_CHARSET', DatabaseDriver::Mariadb->getDefaultOption('charset')), + 'collation' => env('DB_COLLATION', DatabaseDriver::Mariadb->getDefaultOption('collation')), + 'prefix' => env('DB_PREFIX', DatabaseDriver::Mariadb->getDefaultOption('prefix')), + 'prefix_indexes' => DatabaseDriver::Mariadb->getDefaultOption('prefix_indexes'), + 'strict' => env('DB_STRICT_MODE', DatabaseDriver::Mariadb->getDefaultOption('strict')), + 'engine' => DatabaseDriver::Mariadb->getDefaultOption('engine'), 'options' => extension_loaded('pdo_mysql') ? array_filter([ PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), ]) : [], + 'test_database' => DatabaseDriver::Mariadb->getDefaultOption('test_database'), ], 'pgsql' => [ - 'driver' => 'pgsql', - 'url' => env('DB_URL'), - 'host' => env('DB_HOST', '127.0.0.1'), - 'port' => env('DB_PORT', '5432'), - 'database' => env('DB_DATABASE', 'panel'), - 'username' => env('DB_USERNAME', 'pelican'), - 'password' => env('DB_PASSWORD', ''), - 'charset' => env('DB_CHARSET', 'UTF8'), - 'prefix' => env('DB_PREFIX', ''), - 'prefix_indexes' => true, - 'strict' => env('DB_STRICT_MODE', false), - 'engine' => null, - 'sslmode' => 'prefer', + 'driver' => DatabaseDriver::Postgresql->value, + 'url' => env('DB_URL', DatabaseDriver::Postgresql->getDefaultOption('url')), + 'host' => env('DB_HOST', DatabaseDriver::Postgresql->getDefaultOption('host')), + 'port' => env('DB_PORT', DatabaseDriver::Postgresql->getDefaultOption('port')), + 'database' => env('DB_DATABASE', DatabaseDriver::Postgresql->getDefaultOption('database')), + 'username' => env('DB_USERNAME', DatabaseDriver::Postgresql->getDefaultOption('username')), + 'password' => env('DB_PASSWORD', DatabaseDriver::Postgresql->getDefaultOption('password')), + 'charset' => env('DB_CHARSET', DatabaseDriver::Postgresql->getDefaultOption('charset')), + 'prefix' => env('DB_PREFIX', DatabaseDriver::Postgresql->getDefaultOption('prefix')), + 'prefix_indexes' => DatabaseDriver::Postgresql->getDefaultOption('prefix_indexes'), + 'sslmode' => DatabaseDriver::Postgresql->getDefaultOption('sslmode'), + 'test_database' => DatabaseDriver::Postgresql->getDefaultOption('test_database'), ], ], diff --git a/database/schema/pgsql-schema.sql b/database/schema/pgsql-schema.sql index 92aa3e55fd..248477208a 100644 --- a/database/schema/pgsql-schema.sql +++ b/database/schema/pgsql-schema.sql @@ -1228,7 +1228,7 @@ CREATE TABLE public.users ( email character varying(255) NOT NULL, password text NOT NULL, remember_token character varying(255), - language character(5) DEFAULT 'en'::bpchar NOT NULL, + language character varying(5) DEFAULT 'en'::character varying NOT NULL, use_totp smallint NOT NULL, totp_secret text, created_at timestamp(0) without time zone,