diff --git a/app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php b/app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php index b41272c12..a03844649 100644 --- a/app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php +++ b/app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php @@ -687,8 +687,22 @@ class EditServer extends EditRecord ->modalHeading(trans('admin/server.delete_db_heading')) ->modalSubmitActionLabel(trans('filament-actions::delete.single.label')) ->modalDescription(fn (Get $get) => trans('admin/server.delete_db', ['name' => $get('database')])) - ->action(function (DatabaseManagementService $databaseManagementService, $record) { - $databaseManagementService->delete($record); + ->action(function (DatabaseManagementService $service, $record) { + try { + $service->delete($record); + + Notification::make() + ->title(trans('server/database.delete_notification', ['database' => $record->database])) + ->success() + ->send(); + } catch (Exception $e) { + Notification::make() + ->title(trans('server/database.delete_notification_fail', ['database' => $record->database])) + ->body($e->getMessage()) + ->danger() + ->persistent()->send(); + } + $this->fillForm(); }) ), @@ -737,21 +751,22 @@ class EditServer extends EditRecord ->label(fn () => DatabaseHost::query()->count() < 1 ? trans('admin/server.no_db_hosts') : trans('admin/server.create_database')) ->color(fn () => DatabaseHost::query()->count() < 1 ? 'danger' : 'primary') ->modalSubmitActionLabel(trans('admin/server.create_database')) - ->action(function (array $data, DatabaseManagementService $service, Server $server, RandomWordService $randomWordService) { - if (empty($data['database'])) { - $data['database'] = $randomWordService->word() . random_int(1, 420); - } - if (empty($data['remote'])) { - $data['remote'] = '%'; - } + ->action(function (array $data, DatabaseManagementService $service, Server $server) { + $data['database'] ??= str_random(12); + $data['remote'] ??= '%'; $data['database'] = $service->generateUniqueDatabaseName($data['database'], $server->id); try { $service->setValidateDatabaseLimit(false)->create($server, $data); + + Notification::make() + ->title(trans('server/database.create_notification', ['database' => $data['database']])) + ->success() + ->send(); } catch (Exception $e) { Notification::make() - ->title(trans('admin/server.failed_to_create')) + ->title(trans('server/database.create_notification_fail', ['database' => $data['database']])) ->body($e->getMessage()) ->danger() ->persistent()->send(); diff --git a/app/Filament/Components/Forms/Actions/RotateDatabasePasswordAction.php b/app/Filament/Components/Forms/Actions/RotateDatabasePasswordAction.php index a7eb4ca34..209b01a11 100644 --- a/app/Filament/Components/Forms/Actions/RotateDatabasePasswordAction.php +++ b/app/Filament/Components/Forms/Actions/RotateDatabasePasswordAction.php @@ -4,9 +4,10 @@ namespace App\Filament\Components\Forms\Actions; use App\Facades\Activity; use App\Models\Database; -use App\Services\Databases\DatabasePasswordService; +use App\Services\Databases\DatabaseManagementService; use Exception; use Filament\Actions\StaticAction; +use Filament\Facades\Filament; use Filament\Forms\Components\Actions\Action; use Filament\Forms\Set; use Filament\Notifications\Notification; @@ -36,10 +37,9 @@ class RotateDatabasePasswordAction extends Action $this->requiresConfirmation(); - $this->action(function (DatabasePasswordService $service, Database $database, Set $set) { + $this->action(function (DatabaseManagementService $service, Database $database, Set $set) { try { - $service->handle($database); - + $service->rotatePassword($database); $database->refresh(); $set('password', $database->password); @@ -57,7 +57,7 @@ class RotateDatabasePasswordAction extends Action } catch (Exception $exception) { Notification::make() ->title(trans('admin/databasehost.rotate_error')) - ->body($exception->getMessage()) + ->body(fn () => auth()->user()->canAccessPanel(Filament::getPanel('admin')) ? $exception->getMessage() : null) ->danger() ->send(); diff --git a/app/Filament/Server/Resources/DatabaseResource.php b/app/Filament/Server/Resources/DatabaseResource.php index ad163c8ee..501f492ea 100644 --- a/app/Filament/Server/Resources/DatabaseResource.php +++ b/app/Filament/Server/Resources/DatabaseResource.php @@ -15,9 +15,11 @@ use App\Traits\Filament\CanCustomizeRelations; use App\Traits\Filament\CanModifyForm; use App\Traits\Filament\CanModifyTable; use App\Traits\Filament\HasLimitBadge; +use Exception; use Filament\Facades\Filament; use Filament\Forms\Components\TextInput; use Filament\Forms\Form; +use Filament\Notifications\Notification; use Filament\Resources\Pages\PageRegistration; use Filament\Resources\Resource; use Filament\Tables\Actions\DeleteAction; @@ -122,7 +124,23 @@ class DatabaseResource extends Resource ViewAction::make() ->modalHeading(fn (Database $database) => trans('server/database.viewing', ['database' => $database->database])), DeleteAction::make() - ->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)), + ->using(function (Database $database, DatabaseManagementService $service) { + try { + $service->delete($database); + + Notification::make() + ->title(trans('server/database.delete_notification', ['database' => $database->database])) + ->success() + ->send(); + } catch (Exception $exception) { + Notification::make() + ->title(trans('server/database.delete_notification_fail', ['database' => $database->database])) + ->danger() + ->send(); + + report($exception); + } + }), ]); } diff --git a/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php b/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php index 5e71635f6..13c2064f0 100644 --- a/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php +++ b/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php @@ -8,6 +8,7 @@ use App\Models\Server; use App\Services\Databases\DatabaseManagementService; use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Exception; use Filament\Actions\Action; use Filament\Actions\ActionGroup; use Filament\Actions\CreateAction; @@ -15,6 +16,7 @@ use Filament\Facades\Filament; use Filament\Forms\Components\Grid; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; +use Filament\Notifications\Notification; use Filament\Resources\Pages\ListRecords; use Filament\Support\Enums\IconSize; use Illuminate\Support\Str; @@ -63,12 +65,24 @@ class ListDatabases extends ListRecords ]), ]) ->action(function ($data, DatabaseManagementService $service) use ($server) { - if (empty($data['database'])) { - $data['database'] = Str::random(12); - } - $data['database'] = 's'. $server->id . '_' . $data['database']; + $data['database'] ??= Str::random(12); + $data['database'] = $service->generateUniqueDatabaseName($data['database'], $server->id); - $service->create($server, $data); + try { + $service->create($server, $data); + + Notification::make() + ->title(trans('server/database.create_notification', ['database' => $data['database']])) + ->success() + ->send(); + } catch (Exception $exception) { + Notification::make() + ->title(trans('server/database.create_notification_fail', ['database' => $data['database']])) + ->danger() + ->send(); + + report($exception); + } }), ]; } diff --git a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php index 0851c6961..a7f7a6218 100644 --- a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php +++ b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php @@ -6,7 +6,6 @@ use Illuminate\Http\Response; use App\Models\Server; use App\Models\Database; use Illuminate\Http\JsonResponse; -use App\Services\Databases\DatabasePasswordService; use App\Services\Databases\DatabaseManagementService; use App\Transformers\Api\Application\ServerDatabaseTransformer; use App\Http\Controllers\Api\Application\ApplicationApiController; @@ -24,7 +23,6 @@ class DatabaseController extends ApplicationApiController */ public function __construct( private DatabaseManagementService $databaseManagementService, - private DatabasePasswordService $databasePasswordService ) { parent::__construct(); } @@ -66,7 +64,7 @@ class DatabaseController extends ApplicationApiController */ public function resetPassword(ServerDatabaseWriteRequest $request, Server $server, Database $database): JsonResponse { - $this->databasePasswordService->handle($database); + $this->databaseManagementService->rotatePassword($database); return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); } diff --git a/app/Http/Controllers/Api/Client/Servers/DatabaseController.php b/app/Http/Controllers/Api/Client/Servers/DatabaseController.php index 62be4125a..0752f5e3a 100644 --- a/app/Http/Controllers/Api/Client/Servers/DatabaseController.php +++ b/app/Http/Controllers/Api/Client/Servers/DatabaseController.php @@ -6,7 +6,6 @@ use Illuminate\Http\Response; use App\Models\Server; use App\Models\Database; use App\Facades\Activity; -use App\Services\Databases\DatabasePasswordService; use App\Transformers\Api\Client\DatabaseTransformer; use App\Services\Databases\DatabaseManagementService; use App\Services\Databases\DeployServerDatabaseService; @@ -26,7 +25,6 @@ class DatabaseController extends ClientApiController public function __construct( private DeployServerDatabaseService $deployDatabaseService, private DatabaseManagementService $managementService, - private DatabasePasswordService $passwordService ) { parent::__construct(); } @@ -83,7 +81,7 @@ class DatabaseController extends ClientApiController */ public function rotatePassword(RotatePasswordRequest $request, Server $server, Database $database): array { - $this->passwordService->handle($database); + $this->managementService->rotatePassword($database); $database->refresh(); Activity::event('server:database.rotate-password') diff --git a/app/Models/Database.php b/app/Models/Database.php index 0b7624cec..adb5486f0 100644 --- a/app/Models/Database.php +++ b/app/Models/Database.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use PDOException; /** * @property int $id @@ -97,71 +98,97 @@ class Database extends Model implements Validatable } /** + * @throws PDOException + * * Run the provided statement against the database on a given connection. */ - private function run(string $statement): bool + private function run(string $statement): void { - return $this->host->buildConnection()->statement($statement); + $this->host->buildConnection()->statement($statement); } /** + * @throws PDOException + * * Create a new database on a given connection. */ - public function createDatabase(string $database): bool + public function createDatabase(): self { - return $this->run(sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $database)); + $this->run(sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $this->database)); + + return $this; } /** + * @throws PDOException + * * Create a new database user on a given connection. */ - public function createUser(string $username, string $remote, string $password, ?int $max_connections): bool + public function createUser(): self { - $args = [$username, $remote, $password]; + $args = [$this->username, $this->remote, $this->password]; $command = 'CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\''; - if (!empty($max_connections)) { - $args[] = $max_connections; + if (!empty($this->max_connections)) { + $args[] = $this->max_connections; $command .= ' WITH MAX_USER_CONNECTIONS %s'; } - return $this->run(sprintf($command, ...$args)); + $this->run(sprintf($command, ...$args)); + + return $this; } /** + * @throws PDOException + * * Give a specific user access to a given database. */ - public function assignUserToDatabase(string $database, string $username, string $remote): bool + public function assignUserToDatabase(): self { - return $this->run(sprintf( + $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 + $this->database, + $this->username, + $this->remote )); + + return $this; } /** + * @throws PDOException + * * Flush the privileges for a given connection. */ - public function flush(): bool + public function flushPrivileges(): self { - return $this->run('FLUSH PRIVILEGES'); + $this->run('FLUSH PRIVILEGES'); + + return $this; } /** + * @throws PDOException + * * Drop a given database on a specific connection. */ - public function dropDatabase(string $database): bool + public function dropDatabase(): self { - return $this->run(sprintf('DROP DATABASE IF EXISTS `%s`', $database)); + $this->run(sprintf('DROP DATABASE IF EXISTS `%s`', $this->database)); + + return $this; } /** + * @throws PDOException + * * Drop a given user on a specific connection. */ - public function dropUser(string $username, string $remote): bool + public function dropUser(): self { - return $this->run(sprintf('DROP USER IF EXISTS `%s`@`%s`', $username, $remote)); + $this->run(sprintf('DROP USER IF EXISTS `%s`@`%s`', $this->username, $this->remote)); + + return $this; } } diff --git a/app/Services/Databases/DatabaseManagementService.php b/app/Services/Databases/DatabaseManagementService.php index 8506b738c..0cdab4fcc 100644 --- a/app/Services/Databases/DatabaseManagementService.php +++ b/app/Services/Databases/DatabaseManagementService.php @@ -93,15 +93,11 @@ class DatabaseManagementService return $this->connection->transaction(function () use ($data) { $database = $this->createModel($data); - $database->createDatabase($database->database); - $database->createUser( - $database->username, - $database->remote, - $database->password, - $database->max_connections - ); - $database->assignUserToDatabase($database->database, $database->username, $database->remote); - $database->flush(); + $database + ->createDatabase() + ->createUser() + ->assignUserToDatabase() + ->flushPrivileges(); Activity::event('server:database.create') ->subject($database) @@ -115,14 +111,15 @@ class DatabaseManagementService /** * Delete a database from the given host server. * - * @throws \Exception + * @throws \Throwable */ public function delete(Database $database): ?bool { return $this->connection->transaction(function () use ($database) { - $database->dropDatabase($database->database); - $database->dropUser($database->username, $database->remote); - $database->flush(); + $database + ->dropDatabase() + ->dropUser() + ->flushPrivileges(); Activity::event('server:database.delete') ->subject($database) @@ -133,6 +130,28 @@ class DatabaseManagementService }); } + /** + * Updates a password for a given database. + * + * @throws \Exception + */ + public function rotatePassword(Database $database): void + { + $password = Utilities::randomStringWithSpecialCharacters(24); + + $this->connection->transaction(function () use ($database, $password) { + $database->update([ + 'password' => $password, + ]); + + $database + ->dropUser() + ->createUser() + ->assignUserToDatabase() + ->flushPrivileges(); + }); + } + /** * Create the database if there is not an identical match in the DB. While you can technically * have the same name across multiple hosts, for the sake of keeping this logic easy to understand diff --git a/app/Services/Databases/DatabasePasswordService.php b/app/Services/Databases/DatabasePasswordService.php deleted file mode 100644 index c2abb1cc5..000000000 --- a/app/Services/Databases/DatabasePasswordService.php +++ /dev/null @@ -1,40 +0,0 @@ -findOrFail($database); - } - - $password = Utilities::randomStringWithSpecialCharacters(24); - - $this->connection->transaction(function () use ($database, $password) { - $database->update([ - 'password' => $password, - ]); - - $database->dropUser($database->username, $database->remote); - $database->createUser($database->username, $database->remote, $password, $database->max_connections); - $database->assignUserToDatabase($database->database, $database->username, $database->remote); - $database->flush(); - }); - } -} diff --git a/lang/en/server/database.php b/lang/en/server/database.php index 219e24809..867498cb9 100644 --- a/lang/en/server/database.php +++ b/lang/en/server/database.php @@ -19,4 +19,8 @@ return [ 'database_host' => 'Database Host', 'database_host_select' => 'Select Database Host', 'jdbc' => 'JDBC Connection String', + 'create_notification' => 'Created :database', + 'create_notification_fail' => 'Failed to create :database', + 'delete_notification' => 'Deleted :database', + 'delete_notification_fail' => 'Failed to delete :database', ]; diff --git a/tests/Integration/Api/Client/Server/Database/DatabaseAuthorizationTest.php b/tests/Integration/Api/Client/Server/Database/DatabaseAuthorizationTest.php index b493588c8..576f838e0 100644 --- a/tests/Integration/Api/Client/Server/Database/DatabaseAuthorizationTest.php +++ b/tests/Integration/Api/Client/Server/Database/DatabaseAuthorizationTest.php @@ -5,7 +5,6 @@ namespace App\Tests\Integration\Api\Client\Server\Database; use App\Models\Subuser; use App\Models\Database; use App\Models\DatabaseHost; -use App\Services\Databases\DatabasePasswordService; use App\Services\Databases\DatabaseManagementService; use App\Tests\Integration\Api\Client\ClientApiIntegrationTestCase; use PHPUnit\Framework\Attributes\DataProvider; @@ -33,8 +32,8 @@ class DatabaseAuthorizationTest extends ClientApiIntegrationTestCase $database3 = Database::factory()->create(['server_id' => $server3->id, 'database_host_id' => $host->id]); $this - ->mock($method === 'POST' ? DatabasePasswordService::class : DatabaseManagementService::class) - ->expects($method === 'POST' ? 'handle' : 'delete') + ->mock(DatabaseManagementService::class) + ->expects($method === 'POST' ? 'rotatePassword' : 'delete') ->andReturn($method === 'POST' ? 'foo' : null); // This is the only valid call for this test, accessing the database for the same