From 840754757479c94aa42f4124f719899356cb6e6b Mon Sep 17 00:00:00 2001 From: pelican-vehikl Date: Thu, 24 Apr 2025 18:24:18 -0400 Subject: [PATCH 1/8] Add back Egg Features (#1271) Co-authored-by: Boy132 Co-authored-by: Lance Pioch Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com> --- app/Extensions/Features/FeatureProvider.php | 51 ++++++++ app/Extensions/Features/GSLToken.php | 122 ++++++++++++++++++ app/Extensions/Features/JavaVersion.php | 97 ++++++++++++++ app/Extensions/Features/MinecraftEula.php | 72 +++++++++++ app/Extensions/Features/PIDLimit.php | 76 +++++++++++ app/Extensions/Features/SteamDiskSpace.php | 64 +++++++++ app/Filament/Server/Pages/Console.php | 43 ++++-- app/Models/Egg.php | 20 +-- app/Providers/AppServiceProvider.php | 12 ++ .../ServerConfigurationStructureService.php | 3 + .../Api/Application/EggTransformer.php | 1 + .../components/server-console.blade.php | 3 + .../filament/server/pages/console.blade.php | 3 + 13 files changed, 545 insertions(+), 22 deletions(-) create mode 100644 app/Extensions/Features/FeatureProvider.php create mode 100644 app/Extensions/Features/GSLToken.php create mode 100644 app/Extensions/Features/JavaVersion.php create mode 100644 app/Extensions/Features/MinecraftEula.php create mode 100644 app/Extensions/Features/PIDLimit.php create mode 100644 app/Extensions/Features/SteamDiskSpace.php diff --git a/app/Extensions/Features/FeatureProvider.php b/app/Extensions/Features/FeatureProvider.php new file mode 100644 index 000000000..eca3ffbb0 --- /dev/null +++ b/app/Extensions/Features/FeatureProvider.php @@ -0,0 +1,51 @@ + + */ + protected static array $providers = []; + + /** + * @param string[] $id + * @return self|static[] + */ + public static function getProviders(string|array|null $id = null): array|self + { + if (is_array($id)) { + return array_intersect_key(static::$providers, array_flip($id)); + } + + return $id ? static::$providers[$id] : static::$providers; + } + + protected function __construct(protected Application $app) + { + if (array_key_exists($this->getId(), static::$providers)) { + if (!$this->app->runningUnitTests()) { + logger()->warning("Tried to create duplicate Feature provider with id '{$this->getId()}'"); + } + + return; + } + + static::$providers[$this->getId()] = $this; + } + + abstract public function getId(): string; + + /** + * A matching subset string (case-sensitive) from the console output + * + * @return array + */ + abstract public function getListeners(): array; + + abstract public function getAction(): Action; +} diff --git a/app/Extensions/Features/GSLToken.php b/app/Extensions/Features/GSLToken.php new file mode 100644 index 000000000..c4fc8508c --- /dev/null +++ b/app/Extensions/Features/GSLToken.php @@ -0,0 +1,122 @@ + */ + public function getListeners(): array + { + return [ + 'gsl token expired', + 'account not found', + ]; + } + + public function getId(): string + { + return 'gsltoken'; + } + + public function getAction(): Action + { + /** @var Server $server */ + $server = Filament::getTenant(); + + /** @var ServerVariable $serverVariable */ + $serverVariable = $server->serverVariables()->where('env_variable', 'STEAM_ACC')->first(); + + return Action::make($this->getId()) + ->requiresConfirmation() + ->modalHeading('Invalid GSL token') + ->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.') + ->modalSubmitActionLabel('Update GSL Token') + ->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server)) + ->form([ + Placeholder::make('java') + ->label('You can either generate a new one and enter it below or leave the field blank to remove it + completely.'), + TextInput::make('gsltoken') + ->label('GSL Token') + ->rules([ + fn (): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) { + $validator = Validator::make(['validatorkey' => $value], [ + 'validatorkey' => $serverVariable->variable->rules, + ]); + + if ($validator->fails()) { + $message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name); + + $fail($message); + } + }, + ]) + ->hintIcon('tabler-code') + ->label(fn () => $serverVariable->variable->name) + ->hintIconTooltip(fn () => implode('|', $serverVariable->variable->rules)) + ->prefix(fn () => '{{' . $serverVariable->variable->env_variable . '}}') + ->helperText(fn () => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description), + ]) + ->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server, $serverVariable) { + /** @var Server $server */ + $server = Filament::getTenant(); + try { + $new = $data['gsltoken'] ?? ''; + $original = $serverVariable->variable_value; + + $serverVariable->update([ + 'variable_value' => $new, + ]); + + if ($original !== $new) { + + Activity::event('server:startup.edit') + ->property([ + 'variable' => $serverVariable->variable->env_variable, + 'old' => $original, + 'new' => $new, + ]) + ->log(); + } + + $powerRepository->setServer($server)->send('restart'); + + Notification::make() + ->title('GSL Token updated') + ->body('Restart the server to use the new token.') + ->success() + ->send(); + } catch (\Exception $e) { + Notification::make() + ->title('Error') + ->body($e->getMessage()) + ->danger() + ->send(); + } + }); + } + + public static function register(Application $app): self + { + return new self($app); + } +} diff --git a/app/Extensions/Features/JavaVersion.php b/app/Extensions/Features/JavaVersion.php new file mode 100644 index 000000000..d7f5b80d1 --- /dev/null +++ b/app/Extensions/Features/JavaVersion.php @@ -0,0 +1,97 @@ + */ + public function getListeners(): array + { + return [ + 'java.lang.UnsupportedClassVersionError', + 'minecraft 1.17 requires running the server with java 16 or above', + 'minecraft 1.18 requires running the server with java 17 or above', + 'unsupported major.minor version', + 'has been compiled by a more recent version of the java runtime', + ]; + } + + public function getId(): string + { + return 'java_version'; + } + + public function getAction(): Action + { + /** @var Server $server */ + $server = Filament::getTenant(); + + return Action::make($this->getId()) + ->requiresConfirmation() + ->modalHeading('Unsupported Java Version') + ->modalDescription('This server is currently running an unsupported version of Java and cannot be started.') + ->modalSubmitActionLabel('Update Docker Image') + ->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server)) + ->form([ + Placeholder::make('java') + ->label('Please select a supported version from the list below to continue starting the server.'), + Select::make('image') + ->label('Docker Image') + ->disabled(fn () => !in_array($server->image, $server->egg->docker_images)) + ->options(fn () => collect($server->egg->docker_images)->mapWithKeys(fn ($key, $value) => [$key => $value])) + ->selectablePlaceholder(false) + ->default(fn () => $server->image) + ->notIn(fn () => $server->image) + ->required() + ->preload() + ->native(false), + ]) + ->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server) { + try { + $new = $data['image']; + $original = $server->image; + $server->forceFill(['image' => $new])->saveOrFail(); + + if ($original !== $server->image) { + Activity::event('server:startup.image') + ->property(['old' => $original, 'new' => $new]) + ->log(); + } + $powerRepository->setServer($server)->send('restart'); + + Notification::make() + ->title('Docker image updated') + ->body('Restart the server to use the new image.') + ->success() + ->send(); + } catch (\Exception $e) { + Notification::make() + ->title('Error') + ->body($e->getMessage()) + ->danger() + ->send(); + } + }); + } + + public static function register(Application $app): self + { + return new self($app); + } +} diff --git a/app/Extensions/Features/MinecraftEula.php b/app/Extensions/Features/MinecraftEula.php new file mode 100644 index 000000000..7627f9fc3 --- /dev/null +++ b/app/Extensions/Features/MinecraftEula.php @@ -0,0 +1,72 @@ + */ + public function getListeners(): array + { + return [ + 'You need to agree to the EULA in order to run the server', + ]; + } + + public function getId(): string + { + return 'eula'; + } + + public function getAction(): Action + { + return Action::make($this->getId()) + ->requiresConfirmation() + ->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) { + try { + /** @var Server $server */ + $server = Filament::getTenant(); + $content = $fileRepository->setServer($server)->getContent('eula.txt'); + $content = preg_replace('/(eula=)false/', '\1true', $content); + $fileRepository->setServer($server)->putContent('eula.txt', $content); + $powerRepository->setServer($server)->send('restart'); + + Notification::make() + ->title('Docker image updated') + ->body('Restart the server.') + ->success() + ->send(); + } catch (Exception $e) { + Notification::make() + ->title('Error') + ->body($e->getMessage()) + ->danger() + ->send(); + } + } + ); + } + + public static function register(Application $app): self + { + return new self($app); + } +} diff --git a/app/Extensions/Features/PIDLimit.php b/app/Extensions/Features/PIDLimit.php new file mode 100644 index 000000000..dfae1d8c4 --- /dev/null +++ b/app/Extensions/Features/PIDLimit.php @@ -0,0 +1,76 @@ + */ + public function getListeners(): array + { + return [ + 'pthread_create failed', + 'failed to create thread', + 'unable to create thread', + 'unable to create native thread', + 'unable to create new native thread', + 'exception in thread "craft async scheduler management thread"', + ]; + } + + public function getId(): string + { + return 'pid_limit'; + } + + public function getAction(): Action + { + return Action::make($this->getId()) + ->requiresConfirmation() + ->icon('tabler-alert-triangle') + ->modalHeading(fn () => auth()->user()->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...') + ->modalDescription(new HtmlString(Blade::render( + auth()->user()->isAdmin() ? <<<'HTML' +

+ This server has reached the maximum process or memory limit. +

+

+ Increasing container_pid_limit in the wings + configuration, config.yml, might help resolve + this issue. +

+

+ Note: Wings must be restarted for the configuration file changes to take effect +

+ HTML + : + <<<'HTML' +

+ This server is attempting to use more resources than allocated. Please contact the administrator + and give them the error below. +

+

+ + pthread_create failed, Possibly out of memory or process/resource limits reached + +

+ HTML + ))) + ->modalCancelActionLabel('Close') + ->action(fn () => null); + } + + public static function register(Application $app): self + { + return new self($app); + } +} diff --git a/app/Extensions/Features/SteamDiskSpace.php b/app/Extensions/Features/SteamDiskSpace.php new file mode 100644 index 000000000..c4479a987 --- /dev/null +++ b/app/Extensions/Features/SteamDiskSpace.php @@ -0,0 +1,64 @@ + */ + public function getListeners(): array + { + return [ + 'steamcmd needs 250mb of free disk space to update', + '0x202 after update job', + ]; + } + + public function getId(): string + { + return 'steam_disk_space'; + } + + public function getAction(): Action + { + return Action::make($this->getId()) + ->requiresConfirmation() + ->modalHeading('Out of available disk space...') + ->modalDescription(new HtmlString(Blade::render( + auth()->user()->isAdmin() ? <<<'HTML' +

+ This server has run out of available disk space and cannot complete the install or update + process. +

+

+ Ensure the machine has enough disk space by typing{' '} + df -h on the machine hosting + this server. Delete files or increase the available disk space to resolve the issue. +

+ HTML + : + <<<'HTML' +

+ This server has run out of available disk space and cannot complete the install or update + process. Please get in touch with the administrator(s) and inform them of disk space issues. +

+ HTML + ))) + ->modalCancelActionLabel('Close') + ->action(fn () => null); + } + + public static function register(Application $app): self + { + return new self($app); + } +} diff --git a/app/Filament/Server/Pages/Console.php b/app/Filament/Server/Pages/Console.php index b909b665e..065482bfb 100644 --- a/app/Filament/Server/Pages/Console.php +++ b/app/Filament/Server/Pages/Console.php @@ -5,6 +5,7 @@ namespace App\Filament\Server\Pages; use App\Enums\ConsoleWidgetPosition; use App\Enums\ContainerStatus; use App\Exceptions\Http\Server\ServerStateConflictException; +use App\Extensions\Features\FeatureProvider; use App\Filament\Server\Widgets\ServerConsole; use App\Filament\Server\Widgets\ServerCpuChart; use App\Filament\Server\Widgets\ServerMemoryChart; @@ -13,8 +14,9 @@ use App\Filament\Server\Widgets\ServerOverview; use App\Livewire\AlertBanner; use App\Models\Permission; use App\Models\Server; -use Filament\Actions\Action; +use Filament\Actions\Concerns\InteractsWithActions; use Filament\Facades\Filament; +use Filament\Actions\Action; use Filament\Pages\Page; use Filament\Support\Enums\ActionSize; use Filament\Widgets\Widget; @@ -23,6 +25,8 @@ use Livewire\Attributes\On; class Console extends Page { + use InteractsWithActions; + protected static ?string $navigationIcon = 'tabler-brand-tabler'; protected static ?int $navigationSort = 1; @@ -47,6 +51,30 @@ class Console extends Page } } + public function boot(): void + { + /** @var Server $server */ + $server = Filament::getTenant(); + /** @var FeatureProvider $feature */ + foreach ($server->egg->features() as $feature) { + $this->cacheAction($feature->getAction()); + } + } + + #[On('mount-feature')] + public function mountFeature(string $data): void + { + $data = json_decode($data); + $feature = data_get($data, 'key'); + + $feature = FeatureProvider::getProviders($feature); + if ($this->getMountedAction()) { + return; + } + $this->mountAction($feature->getId()); + sleep(2); // TODO find a better way + } + public function getWidgetData(): array { return [ @@ -126,33 +154,30 @@ class Console extends Page Action::make('start') ->color('primary') ->size(ActionSize::ExtraLarge) - ->action(fn () => $this->dispatch('setServerState', state: 'start', uuid: $server->uuid)) + ->dispatch('setServerState', ['state' => 'start', 'uuid' => $server->uuid]) ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_START, $server)) ->disabled(fn () => $server->isInConflictState() || !$this->status->isStartable()) ->icon('tabler-player-play-filled'), Action::make('restart') ->color('gray') ->size(ActionSize::ExtraLarge) - ->action(fn () => $this->dispatch('setServerState', state: 'restart', uuid: $server->uuid)) + ->dispatch('setServerState', ['state' => 'restart', 'uuid' => $server->uuid]) ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server)) ->disabled(fn () => $server->isInConflictState() || !$this->status->isRestartable()) ->icon('tabler-reload'), Action::make('stop') ->color('danger') ->size(ActionSize::ExtraLarge) - ->action(fn () => $this->dispatch('setServerState', state: 'stop', uuid: $server->uuid)) + ->dispatch('setServerState', ['state' => 'stop', 'uuid' => $server->uuid]) ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server)) ->hidden(fn () => $this->status->isStartingOrStopping() || $this->status->isKillable()) ->disabled(fn () => $server->isInConflictState() || !$this->status->isStoppable()) ->icon('tabler-player-stop-filled'), Action::make('kill') ->color('danger') - ->requiresConfirmation() - ->modalHeading('Do you wish to kill this server?') - ->modalDescription('This can result in data corruption and/or data loss!') - ->modalSubmitActionLabel('Kill Server') + ->tooltip('This can result in data corruption and/or data loss!') ->size(ActionSize::ExtraLarge) - ->action(fn () => $this->dispatch('setServerState', state: 'kill', uuid: $server->uuid)) + ->dispatch('setServerState', ['state' => 'kill', 'uuid' => $server->uuid]) ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server)) ->hidden(fn () => $server->isInConflictState() || !$this->status->isKillable()) ->icon('tabler-alert-square'), diff --git a/app/Models/Egg.php b/app/Models/Egg.php index 300371f25..f3091016b 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -5,6 +5,7 @@ namespace App\Models; use App\Contracts\Validatable; use App\Exceptions\Service\Egg\HasChildrenException; use App\Exceptions\Service\HasActiveServersException; +use App\Extensions\Features\FeatureProvider; use App\Traits\HasValidation; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -70,19 +71,6 @@ class Egg extends Model implements Validatable */ public const EXPORT_VERSION = 'PLCN_v1'; - /** - * Different features that can be enabled on any given egg. These are used internally - * to determine which types of frontend functionality should be shown to the user. Eggs - * will automatically inherit features from a parent egg if they are already configured - * to copy configuration values from said egg. - * - * To skip copying the features, an empty array value should be passed in ("[]") rather - * than leaving it null. - */ - public const FEATURE_EULA_POPUP = 'eula'; - - public const FEATURE_FASTDL = 'fastdl'; - /** * Fields that are not mass assignable. */ @@ -172,6 +160,12 @@ class Egg extends Model implements Validatable }); } + /** @return array */ + public function features(): array + { + return FeatureProvider::getProviders($this->features); + } + /** * Returns the install script for the egg; if egg is copying from another * it will return the copied script. diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 2dd341b8d..7ba52a820 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -15,6 +15,11 @@ use App\Extensions\Avatar\Providers\UiAvatarsProvider; use App\Extensions\OAuth\Providers\GitlabProvider; use App\Models; use App\Extensions\Captcha\Providers\TurnstileProvider; +use App\Extensions\Features\GSLToken; +use App\Extensions\Features\JavaVersion; +use App\Extensions\Features\MinecraftEula; +use App\Extensions\Features\PIDLimit; +use App\Extensions\Features\SteamDiskSpace; use App\Extensions\OAuth\Providers\AuthentikProvider; use App\Extensions\OAuth\Providers\CommonProvider; use App\Extensions\OAuth\Providers\DiscordProvider; @@ -121,6 +126,13 @@ class AppServiceProvider extends ServiceProvider GravatarProvider::register(); UiAvatarsProvider::register(); + // Default Feature providers + GSLToken::register($app); + JavaVersion::register($app); + MinecraftEula::register($app); + PIDLimit::register($app); + SteamDiskSpace::register($app); + FilamentColor::register([ 'danger' => Color::Red, 'gray' => Color::Zinc, diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index 541d07399..b0c8156c3 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -101,6 +101,9 @@ class ServerConfigurationStructureService 'egg' => [ 'id' => $server->egg->uuid, 'file_denylist' => $server->egg->inherit_file_denylist, + 'features' => collect($server->egg->features())->mapWithKeys(fn ($feature) => [ + $feature->getId() => $feature->getListeners(), + ])->all(), ], ]; diff --git a/app/Transformers/Api/Application/EggTransformer.php b/app/Transformers/Api/Application/EggTransformer.php index be342f24f..a416ed026 100644 --- a/app/Transformers/Api/Application/EggTransformer.php +++ b/app/Transformers/Api/Application/EggTransformer.php @@ -46,6 +46,7 @@ class EggTransformer extends BaseTransformer 'name' => $model->name, 'author' => $model->author, 'description' => $model->description, + 'features' => $model->features, // "docker_image" is deprecated, but left here to avoid breaking too many things at once // in external software. We'll remove it down the road once things have gotten the chance // to upgrade to using "docker_images". diff --git a/resources/views/filament/components/server-console.blade.php b/resources/views/filament/components/server-console.blade.php index a2e19f487..4ce34a40d 100644 --- a/resources/views/filament/components/server-console.blade.php +++ b/resources/views/filament/components/server-console.blade.php @@ -129,6 +129,9 @@ case 'install output': handleConsoleOutput(args[0]); break; + case 'feature match': + Livewire.dispatch('mount-feature', { data: args[0] }); + break; case 'status': handlePowerChangeEvent(args[0]); diff --git a/resources/views/filament/server/pages/console.blade.php b/resources/views/filament/server/pages/console.blade.php index 2deb74c68..7e3f6250c 100644 --- a/resources/views/filament/server/pages/console.blade.php +++ b/resources/views/filament/server/pages/console.blade.php @@ -4,4 +4,7 @@ :data="$this->getWidgetData()" :widgets="$this->getVisibleWidgets()" /> + + + From 98a2cab5ca24eac0dbc2a9a71c181ba363e6d40b Mon Sep 17 00:00:00 2001 From: MartinOscar <40749467+rmartinoscar@users.noreply.github.com> Date: Sat, 26 Apr 2025 12:41:59 +0200 Subject: [PATCH 2/8] Case insensitive EggFeature Listeners (#1303) --- app/Extensions/Features/FeatureProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Extensions/Features/FeatureProvider.php b/app/Extensions/Features/FeatureProvider.php index eca3ffbb0..2558bd1ed 100644 --- a/app/Extensions/Features/FeatureProvider.php +++ b/app/Extensions/Features/FeatureProvider.php @@ -41,7 +41,7 @@ abstract class FeatureProvider abstract public function getId(): string; /** - * A matching subset string (case-sensitive) from the console output + * A matching subset string (case-insensitive) from the console output * * @return array */ From 329a29f7daad089dd3901cc06fd68b2075cd76d0 Mon Sep 17 00:00:00 2001 From: MartinOscar <40749467+rmartinoscar@users.noreply.github.com> Date: Sat, 26 Apr 2025 12:42:29 +0200 Subject: [PATCH 3/8] Add missing `disabled` in `AllocationsRelationManager` (#1304) --- .../RelationManagers/AllocationsRelationManager.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Filament/Admin/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php b/app/Filament/Admin/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php index 736f56183..0ad4997a3 100644 --- a/app/Filament/Admin/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php +++ b/app/Filament/Admin/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php @@ -70,6 +70,7 @@ class AllocationsRelationManager extends RelationManager ->label(trans('admin/server.ip_address')) ->inlineLabel() ->ipv4() + ->live() ->afterStateUpdated(fn (Set $set) => $set('allocation_ports', [])) ->required(), TextInput::make('allocation_alias') @@ -83,6 +84,7 @@ class AllocationsRelationManager extends RelationManager ->label(trans('admin/server.ports')) ->inlineLabel() ->live() + ->disabled(fn (Get $get) => empty($get('allocation_ip'))) ->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip'))) ) From 8ae3c88c913e5766816b8de555b3d2a627c59cee Mon Sep 17 00:00:00 2001 From: Letter N <24603524+LetterN@users.noreply.github.com> Date: Sun, 27 Apr 2025 02:06:30 +0800 Subject: [PATCH 4/8] generalize sponge installation (#1300) --- .../minecraft/egg-sponge--sponge-vanilla.json | 56 ++++++++++++++----- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.json b/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.json index 2296aba9a..6f87376da 100644 --- a/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.json +++ b/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.json @@ -4,11 +4,11 @@ "version": "PLCN_v1", "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-sponge--sponge-vanilla.json" }, - "exported_at": "2025-03-18T12:35:50+00:00", - "name": "Sponge (SpongeVanilla)", + "exported_at": "2025-04-25T06:05:10+00:00", + "name": "Sponge", "author": "panel@example.com", "uuid": "f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d", - "description": "SpongeVanilla is the SpongeAPI implementation for Vanilla Minecraft.", + "description": "A community-driven open source Minecraft: Java Edition modding platform.", "tags": [ "minecraft" ], @@ -34,28 +34,42 @@ }, "scripts": { "installation": { - "script": "#!\/bin\/ash\r\n# Sponge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\ncd \/mnt\/server\r\n\r\ncurl -sSL \"https:\/\/repo.spongepowered.org\/maven\/org\/spongepowered\/spongevanilla\/${SPONGE_VERSION}\/spongevanilla-${SPONGE_VERSION}.jar\" -o ${SERVER_JARFILE}", + "script": "#!\/bin\/ash\r\n# Sponge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\ncd \/mnt\/server\r\n\r\nif [ $MINECRAFT_VERSION = 'latest' ] || [ -z $MINECRAFT_VERSION ]; then\r\n TARGET_VERSION_JSON=$(curl -sSL https:\/\/dl-api.spongepowered.org\/v2\/groups\/org.spongepowered\/artifacts\/${SPONGE_TYPE}\/latest?recommended=true)\r\n if [ -z \"${TARGET_VERSION_JSON}\" ]; then\r\n echo -e \"Failed to find latest recommended version!\"\r\n exit 1\r\n fi\r\n echo -e \"Found latest version for ${SPONGE_TYPE}\"\r\nelse\r\n if [ $SPONGE_TYPE = 'spongevanilla' ]; then \r\n VERSIONS_JSON=$(curl -sSL https:\/\/dl-api.spongepowered.org\/v2\/groups\/org.spongepowered\/artifacts\/${SPONGE_TYPE}\/versions?tags=,minecraft:${MINECRAFT_VERSION}&offset=0&limit=1)\r\n else\r\n FORGETAG='forge'\r\n if [ $SPONGE_TYPE = 'spongeneo' ]; then\r\n FORGETAG='neoforge'\r\n fi\r\n VERSIONS_JSON=$(curl -sSL https:\/\/dl-api.spongepowered.org\/v2\/groups\/org.spongepowered\/artifacts\/${SPONGE_TYPE}\/versions?tags=,minecraft:${MINECRAFT_VERSION},${FORGETAG}:${FORGE_VERSION}&offset=0&limit=1)\r\n fi\r\n \r\n if [ -z \"${VERSIONS_JSON}\" ]; then\r\n echo -e \"Failed to find recommended ${MINECRAFT_VERSION} version for ${SPONGE_TYPE} ${FORGE_VERSION}!\"\r\n exit 1\r\n fi\r\n \r\n VERSION_KEY=$(echo $VERSIONS_JSON | jq -r '.artifacts | to_entries[0].key')\r\n TARGET_VERSION_JSON=$(curl -sSL https:\/\/dl-api.spongepowered.org\/v2\/groups\/org.spongepowered\/artifacts\/${SPONGE_TYPE}\/versions\/${VERSION_KEY})\r\n \r\n if [ -z \"${TARGET_VERSION_JSON}\" ]; then\r\n echo -e \"Failed to find ${VERSION_KEY} for ${SPONGE_TYPE} ${FORGE_VERSION}!\"\r\n exit 1\r\n fi\r\n\r\n echo -e \"Found ${MINECRAFT_VERSION} for ${SPONGE_TYPE}\"\r\nfi\r\n\r\nTARGET_VERSION=`echo $TARGET_VERSION_JSON | jq '.assets[] | select(.classifier == \"universal\")'`\r\nif [ -z \"${TARGET_VERSION}\" ]; then\r\n TARGET_VERSION=`echo $TARGET_VERSION_JSON | jq '.assets[] | select(.classifier == \"\" and .extension == \"jar\")'`\r\nfi\r\n\r\nif [ -z \"${TARGET_VERSION}\" ]; then\r\n echo -e \"Failed to get download url data from the selected version\"\r\n exit 1\r\nfi\r\n\r\nSPONGE_URL=$(echo $TARGET_VERSION | jq -r '.downloadUrl')\r\nCHECKSUM=$(echo $TARGET_VERSION | jq -r '.md5')\r\necho -e \"Found file at ${SPONGE_URL} with checksum ${CHECKSUM}\"\r\n\r\necho -e \"running: curl -o ${SERVER_JARFILE} ${SPONGE_URL}\"\r\ncurl -o ${SERVER_JARFILE} ${SPONGE_URL}\r\n\r\nif [ $(basename $(md5sum ${SERVER_JARFILE})) = ${CHECKSUM} ] ; then\r\n echo \"Checksum passed\"\r\nelse\r\n echo \"Checksum failed\"\r\nfi\r\n\r\necho -e \"Install Complete\"", "container": "ghcr.io\/parkervcp\/installers:alpine", "entrypoint": "ash" } }, "variables": [ { - "name": "Sponge Version", - "description": "The version of SpongeVanilla to download and use.", - "env_variable": "SPONGE_VERSION", - "default_value": "1.12.2-7.3.0", + "sort": 3, + "name": "Forge\/Neoforge Version", + "description": "The modding api target version if set to `spongeforge` or `spongeneo`. Leave blank if using `spongevanilla`", + "env_variable": "FORGE_VERSION", + "default_value": "", + "user_viewable": true, + "user_editable": true, + "rules": [ + "string" + ] + }, + { + "sort": 1, + "name": "Minecraft Version", + "description": "The version of Minecraft to target. Use \"latest\" to install the latest version. Go to Settings > Reinstall Server to apply.", + "env_variable": "MINECRAFT_VERSION", + "default_value": "latest", "user_viewable": true, "user_editable": true, "rules": [ "required", - "regex:\/^([a-zA-Z0-9.\\-_]+)$\/" - ], - "sort": 1 + "string", + "between:3,15" + ] }, { + "sort": 4, "name": "Server Jar File", - "description": "The name of the Jarfile to use when running SpongeVanilla.", + "description": "The name of the Jarfile to use when running Sponge.", "env_variable": "SERVER_JARFILE", "default_value": "server.jar", "user_viewable": true, @@ -63,8 +77,20 @@ "rules": [ "required", "regex:\/^([\\w\\d._-]+)(\\.jar)$\/" - ], - "sort": 2 + ] + }, + { + "sort": 2, + "name": "Sponge Type", + "description": "SpongeVanilla if you are only using Sponge plugins.\nSpongeForge when using Forge mods and Sponge plugins.\nSpongeNeo when using NeoForge mods and Sponge plugins.", + "env_variable": "SPONGE_TYPE", + "default_value": "spongevanilla", + "user_viewable": true, + "user_editable": true, + "rules": [ + "required", + "in:spongevanilla,spongeforge,spongeneo" + ] } ] -} \ No newline at end of file +} From b39a8186ae2b9e79963a9ecf15813b776972408c Mon Sep 17 00:00:00 2001 From: "Michael (Parker) Parker" Date: Sun, 27 Apr 2025 20:56:10 -0400 Subject: [PATCH 5/8] Resolve issue with avatar storage (#1281) * Resolve issue with avatar storage This resolves the issue with getting avatar storage working updates the entrypoint to create the `pelican-data/storage` folder on start. Adds a dev dockerfile to build locally instead of needing to update the standard dockerfile. * Move avatar folder Moves the avatars folder in the storage folder in-case anything else needs storage as well. Fixes an issue in the entrypoint where it wasn't creating the sub-folder correctly. --- Dockerfile | 24 ++++------ Dockerfile.dev | 111 +++++++++++++++++++++++++++++++++++++++++++ docker/entrypoint.sh | 2 +- 3 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 Dockerfile.dev diff --git a/Dockerfile b/Dockerfile index 1b719bedb..f07367734 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,9 @@ # syntax=docker.io/docker/dockerfile:1.13-labs # Pelican Production Dockerfile - -# For those who want to build this Dockerfile themselves, uncomment lines 6-12 and replace "localhost:5000/base-php:$TARGETARCH" on lines 17 and 67 with "base". - -# FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine as base - -# ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ - -# RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql - -# RUN rm /usr/local/bin/install-php-extensions +## +# If you want to build this locally you want to run `docker build -f Dockerfile.dev` +## # ================================ # Stage 1-1: Composer Install @@ -82,15 +75,14 @@ RUN chown root:www-data ./ \ && chmod 750 ./ \ # Files should not have execute set, but directories need it && find ./ -type d -exec chmod 750 {} \; \ - # Symlink to env/database path, as www-data won't be able to write to webroot + # Create necessary directories + && mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \ + # Symlinks for env, database, and avatars && ln -s /pelican-data/.env ./.env \ && ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \ - && mkdir -p /pelican-data/storage \ && ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \ - && ln -s /pelican-data/storage /var/www/html/storage/app/public/avatars \ - # Create necessary directories - && mkdir -p /pelican-data /var/run/supervisord /etc/supercronic \ - # Finally allow www-data write permissions where necessary + && ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \ + # Allow www-data write permissions where necessary && chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \ && chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 000000000..078e107c9 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,111 @@ +# syntax=docker.io/docker/dockerfile:1.13-labs +# Pelican Development Dockerfile + +FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine AS base + +ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ + +RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql + +RUN rm /usr/local/bin/install-php-extensions + +# ================================ +# Stage 1-1: Composer Install +# ================================ +FROM --platform=$TARGETOS/$TARGETARCH base AS composer + +WORKDIR /build + +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer + +# Copy bare minimum to install Composer dependencies +COPY composer.json composer.lock ./ + +RUN composer install --no-dev --no-interaction --no-autoloader --no-scripts + +# ================================ +# Stage 1-2: Yarn Install +# ================================ +FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn + +WORKDIR /build + +# Copy bare minimum to install Yarn dependencies +COPY package.json yarn.lock ./ + +RUN yarn config set network-timeout 300000 \ + && yarn install --frozen-lockfile + +# ================================ +# Stage 2-1: Composer Optimize +# ================================ +FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild + +# Copy full code to optimize autoload +COPY --exclude=Caddyfile --exclude=docker/ . ./ + +RUN composer dump-autoload --optimize + +# ================================ +# Stage 2-2: Build Frontend Assets +# ================================ +FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild + +WORKDIR /build + +# Copy full code +COPY --exclude=Caddyfile --exclude=docker/ . ./ +COPY --from=composer /build . + +RUN yarn run build + +# ================================ +# Stage 5: Build Final Application Image +# ================================ +FROM --platform=$TARGETOS/$TARGETARCH base AS final + +WORKDIR /var/www/html + +# Install additional required libraries +RUN apk update && apk add --no-cache \ + caddy ca-certificates supervisor supercronic + +COPY --chown=root:www-data --chmod=640 --from=composerbuild /build . +COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public + +# Set permissions +# First ensure all files are owned by root and restrict www-data to read access +RUN chown root:www-data ./ \ + && chmod 750 ./ \ + # Files should not have execute set, but directories need it + && find ./ -type d -exec chmod 750 {} \; \ + # Create necessary directories + && mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \ + # Symlinks for env, database, and avatars + && ln -s /pelican-data/.env ./.env \ + && ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \ + && ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \ + && ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \ + # Allow www-data write permissions where necessary + && chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \ + && chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord + +# Configure Supervisor +COPY docker/supervisord.conf /etc/supervisord.conf +COPY docker/Caddyfile /etc/caddy/Caddyfile +# Add Laravel scheduler to crontab +COPY docker/crontab /etc/supercronic/crontab + +COPY docker/entrypoint.sh ./docker/entrypoint.sh + +HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost/up || exit 1 + +EXPOSE 80 443 + +VOLUME /pelican-data + +USER www-data + +ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ] +CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index c97577bf3..0dd8d2e29 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -23,7 +23,7 @@ else echo -e "APP_INSTALLED=false" >> /pelican-data/.env fi -mkdir /pelican-data/database /var/www/html/storage/logs/supervisord 2>/dev/null +mkdir -p /pelican-data/database /pelican-data/storage/avatars /var/www/html/storage/logs/supervisord 2>/dev/null if ! grep -q "APP_KEY=" .env || grep -q "APP_KEY=$" .env; then echo "Generating APP_KEY..." From 2046fa453ac36642562fbcb27648e8d414ce2252 Mon Sep 17 00:00:00 2001 From: pelican-vehikl Date: Mon, 28 Apr 2025 10:20:33 -0400 Subject: [PATCH 6/8] Pest Test Improvements (#1137) Co-authored-by: Lance Pioch Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com> --- .gitignore | 3 +- app/Enums/RolePermissionModels.php | 20 ++ .../Api/Client/Servers/SettingsController.php | 14 +- app/Models/Permission.php | 3 +- app/Models/Role.php | 3 + composer.json | 2 + composer.lock | 135 ++++++++++++- database/Factories/PermissionFactory.php | 18 ++ database/Factories/RoleFactory.php | 22 +++ tests/Feature/SettingsControllerTest.php | 179 ++++++++++++++++++ tests/Filament/Admin/ListEggsTest.php | 49 +++++ tests/Filament/Admin/ListNodesTest.php | 67 +++++++ tests/Pest.php | 132 +++++++++++++ 13 files changed, 633 insertions(+), 14 deletions(-) create mode 100644 database/Factories/PermissionFactory.php create mode 100644 database/Factories/RoleFactory.php create mode 100644 tests/Feature/SettingsControllerTest.php create mode 100644 tests/Filament/Admin/ListEggsTest.php create mode 100644 tests/Filament/Admin/ListNodesTest.php diff --git a/.gitignore b/.gitignore index 91984a1ab..e0865a5d4 100644 --- a/.gitignore +++ b/.gitignore @@ -24,8 +24,7 @@ yarn-error.log /.vscode public/assets/manifest.json -/database/*.sqlite -/database/*.sqlite-journal +/database/*.sqlite* filament-monaco-editor/ _ide_helper* /.phpstorm.meta.php diff --git a/app/Enums/RolePermissionModels.php b/app/Enums/RolePermissionModels.php index 069910957..013d5b857 100644 --- a/app/Enums/RolePermissionModels.php +++ b/app/Enums/RolePermissionModels.php @@ -14,4 +14,24 @@ enum RolePermissionModels: string case Server = 'server'; case User = 'user'; case Webhook = 'webhook'; + + public function viewAny(): string + { + return RolePermissionPrefixes::ViewAny->value . ' ' . $this->value; + } + + public function view(): string + { + return RolePermissionPrefixes::View->value . ' ' . $this->value; + } + + public function create(): string + { + return RolePermissionPrefixes::Create->value . ' ' . $this->value; + } + + public function update(): string + { + return RolePermissionPrefixes::Update->value . ' ' . $this->value; + } } diff --git a/app/Http/Controllers/Api/Client/Servers/SettingsController.php b/app/Http/Controllers/Api/Client/Servers/SettingsController.php index ddb512b9f..169f825b5 100644 --- a/app/Http/Controllers/Api/Client/Servers/SettingsController.php +++ b/app/Http/Controllers/Api/Client/Servers/SettingsController.php @@ -36,26 +36,22 @@ class SettingsController extends ClientApiController $name = $request->input('name'); $description = $request->has('description') ? (string) $request->input('description') : $server->description; - $server->name = $name; - - if (config('panel.editable_server_descriptions')) { - $server->description = $description; - } - - $server->save(); - if ($server->name !== $name) { Activity::event('server:settings.rename') ->property(['old' => $server->name, 'new' => $name]) ->log(); + $server->name = $name; } - if ($server->description !== $description) { + if ($server->description !== $description && config('panel.editable_server_descriptions')) { Activity::event('server:settings.description') ->property(['old' => $server->description, 'new' => $description]) ->log(); + $server->description = $description; } + $server->save(); + return new JsonResponse([], Response::HTTP_NO_CONTENT); } diff --git a/app/Models/Permission.php b/app/Models/Permission.php index 02b3870b6..62d36e2c4 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -4,12 +4,13 @@ namespace App\Models; use App\Contracts\Validatable; use App\Traits\HasValidation; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; class Permission extends Model implements Validatable { - use HasValidation; + use HasFactory, HasValidation; /** * The resource name for this model when it is transformed into an diff --git a/app/Models/Role.php b/app/Models/Role.php index be4a93b40..5f48c62a0 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -4,6 +4,7 @@ namespace App\Models; use App\Enums\RolePermissionModels; use App\Enums\RolePermissionPrefixes; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Spatie\Permission\Models\Role as BaseRole; /** @@ -17,6 +18,8 @@ use Spatie\Permission\Models\Role as BaseRole; */ class Role extends BaseRole { + use HasFactory; + public const RESOURCE_NAME = 'role'; public const ROOT_ADMIN = 'Root Admin'; diff --git a/composer.json b/composer.json index d43766a54..b89dcccbc 100644 --- a/composer.json +++ b/composer.json @@ -56,6 +56,8 @@ "mockery/mockery": "^1.6.11", "nunomaduro/collision": "^8.6", "pestphp/pest": "^3.7", + "pestphp/pest-plugin-faker": "^3.0", + "pestphp/pest-plugin-livewire": "^3.0", "spatie/laravel-ignition": "^2.9" }, "autoload": { diff --git a/composer.lock b/composer.lock index 03bc7f6f3..dcd3cba4c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e78193e058fd9f763da97bbc11934c2d", + "content-hash": "556cda6914cb34938f738f77ab4b6949", "packages": [ { "name": "abdelhamiderrahmouni/filament-monaco-editor", @@ -13616,6 +13616,137 @@ ], "time": "2025-04-16T22:59:48+00:00" }, + { + "name": "pestphp/pest-plugin-faker", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-faker.git", + "reference": "48343e2806cfc12a042dead90ffff4a043167e3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-faker/zipball/48343e2806cfc12a042dead90ffff4a043167e3e", + "reference": "48343e2806cfc12a042dead90ffff4a043167e3e", + "shasum": "" + }, + "require": { + "fakerphp/faker": "^1.23.1", + "pestphp/pest": "^3.0.0", + "php": "^8.2" + }, + "require-dev": { + "pestphp/pest-dev-tools": "^3.0.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Faker.php" + ], + "psr-4": { + "Pest\\Faker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Faker Plugin", + "keywords": [ + "faker", + "framework", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-faker/tree/v3.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2024-09-08T23:56:08+00:00" + }, + { + "name": "pestphp/pest-plugin-livewire", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-livewire.git", + "reference": "e2f2edb0a7d414d6837d87908a0e148256d3bf89" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-livewire/zipball/e2f2edb0a7d414d6837d87908a0e148256d3bf89", + "reference": "e2f2edb0a7d414d6837d87908a0e148256d3bf89", + "shasum": "" + }, + "require": { + "livewire/livewire": "^3.5.6", + "pestphp/pest": "^3.0.0", + "php": "^8.1" + }, + "require-dev": { + "orchestra/testbench": "^9.4.0", + "pestphp/pest-dev-tools": "^3.0.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Livewire\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Livewire Plugin", + "keywords": [ + "framework", + "livewire", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-livewire/tree/v3.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2024-09-09T00:05:59+00:00" + }, { "name": "pestphp/pest-plugin-mutate", "version": "v3.0.5", @@ -15775,5 +15906,5 @@ "platform-overrides": { "php": "8.2" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/database/Factories/PermissionFactory.php b/database/Factories/PermissionFactory.php new file mode 100644 index 000000000..7b73fac68 --- /dev/null +++ b/database/Factories/PermissionFactory.php @@ -0,0 +1,18 @@ + $this->faker->name(), + 'guard_name' => $this->faker->name(), + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ]; + } +} diff --git a/tests/Feature/SettingsControllerTest.php b/tests/Feature/SettingsControllerTest.php new file mode 100644 index 000000000..c1850489e --- /dev/null +++ b/tests/Feature/SettingsControllerTest.php @@ -0,0 +1,179 @@ +group('API'); + +covers(SettingsController::class); + +it('server name cannot be changed', function () { + [$user, $server] = generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]); + $originalName = $server->name; + + $this->actingAs($user) + ->post("/api/client/servers/$server->uuid/settings/rename", [ + 'name' => 'Test Server Name', + ]) + ->assertStatus(Response::HTTP_FORBIDDEN); + + $server = $server->refresh(); + expect()->toLogActivities(0) + ->and($server->name)->toBe($originalName); +}); + +it('server description can be changed', function () { + [$user, $server] = generateTestAccount([Permission::ACTION_SETTINGS_DESCRIPTION]); + $originalDescription = $server->description; + + $newDescription = 'Test Server Description'; + $this->actingAs($user) + ->post("/api/client/servers/$server->uuid/settings/description", [ + 'description' => $newDescription, + ]) + ->assertStatus(Response::HTTP_NO_CONTENT); + + $server = $server->refresh(); + $logged = \App\Models\ActivityLog::first(); + expect()->toLogActivities(1) + ->and($logged->properties['old'])->toBe($originalDescription) + ->and($logged->properties['new'])->toBe($newDescription) + ->and($server->description)->not()->toBe($originalDescription); +}); + +it('server description cannot be changed', function () { + [$user, $server] = generateTestAccount([Permission::ACTION_SETTINGS_DESCRIPTION]); + Config::set('panel.editable_server_descriptions', false); + $originalDescription = $server->description; + + $this->actingAs($user) + ->post("/api/client/servers/$server->uuid/settings/description", [ + 'description' => 'Test Description', + ]) + ->assertStatus(Response::HTTP_NO_CONTENT); + + $server = $server->refresh(); + expect()->toLogActivities(0) + ->and($server->description)->toBe($originalDescription); +}); + +it('server name can be changed', function () { + [$user, $server] = generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT, Permission::ACTION_SETTINGS_RENAME]); + $originalName = $server->name; + + $this->actingAs($user) + ->post("/api/client/servers/$server->uuid/settings/rename", [ + 'name' => 'Test Server Name', + ]) + ->assertStatus(Response::HTTP_NO_CONTENT); + + $server = $server->refresh(); + expect()->toLogActivities(1) + ->and($server->name)->not()->toBe($originalName); +}); + +test('unauthorized user cannot change docker image in use by server', function () { + [$user, $server] = generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]); + $originalImage = $server->image; + + $this->actingAs($user) + ->put("/api/client/servers/$server->uuid/settings/docker-image", [ + 'docker_image' => 'ghcr.io/pelican-dev/yolks:java_21', + ]) + ->assertStatus(Response::HTTP_FORBIDDEN); + + $server = $server->refresh(); + expect()->toLogActivities(0) + ->and($server->image)->toBe($originalImage); +}); + +test('cannot change docker image to image not allowed by egg', function () { + + [$user, $server] = generateTestAccount([Permission::ACTION_STARTUP_DOCKER_IMAGE]); + $server->image = 'ghcr.io/parkervcp/yolks:java_17'; + $server->save(); + + $newImage = 'ghcr.io/parkervcp/fake:image'; + + $server = $server->refresh(); + + $this->actingAs($user) + ->putJson("/api/client/servers/$server->uuid/settings/docker-image", [ + 'docker_image' => $newImage, + ]) + ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + + $server->refresh(); + expect()->toLogActivities(0) + ->and($server->image)->not()->toBe($newImage); +}); + +test('can change docker image in use by server', function () { + [$user, $server] = generateTestAccount([Permission::ACTION_STARTUP_DOCKER_IMAGE]); + $oldImage = 'ghcr.io/parkervcp/yolks:java_17'; + $server->image = $oldImage; + $server->save(); + + $newImage = 'ghcr.io/parkervcp/yolks:java_21'; + + $this->actingAs($user) + ->putJson("/api/client/servers/$server->uuid/settings/docker-image", [ + 'docker_image' => $newImage, + ]) + ->assertStatus(Response::HTTP_NO_CONTENT); + + $server = $server->refresh(); + + $logItem = \App\Models\ActivityLog::first(); + expect()->toLogActivities(1) + ->and($logItem->properties['old'])->toBe($oldImage) + ->and($logItem->properties['new'])->toBe($newImage) + ->and($server->image)->toBe($newImage); +}); + +test('unable to change the docker image set by administrator', function () { + [$user, $server] = generateTestAccount([Permission::ACTION_STARTUP_DOCKER_IMAGE]); + $oldImage = 'ghcr.io/parkervcp/yolks:java_custom'; + $server->image = $oldImage; + $server->save(); + + $newImage = 'ghcr.io/parkervcp/yolks:java_8'; + + $this->actingAs($user) + ->putJson("/api/client/servers/$server->uuid/settings/docker-image", [ + 'docker_image' => $newImage, + ]) + ->assertStatus(Response::HTTP_BAD_REQUEST); + + $server = $server->refresh(); + + expect()->toLogActivities(0) + ->and($server->image)->toBe($oldImage); +}); + +test('can be reinstalled', function () { + [$user, $server] = generateTestAccount([Permission::ACTION_SETTINGS_REINSTALL]); + expect($server->isInstalled())->toBeTrue(); + + $service = \Mockery::mock(DaemonServerRepository::class); + $this->app->instance(DaemonServerRepository::class, $service); + + $service->expects('setServer') + ->with(\Mockery::on(function ($value) use ($server) { + return $value->uuid === $server->uuid; + })) + ->andReturnSelf() + ->getMock() + ->expects('reinstall') + ->andReturnUndefined(); + + $this->actingAs($user)->postJson("/api/client/servers/$server->uuid/settings/reinstall") + ->assertStatus(Response::HTTP_ACCEPTED); + + $server = $server->refresh(); + expect()->toLogActivities(1) + ->and($server->status)->toBe(ServerState::Installing); +}); diff --git a/tests/Filament/Admin/ListEggsTest.php b/tests/Filament/Admin/ListEggsTest.php new file mode 100644 index 000000000..0e34b22fa --- /dev/null +++ b/tests/Filament/Admin/ListEggsTest.php @@ -0,0 +1,49 @@ +syncRoles(Role::getRootAdmin()); + + $this->actingAs($admin); + livewire(ListEggs::class) + ->assertSuccessful() + ->assertCountTableRecords($eggs->count()) + ->assertCanSeeTableRecords($eggs); +}); + +it('non root admin cannot see any eggs', function () { + $role = Role::factory()->create(['name' => 'Node Viewer', 'guard_name' => 'web']); + // Node Permission is on purpose, we check the wrong permissions. + $permission = Permission::factory()->create(['name' => RolePermissionModels::Node->viewAny(), 'guard_name' => 'web']); + $role->permissions()->attach($permission); + [$user] = generateTestAccount([]); + + $this->actingAs($user); + livewire(ListEggs::class) + ->assertForbidden(); +}); + +it('non root admin with permissions can see eggs', function () { + $role = Role::factory()->create(['name' => 'Egg Viewer', 'guard_name' => 'web']); + $permission = Permission::factory()->create(['name' => RolePermissionModels::Egg->viewAny(), 'guard_name' => 'web']); + $role->permissions()->attach($permission); + + $eggs = Egg::all(); + [$user] = generateTestAccount([]); + $user = $user->syncRoles($role); + + $this->actingAs($user); + livewire(ListEggs::class) + ->assertSuccessful() + ->assertCountTableRecords($eggs->count()) + ->assertCanSeeTableRecords($eggs); +}); diff --git a/tests/Filament/Admin/ListNodesTest.php b/tests/Filament/Admin/ListNodesTest.php new file mode 100644 index 000000000..9f7115556 --- /dev/null +++ b/tests/Filament/Admin/ListNodesTest.php @@ -0,0 +1,67 @@ +syncRoles(Role::getRootAdmin()); + $nodes = Node::all(); + + $this->actingAs($admin); + livewire(ListNodes::class) + ->assertSuccessful() + ->assertCountTableRecords($nodes->count()) + ->assertCanSeeTableRecords($nodes); +}); + +it('non root admin cannot see any nodes', function () { + $role = Role::factory()->create(['name' => 'Egg Viewer', 'guard_name' => 'web']); + // Egg Permission is on purpose, we check the wrong permissions. + $permission = Permission::factory()->create(['name' => RolePermissionModels::Egg->viewAny(), 'guard_name' => 'web']); + $role->permissions()->attach($permission); + [$user] = generateTestAccount(); + + $this->actingAs($user); + livewire(ListNodes::class) + ->assertForbidden(); +}); + +it('non root admin with permissions can see nodes', function () { + $role = Role::factory()->create(['name' => 'Node Viewer', 'guard_name' => 'web']); + $permission = Permission::factory()->create(['name' => RolePermissionModels::Node->viewAny(), 'guard_name' => 'web']); + $role->permissions()->attach($permission); + + [$user] = generateTestAccount(); + $nodes = Node::all(); + $user = $user->syncRoles($role); + + $this->actingAs($user); + livewire(ListNodes::class) + ->assertSuccessful() + ->assertCountTableRecords($nodes->count()) + ->assertCanSeeTableRecords($nodes); +}); + +it('displays the create button in the table instead of the header when 0 nodes', function () { + [$admin] = generateTestAccount([]); + $admin = $admin->syncRoles(Role::getRootAdmin()); + + // Nuke servers & nodes + Server::truncate(); + Node::truncate(); + + $this->actingAs($admin); + livewire(ListNodes::class) + ->assertSuccessful() + ->assertHeaderMissing(CreateAction::class) + ->assertActionExists(TableCreateAction::class); +}); diff --git a/tests/Pest.php b/tests/Pest.php index fd279ada6..c396b3bb7 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -24,10 +24,26 @@ | */ +use App\Models\ActivityLog; +use App\Models\Allocation; +use App\Models\Egg; +use App\Models\Node; +use App\Models\Server; +use App\Models\Subuser; +use App\Models\User; +use App\Tests\Integration\IntegrationTestCase; +use Ramsey\Uuid\Uuid; + expect()->extend('toBeOne', function () { return $this->toBe(1); }); +expect()->extend('toLogActivities', function (int $times) { + expect(ActivityLog::count())->toBe($times); +}); + +uses(IntegrationTestCase::class)->in('Feature', 'Filament'); + /* |-------------------------------------------------------------------------- | Functions @@ -43,3 +59,119 @@ function something() { // .. } + +/** + * Generates a user and a server for that user. If an array of permissions is passed it + * is assumed that the user is actually a subuser of the server. + * + * @param string[] $permissions + * @return array{\App\Models\User, \App\Models\Server} + */ + +/** + * Creates a server model in the databases for the purpose of testing. If an attribute + * is passed in that normally requires this function to create a model no model will be + * created and that attribute's value will be used. + * + * The returned server model will have all the relationships loaded onto it. + */ +function createServerModel(array $attributes = []): Server +{ + if (isset($attributes['user_id'])) { + $attributes['owner_id'] = $attributes['user_id']; + } + + if (!isset($attributes['owner_id'])) { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + $attributes['owner_id'] = $user->id; + } + + if (!isset($attributes['node_id'])) { + /** @var \App\Models\Node $node */ + $node = Node::factory()->create(); + $attributes['node_id'] = $node->id; + } + + if (!isset($attributes['allocation_id'])) { + /** @var \App\Models\Allocation $allocation */ + $allocation = Allocation::factory()->create(['node_id' => $attributes['node_id']]); + $attributes['allocation_id'] = $allocation->id; + } + + if (empty($attributes['egg_id'])) { + $egg = getBungeecordEgg(); + + $attributes['egg_id'] = $egg->id; + } + + unset($attributes['user_id']); + + /** @var \App\Models\Server $server */ + $server = Server::factory()->create($attributes); + + Allocation::query()->where('id', $server->allocation_id)->update(['server_id' => $server->id]); + + return $server->fresh([ + 'user', 'node', 'allocation', 'egg', + ]); +} + +/** + * Generates a user and a server for that user. If an array of permissions is passed it + * is assumed that the user is actually a subuser of the server. + * + * @param string[] $permissions + * @return array{\App\Models\User, \App\Models\Server} + */ +function generateTestAccount(array $permissions = []): array +{ + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + if (empty($permissions)) { + return [$user, createServerModel(['user_id' => $user->id])]; + } + + $server = createServerModel(); + + Subuser::query()->create([ + 'user_id' => $user->id, + 'server_id' => $server->id, + 'permissions' => $permissions, + ]); + + return [$user, $server]; +} + +/** + * Clones a given egg allowing us to make modifications that don't affect other + * tests that rely on the egg existing in the correct state. + */ +function cloneEggAndVariables(Egg $egg): Egg +{ + $model = $egg->replicate(['id', 'uuid']); + $model->uuid = Uuid::uuid4()->toString(); + $model->push(); + + /** @var \App\Models\Egg $model */ + $model = $model->fresh(); + + foreach ($egg->variables as $variable) { + $variable->replicate(['id', 'egg_id'])->forceFill(['egg_id' => $model->id])->push(); + } + + return $model->fresh(); +} + +/** + * Almost every test just assumes it is using BungeeCord — this is the critical + * egg model for all tests unless specified otherwise. + */ +function getBungeecordEgg(): Egg +{ + /** @var \App\Models\Egg $egg */ + $egg = Egg::query()->where('author', 'panel@example.com')->where('name', 'Bungeecord')->firstOrFail(); + + return $egg; +} From 92c23451af2318a75feb992e0e2d075c3bd03497 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 29 Apr 2025 17:05:29 +0200 Subject: [PATCH 7/8] Improve file error handling (#1314) * improve file error handling * small cleanup * fix typo --- .../FileResource/Pages/EditFiles.php | 21 ++- .../FileResource/Pages/ListFiles.php | 160 +++++++----------- app/Models/File.php | 20 +-- 3 files changed, 83 insertions(+), 118 deletions(-) diff --git a/app/Filament/Server/Resources/FileResource/Pages/EditFiles.php b/app/Filament/Server/Resources/FileResource/Pages/EditFiles.php index 3d0ee258d..8795bee82 100644 --- a/app/Filament/Server/Resources/FileResource/Pages/EditFiles.php +++ b/app/Filament/Server/Resources/FileResource/Pages/EditFiles.php @@ -26,6 +26,7 @@ use Filament\Resources\Pages\Page; use Filament\Resources\Pages\PageRegistration; use Filament\Support\Enums\Alignment; use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Http\Client\ConnectionException; use Illuminate\Routing\Route; use Illuminate\Support\Facades\Route as RouteFacade; use Livewire\Attributes\Locked; @@ -128,31 +129,33 @@ class EditFiles extends Page return $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size')); } catch (FileSizeTooLargeException) { AlertBanner::make() - ->title('File too large!') - ->body('' . $this->path . ' Max is ' . convert_bytes_to_readable(config('panel.files.max_edit_size'))) + ->title('' . basename($this->path) . ' is too large!') + ->body('Max is ' . convert_bytes_to_readable(config('panel.files.max_edit_size'))) ->danger() ->closable() ->send(); - $this->redirect(ListFiles::getUrl()); + $this->redirect(ListFiles::getUrl(['path' => dirname($this->path)])); } catch (FileNotFoundException) { AlertBanner::make() - ->title('File Not found!') - ->body('' . $this->path . '') + ->title('' . basename($this->path) . ' not found!') ->danger() ->closable() ->send(); - $this->redirect(ListFiles::getUrl()); + $this->redirect(ListFiles::getUrl(['path' => dirname($this->path)])); } catch (FileNotEditableException) { AlertBanner::make() - ->title('Could not edit directory!') - ->body('' . $this->path . '') + ->title('' . basename($this->path) . ' is a directory') ->danger() ->closable() ->send(); - $this->redirect(ListFiles::getUrl()); + $this->redirect(ListFiles::getUrl(['path' => dirname($this->path)])); + } catch (ConnectionException) { + // Alert banner for this one will be handled by ListFiles + + $this->redirect(ListFiles::getUrl(['path' => dirname($this->path)])); } }) ->language(fn (Get $get) => $get('lang')) diff --git a/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php b/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php index fd709812f..5992f31a7 100644 --- a/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php +++ b/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php @@ -12,7 +12,6 @@ use App\Models\Server; use App\Repositories\Daemon\DaemonFileRepository; use App\Filament\Components\Tables\Columns\BytesColumn; use App\Filament\Components\Tables\Columns\DateTimeColumn; -use App\Livewire\AlertBanner; use Filament\Actions\Action as HeaderAction; use Filament\Facades\Filament; use Filament\Forms\Components\CheckboxList; @@ -30,14 +29,12 @@ use Filament\Resources\Pages\PageRegistration; use Filament\Tables\Actions\Action; use Filament\Tables\Actions\ActionGroup; use Filament\Tables\Actions\BulkAction; -use Filament\Tables\Actions\BulkActionGroup; use Filament\Tables\Actions\DeleteAction; use Filament\Tables\Actions\DeleteBulkAction; use Filament\Tables\Actions\EditAction; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Collection; -use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\UploadedFile; use Illuminate\Routing\Route; use Illuminate\Support\Carbon; @@ -53,24 +50,11 @@ class ListFiles extends ListRecords private DaemonFileRepository $fileRepository; - private bool $isDisabled = false; - public function mount(?string $path = null): void { parent::mount(); $this->path = $path ?? '/'; - - try { - $this->getDaemonFileRepository()->getDirectory('/'); - } catch (ConnectionException) { - $this->isDisabled = true; - - AlertBanner::make('node_connection_error') - ->title('Could not connect to the node!') - ->danger() - ->send(); - } } public function getBreadcrumbs(): array @@ -130,21 +114,18 @@ class ListFiles extends ListRecords ->actions([ Action::make('view') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server)) - ->disabled($this->isDisabled) ->label('Open') ->icon('tabler-eye') ->visible(fn (File $file) => $file->is_directory) ->url(fn (File $file) => self::getUrl(['path' => join_paths($this->path, $file->name)])), EditAction::make('edit') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server)) - ->disabled($this->isDisabled) ->icon('tabler-edit') ->visible(fn (File $file) => $file->canEdit()) ->url(fn (File $file) => EditFiles::getUrl(['path' => join_paths($this->path, $file->name)])), ActionGroup::make([ Action::make('rename') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server)) - ->disabled($this->isDisabled) ->label('Rename') ->icon('tabler-forms') ->form([ @@ -173,7 +154,6 @@ class ListFiles extends ListRecords }), Action::make('copy') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server)) - ->disabled($this->isDisabled) ->label('Copy') ->icon('tabler-copy') ->visible(fn (File $file) => $file->is_file) @@ -193,14 +173,12 @@ class ListFiles extends ListRecords }), Action::make('download') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server)) - ->disabled($this->isDisabled) ->label('Download') ->icon('tabler-download') ->visible(fn (File $file) => $file->is_file) ->url(fn (File $file) => DownloadFiles::getUrl(['path' => join_paths($this->path, $file->name)]), true), Action::make('move') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server)) - ->disabled($this->isDisabled) ->label('Move') ->icon('tabler-replace') ->form([ @@ -236,7 +214,6 @@ class ListFiles extends ListRecords }), Action::make('permissions') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server)) - ->disabled($this->isDisabled) ->label('Permissions') ->icon('tabler-license') ->form([ @@ -293,7 +270,6 @@ class ListFiles extends ListRecords }), Action::make('archive') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server)) - ->disabled($this->isDisabled) ->label('Archive') ->icon('tabler-archive') ->form([ @@ -321,7 +297,6 @@ class ListFiles extends ListRecords }), Action::make('unarchive') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server)) - ->disabled($this->isDisabled) ->label('Unarchive') ->icon('tabler-archive') ->visible(fn (File $file) => $file->isArchive()) @@ -343,7 +318,6 @@ class ListFiles extends ListRecords ]), DeleteAction::make() ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server)) - ->disabled($this->isDisabled) ->label('') ->icon('tabler-trash') ->requiresConfirmation() @@ -358,83 +332,77 @@ class ListFiles extends ListRecords ->log(); }), ]) - ->bulkActions([ - BulkActionGroup::make([ - BulkAction::make('move') - ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server)) - ->disabled($this->isDisabled) - ->form([ - TextInput::make('location') - ->label('Directory') - ->hint('Enter the new directory, relative to the current directory.') - ->required() - ->live(), - Placeholder::make('new_location') - ->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))), - ]) - ->action(function (Collection $files, $data) { - $location = rtrim($data['location'], '/'); + ->groupedBulkActions([ + BulkAction::make('move') + ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server)) + ->form([ + TextInput::make('location') + ->label('Directory') + ->hint('Enter the new directory, relative to the current directory.') + ->required() + ->live(), + Placeholder::make('new_location') + ->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))), + ]) + ->action(function (Collection $files, $data) { + $location = rtrim($data['location'], '/'); - $files = $files->map(fn ($file) => ['to' => join_paths($location, $file['name']), 'from' => $file['name']])->toArray(); - $this->getDaemonFileRepository() - ->renameFiles($this->path, $files); + $files = $files->map(fn ($file) => ['to' => join_paths($location, $file['name']), 'from' => $file['name']])->toArray(); + $this->getDaemonFileRepository()->renameFiles($this->path, $files); - Activity::event('server:file.rename') - ->property('directory', $this->path) - ->property('files', $files) - ->log(); + Activity::event('server:file.rename') + ->property('directory', $this->path) + ->property('files', $files) + ->log(); - Notification::make() - ->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location))) - ->success() - ->send(); - }), - BulkAction::make('archive') - ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server)) - ->disabled($this->isDisabled) - ->form([ - TextInput::make('name') - ->label('Archive name') - ->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z') - ->suffix('.tar.gz'), - ]) - ->action(function ($data, Collection $files) { - $files = $files->map(fn ($file) => $file['name'])->toArray(); + Notification::make() + ->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location))) + ->success() + ->send(); + }), + BulkAction::make('archive') + ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server)) + ->form([ + TextInput::make('name') + ->label('Archive name') + ->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z') + ->suffix('.tar.gz'), + ]) + ->action(function ($data, Collection $files) { + $files = $files->map(fn ($file) => $file['name'])->toArray(); - $archive = $this->getDaemonFileRepository()->compressFiles($this->path, $files, $data['name']); + $archive = $this->getDaemonFileRepository()->compressFiles($this->path, $files, $data['name']); - Activity::event('server:file.compress') - ->property('name', $archive['name']) - ->property('directory', $this->path) - ->property('files', $files) - ->log(); + Activity::event('server:file.compress') + ->property('name', $archive['name']) + ->property('directory', $this->path) + ->property('files', $files) + ->log(); - Notification::make() - ->title('Archive created') - ->body($archive['name']) - ->success() - ->send(); + Notification::make() + ->title('Archive created') + ->body($archive['name']) + ->success() + ->send(); - return redirect(ListFiles::getUrl(['path' => $this->path])); - }), - DeleteBulkAction::make() - ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server)) - ->disabled($this->isDisabled) - ->action(function (Collection $files) { - $files = $files->map(fn ($file) => $file['name'])->toArray(); - $this->getDaemonFileRepository()->deleteFiles($this->path, $files); + return redirect(ListFiles::getUrl(['path' => $this->path])); + }), + DeleteBulkAction::make() + ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server)) + ->action(function (Collection $files) { + $files = $files->map(fn ($file) => $file['name'])->toArray(); + $this->getDaemonFileRepository()->deleteFiles($this->path, $files); - Activity::event('server:file.delete') - ->property('directory', $this->path) - ->property('files', $files) - ->log(); + Activity::event('server:file.delete') + ->property('directory', $this->path) + ->property('files', $files) + ->log(); - Notification::make() - ->title(count($files) . ' Files deleted.') - ->success() - ->send(); - }), - ]), + Notification::make() + ->title(count($files) . ' Files deleted.') + ->success() + ->send(); + }), ]); } @@ -446,7 +414,6 @@ class ListFiles extends ListRecords return [ HeaderAction::make('new_file') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server)) - ->disabled($this->isDisabled) ->label('New File') ->color('gray') ->keyBindings('') @@ -478,7 +445,6 @@ class ListFiles extends ListRecords ]), HeaderAction::make('new_folder') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server)) - ->disabled($this->isDisabled) ->label('New Folder') ->color('gray') ->action(function ($data) { @@ -495,7 +461,6 @@ class ListFiles extends ListRecords ]), HeaderAction::make('upload') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server)) - ->disabled($this->isDisabled) ->label('Upload') ->action(function ($data) { if (count($data['files']) > 0 && !isset($data['url'])) { @@ -545,7 +510,6 @@ class ListFiles extends ListRecords ]), HeaderAction::make('search') ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server)) - ->disabled($this->isDisabled) ->label('Global Search') ->modalSubmitActionLabel('Search') ->form([ diff --git a/app/Models/File.php b/app/Models/File.php index 12479554b..1f4aa8b31 100644 --- a/app/Models/File.php +++ b/app/Models/File.php @@ -153,16 +153,10 @@ class File extends Model try { $fileRepository = (new DaemonFileRepository())->setServer(self::$server); - $contents = []; - - try { - if (!is_null(self::$searchTerm)) { - $contents = cache()->remember('file_search_' . self::$path . '_' . self::$searchTerm, now()->addMinute(), fn () => $fileRepository->search(self::$searchTerm, self::$path)); - } else { - $contents = $fileRepository->getDirectory(self::$path ?? '/'); - } - } catch (ConnectionException $exception) { - report($exception); + if (!is_null(self::$searchTerm)) { + $contents = cache()->remember('file_search_' . self::$path . '_' . self::$searchTerm, now()->addMinute(), fn () => $fileRepository->search(self::$searchTerm, self::$path)); + } else { + $contents = $fileRepository->getDirectory(self::$path ?? '/'); } if (isset($contents['error'])) { @@ -199,8 +193,12 @@ class File extends Model $message = $message->after('cURL error 7: ')->before(' after '); } + if ($exception instanceof ConnectionException) { + $message = str('Node connection failed'); + } + AlertBanner::make() - ->title('Could not load files') + ->title('Could not load files!') ->body($message->toString()) ->danger() ->send(); From 14d351103c58f84268b78a5e5a77c04c05381d49 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 29 Apr 2025 17:05:49 +0200 Subject: [PATCH 8/8] Fix database & user not being deleted (#1315) --- .../DatabaseResource/Pages/ListDatabases.php | 8 +------- .../Databases/DatabaseManagementService.php | 15 +++++++++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php b/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php index 30549c6d3..3c7eda0aa 100644 --- a/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php +++ b/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php @@ -2,7 +2,6 @@ namespace App\Filament\Server\Resources\DatabaseResource\Pages; -use App\Facades\Activity; use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction; use App\Filament\Components\Tables\Columns\DateTimeColumn; use App\Filament\Server\Resources\DatabaseResource; @@ -82,12 +81,7 @@ class ListDatabases extends ListRecords ViewAction::make() ->modalHeading(fn (Database $database) => 'Viewing ' . $database->database), DeleteAction::make() - ->after(function (Database $database) { - Activity::event('server:database.delete') - ->subject($database) - ->property('name', $database->database) - ->log(); - }), + ->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)), ]); } diff --git a/app/Services/Databases/DatabaseManagementService.php b/app/Services/Databases/DatabaseManagementService.php index 56a723c95..b82c06934 100644 --- a/app/Services/Databases/DatabaseManagementService.php +++ b/app/Services/Databases/DatabaseManagementService.php @@ -118,11 +118,18 @@ class DatabaseManagementService */ public function delete(Database $database): ?bool { - $database->dropDatabase($database->database); - $database->dropUser($database->username, $database->remote); - $database->flush(); + return $this->connection->transaction(function () use ($database) { + $database->dropDatabase($database->database); + $database->dropUser($database->username, $database->remote); + $database->flush(); - return $database->delete(); + Activity::event('server:database.delete') + ->subject($database) + ->property('name', $database->database) + ->log(); + + return $database->delete(); + }); } /**