From 7b34f7dff3ba0a340b4a8936d1f7b7ecea16c05a Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 6 Jan 2026 08:59:04 +0100 Subject: [PATCH 01/14] change "Github" to "GitHub" for consistency --- app/Extensions/OAuth/Schemas/GithubSchema.php | 4 ++-- contributing.md | 2 +- security.md | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/Extensions/OAuth/Schemas/GithubSchema.php b/app/Extensions/OAuth/Schemas/GithubSchema.php index c3dfc4894f..b7258ab4b0 100644 --- a/app/Extensions/OAuth/Schemas/GithubSchema.php +++ b/app/Extensions/OAuth/Schemas/GithubSchema.php @@ -18,11 +18,11 @@ public function getId(): string public function getSetupSteps(): array { return array_merge([ - Step::make('Register new Github OAuth App') + Step::make('Register new GitHub OAuth App') ->schema([ TextEntry::make('create_application') ->hiddenLabel() - ->state(new HtmlString(Blade::render('

Visit the Github Developer Dashboard, go to OAuth Apps and click on New OAuth App.

Enter an Application name (e.g. your panel name), set Homepage URL to your panel url and enter the below url as Authorization callback URL.

'))), + ->state(new HtmlString(Blade::render('

Visit the GitHub Developer Dashboard, go to OAuth Apps and click on New OAuth App.

Enter an Application name (e.g. your panel name), set Homepage URL to your panel url and enter the below url as Authorization callback URL.

'))), TextInput::make('_noenv_callback') ->label('Authorization callback URL') ->dehydrated() diff --git a/contributing.md b/contributing.md index 70cc654c71..627c04e4c4 100644 --- a/contributing.md +++ b/contributing.md @@ -8,7 +8,7 @@ To start contributing to Pelican Panel, you need to have a basic understanding o * [PHP](https://php.net) & [Laravel](https://laravel.com) * [Livewire](https://laravel-livewire.com) & [Filament](https://filamentphp.com) -* [Git](https://git-scm.com) & [Github](https://github.com) +* [Git](https://git-scm.com) & [GitHub](https://github.com) ## Dev Environment Setup diff --git a/security.md b/security.md index 093456c581..520e6e2d6a 100644 --- a/security.md +++ b/security.md @@ -8,8 +8,9 @@ While Pelican is in beta, we only provide security fixes for the most recent bet ## Reporting a Vulnerability Please report any vulnerabilities via _one_ of the following methods: -- [Create a security advisory on Github](https://github.com/pelican-dev/panel/security/advisories/new) -- Send an e-mail to team@pelican.dev + +- [Create a security advisory on GitHub](https://github.com/pelican-dev/panel/security/advisories/new) +- Send an e-mail to Include steps to reproduce, affected versions, impact, and a proof of concept if available. From 768023ea2138f05556d7f8b87778ef7e468abca1 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 6 Jan 2026 10:38:20 +0100 Subject: [PATCH 02/14] Disconnect SFTP/Websocket when a user is removed as a subuser Co-authored-by: DaneEveritt --- .../Daemon/DaemonServerRepository.php | 18 ++++++++++++++++++ .../Servers/DetailsModificationService.php | 2 +- .../Subusers/SubuserDeletionService.php | 2 +- app/Services/Subusers/SubuserUpdateService.php | 2 +- .../Server/Subuser/DeleteSubuserTest.php | 4 ++-- .../Subuser/SubuserAuthorizationTest.php | 2 +- 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/app/Repositories/Daemon/DaemonServerRepository.php b/app/Repositories/Daemon/DaemonServerRepository.php index e4abe7408e..6088acf130 100644 --- a/app/Repositories/Daemon/DaemonServerRepository.php +++ b/app/Repositories/Daemon/DaemonServerRepository.php @@ -133,6 +133,9 @@ public function cancelTransfer(): void * make it easier to revoke tokens on the fly. This ensures that the JTI key is formatted * correctly and avoids any costly mistakes in the codebase. * + * @deprecated + * @see self::deauthorize() + * * @throws ConnectionException */ public function revokeUserJTI(int $id): void @@ -143,6 +146,21 @@ public function revokeUserJTI(int $id): void ]); } + /** + * Deauthorizes a user (disconnects websockets and SFTP) on the Wings instance for the server. + * + * @throws ConnectionException + */ + public function deauthorize(string $user): void + { + $this->getHttpClient()->post('/api/deauthorize-user', [ + 'json' => [ + 'user' => $user, + 'servers' => [$this->server->uuid], + ], + ]); + } + public function getInstallLogs(): string { return $this->getHttpClient() diff --git a/app/Services/Servers/DetailsModificationService.php b/app/Services/Servers/DetailsModificationService.php index b3b789d604..1b10f165c5 100644 --- a/app/Services/Servers/DetailsModificationService.php +++ b/app/Services/Servers/DetailsModificationService.php @@ -48,7 +48,7 @@ public function handle(Server $server, array $data): Server // websockets. if ($server->owner_id !== $owner) { try { - $this->serverRepository->setServer($server)->revokeUserJTI($owner); + $this->serverRepository->setServer($server)->deauthorize($owner); } catch (ConnectionException) { // Do nothing. A failure here is not ideal, but it is likely to be caused by daemon // being offline, or in an entirely broken state. Remember, these tokens reset every diff --git a/app/Services/Subusers/SubuserDeletionService.php b/app/Services/Subusers/SubuserDeletionService.php index b68b948f86..1060f96cd1 100644 --- a/app/Services/Subusers/SubuserDeletionService.php +++ b/app/Services/Subusers/SubuserDeletionService.php @@ -28,7 +28,7 @@ public function handle(Subuser $subuser, Server $server): void event(new SubUserRemoved($subuser->server, $subuser->user)); try { - $this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id); + $this->serverRepository->setServer($server)->deauthorize($subuser->user_id); } catch (ConnectionException $exception) { // Don't block this request if we can't connect to the daemon instance. logger()->warning($exception, ['user_id' => $subuser->user_id, 'server_id' => $server->id]); diff --git a/app/Services/Subusers/SubuserUpdateService.php b/app/Services/Subusers/SubuserUpdateService.php index 14fd9decd2..1df3e25006 100644 --- a/app/Services/Subusers/SubuserUpdateService.php +++ b/app/Services/Subusers/SubuserUpdateService.php @@ -46,7 +46,7 @@ public function handle(Subuser $subuser, Server $server, array $permissions): vo $subuser->update(['permissions' => $cleanedPermissions]); try { - $this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id); + $this->serverRepository->setServer($server)->deauthorize($subuser->user_id); } catch (ConnectionException $exception) { // Don't block this request if we can't connect to the daemon instance. Chances are it is // offline and the token will be invalid once daemon boots back. diff --git a/tests/Integration/Api/Client/Server/Subuser/DeleteSubuserTest.php b/tests/Integration/Api/Client/Server/Subuser/DeleteSubuserTest.php index 63440a0845..5f54483774 100644 --- a/tests/Integration/Api/Client/Server/Subuser/DeleteSubuserTest.php +++ b/tests/Integration/Api/Client/Server/Subuser/DeleteSubuserTest.php @@ -42,7 +42,7 @@ public function test_correct_subuser_is_deleted_from_server(): void 'permissions' => [SubuserPermission::WebsocketConnect], ]); - $mock->expects('setServer->revokeUserJTI')->with($subuser->id)->andReturnUndefined(); + $mock->expects('setServer->deauthorize')->with($subuser->id)->andReturnUndefined(); $this->actingAs($user)->deleteJson($this->link($server) . "/users/$subuser->uuid")->assertNoContent(); @@ -58,7 +58,7 @@ public function test_correct_subuser_is_deleted_from_server(): void 'permissions' => [SubuserPermission::WebsocketConnect], ]); - $mock->expects('setServer->revokeUserJTI')->with($subuser->id)->andReturnUndefined(); + $mock->expects('setServer->deauthorize')->with($subuser->id)->andReturnUndefined(); $this->actingAs($user)->deleteJson($this->link($server) . "/users/$subuser->uuid")->assertNoContent(); } diff --git a/tests/Integration/Api/Client/Server/Subuser/SubuserAuthorizationTest.php b/tests/Integration/Api/Client/Server/Subuser/SubuserAuthorizationTest.php index 31370dcc70..3d8e17f9fc 100644 --- a/tests/Integration/Api/Client/Server/Subuser/SubuserAuthorizationTest.php +++ b/tests/Integration/Api/Client/Server/Subuser/SubuserAuthorizationTest.php @@ -37,7 +37,7 @@ public function test_user_cannot_access_resource_belonging_to_other_servers(stri $this->instance(DaemonServerRepository::class, $mock = \Mockery::mock(DaemonServerRepository::class)); if ($method === 'DELETE') { - $mock->expects('setServer->revokeUserJTI')->with($internal->id)->andReturnUndefined(); + $mock->expects('setServer->deauthorize')->with($internal->id)->andReturnUndefined(); } // This route is acceptable since they're accessing a subuser on their own server. From 34c86c7b2051723efd105731687b2ad8ee8723b4 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 6 Jan 2026 10:41:23 +0100 Subject: [PATCH 03/14] Clear allocation notes on server deletion --- app/Services/Servers/ServerDeletionService.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Services/Servers/ServerDeletionService.php b/app/Services/Servers/ServerDeletionService.php index 925694f271..7f59d017c5 100644 --- a/app/Services/Servers/ServerDeletionService.php +++ b/app/Services/Servers/ServerDeletionService.php @@ -78,7 +78,10 @@ public function handle(Server $server): void } } - $server->allocations()->update(['server_id' => null]); + $server->allocations()->update([ + 'server_id' => null, + 'notes' => null, + ]); $server->delete(); }); From bda9ddd92c236a6900bd9ee3c887ff435ddf4e33 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 6 Jan 2026 10:44:56 +0100 Subject: [PATCH 04/14] Don't be strict about upload size, so long as it is a positive integer --- app/Filament/Admin/Resources/Nodes/Pages/CreateNode.php | 1 - app/Filament/Admin/Resources/Nodes/Pages/EditNode.php | 4 ++-- app/Models/Node.php | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/Filament/Admin/Resources/Nodes/Pages/CreateNode.php b/app/Filament/Admin/Resources/Nodes/Pages/CreateNode.php index 5850bc8b8e..782b0f49a1 100644 --- a/app/Filament/Admin/Resources/Nodes/Pages/CreateNode.php +++ b/app/Filament/Admin/Resources/Nodes/Pages/CreateNode.php @@ -259,7 +259,6 @@ public function form(Schema $schema): Schema ->numeric()->required() ->default(256) ->minValue(1) - ->maxValue(1024) ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'), TextInput::make('daemon_sftp') ->columnSpan(1) diff --git a/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php b/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php index 20cd7036c6..a4880f3d53 100644 --- a/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php +++ b/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php @@ -320,9 +320,9 @@ public function form(Schema $schema): Schema ]) ->label(trans('admin/node.upload_limit')) ->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help.0') . trans('admin/node.upload_limit_help.1')) - ->numeric()->required() + ->numeric() + ->required() ->minValue(1) - ->maxValue(1024) ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'), TextInput::make('daemon_sftp') ->columnSpan([ diff --git a/app/Models/Node.php b/app/Models/Node.php index 69b1072d51..3b7625498d 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -81,7 +81,7 @@ class Node extends Model implements Validatable * Fields that are mass assignable. */ protected $fillable = [ - 'public', 'name', + 'public', 'name', 'description', 'fqdn', 'scheme', 'behind_proxy', 'memory', 'memory_overallocate', 'disk', 'disk_overallocate', 'cpu', 'cpu_overallocate', @@ -110,7 +110,7 @@ class Node extends Model implements Validatable 'daemon_listen' => ['required', 'numeric', 'between:1,65535'], 'daemon_connect' => ['required', 'numeric', 'between:1,65535'], 'maintenance_mode' => ['boolean'], - 'upload_size' => ['int', 'between:1,1024'], + 'upload_size' => ['int', 'min:1'], 'tags' => ['array'], ]; From 5fd2c4785f025c38c7a7ca32370f7dc3e6c2d38c Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 6 Jan 2026 11:00:15 +0100 Subject: [PATCH 05/14] Add throttling to resource creation endpoints Co-authored-by: DaneEveritt --- app/Enums/ResourceLimit.php | 65 ++++++++++++++++++++++++++ app/Providers/RouteServiceProvider.php | 3 ++ config/http.php | 2 +- routes/api-client.php | 23 ++++++--- 4 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 app/Enums/ResourceLimit.php diff --git a/app/Enums/ResourceLimit.php b/app/Enums/ResourceLimit.php new file mode 100644 index 0000000000..59707601a4 --- /dev/null +++ b/app/Enums/ResourceLimit.php @@ -0,0 +1,65 @@ +name}"; + } + + /** + * Returns a middleware that will throttle the specific resource by server. This + * throttle applies to any user making changes to that resource on the specific + * server, it is NOT per-user. + */ + public function middleware(): string + { + return ThrottleRequests::using($this->throttleKey()); + } + + public function limit(): Limit + { + return match ($this) { + self::Websocket => Limit::perMinute(5), + self::BackupRestore => Limit::perMinutes(15, 3), + self::DatabaseCreate => Limit::perMinute(2), + self::SubuserCreate => Limit::perMinutes(15, 10), + self::FilePull => Limit::perMinutes(10, 5), + default => Limit::perMinute(2), + }; + } + + public static function boot(): void + { + foreach (self::cases() as $case) { + RateLimiter::for($case->throttleKey(), function (Request $request) use ($case) { + Assert::isInstanceOf($server = $request->route()->parameter('server'), Server::class); + + return $case->limit()->by($server->uuid); + }); + } + } +} diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 311e3d0fc0..36c42d07cd 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use App\Enums\ResourceLimit; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Foundation\Http\Middleware\TrimStrings; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; @@ -98,5 +99,7 @@ protected function configureRateLimiting(): void config('http.rate_limit.application') )->by($key); }); + + ResourceLimit::boot(); } } diff --git a/config/http.php b/config/http.php index ed54e475b1..69bf4b5305 100644 --- a/config/http.php +++ b/config/http.php @@ -13,7 +13,7 @@ */ 'rate_limit' => [ 'client_period' => 1, - 'client' => env('APP_API_CLIENT_RATELIMIT', 720), + 'client' => env('APP_API_CLIENT_RATELIMIT', 120), 'application_period' => 1, 'application' => env('APP_API_APPLICATION_RATELIMIT', 240), diff --git a/routes/api-client.php b/routes/api-client.php index 12ab28af7d..4365cfeb22 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -1,5 +1,6 @@ middleware([ServerSubject::class, AuthenticateServerAccess::class, ResourceBelongsToServer::class])->group(function () { Route::get('/', [Client\Servers\ServerController::class, 'index'])->name('api:client:server.view'); - Route::get('/websocket', Client\Servers\WebsocketController::class)->name('api:client:server.ws'); + Route::middleware([ResourceLimit::Websocket->middleware()]) + ->get('/websocket', Client\Servers\WebsocketController::class) + ->name('api:client:server.ws'); Route::get('/resources', Client\Servers\ResourceUtilizationController::class)->name('api:client:server.resources'); Route::get('/activity', Client\Servers\ActivityLogController::class)->name('api:client:server.activity'); @@ -59,7 +62,8 @@ Route::prefix('/databases')->group(function () { Route::get('/', [Client\Servers\DatabaseController::class, 'index']); - Route::post('/', [Client\Servers\DatabaseController::class, 'store']); + Route::middleware([ResourceLimit::DatabaseCreate->middleware()]) + ->post('/', [Client\Servers\DatabaseController::class, 'store']); Route::post('/{database}/rotate-password', [Client\Servers\DatabaseController::class, 'rotatePassword']); Route::delete('/{database}', [Client\Servers\DatabaseController::class, 'delete']); }); @@ -76,13 +80,15 @@ Route::post('/delete', [Client\Servers\FileController::class, 'delete']); Route::post('/create-folder', [Client\Servers\FileController::class, 'create']); Route::post('/chmod', [Client\Servers\FileController::class, 'chmod']); - Route::post('/pull', [Client\Servers\FileController::class, 'pull'])->middleware(['throttle:10,5']); + Route::middleware([ResourceLimit::FilePull->middleware()]) + ->post('/pull', [Client\Servers\FileController::class, 'pull']); Route::get('/upload', Client\Servers\FileUploadController::class); }); Route::prefix('/schedules')->group(function () { Route::get('/', [Client\Servers\ScheduleController::class, 'index']); - Route::post('/', [Client\Servers\ScheduleController::class, 'store']); + Route::middleware([ResourceLimit::ScheduleCreate->middleware()]) + ->post('/', [Client\Servers\ScheduleController::class, 'store']); Route::get('/{schedule}', [Client\Servers\ScheduleController::class, 'view']); Route::post('/{schedule}', [Client\Servers\ScheduleController::class, 'update']); Route::post('/{schedule}/execute', [Client\Servers\ScheduleController::class, 'execute']); @@ -95,7 +101,8 @@ Route::prefix('/network/allocations')->group(function () { Route::get('/', [Client\Servers\NetworkAllocationController::class, 'index']); - Route::post('/', [Client\Servers\NetworkAllocationController::class, 'store']); + Route::middleware([ResourceLimit::AllocationCreate->middleware()]) + ->post('/', [Client\Servers\NetworkAllocationController::class, 'store']); Route::post('/{allocation}', [Client\Servers\NetworkAllocationController::class, 'update']); Route::post('/{allocation}/primary', [Client\Servers\NetworkAllocationController::class, 'setPrimary']); Route::delete('/{allocation}', [Client\Servers\NetworkAllocationController::class, 'delete']); @@ -103,7 +110,8 @@ Route::prefix('/users')->group(function () { Route::get('/', [Client\Servers\SubuserController::class, 'index']); - Route::post('/', [Client\Servers\SubuserController::class, 'store']); + Route::middleware([ResourceLimit::SubuserCreate->middleware()]) + ->post('/', [Client\Servers\SubuserController::class, 'store']); Route::get('/{user:uuid}', [Client\Servers\SubuserController::class, 'view']); Route::post('/{user:uuid}', [Client\Servers\SubuserController::class, 'update']); Route::delete('/{user:uuid}', [Client\Servers\SubuserController::class, 'delete']); @@ -116,7 +124,8 @@ Route::get('/{backup:uuid}/download', [Client\Servers\BackupController::class, 'download']); Route::put('/{backup:uuid}/rename', [Client\Servers\BackupController::class, 'rename']); Route::post('/{backup:uuid}/lock', [Client\Servers\BackupController::class, 'toggleLock']); - Route::post('/{backup:uuid}/restore', [Client\Servers\BackupController::class, 'restore']); + Route::middleware([ResourceLimit::BackupRestore->middleware()]) + ->post('/{backup:uuid}/restore', [Client\Servers\BackupController::class, 'restore']); Route::delete('/{backup:uuid}', [Client\Servers\BackupController::class, 'delete']); }); From 1c4c68c2363d4ebd9d556a60f47c98b176f7b847 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 6 Jan 2026 11:07:04 +0100 Subject: [PATCH 06/14] Lock the server's resource models when adding new ones Co-authored-by: DaneEveritt --- .../Api/Client/Servers/BackupController.php | 18 ++++++++++++------ .../Api/Client/Servers/DatabaseController.php | 13 ++++++++----- .../Servers/NetworkAllocationController.php | 17 +++++++++-------- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/app/Http/Controllers/Api/Client/Servers/BackupController.php b/app/Http/Controllers/Api/Client/Servers/BackupController.php index fd6217ad60..d5492cbcd8 100644 --- a/app/Http/Controllers/Api/Client/Servers/BackupController.php +++ b/app/Http/Controllers/Api/Client/Servers/BackupController.php @@ -83,15 +83,21 @@ public function store(StoreBackupRequest $request, Server $server): array // how best to allow a user to create a backup that is locked without also preventing // them from just filling up a server with backups that can never be deleted? if ($request->user()->can(SubuserPermission::BackupDelete, $server)) { - $action->setIsLocked((bool) $request->input('is_locked')); + $action->setIsLocked($request->boolean('is_locked')); } - $backup = $action->handle($server, $request->input('name')); + $backup = Activity::event('server:backup.start')->transaction(function ($log) use ($action, $server, $request) { + $server->backups()->lockForUpdate(); - Activity::event('server:backup.start') - ->subject($backup) - ->property(['name' => $backup->name, 'locked' => (bool) $request->input('is_locked')]) - ->log(); + $backup = $action->handle($server, $request->input('name')); + + $log->subject($backup)->property([ + 'name' => $backup->name, + 'locked' => $request->boolean('is_locked'), + ]); + + return $backup; + }); return $this->fractal->item($backup) ->transformWith($this->getTransformer(BackupTransformer::class)) diff --git a/app/Http/Controllers/Api/Client/Servers/DatabaseController.php b/app/Http/Controllers/Api/Client/Servers/DatabaseController.php index 2ce65f0173..445c8b2bcc 100644 --- a/app/Http/Controllers/Api/Client/Servers/DatabaseController.php +++ b/app/Http/Controllers/Api/Client/Servers/DatabaseController.php @@ -59,12 +59,15 @@ public function index(GetDatabasesRequest $request, Server $server): array */ public function store(StoreDatabaseRequest $request, Server $server): array { - $database = $this->deployDatabaseService->handle($server, $request->validated()); + $database = Activity::event('server:database.create')->transaction(function ($log) use ($request, $server) { + $server->databases()->lockForUpdate(); - Activity::event('server:database.create') - ->subject($database) - ->property('name', $database->database) - ->log(); + $database = $this->deployDatabaseService->handle($server, $request->validated()); + + $log->subject($database)->property('name', $database->database); + + return $database; + }); return $this->fractal->item($database) ->parseIncludes(['password']) diff --git a/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php b/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php index 08bfb54f20..d8ef9ccfa0 100644 --- a/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php +++ b/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php @@ -107,16 +107,17 @@ public function setPrimary(SetPrimaryAllocationRequest $request, Server $server, */ public function store(NewAllocationRequest $request, Server $server): array { - if ($server->allocations()->count() >= $server->allocation_limit) { - throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.'); - } + $allocation = Activity::event('server:allocation.create')->transaction(function ($log) use ($server) { + if ($server->allocations()->lockForUpdate()->count() >= $server->allocation_limit) { + throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.'); + } - $allocation = $this->assignableAllocationService->handle($server); + $allocation = $this->assignableAllocationService->handle($server); - Activity::event('server:allocation.create') - ->subject($allocation) - ->property('allocation', $allocation->address) - ->log(); + $log->subject($allocation)->property('allocation', $allocation->address); + + return $allocation; + }); return $this->fractal->item($allocation) ->transformWith($this->getTransformer(AllocationTransformer::class)) From 506003897f3d0f0438a516ceb2a981e3213e15bd Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 6 Jan 2026 11:20:00 +0100 Subject: [PATCH 07/14] small fix --- app/Services/Servers/DetailsModificationService.php | 2 +- app/Services/Subusers/SubuserDeletionService.php | 2 +- app/Services/Subusers/SubuserUpdateService.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Services/Servers/DetailsModificationService.php b/app/Services/Servers/DetailsModificationService.php index 1b10f165c5..e8c40e3c56 100644 --- a/app/Services/Servers/DetailsModificationService.php +++ b/app/Services/Servers/DetailsModificationService.php @@ -48,7 +48,7 @@ public function handle(Server $server, array $data): Server // websockets. if ($server->owner_id !== $owner) { try { - $this->serverRepository->setServer($server)->deauthorize($owner); + $this->serverRepository->setServer($server)->deauthorize($server->user->uuid); } catch (ConnectionException) { // Do nothing. A failure here is not ideal, but it is likely to be caused by daemon // being offline, or in an entirely broken state. Remember, these tokens reset every diff --git a/app/Services/Subusers/SubuserDeletionService.php b/app/Services/Subusers/SubuserDeletionService.php index 1060f96cd1..bbf232995d 100644 --- a/app/Services/Subusers/SubuserDeletionService.php +++ b/app/Services/Subusers/SubuserDeletionService.php @@ -28,7 +28,7 @@ public function handle(Subuser $subuser, Server $server): void event(new SubUserRemoved($subuser->server, $subuser->user)); try { - $this->serverRepository->setServer($server)->deauthorize($subuser->user_id); + $this->serverRepository->setServer($server)->deauthorize($subuser->user->uuid); } catch (ConnectionException $exception) { // Don't block this request if we can't connect to the daemon instance. logger()->warning($exception, ['user_id' => $subuser->user_id, 'server_id' => $server->id]); diff --git a/app/Services/Subusers/SubuserUpdateService.php b/app/Services/Subusers/SubuserUpdateService.php index 1df3e25006..1ce9c0ac53 100644 --- a/app/Services/Subusers/SubuserUpdateService.php +++ b/app/Services/Subusers/SubuserUpdateService.php @@ -46,7 +46,7 @@ public function handle(Subuser $subuser, Server $server, array $permissions): vo $subuser->update(['permissions' => $cleanedPermissions]); try { - $this->serverRepository->setServer($server)->deauthorize($subuser->user_id); + $this->serverRepository->setServer($server)->deauthorize($subuser->user->uuid); } catch (ConnectionException $exception) { // Don't block this request if we can't connect to the daemon instance. Chances are it is // offline and the token will be invalid once daemon boots back. From 9eca8873ef51b282fa8c24acecd24bd3d2b752af Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 6 Jan 2026 12:50:46 +0100 Subject: [PATCH 08/14] fix tests --- .../Api/Client/Server/Subuser/DeleteSubuserTest.php | 4 ++-- .../Api/Client/Server/Subuser/SubuserAuthorizationTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Integration/Api/Client/Server/Subuser/DeleteSubuserTest.php b/tests/Integration/Api/Client/Server/Subuser/DeleteSubuserTest.php index 5f54483774..f0a368c25f 100644 --- a/tests/Integration/Api/Client/Server/Subuser/DeleteSubuserTest.php +++ b/tests/Integration/Api/Client/Server/Subuser/DeleteSubuserTest.php @@ -42,7 +42,7 @@ public function test_correct_subuser_is_deleted_from_server(): void 'permissions' => [SubuserPermission::WebsocketConnect], ]); - $mock->expects('setServer->deauthorize')->with($subuser->id)->andReturnUndefined(); + $mock->expects('setServer->deauthorize')->with($subuser->uuid)->andReturnUndefined(); $this->actingAs($user)->deleteJson($this->link($server) . "/users/$subuser->uuid")->assertNoContent(); @@ -58,7 +58,7 @@ public function test_correct_subuser_is_deleted_from_server(): void 'permissions' => [SubuserPermission::WebsocketConnect], ]); - $mock->expects('setServer->deauthorize')->with($subuser->id)->andReturnUndefined(); + $mock->expects('setServer->deauthorize')->with($subuser->uuid)->andReturnUndefined(); $this->actingAs($user)->deleteJson($this->link($server) . "/users/$subuser->uuid")->assertNoContent(); } diff --git a/tests/Integration/Api/Client/Server/Subuser/SubuserAuthorizationTest.php b/tests/Integration/Api/Client/Server/Subuser/SubuserAuthorizationTest.php index 3d8e17f9fc..b838c97783 100644 --- a/tests/Integration/Api/Client/Server/Subuser/SubuserAuthorizationTest.php +++ b/tests/Integration/Api/Client/Server/Subuser/SubuserAuthorizationTest.php @@ -37,7 +37,7 @@ public function test_user_cannot_access_resource_belonging_to_other_servers(stri $this->instance(DaemonServerRepository::class, $mock = \Mockery::mock(DaemonServerRepository::class)); if ($method === 'DELETE') { - $mock->expects('setServer->deauthorize')->with($internal->id)->andReturnUndefined(); + $mock->expects('setServer->deauthorize')->with($internal->uuid)->andReturnUndefined(); } // This route is acceptable since they're accessing a subuser on their own server. From 0085fe526648cf0735d1d3150edc2b9704618c8a Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 6 Jan 2026 13:01:38 +0100 Subject: [PATCH 09/14] remove duplicate --- app/Models/Node.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/Node.php b/app/Models/Node.php index 3b7625498d..4c5479dc3f 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -81,7 +81,7 @@ class Node extends Model implements Validatable * Fields that are mass assignable. */ protected $fillable = [ - 'public', 'name', 'description', + 'public', 'name', 'fqdn', 'scheme', 'behind_proxy', 'memory', 'memory_overallocate', 'disk', 'disk_overallocate', 'cpu', 'cpu_overallocate', From fad5868da7e8457337c2b07f82576a9d61998ebd Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 6 Jan 2026 13:08:29 +0100 Subject: [PATCH 10/14] fix tests part two --- bootstrap/app.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bootstrap/app.php b/bootstrap/app.php index d40ab8cc38..cd5ca70057 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -44,6 +44,10 @@ 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'node.maintenance' => \App\Http\Middleware\MaintenanceMiddleware::class, ]); + + $middleware->priority([ + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ]); }) ->withSingletons([ \Illuminate\Contracts\Console\Kernel::class => \App\Console\Kernel::class, From 02dddcfb61f70261bb534a125feea383d9e64238 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 6 Jan 2026 13:18:42 +0100 Subject: [PATCH 11/14] fix postgresql --- .../Api/Client/Servers/NetworkAllocationController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php b/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php index d8ef9ccfa0..fc8ae338d9 100644 --- a/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php +++ b/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php @@ -108,7 +108,9 @@ public function setPrimary(SetPrimaryAllocationRequest $request, Server $server, public function store(NewAllocationRequest $request, Server $server): array { $allocation = Activity::event('server:allocation.create')->transaction(function ($log) use ($server) { - if ($server->allocations()->lockForUpdate()->count() >= $server->allocation_limit) { + $server->allocations()->lockForUpdate(); + + if ($server->allocations->count() >= $server->allocation_limit) { throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.'); } From 85fd11a492f2c8691a2d53c2a0015d8196aa1b71 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 8 Jan 2026 08:35:35 +0100 Subject: [PATCH 12/14] remove info about webserver limit --- app/Filament/Admin/Resources/Nodes/Pages/CreateNode.php | 3 +-- app/Filament/Admin/Resources/Nodes/Pages/EditNode.php | 2 +- lang/en/admin/node.php | 5 +---- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/Filament/Admin/Resources/Nodes/Pages/CreateNode.php b/app/Filament/Admin/Resources/Nodes/Pages/CreateNode.php index 782b0f49a1..b5de18d5d9 100644 --- a/app/Filament/Admin/Resources/Nodes/Pages/CreateNode.php +++ b/app/Filament/Admin/Resources/Nodes/Pages/CreateNode.php @@ -253,8 +253,7 @@ public function form(Schema $schema): Schema ->columnSpan(2), TextInput::make('upload_size') ->label(trans('admin/node.upload_limit')) - ->helperText(trans('admin/node.upload_limit_help.0')) - ->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help.1')) + ->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help')) ->columnSpan(1) ->numeric()->required() ->default(256) diff --git a/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php b/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php index a4880f3d53..0497beae22 100644 --- a/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php +++ b/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php @@ -319,7 +319,7 @@ public function form(Schema $schema): Schema 'lg' => 1, ]) ->label(trans('admin/node.upload_limit')) - ->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help.0') . trans('admin/node.upload_limit_help.1')) + ->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help')) ->numeric() ->required() ->minValue(1) diff --git a/lang/en/admin/node.php b/lang/en/admin/node.php index 40b514673c..32b3fdd322 100644 --- a/lang/en/admin/node.php +++ b/lang/en/admin/node.php @@ -61,10 +61,7 @@ 'tags' => 'Tags', 'upload_limit' => 'Upload Limit', - 'upload_limit_help' => [ - 'Enter the maximum size of files that can be uploaded through the web-based file manager.', - 'Make sure your webserver supports file uploads of this size!', - ], + 'upload_limit_help' => 'Enter the maximum size of files that can be uploaded through the web-based file manager.', 'sftp_port' => 'SFTP Port', 'sftp_alias' => 'SFTP Alias', 'sftp_alias_help' => 'Display alias for the SFTP address. Leave empty to use the Node FQDN.', From c89aa4fca5e6e35d84e8c27c4dcb93a2dcf8435f Mon Sep 17 00:00:00 2001 From: Boy132 Date: Fri, 9 Jan 2026 14:42:42 +0100 Subject: [PATCH 13/14] add migration to clear unused allocation notes --- ...9_134116_clear_unused_allocation_notes.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 database/migrations/2026_01_09_134116_clear_unused_allocation_notes.php diff --git a/database/migrations/2026_01_09_134116_clear_unused_allocation_notes.php b/database/migrations/2026_01_09_134116_clear_unused_allocation_notes.php new file mode 100644 index 0000000000..6eb6e5c2d1 --- /dev/null +++ b/database/migrations/2026_01_09_134116_clear_unused_allocation_notes.php @@ -0,0 +1,25 @@ +whereNull('server_id') + ->update(['notes' => null]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Not needed + } +}; From a28111e6e90cdfeace11dbf1d06c0e8c4cf20342 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Sun, 11 Jan 2026 14:19:15 +0100 Subject: [PATCH 14/14] Cast presigned URL lifespan to integer --- .../Remote/Backups/BackupRemoteUploadController.php | 2 +- config/backups.php | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php b/app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php index 77d869949f..fa149c31e6 100644 --- a/app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php +++ b/app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php @@ -131,7 +131,7 @@ public function __invoke(Request $request, string $backup): JsonResponse */ private function getConfiguredMaxPartSize(): int { - $maxPartSize = (int) config('backups.max_part_size', self::DEFAULT_MAX_PART_SIZE); + $maxPartSize = config('backups.max_part_size', self::DEFAULT_MAX_PART_SIZE); if ($maxPartSize <= 0) { $maxPartSize = self::DEFAULT_MAX_PART_SIZE; } diff --git a/config/backups.php b/config/backups.php index 0074bf8d7a..c3bf16cf71 100644 --- a/config/backups.php +++ b/config/backups.php @@ -1,5 +1,6 @@ env('BACKUP_PRESIGNED_URL_LIFESPAN', 60), + 'presigned_url_lifespan' => (int) env('BACKUP_PRESIGNED_URL_LIFESPAN', 60), // This value defines the maximal size of a single part for the S3 multipart upload during backups // The maximal part size must be given in bytes. The default value is 5GB. // Note that 5GB is the maximum for a single part when using AWS S3. - 'max_part_size' => env('BACKUP_MAX_PART_SIZE', 5 * 1024 * 1024 * 1024), + 'max_part_size' => (int) env('BACKUP_MAX_PART_SIZE', BackupRemoteUploadController::DEFAULT_MAX_PART_SIZE), // The time to wait before automatically failing a backup, time is in minutes and defaults // to 6 hours. To disable this feature, set the value to `0`. - 'prune_age' => env('BACKUP_PRUNE_AGE', 360), + 'prune_age' => (int) env('BACKUP_PRUNE_AGE', 360), // Defines the backup creation throttle limits for users. In this default example, we allow // a user to create two (successful or pending) backups per 10 minutes. Even if they delete @@ -27,8 +28,8 @@ // // Set the period to "0" to disable this throttle. The period is defined in seconds. 'throttles' => [ - 'limit' => env('BACKUP_THROTTLE_LIMIT', 2), - 'period' => env('BACKUP_THROTTLE_PERIOD', 600), + 'limit' => (int) env('BACKUP_THROTTLE_LIMIT', 2), + 'period' => (int) env('BACKUP_THROTTLE_PERIOD', 600), ], 'disks' => [