Refactor & Catch DatabaseManagementService (#1671)

Co-authored-by: notCharles <charles@pelican.dev>
This commit is contained in:
MartinOscar 2025-09-06 22:57:11 +02:00 committed by GitHub
parent 420730ba1f
commit 2ef81eae1a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 155 additions and 103 deletions

View File

@ -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();

View File

@ -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();

View File

@ -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);
}
}),
]);
}

View File

@ -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);
}
}),
];
}

View File

@ -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);
}

View File

@ -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')

View File

@ -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;
}
}

View File

@ -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

View File

@ -1,40 +0,0 @@
<?php
namespace App\Services\Databases;
use App\Models\Database;
use App\Helpers\Utilities;
use Illuminate\Database\ConnectionInterface;
class DatabasePasswordService
{
/**
* DatabasePasswordService constructor.
*/
public function __construct(
private ConnectionInterface $connection,
) {}
/**
* Updates a password for a given database.
*/
public function handle(Database|int $database): void
{
if (is_int($database)) {
$database = Database::query()->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();
});
}
}

View File

@ -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',
];

View File

@ -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