From 420730ba1f25cf74ff940caa66864838a51e61d2 Mon Sep 17 00:00:00 2001 From: Charles Date: Sat, 6 Sep 2025 16:47:54 -0400 Subject: [PATCH 1/4] Replace `str_random` with `Str::random` (#1676) Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com> --- .../Admin/Resources/ApiKeyResource/Pages/CreateApiKey.php | 3 ++- .../Resources/DatabaseResource/Pages/ListDatabases.php | 3 ++- app/Helpers/Utilities.php | 3 ++- app/Services/Api/KeyCreationService.php | 3 ++- app/Services/Databases/DatabaseManagementService.php | 3 ++- app/Services/Users/ToggleTwoFactorService.php | 3 ++- app/Services/Users/UserCreationService.php | 2 +- .../2017_09_23_173628_RemoveDaemonSecretFromServersTable.php | 3 ++- ...2017_09_23_185022_RemoveDaemonSecretFromSubusersTable.php | 3 ++- ...0_02_202007_ChangeToABetterUniqueServiceConfiguration.php | 3 ++- .../2017_11_19_122708_MigratePubPrivFormatToSingleKey.php | 5 +++-- 11 files changed, 22 insertions(+), 12 deletions(-) diff --git a/app/Filament/Admin/Resources/ApiKeyResource/Pages/CreateApiKey.php b/app/Filament/Admin/Resources/ApiKeyResource/Pages/CreateApiKey.php index 351c577a2..a1411d081 100644 --- a/app/Filament/Admin/Resources/ApiKeyResource/Pages/CreateApiKey.php +++ b/app/Filament/Admin/Resources/ApiKeyResource/Pages/CreateApiKey.php @@ -10,6 +10,7 @@ use Filament\Actions\Action; use Filament\Actions\ActionGroup; use Filament\Resources\Pages\CreateRecord; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Str; class CreateApiKey extends CreateRecord { @@ -36,7 +37,7 @@ class CreateApiKey extends CreateRecord protected function handleRecordCreation(array $data): Model { $data['identifier'] = ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION); - $data['token'] = str_random(ApiKey::KEY_LENGTH); + $data['token'] = Str::random(ApiKey::KEY_LENGTH); $data['user_id'] = auth()->user()->id; $data['key_type'] = ApiKey::TYPE_APPLICATION; diff --git a/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php b/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php index 5440a71fd..5e71635f6 100644 --- a/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php +++ b/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php @@ -17,6 +17,7 @@ use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Resources\Pages\ListRecords; use Filament\Support\Enums\IconSize; +use Illuminate\Support\Str; class ListDatabases extends ListRecords { @@ -63,7 +64,7 @@ class ListDatabases extends ListRecords ]) ->action(function ($data, DatabaseManagementService $service) use ($server) { if (empty($data['database'])) { - $data['database'] = str_random(12); + $data['database'] = Str::random(12); } $data['database'] = 's'. $server->id . '_' . $data['database']; diff --git a/app/Helpers/Utilities.php b/app/Helpers/Utilities.php index c810788cc..b99c5e55a 100644 --- a/app/Helpers/Utilities.php +++ b/app/Helpers/Utilities.php @@ -4,6 +4,7 @@ namespace App\Helpers; use Carbon\Carbon; use Cron\CronExpression; +use Illuminate\Support\Str; use Illuminate\Support\ViewErrorBag; class Utilities @@ -14,7 +15,7 @@ class Utilities */ public static function randomStringWithSpecialCharacters(int $length = 16): string { - $string = str_random($length); + $string = Str::random($length); // Given a random string of characters, randomly loop through the characters and replace some // with special characters to avoid issues with MySQL password requirements on some servers. try { diff --git a/app/Services/Api/KeyCreationService.php b/app/Services/Api/KeyCreationService.php index 878d00f83..492661a03 100644 --- a/app/Services/Api/KeyCreationService.php +++ b/app/Services/Api/KeyCreationService.php @@ -3,6 +3,7 @@ namespace App\Services\Api; use App\Models\ApiKey; +use Illuminate\Support\Str; class KeyCreationService { @@ -33,7 +34,7 @@ class KeyCreationService $data = array_merge($data, [ 'key_type' => $this->keyType, 'identifier' => ApiKey::generateTokenIdentifier($this->keyType), - 'token' => str_random(ApiKey::KEY_LENGTH), + 'token' => Str::random(ApiKey::KEY_LENGTH), ]); if ($this->keyType !== ApiKey::TYPE_APPLICATION) { diff --git a/app/Services/Databases/DatabaseManagementService.php b/app/Services/Databases/DatabaseManagementService.php index b82c06934..8506b738c 100644 --- a/app/Services/Databases/DatabaseManagementService.php +++ b/app/Services/Databases/DatabaseManagementService.php @@ -10,6 +10,7 @@ use Illuminate\Database\ConnectionInterface; use App\Exceptions\Repository\DuplicateDatabaseNameException; use App\Exceptions\Service\Database\TooManyDatabasesException; use App\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException; +use Illuminate\Support\Str; class DatabaseManagementService { @@ -85,7 +86,7 @@ class DatabaseManagementService $data = array_merge($data, [ 'server_id' => $server->id, - 'username' => sprintf('u%d_%s', $server->id, str_random(10)), + 'username' => sprintf('u%d_%s', $server->id, Str::random(10)), 'password' => Utilities::randomStringWithSpecialCharacters(24), ]); diff --git a/app/Services/Users/ToggleTwoFactorService.php b/app/Services/Users/ToggleTwoFactorService.php index 434e5d3ef..26df0e7e8 100644 --- a/app/Services/Users/ToggleTwoFactorService.php +++ b/app/Services/Users/ToggleTwoFactorService.php @@ -3,6 +3,7 @@ namespace App\Services\Users; use App\Models\User; +use Illuminate\Support\Str; use PragmaRX\Google2FA\Google2FA; use Illuminate\Database\ConnectionInterface; use App\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid; @@ -48,7 +49,7 @@ class ToggleTwoFactorService if ((!$toggleState && !$user->use_totp) || $toggleState) { $user->recoveryTokens()->delete(); for ($i = 0; $i < 10; $i++) { - $token = str_random(10); + $token = Str::random(10); $user->recoveryTokens()->forceCreate([ 'token' => password_hash($token, PASSWORD_DEFAULT), ]); diff --git a/app/Services/Users/UserCreationService.php b/app/Services/Users/UserCreationService.php index 644dac7f5..eb4789b1f 100644 --- a/app/Services/Users/UserCreationService.php +++ b/app/Services/Users/UserCreationService.php @@ -37,7 +37,7 @@ class UserCreationService $this->connection->beginTransaction(); if (empty($data['password'])) { $generateResetToken = true; - $data['password'] = $this->hasher->make(str_random(30)); + $data['password'] = $this->hasher->make(Str::random(30)); } $isRootAdmin = array_key_exists('root_admin', $data) && $data['root_admin']; diff --git a/database/migrations/2017_09_23_173628_RemoveDaemonSecretFromServersTable.php b/database/migrations/2017_09_23_173628_RemoveDaemonSecretFromServersTable.php index a26c13c9d..db593a46c 100644 --- a/database/migrations/2017_09_23_173628_RemoveDaemonSecretFromServersTable.php +++ b/database/migrations/2017_09_23_173628_RemoveDaemonSecretFromServersTable.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; use App\Contracts\Repository\DaemonKeyRepositoryInterface; +use Illuminate\Support\Str; return new class extends Migration { @@ -21,7 +22,7 @@ return new class extends Migration $inserts[] = [ 'user_id' => $server->owner_id, 'server_id' => $server->id, - 'secret' => DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . str_random(40), + 'secret' => DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . Str::random(40), 'expires_at' => Carbon::now()->addMinutes(config('panel.api.key_expire_time', 720))->toDateTimeString(), 'created_at' => Carbon::now()->toDateTimeString(), 'updated_at' => Carbon::now()->toDateTimeString(), diff --git a/database/migrations/2017_09_23_185022_RemoveDaemonSecretFromSubusersTable.php b/database/migrations/2017_09_23_185022_RemoveDaemonSecretFromSubusersTable.php index 6216dd5d8..979c31ee5 100644 --- a/database/migrations/2017_09_23_185022_RemoveDaemonSecretFromSubusersTable.php +++ b/database/migrations/2017_09_23_185022_RemoveDaemonSecretFromSubusersTable.php @@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; use App\Contracts\Repository\DaemonKeyRepositoryInterface; +use Illuminate\Support\Str; return new class extends Migration { @@ -19,7 +20,7 @@ return new class extends Migration $inserts[] = [ 'user_id' => $subuser->user_id, 'server_id' => $subuser->server_id, - 'secret' => DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . str_random(40), + 'secret' => DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . Str::random(40), 'expires_at' => Carbon::now()->addMinutes(config('panel.api.key_expire_time', 720))->toDateTimeString(), 'created_at' => Carbon::now()->toDateTimeString(), 'updated_at' => Carbon::now()->toDateTimeString(), diff --git a/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php b/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php index 1a2497cd3..31d073a4c 100644 --- a/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php +++ b/database/migrations/2017_10_02_202007_ChangeToABetterUniqueServiceConfiguration.php @@ -1,5 +1,6 @@ select(['id', 'tag'])->get()->each(function ($option) { DB::table('service_options')->where('id', $option->id)->update([ - 'tag' => str_random(10), + 'tag' => Str::random(10), ]); }); }); diff --git a/database/migrations/2017_11_19_122708_MigratePubPrivFormatToSingleKey.php b/database/migrations/2017_11_19_122708_MigratePubPrivFormatToSingleKey.php index 82c25e65e..e5582ba54 100644 --- a/database/migrations/2017_11_19_122708_MigratePubPrivFormatToSingleKey.php +++ b/database/migrations/2017_11_19_122708_MigratePubPrivFormatToSingleKey.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; use Illuminate\Contracts\Encryption\DecryptException; +use Illuminate\Support\Str; return new class extends Migration { @@ -19,7 +20,7 @@ return new class extends Migration try { $decrypted = Crypt::decrypt($item->secret); } catch (DecryptException $exception) { - $decrypted = str_random(32); + $decrypted = Str::random(32); } finally { DB::table('api_keys')->where('id', $item->id)->update([ 'secret' => $decrypted, @@ -66,7 +67,7 @@ return new class extends Migration DB::transaction(function () { DB::table('api_keys')->get()->each(function ($item) { DB::table('api_keys')->where('id', $item->id)->update([ - 'public' => str_random(16), + 'public' => Str::random(16), 'secret' => Crypt::encrypt($item->secret), ]); }); From 2ef81eae1a4ad6e2254937072a68da8195ac2834 Mon Sep 17 00:00:00 2001 From: MartinOscar <40749467+rmartinoscar@users.noreply.github.com> Date: Sat, 6 Sep 2025 22:57:11 +0200 Subject: [PATCH 2/4] Refactor & Catch DatabaseManagementService (#1671) Co-authored-by: notCharles --- .../ServerResource/Pages/EditServer.php | 35 +++++++--- .../Actions/RotateDatabasePasswordAction.php | 10 +-- .../Server/Resources/DatabaseResource.php | 20 +++++- .../DatabaseResource/Pages/ListDatabases.php | 24 +++++-- .../Servers/DatabaseController.php | 4 +- .../Api/Client/Servers/DatabaseController.php | 4 +- app/Models/Database.php | 67 +++++++++++++------ .../Databases/DatabaseManagementService.php | 45 +++++++++---- .../Databases/DatabasePasswordService.php | 40 ----------- lang/en/server/database.php | 4 ++ .../Database/DatabaseAuthorizationTest.php | 5 +- 11 files changed, 155 insertions(+), 103 deletions(-) delete mode 100644 app/Services/Databases/DatabasePasswordService.php 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 From 47557021fd84874e0492b02b30e4513b6cd497d1 Mon Sep 17 00:00:00 2001 From: MartinOscar <40749467+rmartinoscar@users.noreply.github.com> Date: Mon, 8 Sep 2025 08:56:59 +0200 Subject: [PATCH 3/4] Remove DaemonPowerRepository (#1673) --- .../Server/BulkPowerActionCommand.php | 8 +++---- .../Features/Schemas/GSLTokenSchema.php | 6 +++--- .../Features/Schemas/JavaVersionSchema.php | 6 +++--- .../Features/Schemas/MinecraftEulaSchema.php | 6 +++--- .../ServerResource/Pages/ListServers.php | 8 +++---- .../Api/Client/Servers/PowerController.php | 6 +++--- app/Jobs/Schedule/RunTaskJob.php | 6 +++--- .../Daemon/DaemonPowerRepository.php | 21 ------------------- .../Daemon/DaemonServerRepository.php | 13 ++++++++++++ .../Api/Client/Server/PowerControllerTest.php | 8 +++---- .../Jobs/Schedule/RunTaskJobTest.php | 14 ++++++------- 11 files changed, 47 insertions(+), 55 deletions(-) delete mode 100644 app/Repositories/Daemon/DaemonPowerRepository.php diff --git a/app/Console/Commands/Server/BulkPowerActionCommand.php b/app/Console/Commands/Server/BulkPowerActionCommand.php index 2132f0ee1..3b8853c62 100644 --- a/app/Console/Commands/Server/BulkPowerActionCommand.php +++ b/app/Console/Commands/Server/BulkPowerActionCommand.php @@ -3,11 +3,11 @@ namespace App\Console\Commands\Server; use App\Models\Server; +use App\Repositories\Daemon\DaemonServerRepository; use Illuminate\Console\Command; use Illuminate\Database\Eloquent\Builder; use Illuminate\Validation\ValidationException; use Illuminate\Validation\Factory as ValidatorFactory; -use App\Repositories\Daemon\DaemonPowerRepository; use Exception; class BulkPowerActionCommand extends Command @@ -19,7 +19,7 @@ class BulkPowerActionCommand extends Command protected $description = 'Perform bulk power management on large groupings of servers or nodes at once.'; - public function handle(DaemonPowerRepository $powerRepository, ValidatorFactory $validator): void + public function handle(DaemonServerRepository $serverRepository, ValidatorFactory $validator): void { $action = $this->argument('action'); $nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes')); @@ -52,7 +52,7 @@ class BulkPowerActionCommand extends Command $bar = $this->output->createProgressBar($count); - $this->getQueryBuilder($servers, $nodes)->get()->each(function ($server, int $index) use ($action, $powerRepository, &$bar): mixed { + $this->getQueryBuilder($servers, $nodes)->get()->each(function ($server, int $index) use ($action, $serverRepository, &$bar): mixed { $bar->clear(); if (!$server instanceof Server) { @@ -60,7 +60,7 @@ class BulkPowerActionCommand extends Command } try { - $powerRepository->setServer($server)->send($action); + $serverRepository->setServer($server)->power($action); } catch (Exception $exception) { $this->output->error(trans('command/messages.server.power.action_failed', [ 'name' => $server->name, diff --git a/app/Extensions/Features/Schemas/GSLTokenSchema.php b/app/Extensions/Features/Schemas/GSLTokenSchema.php index 698d3bcdc..d6b7b455e 100644 --- a/app/Extensions/Features/Schemas/GSLTokenSchema.php +++ b/app/Extensions/Features/Schemas/GSLTokenSchema.php @@ -7,7 +7,7 @@ use App\Facades\Activity; use App\Models\Permission; use App\Models\Server; use App\Models\ServerVariable; -use App\Repositories\Daemon\DaemonPowerRepository; +use App\Repositories\Daemon\DaemonServerRepository; use Closure; use Exception; use Filament\Actions\Action; @@ -76,7 +76,7 @@ class GSLTokenSchema implements FeatureSchemaInterface ->prefix(fn () => '{{' . $serverVariable->variable->env_variable . '}}') ->helperText(fn () => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description), ]) - ->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server, $serverVariable) { + ->action(function (array $data, DaemonServerRepository $serverRepository) use ($server, $serverVariable) { /** @var Server $server */ $server = Filament::getTenant(); try { @@ -98,7 +98,7 @@ class GSLTokenSchema implements FeatureSchemaInterface ->log(); } - $powerRepository->setServer($server)->send('restart'); + $serverRepository->setServer($server)->power('restart'); Notification::make() ->title('GSL Token updated') diff --git a/app/Extensions/Features/Schemas/JavaVersionSchema.php b/app/Extensions/Features/Schemas/JavaVersionSchema.php index 6b79ff28d..827fcee21 100644 --- a/app/Extensions/Features/Schemas/JavaVersionSchema.php +++ b/app/Extensions/Features/Schemas/JavaVersionSchema.php @@ -6,7 +6,7 @@ use App\Extensions\Features\FeatureSchemaInterface; use App\Facades\Activity; use App\Models\Permission; use App\Models\Server; -use App\Repositories\Daemon\DaemonPowerRepository; +use App\Repositories\Daemon\DaemonServerRepository; use Exception; use Filament\Actions\Action; use Filament\Facades\Filament; @@ -59,7 +59,7 @@ class JavaVersionSchema implements FeatureSchemaInterface ->preload() ->native(false), ]) - ->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server) { + ->action(function (array $data, DaemonServerRepository $serverRepository) use ($server) { try { $new = $data['image']; $original = $server->image; @@ -71,7 +71,7 @@ class JavaVersionSchema implements FeatureSchemaInterface ->log(); } - $powerRepository->setServer($server)->send('restart'); + $serverRepository->setServer($server)->power('restart'); Notification::make() ->title('Docker image updated') diff --git a/app/Extensions/Features/Schemas/MinecraftEulaSchema.php b/app/Extensions/Features/Schemas/MinecraftEulaSchema.php index 9249748d6..fb40c30c1 100644 --- a/app/Extensions/Features/Schemas/MinecraftEulaSchema.php +++ b/app/Extensions/Features/Schemas/MinecraftEulaSchema.php @@ -5,7 +5,7 @@ namespace App\Extensions\Features\Schemas; use App\Extensions\Features\FeatureSchemaInterface; use App\Models\Server; use App\Repositories\Daemon\DaemonFileRepository; -use App\Repositories\Daemon\DaemonPowerRepository; +use App\Repositories\Daemon\DaemonServerRepository; use Exception; use Filament\Actions\Action; use Filament\Facades\Filament; @@ -35,14 +35,14 @@ class MinecraftEulaSchema implements FeatureSchemaInterface ->modalHeading('Minecraft EULA') ->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the Minecraft EULA .'))) ->modalSubmitActionLabel('I Accept') - ->action(function (DaemonFileRepository $fileRepository, DaemonPowerRepository $powerRepository) { + ->action(function (DaemonFileRepository $fileRepository, DaemonServerRepository $serverRepository) { try { /** @var Server $server */ $server = Filament::getTenant(); $fileRepository->setServer($server)->putContent('eula.txt', 'eula=true'); - $powerRepository->setServer($server)->send('restart'); + $serverRepository->setServer($server)->power('restart'); Notification::make() ->title('Minecraft EULA accepted') diff --git a/app/Filament/App/Resources/ServerResource/Pages/ListServers.php b/app/Filament/App/Resources/ServerResource/Pages/ListServers.php index 484bc8823..959f19a82 100644 --- a/app/Filament/App/Resources/ServerResource/Pages/ListServers.php +++ b/app/Filament/App/Resources/ServerResource/Pages/ListServers.php @@ -8,7 +8,7 @@ use App\Filament\Components\Tables\Columns\ServerEntryColumn; use App\Filament\Server\Pages\Console; use App\Models\Permission; use App\Models\Server; -use App\Repositories\Daemon\DaemonPowerRepository; +use App\Repositories\Daemon\DaemonServerRepository; use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderWidgets; use Filament\Notifications\Notification; @@ -38,11 +38,11 @@ class ListServers extends ListRecords public const WARNING_THRESHOLD = 0.7; - private DaemonPowerRepository $daemonPowerRepository; + private DaemonServerRepository $daemonServerRepository; public function boot(): void { - $this->daemonPowerRepository = new DaemonPowerRepository(); + $this->daemonServerRepository = new DaemonServerRepository(); } /** @return Stack[] */ @@ -204,7 +204,7 @@ class ListServers extends ListRecords public function powerAction(Server $server, string $action): void { try { - $this->daemonPowerRepository->setServer($server)->send($action); + $this->daemonServerRepository->setServer($server)->power($action); Notification::make() ->title(trans('server/dashboard.power_actions')) diff --git a/app/Http/Controllers/Api/Client/Servers/PowerController.php b/app/Http/Controllers/Api/Client/Servers/PowerController.php index dcf78e0a7..cef25effa 100644 --- a/app/Http/Controllers/Api/Client/Servers/PowerController.php +++ b/app/Http/Controllers/Api/Client/Servers/PowerController.php @@ -5,9 +5,9 @@ namespace App\Http\Controllers\Api\Client\Servers; use Illuminate\Http\Response; use App\Models\Server; use App\Facades\Activity; -use App\Repositories\Daemon\DaemonPowerRepository; use App\Http\Controllers\Api\Client\ClientApiController; use App\Http\Requests\Api\Client\Servers\SendPowerRequest; +use App\Repositories\Daemon\DaemonServerRepository; use Dedoc\Scramble\Attributes\Group; use Illuminate\Http\Client\ConnectionException; @@ -17,7 +17,7 @@ class PowerController extends ClientApiController /** * PowerController constructor. */ - public function __construct(private DaemonPowerRepository $repository) + public function __construct(private DaemonServerRepository $repository) { parent::__construct(); } @@ -31,7 +31,7 @@ class PowerController extends ClientApiController */ public function index(SendPowerRequest $request, Server $server): Response { - $this->repository->setServer($server)->send( + $this->repository->setServer($server)->power( $request->input('signal') ); diff --git a/app/Jobs/Schedule/RunTaskJob.php b/app/Jobs/Schedule/RunTaskJob.php index acb50f6d5..d4f1a0d1b 100644 --- a/app/Jobs/Schedule/RunTaskJob.php +++ b/app/Jobs/Schedule/RunTaskJob.php @@ -5,11 +5,11 @@ namespace App\Jobs\Schedule; use App\Jobs\Job; use Carbon\CarbonImmutable; use App\Models\Task; +use App\Repositories\Daemon\DaemonServerRepository; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use App\Services\Backups\InitiateBackupService; -use App\Repositories\Daemon\DaemonPowerRepository; use App\Services\Files\DeleteFilesService; use Exception; use Illuminate\Http\Client\ConnectionException; @@ -31,7 +31,7 @@ class RunTaskJob extends Job implements ShouldQueue */ public function handle( InitiateBackupService $backupService, - DaemonPowerRepository $powerRepository, + DaemonServerRepository $serverRepository, DeleteFilesService $deleteFilesService ): void { // Do not process a task that is not set to active, unless it's been manually triggered. @@ -57,7 +57,7 @@ class RunTaskJob extends Job implements ShouldQueue try { switch ($this->task->action) { case Task::ACTION_POWER: - $powerRepository->setServer($server)->send($this->task->payload); + $serverRepository->setServer($server)->power($this->task->payload); break; case Task::ACTION_COMMAND: $server->send($this->task->payload); diff --git a/app/Repositories/Daemon/DaemonPowerRepository.php b/app/Repositories/Daemon/DaemonPowerRepository.php deleted file mode 100644 index c34411421..000000000 --- a/app/Repositories/Daemon/DaemonPowerRepository.php +++ /dev/null @@ -1,21 +0,0 @@ -getHttpClient()->post("/api/servers/{$this->server->uuid}/power", - ['action' => $action], - ); - } -} diff --git a/app/Repositories/Daemon/DaemonServerRepository.php b/app/Repositories/Daemon/DaemonServerRepository.php index 552c55e32..e4abe7408 100644 --- a/app/Repositories/Daemon/DaemonServerRepository.php +++ b/app/Repositories/Daemon/DaemonServerRepository.php @@ -8,6 +8,7 @@ use Exception; use Filament\Notifications\Notification; use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\RequestException; +use Illuminate\Http\Client\Response; class DaemonServerRepository extends DaemonRepository { @@ -149,4 +150,16 @@ class DaemonServerRepository extends DaemonRepository ->throw() ->json('data'); } + + /** + * Sends a power action to the server instance. + * + * @throws ConnectionException + */ + public function power(string $action): Response + { + return $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/power", + ['action' => $action], + ); + } } diff --git a/tests/Integration/Api/Client/Server/PowerControllerTest.php b/tests/Integration/Api/Client/Server/PowerControllerTest.php index 9fd4fadfe..9458fadde 100644 --- a/tests/Integration/Api/Client/Server/PowerControllerTest.php +++ b/tests/Integration/Api/Client/Server/PowerControllerTest.php @@ -4,7 +4,7 @@ namespace App\Tests\Integration\Api\Client\Server; use Illuminate\Http\Response; use App\Models\Permission; -use App\Repositories\Daemon\DaemonPowerRepository; +use App\Repositories\Daemon\DaemonServerRepository; use App\Tests\Integration\Api\Client\ClientApiIntegrationTestCase; use PHPUnit\Framework\Attributes\DataProvider; @@ -49,8 +49,8 @@ class PowerControllerTest extends ClientApiIntegrationTestCase #[DataProvider('validPowerActionDataProvider')] public function test_action_can_be_sent_to_server(string $action, string $permission): void { - $service = \Mockery::mock(DaemonPowerRepository::class); - $this->app->instance(DaemonPowerRepository::class, $service); + $service = \Mockery::mock(DaemonServerRepository::class); + $this->app->instance(DaemonServerRepository::class, $service); [$user, $server] = $this->generateTestAccount([$permission]); @@ -60,7 +60,7 @@ class PowerControllerTest extends ClientApiIntegrationTestCase })) ->andReturnSelf() ->getMock() - ->expects('send') + ->expects('power') ->with(trim($action)); $this->actingAs($user) diff --git a/tests/Integration/Jobs/Schedule/RunTaskJobTest.php b/tests/Integration/Jobs/Schedule/RunTaskJobTest.php index bb7a9c9f0..9cef21548 100644 --- a/tests/Integration/Jobs/Schedule/RunTaskJobTest.php +++ b/tests/Integration/Jobs/Schedule/RunTaskJobTest.php @@ -10,8 +10,8 @@ use App\Models\Server; use App\Models\Schedule; use Illuminate\Support\Facades\Bus; use App\Jobs\Schedule\RunTaskJob; +use App\Repositories\Daemon\DaemonServerRepository; use App\Tests\Integration\IntegrationTestCase; -use App\Repositories\Daemon\DaemonPowerRepository; use Illuminate\Http\Client\ConnectionException; use PHPUnit\Framework\Attributes\DataProvider; @@ -84,13 +84,13 @@ class RunTaskJobTest extends IntegrationTestCase 'continue_on_failure' => false, ]); - $mock = \Mockery::mock(DaemonPowerRepository::class); - $this->instance(DaemonPowerRepository::class, $mock); + $mock = \Mockery::mock(DaemonServerRepository::class); + $this->instance(DaemonServerRepository::class, $mock); $mock->expects('setServer')->with(\Mockery::on(function ($value) use ($server) { return $value instanceof Server && $value->id === $server->id; }))->andReturnSelf(); - $mock->expects('send')->with('start'); + $mock->expects('power')->with('start'); Bus::dispatchSync(new RunTaskJob($task, $isManualRun)); @@ -117,10 +117,10 @@ class RunTaskJobTest extends IntegrationTestCase 'continue_on_failure' => $continueOnFailure, ]); - $mock = \Mockery::mock(DaemonPowerRepository::class); - $this->instance(DaemonPowerRepository::class, $mock); + $mock = \Mockery::mock(DaemonServerRepository::class); + $this->instance(DaemonServerRepository::class, $mock); - $mock->expects('setServer->send')->andThrow(new ConnectionException()); + $mock->expects('setServer->power')->andThrow(new ConnectionException()); if (!$continueOnFailure) { $this->expectException(ConnectionException::class); From 32eb1abd4ae692236381598769e1d960190f30ad Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 8 Sep 2025 09:03:23 +0200 Subject: [PATCH 4/4] Improve `join_paths` helper method (#1668) --- .../Server/Resources/FileResource/Pages/ListFiles.php | 11 ++++++----- app/helpers.php | 11 ++++++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php b/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php index e118d1bb2..0a284ed92 100644 --- a/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php +++ b/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php @@ -193,10 +193,10 @@ class ListFiles extends ListRecords ->required() ->live(), Placeholder::make('new_location') - ->content(fn (Get $get, File $file) => resolve_path('./' . join_paths($this->path, $get('location') ?? '/', $file->name))), + ->content(fn (Get $get, File $file) => resolve_path(join_paths($this->path, $get('location') ?? '/', $file->name))), ]) ->action(function ($data, File $file) { - $location = rtrim($data['location'], '/'); + $location = $data['location']; $files = [['to' => join_paths($location, $file->name), 'from' => $file->name]]; $this->getDaemonFileRepository()->renameFiles($this->path, $files); @@ -353,10 +353,10 @@ class ListFiles extends ListRecords ->required() ->live(), Placeholder::make('new_location') - ->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))), + ->content(fn (Get $get) => resolve_path(join_paths($this->path, $get('location') ?? ''))), ]) ->action(function (Collection $files, $data) { - $location = rtrim($data['location'], '/'); + $location = $data['location']; $files = $files->map(fn ($file) => ['to' => join_paths($location, $file['name']), 'from' => $file['name']])->toArray(); $this->getDaemonFileRepository()->renameFiles($this->path, $files); @@ -432,11 +432,12 @@ class ListFiles extends ListRecords ->modalSubmitActionLabel(trans('server/file.actions.new_file.create')) ->action(function ($data) { $path = join_paths($this->path, $data['name']); + try { $this->getDaemonFileRepository()->putContent($path, $data['editor'] ?? ''); Activity::event('server:file.write') - ->property('file', join_paths($path, $data['name'])) + ->property('file', $path) ->log(); } catch (FileExistsException) { AlertBanner::make('file_already_exists') diff --git a/app/helpers.php b/app/helpers.php index 88e309f78..e447e1fd6 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -52,11 +52,16 @@ if (!function_exists('convert_bytes_to_readable')) { if (!function_exists('join_paths')) { function join_paths(string $base, string ...$paths): string { - if ($base === '/') { - return str_replace('//', '', implode('/', $paths)); + $base = trim($base, '/'); + + $paths = array_map(fn (string $path) => trim($path, '/'), $paths); + $paths = array_filter($paths, fn (string $path) => strlen($path) > 0); + + if (empty($base)) { + return implode('/', $paths); } - return str_replace('//', '', $base . '/' . implode('/', $paths)); + return $base . '/' . implode('/', $paths); } }