From e28973bcaedc43840c44fba409d3b30a3547b341 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 25 Feb 2018 15:30:56 -0600 Subject: [PATCH 01/25] Move everything around as needed to get things setup for the client API --- .../InvalidTransformerLevelException.php | 9 ++++ .../Application/ApplicationApiController.php | 10 ++++- .../Api/Client/ClientApiController.php | 30 +++++++++++++ .../Api/Client/ClientController.php | 9 ++++ app/Http/Kernel.php | 23 +++++++--- ...er.php => AuthenticateApplicationUser.php} | 2 +- .../AuthenticateIPAccess.php | 2 +- .../Api/{Application => }/AuthenticateKey.php | 7 +-- .../Api/Client/AuthenticateClientAccess.php | 27 ++++++++++++ .../{Application => }/SetSessionDriver.php | 2 +- app/Providers/RouteServiceProvider.php | 4 ++ .../Api/Application/BaseTransformer.php | 9 +++- .../Api/Application/ServerTransformer.php | 2 + .../Api/Client/BaseClientTransformer.php | 44 +++++++++++++++++++ routes/api-client.php | 28 ++++++++++++ routes/api.php | 27 ------------ .../Api/Application/AuthenticateUserTest.php | 10 ++--- 17 files changed, 199 insertions(+), 46 deletions(-) create mode 100644 app/Exceptions/Transformer/InvalidTransformerLevelException.php create mode 100644 app/Http/Controllers/Api/Client/ClientApiController.php create mode 100644 app/Http/Controllers/Api/Client/ClientController.php rename app/Http/Middleware/Api/Application/{AuthenticateUser.php => AuthenticateApplicationUser.php} (95%) rename app/Http/Middleware/Api/{Application => }/AuthenticateIPAccess.php (95%) rename app/Http/Middleware/Api/{Application => }/AuthenticateKey.php (92%) create mode 100644 app/Http/Middleware/Api/Client/AuthenticateClientAccess.php rename app/Http/Middleware/Api/{Application => }/SetSessionDriver.php (95%) create mode 100644 app/Transformers/Api/Client/BaseClientTransformer.php create mode 100644 routes/api-client.php delete mode 100644 routes/api.php diff --git a/app/Exceptions/Transformer/InvalidTransformerLevelException.php b/app/Exceptions/Transformer/InvalidTransformerLevelException.php new file mode 100644 index 000000000..3d4c24248 --- /dev/null +++ b/app/Exceptions/Transformer/InvalidTransformerLevelException.php @@ -0,0 +1,9 @@ +make($abstract); $transformer->setKey($this->request->attributes->get('api_key')); + if (! $transformer instanceof BaseTransformer) { + throw new InvalidTransformerLevelException('Calls to ' . __METHOD__ . ' must return a transformer that is an instance of ' . __CLASS__); + } + return $transformer; } diff --git a/app/Http/Controllers/Api/Client/ClientApiController.php b/app/Http/Controllers/Api/Client/ClientApiController.php new file mode 100644 index 000000000..2ade63dff --- /dev/null +++ b/app/Http/Controllers/Api/Client/ClientApiController.php @@ -0,0 +1,30 @@ +make($abstract); + $transformer->setKey($this->request->attributes->get('api_key')); + + if (! $transformer instanceof self) { + throw new InvalidTransformerLevelException('Calls to ' . __METHOD__ . ' must return a transformer that is an instance of ' . __CLASS__); + } + + return $transformer; + } +} diff --git a/app/Http/Controllers/Api/Client/ClientController.php b/app/Http/Controllers/Api/Client/ClientController.php new file mode 100644 index 000000000..87a5e5f15 --- /dev/null +++ b/app/Http/Controllers/Api/Client/ClientController.php @@ -0,0 +1,9 @@ + [ + 'throttle:60,1', + ApiSubstituteBindings::class, + SetSessionDriver::class, + 'api..key:' . ApiKey::TYPE_ACCOUNT, AuthenticateIPAccess::class, ], 'daemon' => [ @@ -107,5 +115,8 @@ class Kernel extends HttpKernel 'server..database' => DatabaseBelongsToServer::class, 'server..subuser' => SubuserBelongsToServer::class, 'server..schedule' => ScheduleBelongsToServer::class, + + // API Specific Middleware + 'api..key' => AuthenticateKey::class, ]; } diff --git a/app/Http/Middleware/Api/Application/AuthenticateUser.php b/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php similarity index 95% rename from app/Http/Middleware/Api/Application/AuthenticateUser.php rename to app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php index 7208dbaf9..48da8a74d 100644 --- a/app/Http/Middleware/Api/Application/AuthenticateUser.php +++ b/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php @@ -6,7 +6,7 @@ use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; -class AuthenticateUser +class AuthenticateApplicationUser { /** * Authenticate that the currently authenticated user is an administrator diff --git a/app/Http/Middleware/Api/Application/AuthenticateIPAccess.php b/app/Http/Middleware/Api/AuthenticateIPAccess.php similarity index 95% rename from app/Http/Middleware/Api/Application/AuthenticateIPAccess.php rename to app/Http/Middleware/Api/AuthenticateIPAccess.php index 6988c637d..4815063d5 100644 --- a/app/Http/Middleware/Api/Application/AuthenticateIPAccess.php +++ b/app/Http/Middleware/Api/AuthenticateIPAccess.php @@ -1,6 +1,6 @@ bearerToken())) { throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']); @@ -68,7 +69,7 @@ class AuthenticateKey try { $model = $this->repository->findFirstWhere([ ['identifier', '=', $identifier], - ['key_type', '=', ApiKey::TYPE_APPLICATION], + ['key_type', '=', $keyType], ]); } catch (RecordNotFoundException $exception) { throw new AccessDeniedHttpException; diff --git a/app/Http/Middleware/Api/Client/AuthenticateClientAccess.php b/app/Http/Middleware/Api/Client/AuthenticateClientAccess.php new file mode 100644 index 000000000..0a006aef0 --- /dev/null +++ b/app/Http/Middleware/Api/Client/AuthenticateClientAccess.php @@ -0,0 +1,27 @@ +user())) { + throw new AccessDeniedHttpException('This account does not have permission to access this resource.'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/Api/Application/SetSessionDriver.php b/app/Http/Middleware/Api/SetSessionDriver.php similarity index 95% rename from app/Http/Middleware/Api/Application/SetSessionDriver.php rename to app/Http/Middleware/Api/SetSessionDriver.php index c4660ec9b..c69311a65 100644 --- a/app/Http/Middleware/Api/Application/SetSessionDriver.php +++ b/app/Http/Middleware/Api/SetSessionDriver.php @@ -1,6 +1,6 @@ namespace($this->namespace . '\Api\Application') ->group(base_path('routes/api-application.php')); + Route::middleware(['client-api'])->prefix('/api/client') + ->namespace($this->namespace . '\Api\Client') + ->group(base_path('routes/api-client.php')); + Route::middleware(['daemon'])->prefix('/api/remote') ->namespace($this->namespace . '\Api\Remote') ->group(base_path('routes/api-remote.php')); diff --git a/app/Transformers/Api/Application/BaseTransformer.php b/app/Transformers/Api/Application/BaseTransformer.php index c2fdf6137..5766088c9 100644 --- a/app/Transformers/Api/Application/BaseTransformer.php +++ b/app/Transformers/Api/Application/BaseTransformer.php @@ -7,6 +7,7 @@ use Pterodactyl\Models\ApiKey; use Illuminate\Container\Container; use League\Fractal\TransformerAbstract; use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException; abstract class BaseTransformer extends TransformerAbstract { @@ -78,13 +79,19 @@ abstract class BaseTransformer extends TransformerAbstract * @param string $abstract * @param array $parameters * @return \Pterodactyl\Transformers\Api\Application\BaseTransformer + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ - protected function makeTransformer(string $abstract, array $parameters = []): self + protected function makeTransformer(string $abstract, array $parameters = []) { /** @var \Pterodactyl\Transformers\Api\Application\BaseTransformer $transformer */ $transformer = Container::getInstance()->makeWith($abstract, $parameters); $transformer->setKey($this->getKey()); + if (! $transformer instanceof self) { + throw new InvalidTransformerLevelException('Calls to ' . __METHOD__ . ' must return a transformer that is an instance of ' . __CLASS__); + } + return $transformer; } diff --git a/app/Transformers/Api/Application/ServerTransformer.php b/app/Transformers/Api/Application/ServerTransformer.php index 69115d1ed..31eed90c5 100644 --- a/app/Transformers/Api/Application/ServerTransformer.php +++ b/app/Transformers/Api/Application/ServerTransformer.php @@ -97,6 +97,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeAllocations(Server $server) { diff --git a/app/Transformers/Api/Client/BaseClientTransformer.php b/app/Transformers/Api/Client/BaseClientTransformer.php new file mode 100644 index 000000000..76b618e4d --- /dev/null +++ b/app/Transformers/Api/Client/BaseClientTransformer.php @@ -0,0 +1,44 @@ +getKey(), $resource, AdminAcl::READ); + } + + /** + * Create a new instance of the transformer and pass along the currently + * set API key. + * + * @param string $abstract + * @param array $parameters + * @return self + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException + */ + protected function makeTransformer(string $abstract, array $parameters = []) + { + $transformer = parent::makeTransformer($abstract, $parameters); + + if (! $transformer instanceof self) { + throw new InvalidTransformerLevelException('Calls to ' . __METHOD__ . ' must return a transformer that is an instance of ' . __CLASS__); + } + + return $transformer; + } +} diff --git a/routes/api-client.php b/routes/api-client.php new file mode 100644 index 000000000..fa4cb6c31 --- /dev/null +++ b/routes/api-client.php @@ -0,0 +1,28 @@ +name('api.client.index'); + +/* +|-------------------------------------------------------------------------- +| Client Control API +|-------------------------------------------------------------------------- +| +| Endpoint: /api/client/servers/{server} +| +*/ +Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateClientAccess::class]], function () { + Route::get('/', 'Server\ServerController@index')->name('api.client.servers.view'); + + Route::post('/command', 'Server\CommandController@index')->name('api.client.servers.command'); + Route::post('/power', 'Server\PowerController@index')->name('api.client.servers.power'); +}); diff --git a/routes/api.php b/routes/api.php deleted file mode 100644 index 96dfe5dde..000000000 --- a/routes/api.php +++ /dev/null @@ -1,27 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ -//Route::get('/', 'CoreController@index')->name('api.user'); -// -///* -//|-------------------------------------------------------------------------- -//| Server Controller Routes -//|-------------------------------------------------------------------------- -//| -//| Endpoint: /api/user/server/{server} -//| -//*/ -//Route::group([ -// 'prefix' => '/server/{server}', -// 'middleware' => 'server', -//], function () { -// Route::get('/', 'ServerController@index')->name('api.user.server'); -// -// Route::post('/power', 'ServerController@power')->name('api.user.server.power'); -// Route::post('/command', 'ServerController@command')->name('api.user.server.command'); -//}); diff --git a/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php b/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php index 56c7f5ffb..7c0cfc9e7 100644 --- a/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php +++ b/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php @@ -1,9 +1,9 @@ Date: Sun, 25 Feb 2018 15:34:01 -0600 Subject: [PATCH 02/25] Fix some broken tests --- .../AuthenticateIPAccessTest.php | 6 +-- .../AuthenticateKeyTest.php | 37 +++++++++++++++---- .../SetSessionDriverTest.php | 6 +-- 3 files changed, 36 insertions(+), 13 deletions(-) rename tests/Unit/Http/Middleware/{Api/Application => API}/AuthenticateIPAccessTest.php (91%) rename tests/Unit/Http/Middleware/{Api/Application => API}/AuthenticateKeyTest.php (74%) rename tests/Unit/Http/Middleware/{Api/Application => API}/SetSessionDriverTest.php (90%) diff --git a/tests/Unit/Http/Middleware/Api/Application/AuthenticateIPAccessTest.php b/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php similarity index 91% rename from tests/Unit/Http/Middleware/Api/Application/AuthenticateIPAccessTest.php rename to tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php index cf23d0292..59b9137d9 100644 --- a/tests/Unit/Http/Middleware/Api/Application/AuthenticateIPAccessTest.php +++ b/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php @@ -1,10 +1,10 @@ request->shouldReceive('bearerToken')->withNoArgs()->once()->andReturnNull(); try { - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_APPLICATION); } catch (HttpException $exception) { $this->assertEquals(401, $exception->getStatusCode()); $this->assertEquals(['WWW-Authenticate' => 'Bearer'], $exception->getHeaders()); @@ -68,7 +68,7 @@ class AuthenticateKeyTest extends MiddlewareTestCase $this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn('abcd1234'); $this->repository->shouldReceive('findFirstWhere')->andThrow(new RecordNotFoundException); - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_APPLICATION); } /** @@ -90,7 +90,30 @@ class AuthenticateKeyTest extends MiddlewareTestCase 'last_used_at' => Chronos::now(), ])->once()->andReturnNull(); - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_APPLICATION); + $this->assertEquals($model, $this->request->attributes->get('api_key')); + } + + /** + * Test that a valid token can continue past the middleware when set as a user token. + */ + public function testValidTokenWithUserKey() + { + $model = factory(ApiKey::class)->make(); + + $this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn($model->identifier . 'decrypted'); + $this->repository->shouldReceive('findFirstWhere')->with([ + ['identifier', '=', $model->identifier], + ['key_type', '=', ApiKey::TYPE_ACCOUNT], + ])->once()->andReturn($model); + $this->encrypter->shouldReceive('decrypt')->with($model->token)->once()->andReturn('decrypted'); + $this->auth->shouldReceive('guard->loginUsingId')->with($model->user_id)->once()->andReturnNull(); + + $this->repository->shouldReceive('withoutFreshModel->update')->with($model->id, [ + 'last_used_at' => Chronos::now(), + ])->once()->andReturnNull(); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_ACCOUNT); $this->assertEquals($model, $this->request->attributes->get('api_key')); } @@ -111,13 +134,13 @@ class AuthenticateKeyTest extends MiddlewareTestCase ])->once()->andReturn($model); $this->encrypter->shouldReceive('decrypt')->with($model->token)->once()->andReturn('decrypted'); - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_APPLICATION); } /** * Return an instance of the middleware with mocked dependencies for testing. * - * @return \Pterodactyl\Http\Middleware\Api\Application\AuthenticateKey + * @return \Pterodactyl\Http\Middleware\Api\AuthenticateKey */ private function getMiddleware(): AuthenticateKey { diff --git a/tests/Unit/Http/Middleware/Api/Application/SetSessionDriverTest.php b/tests/Unit/Http/Middleware/API/SetSessionDriverTest.php similarity index 90% rename from tests/Unit/Http/Middleware/Api/Application/SetSessionDriverTest.php rename to tests/Unit/Http/Middleware/API/SetSessionDriverTest.php index 7804f8209..0f33f6735 100644 --- a/tests/Unit/Http/Middleware/Api/Application/SetSessionDriverTest.php +++ b/tests/Unit/Http/Middleware/API/SetSessionDriverTest.php @@ -1,13 +1,13 @@ Date: Tue, 27 Feb 2018 19:43:47 -0600 Subject: [PATCH 03/25] closes #991 --- CHANGELOG.md | 4 +++ .../Servers/ReinstallServerService.php | 2 +- .../Servers/ReinstallServerServiceTest.php | 28 ++++++++----------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c69c8776..bbed28aac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ This file is a running track of new features and fixes to each version of the pa This project follows [Semantic Versioning](http://semver.org) guidelines. +## v0.7.4 (Derelict Dermodactylus) +### Fixed +* Fixes a bug when reinstalling a server that would not mark the server as installing, resulting in some UI issues. + ## v0.7.3 (Derelict Dermodactylus) ### Fixed * Fixes server creation API endpoint not passing the provided `external_id` to the creation service. diff --git a/app/Services/Servers/ReinstallServerService.php b/app/Services/Servers/ReinstallServerService.php index 682813e36..85800473f 100644 --- a/app/Services/Servers/ReinstallServerService.php +++ b/app/Services/Servers/ReinstallServerService.php @@ -66,7 +66,7 @@ class ReinstallServerService $this->database->beginTransaction(); $this->repository->withoutFreshModel()->update($server->id, [ 'installed' => 0, - ]); + ], true, true); try { $this->daemonServerRepository->setServer($server)->reinstall(); diff --git a/tests/Unit/Services/Servers/ReinstallServerServiceTest.php b/tests/Unit/Services/Servers/ReinstallServerServiceTest.php index 349aa571a..f00614f77 100644 --- a/tests/Unit/Services/Servers/ReinstallServerServiceTest.php +++ b/tests/Unit/Services/Servers/ReinstallServerServiceTest.php @@ -81,10 +81,9 @@ class ReinstallServerServiceTest extends TestCase $this->repository->shouldNotReceive('find'); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($this->server->id, [ - 'installed' => 0, - ])->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFreshModel->update')->with($this->server->id, [ + 'installed' => 0, + ], true, true)->once()->andReturnNull(); $this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andReturnSelf() ->shouldReceive('reinstall')->withNoArgs()->once()->andReturn(new Response); @@ -101,10 +100,9 @@ class ReinstallServerServiceTest extends TestCase $this->repository->shouldReceive('find')->with($this->server->id)->once()->andReturn($this->server); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($this->server->id, [ - 'installed' => 0, - ])->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFreshModel->update')->with($this->server->id, [ + 'installed' => 0, + ], true, true)->once()->andReturnNull(); $this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andReturnSelf() ->shouldReceive('reinstall')->withNoArgs()->once()->andReturn(new Response); @@ -121,10 +119,9 @@ class ReinstallServerServiceTest extends TestCase public function testExceptionThrownByGuzzleShouldBeReRenderedAsDisplayable() { $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($this->server->id, [ - 'installed' => 0, - ])->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFreshModel->update')->with($this->server->id, [ + 'installed' => 0, + ], true, true)->once()->andReturnNull(); $this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andThrow($this->exception); @@ -139,10 +136,9 @@ class ReinstallServerServiceTest extends TestCase public function testExceptionNotThrownByGuzzleShouldNotBeTransformedToDisplayable() { $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($this->server->id, [ - 'installed' => 0, - ])->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFreshModel->update')->with($this->server->id, [ + 'installed' => 0, + ], true, true)->once()->andReturnNull(); $this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andThrow(new Exception()); From 23e07689a78b1cb486baa3e4838b858a02f0a661 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 27 Feb 2018 21:04:04 -0600 Subject: [PATCH 04/25] Handle 404 errors in API bindings correctly to avoid explosing that a resource exists before validating a key --- CHANGELOG.md | 1 + app/Http/Middleware/Api/ApiSubstituteBindings.php | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbed28aac..a3e47a3b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. ## v0.7.4 (Derelict Dermodactylus) ### Fixed * Fixes a bug when reinstalling a server that would not mark the server as installing, resulting in some UI issues. +* Handle 404 errors from missing models in the application API bindings correctly. ## v0.7.3 (Derelict Dermodactylus) ### Fixed diff --git a/app/Http/Middleware/Api/ApiSubstituteBindings.php b/app/Http/Middleware/Api/ApiSubstituteBindings.php index b270be4ca..a56a3426f 100644 --- a/app/Http/Middleware/Api/ApiSubstituteBindings.php +++ b/app/Http/Middleware/Api/ApiSubstituteBindings.php @@ -45,7 +45,9 @@ class ApiSubstituteBindings extends SubstituteBindings $route = $request->route(); foreach (self::$mappings as $key => $model) { - $this->router->model($key, $model); + $this->router->model($key, $model, function () use ($request) { + $request->attributes->set('is_missing_model', true); + }); } $this->router->substituteBindings($route); From cef3e4ced4cb1968780163c786dc3e20136923e4 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 27 Feb 2018 21:28:43 -0600 Subject: [PATCH 05/25] Add base routes for managing servers as a client --- .../Application/ApplicationApiController.php | 8 +- .../Api/Client/ClientApiController.php | 15 ++-- .../Api/Client/ClientController.php | 37 +++++++- .../Api/Client/Servers/ServerController.php | 25 ++++++ app/Http/Kernel.php | 3 +- .../Middleware/Api/ApiSubstituteBindings.php | 9 ++ .../Client/SubstituteClientApiBindings.php | 39 +++++++++ .../Requests/Api/Client/ClientApiRequest.php | 19 +++++ .../Requests/Api/Client/GetServersRequest.php | 14 +++ .../Api/Client/Servers/GetServerRequest.php | 31 +++++++ .../Api/Client/BaseClientTransformer.php | 38 ++++++++- .../Api/Client/ServerTransformer.php | 41 +++++++++ .../User/AllocationTransformer.php | 47 ---------- app/Transformers/User/OverviewTransformer.php | 35 -------- app/Transformers/User/ServerTransformer.php | 85 ------------------- app/Transformers/User/StatsTransformer.php | 48 ----------- app/Transformers/User/SubuserTransformer.php | 32 ------- routes/api-client.php | 6 +- 18 files changed, 262 insertions(+), 270 deletions(-) create mode 100644 app/Http/Controllers/Api/Client/Servers/ServerController.php create mode 100644 app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php create mode 100644 app/Http/Requests/Api/Client/ClientApiRequest.php create mode 100644 app/Http/Requests/Api/Client/GetServersRequest.php create mode 100644 app/Http/Requests/Api/Client/Servers/GetServerRequest.php create mode 100644 app/Transformers/Api/Client/ServerTransformer.php delete mode 100644 app/Transformers/User/AllocationTransformer.php delete mode 100644 app/Transformers/User/OverviewTransformer.php delete mode 100644 app/Transformers/User/ServerTransformer.php delete mode 100644 app/Transformers/User/StatsTransformer.php delete mode 100644 app/Transformers/User/SubuserTransformer.php diff --git a/app/Http/Controllers/Api/Application/ApplicationApiController.php b/app/Http/Controllers/Api/Application/ApplicationApiController.php index b311c110e..bdd5f9e7b 100644 --- a/app/Http/Controllers/Api/Application/ApplicationApiController.php +++ b/app/Http/Controllers/Api/Application/ApplicationApiController.php @@ -3,12 +3,12 @@ namespace Pterodactyl\Http\Controllers\Api\Application; use Illuminate\Http\Request; +use Webmozart\Assert\Assert; use Illuminate\Http\Response; use Illuminate\Container\Container; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Extensions\Spatie\Fractalistic\Fractal; use Pterodactyl\Transformers\Api\Application\BaseTransformer; -use Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException; abstract class ApplicationApiController extends Controller { @@ -56,8 +56,6 @@ abstract class ApplicationApiController extends Controller * * @param string $abstract * @return \Pterodactyl\Transformers\Api\Application\BaseTransformer - * - * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function getTransformer(string $abstract) { @@ -65,9 +63,7 @@ abstract class ApplicationApiController extends Controller $transformer = Container::getInstance()->make($abstract); $transformer->setKey($this->request->attributes->get('api_key')); - if (! $transformer instanceof BaseTransformer) { - throw new InvalidTransformerLevelException('Calls to ' . __METHOD__ . ' must return a transformer that is an instance of ' . __CLASS__); - } + Assert::isInstanceOf($transformer, BaseTransformer::class); return $transformer; } diff --git a/app/Http/Controllers/Api/Client/ClientApiController.php b/app/Http/Controllers/Api/Client/ClientApiController.php index 2ade63dff..e2d4b3f83 100644 --- a/app/Http/Controllers/Api/Client/ClientApiController.php +++ b/app/Http/Controllers/Api/Client/ClientApiController.php @@ -1,9 +1,11 @@ make($abstract); - $transformer->setKey($this->request->attributes->get('api_key')); + Assert::isInstanceOf($transformer, BaseClientTransformer::class); - if (! $transformer instanceof self) { - throw new InvalidTransformerLevelException('Calls to ' . __METHOD__ . ' must return a transformer that is an instance of ' . __CLASS__); - } + $transformer->setKey($this->request->attributes->get('api_key')); + $transformer->setUser($this->request->user()); return $transformer; } diff --git a/app/Http/Controllers/Api/Client/ClientController.php b/app/Http/Controllers/Api/Client/ClientController.php index 87a5e5f15..d2e1f33a9 100644 --- a/app/Http/Controllers/Api/Client/ClientController.php +++ b/app/Http/Controllers/Api/Client/ClientController.php @@ -2,8 +2,43 @@ namespace Pterodactyl\Http\Controllers\Api\Client; -use Pterodactyl\Http\Controllers\Api\Application\ClientApiController; +use Pterodactyl\Models\User; +use Pterodactyl\Transformers\Api\Client\ServerTransformer; +use Pterodactyl\Http\Requests\Api\Client\GetServersRequest; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class ClientController extends ClientApiController { + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + private $repository; + + /** + * ClientController constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + */ + public function __construct(ServerRepositoryInterface $repository) + { + parent::__construct(); + + $this->repository = $repository; + } + + /** + * Return all of the servers available to the client making the API + * request, including servers the user has access to as a subuser. + * + * @param \Pterodactyl\Http\Requests\Api\Client\GetServersRequest $request + * @return array + */ + public function index(GetServersRequest $request): array + { + $servers = $this->repository->filterUserAccessServers($request->user(), User::FILTER_LEVEL_SUBUSER); + + return $this->fractal->collection($servers) + ->transformWith($this->getTransformer(ServerTransformer::class)) + ->toArray(); + } } diff --git a/app/Http/Controllers/Api/Client/Servers/ServerController.php b/app/Http/Controllers/Api/Client/Servers/ServerController.php new file mode 100644 index 000000000..ce4502e3a --- /dev/null +++ b/app/Http/Controllers/Api/Client/Servers/ServerController.php @@ -0,0 +1,25 @@ +fractal->item($request->getModel(Server::class)) + ->transformWith($this->getTransformer(ServerTransformer::class)) + ->toArray(); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 5e8012015..b6d44530e 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -34,6 +34,7 @@ use Pterodactyl\Http\Middleware\Server\DatabaseBelongsToServer; use Pterodactyl\Http\Middleware\Server\ScheduleBelongsToServer; use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode; use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull; +use Pterodactyl\Http\Middleware\Api\Client\SubstituteClientApiBindings; use Pterodactyl\Http\Middleware\Api\Application\AuthenticateApplicationUser; use Pterodactyl\Http\Middleware\DaemonAuthenticate as OldDaemonAuthenticate; @@ -78,7 +79,7 @@ class Kernel extends HttpKernel ], 'client-api' => [ 'throttle:60,1', - ApiSubstituteBindings::class, + SubstituteClientApiBindings::class, SetSessionDriver::class, 'api..key:' . ApiKey::TYPE_ACCOUNT, AuthenticateIPAccess::class, diff --git a/app/Http/Middleware/Api/ApiSubstituteBindings.php b/app/Http/Middleware/Api/ApiSubstituteBindings.php index a56a3426f..94af9b1d4 100644 --- a/app/Http/Middleware/Api/ApiSubstituteBindings.php +++ b/app/Http/Middleware/Api/ApiSubstituteBindings.php @@ -32,6 +32,11 @@ class ApiSubstituteBindings extends SubstituteBindings 'user' => User::class, ]; + /** + * @var \Illuminate\Routing\Router + */ + protected $router; + /** * Perform substitution of route parameters without triggering * a 404 error if a model is not found. @@ -45,6 +50,10 @@ class ApiSubstituteBindings extends SubstituteBindings $route = $request->route(); foreach (self::$mappings as $key => $model) { + if (! is_null($this->router->getBindingCallback($key))) { + continue; + } + $this->router->model($key, $model, function () use ($request) { $request->attributes->set('is_missing_model', true); }); diff --git a/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php new file mode 100644 index 000000000..f8a35fdd8 --- /dev/null +++ b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php @@ -0,0 +1,39 @@ +router->bind('server', function ($value) use ($request) { + try { + return Container::getInstance()->make(ServerRepositoryInterface::class)->findFirstWhere([ + ['uuidShort', '=', $value], + ]); + } catch (RecordNotFoundException $ex) { + $request->attributes->set('is_missing_model', true); + + return null; + } + }); + + return parent::handle($request, $next); + } +} diff --git a/app/Http/Requests/Api/Client/ClientApiRequest.php b/app/Http/Requests/Api/Client/ClientApiRequest.php new file mode 100644 index 000000000..ed63ccbf0 --- /dev/null +++ b/app/Http/Requests/Api/Client/ClientApiRequest.php @@ -0,0 +1,19 @@ +user()->can('view-server', $this->getModel(Server::class)); + } +} diff --git a/app/Transformers/Api/Client/BaseClientTransformer.php b/app/Transformers/Api/Client/BaseClientTransformer.php index 76b618e4d..faa1abbed 100644 --- a/app/Transformers/Api/Client/BaseClientTransformer.php +++ b/app/Transformers/Api/Client/BaseClientTransformer.php @@ -2,23 +2,53 @@ namespace Pterodactyl\Transformers\Api\Client; -use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Models\User; +use Webmozart\Assert\Assert; +use Pterodactyl\Models\Server; use Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException; use Pterodactyl\Transformers\Api\Application\BaseTransformer as BaseApplicationTransformer; abstract class BaseClientTransformer extends BaseApplicationTransformer { + /** + * @var \Pterodactyl\Models\User + */ + private $user; + + /** + * Return the user model of the user requesting this transformation. + * + * @return \Pterodactyl\Models\User + */ + public function getUser(): User + { + return $this->user; + } + + /** + * Set the user model of the user requesting this transformation. + * + * @param \Pterodactyl\Models\User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + /** * Determine if the API key loaded onto the transformer has permission * to access a different resource. This is used when including other * models on a transformation request. * - * @param string $resource + * @param string $ability + * @param \Pterodactyl\Models\Server $server * @return bool */ - protected function authorize(string $resource): bool + protected function authorize(string $ability, Server $server = null): bool { - return AdminAcl::check($this->getKey(), $resource, AdminAcl::READ); + Assert::isInstanceOf($server, Server::class); + + return $this->getUser()->can($ability, [$server]); } /** diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php new file mode 100644 index 000000000..4f08c208d --- /dev/null +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -0,0 +1,41 @@ + $this->getKey()->user_id === $server->owner_id, + 'identifier' => $server->uuidShort, + 'uuid' => $server->uuid, + 'name' => $server->name, + 'description' => $server->description, + 'limits' => [ + 'memory' => $server->memory, + 'swap' => $server->swap, + 'disk' => $server->disk, + 'io' => $server->io, + 'cpu' => $server->cpu, + ], + ]; + } +} diff --git a/app/Transformers/User/AllocationTransformer.php b/app/Transformers/User/AllocationTransformer.php deleted file mode 100644 index 8ea0c8f91..000000000 --- a/app/Transformers/User/AllocationTransformer.php +++ /dev/null @@ -1,47 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\User; - -use Pterodactyl\Models\Server; -use Pterodactyl\Models\Allocation; -use League\Fractal\TransformerAbstract; - -class AllocationTransformer extends TransformerAbstract -{ - /** - * Server eloquent model. - * - * @return \Pterodactyl\Models\Server - */ - protected $server; - - /** - * Setup allocation transformer with access to server data. - */ - public function __construct(Server $server) - { - $this->server = $server; - } - - /** - * Return a generic transformed allocation array. - * - * @return array - */ - public function transform(Allocation $allocation) - { - return [ - 'id' => $allocation->id, - 'ip' => $allocation->alias, - 'port' => $allocation->port, - 'default' => ($allocation->id === $this->server->allocation_id), - ]; - } -} diff --git a/app/Transformers/User/OverviewTransformer.php b/app/Transformers/User/OverviewTransformer.php deleted file mode 100644 index b59990766..000000000 --- a/app/Transformers/User/OverviewTransformer.php +++ /dev/null @@ -1,35 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\User; - -use Pterodactyl\Models\Server; -use League\Fractal\TransformerAbstract; - -class OverviewTransformer extends TransformerAbstract -{ - /** - * Return a generic transformed server array. - * - * @return array - */ - public function transform(Server $server) - { - return [ - 'id' => $server->uuidShort, - 'uuid' => $server->uuid, - 'name' => $server->name, - 'node' => $server->node->name, - 'ip' => $server->allocation->alias, - 'port' => $server->allocation->port, - 'service' => $server->service->name, - 'option' => $server->option->name, - ]; - } -} diff --git a/app/Transformers/User/ServerTransformer.php b/app/Transformers/User/ServerTransformer.php deleted file mode 100644 index 031ae82f8..000000000 --- a/app/Transformers/User/ServerTransformer.php +++ /dev/null @@ -1,85 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\User; - -use Pterodactyl\Models\Server; -use League\Fractal\TransformerAbstract; - -class ServerTransformer extends TransformerAbstract -{ - /** - * List of resources that can be included. - * - * @var array - */ - protected $availableIncludes = [ - 'allocations', - 'subusers', - 'stats', - ]; - - /** - * Return a generic transformed server array. - * - * @return array - */ - public function transform(Server $server) - { - return [ - 'id' => $server->uuidShort, - 'uuid' => $server->uuid, - 'name' => $server->name, - 'description' => $server->description, - 'node' => $server->node->name, - 'limits' => [ - 'memory' => $server->memory, - 'swap' => $server->swap, - 'disk' => $server->disk, - 'io' => $server->io, - 'cpu' => $server->cpu, - 'oom_disabled' => (bool) $server->oom_disabled, - ], - ]; - } - - /** - * Return a generic array of allocations for this server. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeAllocations(Server $server) - { - $allocations = $server->allocations; - - return $this->collection($allocations, new AllocationTransformer($server), 'allocation'); - } - - /** - * Return a generic array of subusers for this server. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeSubusers(Server $server) - { - $server->load('subusers.permissions', 'subusers.user'); - - return $this->collection($server->subusers, new SubuserTransformer, 'subuser'); - } - - /** - * Return a generic array of allocations for this server. - * - * @return \Leauge\Fractal\Resource\Item - */ - public function includeStats(Server $server) - { - return $this->item($server->guzzleClient(), new StatsTransformer, 'stat'); - } -} diff --git a/app/Transformers/User/StatsTransformer.php b/app/Transformers/User/StatsTransformer.php deleted file mode 100644 index 2a2e1d5ec..000000000 --- a/app/Transformers/User/StatsTransformer.php +++ /dev/null @@ -1,48 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\User; - -use GuzzleHttp\Client; -use League\Fractal\TransformerAbstract; -use GuzzleHttp\Exception\ConnectException; - -class StatsTransformer extends TransformerAbstract -{ - /** - * Return a generic transformed subuser array. - * - * @return array - */ - public function transform(Client $client) - { - try { - $res = $client->request('GET', '/server', ['http_errors' => false]); - - if ($res->getStatusCode() !== 200) { - return [ - 'error' => 'Error: HttpResponseException. Recieved non-200 HTTP status code from daemon: ' . $res->statusCode(), - ]; - } - - $json = json_decode($res->getBody()); - - return [ - 'id' => 1, - 'status' => $json->status, - 'resources' => $json->proc, - ]; - } catch (ConnectException $ex) { - return [ - 'error' => 'Error: ConnectException. Unable to contact the daemon to request server status.', - 'exception' => (config('app.debug')) ? $ex->getMessage() : null, - ]; - } - } -} diff --git a/app/Transformers/User/SubuserTransformer.php b/app/Transformers/User/SubuserTransformer.php deleted file mode 100644 index faac5965c..000000000 --- a/app/Transformers/User/SubuserTransformer.php +++ /dev/null @@ -1,32 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\User; - -use Pterodactyl\Models\Subuser; -use League\Fractal\TransformerAbstract; - -class SubuserTransformer extends TransformerAbstract -{ - /** - * Return a generic transformed subuser array. - * - * @return array - */ - public function transform(Subuser $subuser) - { - return [ - 'id' => $subuser->id, - 'username' => $subuser->user->username, - 'email' => $subuser->user->email, - '2fa' => (bool) $subuser->user->use_totp, - 'permissions' => $subuser->permissions->pluck('permission'), - ]; - } -} diff --git a/routes/api-client.php b/routes/api-client.php index fa4cb6c31..b1f4c1b1b 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -21,8 +21,8 @@ Route::get('/', 'ClientController@index')->name('api.client.index'); | */ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateClientAccess::class]], function () { - Route::get('/', 'Server\ServerController@index')->name('api.client.servers.view'); + Route::get('/', 'Servers\ServerController@index')->name('api.client.servers.view'); - Route::post('/command', 'Server\CommandController@index')->name('api.client.servers.command'); - Route::post('/power', 'Server\PowerController@index')->name('api.client.servers.power'); + Route::post('/command', 'Servers\CommandController@index')->name('api.client.servers.command'); + Route::post('/power', 'Servers\PowerController@index')->name('api.client.servers.power'); }); From 4e12c289ed6bd362314108fac7f4148794dba590 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 27 Feb 2018 22:09:34 -0600 Subject: [PATCH 06/25] Add command sending --- .../Api/Client/Servers/CommandController.php | 74 +++++++++++++++++++ .../Api/Client/Servers/SendCommandRequest.php | 30 ++++++++ app/Repositories/Daemon/CommandRepository.php | 1 - 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/Api/Client/Servers/CommandController.php create mode 100644 app/Http/Requests/Api/Client/Servers/SendCommandRequest.php diff --git a/app/Http/Controllers/Api/Client/Servers/CommandController.php b/app/Http/Controllers/Api/Client/Servers/CommandController.php new file mode 100644 index 000000000..8a5b951f5 --- /dev/null +++ b/app/Http/Controllers/Api/Client/Servers/CommandController.php @@ -0,0 +1,74 @@ +keyProviderService = $keyProviderService; + $this->repository = $repository; + } + + /** + * Send a command to a running server. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\SendCommandRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + */ + public function index(SendCommandRequest $request): Response + { + $server = $request->getModel(Server::class); + $token = $this->keyProviderService->handle($server, $request->user()); + + try { + $this->repository->setServer($server) + ->setToken($token) + ->send($request->input('command')); + } catch (RequestException $exception) { + if ($exception instanceof ClientException) { + if ($exception->getResponse() instanceof ResponseInterface && $exception->getResponse()->getStatusCode() === 412) { + throw new PreconditionFailedHttpException('Server is not online.'); + } + } + + throw new DaemonConnectionException($exception); + } + + return $this->returnNoContent(); + } +} diff --git a/app/Http/Requests/Api/Client/Servers/SendCommandRequest.php b/app/Http/Requests/Api/Client/Servers/SendCommandRequest.php new file mode 100644 index 000000000..68757658a --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/SendCommandRequest.php @@ -0,0 +1,30 @@ +user()->can('send-command', $this->getModel(Server::class)); + } + + /** + * Rules to validate this request aganist. + * + * @return array + */ + public function rules(): array + { + return [ + 'command' => 'string|min:1', + ]; + } +} diff --git a/app/Repositories/Daemon/CommandRepository.php b/app/Repositories/Daemon/CommandRepository.php index 31cb6b9b7..7b7577b32 100644 --- a/app/Repositories/Daemon/CommandRepository.php +++ b/app/Repositories/Daemon/CommandRepository.php @@ -12,7 +12,6 @@ class CommandRepository extends BaseRepository implements CommandRepositoryInter * * @param string $command * @return \Psr\Http\Message\ResponseInterface - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function send(string $command): ResponseInterface { From 2017e640b662a6d60510ac70d77e24f8d044cca0 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 28 Feb 2018 22:51:04 -0600 Subject: [PATCH 07/25] Add client API --- .../Api/Client/Servers/PowerController.php | 57 +++++++++++++++++++ .../Api/Client/Servers/SendCommandRequest.php | 2 +- .../Api/Client/Servers/SendPowerRequest.php | 31 ++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/Api/Client/Servers/PowerController.php create mode 100644 app/Http/Requests/Api/Client/Servers/SendPowerRequest.php diff --git a/app/Http/Controllers/Api/Client/Servers/PowerController.php b/app/Http/Controllers/Api/Client/Servers/PowerController.php new file mode 100644 index 000000000..113b83398 --- /dev/null +++ b/app/Http/Controllers/Api/Client/Servers/PowerController.php @@ -0,0 +1,57 @@ +keyProviderService = $keyProviderService; + $this->repository = $repository; + } + + /** + * Send a power action to a server. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\SendPowerRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Repository\Daemon\InvalidPowerSignalException + */ + public function index(SendPowerRequest $request): Response + { + $server = $request->getModel(Server::class); + $token = $this->keyProviderService->handle($server, $request->user()); + + $this->repository->setServer($server)->setToken($token)->sendSignal($request->input('signal')); + + return $this->returnNoContent(); + } +} diff --git a/app/Http/Requests/Api/Client/Servers/SendCommandRequest.php b/app/Http/Requests/Api/Client/Servers/SendCommandRequest.php index 68757658a..788f97739 100644 --- a/app/Http/Requests/Api/Client/Servers/SendCommandRequest.php +++ b/app/Http/Requests/Api/Client/Servers/SendCommandRequest.php @@ -24,7 +24,7 @@ class SendCommandRequest extends GetServerRequest public function rules(): array { return [ - 'command' => 'string|min:1', + 'command' => 'required|string|min:1', ]; } } diff --git a/app/Http/Requests/Api/Client/Servers/SendPowerRequest.php b/app/Http/Requests/Api/Client/Servers/SendPowerRequest.php new file mode 100644 index 000000000..19614d182 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/SendPowerRequest.php @@ -0,0 +1,31 @@ +user()->can('power-' . $this->input('signal', '_undefined'), $this->getModel(Server::class)); + } + + /** + * Rules to validate this request aganist. + * + * @return array + */ + public function rules(): array + { + return [ + 'signal' => 'required|string|in:start,stop,restart,kill', + ]; + } +} From 9b93629f456472c3207880ef9174216936cfe417 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 28 Feb 2018 23:30:39 -0600 Subject: [PATCH 08/25] Add UI for client API keys --- CHANGELOG.md | 3 + .../Controllers/Base/ClientApiController.php | 109 ++++++++++++++++++ .../Base/CreateClientApiKeyRequest.php | 21 ++++ public/js/laroute.js | 2 +- .../pterodactyl/admin/api/new.blade.php | 2 +- .../pterodactyl/base/api/index.blade.php | 69 ++++++----- .../themes/pterodactyl/base/api/new.blade.php | 67 +++++------ .../pterodactyl/layouts/master.blade.php | 10 +- routes/base.php | 17 ++- 9 files changed, 215 insertions(+), 85 deletions(-) create mode 100644 app/Http/Controllers/Base/ClientApiController.php create mode 100644 app/Http/Requests/Base/CreateClientApiKeyRequest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a3e47a3b1..054bfb0c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * Fixes a bug when reinstalling a server that would not mark the server as installing, resulting in some UI issues. * Handle 404 errors from missing models in the application API bindings correctly. +### Added +* Adds back client API for sending commands or power toggles to a server though the Panel API: `/api/client/servers/` + ## v0.7.3 (Derelict Dermodactylus) ### Fixed * Fixes server creation API endpoint not passing the provided `external_id` to the creation service. diff --git a/app/Http/Controllers/Base/ClientApiController.php b/app/Http/Controllers/Base/ClientApiController.php new file mode 100644 index 000000000..a74c28db8 --- /dev/null +++ b/app/Http/Controllers/Base/ClientApiController.php @@ -0,0 +1,109 @@ +alert = $alert; + $this->creationService = $creationService; + $this->repository = $repository; + } + + /** + * Return all of the API keys available to this user. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\View\View + */ + public function index(Request $request): View + { + return view('base.api.index', [ + 'keys' => $this->repository->getAccountKeys($request->user()), + ]); + } + + /** + * Render UI to allow creation of an API key. + * + * @return \Illuminate\View\View + */ + public function create(): View + { + return view('base.api.new'); + } + + /** + * Create the API key and return the user to the key listing page. + * + * @param \Pterodactyl\Http\Requests\Base\CreateClientApiKeyRequest $request + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function store(CreateClientApiKeyRequest $request): RedirectResponse + { + $allowedIps = null; + if (! is_null($request->input('allowed_ips'))) { + $allowedIps = json_encode(explode(PHP_EOL, $request->input('allowed_ips'))); + } + + $this->creationService->setKeyType(ApiKey::TYPE_ACCOUNT)->handle([ + 'memo' => $request->input('memo'), + 'allowed_ips' => $allowedIps, + 'user_id' => $request->user()->id, + ]); + + $this->alert->success('A new client API key has been generated for your account.')->flash(); + + return redirect()->route('account.api'); + } + + /** + * Delete a client's API key from the panel. + * + * @param \Illuminate\Http\Request $request + * @param $identifier + * @return \Illuminate\Http\Response + */ + public function delete(Request $request, $identifier): Response + { + $this->repository->deleteAccountKey($request->user(), $identifier); + + return response('', 204); + } +} diff --git a/app/Http/Requests/Base/CreateClientApiKeyRequest.php b/app/Http/Requests/Base/CreateClientApiKeyRequest.php new file mode 100644 index 000000000..b8e7bbfe2 --- /dev/null +++ b/app/Http/Requests/Base/CreateClientApiKeyRequest.php @@ -0,0 +1,21 @@ + 'required|string|max:255', + 'allowed_ips' => 'nullable|string', + ]; + } +} diff --git a/public/js/laroute.js b/public/js/laroute.js index 05740f254..c0e77614d 100644 --- a/public/js/laroute.js +++ b/public/js/laroute.js @@ -6,7 +6,7 @@ absolute: false, rootUrl: 'http://pterodactyl.local', - routes : [{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/open","name":"debugbar.openhandler","action":"Barryvdh\Debugbar\Controllers\OpenHandlerController@handle"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/clockwork\/{id}","name":"debugbar.clockwork","action":"Barryvdh\Debugbar\Controllers\OpenHandlerController@clockwork"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/assets\/stylesheets","name":"debugbar.assets.css","action":"Barryvdh\Debugbar\Controllers\AssetController@css"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/assets\/javascript","name":"debugbar.assets.js","action":"Barryvdh\Debugbar\Controllers\AssetController@js"},{"host":null,"methods":["DELETE"],"uri":"_debugbar\/cache\/{key}\/{tags?}","name":"debugbar.cache.delete","action":"Barryvdh\Debugbar\Controllers\CacheController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"\/","name":"index","action":"Pterodactyl\Http\Controllers\Base\IndexController@getIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"status\/{server}","name":"index.status","action":"Pterodactyl\Http\Controllers\Base\IndexController@status"},{"host":null,"methods":["GET","HEAD"],"uri":"account","name":"account","action":"Pterodactyl\Http\Controllers\Base\AccountController@index"},{"host":null,"methods":["POST"],"uri":"account","name":null,"action":"Pterodactyl\Http\Controllers\Base\AccountController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/security","name":"account.security","action":"Pterodactyl\Http\Controllers\Base\SecurityController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/security\/revoke\/{id}","name":"account.security.revoke","action":"Pterodactyl\Http\Controllers\Base\SecurityController@revoke"},{"host":null,"methods":["PUT"],"uri":"account\/security\/totp","name":"account.security.totp","action":"Pterodactyl\Http\Controllers\Base\SecurityController@generateTotp"},{"host":null,"methods":["POST"],"uri":"account\/security\/totp","name":"account.security.totp.set","action":"Pterodactyl\Http\Controllers\Base\SecurityController@setTotp"},{"host":null,"methods":["DELETE"],"uri":"account\/security\/totp","name":"account.security.totp.disable","action":"Pterodactyl\Http\Controllers\Base\SecurityController@disableTotp"},{"host":null,"methods":["GET","HEAD"],"uri":"admin","name":"admin.index","action":"Pterodactyl\Http\Controllers\Admin\BaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/api","name":"admin.api.index","action":"Pterodactyl\Http\Controllers\Admin\ApiController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/api\/new","name":"admin.api.new","action":"Pterodactyl\Http\Controllers\Admin\ApiController@create"},{"host":null,"methods":["POST"],"uri":"admin\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ApiController@store"},{"host":null,"methods":["DELETE"],"uri":"admin\/api\/revoke\/{identifier}","name":"admin.api.delete","action":"Pterodactyl\Http\Controllers\Admin\ApiController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/locations","name":"admin.locations","action":"Pterodactyl\Http\Controllers\Admin\LocationController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/locations\/view\/{location}","name":"admin.locations.view","action":"Pterodactyl\Http\Controllers\Admin\LocationController@view"},{"host":null,"methods":["POST"],"uri":"admin\/locations","name":null,"action":"Pterodactyl\Http\Controllers\Admin\LocationController@create"},{"host":null,"methods":["PATCH"],"uri":"admin\/locations\/view\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\LocationController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/databases","name":"admin.databases","action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/databases\/view\/{host}","name":"admin.databases.view","action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@view"},{"host":null,"methods":["POST"],"uri":"admin\/databases","name":null,"action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@create"},{"host":null,"methods":["PATCH"],"uri":"admin\/databases\/view\/{host}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/databases\/view\/{host}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings","name":"admin.settings","action":"Pterodactyl\Http\Controllers\Admin\Settings\IndexController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings\/mail","name":"admin.settings.mail","action":"Pterodactyl\Http\Controllers\Admin\Settings\MailController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings\/advanced","name":"admin.settings.advanced","action":"Pterodactyl\Http\Controllers\Admin\Settings\AdvancedController@index"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\IndexController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings\/mail","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\MailController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings\/advanced","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\AdvancedController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users","name":"admin.users","action":"Pterodactyl\Http\Controllers\Admin\UserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/accounts.json","name":"admin.users.json","action":"Pterodactyl\Http\Controllers\Admin\UserController@json"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/new","name":"admin.users.new","action":"Pterodactyl\Http\Controllers\Admin\UserController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/view\/{user}","name":"admin.users.view","action":"Pterodactyl\Http\Controllers\Admin\UserController@view"},{"host":null,"methods":["POST"],"uri":"admin\/users\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@store"},{"host":null,"methods":["PATCH"],"uri":"admin\/users\/view\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/users\/view\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers","name":"admin.servers","action":"Pterodactyl\Http\Controllers\Admin\ServersController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/new","name":"admin.servers.new","action":"Pterodactyl\Http\Controllers\Admin\ServersController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}","name":"admin.servers.view","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/details","name":"admin.servers.view.details","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDetails"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/build","name":"admin.servers.view.build","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewBuild"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/startup","name":"admin.servers.view.startup","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewStartup"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/database","name":"admin.servers.view.database","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDatabase"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/manage","name":"admin.servers.view.manage","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewManage"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/delete","name":"admin.servers.view.delete","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDelete"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@store"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/build","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@updateBuild"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/startup","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@saveStartup"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/database","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@newDatabase"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/toggle","name":"admin.servers.view.manage.toggle","action":"Pterodactyl\Http\Controllers\Admin\ServersController@toggleInstall"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/rebuild","name":"admin.servers.view.manage.rebuild","action":"Pterodactyl\Http\Controllers\Admin\ServersController@rebuildContainer"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/suspension","name":"admin.servers.view.manage.suspension","action":"Pterodactyl\Http\Controllers\Admin\ServersController@manageSuspension"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/reinstall","name":"admin.servers.view.manage.reinstall","action":"Pterodactyl\Http\Controllers\Admin\ServersController@reinstallServer"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/delete","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@delete"},{"host":null,"methods":["PATCH"],"uri":"admin\/servers\/view\/{server}\/details","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@setDetails"},{"host":null,"methods":["PATCH"],"uri":"admin\/servers\/view\/{server}\/database","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@resetDatabasePassword"},{"host":null,"methods":["DELETE"],"uri":"admin\/servers\/view\/{server}\/database\/{database}\/delete","name":"admin.servers.view.database.delete","action":"Pterodactyl\Http\Controllers\Admin\ServersController@deleteDatabase"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes","name":"admin.nodes","action":"Pterodactyl\Http\Controllers\Admin\NodesController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/new","name":"admin.nodes.new","action":"Pterodactyl\Http\Controllers\Admin\NodesController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}","name":"admin.nodes.view","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/settings","name":"admin.nodes.view.settings","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewSettings"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/configuration","name":"admin.nodes.view.configuration","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewConfiguration"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/allocation","name":"admin.nodes.view.allocation","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewAllocation"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/servers","name":"admin.nodes.view.servers","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewServers"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/settings\/token","name":"admin.nodes.view.configuration.token","action":"Pterodactyl\Http\Controllers\Admin\NodesController@setToken"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@store"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@createAllocation"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation\/remove","name":"admin.nodes.view.allocation.removeBlock","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationRemoveBlock"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation\/alias","name":"admin.nodes.view.allocation.setAlias","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationSetAlias"},{"host":null,"methods":["PATCH"],"uri":"admin\/nodes\/view\/{node}\/settings","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@updateSettings"},{"host":null,"methods":["DELETE"],"uri":"admin\/nodes\/view\/{node}\/delete","name":"admin.nodes.view.delete","action":"Pterodactyl\Http\Controllers\Admin\NodesController@delete"},{"host":null,"methods":["DELETE"],"uri":"admin\/nodes\/view\/{node}\/allocation\/remove\/{allocation}","name":"admin.nodes.view.allocation.removeSingle","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationRemoveSingle"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests","name":"admin.nests","action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/new","name":"admin.nests.new","action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/view\/{nest}","name":"admin.nests.view","action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/new","name":"admin.nests.egg.new","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}","name":"admin.nests.egg.view","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}\/export","name":"admin.nests.egg.export","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggShareController@export"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}\/variables","name":"admin.nests.egg.variables","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}\/scripts","name":"admin.nests.egg.scripts","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggScriptController@index"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@store"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/import","name":"admin.nests.egg.import","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggShareController@import"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/egg\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@store"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/egg\/{egg}\/variables","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@store"},{"host":null,"methods":["PUT"],"uri":"admin\/nests\/egg\/{egg}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggShareController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/view\/{nest}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/egg\/{egg}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/egg\/{egg}\/scripts","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggScriptController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/egg\/{egg}\/variables\/{variable}","name":"admin.nests.egg.variables.edit","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/nests\/view\/{nest}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@destroy"},{"host":null,"methods":["DELETE"],"uri":"admin\/nests\/egg\/{egg}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@destroy"},{"host":null,"methods":["DELETE"],"uri":"admin\/nests\/egg\/{egg}\/variables\/{variable}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@destroy"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs","name":"admin.packs","action":"Pterodactyl\Http\Controllers\Admin\PackController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/new","name":"admin.packs.new","action":"Pterodactyl\Http\Controllers\Admin\PackController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/new\/template","name":"admin.packs.new.template","action":"Pterodactyl\Http\Controllers\Admin\PackController@newTemplate"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/view\/{pack}","name":"admin.packs.view","action":"Pterodactyl\Http\Controllers\Admin\PackController@view"},{"host":null,"methods":["POST"],"uri":"admin\/packs\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@store"},{"host":null,"methods":["POST"],"uri":"admin\/packs\/view\/{pack}\/export\/{files?}","name":"admin.packs.view.export","action":"Pterodactyl\Http\Controllers\Admin\PackController@export"},{"host":null,"methods":["PATCH"],"uri":"admin\/packs\/view\/{pack}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/packs\/view\/{pack}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@destroy"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/login","name":"auth.login","action":"Pterodactyl\Http\Controllers\Auth\LoginController@showLoginForm"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/login\/totp","name":"auth.totp","action":"Pterodactyl\Http\Controllers\Auth\LoginController@totp"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/password","name":"auth.password","action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestForm"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/password\/reset\/{token}","name":"auth.reset","action":"Pterodactyl\Http\Controllers\Auth\ResetPasswordController@showResetForm"},{"host":null,"methods":["POST"],"uri":"auth\/login","name":null,"action":"Pterodactyl\Http\Controllers\Auth\LoginController@login"},{"host":null,"methods":["POST"],"uri":"auth\/login\/totp","name":null,"action":"Pterodactyl\Http\Controllers\Auth\LoginController@loginUsingTotp"},{"host":null,"methods":["POST"],"uri":"auth\/password","name":null,"action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail"},{"host":null,"methods":["POST"],"uri":"auth\/password\/reset","name":"auth.reset.post","action":"Pterodactyl\Http\Controllers\Auth\ResetPasswordController@reset"},{"host":null,"methods":["POST"],"uri":"auth\/password\/reset\/{token}","name":null,"action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/logout","name":"auth.logout","action":"Pterodactyl\Http\Controllers\Auth\LoginController@logout"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}","name":"server.index","action":"Pterodactyl\Http\Controllers\Server\ConsoleController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/console","name":"server.console","action":"Pterodactyl\Http\Controllers\Server\ConsoleController@console"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/allocation","name":"server.settings.allocation","action":"Pterodactyl\Http\Controllers\Server\Settings\AllocationController@index"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/settings\/allocation","name":null,"action":"Pterodactyl\Http\Controllers\Server\Settings\AllocationController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/sftp","name":"server.settings.sftp","action":"Pterodactyl\Http\Controllers\Server\Settings\SftpController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/startup","name":"server.settings.startup","action":"Pterodactyl\Http\Controllers\Server\Settings\StartupController@index"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/settings\/startup","name":null,"action":"Pterodactyl\Http\Controllers\Server\Settings\StartupController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/databases","name":"server.databases.index","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@index"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/databases\/password","name":"server.databases.password","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files","name":"server.files.index","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/add","name":"server.files.add","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/edit\/{file}","name":"server.files.edit","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/download\/{file}","name":"server.files.edit","action":"Pterodactyl\Http\Controllers\Server\Files\DownloadController@index"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/files\/directory-list","name":"server.files.directory-list","action":"Pterodactyl\Http\Controllers\Server\Files\RemoteRequestController@directory"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/files\/save","name":"server.files.save","action":"Pterodactyl\Http\Controllers\Server\Files\RemoteRequestController@store"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users","name":"server.subusers","action":"Pterodactyl\Http\Controllers\Server\SubuserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users\/new","name":"server.subusers.new","action":"Pterodactyl\Http\Controllers\Server\SubuserController@create"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/users\/new","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@store"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users\/view\/{subuser}","name":"server.subusers.view","action":"Pterodactyl\Http\Controllers\Server\SubuserController@view"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/users\/view\/{subuser}","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@update"},{"host":null,"methods":["DELETE"],"uri":"server\/{server}\/users\/view\/{subuser}","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules","name":"server.schedules","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules\/new","name":"server.schedules.new","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@create"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/new","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@store"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":"server.schedules.view","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@view"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@update"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/view\/{schedule}\/toggle","name":"server.schedules.toggle","action":"Pterodactyl\Http\Controllers\Server\Tasks\ActionController@toggle"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/view\/{schedule}\/trigger","name":"server.schedules.trigger","action":"Pterodactyl\Http\Controllers\Server\Tasks\ActionController@trigger"},{"host":null,"methods":["DELETE"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users","name":"api.application.users","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users\/{user}","name":"api.application.users.view","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users\/external\/{external_id}","name":"api.application.users.external","action":"Pterodactyl\Http\Controllers\Api\Application\Users\ExternalUserController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/users","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/users\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/users\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes","name":"api.application.nodes","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes\/{node}","name":"api.application.nodes.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/nodes","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/nodes\/{node}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/nodes\/{node}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes\/{node}\/allocations","name":"api.application.allocations","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/nodes\/{node}\/allocations","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@store"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/nodes\/{node}\/allocations\/{allocation}","name":"api.application.allocations.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/locations","name":"api.applications.locations","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/locations\/{location}","name":"api.application.locations.view","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/locations","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/locations\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/locations\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers","name":"api.application.servers","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}","name":"api.application.servers.view","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@view"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/details","name":"api.application.servers.details","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController@details"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/build","name":"api.application.servers.build","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController@build"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/startup","name":"api.application.servers.startup","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\StartupController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@store"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/suspend","name":"api.application.servers.suspend","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@suspend"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/unsuspend","name":"api.application.servers.unsuspend","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@unsuspend"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/reinstall","name":"api.application.servers.reinstall","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@reinstall"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/rebuild","name":"api.application.servers.rebuild","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@rebuild"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@delete"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}\/{force?}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}\/databases","name":"api.application.servers.databases","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}\/databases\/{database}","name":"api.application.servers.databases.view","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/databases","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@store"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/databases\/{database}\/reset-password","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@resetPassword"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}\/databases\/{database}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests","name":"api.application.nests","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\NestController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}","name":"api.application.nests.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\NestController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}\/eggs","name":"api.application.nests.eggs","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\EggController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}\/eggs\/{egg}","name":"api.application.nests.eggs.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\EggController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/authenticate\/{token}","name":"api.remote.authenticate","action":"Pterodactyl\Http\Controllers\Api\Remote\ValidateKeyController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/eggs","name":"api.remote.eggs","action":"Pterodactyl\Http\Controllers\Api\Remote\EggRetrievalController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/eggs\/{uuid}","name":"api.remote.eggs.download","action":"Pterodactyl\Http\Controllers\Api\Remote\EggRetrievalController@download"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/scripts\/{uuid}","name":"api.remote.scripts","action":"Pterodactyl\Http\Controllers\Api\Remote\EggInstallController@index"},{"host":null,"methods":["POST"],"uri":"api\/remote\/sftp","name":"api.remote.sftp","action":"Pterodactyl\Http\Controllers\Api\Remote\SftpController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/packs\/pull\/{uuid}","name":"daemon.pack.pull","action":"Pterodactyl\Http\Controllers\Daemon\PackController@pull"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/packs\/pull\/{uuid}\/hash","name":"daemon.pack.hash","action":"Pterodactyl\Http\Controllers\Daemon\PackController@hash"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/configure\/{token}","name":"daemon.configuration","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@configuration"},{"host":null,"methods":["POST"],"uri":"daemon\/download","name":"daemon.download","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@authenticateDownload"},{"host":null,"methods":["POST"],"uri":"daemon\/install","name":"daemon.install","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@markInstall"}], + routes : [{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/open","name":"debugbar.openhandler","action":"Barryvdh\Debugbar\Controllers\OpenHandlerController@handle"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/clockwork\/{id}","name":"debugbar.clockwork","action":"Barryvdh\Debugbar\Controllers\OpenHandlerController@clockwork"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/assets\/stylesheets","name":"debugbar.assets.css","action":"Barryvdh\Debugbar\Controllers\AssetController@css"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/assets\/javascript","name":"debugbar.assets.js","action":"Barryvdh\Debugbar\Controllers\AssetController@js"},{"host":null,"methods":["DELETE"],"uri":"_debugbar\/cache\/{key}\/{tags?}","name":"debugbar.cache.delete","action":"Barryvdh\Debugbar\Controllers\CacheController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"\/","name":"index","action":"Pterodactyl\Http\Controllers\Base\IndexController@getIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"status\/{server}","name":"index.status","action":"Pterodactyl\Http\Controllers\Base\IndexController@status"},{"host":null,"methods":["GET","HEAD"],"uri":"account","name":"account","action":"Pterodactyl\Http\Controllers\Base\AccountController@index"},{"host":null,"methods":["POST"],"uri":"account","name":null,"action":"Pterodactyl\Http\Controllers\Base\AccountController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/api","name":"account.api","action":"Pterodactyl\Http\Controllers\Base\ClientApiController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/api\/new","name":"account.api.new","action":"Pterodactyl\Http\Controllers\Base\ClientApiController@create"},{"host":null,"methods":["POST"],"uri":"account\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Base\ClientApiController@store"},{"host":null,"methods":["DELETE"],"uri":"account\/api\/revoke\/{identifier}","name":"account.api.revoke","action":"Pterodactyl\Http\Controllers\Base\ClientApiController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/security","name":"account.security","action":"Pterodactyl\Http\Controllers\Base\SecurityController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/security\/revoke\/{id}","name":"account.security.revoke","action":"Pterodactyl\Http\Controllers\Base\SecurityController@revoke"},{"host":null,"methods":["PUT"],"uri":"account\/security\/totp","name":"account.security.totp","action":"Pterodactyl\Http\Controllers\Base\SecurityController@generateTotp"},{"host":null,"methods":["POST"],"uri":"account\/security\/totp","name":"account.security.totp.set","action":"Pterodactyl\Http\Controllers\Base\SecurityController@setTotp"},{"host":null,"methods":["DELETE"],"uri":"account\/security\/totp","name":"account.security.totp.disable","action":"Pterodactyl\Http\Controllers\Base\SecurityController@disableTotp"},{"host":null,"methods":["GET","HEAD"],"uri":"admin","name":"admin.index","action":"Pterodactyl\Http\Controllers\Admin\BaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/api","name":"admin.api.index","action":"Pterodactyl\Http\Controllers\Admin\ApiController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/api\/new","name":"admin.api.new","action":"Pterodactyl\Http\Controllers\Admin\ApiController@create"},{"host":null,"methods":["POST"],"uri":"admin\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ApiController@store"},{"host":null,"methods":["DELETE"],"uri":"admin\/api\/revoke\/{identifier}","name":"admin.api.delete","action":"Pterodactyl\Http\Controllers\Admin\ApiController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/locations","name":"admin.locations","action":"Pterodactyl\Http\Controllers\Admin\LocationController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/locations\/view\/{location}","name":"admin.locations.view","action":"Pterodactyl\Http\Controllers\Admin\LocationController@view"},{"host":null,"methods":["POST"],"uri":"admin\/locations","name":null,"action":"Pterodactyl\Http\Controllers\Admin\LocationController@create"},{"host":null,"methods":["PATCH"],"uri":"admin\/locations\/view\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\LocationController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/databases","name":"admin.databases","action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/databases\/view\/{host}","name":"admin.databases.view","action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@view"},{"host":null,"methods":["POST"],"uri":"admin\/databases","name":null,"action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@create"},{"host":null,"methods":["PATCH"],"uri":"admin\/databases\/view\/{host}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/databases\/view\/{host}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings","name":"admin.settings","action":"Pterodactyl\Http\Controllers\Admin\Settings\IndexController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings\/mail","name":"admin.settings.mail","action":"Pterodactyl\Http\Controllers\Admin\Settings\MailController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings\/advanced","name":"admin.settings.advanced","action":"Pterodactyl\Http\Controllers\Admin\Settings\AdvancedController@index"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\IndexController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings\/mail","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\MailController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings\/advanced","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\AdvancedController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users","name":"admin.users","action":"Pterodactyl\Http\Controllers\Admin\UserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/accounts.json","name":"admin.users.json","action":"Pterodactyl\Http\Controllers\Admin\UserController@json"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/new","name":"admin.users.new","action":"Pterodactyl\Http\Controllers\Admin\UserController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/view\/{user}","name":"admin.users.view","action":"Pterodactyl\Http\Controllers\Admin\UserController@view"},{"host":null,"methods":["POST"],"uri":"admin\/users\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@store"},{"host":null,"methods":["PATCH"],"uri":"admin\/users\/view\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/users\/view\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers","name":"admin.servers","action":"Pterodactyl\Http\Controllers\Admin\ServersController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/new","name":"admin.servers.new","action":"Pterodactyl\Http\Controllers\Admin\ServersController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}","name":"admin.servers.view","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/details","name":"admin.servers.view.details","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDetails"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/build","name":"admin.servers.view.build","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewBuild"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/startup","name":"admin.servers.view.startup","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewStartup"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/database","name":"admin.servers.view.database","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDatabase"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/manage","name":"admin.servers.view.manage","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewManage"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/delete","name":"admin.servers.view.delete","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDelete"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@store"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/build","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@updateBuild"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/startup","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@saveStartup"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/database","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@newDatabase"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/toggle","name":"admin.servers.view.manage.toggle","action":"Pterodactyl\Http\Controllers\Admin\ServersController@toggleInstall"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/rebuild","name":"admin.servers.view.manage.rebuild","action":"Pterodactyl\Http\Controllers\Admin\ServersController@rebuildContainer"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/suspension","name":"admin.servers.view.manage.suspension","action":"Pterodactyl\Http\Controllers\Admin\ServersController@manageSuspension"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/reinstall","name":"admin.servers.view.manage.reinstall","action":"Pterodactyl\Http\Controllers\Admin\ServersController@reinstallServer"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/delete","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@delete"},{"host":null,"methods":["PATCH"],"uri":"admin\/servers\/view\/{server}\/details","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@setDetails"},{"host":null,"methods":["PATCH"],"uri":"admin\/servers\/view\/{server}\/database","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@resetDatabasePassword"},{"host":null,"methods":["DELETE"],"uri":"admin\/servers\/view\/{server}\/database\/{database}\/delete","name":"admin.servers.view.database.delete","action":"Pterodactyl\Http\Controllers\Admin\ServersController@deleteDatabase"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes","name":"admin.nodes","action":"Pterodactyl\Http\Controllers\Admin\NodesController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/new","name":"admin.nodes.new","action":"Pterodactyl\Http\Controllers\Admin\NodesController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}","name":"admin.nodes.view","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/settings","name":"admin.nodes.view.settings","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewSettings"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/configuration","name":"admin.nodes.view.configuration","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewConfiguration"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/allocation","name":"admin.nodes.view.allocation","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewAllocation"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/servers","name":"admin.nodes.view.servers","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewServers"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/settings\/token","name":"admin.nodes.view.configuration.token","action":"Pterodactyl\Http\Controllers\Admin\NodesController@setToken"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@store"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@createAllocation"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation\/remove","name":"admin.nodes.view.allocation.removeBlock","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationRemoveBlock"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation\/alias","name":"admin.nodes.view.allocation.setAlias","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationSetAlias"},{"host":null,"methods":["PATCH"],"uri":"admin\/nodes\/view\/{node}\/settings","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@updateSettings"},{"host":null,"methods":["DELETE"],"uri":"admin\/nodes\/view\/{node}\/delete","name":"admin.nodes.view.delete","action":"Pterodactyl\Http\Controllers\Admin\NodesController@delete"},{"host":null,"methods":["DELETE"],"uri":"admin\/nodes\/view\/{node}\/allocation\/remove\/{allocation}","name":"admin.nodes.view.allocation.removeSingle","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationRemoveSingle"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests","name":"admin.nests","action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/new","name":"admin.nests.new","action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/view\/{nest}","name":"admin.nests.view","action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/new","name":"admin.nests.egg.new","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}","name":"admin.nests.egg.view","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}\/export","name":"admin.nests.egg.export","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggShareController@export"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}\/variables","name":"admin.nests.egg.variables","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}\/scripts","name":"admin.nests.egg.scripts","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggScriptController@index"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@store"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/import","name":"admin.nests.egg.import","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggShareController@import"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/egg\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@store"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/egg\/{egg}\/variables","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@store"},{"host":null,"methods":["PUT"],"uri":"admin\/nests\/egg\/{egg}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggShareController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/view\/{nest}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/egg\/{egg}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/egg\/{egg}\/scripts","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggScriptController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/egg\/{egg}\/variables\/{variable}","name":"admin.nests.egg.variables.edit","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/nests\/view\/{nest}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@destroy"},{"host":null,"methods":["DELETE"],"uri":"admin\/nests\/egg\/{egg}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@destroy"},{"host":null,"methods":["DELETE"],"uri":"admin\/nests\/egg\/{egg}\/variables\/{variable}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@destroy"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs","name":"admin.packs","action":"Pterodactyl\Http\Controllers\Admin\PackController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/new","name":"admin.packs.new","action":"Pterodactyl\Http\Controllers\Admin\PackController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/new\/template","name":"admin.packs.new.template","action":"Pterodactyl\Http\Controllers\Admin\PackController@newTemplate"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/view\/{pack}","name":"admin.packs.view","action":"Pterodactyl\Http\Controllers\Admin\PackController@view"},{"host":null,"methods":["POST"],"uri":"admin\/packs\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@store"},{"host":null,"methods":["POST"],"uri":"admin\/packs\/view\/{pack}\/export\/{files?}","name":"admin.packs.view.export","action":"Pterodactyl\Http\Controllers\Admin\PackController@export"},{"host":null,"methods":["PATCH"],"uri":"admin\/packs\/view\/{pack}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/packs\/view\/{pack}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@destroy"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/login","name":"auth.login","action":"Pterodactyl\Http\Controllers\Auth\LoginController@showLoginForm"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/login\/totp","name":"auth.totp","action":"Pterodactyl\Http\Controllers\Auth\LoginController@totp"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/password","name":"auth.password","action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestForm"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/password\/reset\/{token}","name":"auth.reset","action":"Pterodactyl\Http\Controllers\Auth\ResetPasswordController@showResetForm"},{"host":null,"methods":["POST"],"uri":"auth\/login","name":null,"action":"Pterodactyl\Http\Controllers\Auth\LoginController@login"},{"host":null,"methods":["POST"],"uri":"auth\/login\/totp","name":null,"action":"Pterodactyl\Http\Controllers\Auth\LoginController@loginUsingTotp"},{"host":null,"methods":["POST"],"uri":"auth\/password","name":null,"action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail"},{"host":null,"methods":["POST"],"uri":"auth\/password\/reset","name":"auth.reset.post","action":"Pterodactyl\Http\Controllers\Auth\ResetPasswordController@reset"},{"host":null,"methods":["POST"],"uri":"auth\/password\/reset\/{token}","name":null,"action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/logout","name":"auth.logout","action":"Pterodactyl\Http\Controllers\Auth\LoginController@logout"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}","name":"server.index","action":"Pterodactyl\Http\Controllers\Server\ConsoleController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/console","name":"server.console","action":"Pterodactyl\Http\Controllers\Server\ConsoleController@console"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/allocation","name":"server.settings.allocation","action":"Pterodactyl\Http\Controllers\Server\Settings\AllocationController@index"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/settings\/allocation","name":null,"action":"Pterodactyl\Http\Controllers\Server\Settings\AllocationController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/sftp","name":"server.settings.sftp","action":"Pterodactyl\Http\Controllers\Server\Settings\SftpController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/startup","name":"server.settings.startup","action":"Pterodactyl\Http\Controllers\Server\Settings\StartupController@index"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/settings\/startup","name":null,"action":"Pterodactyl\Http\Controllers\Server\Settings\StartupController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/databases","name":"server.databases.index","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@index"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/databases\/password","name":"server.databases.password","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files","name":"server.files.index","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/add","name":"server.files.add","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/edit\/{file}","name":"server.files.edit","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/download\/{file}","name":"server.files.edit","action":"Pterodactyl\Http\Controllers\Server\Files\DownloadController@index"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/files\/directory-list","name":"server.files.directory-list","action":"Pterodactyl\Http\Controllers\Server\Files\RemoteRequestController@directory"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/files\/save","name":"server.files.save","action":"Pterodactyl\Http\Controllers\Server\Files\RemoteRequestController@store"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users","name":"server.subusers","action":"Pterodactyl\Http\Controllers\Server\SubuserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users\/new","name":"server.subusers.new","action":"Pterodactyl\Http\Controllers\Server\SubuserController@create"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/users\/new","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@store"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users\/view\/{subuser}","name":"server.subusers.view","action":"Pterodactyl\Http\Controllers\Server\SubuserController@view"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/users\/view\/{subuser}","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@update"},{"host":null,"methods":["DELETE"],"uri":"server\/{server}\/users\/view\/{subuser}","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules","name":"server.schedules","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules\/new","name":"server.schedules.new","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@create"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/new","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@store"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":"server.schedules.view","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@view"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@update"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/view\/{schedule}\/toggle","name":"server.schedules.toggle","action":"Pterodactyl\Http\Controllers\Server\Tasks\ActionController@toggle"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/view\/{schedule}\/trigger","name":"server.schedules.trigger","action":"Pterodactyl\Http\Controllers\Server\Tasks\ActionController@trigger"},{"host":null,"methods":["DELETE"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users","name":"api.application.users","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users\/{user}","name":"api.application.users.view","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users\/external\/{external_id}","name":"api.application.users.external","action":"Pterodactyl\Http\Controllers\Api\Application\Users\ExternalUserController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/users","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/users\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/users\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes","name":"api.application.nodes","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes\/{node}","name":"api.application.nodes.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/nodes","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/nodes\/{node}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/nodes\/{node}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes\/{node}\/allocations","name":"api.application.allocations","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/nodes\/{node}\/allocations","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@store"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/nodes\/{node}\/allocations\/{allocation}","name":"api.application.allocations.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/locations","name":"api.applications.locations","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/locations\/{location}","name":"api.application.locations.view","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/locations","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/locations\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/locations\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers","name":"api.application.servers","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}","name":"api.application.servers.view","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/external\/{external_id}","name":"api.application.servers.external","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ExternalServerController@index"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/details","name":"api.application.servers.details","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController@details"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/build","name":"api.application.servers.build","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController@build"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/startup","name":"api.application.servers.startup","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\StartupController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@store"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/suspend","name":"api.application.servers.suspend","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@suspend"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/unsuspend","name":"api.application.servers.unsuspend","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@unsuspend"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/reinstall","name":"api.application.servers.reinstall","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@reinstall"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/rebuild","name":"api.application.servers.rebuild","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@rebuild"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@delete"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}\/{force?}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}\/databases","name":"api.application.servers.databases","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}\/databases\/{database}","name":"api.application.servers.databases.view","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/databases","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@store"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/databases\/{database}\/reset-password","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@resetPassword"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}\/databases\/{database}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests","name":"api.application.nests","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\NestController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}","name":"api.application.nests.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\NestController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}\/eggs","name":"api.application.nests.eggs","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\EggController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}\/eggs\/{egg}","name":"api.application.nests.eggs.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\EggController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/client","name":"api.client.index","action":"Pterodactyl\Http\Controllers\Api\Client\ClientController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/client\/servers\/{server}","name":"api.client.servers.view","action":"Pterodactyl\Http\Controllers\Api\Client\Servers\ServerController@index"},{"host":null,"methods":["POST"],"uri":"api\/client\/servers\/{server}\/command","name":"api.client.servers.command","action":"Pterodactyl\Http\Controllers\Api\Client\Servers\CommandController@index"},{"host":null,"methods":["POST"],"uri":"api\/client\/servers\/{server}\/power","name":"api.client.servers.power","action":"Pterodactyl\Http\Controllers\Api\Client\Servers\PowerController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/authenticate\/{token}","name":"api.remote.authenticate","action":"Pterodactyl\Http\Controllers\Api\Remote\ValidateKeyController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/eggs","name":"api.remote.eggs","action":"Pterodactyl\Http\Controllers\Api\Remote\EggRetrievalController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/eggs\/{uuid}","name":"api.remote.eggs.download","action":"Pterodactyl\Http\Controllers\Api\Remote\EggRetrievalController@download"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/scripts\/{uuid}","name":"api.remote.scripts","action":"Pterodactyl\Http\Controllers\Api\Remote\EggInstallController@index"},{"host":null,"methods":["POST"],"uri":"api\/remote\/sftp","name":"api.remote.sftp","action":"Pterodactyl\Http\Controllers\Api\Remote\SftpController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/packs\/pull\/{uuid}","name":"daemon.pack.pull","action":"Pterodactyl\Http\Controllers\Daemon\PackController@pull"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/packs\/pull\/{uuid}\/hash","name":"daemon.pack.hash","action":"Pterodactyl\Http\Controllers\Daemon\PackController@hash"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/configure\/{token}","name":"daemon.configuration","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@configuration"},{"host":null,"methods":["POST"],"uri":"daemon\/download","name":"daemon.download","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@authenticateDownload"},{"host":null,"methods":["POST"],"uri":"daemon\/install","name":"daemon.install","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@markInstall"}], prefix: '', route : function (name, parameters, route) { diff --git a/resources/themes/pterodactyl/admin/api/new.blade.php b/resources/themes/pterodactyl/admin/api/new.blade.php index b5876ee8c..b5db6a154 100644 --- a/resources/themes/pterodactyl/admin/api/new.blade.php +++ b/resources/themes/pterodactyl/admin/api/new.blade.php @@ -15,7 +15,7 @@ @section('content')
-
+
diff --git a/resources/themes/pterodactyl/base/api/index.blade.php b/resources/themes/pterodactyl/base/api/index.blade.php index e21e5aecc..24a717347 100644 --- a/resources/themes/pterodactyl/base/api/index.blade.php +++ b/resources/themes/pterodactyl/base/api/index.blade.php @@ -18,57 +18,70 @@ @endsection @section('content') -
-
-
-
-

@lang('base.api.index.list')

-
- +
+
+
+
+

Credentials List

+
-
-
- - +
+
- - - - + + + + - @foreach ($keys as $key) + @foreach($keys as $key) + - - - - + @endforeach - -
@lang('strings.memo')@lang('strings.public_key')KeyMemoLast UsedCreated
+ + •••••••• + + + {{ $key->memo }}{{ $key->identifier . decrypt($key->token) }} @if(!is_null($key->last_used_at)) @datetimeHuman($key->last_used_at) - @else + @else — @endif - + @datetimeHuman($key->created_at) + + +
+ +
-
@endsection @section('footer-scripts') @parent -@endsection - @section('content') -
-
-
-
-
@lang('base.api.new.form_title')
-
-
-
-
- - -

@lang('base.api.new.descriptive_memo.description')

-
-
- - -

@lang('base.api.new.allowed_ips.description')

-
-
-
-
- {!! csrf_field() !!} - + +
+
+
+
+ +
+

Set an easy to understand description for this API key to help you identify it later on.

-
+
+
+
+
+ + +
+

If you would like to limit this API key to specific IP addresses enter them above, one per line. CIDR notation is allowed for each IP address. Leave blank to allow any IP address.

+
+ +
+
+
- @endsection diff --git a/resources/themes/pterodactyl/layouts/master.blade.php b/resources/themes/pterodactyl/layouts/master.blade.php index 39e5a9dd6..060c76ced 100644 --- a/resources/themes/pterodactyl/layouts/master.blade.php +++ b/resources/themes/pterodactyl/layouts/master.blade.php @@ -101,11 +101,11 @@ @lang('navigation.account.security_controls') - {{--
  • --}} - {{----}} - {{-- @lang('navigation.account.api_access')--}} - {{----}} - {{--
  • --}} +
  • + + @lang('navigation.account.api_access') + +
  • @lang('navigation.account.my_servers') diff --git a/routes/base.php b/routes/base.php index 3947b7551..4955afd42 100644 --- a/routes/base.php +++ b/routes/base.php @@ -30,16 +30,15 @@ Route::group(['prefix' => 'account'], function () { | | Endpoint: /account/api | -| Temporarily Disabled */ -//Route::group(['prefix' => 'account/api'], function () { -// Route::get('/', 'AccountKeyController@index')->name('account.api'); -// Route::get('/new', 'AccountKeyController@create')->name('account.api.new'); -// -// Route::post('/new', 'AccountKeyController@store'); -// -// Route::delete('/revoke/{identifier}', 'AccountKeyController@revoke')->name('account.api.revoke'); -//}); +Route::group(['prefix' => 'account/api'], function () { + Route::get('/', 'ClientApiController@index')->name('account.api'); + Route::get('/new', 'ClientApiController@create')->name('account.api.new'); + + Route::post('/new', 'ClientApiController@store'); + + Route::delete('/revoke/{identifier}', 'ClientApiController@delete')->name('account.api.revoke'); +}); /* |-------------------------------------------------------------------------- From 8f72571895dc9d659b161f426d85f9b301a4c60f Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 28 Feb 2018 23:39:59 -0600 Subject: [PATCH 09/25] Fix IP access middleware --- app/Http/Middleware/Api/AuthenticateIPAccess.php | 4 ++-- .../Unit/Http/Middleware/API/AuthenticateIPAccessTest.php | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Http/Middleware/Api/AuthenticateIPAccess.php b/app/Http/Middleware/Api/AuthenticateIPAccess.php index 4815063d5..aed8f53a4 100644 --- a/app/Http/Middleware/Api/AuthenticateIPAccess.php +++ b/app/Http/Middleware/Api/AuthenticateIPAccess.php @@ -29,12 +29,12 @@ class AuthenticateIPAccess } $find = new IP($request->ip()); - foreach ($model->allowed_ips as $ip) { + foreach (json_decode($model->allowed_ips) as $ip) { if (Range::parse($ip)->contains($find)) { return $next($request); } } - throw new AccessDeniedHttpException('This IP address does not have permission to access the API using these credentials.'); + throw new AccessDeniedHttpException('This IP address (' . $request->ip() . ') does not have permission to access the API using these credentials.'); } } diff --git a/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php b/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php index 59b9137d9..babd95358 100644 --- a/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php +++ b/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php @@ -25,7 +25,7 @@ class AuthenticateIPAccessTest extends MiddlewareTestCase */ public function testWithValidIP() { - $model = factory(ApiKey::class)->make(['allowed_ips' => ['127.0.0.1']]); + $model = factory(ApiKey::class)->make(['allowed_ips' => '["127.0.0.1"]']); $this->setRequestAttribute('api_key', $model); $this->request->shouldReceive('ip')->withNoArgs()->once()->andReturn('127.0.0.1'); @@ -38,7 +38,7 @@ class AuthenticateIPAccessTest extends MiddlewareTestCase */ public function testValidIPAganistCIDRRange() { - $model = factory(ApiKey::class)->make(['allowed_ips' => ['192.168.1.1/28']]); + $model = factory(ApiKey::class)->make(['allowed_ips' => '["192.168.1.1/28"]']); $this->setRequestAttribute('api_key', $model); $this->request->shouldReceive('ip')->withNoArgs()->once()->andReturn('192.168.1.15'); @@ -54,10 +54,10 @@ class AuthenticateIPAccessTest extends MiddlewareTestCase */ public function testWithInvalidIP() { - $model = factory(ApiKey::class)->make(['allowed_ips' => ['127.0.0.1']]); + $model = factory(ApiKey::class)->make(['allowed_ips' => '["127.0.0.1"]']); $this->setRequestAttribute('api_key', $model); - $this->request->shouldReceive('ip')->withNoArgs()->once()->andReturn('127.0.0.2'); + $this->request->shouldReceive('ip')->withNoArgs()->twice()->andReturn('127.0.0.2'); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); } From 0a39a9b6bfefc0fc9b6a2b189bcdd7689e4b7991 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 1 Mar 2018 18:35:53 -0600 Subject: [PATCH 10/25] Don't require an environment variable to be present if none are required anyways, closes #1007 --- CHANGELOG.md | 1 + .../Requests/Api/Application/Servers/StoreServerRequest.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 054bfb0c6..eee443604 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. ### Fixed * Fixes a bug when reinstalling a server that would not mark the server as installing, resulting in some UI issues. * Handle 404 errors from missing models in the application API bindings correctly. +* Fix validation error returned when no environment variables are passed, even if there are no variables required. ### Added * Adds back client API for sending commands or power toggles to a server though the Panel API: `/api/client/servers/` diff --git a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php index 6d0d2ecf3..d12b738e6 100644 --- a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php +++ b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php @@ -39,7 +39,7 @@ class StoreServerRequest extends ApplicationApiRequest 'pack' => $rules['pack_id'], 'docker_image' => $rules['image'], 'startup' => $rules['startup'], - 'environment' => 'required|array', + 'environment' => 'present|array', 'skip_scripts' => 'sometimes|boolean', // Resource limitations From ab2973c4d1c62c0bfc5e34c111d8ac62c64304f7 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 1 Mar 2018 18:43:39 -0600 Subject: [PATCH 11/25] Add pack transformer support --- CHANGELOG.md | 1 + app/Providers/MacroServiceProvider.php | 44 +-------- .../Api/Application/PackTransformer.php | 92 +++++-------------- .../Api/Application/ServerTransformer.php | 59 ++++++++---- .../Api/Application/SubuserTransformer.php | 60 ------------ 5 files changed, 62 insertions(+), 194 deletions(-) delete mode 100644 app/Transformers/Api/Application/SubuserTransformer.php diff --git a/CHANGELOG.md b/CHANGELOG.md index eee443604..f0953dcdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. ### Added * Adds back client API for sending commands or power toggles to a server though the Panel API: `/api/client/servers/` +* Added proper transformer for Packs and re-enabled missing includes on server. ## v0.7.3 (Derelict Dermodactylus) ### Fixed diff --git a/app/Providers/MacroServiceProvider.php b/app/Providers/MacroServiceProvider.php index ddfbf7aa8..9eae42b81 100644 --- a/app/Providers/MacroServiceProvider.php +++ b/app/Providers/MacroServiceProvider.php @@ -1,21 +1,9 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Providers; -use File; -use Cache; -use Carbon; -use Request; -use Pterodactyl\Models\ApiKey; +use Illuminate\Support\Facades\File; use Illuminate\Support\ServiceProvider; -use Pterodactyl\Services\ApiKeyService; class MacroServiceProvider extends ServiceProvider { @@ -36,35 +24,5 @@ class MacroServiceProvider extends ServiceProvider return round($size, ($i < 2) ? 0 : $precision) . ' ' . $units[$i]; }); - - Request::macro('apiKey', function () { - if (! Request::bearerToken()) { - return false; - } - - $parts = explode('.', Request::bearerToken()); - - if (count($parts) === 2 && strlen($parts[0]) === ApiKeyService::PUB_CRYPTO_BYTES * 2) { - // Because the key itself isn't changing frequently, we simply cache this for - // 15 minutes to speed up the API and keep requests flowing. - return Cache::tags([ - 'ApiKeyMacro', - 'ApiKeyMacro:Key:' . $parts[0], - ])->remember('ApiKeyMacro.' . $parts[0], Carbon::now()->addMinutes(15), function () use ($parts) { - return ApiKey::where('public', $parts[0])->first(); - }); - } - - return false; - }); - - Request::macro('apiKeyHasPermission', function ($permission) { - $key = Request::apiKey(); - if (! $key) { - return false; - } - - return Request::user()->can($permission, $key); - }); } } diff --git a/app/Transformers/Api/Application/PackTransformer.php b/app/Transformers/Api/Application/PackTransformer.php index 973002ae8..e77bdd459 100644 --- a/app/Transformers/Api/Application/PackTransformer.php +++ b/app/Transformers/Api/Application/PackTransformer.php @@ -1,90 +1,40 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ -namespace Pterodactyl\Transformers\Admin; +namespace Pterodactyl\Transformers\Api\Application; -use Illuminate\Http\Request; use Pterodactyl\Models\Pack; -use League\Fractal\TransformerAbstract; -class PackTransformer extends TransformerAbstract +class PackTransformer extends BaseTransformer { /** - * List of resources that can be included. + * Return the resource name for the JSONAPI output. * - * @var array + * @return string */ - protected $availableIncludes = [ - 'option', - 'servers', - ]; - - /** - * The Illuminate Request object if provided. - * - * @var \Illuminate\Http\Request|bool - */ - protected $request; - - /** - * Setup request object for transformer. - * - * @param \Illuminate\Http\Request|bool $request - */ - public function __construct($request = false) + public function getResourceName(): string { - if (! $request instanceof Request && $request !== false) { - throw new DisplayException('Request passed to constructor must be of type Request or false.'); - } - - $this->request = $request; + return Pack::RESOURCE_NAME; } /** - * Return a generic transformed pack array. + * Return a transformed User model that can be consumed by external services. * + * @param \Pterodactyl\Models\Pack $pack * @return array */ - public function transform($pack) + public function transform(Pack $pack): array { - if (! $pack instanceof Pack) { - return ['id' => null]; - } - - return $pack->toArray(); - } - - /** - * Return the packs associated with this service. - * - * @return \Leauge\Fractal\Resource\Item - */ - public function includeOption(Pack $pack) - { - if ($this->request && ! $this->request->apiKeyHasPermission('option-view')) { - return; - } - - return $this->item($pack->option, new OptionTransformer($this->request), 'option'); - } - - /** - * Return the packs associated with this service. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeServers(Pack $pack) - { - if ($this->request && ! $this->request->apiKeyHasPermission('server-list')) { - return; - } - - return $this->collection($pack->servers, new ServerTransformer($this->request), 'server'); + return [ + 'id' => $pack->id, + 'uuid' => $pack->uuid, + 'egg' => $pack->egg_id, + 'name' => $pack->name, + 'description' => $pack->description, + 'is_selectable' => (bool) $pack->selectable, + 'is_visible' => (bool) $pack->visible, + 'is_locked' => (bool) $pack->locked, + 'created_at' => $this->formatTimestamp($pack->created_at), + 'updated_at' => $this->formatTimestamp($pack->updated_at), + ]; } } diff --git a/app/Transformers/Api/Application/ServerTransformer.php b/app/Transformers/Api/Application/ServerTransformer.php index 31eed90c5..6a003b4dc 100644 --- a/app/Transformers/Api/Application/ServerTransformer.php +++ b/app/Transformers/Api/Application/ServerTransformer.php @@ -116,6 +116,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeSubusers(Server $server) { @@ -133,6 +135,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeUser(Server $server) { @@ -150,40 +154,49 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ -// public function includePack(Server $server) -// { -// if (! $this->authorize(AdminAcl::RESOURCE_PACKS)) { -// return $this->null(); -// } -// -// $server->loadMissing('pack'); -// -// return $this->item($server->getRelation('pack'), $this->makeTransformer(PackTransformer::class), 'pack'); -// } + public function includePack(Server $server) + { + if (! $this->authorize(AdminAcl::RESOURCE_PACKS)) { + return $this->null(); + } + + $server->loadMissing('pack'); + if (is_null($server->getRelation('pack'))) { + return $this->null(); + } + + return $this->item($server->getRelation('pack'), $this->makeTransformer(PackTransformer::class), 'pack'); + } /** * Return a generic array with nest information for this server. * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ -// public function includeNest(Server $server) -// { -// if (! $this->authorize(AdminAcl::RESOURCE_NESTS)) { -// return $this->null(); -// } -// -// $server->loadMissing('nest'); -// -// return $this->item($server->getRelation('nest'), $this->makeTransformer(NestTransformer::class), 'nest'); -// } + public function includeNest(Server $server) + { + if (! $this->authorize(AdminAcl::RESOURCE_NESTS)) { + return $this->null(); + } + + $server->loadMissing('nest'); + + return $this->item($server->getRelation('nest'), $this->makeTransformer(NestTransformer::class), 'nest'); + } /** * Return a generic array with service option information for this server. * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeOption(Server $server) { @@ -201,6 +214,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeVariables(Server $server) { @@ -218,6 +233,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeLocation(Server $server) { @@ -235,6 +252,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeNode(Server $server) { diff --git a/app/Transformers/Api/Application/SubuserTransformer.php b/app/Transformers/Api/Application/SubuserTransformer.php deleted file mode 100644 index 93ed25d52..000000000 --- a/app/Transformers/Api/Application/SubuserTransformer.php +++ /dev/null @@ -1,60 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\Admin; - -use Illuminate\Http\Request; -use Pterodactyl\Models\Subuser; -use League\Fractal\TransformerAbstract; - -class SubuserTransformer extends TransformerAbstract -{ - /** - * The Illuminate Request object if provided. - * - * @var \Illuminate\Http\Request|bool - */ - protected $request; - - /** - * Setup request object for transformer. - * - * @param \Illuminate\Http\Request|bool $request - */ - public function __construct($request = false) - { - if (! $request instanceof Request && $request !== false) { - throw new DisplayException('Request passed to constructor must be of type Request or false.'); - } - - $this->request = $request; - } - - /** - * Return a generic transformed subuser array. - * - * @return array - */ - public function transform(Subuser $subuser) - { - if ($this->request && ! $this->request->apiKeyHasPermission('server-view')) { - return; - } - - return [ - 'id' => $subuser->id, - 'username' => $subuser->user->username, - 'email' => $subuser->user->email, - '2fa' => (bool) $subuser->user->use_totp, - 'permissions' => $subuser->permissions->pluck('permission'), - 'created_at' => $subuser->created_at, - 'updated_at' => $subuser->updated_at, - ]; - } -} From 838b9a9093bc3beb643cdc2cf5c9894eb10fb4c4 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 1 Mar 2018 18:46:59 -0600 Subject: [PATCH 12/25] Add support for filesystem caching, closes #993 --- .../Commands/Environment/AppSettingsCommand.php | 1 + app/Http/Controllers/Admin/NodesController.php | 2 +- app/Http/Controllers/Daemon/ActionController.php | 11 ++--------- .../Controllers/Server/Files/DownloadController.php | 2 +- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/app/Console/Commands/Environment/AppSettingsCommand.php b/app/Console/Commands/Environment/AppSettingsCommand.php index ecf171adc..17c2b5bbc 100644 --- a/app/Console/Commands/Environment/AppSettingsCommand.php +++ b/app/Console/Commands/Environment/AppSettingsCommand.php @@ -22,6 +22,7 @@ class AppSettingsCommand extends Command const ALLOWED_CACHE_DRIVERS = [ 'redis' => 'Redis (recommended)', 'memcached' => 'Memcached', + 'file' => 'Filesystem', ]; const ALLOWED_SESSION_DRIVERS = [ diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index a67abf9d1..0d0dd187b 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -366,7 +366,7 @@ class NodesController extends Controller public function setToken(Node $node) { $token = bin2hex(random_bytes(16)); - $this->cache->tags(['Node:Configuration'])->put($token, $node->id, 5); + $this->cache->put('Node:Configuration:' . $token, $node->id, 5); return response()->json(['token' => $token]); } diff --git a/app/Http/Controllers/Daemon/ActionController.php b/app/Http/Controllers/Daemon/ActionController.php index 1c67ec6cd..64c0a0c21 100644 --- a/app/Http/Controllers/Daemon/ActionController.php +++ b/app/Http/Controllers/Daemon/ActionController.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Http\Controllers\Daemon; @@ -25,7 +18,7 @@ class ActionController extends Controller */ public function authenticateDownload(Request $request) { - $download = Cache::tags(['Server:Downloads'])->pull($request->input('token')); + $download = Cache::pull('Server:Downloads:' . $request->input('token')); if (is_null($download)) { return response()->json([ @@ -78,7 +71,7 @@ class ActionController extends Controller */ public function configuration(Request $request, $token) { - $nodeId = Cache::tags(['Node:Configuration'])->pull($token); + $nodeId = Cache::pull('Node:Configuration:' . $token); if (is_null($nodeId)) { return response()->json(['error' => 'token_invalid'], 403); } diff --git a/app/Http/Controllers/Server/Files/DownloadController.php b/app/Http/Controllers/Server/Files/DownloadController.php index 79155a63a..06a31f9e5 100644 --- a/app/Http/Controllers/Server/Files/DownloadController.php +++ b/app/Http/Controllers/Server/Files/DownloadController.php @@ -48,7 +48,7 @@ class DownloadController extends Controller $token = str_random(40); $node = $server->getRelation('node'); - $this->cache->tags(['Server:Downloads'])->put($token, ['server' => $server->uuid, 'path' => $file], 5); + $this->cache->put('Server:Downloads:' . $token, ['server' => $server->uuid, 'path' => $file], 5); return redirect(sprintf('%s://%s:%s/v1/server/file/download/%s', $node->scheme, $node->fqdn, $node->daemonListen, $token)); } From 85bdbdce143145a7100b7a9bd5b0b1a01919edac Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 1 Mar 2018 19:19:19 -0600 Subject: [PATCH 13/25] Better handling of file download requests --- CHANGELOG.md | 1 + .../Api/Remote/FileDownloadController.php | 50 +++++++++++++++++++ .../Controllers/Daemon/ActionController.php | 22 -------- .../Server/Files/DownloadController.php | 4 +- routes/api-remote.php | 9 +--- routes/daemon.php | 1 - 6 files changed, 56 insertions(+), 31 deletions(-) create mode 100644 app/Http/Controllers/Api/Remote/FileDownloadController.php diff --git a/CHANGELOG.md b/CHANGELOG.md index f0953dcdd..9cd81951c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. ### Added * Adds back client API for sending commands or power toggles to a server though the Panel API: `/api/client/servers/` * Added proper transformer for Packs and re-enabled missing includes on server. +* Added support for using Filesystem as a caching driver, although not recommended. ## v0.7.3 (Derelict Dermodactylus) ### Fixed diff --git a/app/Http/Controllers/Api/Remote/FileDownloadController.php b/app/Http/Controllers/Api/Remote/FileDownloadController.php new file mode 100644 index 000000000..fa4818fc9 --- /dev/null +++ b/app/Http/Controllers/Api/Remote/FileDownloadController.php @@ -0,0 +1,50 @@ +cache = $cache; + } + + /** + * Handle a request to authenticate a download using a token and return + * the path of the file to the daemon. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + * + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + */ + public function index(Request $request): JsonResponse + { + $download = $this->cache->pull('Server:Downloads:' . $request->input('token', '')); + + if (is_null($download)) { + throw new NotFoundHttpException('No file was found using the token provided.'); + } + + return response()->json([ + 'path' => array_get($download, 'path'), + 'server' => array_get($download, 'server'), + ]); + } +} diff --git a/app/Http/Controllers/Daemon/ActionController.php b/app/Http/Controllers/Daemon/ActionController.php index 64c0a0c21..fef0b35b7 100644 --- a/app/Http/Controllers/Daemon/ActionController.php +++ b/app/Http/Controllers/Daemon/ActionController.php @@ -10,28 +10,6 @@ use Pterodactyl\Http\Controllers\Controller; class ActionController extends Controller { - /** - * Handles download request from daemon. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse - */ - public function authenticateDownload(Request $request) - { - $download = Cache::pull('Server:Downloads:' . $request->input('token')); - - if (is_null($download)) { - return response()->json([ - 'error' => 'An invalid request token was recieved with this request.', - ], 403); - } - - return response()->json([ - 'path' => $download['path'], - 'server' => $download['server'], - ]); - } - /** * Handles install toggle request from daemon. * diff --git a/app/Http/Controllers/Server/Files/DownloadController.php b/app/Http/Controllers/Server/Files/DownloadController.php index 06a31f9e5..04b16d084 100644 --- a/app/Http/Controllers/Server/Files/DownloadController.php +++ b/app/Http/Controllers/Server/Files/DownloadController.php @@ -9,6 +9,7 @@ namespace Pterodactyl\Http\Controllers\Server\Files; +use Ramsey\Uuid\Uuid; use Illuminate\Http\Request; use Illuminate\Cache\Repository; use Illuminate\Http\RedirectResponse; @@ -46,8 +47,9 @@ class DownloadController extends Controller $server = $request->attributes->get('server'); $this->authorize('download-files', $server); - $token = str_random(40); + $token = Uuid::uuid4()->toString(); $node = $server->getRelation('node'); + $this->cache->put('Server:Downloads:' . $token, ['server' => $server->uuid, 'path' => $file], 5); return redirect(sprintf('%s://%s:%s/v1/server/file/download/%s', $node->scheme, $node->fqdn, $node->daemonListen, $token)); diff --git a/routes/api-remote.php b/routes/api-remote.php index a06a72feb..5566651d4 100644 --- a/routes/api-remote.php +++ b/routes/api-remote.php @@ -1,12 +1,7 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ + Route::get('/authenticate/{token}', 'ValidateKeyController@index')->name('api.remote.authenticate'); +Route::post('/download-file', 'FileDownloadController@index')->name('api.remote.download_file'); Route::group(['prefix' => '/eggs'], function () { Route::get('/', 'EggRetrievalController@index')->name('api.remote.eggs'); diff --git a/routes/daemon.php b/routes/daemon.php index b74a005a7..2c8058e36 100644 --- a/routes/daemon.php +++ b/routes/daemon.php @@ -10,5 +10,4 @@ Route::get('/packs/pull/{uuid}', 'PackController@pull')->name('daemon.pack.pull' Route::get('/packs/pull/{uuid}/hash', 'PackController@hash')->name('daemon.pack.hash'); Route::get('/configure/{token}', 'ActionController@configuration')->name('daemon.configuration'); -Route::post('/download', 'ActionController@authenticateDownload')->name('daemon.download'); Route::post('/install', 'ActionController@markInstall')->name('daemon.install'); From 070239abcf54738cb494c3b0df520c3767d4ffd0 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 1 Mar 2018 19:26:11 -0600 Subject: [PATCH 14/25] Fix inability to edit certain environment vars and start line, closes #1008 --- CHANGELOG.md | 1 + .../Api/Application/Servers/StartupController.php | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cd81951c..bc82c566a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * Fixes a bug when reinstalling a server that would not mark the server as installing, resulting in some UI issues. * Handle 404 errors from missing models in the application API bindings correctly. * Fix validation error returned when no environment variables are passed, even if there are no variables required. +* Fix improper permissions on `PATCH /api/servers//startup` endpoint which was preventing enditing any start variables. ### Added * Adds back client API for sending commands or power toggles to a server though the Panel API: `/api/client/servers/` diff --git a/app/Http/Controllers/Api/Application/Servers/StartupController.php b/app/Http/Controllers/Api/Application/Servers/StartupController.php index e6b8015d8..0265af464 100644 --- a/app/Http/Controllers/Api/Application/Servers/StartupController.php +++ b/app/Http/Controllers/Api/Application/Servers/StartupController.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Servers; +use Pterodactyl\Models\User; use Pterodactyl\Models\Server; use Pterodactyl\Services\Servers\StartupModificationService; use Pterodactyl\Transformers\Api\Application\ServerTransformer; @@ -40,7 +41,9 @@ class StartupController extends ApplicationApiController */ public function index(UpdateServerStartupRequest $request): array { - $server = $this->modificationService->handle($request->getModel(Server::class), $request->validated()); + $server = $this->modificationService + ->setUserLevel(User::USER_LEVEL_ADMIN) + ->handle($request->getModel(Server::class), $request->validated()); return $this->fractal->item($server) ->transformWith($this->getTransformer(ServerTransformer::class)) From 5f6c153537ffbd3027f8dd51e52da09a8fb5b723 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 1 Mar 2018 20:00:14 -0600 Subject: [PATCH 15/25] Validate resource existence before validating data sent --- CHANGELOG.md | 3 ++ .../Api/Application/ApplicationApiRequest.php | 32 ++++++++++++++++++- .../Servers/ServerWriteRequest.php | 13 -------- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc82c566a..3960ae599 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * Fix validation error returned when no environment variables are passed, even if there are no variables required. * Fix improper permissions on `PATCH /api/servers//startup` endpoint which was preventing enditing any start variables. +### Changed +* Changes order that validation of resource existence occurs in API requests to not try and use a non-existent model when validating data. + ### Added * Adds back client API for sending commands or power toggles to a server though the Panel API: `/api/client/servers/` * Added proper transformer for Packs and re-enabled missing includes on server. diff --git a/app/Http/Requests/Api/Application/ApplicationApiRequest.php b/app/Http/Requests/Api/Application/ApplicationApiRequest.php index ada9b4b00..084a89bdd 100644 --- a/app/Http/Requests/Api/Application/ApplicationApiRequest.php +++ b/app/Http/Requests/Api/Application/ApplicationApiRequest.php @@ -3,7 +3,6 @@ namespace Pterodactyl\Http\Requests\Api\Application; use Pterodactyl\Models\ApiKey; -use Illuminate\Database\Eloquent\Model; use Pterodactyl\Services\Acl\Api\AdminAcl; use Illuminate\Foundation\Http\FormRequest; use Pterodactyl\Exceptions\PterodactylException; @@ -13,6 +12,14 @@ use Symfony\Component\Routing\Exception\InvalidParameterException; abstract class ApplicationApiRequest extends FormRequest { + /** + * Tracks if the request has been validated internally or not to avoid + * making duplicate validation calls. + * + * @var bool + */ + private $hasValidated = false; + /** * The resource that should be checked when performing the authorization * function for this request. @@ -96,6 +103,21 @@ abstract class ApplicationApiRequest extends FormRequest return $this->route()->parameter($parameterKey); } + /** + * Validate that the resource exists and can be accessed prior to booting + * the validator and attempting to use the data. + * + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + protected function prepareForValidation() + { + if (! $this->passesAuthorization()) { + $this->failedAuthorization(); + } + + $this->hasValidated = true; + } + /* * Determine if the request passes the authorization check as well * as the exists check. @@ -110,6 +132,14 @@ abstract class ApplicationApiRequest extends FormRequest */ protected function passesAuthorization() { + // If we have already validated we do not need to call this function + // again. This is needed to work around Laravel's normal auth validation + // that occurs after validating the request params since we are doing auth + // validation in the prepareForValidation() function. + if ($this->hasValidated) { + return true; + } + if (! parent::passesAuthorization()) { return false; } diff --git a/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php b/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php index 728b1ce52..07c201336 100644 --- a/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php +++ b/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers; -use Pterodactyl\Models\Server; use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; @@ -17,16 +16,4 @@ class ServerWriteRequest extends ApplicationApiRequest * @var int */ protected $permission = AdminAcl::WRITE; - - /** - * Determine if the requested server exists on the Panel. - * - * @return bool - */ - public function resourceExists(): bool - { - $server = $this->route()->parameter('server'); - - return $server instanceof Server && $server->exists; - } } From 87b96bdfc86045135ba74a7439aa7c7bced13930 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 1 Mar 2018 20:08:27 -0600 Subject: [PATCH 16/25] Add core logic to allow for limited databases and allocations --- .../UpdateServerBuildConfigurationRequest.php | 11 +- app/Models/Server.php | 6 + .../Servers/BuildModificationService.php | 2 + .../Api/Application/ServerTransformer.php | 4 + .../Api/Client/ServerTransformer.php | 4 + ...nd_port_limit_columns_to_servers_table.php | 33 ++++++ .../pterodactyl/admin/servers/new.blade.php | 2 +- .../admin/servers/view/build.blade.php | 109 +++++++++++------- 8 files changed, 128 insertions(+), 43 deletions(-) create mode 100644 database/migrations/2018_03_01_192831_add_database_and_port_limit_columns_to_servers_table.php diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php index 893ff5ff7..076abdf4a 100644 --- a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php @@ -13,7 +13,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest */ public function rules(): array { - $rules = Server::getUpdateRulesForId($this->route()->parameter('server')->id); + $rules = Server::getUpdateRulesForId($this->getModel(Server::class)->id); return [ 'allocation' => $rules['allocation_id'], @@ -26,6 +26,9 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest 'add_allocations.*' => 'integer', 'remove_allocations' => 'bail|array', 'remove_allocations.*' => 'integer', + 'feature_limits' => 'required|array', + 'feature_limits.databases' => $rules['database_limit'], + 'feature_limits.allocations' => $rules['allocation_limit'], ]; } @@ -39,7 +42,9 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest $data = parent::validated(); $data['allocation_id'] = $data['allocation']; - unset($data['allocation']); + $data['database_limit'] = $data['feature_limits']['databases']; + $data['allocation_limit'] = $data['feature_limits']['allocations']; + unset($data['allocation'], $data['feature_limits']); return $data; } @@ -56,6 +61,8 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest 'remove_allocations' => 'allocations to remove', 'add_allocations.*' => 'allocation to add', 'remove_allocations.*' => 'allocation to remove', + 'feature_limits.databases' => 'Database Limit', + 'feature_limits.allocations' => 'Allocation Limit', ]; } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 458bff1d6..d5c6b3a8d 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -69,6 +69,8 @@ class Server extends Model implements CleansAttributes, ValidableContract 'skip_scripts' => 'sometimes', 'image' => 'required', 'startup' => 'required', + 'database_limit' => 'present', + 'allocation_limit' => 'present', ]; /** @@ -93,6 +95,8 @@ class Server extends Model implements CleansAttributes, ValidableContract 'skip_scripts' => 'boolean', 'image' => 'string|max:255', 'installed' => 'boolean', + 'database_limit' => 'nullable|integer|min:0', + 'allocation_limit' => 'nullable|integer|min:0', ]; /** @@ -116,6 +120,8 @@ class Server extends Model implements CleansAttributes, ValidableContract 'egg_id' => 'integer', 'pack_id' => 'integer', 'installed' => 'integer', + 'database_limit' => 'integer', + 'allocation_limit' => 'integer', ]; /** diff --git a/app/Services/Servers/BuildModificationService.php b/app/Services/Servers/BuildModificationService.php index 8924b2a04..5d36b4c5c 100644 --- a/app/Services/Servers/BuildModificationService.php +++ b/app/Services/Servers/BuildModificationService.php @@ -91,6 +91,8 @@ class BuildModificationService 'cpu' => array_get($data, 'cpu'), 'disk' => array_get($data, 'disk'), 'allocation_id' => array_get($data, 'allocation_id'), + 'database_limit' => array_get($data, 'database_limit'), + 'allocation_limit' => array_get($data, 'allocation_limit'), ]); $allocations = $this->allocationRepository->findWhere([['server_id', '=', $server->id]]); diff --git a/app/Transformers/Api/Application/ServerTransformer.php b/app/Transformers/Api/Application/ServerTransformer.php index 6a003b4dc..2a542dbcd 100644 --- a/app/Transformers/Api/Application/ServerTransformer.php +++ b/app/Transformers/Api/Application/ServerTransformer.php @@ -75,6 +75,10 @@ class ServerTransformer extends BaseTransformer 'io' => $server->io, 'cpu' => $server->cpu, ], + 'feature_limits' => [ + 'databases' => $server->database_limit, + 'allocations' => $server->allocation_limit, + ], 'user' => $server->owner_id, 'node' => $server->node_id, 'allocation' => $server->allocation_id, diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php index 4f08c208d..6816d6d74 100644 --- a/app/Transformers/Api/Client/ServerTransformer.php +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -36,6 +36,10 @@ class ServerTransformer extends BaseClientTransformer 'io' => $server->io, 'cpu' => $server->cpu, ], + 'feature_limits' => [ + 'databases' => $server->database_limit, + 'allocations' => $server->allocation_limit, + ], ]; } } diff --git a/database/migrations/2018_03_01_192831_add_database_and_port_limit_columns_to_servers_table.php b/database/migrations/2018_03_01_192831_add_database_and_port_limit_columns_to_servers_table.php new file mode 100644 index 000000000..4e85e8aeb --- /dev/null +++ b/database/migrations/2018_03_01_192831_add_database_and_port_limit_columns_to_servers_table.php @@ -0,0 +1,33 @@ +unsignedInteger('database_limit')->after('installed')->nullable()->default(0); + $table->unsignedInteger('allocation_limit')->after('installed')->nullable()->default(0); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn(['database_limit', 'allocation_limit']); + }); + } +} diff --git a/resources/themes/pterodactyl/admin/servers/new.blade.php b/resources/themes/pterodactyl/admin/servers/new.blade.php index bfb6760b4..bad452312 100644 --- a/resources/themes/pterodactyl/admin/servers/new.blade.php +++ b/resources/themes/pterodactyl/admin/servers/new.blade.php @@ -111,7 +111,7 @@
    - + MB
    diff --git a/resources/themes/pterodactyl/admin/servers/view/build.blade.php b/resources/themes/pterodactyl/admin/servers/view/build.blade.php index f6e9e607b..8900bf90a 100644 --- a/resources/themes/pterodactyl/admin/servers/view/build.blade.php +++ b/resources/themes/pterodactyl/admin/servers/view/build.blade.php @@ -89,50 +89,79 @@
  • -
    -
    -

    Allocation Management

    -
    -
    -
    - - -

    The default connection address that will be used for this game server.

    -
    -
    - -
    - +
    +
    +
    +
    +

    Application Feature Limits

    -

    Please note that due to software limitations you cannot assign identical ports on different IPs to the same server.

    -
    -
    - -
    - +
    +
    +
    + +
    + +
    +

    The total number of databases a user is allowed to create for this server. Leave blank to allow unlimmited.

    +
    +
    + +
    + +
    +

    This feature is not currently implemented. The total number of allocations a user is allowed to create for this server. Leave blank to allow unlimited.

    +
    +
    -

    Simply select which ports you would like to remove from the list above. If you want to assign a port on a different IP that is already in use you can select it from the left and delete it here.

    -
    From 07893effa3aa40994da06eb03110205e296c791e Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 1 Mar 2018 21:27:37 -0600 Subject: [PATCH 17/25] Add initial go at user created databases for servers, still needs cleaning --- ...tabaseClientFeatureNotEnabledException.php | 13 +++ .../NoSuitableDatabaseHostException.php | 16 ++++ .../Database/TooManyDatabasesException.php | 13 +++ .../Controllers/Server/DatabaseController.php | 76 ++++++++++++++-- .../Database/StoreServerDatabaseRequest.php | 43 +++++++++ .../Requests/Server/ServerFormRequest.php | 8 +- .../Databases/DatabaseManagementService.php | 15 ++-- .../Databases/DeployServerDatabaseService.php | 87 +++++++++++++++++++ config/pterodactyl.php | 15 ++++ .../server/databases/index.blade.php | 51 ++++++++--- routes/server.php | 2 + 11 files changed, 314 insertions(+), 25 deletions(-) create mode 100644 app/Exceptions/Service/Database/DatabaseClientFeatureNotEnabledException.php create mode 100644 app/Exceptions/Service/Database/NoSuitableDatabaseHostException.php create mode 100644 app/Exceptions/Service/Database/TooManyDatabasesException.php create mode 100644 app/Http/Requests/Server/Database/StoreServerDatabaseRequest.php create mode 100644 app/Services/Databases/DeployServerDatabaseService.php diff --git a/app/Exceptions/Service/Database/DatabaseClientFeatureNotEnabledException.php b/app/Exceptions/Service/Database/DatabaseClientFeatureNotEnabledException.php new file mode 100644 index 000000000..809cb4fbf --- /dev/null +++ b/app/Exceptions/Service/Database/DatabaseClientFeatureNotEnabledException.php @@ -0,0 +1,13 @@ +alert = $alert; + $this->databaseHostRepository = $databaseHostRepository; + $this->deployServerDatabaseService = $deployServerDatabaseService; $this->passwordService = $passwordService; $this->repository = $repository; } @@ -50,11 +81,42 @@ class DatabaseController extends Controller $this->authorize('view-databases', $server); $this->setRequest($request)->injectJavascript(); + $canCreateDatabase = config('pterodactyl.client_features.databases.enabled'); + $allowRandom = config('pterodactyl.client_features.databases.allow_random'); + + if ($this->databaseHostRepository->findCountWhere([['node_id', '=', $server->node_id]]) === 0) { + if ($canCreateDatabase && ! $allowRandom) { + $canCreateDatabase = false; + } + } + + $databases = $this->repository->getDatabasesForServer($server->id); + return view('server.databases.index', [ - 'databases' => $this->repository->getDatabasesForServer($server->id), + 'allowCreation' => $canCreateDatabase, + 'overLimit' => ! is_null($server->database_limit) && count($databases) >= $server->database_limit, + 'databases' => $databases, ]); } + /** + * Handle a request from a user to create a new database for the server. + * + * @param \Pterodactyl\Http\Requests\Server\Database\StoreServerDatabaseRequest $request + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException + */ + public function store(StoreServerDatabaseRequest $request): RedirectResponse + { + $this->deployServerDatabaseService->handle($request->getServer(), $request->validated()); + + $this->alert->success('Successfully created a new database.')->flash(); + + return redirect()->route('server.databases.index', $request->getServer()->uuidShort); + } + /** * Handle a request to update the password for a specific database. * diff --git a/app/Http/Requests/Server/Database/StoreServerDatabaseRequest.php b/app/Http/Requests/Server/Database/StoreServerDatabaseRequest.php new file mode 100644 index 000000000..2af23a914 --- /dev/null +++ b/app/Http/Requests/Server/Database/StoreServerDatabaseRequest.php @@ -0,0 +1,43 @@ + 'required|string|min:1', + 'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/', + ]; + } +} diff --git a/app/Http/Requests/Server/ServerFormRequest.php b/app/Http/Requests/Server/ServerFormRequest.php index b796a21e0..f59ea3ae3 100644 --- a/app/Http/Requests/Server/ServerFormRequest.php +++ b/app/Http/Requests/Server/ServerFormRequest.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Requests\Server; +use Pterodactyl\Models\Server; use Pterodactyl\Http\Requests\FrontendUserFormRequest; abstract class ServerFormRequest extends FrontendUserFormRequest @@ -24,6 +25,11 @@ abstract class ServerFormRequest extends FrontendUserFormRequest return false; } - return $this->user()->can($this->permission(), $this->attributes->get('server')); + return $this->user()->can($this->permission(), $this->getServer()); + } + + public function getServer(): Server + { + return $this->attributes->get('server'); } } diff --git a/app/Services/Databases/DatabaseManagementService.php b/app/Services/Databases/DatabaseManagementService.php index b05ddc3ff..dc91e11f9 100644 --- a/app/Services/Databases/DatabaseManagementService.php +++ b/app/Services/Databases/DatabaseManagementService.php @@ -13,22 +13,27 @@ class DatabaseManagementService /** * @var \Illuminate\Database\DatabaseManager */ - protected $database; + private $database; /** * @var \Pterodactyl\Extensions\DynamicDatabaseConnection */ - protected $dynamic; + private $dynamic; /** * @var \Illuminate\Contracts\Encryption\Encrypter */ - protected $encrypter; + private $encrypter; /** * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface */ - protected $repository; + private $repository; + + /** + * @var bool + */ + protected $useRandomHost = false; /** * CreationService constructor. @@ -55,7 +60,7 @@ class DatabaseManagementService * * @param int $server * @param array $data - * @return \Illuminate\Database\Eloquent\Model + * @return \Pterodactyl\Models\Database * * @throws \Exception */ diff --git a/app/Services/Databases/DeployServerDatabaseService.php b/app/Services/Databases/DeployServerDatabaseService.php new file mode 100644 index 000000000..2932b87f2 --- /dev/null +++ b/app/Services/Databases/DeployServerDatabaseService.php @@ -0,0 +1,87 @@ +databaseHostRepository = $databaseHostRepository; + $this->managementService = $managementService; + $this->repository = $repository; + } + + /** + * @param \Pterodactyl\Models\Server $server + * @param array $data + * @return \Pterodactyl\Models\Database + * + * @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException + * @throws \Exception + */ + public function handle(Server $server, array $data): Database + { + if (! config('pterodactyl.client_features.databases.enabled')) { + throw new DatabaseClientFeatureNotEnabledException; + } + + $databases = $this->repository->findCountWhere([['server_id', '=', $server->id]]); + if (! is_null($server->database_limit) && $databases >= $server->database_limit) { + throw new TooManyDatabasesException; + } + + $allowRandom = config('pterodactyl.client_features.databases.allow_random'); + $host = $this->databaseHostRepository->setColumns(['id'])->findWhere([ + ['node_id', '=', $server->node_id], + ])->random(); + + if (empty($host) && ! $allowRandom) { + throw new NoSuitableDatabaseHostException; + } + + if (empty($host)) { + $host = $this->databaseHostRepository->setColumns(['id'])->all()->random(); + if (empty($host)) { + throw new NoSuitableDatabaseHostException; + } + } + + return $this->managementService->create($server->id, [ + 'database_host_id' => $host->id, + 'database' => array_get($data, 'database'), + 'remote' => array_get($data, 'remote'), + ]); + } +} diff --git a/config/pterodactyl.php b/config/pterodactyl.php index 523080ae3..e06c709ef 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -163,6 +163,21 @@ return [ 'in_context' => env('PHRASE_IN_CONTEXT', false), ], + /* + |-------------------------------------------------------------------------- + | Language Editor + |-------------------------------------------------------------------------- + | + | Set `PHRASE_IN_CONTEXT` to true to enable the PhaseApp in-context editor + | on this site which allows you to translate the panel, from the panel. + */ + 'client_features' => [ + 'databases' => [ + 'enabled' => env('PTERODACTYL_CLIENT_DATABASES_ENABLED', true), + 'allow_random' => env('PTERODACTYL_CLIENT_DATABASES_ALLOW_RANDOM', true), + ], + ], + /* |-------------------------------------------------------------------------- | File Editor diff --git a/resources/themes/pterodactyl/server/databases/index.blade.php b/resources/themes/pterodactyl/server/databases/index.blade.php index 6ba24c1ad..b8e01db84 100644 --- a/resources/themes/pterodactyl/server/databases/index.blade.php +++ b/resources/themes/pterodactyl/server/databases/index.blade.php @@ -21,15 +21,10 @@ @section('content')
    -
    +
    + @if($allowCreation) +
    +
    +
    +

    Create New Database

    +
    + @if($overLimit) +
    +
    + You are currently using {{ count($databases) }} of your {{ $server->database_limit ?? '∞' }} allowed databases. +
    +
    + @else +
    +
    +
    + +
    + s{{ $server->id }}_ + +
    +
    +
    + + +

    This should reflect the IP address that connections are allowed from. Uses standard MySQL notation. If unsure leave as %.

    +
    +
    + +
    + @endif +
    +
    + @endif
    @endsection diff --git a/routes/server.php b/routes/server.php index 85283df9e..93005aaec 100644 --- a/routes/server.php +++ b/routes/server.php @@ -38,6 +38,8 @@ Route::group(['prefix' => 'settings'], function () { Route::group(['prefix' => 'databases'], function () { Route::get('/', 'DatabaseController@index')->name('server.databases.index'); + Route::post('/new', 'DatabaseController@store')->name('server.databases.new'); + Route::patch('/password', 'DatabaseController@update')->middleware('server..database')->name('server.databases.password'); }); From d03559080f8d17f8eb9ea72a5012fe55fdd77ba7 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 2 Mar 2018 18:37:39 -0600 Subject: [PATCH 18/25] Theoretically fix migration issues for api_key table --- CHANGELOG.md | 1 + .../migrations/2018_01_13_142012_SetupTableForKeyEncryption.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3960ae599..fef00d908 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * Handle 404 errors from missing models in the application API bindings correctly. * Fix validation error returned when no environment variables are passed, even if there are no variables required. * Fix improper permissions on `PATCH /api/servers//startup` endpoint which was preventing enditing any start variables. +* Should fix migration issues from 0.6 when there are more than API key in the database. ### Changed * Changes order that validation of resource existence occurs in API requests to not try and use a non-existent model when validating data. diff --git a/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php b/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php index e7fd0c58c..5dba9c113 100644 --- a/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php +++ b/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php @@ -16,7 +16,7 @@ class SetupTableForKeyEncryption extends Migration public function up() { Schema::table('api_keys', function (Blueprint $table) { - $table->char('identifier', 16)->unique()->after('user_id'); + $table->char('identifier', 16)->nullable()->unique()->after('user_id'); $table->dropUnique(['token']); }); From bcb69603ad5769a623cf813f0fb89dbfe77f225d Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 2 Mar 2018 19:03:55 -0600 Subject: [PATCH 19/25] Add support for user management of databases --- CHANGELOG.md | 1 + .../Controllers/Server/DatabaseController.php | 26 ++++++++++ .../Server/DatabaseBelongsToServer.php | 7 ++- .../Database/DeleteServerDatabaseRequest.php | 40 +++++++++++++++ app/Models/Permission.php | 2 + public/js/laroute.js | 2 +- resources/lang/en/server.php | 8 +++ .../server/databases/index.blade.php | 51 +++++++++++++++++-- routes/server.php | 2 + 9 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 app/Http/Requests/Server/Database/DeleteServerDatabaseRequest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index fef00d908..d182ef4a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * Adds back client API for sending commands or power toggles to a server though the Panel API: `/api/client/servers/` * Added proper transformer for Packs and re-enabled missing includes on server. * Added support for using Filesystem as a caching driver, although not recommended. +* Added support for user management of server databases. ## v0.7.3 (Derelict Dermodactylus) ### Fixed diff --git a/app/Http/Controllers/Server/DatabaseController.php b/app/Http/Controllers/Server/DatabaseController.php index 1c2875a02..be7d501ba 100644 --- a/app/Http/Controllers/Server/DatabaseController.php +++ b/app/Http/Controllers/Server/DatabaseController.php @@ -4,16 +4,19 @@ namespace Pterodactyl\Http\Controllers\Server; use Illuminate\View\View; use Illuminate\Http\Request; +use Illuminate\Http\Response; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Traits\Controllers\JavascriptInjection; use Pterodactyl\Services\Databases\DatabasePasswordService; +use Pterodactyl\Services\Databases\DatabaseManagementService; use Pterodactyl\Services\Databases\DeployServerDatabaseService; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; use Pterodactyl\Http\Requests\Server\Database\StoreServerDatabaseRequest; +use Pterodactyl\Http\Requests\Server\Database\DeleteServerDatabaseRequest; class DatabaseController extends Controller { @@ -34,6 +37,11 @@ class DatabaseController extends Controller */ private $databaseHostRepository; + /** + * @var \Pterodactyl\Services\Databases\DatabaseManagementService + */ + private $managementService; + /** * @var \Pterodactyl\Services\Databases\DatabasePasswordService */ @@ -50,6 +58,7 @@ class DatabaseController extends Controller * @param \Prologue\Alerts\AlertsMessageBag $alert * @param \Pterodactyl\Services\Databases\DeployServerDatabaseService $deployServerDatabaseService * @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $databaseHostRepository + * @param \Pterodactyl\Services\Databases\DatabaseManagementService $managementService * @param \Pterodactyl\Services\Databases\DatabasePasswordService $passwordService * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository */ @@ -57,12 +66,14 @@ class DatabaseController extends Controller AlertsMessageBag $alert, DeployServerDatabaseService $deployServerDatabaseService, DatabaseHostRepositoryInterface $databaseHostRepository, + DatabaseManagementService $managementService, DatabasePasswordService $passwordService, DatabaseRepositoryInterface $repository ) { $this->alert = $alert; $this->databaseHostRepository = $databaseHostRepository; $this->deployServerDatabaseService = $deployServerDatabaseService; + $this->managementService = $managementService; $this->passwordService = $passwordService; $this->repository = $repository; } @@ -136,4 +147,19 @@ class DatabaseController extends Controller return response()->json(['password' => $password]); } + + /** + * Delete a database for this server from the SQL server and Panel database. + * + * @param \Pterodactyl\Http\Requests\Server\Database\DeleteServerDatabaseRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function delete(DeleteServerDatabaseRequest $request): Response + { + $this->managementService->delete($request->attributes->get('database')->id); + + return response('', Response::HTTP_NO_CONTENT); + } } diff --git a/app/Http/Middleware/Server/DatabaseBelongsToServer.php b/app/Http/Middleware/Server/DatabaseBelongsToServer.php index d7e34d211..1617a8520 100644 --- a/app/Http/Middleware/Server/DatabaseBelongsToServer.php +++ b/app/Http/Middleware/Server/DatabaseBelongsToServer.php @@ -38,8 +38,13 @@ class DatabaseBelongsToServer public function handle(Request $request, Closure $next) { $server = $request->attributes->get('server'); + $database = $request->input('database') ?? $request->route()->parameter('database'); - $database = $this->repository->find($request->input('database')); + if (! is_digit($database)) { + throw new NotFoundHttpException; + } + + $database = $this->repository->find($database); if (is_null($database) || $database->server_id !== $server->id) { throw new NotFoundHttpException; } diff --git a/app/Http/Requests/Server/Database/DeleteServerDatabaseRequest.php b/app/Http/Requests/Server/Database/DeleteServerDatabaseRequest.php new file mode 100644 index 000000000..f2c81d9c8 --- /dev/null +++ b/app/Http/Requests/Server/Database/DeleteServerDatabaseRequest.php @@ -0,0 +1,40 @@ + [ 'view-databases' => null, 'reset-db-password' => null, + 'delete-database' => null, + 'create-database' => null, ], 'file' => [ 'access-sftp' => null, diff --git a/public/js/laroute.js b/public/js/laroute.js index c0e77614d..1ffbc7b1f 100644 --- a/public/js/laroute.js +++ b/public/js/laroute.js @@ -6,7 +6,7 @@ absolute: false, rootUrl: 'http://pterodactyl.local', - routes : [{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/open","name":"debugbar.openhandler","action":"Barryvdh\Debugbar\Controllers\OpenHandlerController@handle"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/clockwork\/{id}","name":"debugbar.clockwork","action":"Barryvdh\Debugbar\Controllers\OpenHandlerController@clockwork"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/assets\/stylesheets","name":"debugbar.assets.css","action":"Barryvdh\Debugbar\Controllers\AssetController@css"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/assets\/javascript","name":"debugbar.assets.js","action":"Barryvdh\Debugbar\Controllers\AssetController@js"},{"host":null,"methods":["DELETE"],"uri":"_debugbar\/cache\/{key}\/{tags?}","name":"debugbar.cache.delete","action":"Barryvdh\Debugbar\Controllers\CacheController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"\/","name":"index","action":"Pterodactyl\Http\Controllers\Base\IndexController@getIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"status\/{server}","name":"index.status","action":"Pterodactyl\Http\Controllers\Base\IndexController@status"},{"host":null,"methods":["GET","HEAD"],"uri":"account","name":"account","action":"Pterodactyl\Http\Controllers\Base\AccountController@index"},{"host":null,"methods":["POST"],"uri":"account","name":null,"action":"Pterodactyl\Http\Controllers\Base\AccountController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/api","name":"account.api","action":"Pterodactyl\Http\Controllers\Base\ClientApiController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/api\/new","name":"account.api.new","action":"Pterodactyl\Http\Controllers\Base\ClientApiController@create"},{"host":null,"methods":["POST"],"uri":"account\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Base\ClientApiController@store"},{"host":null,"methods":["DELETE"],"uri":"account\/api\/revoke\/{identifier}","name":"account.api.revoke","action":"Pterodactyl\Http\Controllers\Base\ClientApiController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/security","name":"account.security","action":"Pterodactyl\Http\Controllers\Base\SecurityController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/security\/revoke\/{id}","name":"account.security.revoke","action":"Pterodactyl\Http\Controllers\Base\SecurityController@revoke"},{"host":null,"methods":["PUT"],"uri":"account\/security\/totp","name":"account.security.totp","action":"Pterodactyl\Http\Controllers\Base\SecurityController@generateTotp"},{"host":null,"methods":["POST"],"uri":"account\/security\/totp","name":"account.security.totp.set","action":"Pterodactyl\Http\Controllers\Base\SecurityController@setTotp"},{"host":null,"methods":["DELETE"],"uri":"account\/security\/totp","name":"account.security.totp.disable","action":"Pterodactyl\Http\Controllers\Base\SecurityController@disableTotp"},{"host":null,"methods":["GET","HEAD"],"uri":"admin","name":"admin.index","action":"Pterodactyl\Http\Controllers\Admin\BaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/api","name":"admin.api.index","action":"Pterodactyl\Http\Controllers\Admin\ApiController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/api\/new","name":"admin.api.new","action":"Pterodactyl\Http\Controllers\Admin\ApiController@create"},{"host":null,"methods":["POST"],"uri":"admin\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ApiController@store"},{"host":null,"methods":["DELETE"],"uri":"admin\/api\/revoke\/{identifier}","name":"admin.api.delete","action":"Pterodactyl\Http\Controllers\Admin\ApiController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/locations","name":"admin.locations","action":"Pterodactyl\Http\Controllers\Admin\LocationController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/locations\/view\/{location}","name":"admin.locations.view","action":"Pterodactyl\Http\Controllers\Admin\LocationController@view"},{"host":null,"methods":["POST"],"uri":"admin\/locations","name":null,"action":"Pterodactyl\Http\Controllers\Admin\LocationController@create"},{"host":null,"methods":["PATCH"],"uri":"admin\/locations\/view\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\LocationController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/databases","name":"admin.databases","action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/databases\/view\/{host}","name":"admin.databases.view","action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@view"},{"host":null,"methods":["POST"],"uri":"admin\/databases","name":null,"action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@create"},{"host":null,"methods":["PATCH"],"uri":"admin\/databases\/view\/{host}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/databases\/view\/{host}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings","name":"admin.settings","action":"Pterodactyl\Http\Controllers\Admin\Settings\IndexController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings\/mail","name":"admin.settings.mail","action":"Pterodactyl\Http\Controllers\Admin\Settings\MailController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings\/advanced","name":"admin.settings.advanced","action":"Pterodactyl\Http\Controllers\Admin\Settings\AdvancedController@index"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\IndexController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings\/mail","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\MailController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings\/advanced","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\AdvancedController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users","name":"admin.users","action":"Pterodactyl\Http\Controllers\Admin\UserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/accounts.json","name":"admin.users.json","action":"Pterodactyl\Http\Controllers\Admin\UserController@json"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/new","name":"admin.users.new","action":"Pterodactyl\Http\Controllers\Admin\UserController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/view\/{user}","name":"admin.users.view","action":"Pterodactyl\Http\Controllers\Admin\UserController@view"},{"host":null,"methods":["POST"],"uri":"admin\/users\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@store"},{"host":null,"methods":["PATCH"],"uri":"admin\/users\/view\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/users\/view\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers","name":"admin.servers","action":"Pterodactyl\Http\Controllers\Admin\ServersController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/new","name":"admin.servers.new","action":"Pterodactyl\Http\Controllers\Admin\ServersController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}","name":"admin.servers.view","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/details","name":"admin.servers.view.details","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDetails"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/build","name":"admin.servers.view.build","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewBuild"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/startup","name":"admin.servers.view.startup","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewStartup"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/database","name":"admin.servers.view.database","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDatabase"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/manage","name":"admin.servers.view.manage","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewManage"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/delete","name":"admin.servers.view.delete","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDelete"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@store"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/build","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@updateBuild"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/startup","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@saveStartup"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/database","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@newDatabase"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/toggle","name":"admin.servers.view.manage.toggle","action":"Pterodactyl\Http\Controllers\Admin\ServersController@toggleInstall"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/rebuild","name":"admin.servers.view.manage.rebuild","action":"Pterodactyl\Http\Controllers\Admin\ServersController@rebuildContainer"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/suspension","name":"admin.servers.view.manage.suspension","action":"Pterodactyl\Http\Controllers\Admin\ServersController@manageSuspension"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/reinstall","name":"admin.servers.view.manage.reinstall","action":"Pterodactyl\Http\Controllers\Admin\ServersController@reinstallServer"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/delete","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@delete"},{"host":null,"methods":["PATCH"],"uri":"admin\/servers\/view\/{server}\/details","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@setDetails"},{"host":null,"methods":["PATCH"],"uri":"admin\/servers\/view\/{server}\/database","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@resetDatabasePassword"},{"host":null,"methods":["DELETE"],"uri":"admin\/servers\/view\/{server}\/database\/{database}\/delete","name":"admin.servers.view.database.delete","action":"Pterodactyl\Http\Controllers\Admin\ServersController@deleteDatabase"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes","name":"admin.nodes","action":"Pterodactyl\Http\Controllers\Admin\NodesController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/new","name":"admin.nodes.new","action":"Pterodactyl\Http\Controllers\Admin\NodesController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}","name":"admin.nodes.view","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/settings","name":"admin.nodes.view.settings","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewSettings"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/configuration","name":"admin.nodes.view.configuration","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewConfiguration"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/allocation","name":"admin.nodes.view.allocation","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewAllocation"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/servers","name":"admin.nodes.view.servers","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewServers"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/settings\/token","name":"admin.nodes.view.configuration.token","action":"Pterodactyl\Http\Controllers\Admin\NodesController@setToken"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@store"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@createAllocation"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation\/remove","name":"admin.nodes.view.allocation.removeBlock","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationRemoveBlock"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation\/alias","name":"admin.nodes.view.allocation.setAlias","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationSetAlias"},{"host":null,"methods":["PATCH"],"uri":"admin\/nodes\/view\/{node}\/settings","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@updateSettings"},{"host":null,"methods":["DELETE"],"uri":"admin\/nodes\/view\/{node}\/delete","name":"admin.nodes.view.delete","action":"Pterodactyl\Http\Controllers\Admin\NodesController@delete"},{"host":null,"methods":["DELETE"],"uri":"admin\/nodes\/view\/{node}\/allocation\/remove\/{allocation}","name":"admin.nodes.view.allocation.removeSingle","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationRemoveSingle"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests","name":"admin.nests","action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/new","name":"admin.nests.new","action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/view\/{nest}","name":"admin.nests.view","action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/new","name":"admin.nests.egg.new","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}","name":"admin.nests.egg.view","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}\/export","name":"admin.nests.egg.export","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggShareController@export"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}\/variables","name":"admin.nests.egg.variables","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}\/scripts","name":"admin.nests.egg.scripts","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggScriptController@index"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@store"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/import","name":"admin.nests.egg.import","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggShareController@import"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/egg\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@store"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/egg\/{egg}\/variables","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@store"},{"host":null,"methods":["PUT"],"uri":"admin\/nests\/egg\/{egg}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggShareController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/view\/{nest}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/egg\/{egg}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/egg\/{egg}\/scripts","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggScriptController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/egg\/{egg}\/variables\/{variable}","name":"admin.nests.egg.variables.edit","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/nests\/view\/{nest}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@destroy"},{"host":null,"methods":["DELETE"],"uri":"admin\/nests\/egg\/{egg}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@destroy"},{"host":null,"methods":["DELETE"],"uri":"admin\/nests\/egg\/{egg}\/variables\/{variable}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@destroy"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs","name":"admin.packs","action":"Pterodactyl\Http\Controllers\Admin\PackController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/new","name":"admin.packs.new","action":"Pterodactyl\Http\Controllers\Admin\PackController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/new\/template","name":"admin.packs.new.template","action":"Pterodactyl\Http\Controllers\Admin\PackController@newTemplate"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/view\/{pack}","name":"admin.packs.view","action":"Pterodactyl\Http\Controllers\Admin\PackController@view"},{"host":null,"methods":["POST"],"uri":"admin\/packs\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@store"},{"host":null,"methods":["POST"],"uri":"admin\/packs\/view\/{pack}\/export\/{files?}","name":"admin.packs.view.export","action":"Pterodactyl\Http\Controllers\Admin\PackController@export"},{"host":null,"methods":["PATCH"],"uri":"admin\/packs\/view\/{pack}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/packs\/view\/{pack}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@destroy"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/login","name":"auth.login","action":"Pterodactyl\Http\Controllers\Auth\LoginController@showLoginForm"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/login\/totp","name":"auth.totp","action":"Pterodactyl\Http\Controllers\Auth\LoginController@totp"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/password","name":"auth.password","action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestForm"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/password\/reset\/{token}","name":"auth.reset","action":"Pterodactyl\Http\Controllers\Auth\ResetPasswordController@showResetForm"},{"host":null,"methods":["POST"],"uri":"auth\/login","name":null,"action":"Pterodactyl\Http\Controllers\Auth\LoginController@login"},{"host":null,"methods":["POST"],"uri":"auth\/login\/totp","name":null,"action":"Pterodactyl\Http\Controllers\Auth\LoginController@loginUsingTotp"},{"host":null,"methods":["POST"],"uri":"auth\/password","name":null,"action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail"},{"host":null,"methods":["POST"],"uri":"auth\/password\/reset","name":"auth.reset.post","action":"Pterodactyl\Http\Controllers\Auth\ResetPasswordController@reset"},{"host":null,"methods":["POST"],"uri":"auth\/password\/reset\/{token}","name":null,"action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/logout","name":"auth.logout","action":"Pterodactyl\Http\Controllers\Auth\LoginController@logout"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}","name":"server.index","action":"Pterodactyl\Http\Controllers\Server\ConsoleController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/console","name":"server.console","action":"Pterodactyl\Http\Controllers\Server\ConsoleController@console"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/allocation","name":"server.settings.allocation","action":"Pterodactyl\Http\Controllers\Server\Settings\AllocationController@index"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/settings\/allocation","name":null,"action":"Pterodactyl\Http\Controllers\Server\Settings\AllocationController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/sftp","name":"server.settings.sftp","action":"Pterodactyl\Http\Controllers\Server\Settings\SftpController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/startup","name":"server.settings.startup","action":"Pterodactyl\Http\Controllers\Server\Settings\StartupController@index"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/settings\/startup","name":null,"action":"Pterodactyl\Http\Controllers\Server\Settings\StartupController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/databases","name":"server.databases.index","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@index"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/databases\/password","name":"server.databases.password","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files","name":"server.files.index","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/add","name":"server.files.add","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/edit\/{file}","name":"server.files.edit","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/download\/{file}","name":"server.files.edit","action":"Pterodactyl\Http\Controllers\Server\Files\DownloadController@index"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/files\/directory-list","name":"server.files.directory-list","action":"Pterodactyl\Http\Controllers\Server\Files\RemoteRequestController@directory"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/files\/save","name":"server.files.save","action":"Pterodactyl\Http\Controllers\Server\Files\RemoteRequestController@store"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users","name":"server.subusers","action":"Pterodactyl\Http\Controllers\Server\SubuserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users\/new","name":"server.subusers.new","action":"Pterodactyl\Http\Controllers\Server\SubuserController@create"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/users\/new","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@store"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users\/view\/{subuser}","name":"server.subusers.view","action":"Pterodactyl\Http\Controllers\Server\SubuserController@view"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/users\/view\/{subuser}","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@update"},{"host":null,"methods":["DELETE"],"uri":"server\/{server}\/users\/view\/{subuser}","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules","name":"server.schedules","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules\/new","name":"server.schedules.new","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@create"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/new","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@store"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":"server.schedules.view","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@view"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@update"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/view\/{schedule}\/toggle","name":"server.schedules.toggle","action":"Pterodactyl\Http\Controllers\Server\Tasks\ActionController@toggle"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/view\/{schedule}\/trigger","name":"server.schedules.trigger","action":"Pterodactyl\Http\Controllers\Server\Tasks\ActionController@trigger"},{"host":null,"methods":["DELETE"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users","name":"api.application.users","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users\/{user}","name":"api.application.users.view","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users\/external\/{external_id}","name":"api.application.users.external","action":"Pterodactyl\Http\Controllers\Api\Application\Users\ExternalUserController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/users","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/users\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/users\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes","name":"api.application.nodes","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes\/{node}","name":"api.application.nodes.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/nodes","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/nodes\/{node}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/nodes\/{node}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes\/{node}\/allocations","name":"api.application.allocations","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/nodes\/{node}\/allocations","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@store"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/nodes\/{node}\/allocations\/{allocation}","name":"api.application.allocations.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/locations","name":"api.applications.locations","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/locations\/{location}","name":"api.application.locations.view","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/locations","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/locations\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/locations\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers","name":"api.application.servers","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}","name":"api.application.servers.view","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/external\/{external_id}","name":"api.application.servers.external","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ExternalServerController@index"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/details","name":"api.application.servers.details","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController@details"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/build","name":"api.application.servers.build","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController@build"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/startup","name":"api.application.servers.startup","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\StartupController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@store"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/suspend","name":"api.application.servers.suspend","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@suspend"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/unsuspend","name":"api.application.servers.unsuspend","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@unsuspend"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/reinstall","name":"api.application.servers.reinstall","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@reinstall"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/rebuild","name":"api.application.servers.rebuild","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@rebuild"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@delete"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}\/{force?}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}\/databases","name":"api.application.servers.databases","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}\/databases\/{database}","name":"api.application.servers.databases.view","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/databases","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@store"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/databases\/{database}\/reset-password","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@resetPassword"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}\/databases\/{database}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests","name":"api.application.nests","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\NestController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}","name":"api.application.nests.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\NestController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}\/eggs","name":"api.application.nests.eggs","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\EggController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}\/eggs\/{egg}","name":"api.application.nests.eggs.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\EggController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/client","name":"api.client.index","action":"Pterodactyl\Http\Controllers\Api\Client\ClientController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/client\/servers\/{server}","name":"api.client.servers.view","action":"Pterodactyl\Http\Controllers\Api\Client\Servers\ServerController@index"},{"host":null,"methods":["POST"],"uri":"api\/client\/servers\/{server}\/command","name":"api.client.servers.command","action":"Pterodactyl\Http\Controllers\Api\Client\Servers\CommandController@index"},{"host":null,"methods":["POST"],"uri":"api\/client\/servers\/{server}\/power","name":"api.client.servers.power","action":"Pterodactyl\Http\Controllers\Api\Client\Servers\PowerController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/authenticate\/{token}","name":"api.remote.authenticate","action":"Pterodactyl\Http\Controllers\Api\Remote\ValidateKeyController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/eggs","name":"api.remote.eggs","action":"Pterodactyl\Http\Controllers\Api\Remote\EggRetrievalController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/eggs\/{uuid}","name":"api.remote.eggs.download","action":"Pterodactyl\Http\Controllers\Api\Remote\EggRetrievalController@download"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/scripts\/{uuid}","name":"api.remote.scripts","action":"Pterodactyl\Http\Controllers\Api\Remote\EggInstallController@index"},{"host":null,"methods":["POST"],"uri":"api\/remote\/sftp","name":"api.remote.sftp","action":"Pterodactyl\Http\Controllers\Api\Remote\SftpController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/packs\/pull\/{uuid}","name":"daemon.pack.pull","action":"Pterodactyl\Http\Controllers\Daemon\PackController@pull"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/packs\/pull\/{uuid}\/hash","name":"daemon.pack.hash","action":"Pterodactyl\Http\Controllers\Daemon\PackController@hash"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/configure\/{token}","name":"daemon.configuration","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@configuration"},{"host":null,"methods":["POST"],"uri":"daemon\/download","name":"daemon.download","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@authenticateDownload"},{"host":null,"methods":["POST"],"uri":"daemon\/install","name":"daemon.install","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@markInstall"}], + routes : [{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/open","name":"debugbar.openhandler","action":"Barryvdh\Debugbar\Controllers\OpenHandlerController@handle"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/clockwork\/{id}","name":"debugbar.clockwork","action":"Barryvdh\Debugbar\Controllers\OpenHandlerController@clockwork"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/assets\/stylesheets","name":"debugbar.assets.css","action":"Barryvdh\Debugbar\Controllers\AssetController@css"},{"host":null,"methods":["GET","HEAD"],"uri":"_debugbar\/assets\/javascript","name":"debugbar.assets.js","action":"Barryvdh\Debugbar\Controllers\AssetController@js"},{"host":null,"methods":["DELETE"],"uri":"_debugbar\/cache\/{key}\/{tags?}","name":"debugbar.cache.delete","action":"Barryvdh\Debugbar\Controllers\CacheController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"\/","name":"index","action":"Pterodactyl\Http\Controllers\Base\IndexController@getIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"status\/{server}","name":"index.status","action":"Pterodactyl\Http\Controllers\Base\IndexController@status"},{"host":null,"methods":["GET","HEAD"],"uri":"account","name":"account","action":"Pterodactyl\Http\Controllers\Base\AccountController@index"},{"host":null,"methods":["POST"],"uri":"account","name":null,"action":"Pterodactyl\Http\Controllers\Base\AccountController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/api","name":"account.api","action":"Pterodactyl\Http\Controllers\Base\ClientApiController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/api\/new","name":"account.api.new","action":"Pterodactyl\Http\Controllers\Base\ClientApiController@create"},{"host":null,"methods":["POST"],"uri":"account\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Base\ClientApiController@store"},{"host":null,"methods":["DELETE"],"uri":"account\/api\/revoke\/{identifier}","name":"account.api.revoke","action":"Pterodactyl\Http\Controllers\Base\ClientApiController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/security","name":"account.security","action":"Pterodactyl\Http\Controllers\Base\SecurityController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/security\/revoke\/{id}","name":"account.security.revoke","action":"Pterodactyl\Http\Controllers\Base\SecurityController@revoke"},{"host":null,"methods":["PUT"],"uri":"account\/security\/totp","name":"account.security.totp","action":"Pterodactyl\Http\Controllers\Base\SecurityController@generateTotp"},{"host":null,"methods":["POST"],"uri":"account\/security\/totp","name":"account.security.totp.set","action":"Pterodactyl\Http\Controllers\Base\SecurityController@setTotp"},{"host":null,"methods":["DELETE"],"uri":"account\/security\/totp","name":"account.security.totp.disable","action":"Pterodactyl\Http\Controllers\Base\SecurityController@disableTotp"},{"host":null,"methods":["GET","HEAD"],"uri":"admin","name":"admin.index","action":"Pterodactyl\Http\Controllers\Admin\BaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/api","name":"admin.api.index","action":"Pterodactyl\Http\Controllers\Admin\ApiController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/api\/new","name":"admin.api.new","action":"Pterodactyl\Http\Controllers\Admin\ApiController@create"},{"host":null,"methods":["POST"],"uri":"admin\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ApiController@store"},{"host":null,"methods":["DELETE"],"uri":"admin\/api\/revoke\/{identifier}","name":"admin.api.delete","action":"Pterodactyl\Http\Controllers\Admin\ApiController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/locations","name":"admin.locations","action":"Pterodactyl\Http\Controllers\Admin\LocationController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/locations\/view\/{location}","name":"admin.locations.view","action":"Pterodactyl\Http\Controllers\Admin\LocationController@view"},{"host":null,"methods":["POST"],"uri":"admin\/locations","name":null,"action":"Pterodactyl\Http\Controllers\Admin\LocationController@create"},{"host":null,"methods":["PATCH"],"uri":"admin\/locations\/view\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\LocationController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/databases","name":"admin.databases","action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/databases\/view\/{host}","name":"admin.databases.view","action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@view"},{"host":null,"methods":["POST"],"uri":"admin\/databases","name":null,"action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@create"},{"host":null,"methods":["PATCH"],"uri":"admin\/databases\/view\/{host}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/databases\/view\/{host}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\DatabaseController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings","name":"admin.settings","action":"Pterodactyl\Http\Controllers\Admin\Settings\IndexController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings\/mail","name":"admin.settings.mail","action":"Pterodactyl\Http\Controllers\Admin\Settings\MailController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/settings\/advanced","name":"admin.settings.advanced","action":"Pterodactyl\Http\Controllers\Admin\Settings\AdvancedController@index"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\IndexController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings\/mail","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\MailController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/settings\/advanced","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Settings\AdvancedController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users","name":"admin.users","action":"Pterodactyl\Http\Controllers\Admin\UserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/accounts.json","name":"admin.users.json","action":"Pterodactyl\Http\Controllers\Admin\UserController@json"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/new","name":"admin.users.new","action":"Pterodactyl\Http\Controllers\Admin\UserController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/users\/view\/{user}","name":"admin.users.view","action":"Pterodactyl\Http\Controllers\Admin\UserController@view"},{"host":null,"methods":["POST"],"uri":"admin\/users\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@store"},{"host":null,"methods":["PATCH"],"uri":"admin\/users\/view\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/users\/view\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\UserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers","name":"admin.servers","action":"Pterodactyl\Http\Controllers\Admin\ServersController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/new","name":"admin.servers.new","action":"Pterodactyl\Http\Controllers\Admin\ServersController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}","name":"admin.servers.view","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/details","name":"admin.servers.view.details","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDetails"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/build","name":"admin.servers.view.build","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewBuild"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/startup","name":"admin.servers.view.startup","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewStartup"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/database","name":"admin.servers.view.database","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDatabase"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/manage","name":"admin.servers.view.manage","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewManage"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/servers\/view\/{server}\/delete","name":"admin.servers.view.delete","action":"Pterodactyl\Http\Controllers\Admin\ServersController@viewDelete"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@store"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/build","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@updateBuild"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/startup","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@saveStartup"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/database","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@newDatabase"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/toggle","name":"admin.servers.view.manage.toggle","action":"Pterodactyl\Http\Controllers\Admin\ServersController@toggleInstall"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/rebuild","name":"admin.servers.view.manage.rebuild","action":"Pterodactyl\Http\Controllers\Admin\ServersController@rebuildContainer"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/suspension","name":"admin.servers.view.manage.suspension","action":"Pterodactyl\Http\Controllers\Admin\ServersController@manageSuspension"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/manage\/reinstall","name":"admin.servers.view.manage.reinstall","action":"Pterodactyl\Http\Controllers\Admin\ServersController@reinstallServer"},{"host":null,"methods":["POST"],"uri":"admin\/servers\/view\/{server}\/delete","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@delete"},{"host":null,"methods":["PATCH"],"uri":"admin\/servers\/view\/{server}\/details","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@setDetails"},{"host":null,"methods":["PATCH"],"uri":"admin\/servers\/view\/{server}\/database","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ServersController@resetDatabasePassword"},{"host":null,"methods":["DELETE"],"uri":"admin\/servers\/view\/{server}\/database\/{database}\/delete","name":"admin.servers.view.database.delete","action":"Pterodactyl\Http\Controllers\Admin\ServersController@deleteDatabase"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes","name":"admin.nodes","action":"Pterodactyl\Http\Controllers\Admin\NodesController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/new","name":"admin.nodes.new","action":"Pterodactyl\Http\Controllers\Admin\NodesController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}","name":"admin.nodes.view","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewIndex"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/settings","name":"admin.nodes.view.settings","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewSettings"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/configuration","name":"admin.nodes.view.configuration","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewConfiguration"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/allocation","name":"admin.nodes.view.allocation","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewAllocation"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/servers","name":"admin.nodes.view.servers","action":"Pterodactyl\Http\Controllers\Admin\NodesController@viewServers"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nodes\/view\/{node}\/settings\/token","name":"admin.nodes.view.configuration.token","action":"Pterodactyl\Http\Controllers\Admin\NodesController@setToken"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@store"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@createAllocation"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation\/remove","name":"admin.nodes.view.allocation.removeBlock","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationRemoveBlock"},{"host":null,"methods":["POST"],"uri":"admin\/nodes\/view\/{node}\/allocation\/alias","name":"admin.nodes.view.allocation.setAlias","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationSetAlias"},{"host":null,"methods":["PATCH"],"uri":"admin\/nodes\/view\/{node}\/settings","name":null,"action":"Pterodactyl\Http\Controllers\Admin\NodesController@updateSettings"},{"host":null,"methods":["DELETE"],"uri":"admin\/nodes\/view\/{node}\/delete","name":"admin.nodes.view.delete","action":"Pterodactyl\Http\Controllers\Admin\NodesController@delete"},{"host":null,"methods":["DELETE"],"uri":"admin\/nodes\/view\/{node}\/allocation\/remove\/{allocation}","name":"admin.nodes.view.allocation.removeSingle","action":"Pterodactyl\Http\Controllers\Admin\NodesController@allocationRemoveSingle"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests","name":"admin.nests","action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/new","name":"admin.nests.new","action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/view\/{nest}","name":"admin.nests.view","action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/new","name":"admin.nests.egg.new","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}","name":"admin.nests.egg.view","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}\/export","name":"admin.nests.egg.export","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggShareController@export"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}\/variables","name":"admin.nests.egg.variables","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/nests\/egg\/{egg}\/scripts","name":"admin.nests.egg.scripts","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggScriptController@index"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@store"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/import","name":"admin.nests.egg.import","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggShareController@import"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/egg\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@store"},{"host":null,"methods":["POST"],"uri":"admin\/nests\/egg\/{egg}\/variables","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@store"},{"host":null,"methods":["PUT"],"uri":"admin\/nests\/egg\/{egg}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggShareController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/view\/{nest}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/egg\/{egg}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/egg\/{egg}\/scripts","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggScriptController@update"},{"host":null,"methods":["PATCH"],"uri":"admin\/nests\/egg\/{egg}\/variables\/{variable}","name":"admin.nests.egg.variables.edit","action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/nests\/view\/{nest}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\NestController@destroy"},{"host":null,"methods":["DELETE"],"uri":"admin\/nests\/egg\/{egg}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggController@destroy"},{"host":null,"methods":["DELETE"],"uri":"admin\/nests\/egg\/{egg}\/variables\/{variable}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\Nests\EggVariableController@destroy"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs","name":"admin.packs","action":"Pterodactyl\Http\Controllers\Admin\PackController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/new","name":"admin.packs.new","action":"Pterodactyl\Http\Controllers\Admin\PackController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/new\/template","name":"admin.packs.new.template","action":"Pterodactyl\Http\Controllers\Admin\PackController@newTemplate"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/packs\/view\/{pack}","name":"admin.packs.view","action":"Pterodactyl\Http\Controllers\Admin\PackController@view"},{"host":null,"methods":["POST"],"uri":"admin\/packs\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@store"},{"host":null,"methods":["POST"],"uri":"admin\/packs\/view\/{pack}\/export\/{files?}","name":"admin.packs.view.export","action":"Pterodactyl\Http\Controllers\Admin\PackController@export"},{"host":null,"methods":["PATCH"],"uri":"admin\/packs\/view\/{pack}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@update"},{"host":null,"methods":["DELETE"],"uri":"admin\/packs\/view\/{pack}","name":null,"action":"Pterodactyl\Http\Controllers\Admin\PackController@destroy"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/login","name":"auth.login","action":"Pterodactyl\Http\Controllers\Auth\LoginController@showLoginForm"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/login\/totp","name":"auth.totp","action":"Pterodactyl\Http\Controllers\Auth\LoginController@totp"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/password","name":"auth.password","action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestForm"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/password\/reset\/{token}","name":"auth.reset","action":"Pterodactyl\Http\Controllers\Auth\ResetPasswordController@showResetForm"},{"host":null,"methods":["POST"],"uri":"auth\/login","name":null,"action":"Pterodactyl\Http\Controllers\Auth\LoginController@login"},{"host":null,"methods":["POST"],"uri":"auth\/login\/totp","name":null,"action":"Pterodactyl\Http\Controllers\Auth\LoginController@loginUsingTotp"},{"host":null,"methods":["POST"],"uri":"auth\/password","name":null,"action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail"},{"host":null,"methods":["POST"],"uri":"auth\/password\/reset","name":"auth.reset.post","action":"Pterodactyl\Http\Controllers\Auth\ResetPasswordController@reset"},{"host":null,"methods":["POST"],"uri":"auth\/password\/reset\/{token}","name":null,"action":"Pterodactyl\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail"},{"host":null,"methods":["GET","HEAD"],"uri":"auth\/logout","name":"auth.logout","action":"Pterodactyl\Http\Controllers\Auth\LoginController@logout"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}","name":"server.index","action":"Pterodactyl\Http\Controllers\Server\ConsoleController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/console","name":"server.console","action":"Pterodactyl\Http\Controllers\Server\ConsoleController@console"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/allocation","name":"server.settings.allocation","action":"Pterodactyl\Http\Controllers\Server\Settings\AllocationController@index"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/settings\/allocation","name":null,"action":"Pterodactyl\Http\Controllers\Server\Settings\AllocationController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/sftp","name":"server.settings.sftp","action":"Pterodactyl\Http\Controllers\Server\Settings\SftpController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/settings\/startup","name":"server.settings.startup","action":"Pterodactyl\Http\Controllers\Server\Settings\StartupController@index"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/settings\/startup","name":null,"action":"Pterodactyl\Http\Controllers\Server\Settings\StartupController@update"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/databases","name":"server.databases.index","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@index"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/databases\/new","name":"server.databases.new","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@store"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/databases\/password","name":"server.databases.password","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@update"},{"host":null,"methods":["DELETE"],"uri":"server\/{server}\/databases\/delete\/{database}","name":"server.databases.delete","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files","name":"server.files.index","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/add","name":"server.files.add","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@create"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/edit\/{file}","name":"server.files.edit","action":"Pterodactyl\Http\Controllers\Server\Files\FileActionsController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/files\/download\/{file}","name":"server.files.edit","action":"Pterodactyl\Http\Controllers\Server\Files\DownloadController@index"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/files\/directory-list","name":"server.files.directory-list","action":"Pterodactyl\Http\Controllers\Server\Files\RemoteRequestController@directory"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/files\/save","name":"server.files.save","action":"Pterodactyl\Http\Controllers\Server\Files\RemoteRequestController@store"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users","name":"server.subusers","action":"Pterodactyl\Http\Controllers\Server\SubuserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users\/new","name":"server.subusers.new","action":"Pterodactyl\Http\Controllers\Server\SubuserController@create"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/users\/new","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@store"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/users\/view\/{subuser}","name":"server.subusers.view","action":"Pterodactyl\Http\Controllers\Server\SubuserController@view"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/users\/view\/{subuser}","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@update"},{"host":null,"methods":["DELETE"],"uri":"server\/{server}\/users\/view\/{subuser}","name":null,"action":"Pterodactyl\Http\Controllers\Server\SubuserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules","name":"server.schedules","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules\/new","name":"server.schedules.new","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@create"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/new","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@store"},{"host":null,"methods":["GET","HEAD"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":"server.schedules.view","action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@view"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@update"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/view\/{schedule}\/toggle","name":"server.schedules.toggle","action":"Pterodactyl\Http\Controllers\Server\Tasks\ActionController@toggle"},{"host":null,"methods":["POST"],"uri":"server\/{server}\/schedules\/view\/{schedule}\/trigger","name":"server.schedules.trigger","action":"Pterodactyl\Http\Controllers\Server\Tasks\ActionController@trigger"},{"host":null,"methods":["DELETE"],"uri":"server\/{server}\/schedules\/view\/{schedule}","name":null,"action":"Pterodactyl\Http\Controllers\Server\Tasks\TaskManagementController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users","name":"api.application.users","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users\/{user}","name":"api.application.users.view","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users\/external\/{external_id}","name":"api.application.users.external","action":"Pterodactyl\Http\Controllers\Api\Application\Users\ExternalUserController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/users","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/users\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/users\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes","name":"api.application.nodes","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes\/{node}","name":"api.application.nodes.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/nodes","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/nodes\/{node}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/nodes\/{node}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes\/{node}\/allocations","name":"api.application.allocations","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/nodes\/{node}\/allocations","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@store"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/nodes\/{node}\/allocations\/{allocation}","name":"api.application.allocations.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/locations","name":"api.applications.locations","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/locations\/{location}","name":"api.application.locations.view","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/locations","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/locations\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/locations\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers","name":"api.application.servers","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}","name":"api.application.servers.view","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/external\/{external_id}","name":"api.application.servers.external","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ExternalServerController@index"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/details","name":"api.application.servers.details","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController@details"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/build","name":"api.application.servers.build","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController@build"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/startup","name":"api.application.servers.startup","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\StartupController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@store"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/suspend","name":"api.application.servers.suspend","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@suspend"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/unsuspend","name":"api.application.servers.unsuspend","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@unsuspend"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/reinstall","name":"api.application.servers.reinstall","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@reinstall"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/rebuild","name":"api.application.servers.rebuild","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@rebuild"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@delete"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}\/{force?}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}\/databases","name":"api.application.servers.databases","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}\/databases\/{database}","name":"api.application.servers.databases.view","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/databases","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@store"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/databases\/{database}\/reset-password","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@resetPassword"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}\/databases\/{database}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests","name":"api.application.nests","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\NestController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}","name":"api.application.nests.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\NestController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}\/eggs","name":"api.application.nests.eggs","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\EggController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}\/eggs\/{egg}","name":"api.application.nests.eggs.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\EggController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/client","name":"api.client.index","action":"Pterodactyl\Http\Controllers\Api\Client\ClientController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/client\/servers\/{server}","name":"api.client.servers.view","action":"Pterodactyl\Http\Controllers\Api\Client\Servers\ServerController@index"},{"host":null,"methods":["POST"],"uri":"api\/client\/servers\/{server}\/command","name":"api.client.servers.command","action":"Pterodactyl\Http\Controllers\Api\Client\Servers\CommandController@index"},{"host":null,"methods":["POST"],"uri":"api\/client\/servers\/{server}\/power","name":"api.client.servers.power","action":"Pterodactyl\Http\Controllers\Api\Client\Servers\PowerController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/authenticate\/{token}","name":"api.remote.authenticate","action":"Pterodactyl\Http\Controllers\Api\Remote\ValidateKeyController@index"},{"host":null,"methods":["POST"],"uri":"api\/remote\/download-file","name":"api.remote.download_file","action":"Pterodactyl\Http\Controllers\Api\Remote\FileDownloadController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/eggs","name":"api.remote.eggs","action":"Pterodactyl\Http\Controllers\Api\Remote\EggRetrievalController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/eggs\/{uuid}","name":"api.remote.eggs.download","action":"Pterodactyl\Http\Controllers\Api\Remote\EggRetrievalController@download"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/remote\/scripts\/{uuid}","name":"api.remote.scripts","action":"Pterodactyl\Http\Controllers\Api\Remote\EggInstallController@index"},{"host":null,"methods":["POST"],"uri":"api\/remote\/sftp","name":"api.remote.sftp","action":"Pterodactyl\Http\Controllers\Api\Remote\SftpController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/packs\/pull\/{uuid}","name":"daemon.pack.pull","action":"Pterodactyl\Http\Controllers\Daemon\PackController@pull"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/packs\/pull\/{uuid}\/hash","name":"daemon.pack.hash","action":"Pterodactyl\Http\Controllers\Daemon\PackController@hash"},{"host":null,"methods":["GET","HEAD"],"uri":"daemon\/configure\/{token}","name":"daemon.configuration","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@configuration"},{"host":null,"methods":["POST"],"uri":"daemon\/install","name":"daemon.install","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@markInstall"}], prefix: '', route : function (name, parameters, route) { diff --git a/resources/lang/en/server.php b/resources/lang/en/server.php index cd89e1f85..8941c4793 100644 --- a/resources/lang/en/server.php +++ b/resources/lang/en/server.php @@ -248,6 +248,14 @@ return [ 'title' => 'Reset Database Password', 'description' => 'Allows a user to reset passwords for databases.', ], + 'delete_database' => [ + 'title' => 'Delete Databases', + 'description' => 'Allows a user to delete databases for this server from the Panel.', + ], + 'create_database' => [ + 'title' => 'Create Database', + 'description' => 'Allows a user to create additional databases for this server.', + ], ], ], 'files' => [ diff --git a/resources/themes/pterodactyl/server/databases/index.blade.php b/resources/themes/pterodactyl/server/databases/index.blade.php index b8e01db84..fb618b64c 100644 --- a/resources/themes/pterodactyl/server/databases/index.blade.php +++ b/resources/themes/pterodactyl/server/databases/index.blade.php @@ -21,7 +21,7 @@ @section('content')
    -
    +

    @lang('server.config.database.your_dbs')

    @@ -50,11 +50,20 @@ {{ $database->host->host }}:{{ $database->host->port }} - @can('reset-db-password', $server) + @if(Gate::allows('reset-db-password', $server) || Gate::allows('delete-database', $server)) - + @can('delete-database', $server) + + @endcan + @can('reset-db-password', $server) + + @endcan - @endcan + @endif @endforeach @@ -69,7 +78,7 @@ @endif
    - @if($allowCreation) + @if($allowCreation && Gate::allows('create-database', $server))
    @@ -153,5 +162,37 @@ }); }); @endcan + @can('delete-database', $server) + $('[data-action="delete-database"]').click(function (event) { + event.preventDefault(); + var self = $(this); + swal({ + title: '', + type: 'warning', + text: 'Are you sure that you want to delete this database? There is no going back, all data will immediately be removed.', + showCancelButton: true, + confirmButtonText: 'Delete', + confirmButtonColor: '#d9534f', + closeOnConfirm: false, + showLoaderOnConfirm: true, + }, function () { + $.ajax({ + method: 'DELETE', + url: Router.route('server.databases.delete', { server: '{{ $server->uuidShort }}', database: self.data('id') }), + headers: { 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content') }, + }).done(function () { + self.parent().parent().slideUp(); + swal.close(); + }).fail(function (jqXHR) { + console.error(jqXHR); + swal({ + type: 'error', + title: 'Whoops!', + text: (typeof jqXHR.responseJSON.error !== 'undefined') ? jqXHR.responseJSON.error : 'An error occured while processing this request.' + }); + }); + }); + }); + @endcan @endsection diff --git a/routes/server.php b/routes/server.php index 93005aaec..a05f4b00b 100644 --- a/routes/server.php +++ b/routes/server.php @@ -41,6 +41,8 @@ Route::group(['prefix' => 'databases'], function () { Route::post('/new', 'DatabaseController@store')->name('server.databases.new'); Route::patch('/password', 'DatabaseController@update')->middleware('server..database')->name('server.databases.password'); + + Route::delete('/delete/{database}', 'DatabaseController@delete')->middleware('server..database')->name('server.databases.delete'); }); /* From e39353a18db134452cdb50502e47aa030282fc19 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 2 Mar 2018 19:37:21 -0600 Subject: [PATCH 20/25] Add tests for new service --- .../Databases/DeployServerDatabaseService.php | 15 +- .../Server/Files/DownloadControllerTest.php | 25 +- .../DeployServerDatabaseServiceTest.php | 236 ++++++++++++++++++ 3 files changed, 256 insertions(+), 20 deletions(-) create mode 100644 tests/Unit/Services/Databases/DeployServerDatabaseServiceTest.php diff --git a/app/Services/Databases/DeployServerDatabaseService.php b/app/Services/Databases/DeployServerDatabaseService.php index 2932b87f2..c8b5ed179 100644 --- a/app/Services/Databases/DeployServerDatabaseService.php +++ b/app/Services/Databases/DeployServerDatabaseService.php @@ -21,6 +21,7 @@ class DeployServerDatabaseService * @var \Pterodactyl\Services\Databases\DatabaseManagementService */ private $managementService; + /** * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface */ @@ -63,21 +64,23 @@ class DeployServerDatabaseService } $allowRandom = config('pterodactyl.client_features.databases.allow_random'); - $host = $this->databaseHostRepository->setColumns(['id'])->findWhere([ + $hosts = $this->databaseHostRepository->setColumns(['id'])->findWhere([ ['node_id', '=', $server->node_id], - ])->random(); + ]); - if (empty($host) && ! $allowRandom) { + if ($hosts->isEmpty() && ! $allowRandom) { throw new NoSuitableDatabaseHostException; } - if (empty($host)) { - $host = $this->databaseHostRepository->setColumns(['id'])->all()->random(); - if (empty($host)) { + if ($hosts->isEmpty()) { + $hosts = $this->databaseHostRepository->setColumns(['id'])->all(); + if ($hosts->isEmpty()) { throw new NoSuitableDatabaseHostException; } } + $host = $hosts->random(); + return $this->managementService->create($server->id, [ 'database_host_id' => $host->id, 'database' => array_get($data, 'database'), diff --git a/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php b/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php index d44481c1b..b76cbfb86 100644 --- a/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php +++ b/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php @@ -1,17 +1,10 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Http\Controllers\Server\Files; use Mockery as m; -use phpmock\phpunit\PHPMock; use Pterodactyl\Models\Node; +use Tests\Traits\MocksUuids; use Pterodactyl\Models\Server; use Illuminate\Cache\Repository; use Tests\Unit\Http\Controllers\ControllerTestCase; @@ -19,7 +12,7 @@ use Pterodactyl\Http\Controllers\Server\Files\DownloadController; class DownloadControllerTest extends ControllerTestCase { - use PHPMock; + use MocksUuids; /** * @var \Illuminate\Cache\Repository|\Mockery\Mock @@ -48,16 +41,20 @@ class DownloadControllerTest extends ControllerTestCase $this->setRequestAttribute('server', $server); $controller->shouldReceive('authorize')->with('download-files', $server)->once()->andReturnNull(); - $this->getFunctionMock('\\Pterodactyl\\Http\\Controllers\\Server\\Files', 'str_random') - ->expects($this->once())->willReturn('randomString'); - $this->cache->shouldReceive('tags')->with(['Server:Downloads'])->once()->andReturnSelf(); - $this->cache->shouldReceive('put')->with('randomString', ['server' => $server->uuid, 'path' => '/my/file.txt'], 5)->once()->andReturnNull(); + $this->cache->shouldReceive('put') + ->once() + ->with('Server:Downloads:' . $this->getKnownUuid(), ['server' => $server->uuid, 'path' => '/my/file.txt'], 5) + ->andReturnNull(); $response = $controller->index($this->request, $server->uuidShort, '/my/file.txt'); $this->assertIsRedirectResponse($response); $this->assertRedirectUrlEquals(sprintf( - '%s://%s:%s/v1/server/file/download/%s', $server->node->scheme, $server->node->fqdn, $server->node->daemonListen, 'randomString' + '%s://%s:%s/v1/server/file/download/%s', + $server->node->scheme, + $server->node->fqdn, + $server->node->daemonListen, + $this->getKnownUuid() ), $response); } diff --git a/tests/Unit/Services/Databases/DeployServerDatabaseServiceTest.php b/tests/Unit/Services/Databases/DeployServerDatabaseServiceTest.php new file mode 100644 index 000000000..f8f2ff70c --- /dev/null +++ b/tests/Unit/Services/Databases/DeployServerDatabaseServiceTest.php @@ -0,0 +1,236 @@ +databaseHostRepository = m::mock(DatabaseHostRepositoryInterface::class); + $this->managementService = m::mock(DatabaseManagementService::class); + $this->repository = m::mock(DatabaseRepositoryInterface::class); + + // Set configs for testing instances. + config()->set('pterodactyl.client_features.databases.enabled', true); + config()->set('pterodactyl.client_features.databases.allow_random', true); + } + + /** + * Test handling of non-random hosts when a host is found. + * + * @dataProvider databaseLimitDataProvider + */ + public function testNonRandomFoundHost($limit, $count) + { + config()->set('pterodactyl.client_features.databases.allow_random', false); + + $server = factory(Server::class)->make(['database_limit' => $limit]); + $model = factory(Database::class)->make(); + + $this->repository->shouldReceive('findCountWhere') + ->once() + ->with([['server_id', '=', $server->id]]) + ->andReturn($count); + + $this->databaseHostRepository->shouldReceive('setColumns->findWhere') + ->once() + ->with([['node_id', '=', $server->node_id]]) + ->andReturn(collect([$model])); + + $this->managementService->shouldReceive('create') + ->once() + ->with($server->id, [ + 'database_host_id' => $model->id, + 'database' => 'testdb', + 'remote' => null, + ]) + ->andReturn($model); + + $response = $this->getService()->handle($server, ['database' => 'testdb']); + + $this->assertInstanceOf(Database::class, $response); + $this->assertSame($model, $response); + } + + /** + * Test that an exception is thrown if in non-random mode and no host is found. + * + * @expectedException \Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException + */ + public function testNonRandomNoHost() + { + config()->set('pterodactyl.client_features.databases.allow_random', false); + + $server = factory(Server::class)->make(['database_limit' => 1]); + + $this->repository->shouldReceive('findCountWhere') + ->once() + ->with([['server_id', '=', $server->id]]) + ->andReturn(0); + + $this->databaseHostRepository->shouldReceive('setColumns->findWhere') + ->once() + ->with([['node_id', '=', $server->node_id]]) + ->andReturn(collect()); + + $this->getService()->handle($server, []); + } + + /** + * Test handling of random host selection. + */ + public function testRandomFoundHost() + { + $server = factory(Server::class)->make(['database_limit' => 1]); + $model = factory(Database::class)->make(); + + $this->repository->shouldReceive('findCountWhere') + ->once() + ->with([['server_id', '=', $server->id]]) + ->andReturn(0); + + $this->databaseHostRepository->shouldReceive('setColumns->findWhere') + ->once() + ->with([['node_id', '=', $server->node_id]]) + ->andReturn(collect()); + + $this->databaseHostRepository->shouldReceive('setColumns->all') + ->once() + ->andReturn(collect([$model])); + + $this->managementService->shouldReceive('create') + ->once() + ->with($server->id, [ + 'database_host_id' => $model->id, + 'database' => 'testdb', + 'remote' => null, + ]) + ->andReturn($model); + + $response = $this->getService()->handle($server, ['database' => 'testdb']); + + $this->assertInstanceOf(Database::class, $response); + $this->assertSame($model, $response); + } + + /** + * Test that an exception is thrown when no host is found and random is allowed. + * + * @expectedException \Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException + */ + public function testRandomNoHost() + { + $server = factory(Server::class)->make(['database_limit' => 1]); + + $this->repository->shouldReceive('findCountWhere') + ->once() + ->with([['server_id', '=', $server->id]]) + ->andReturn(0); + + $this->databaseHostRepository->shouldReceive('setColumns->findWhere') + ->once() + ->with([['node_id', '=', $server->node_id]]) + ->andReturn(collect()); + + $this->databaseHostRepository->shouldReceive('setColumns->all') + ->once() + ->andReturn(collect()); + + $this->getService()->handle($server, []); + } + + /** + * Test that a server over the database limit throws an exception. + * + * @dataProvider databaseExceedingLimitDataProvider + * @expectedException \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException + */ + public function testServerOverDatabaseLimit($limit, $count) + { + $server = factory(Server::class)->make(['database_limit' => $limit]); + + $this->repository->shouldReceive('findCountWhere') + ->once() + ->with([['server_id', '=', $server->id]]) + ->andReturn($count); + + $this->getService()->handle($server, []); + } + + /** + * Test that an exception is thrown if the feature is not enabled. + * + * @expectedException \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException + */ + public function testFeatureNotEnabled() + { + config()->set('pterodactyl.client_features.databases.enabled', false); + + $this->getService()->handle(factory(Server::class)->make(), []); + } + + /** + * Provide limits and current database counts for testing. + * + * @return array + */ + public function databaseLimitDataProvider(): array + { + return [ + [null, 10], + [1, 0], + ]; + } + + /** + * Provide data for servers over their database limit. + * + * @return array + */ + public function databaseExceedingLimitDataProvider(): array + { + return [ + [2, 2], + [2, 3], + ]; + } + + /** + * Return an instance of the service with mocked dependencies for testing. + * + * @return \Pterodactyl\Services\Databases\DeployServerDatabaseService + */ + private function getService(): DeployServerDatabaseService + { + return new DeployServerDatabaseService($this->repository, $this->databaseHostRepository, $this->managementService); + } +} From f3144b872f12382b9f38a52778085cedc07ee87f Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 2 Mar 2018 19:40:14 -0600 Subject: [PATCH 21/25] Set the hashing rounds low to speed up tests --- tests/TestCase.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/TestCase.php b/tests/TestCase.php index 427744f71..4d4ce896e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,6 +3,7 @@ namespace Tests; use Cake\Chronos\Chronos; +use Illuminate\Support\Facades\Hash; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase @@ -16,6 +17,7 @@ abstract class TestCase extends BaseTestCase { parent::setUp(); + Hash::setRounds(4); $this->setKnownUuidFactory(); } From c6137db529e0225e5322d8872c07179f67e5deac Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 2 Mar 2018 19:49:09 -0600 Subject: [PATCH 22/25] Fix build limit management in Admin CP --- app/Http/Controllers/Admin/ServersController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 6b9d44cbb..8adff82d4 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -498,15 +498,17 @@ class ServersController extends Controller * @param \Illuminate\Http\Request $request * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse + * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @internal param int $id + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function updateBuild(Request $request, Server $server) { $this->buildModificationService->handle($server, $request->only([ 'allocation_id', 'add_allocations', 'remove_allocations', 'memory', 'swap', 'io', 'cpu', 'disk', + 'database_limit', 'allocation_limit', ])); $this->alert->success(trans('admin/server.alerts.build_updated'))->flash(); From 021710aa1c6b726dc4eba2549703800c1d169906 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 2 Mar 2018 20:58:58 -0600 Subject: [PATCH 23/25] Add bulk power management via CLI --- CHANGELOG.md | 1 + .../Server/BulkPowerActionCommand.php | 119 ++++++++++++++++++ .../Repository/ServerRepositoryInterface.php | 19 +++ .../Eloquent/ServerRepository.php | 39 ++++++ resources/lang/en/command/messages.php | 4 + 5 files changed, 182 insertions(+) create mode 100644 app/Console/Commands/Server/BulkPowerActionCommand.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d182ef4a2..947098b6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * Added proper transformer for Packs and re-enabled missing includes on server. * Added support for using Filesystem as a caching driver, although not recommended. * Added support for user management of server databases. +* **Added bulk power management CLI interface to send start, stop, kill, restart actions to servers across configurable nodes.** ## v0.7.3 (Derelict Dermodactylus) ### Fixed diff --git a/app/Console/Commands/Server/BulkPowerActionCommand.php b/app/Console/Commands/Server/BulkPowerActionCommand.php new file mode 100644 index 000000000..dbe36718d --- /dev/null +++ b/app/Console/Commands/Server/BulkPowerActionCommand.php @@ -0,0 +1,119 @@ +powerRepository = $powerRepository; + $this->repository = $repository; + $this->validator = $validator; + } + + /** + * Handle the bulk power request. + * + * @throws \Pterodactyl\Exceptions\Repository\Daemon\InvalidPowerSignalException + */ + public function handle() + { + $action = $this->argument('action'); + $nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes')); + $servers = empty($this->option('servers')) ? [] : explode(',', $this->option('servers')); + + $validator = $this->validator->make([ + 'action' => $action, + 'nodes' => $nodes, + 'servers' => $servers, + ], [ + 'action' => 'string|in:start,stop,kill,restart', + 'nodes' => 'array', + 'nodes.*' => 'integer|min:1', + 'servers' => 'array', + 'servers.*' => 'integer|min:1', + ]); + + if ($validator->fails()) { + foreach ($validator->getMessageBag()->all() as $message) { + $this->output->error($message); + } + + return; + } + + $count = $this->repository->getServersForPowerActionCount($servers, $nodes); + if (! $this->confirm(trans('command/messages.server.power.confirm', ['action' => $action, 'count' => $count]))) { + return; + } + + $bar = $this->output->createProgressBar($count); + $servers = $this->repository->getServersForPowerAction($servers, $nodes); + + foreach ($servers as $server) { + $bar->clear(); + + try { + $this->powerRepository->setServer($server)->sendSignal($action); + } catch (RequestException $exception) { + $this->output->error(trans('command/messages.server.power.action_failed', [ + 'name' => $server->name, + 'id' => $server->id, + 'node' => $server->node->name, + 'message' => $exception->getMessage(), + ])); + } + + $bar->advance(); + $bar->display(); + } + + $this->line(''); + } +} diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index 0ca74bf40..983cf7e6e 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -117,4 +117,23 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function getByUuid(string $uuid): Server; + + /** + * Return all of the servers that should have a power action performed aganist them. + * + * @param int[] $servers + * @param int[] $nodes + * @param bool $returnCount + * @return int|\Generator + */ + public function getServersForPowerAction(array $servers = [], array $nodes = [], bool $returnCount = false); + + /** + * Return the total number of servers that will be affected by the query. + * + * @param int[] $servers + * @param int[] $nodes + * @return int + */ + public function getServersForPowerActionCount(array $servers = [], array $nodes = []): int; } diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index 7bca12691..5a53d33f0 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -264,6 +264,45 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt } } + /** + * Return all of the servers that should have a power action performed aganist them. + * + * @param int[] $servers + * @param int[] $nodes + * @param bool $returnCount + * @return int|\Generator + */ + public function getServersForPowerAction(array $servers = [], array $nodes = [], bool $returnCount = false) + { + $instance = $this->getBuilder(); + + if (! empty($nodes) && ! empty($servers)) { + $instance->whereIn('id', $servers)->orWhereIn('node_id', $nodes); + } elseif (empty($nodes) && ! empty($servers)) { + $instance->whereIn('id', $servers); + } elseif (! empty($nodes) && empty($servers)) { + $instance->whereIn('node_id', $nodes); + } + + if ($returnCount) { + return $instance->count(); + } + + return $instance->with('node')->cursor(); + } + + /** + * Return the total number of servers that will be affected by the query. + * + * @param int[] $servers + * @param int[] $nodes + * @return int + */ + public function getServersForPowerActionCount(array $servers = [], array $nodes = []): int + { + return $this->getServersForPowerAction($servers, $nodes, true); + } + /** * Return an array of server IDs that a given user can access based * on owner and subuser permissions. diff --git a/resources/lang/en/command/messages.php b/resources/lang/en/command/messages.php index 77f67c663..4a5250327 100644 --- a/resources/lang/en/command/messages.php +++ b/resources/lang/en/command/messages.php @@ -37,6 +37,10 @@ return [ ], 'server' => [ 'rebuild_failed' => 'Rebuild request for ":name" (#:id) on node ":node" failed with error: :message', + 'power' => [ + 'confirm' => 'You are about to perform a :action aganist :count servers. Do you wish to continue?', + 'action_failed' => 'Power action request for ":name" (#:id) on node ":node" failed with error: :message', + ], ], 'environment' => [ 'mail' => [ From 0135f7ee8e054c0ca64831d5d0717ee82655018a Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 2 Mar 2018 21:26:42 -0600 Subject: [PATCH 24/25] Add test for new command --- .../Server/BulkPowerActionCommand.php | 4 +- .../Server/BulkPowerActionCommandTest.php | 164 ++++++++++++++++++ 2 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/Commands/Server/BulkPowerActionCommandTest.php diff --git a/app/Console/Commands/Server/BulkPowerActionCommand.php b/app/Console/Commands/Server/BulkPowerActionCommand.php index dbe36718d..ced90db81 100644 --- a/app/Console/Commands/Server/BulkPowerActionCommand.php +++ b/app/Console/Commands/Server/BulkPowerActionCommand.php @@ -4,6 +4,7 @@ namespace Pterodactyl\Console\Commands\Server; use Illuminate\Console\Command; use GuzzleHttp\Exception\RequestException; +use Illuminate\Validation\ValidationException; use Illuminate\Validation\Factory as ValidatorFactory; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface; @@ -60,6 +61,7 @@ class BulkPowerActionCommand extends Command /** * Handle the bulk power request. * + * @throws \Illuminate\Validation\ValidationException * @throws \Pterodactyl\Exceptions\Repository\Daemon\InvalidPowerSignalException */ public function handle() @@ -85,7 +87,7 @@ class BulkPowerActionCommand extends Command $this->output->error($message); } - return; + throw new ValidationException($validator); } $count = $this->repository->getServersForPowerActionCount($servers, $nodes); diff --git a/tests/Unit/Commands/Server/BulkPowerActionCommandTest.php b/tests/Unit/Commands/Server/BulkPowerActionCommandTest.php new file mode 100644 index 000000000..684baf931 --- /dev/null +++ b/tests/Unit/Commands/Server/BulkPowerActionCommandTest.php @@ -0,0 +1,164 @@ +powerRepository = m::mock(PowerRepositoryInterface::class); + $this->repository = m::mock(ServerRepositoryInterface::class); + } + + /** + * Test that an action can be sent to all servers. + */ + public function testSendAction() + { + $servers = factory(Server::class)->times(2)->make(); + + $this->repository->shouldReceive('getServersForPowerActionCount') + ->once() + ->with([], []) + ->andReturn(2); + + $this->repository->shouldReceive('getServersForPowerAction') + ->once() + ->with([], []) + ->andReturn($servers); + + for ($i = 0; $i < count($servers); $i++) { + $this->powerRepository->shouldReceive('setServer->sendSignal') + ->once() + ->with('kill') + ->andReturnNull(); + } + + $display = $this->runCommand($this->getCommand(), ['action' => 'kill'], ['yes']); + + $this->assertNotEmpty($display); + $this->assertContains('2/2', $display); + $this->assertContains(trans('command/messages.server.power.confirm', ['action' => 'kill', 'count' => 2]), $display); + } + + /** + * Test filtering servers and nodes. + */ + public function testSendWithFilters() + { + $server = factory(Server::class)->make(); + + $this->repository->shouldReceive('getServersForPowerActionCount') + ->once() + ->with([1, 2], [3, 4]) + ->andReturn(1); + + $this->repository->shouldReceive('getServersForPowerAction') + ->once() + ->with([1, 2], [3, 4]) + ->andReturn([$server]); + + $this->powerRepository->shouldReceive('setServer->sendSignal') + ->once() + ->with('kill') + ->andReturnNull(); + + $display = $this->runCommand($this->getCommand(), [ + 'action' => 'kill', + '--servers' => '1,2', + '--nodes' => '3,4', + ], ['yes']); + + $this->assertNotEmpty($display); + $this->assertContains('1/1', $display); + $this->assertContains(trans('command/messages.server.power.confirm', ['action' => 'kill', 'count' => 1]), $display); + } + + /** + * Test that sending empty options returns the expected results. + */ + public function testSendWithEmptyOptions() + { + $server = factory(Server::class)->make(); + + $this->repository->shouldReceive('getServersForPowerActionCount') + ->once() + ->with([], []) + ->andReturn(1); + + $this->repository->shouldReceive('getServersForPowerAction')->once()->with([], [])->andReturn([$server]); + $this->powerRepository->shouldReceive('setServer->sendSignal')->once()->with('kill')->andReturnNull(); + + $display = $this->runCommand($this->getCommand(), [ + 'action' => 'kill', + '--servers' => '', + '--nodes' => '', + ], ['yes']); + + $this->assertNotEmpty($display); + $this->assertContains('1/1', $display); + $this->assertContains(trans('command/messages.server.power.confirm', ['action' => 'kill', 'count' => 1]), $display); + } + + /** + * Test that validation occurrs correctly. + * + * @param array $data + * + * @dataProvider validationFailureDataProvider + * @expectedException \Illuminate\Validation\ValidationException + */ + public function testValidationErrors(array $data) + { + $this->runCommand($this->getCommand(), $data); + } + + /** + * Provide invalid data for the command. + * + * @return array + */ + public function validationFailureDataProvider(): array + { + return [ + [['action' => 'hodor']], + [['action' => 'hodor', '--servers' => 'hodor']], + [['action' => 'kill', '--servers' => 'hodor']], + [['action' => 'kill', '--servers' => '1,2,3', '--nodes' => 'hodor']], + [['action' => 'kill', '--servers' => '1,2,3', '--nodes' => '1,2,test']], + ]; + } + + /** + * Return an instance of the command with mocked dependencies. + * + * @return \Pterodactyl\Console\Commands\Server\BulkPowerActionCommand + */ + private function getCommand(): BulkPowerActionCommand + { + return new BulkPowerActionCommand($this->powerRepository, $this->repository, $this->app->make(Factory::class)); + } +} From 95c1fe4fb726219a2c49e956e0023e6a995886eb Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 2 Mar 2018 21:28:30 -0600 Subject: [PATCH 25/25] bump for release :+1: --- config/app.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/app.php b/config/app.php index c1c94fa61..f95d7162d 100644 --- a/config/app.php +++ b/config/app.php @@ -9,7 +9,7 @@ return [ | change this value if you are not maintaining your own internal versions. */ - 'version' => 'canary', + 'version' => '0.7.4', /* |--------------------------------------------------------------------------