From 69e67b5e2dfa265d5350ed41536863493afdc548 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 19 Nov 2017 13:00:56 -0600 Subject: [PATCH 01/54] Push migration --- ...122708_MigratePubPrivFormatToSingleKey.php | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 database/migrations/2017_11_19_122708_MigratePubPrivFormatToSingleKey.php diff --git a/database/migrations/2017_11_19_122708_MigratePubPrivFormatToSingleKey.php b/database/migrations/2017_11_19_122708_MigratePubPrivFormatToSingleKey.php new file mode 100644 index 000000000..c2947ee07 --- /dev/null +++ b/database/migrations/2017_11_19_122708_MigratePubPrivFormatToSingleKey.php @@ -0,0 +1,59 @@ +get()->each(function ($item) { + try { + $decrypted = Crypt::decrypt($item->secret); + } catch (DecryptException $exception) { + $decrypted = str_random(32); + } finally { + DB::table('api_keys')->where('id', $item->id)->update([ + 'secret' => $decrypted, + ]); + } + }); + }); + + Schema::table('api_keys', function (Blueprint $table) { + $table->dropColumn('public'); + $table->string('secret', 32)->change(); + }); + + DB::statement('ALTER TABLE `api_keys` CHANGE `secret` `token` CHAR(32) NOT NULL, ADD UNIQUE INDEX `api_keys_token_unique` (`token`(32))'); + } + + /** + * Reverse the migrations. + */ + public function down() + { + DB::statement('ALTER TABLE `api_keys` CHANGE `token` `secret` TEXT, DROP INDEX `api_keys_token_unique`'); + + Schema::table('api_keys', function (Blueprint $table) { + $table->char('public', 16)->after('user_id'); + }); + + DB::transaction(function () { + DB::table('api_keys')->get()->each(function ($item) { + DB::table('api_keys')->where('id', $item->id)->update([ + 'public' => str_random(16), + 'secret' => Crypt::encrypt($item->secret), + ]); + }); + }); + } +} From 47e14ccaae97f65cc82709955f10907ef650ef9b Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 19 Nov 2017 13:32:17 -0600 Subject: [PATCH 02/54] API key UI changes and backend storage of the keys --- CHANGELOG.md | 4 + app/Http/Controllers/Base/APIController.php | 27 +------ app/Http/Middleware/API/AuthenticateKey.php | 40 ++++++++++ app/Models/APIKey.php | 15 +--- app/Services/Api/KeyCreationService.php | 43 +++-------- database/factories/ModelFactory.php | 11 +++ resources/lang/en/base.php | 2 +- resources/lang/en/strings.php | 2 +- .../pterodactyl/base/api/index.blade.php | 9 +-- .../Controllers/Base/APIControllerTest.php | 75 ++++++++----------- .../Services/Api/KeyCreationServiceTest.php | 68 ++++++++--------- 11 files changed, 136 insertions(+), 160 deletions(-) create mode 100644 app/Http/Middleware/API/AuthenticateKey.php diff --git a/CHANGELOG.md b/CHANGELOG.md index b0d21c7c5..822175062 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.0-beta.3 (Derelict Dermodactylus) +### Changed +* API keys have been changed to only use a single public key passed in a bearer token. All existing keys can continue being used, however only the first 32 characters should be sent. + ## v0.7.0-beta.2 (Derelict Dermodactylus) ### Fixed * `[beta.1]` — Fixes a CORS header issue due to a wrong API endpoint being provided in the administrative node listing. diff --git a/app/Http/Controllers/Base/APIController.php b/app/Http/Controllers/Base/APIController.php index 4bf89012f..c73661777 100644 --- a/app/Http/Controllers/Base/APIController.php +++ b/app/Http/Controllers/Base/APIController.php @@ -1,27 +1,4 @@ - * Some Modifications (c) 2015 Dylan Seidt . - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ namespace Pterodactyl\Http\Controllers\Base; @@ -120,7 +97,7 @@ class APIController extends Controller 'memo' => $request->input('memo'), ], $request->input('permissions', []), $adminPermissions); - $this->alert->success(trans('base.api.index.keypair_created', ['token' => $secret]))->flash(); + $this->alert->success(trans('base.api.index.keypair_created'))->flash(); return redirect()->route('account.api'); } @@ -136,7 +113,7 @@ class APIController extends Controller { $this->repository->deleteWhere([ ['user_id', '=', $request->user()->id], - ['public', '=', $key], + ['token', '=', $key], ]); return response('', 204); diff --git a/app/Http/Middleware/API/AuthenticateKey.php b/app/Http/Middleware/API/AuthenticateKey.php new file mode 100644 index 000000000..da833d891 --- /dev/null +++ b/app/Http/Middleware/API/AuthenticateKey.php @@ -0,0 +1,40 @@ +repository = $repository; + } + + /** + * Handle an API request by verifying that the provided API key + * is in a valid format, and the route being accessed is allowed + * for the given key. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + */ + public function handle(Request $request, Closure $next) + { + $this->repository->findFirstWhere([ + '', + ]); + } +} diff --git a/app/Models/APIKey.php b/app/Models/APIKey.php index cbfc996d7..0542fa022 100644 --- a/app/Models/APIKey.php +++ b/app/Models/APIKey.php @@ -19,6 +19,8 @@ class APIKey extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; + const KEY_LENGTH = 32; + /** * The table associated with the model. * @@ -26,13 +28,6 @@ class APIKey extends Model implements CleansAttributes, ValidableContract */ protected $table = 'api_keys'; - /** - * The attributes excluded from the model's JSON form. - * - * @var array - */ - protected $hidden = ['secret']; - /** * Cast values to correct type. * @@ -57,8 +52,7 @@ class APIKey extends Model implements CleansAttributes, ValidableContract protected static $applicationRules = [ 'memo' => 'required', 'user_id' => 'required', - 'secret' => 'required', - 'public' => 'required', + 'token' => 'required', ]; /** @@ -68,8 +62,7 @@ class APIKey extends Model implements CleansAttributes, ValidableContract */ protected static $dataIntegrityRules = [ 'user_id' => 'exists:users,id', - 'public' => 'string|size:16', - 'secret' => 'string', + 'token' => 'string|size:32', 'memo' => 'nullable|string|max:500', 'allowed_ips' => 'nullable|json', 'expires_at' => 'nullable|datetime', diff --git a/app/Services/Api/KeyCreationService.php b/app/Services/Api/KeyCreationService.php index 1a4312542..891a32438 100644 --- a/app/Services/Api/KeyCreationService.php +++ b/app/Services/Api/KeyCreationService.php @@ -1,60 +1,42 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Api; +use Pterodactyl\Models\APIKey; use Illuminate\Database\ConnectionInterface; -use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; class KeyCreationService { - const PUB_CRYPTO_LENGTH = 16; - const PRIV_CRYPTO_LENGTH = 64; - /** * @var \Illuminate\Database\ConnectionInterface */ - protected $connection; - - /** - * @var \Illuminate\Contracts\Encryption\Encrypter - */ - protected $encrypter; + private $connection; /** * @var \Pterodactyl\Services\Api\PermissionService */ - protected $permissionService; + private $permissionService; /** * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface */ - protected $repository; + private $repository; /** * ApiKeyService constructor. * * @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository * @param \Illuminate\Database\ConnectionInterface $connection - * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter * @param \Pterodactyl\Services\Api\PermissionService $permissionService */ public function __construct( ApiKeyRepositoryInterface $repository, ConnectionInterface $connection, - Encrypter $encrypter, PermissionService $permissionService ) { $this->repository = $repository; $this->connection = $connection; - $this->encrypter = $encrypter; $this->permissionService = $permissionService; } @@ -64,24 +46,17 @@ class KeyCreationService * @param array $data * @param array $permissions * @param array $administrative - * @return string + * @return \Pterodactyl\Models\APIKey * * @throws \Exception * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function handle(array $data, array $permissions, array $administrative = []) + public function handle(array $data, array $permissions, array $administrative = []): APIKey { - $publicKey = str_random(self::PUB_CRYPTO_LENGTH); - $secretKey = str_random(self::PRIV_CRYPTO_LENGTH); + $token = str_random(APIKey::KEY_LENGTH); + $data = array_merge($data, ['token' => $token]); - // Start a Transaction $this->connection->beginTransaction(); - - $data = array_merge($data, [ - 'public' => $publicKey, - 'secret' => $this->encrypter->encrypt($secretKey), - ]); - $instance = $this->repository->create($data, true, true); $nodes = $this->permissionService->getPermissions(); @@ -115,6 +90,6 @@ class KeyCreationService $this->connection->commit(); - return $secretKey; + return $instance; } } diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 1df550e0e..16c8d08bf 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -221,3 +221,14 @@ $factory->define(Pterodactyl\Models\DaemonKey::class, function (Faker\Generator 'expires_at' => \Carbon\Carbon::now()->addMinutes(10)->toDateTimeString(), ]; }); + +$factory->define(Pterodactyl\Models\APIKey::class, function (Faker\Generator $faker) { + return [ + 'id' => $faker->unique()->randomNumber(), + 'user_id' => $faker->randomNumber(), + 'token' => str_random(Pterodactyl\Models\APIKey::KEY_LENGTH), + 'memo' => 'Test Function Key', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), + ]; +}); diff --git a/resources/lang/en/base.php b/resources/lang/en/base.php index 9c7bd9d0d..234452da7 100644 --- a/resources/lang/en/base.php +++ b/resources/lang/en/base.php @@ -33,7 +33,7 @@ return [ 'header_sub' => 'Manage your API access keys.', 'list' => 'API Keys', 'create_new' => 'Create New API key', - 'keypair_created' => 'An API Key-Pair has been generated. Your API secret token is :token. Please take note of this key as it will not be displayed again.', + 'keypair_created' => 'An API Key-Pair has been generated and is listed below.', ], 'new' => [ 'header' => 'New API Key', diff --git a/resources/lang/en/strings.php b/resources/lang/en/strings.php index d98a7d01a..c9983e643 100644 --- a/resources/lang/en/strings.php +++ b/resources/lang/en/strings.php @@ -32,7 +32,7 @@ return [ 'memo' => 'Memo', 'created' => 'Created', 'expires' => 'Expires', - 'public_key' => 'Public key', + 'public_key' => 'Token', 'api_access' => 'Api Access', 'never' => 'never', 'sign_out' => 'Sign out', diff --git a/resources/themes/pterodactyl/base/api/index.blade.php b/resources/themes/pterodactyl/base/api/index.blade.php index a07657c96..0d8fc0a3e 100644 --- a/resources/themes/pterodactyl/base/api/index.blade.php +++ b/resources/themes/pterodactyl/base/api/index.blade.php @@ -20,12 +20,7 @@ @section('content')
-
- API functionality is disabled in this beta release. -
-
-

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

@@ -44,7 +39,7 @@ @foreach ($keys as $key) - {{ $key->public }} + {{ $key->token }} {{ $key->memo }} {{ (new Carbon($key->created_at))->toDayDateTimeString() }} @@ -57,7 +52,7 @@ @endif - + @endforeach diff --git a/tests/Unit/Http/Controllers/Base/APIControllerTest.php b/tests/Unit/Http/Controllers/Base/APIControllerTest.php index 055cbd84f..579fb43bb 100644 --- a/tests/Unit/Http/Controllers/Base/APIControllerTest.php +++ b/tests/Unit/Http/Controllers/Base/APIControllerTest.php @@ -1,54 +1,34 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Http\Controllers\Base; use Mockery as m; -use Tests\TestCase; -use Illuminate\Http\Request; use Pterodactyl\Models\User; +use Pterodactyl\Models\APIKey; use Prologue\Alerts\AlertsMessageBag; -use Tests\Assertions\ControllerAssertionsTrait; use Pterodactyl\Services\Api\KeyCreationService; +use Tests\Unit\Http\Controllers\ControllerTestCase; use Pterodactyl\Http\Controllers\Base\APIController; use Pterodactyl\Http\Requests\Base\ApiKeyFormRequest; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; -class APIControllerTest extends TestCase +class APIControllerTest extends ControllerTestCase { - use ControllerAssertionsTrait; - /** - * @var \Prologue\Alerts\AlertsMessageBag + * @var \Prologue\Alerts\AlertsMessageBag|\Mockery\Mock */ protected $alert; /** - * @var \Pterodactyl\Http\Controllers\Base\APIController - */ - protected $controller; - - /** - * @var \Pterodactyl\Services\Api\KeyCreationService + * @var \Pterodactyl\Services\Api\KeyCreationService|\Mockery\Mock */ protected $keyService; /** - * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface|\Mockery\Mock */ protected $repository; - /** - * @var \Illuminate\Http\Request - */ - protected $request; - /** * Setup tests. */ @@ -59,9 +39,6 @@ class APIControllerTest extends TestCase $this->alert = m::mock(AlertsMessageBag::class); $this->keyService = m::mock(KeyCreationService::class); $this->repository = m::mock(ApiKeyRepositoryInterface::class); - $this->request = m::mock(Request::class); - - $this->controller = new APIController($this->alert, $this->repository, $this->keyService); } /** @@ -69,12 +46,11 @@ class APIControllerTest extends TestCase */ public function testIndexController() { - $model = factory(User::class)->make(); + $model = $this->setRequestUser(); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); $this->repository->shouldReceive('findWhere')->with([['user_id', '=', $model->id]])->once()->andReturn(['testkeys']); - $response = $this->controller->index($this->request); + $response = $this->getController()->index($this->request); $this->assertIsViewResponse($response); $this->assertViewNameEquals('base.api.index', $response); $this->assertViewHasKey('keys', $response); @@ -88,10 +64,9 @@ class APIControllerTest extends TestCase */ public function testCreateController($admin) { - $model = factory(User::class)->make(['root_admin' => $admin]); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); + $this->setRequestUser(factory(User::class)->make(['root_admin' => $admin])); - $response = $this->controller->create($this->request); + $response = $this->getController()->create($this->request); $this->assertIsViewResponse($response); $this->assertViewNameEquals('base.api.new', $response); $this->assertViewHasKey('permissions.user', $response); @@ -111,8 +86,9 @@ class APIControllerTest extends TestCase */ public function testStoreController($admin) { - $this->request = m::mock(ApiKeyFormRequest::class); - $model = factory(User::class)->make(['root_admin' => $admin]); + $this->setRequestMockClass(ApiKeyFormRequest::class); + $model = $this->setRequestUser(factory(User::class)->make(['root_admin' => $admin])); + $keyModel = factory(APIKey::class)->make(); if ($admin) { $this->request->shouldReceive('input')->with('admin_permissions', [])->once()->andReturn(['admin.permission']); @@ -127,12 +103,12 @@ class APIControllerTest extends TestCase 'user_id' => $model->id, 'allowed_ips' => null, 'memo' => null, - ], ['test.permission'], ($admin) ? ['admin.permission'] : [])->once()->andReturn('testToken'); + ], ['test.permission'], ($admin) ? ['admin.permission'] : [])->once()->andReturn($keyModel); - $this->alert->shouldReceive('success')->with(trans('base.api.index.keypair_created', ['token' => 'testToken']))->once()->andReturnSelf() - ->shouldReceive('flash')->withNoArgs()->once()->andReturnNull(); + $this->alert->shouldReceive('success')->with(trans('base.api.index.keypair_created'))->once()->andReturnSelf(); + $this->alert->shouldReceive('flash')->withNoArgs()->once()->andReturnNull(); - $response = $this->controller->store($this->request); + $response = $this->getController()->store($this->request); $this->assertIsRedirectResponse($response); $this->assertRedirectRouteEquals('account.api', $response); } @@ -142,15 +118,14 @@ class APIControllerTest extends TestCase */ public function testRevokeController() { - $model = factory(User::class)->make(); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); + $model = $this->setRequestUser(); $this->repository->shouldReceive('deleteWhere')->with([ ['user_id', '=', $model->id], - ['public', '=', 'testKey123'], + ['token', '=', 'testKey123'], ])->once()->andReturnNull(); - $response = $this->controller->revoke($this->request, 'testKey123'); + $response = $this->getController()->revoke($this->request, 'testKey123'); $this->assertIsResponse($response); $this->assertEmpty($response->getContent()); $this->assertResponseCodeEquals(204, $response); @@ -165,4 +140,14 @@ class APIControllerTest extends TestCase { return [[0], [1]]; } + + /** + * Return an instance of the controller with mocked dependencies for testing. + * + * @return \Pterodactyl\Http\Controllers\Base\APIController + */ + private function getController(): APIController + { + return new APIController($this->alert, $this->repository, $this->keyService); + } } diff --git a/tests/Unit/Services/Api/KeyCreationServiceTest.php b/tests/Unit/Services/Api/KeyCreationServiceTest.php index 159d64255..49e0a2fde 100644 --- a/tests/Unit/Services/Api/KeyCreationServiceTest.php +++ b/tests/Unit/Services/Api/KeyCreationServiceTest.php @@ -12,8 +12,8 @@ namespace Tests\Unit\Services\Api; use Mockery as m; use Tests\TestCase; use phpmock\phpunit\PHPMock; +use Pterodactyl\Models\APIKey; use Illuminate\Database\ConnectionInterface; -use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Services\Api\PermissionService; use Pterodactyl\Services\Api\KeyCreationService; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; @@ -23,45 +23,30 @@ class KeyCreationServiceTest extends TestCase use PHPMock; /** - * @var \Illuminate\Database\ConnectionInterface + * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock */ - protected $connection; + private $connection; /** - * @var \Illuminate\Contracts\Encryption\Encrypter + * @var \Pterodactyl\Services\Api\PermissionService|\Mockery\Mock */ - protected $encrypter; + private $permissionService; /** - * @var \Pterodactyl\Services\Api\PermissionService + * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface|\Mockery\Mock */ - protected $permissions; + private $repository; /** - * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface + * Setup tests. */ - protected $repository; - - /** - * @var \Pterodactyl\Services\Api\KeyCreationService - */ - protected $service; - public function setUp() { parent::setUp(); $this->connection = m::mock(ConnectionInterface::class); - $this->encrypter = m::mock(Encrypter::class); - $this->permissions = m::mock(PermissionService::class); + $this->permissionService = m::mock(PermissionService::class); $this->repository = m::mock(ApiKeyRepositoryInterface::class); - - $this->service = new KeyCreationService( - $this->repository, - $this->connection, - $this->encrypter, - $this->permissions - ); } /** @@ -69,37 +54,48 @@ class KeyCreationServiceTest extends TestCase */ public function testKeyIsCreated() { + $model = factory(APIKey::class)->make(); + $this->getFunctionMock('\\Pterodactyl\\Services\\Api', 'str_random') - ->expects($this->exactly(2))->willReturn('random_string'); + ->expects($this->exactly(1))->with(APIKey::KEY_LENGTH)->willReturn($model->token); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->encrypter->shouldReceive('encrypt')->with('random_string')->once()->andReturn('encrypted-secret'); $this->repository->shouldReceive('create')->with([ 'test-data' => 'test', - 'public' => 'random_string', - 'secret' => 'encrypted-secret', - ], true, true)->once()->andReturn((object) ['id' => 1]); + 'token' => $model->token, + ], true, true)->once()->andReturn($model); - $this->permissions->shouldReceive('getPermissions')->withNoArgs()->once()->andReturn([ + $this->permissionService->shouldReceive('getPermissions')->withNoArgs()->once()->andReturn([ '_user' => ['server' => ['list', 'multiple-dash-test']], 'server' => ['create', 'admin-dash-test'], ]); - $this->permissions->shouldReceive('create')->with(1, 'user.server-list')->once()->andReturnNull(); - $this->permissions->shouldReceive('create')->with(1, 'user.server-multiple-dash-test')->once()->andReturnNull(); - $this->permissions->shouldReceive('create')->with(1, 'server-create')->once()->andReturnNull(); - $this->permissions->shouldReceive('create')->with(1, 'server-admin-dash-test')->once()->andReturnNull(); + $this->permissionService->shouldReceive('create')->with($model->id, 'user.server-list')->once()->andReturnNull(); + $this->permissionService->shouldReceive('create')->with($model->id, 'user.server-multiple-dash-test')->once()->andReturnNull(); + $this->permissionService->shouldReceive('create')->with($model->id, 'server-create')->once()->andReturnNull(); + $this->permissionService->shouldReceive('create')->with($model->id, 'server-admin-dash-test')->once()->andReturnNull(); $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - $response = $this->service->handle( + $response = $this->getService()->handle( ['test-data' => 'test'], ['invalid-node', 'server-list', 'server-multiple-dash-test'], ['invalid-node', 'server-create', 'server-admin-dash-test'] ); $this->assertNotEmpty($response); - $this->assertEquals('random_string', $response); + $this->assertInstanceOf(APIKey::class, $response); + $this->assertSame($model, $response); + } + + /** + * Return an instance of the service with mocked dependencies for testing. + * + * @return \Pterodactyl\Services\Api\KeyCreationService + */ + private function getService(): KeyCreationService + { + return new KeyCreationService($this->repository, $this->connection, $this->permissionService); } } From 45a153427e1072828e430d5a51b49a8ea358743b Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 19 Nov 2017 14:05:13 -0600 Subject: [PATCH 03/54] Add new API middleware --- app/Http/Kernel.php | 14 ++--- .../Middleware/API/AuthenticateIPAccess.php | 39 ++++++++++++++ app/Http/Middleware/API/AuthenticateKey.php | 44 ++++++++++++++-- app/Http/Middleware/API/SetSessionDriver.php | 52 +++++++++++++++++++ app/Providers/RouteServiceProvider.php | 14 ++--- 5 files changed, 144 insertions(+), 19 deletions(-) create mode 100644 app/Http/Middleware/API/AuthenticateIPAccess.php create mode 100644 app/Http/Middleware/API/SetSessionDriver.php diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 79c6e9ecf..573a88120 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -11,15 +11,17 @@ use Pterodactyl\Http\Middleware\EncryptCookies; use Pterodactyl\Http\Middleware\VerifyCsrfToken; use Pterodactyl\Http\Middleware\VerifyReCaptcha; use Pterodactyl\Http\Middleware\AdminAuthenticate; -use Pterodactyl\Http\Middleware\HMACAuthorization; use Illuminate\Routing\Middleware\ThrottleRequests; use Pterodactyl\Http\Middleware\LanguageMiddleware; use Illuminate\Foundation\Http\Kernel as HttpKernel; +use Pterodactyl\Http\Middleware\API\AuthenticateKey; use Illuminate\Routing\Middleware\SubstituteBindings; use Pterodactyl\Http\Middleware\AccessingValidServer; +use Pterodactyl\Http\Middleware\API\SetSessionDriver; use Illuminate\View\Middleware\ShareErrorsFromSession; use Pterodactyl\Http\Middleware\RedirectIfAuthenticated; use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; +use Pterodactyl\Http\Middleware\API\AuthenticateIPAccess; use Pterodactyl\Http\Middleware\Daemon\DaemonAuthenticate; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Pterodactyl\Http\Middleware\Server\AuthenticateAsSubuser; @@ -42,10 +44,6 @@ class Kernel extends HttpKernel EncryptCookies::class, AddQueuedCookiesToResponse::class, TrimStrings::class, - - /* - * Custom middleware applied to all routes. - */ TrustProxies::class, ]; @@ -66,9 +64,11 @@ class Kernel extends HttpKernel RequireTwoFactorAuthentication::class, ], 'api' => [ - HMACAuthorization::class, 'throttle:60,1', - 'bindings', + SubstituteBindings::class, + SetSessionDriver::class, + AuthenticateKey::class, + AuthenticateIPAccess::class, ], 'daemon' => [ SubstituteBindings::class, diff --git a/app/Http/Middleware/API/AuthenticateIPAccess.php b/app/Http/Middleware/API/AuthenticateIPAccess.php new file mode 100644 index 000000000..b3d1d5c37 --- /dev/null +++ b/app/Http/Middleware/API/AuthenticateIPAccess.php @@ -0,0 +1,39 @@ +attributes->get('api_key'); + + if (is_null($model->allowed_ips) || empty($model->allowed_ips)) { + return $next($request); + } + + foreach ($model->allowed_ips as $ip) { + if (Range::parse($ip)->contains(new IP($request->ip()))) { + return $next($request); + } + } + + throw new AccessDeniedHttpException('This IP address does not have permission to access the API using these credentials.'); + } +} diff --git a/app/Http/Middleware/API/AuthenticateKey.php b/app/Http/Middleware/API/AuthenticateKey.php index da833d891..0bad6dcb2 100644 --- a/app/Http/Middleware/API/AuthenticateKey.php +++ b/app/Http/Middleware/API/AuthenticateKey.php @@ -4,10 +4,25 @@ namespace Pterodactyl\Http\Middleware\API; use Closure; use Illuminate\Http\Request; +use Illuminate\Auth\AuthManager; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; +use Illuminate\Contracts\Config\Repository as ConfigRepository; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; class AuthenticateKey { + /** + * @var \Illuminate\Auth\AuthManager + */ + private $auth; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ + private $config; + /** * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface */ @@ -17,9 +32,16 @@ class AuthenticateKey * AuthenticateKey constructor. * * @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository + * @param \Illuminate\Auth\AuthManager $auth + * @param \Illuminate\Contracts\Config\Repository $config */ - public function __construct(ApiKeyRepositoryInterface $repository) - { + public function __construct( + ApiKeyRepositoryInterface $repository, + AuthManager $auth, + ConfigRepository $config + ) { + $this->auth = $auth; + $this->config = $config; $this->repository = $repository; } @@ -30,11 +52,23 @@ class AuthenticateKey * * @param \Illuminate\Http\Request $request * @param \Closure $next + * @return mixed */ public function handle(Request $request, Closure $next) { - $this->repository->findFirstWhere([ - '', - ]); + if (is_null($request->bearerToken())) { + throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']); + } + + try { + $model = $this->repository->findFirstWhere([['token', '=', $request->bearerToken()]]); + } catch (RecordNotFoundException $exception) { + throw new AccessDeniedHttpException; + } + + $this->auth->guard()->loginUsingId($model->user_id); + $request->attributes->set('api_key', $model); + + return $next($request); } } diff --git a/app/Http/Middleware/API/SetSessionDriver.php b/app/Http/Middleware/API/SetSessionDriver.php new file mode 100644 index 000000000..9cc5d60e3 --- /dev/null +++ b/app/Http/Middleware/API/SetSessionDriver.php @@ -0,0 +1,52 @@ +app = $app; + $this->config = $config; + } + + /** + * Set the session for API calls to only last for the one request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle(Request $request, Closure $next) + { + if ($this->app->environment() !== 'production') { + $this->app->make(LaravelDebugbar::class)->disable(); + } + + $this->config->set('session.driver', 'array'); + + return $next($request); + } +} diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 57ae43fad..4d8f9b2b6 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -29,13 +29,9 @@ class RouteServiceProvider extends ServiceProvider */ public function map() { - Route::middleware(['api'])->prefix('/api/user') - ->namespace($this->namespace . '\API\User') - ->group(base_path('routes/api.php')); - - Route::middleware(['api'])->prefix('/api/admin') - ->namespace($this->namespace . '\API\Admin') - ->group(base_path('routes/api-admin.php')); +// Route::middleware(['api'])->prefix('/api/user') +// ->namespace($this->namespace . '\API\User') +// ->group(base_path('routes/api.php')); Route::middleware(['web', 'auth', 'csrf']) ->namespace($this->namespace . '\Base') @@ -53,6 +49,10 @@ class RouteServiceProvider extends ServiceProvider ->namespace($this->namespace . '\Server') ->group(base_path('routes/server.php')); + Route::middleware(['api'])->prefix('/api/admin') + ->namespace($this->namespace . '\API\Admin') + ->group(base_path('routes/api-admin.php')); + Route::middleware(['daemon'])->prefix('/api/remote') ->namespace($this->namespace . '\API\Remote') ->group(base_path('routes/api-remote.php')); From 49379bd1154827a73d7eeaf8f141ff20f42788a0 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 19 Nov 2017 14:34:55 -0600 Subject: [PATCH 04/54] Pop some tests for new middleware in there. --- app/Http/Middleware/API/AuthenticateKey.php | 14 +-- .../API/AuthenticateIPAccessTest.php | 82 +++++++++++++++++ .../Middleware/API/AuthenticateKeyTest.php | 90 +++++++++++++++++++ .../Middleware/API/SetSessionDriverTest.php | 69 ++++++++++++++ 4 files changed, 245 insertions(+), 10 deletions(-) create mode 100644 tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php create mode 100644 tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php create mode 100644 tests/Unit/Http/Middleware/API/SetSessionDriverTest.php diff --git a/app/Http/Middleware/API/AuthenticateKey.php b/app/Http/Middleware/API/AuthenticateKey.php index 0bad6dcb2..eb82682cc 100644 --- a/app/Http/Middleware/API/AuthenticateKey.php +++ b/app/Http/Middleware/API/AuthenticateKey.php @@ -7,7 +7,6 @@ use Illuminate\Http\Request; use Illuminate\Auth\AuthManager; use Symfony\Component\HttpKernel\Exception\HttpException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; -use Illuminate\Contracts\Config\Repository as ConfigRepository; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -18,11 +17,6 @@ class AuthenticateKey */ private $auth; - /** - * @var \Illuminate\Contracts\Config\Repository - */ - private $config; - /** * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface */ @@ -33,15 +27,12 @@ class AuthenticateKey * * @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository * @param \Illuminate\Auth\AuthManager $auth - * @param \Illuminate\Contracts\Config\Repository $config */ public function __construct( ApiKeyRepositoryInterface $repository, - AuthManager $auth, - ConfigRepository $config + AuthManager $auth ) { $this->auth = $auth; - $this->config = $config; $this->repository = $repository; } @@ -53,6 +44,9 @@ class AuthenticateKey * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed + * + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException */ public function handle(Request $request, Closure $next) { diff --git a/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php b/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php new file mode 100644 index 000000000..cd122f7cb --- /dev/null +++ b/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php @@ -0,0 +1,82 @@ +make(['allowed_ips' => []]); + $this->setRequestAttribute('api_key', $model); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test middleware works correctly when a valid IP accesses + * and there is an IP restriction. + */ + public function testWithValidIP() + { + $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'); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test that a CIDR range can be used. + */ + public function testValidIPAganistCIDRRange() + { + $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'); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test that an exception is thrown when an invalid IP address + * tries to connect and there is an IP restriction. + * + * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + */ + public function testWithInvalidIP() + { + $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->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Return an instance of the middleware to be used when testing. + * + * @return \Pterodactyl\Http\Middleware\API\AuthenticateIPAccess + */ + private function getMiddleware(): AuthenticateIPAccess + { + return new AuthenticateIPAccess(); + } +} diff --git a/tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php b/tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php new file mode 100644 index 000000000..e2032b588 --- /dev/null +++ b/tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php @@ -0,0 +1,90 @@ +auth = m::mock(AuthManager::class); + $this->repository = m::mock(ApiKeyRepositoryInterface::class); + } + + /** + * Test that a missing bearer token will throw an exception. + */ + public function testMissingBearerTokenThrowsException() + { + $this->request->shouldReceive('bearerToken')->withNoArgs()->once()->andReturnNull(); + + try { + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } catch (HttpException $exception) { + $this->assertEquals(401, $exception->getStatusCode()); + $this->assertEquals(['WWW-Authenticate' => 'Bearer'], $exception->getHeaders()); + } + } + + /** + * Test that an invalid API token throws an exception. + * + * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + */ + public function testInvalidTokenThrowsException() + { + $this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn('abcd1234'); + $this->repository->shouldReceive('findFirstWhere')->andThrow(new RecordNotFoundException); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test that a valid token can continue past the middleware. + */ + public function testValidToken() + { + $model = factory(APIKey::class)->make(); + + $this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn($model->token); + $this->repository->shouldReceive('findFirstWhere')->with([['token', '=', $model->token]])->once()->andReturn($model); + + $this->auth->shouldReceive('guard->loginUsingId')->with($model->user_id)->once()->andReturnNull(); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + $this->assertEquals($model, $this->request->attributes->get('api_key')); + } + + /** + * Return an instance of the middleware with mocked dependencies for testing. + * + * @return \Pterodactyl\Http\Middleware\API\AuthenticateKey + */ + private function getMiddleware(): AuthenticateKey + { + return new AuthenticateKey($this->repository, $this->auth); + } +} diff --git a/tests/Unit/Http/Middleware/API/SetSessionDriverTest.php b/tests/Unit/Http/Middleware/API/SetSessionDriverTest.php new file mode 100644 index 000000000..dd9fa3e04 --- /dev/null +++ b/tests/Unit/Http/Middleware/API/SetSessionDriverTest.php @@ -0,0 +1,69 @@ +appMock = m::mock(Application::class); + $this->config = m::mock(Repository::class); + } + + /** + * Test that a production environment does not try to disable debug bar. + */ + public function testProductionEnvironment() + { + $this->appMock->shouldReceive('environment')->withNoArgs()->once()->andReturn('production'); + $this->config->shouldReceive('set')->with('session.driver', 'array')->once()->andReturnNull(); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test that a local environment does disable debug bar. + */ + public function testLocalEnvironment() + { + $this->appMock->shouldReceive('environment')->withNoArgs()->once()->andReturn('local'); + $this->appMock->shouldReceive('make')->with(LaravelDebugbar::class)->once()->andReturnSelf(); + $this->appMock->shouldReceive('disable')->withNoArgs()->once()->andReturnNull(); + + $this->config->shouldReceive('set')->with('session.driver', 'array')->once()->andReturnNull(); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Return an instance of the middleware with mocked dependencies for testing. + * + * @return \Pterodactyl\Http\Middleware\API\SetSessionDriver + */ + private function getMiddleware(): SetSessionDriver + { + return new SetSessionDriver($this->appMock, $this->config); + } +} From bf9708fe4fac5ef761f1d9e0d42d95b7f7423671 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 19 Nov 2017 15:23:37 -0600 Subject: [PATCH 05/54] Add permissions checking to API middleware list --- .../Repository/ApiKeyRepositoryInterface.php | 10 + app/Http/Kernel.php | 4 + .../API/HasPermissionToResource.php | 58 +++++ app/Http/Middleware/HMACAuthorization.php | 209 ------------------ app/Providers/RouteServiceProvider.php | 2 +- .../Eloquent/ApiKeyRepository.php | 16 ++ database/factories/ModelFactory.php | 8 + .../API/HasPermissionToResourceTest.php | 109 +++++++++ 8 files changed, 206 insertions(+), 210 deletions(-) create mode 100644 app/Http/Middleware/API/HasPermissionToResource.php delete mode 100644 app/Http/Middleware/HMACAuthorization.php create mode 100644 tests/Unit/Http/Middleware/API/HasPermissionToResourceTest.php diff --git a/app/Contracts/Repository/ApiKeyRepositoryInterface.php b/app/Contracts/Repository/ApiKeyRepositoryInterface.php index 5b8f638ef..2fce09cd2 100644 --- a/app/Contracts/Repository/ApiKeyRepositoryInterface.php +++ b/app/Contracts/Repository/ApiKeyRepositoryInterface.php @@ -9,6 +9,16 @@ namespace Pterodactyl\Contracts\Repository; +use Pterodactyl\Models\APIKey; + interface ApiKeyRepositoryInterface extends RepositoryInterface { + /** + * Load permissions for a key onto the model. + * + * @param \Pterodactyl\Models\APIKey $model + * @param bool $refresh + * @return \Pterodactyl\Models\APIKey + */ + public function loadPermissions(APIKey $model, bool $refresh = false): APIKey; } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 573a88120..13a7b29c0 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -24,6 +24,7 @@ use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; use Pterodactyl\Http\Middleware\API\AuthenticateIPAccess; use Pterodactyl\Http\Middleware\Daemon\DaemonAuthenticate; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; +use Pterodactyl\Http\Middleware\API\HasPermissionToResource; use Pterodactyl\Http\Middleware\Server\AuthenticateAsSubuser; use Pterodactyl\Http\Middleware\Server\SubuserBelongsToServer; use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication; @@ -95,6 +96,9 @@ class Kernel extends HttpKernel 'bindings' => SubstituteBindings::class, 'recaptcha' => VerifyReCaptcha::class, + // API specific middleware. + 'api..user_level' => HasPermissionToResource::class, + // Server specific middleware (used for authenticating access to resources) // // These are only used for individual server authentication, and not gloabl diff --git a/app/Http/Middleware/API/HasPermissionToResource.php b/app/Http/Middleware/API/HasPermissionToResource.php new file mode 100644 index 000000000..1d99ffbf7 --- /dev/null +++ b/app/Http/Middleware/API/HasPermissionToResource.php @@ -0,0 +1,58 @@ +repository = $repository; + } + + /** + * Determine if an API key has permission to access the given route. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @param string $role + * @return mixed + */ + public function handle(Request $request, Closure $next, string $role = 'admin') + { + /** @var \Pterodactyl\Models\APIKey $model */ + $model = $request->attributes->get('api_key'); + + if ($role === 'admin' && ! $request->user()->root_admin) { + throw new NotFoundHttpException; + } + + $this->repository->loadPermissions($model); + $routeKey = str_replace(['api.', 'admin.'], '', $request->route()->getName()); + + $count = $model->getRelation('permissions')->filter(function ($permission) use ($routeKey) { + return $routeKey === str_replace('-', '.', $permission->permission); + })->count(); + + if ($count === 1) { + return $next($request); + } + + throw new AccessDeniedHttpException('Cannot access resource without required `' . $routeKey . '` permission.'); + } +} diff --git a/app/Http/Middleware/HMACAuthorization.php b/app/Http/Middleware/HMACAuthorization.php deleted file mode 100644 index fa048b59f..000000000 --- a/app/Http/Middleware/HMACAuthorization.php +++ /dev/null @@ -1,209 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Http\Middleware; - -use Auth; -use Crypt; -use Config; -use Closure; -use Debugbar; -use IPTools\IP; -use IPTools\Range; -use Illuminate\Http\Request; -use Pterodactyl\Models\APIKey; -use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; // 400 -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; // 403 - -class HMACAuthorization -{ - /** - * The algorithm to use for handling HMAC encryption. - * - * @var string - */ - const HMAC_ALGORITHM = 'sha256'; - - /** - * Stored values from the Authorization header. - * - * @var array - */ - protected $token = []; - - /** - * The eloquent model for the API key. - * - * @var \Pterodactyl\Models\APIKey - */ - protected $key; - - /** - * The illuminate request object. - * - * @var \Illuminate\Http\Request - */ - private $request; - - /** - * Construct class instance. - */ - public function __construct() - { - Debugbar::disable(); - Config::set('session.driver', 'array'); - } - - /** - * Handle an incoming request for the API. - * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @return mixed - */ - public function handle(Request $request, Closure $next) - { - $this->request = $request; - - $this->checkBearer(); - $this->validateRequest(); - $this->validateIPAccess(); - $this->validateContents(); - - Auth::loginUsingId($this->key()->user_id); - - return $next($request); - } - - /** - * Checks that the Bearer token is provided and in a valid format. - * - * - * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - */ - protected function checkBearer() - { - if (empty($this->request()->bearerToken())) { - throw new BadRequestHttpException('Request was missing required Authorization header.'); - } - - $token = explode('.', $this->request()->bearerToken()); - if (count($token) !== 2) { - throw new BadRequestHttpException('The Authorization header passed was not in a validate public/private key format.'); - } - - $this->token = [ - 'public' => $token[0], - 'hash' => $token[1], - ]; - } - - /** - * Determine if the request contains a valid public API key - * as well as permissions for the resource. - * - * - * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - */ - protected function validateRequest() - { - $this->key = APIKey::where('public', $this->public())->first(); - if (! $this->key) { - throw new AccessDeniedHttpException('Unable to identify requester. Authorization token is invalid.'); - } - } - - /** - * Determine if the requesting IP address is allowed to use this API key. - * - * @return bool - * - * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - */ - protected function validateIPAccess() - { - if (! is_null($this->key()->allowed_ips)) { - foreach (json_decode($this->key()->allowed_ips) as $ip) { - if (Range::parse($ip)->contains(new IP($this->request()->ip()))) { - return true; - } - } - - throw new AccessDeniedHttpException('This IP address does not have permission to access the API using these credentials.'); - } - - return true; - } - - /** - * Determine if the HMAC sent in the request matches the one generated - * on the panel side. - * - * - * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException - */ - protected function validateContents() - { - if (! hash_equals(base64_decode($this->hash()), $this->generateSignature())) { - throw new BadRequestHttpException('The HMAC for the request was invalid.'); - } - } - - /** - * Generate a HMAC from the request and known API secret key. - * - * @return string - */ - protected function generateSignature() - { - $content = urldecode($this->request()->fullUrl()) . $this->request()->getContent(); - - return hash_hmac(self::HMAC_ALGORITHM, $content, Crypt::decrypt($this->key()->secret), true); - } - - /** - * Return the public key passed in the Authorization header. - * - * @return string - */ - protected function public() - { - return $this->token['public']; - } - - /** - * Return the base64'd HMAC sent in the Authorization header. - * - * @return string - */ - protected function hash() - { - return $this->token['hash']; - } - - /** - * Return the API Key model. - * - * @return \Pterodactyl\Models\APIKey - */ - protected function key() - { - return $this->key; - } - - /** - * Return the Illuminate Request object. - * - * @return \Illuminate\Http\Request - */ - private function request() - { - return $this->request; - } -} diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 4d8f9b2b6..a0f902859 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -49,7 +49,7 @@ class RouteServiceProvider extends ServiceProvider ->namespace($this->namespace . '\Server') ->group(base_path('routes/server.php')); - Route::middleware(['api'])->prefix('/api/admin') + Route::middleware(['api', 'api..user_level:admin'])->prefix('/api/admin') ->namespace($this->namespace . '\API\Admin') ->group(base_path('routes/api-admin.php')); diff --git a/app/Repositories/Eloquent/ApiKeyRepository.php b/app/Repositories/Eloquent/ApiKeyRepository.php index facac39c8..107e0b6c9 100644 --- a/app/Repositories/Eloquent/ApiKeyRepository.php +++ b/app/Repositories/Eloquent/ApiKeyRepository.php @@ -21,4 +21,20 @@ class ApiKeyRepository extends EloquentRepository implements ApiKeyRepositoryInt { return APIKey::class; } + + /** + * Load permissions for a key onto the model. + * + * @param \Pterodactyl\Models\APIKey $model + * @param bool $refresh + * @return \Pterodactyl\Models\APIKey + */ + public function loadPermissions(APIKey $model, bool $refresh = false): APIKey + { + if (! $model->relationLoaded('permissions') || $refresh) { + $model->load('permissions'); + } + + return $model; + } } diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 16c8d08bf..e117b59bb 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -232,3 +232,11 @@ $factory->define(Pterodactyl\Models\APIKey::class, function (Faker\Generator $fa 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), ]; }); + +$factory->define(Pterodactyl\Models\APIPermission::class, function (Faker\Generator $faker) { + return [ + 'id' => $faker->unique()->randomNumber(), + 'key_id' => $faker->randomNumber(), + 'permission' => mb_strtolower($faker->word), + ]; +}); diff --git a/tests/Unit/Http/Middleware/API/HasPermissionToResourceTest.php b/tests/Unit/Http/Middleware/API/HasPermissionToResourceTest.php new file mode 100644 index 000000000..7ef6c3830 --- /dev/null +++ b/tests/Unit/Http/Middleware/API/HasPermissionToResourceTest.php @@ -0,0 +1,109 @@ +repository = m::mock(ApiKeyRepositoryInterface::class); + } + + /** + * Test that a non-admin user cannot access admin level routes. + * + * @expectedException \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + */ + public function testNonAdminAccessingAdminLevel() + { + $model = factory(APIKey::class)->make(); + $this->setRequestAttribute('api_key', $model); + $this->setRequestUser(factory(User::class)->make(['root_admin' => false])); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test non-admin accessing non-admin route. + */ + public function testAccessingAllowedRoute() + { + $model = factory(APIKey::class)->make(); + $model->setRelation('permissions', collect([ + factory(APIPermission::class)->make(['permission' => 'user.test-route']), + ])); + $this->setRequestAttribute('api_key', $model); + $this->setRequestUser(factory(User::class)->make(['root_admin' => false])); + + $this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('api.user.test.route'); + $this->repository->shouldReceive('loadPermissions')->with($model)->once()->andReturn($model); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), 'user'); + } + + /** + * Test admin accessing administrative route. + */ + public function testAccessingAllowedAdminRoute() + { + $model = factory(APIKey::class)->make(); + $model->setRelation('permissions', collect([ + factory(APIPermission::class)->make(['permission' => 'test-route']), + ])); + $this->setRequestAttribute('api_key', $model); + $this->setRequestUser(factory(User::class)->make(['root_admin' => true])); + + $this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('api.admin.test.route'); + $this->repository->shouldReceive('loadPermissions')->with($model)->once()->andReturn($model); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test a user accessing a disallowed route. + * + * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + */ + public function testAccessingDisallowedRoute() + { + $model = factory(APIKey::class)->make(); + $model->setRelation('permissions', collect([ + factory(APIPermission::class)->make(['permission' => 'user.other-route']), + ])); + $this->setRequestAttribute('api_key', $model); + $this->setRequestUser(factory(User::class)->make(['root_admin' => false])); + + $this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('api.user.test.route'); + $this->repository->shouldReceive('loadPermissions')->with($model)->once()->andReturn($model); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), 'user'); + } + + /** + * Return an instance of the middleware with mocked dependencies for testing. + * + * @return \Pterodactyl\Http\Middleware\API\HasPermissionToResource + */ + private function getMiddleware(): HasPermissionToResource + { + return new HasPermissionToResource($this->repository); + } +} From 698c121e1157bbde7700ecec07ddf30f5cb1a551 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 19 Nov 2017 16:30:00 -0600 Subject: [PATCH 06/54] First round of API additions --- .../Repository/RepositoryInterface.php | 3 +- .../API/Admin/Users/UserController.php | 73 ++++++++++++ app/Models/User.php | 2 +- .../Eloquent/EloquentRepository.php | 10 +- app/Transformers/Admin/UserTransformer.php | 59 +++------ app/Transformers/ApiTransformer.php | 39 ++++++ routes/api-admin.php | 112 +++--------------- spec/admin/swagger.yaml | 73 ++++++++++++ 8 files changed, 229 insertions(+), 142 deletions(-) create mode 100644 app/Http/Controllers/API/Admin/Users/UserController.php create mode 100644 app/Transformers/ApiTransformer.php create mode 100644 spec/admin/swagger.yaml diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php index f31e852d8..cfe5fde62 100644 --- a/app/Contracts/Repository/RepositoryInterface.php +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -168,9 +168,10 @@ interface RepositoryInterface /** * Return all records from the model. * + * @param null $paginate * @return mixed */ - public function all(); + public function all($paginate = null); /** * Insert a single or multiple records into the database at once skipping diff --git a/app/Http/Controllers/API/Admin/Users/UserController.php b/app/Http/Controllers/API/Admin/Users/UserController.php new file mode 100644 index 000000000..46243b19c --- /dev/null +++ b/app/Http/Controllers/API/Admin/Users/UserController.php @@ -0,0 +1,73 @@ +fractal = $fractal; + $this->repository = $repository; + $this->config = $config; + } + + /** + * Handle request to list all users on the panel. + * + * @param \Illuminate\Http\Request $request + * @return array + */ + public function index(Request $request) + { + $users = $this->repository->all($this->config->get('pterodactyl.paginate.api.users')); + + $fractal = $this->fractal->collection($users) + ->transformWith(new UserTransformer($request)) + ->withResourceName('user') + ->paginateWith(new IlluminatePaginatorAdapter($users)); + + if ($this->config->get('pterodactyl.api.include_on_list') && $request->input('include')) { + $fractal->parseIncludes(explode(',', $request->input('include'))); + } + + return $fractal->toArray(); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 39e4a0a03..f9291f469 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -89,7 +89,7 @@ class User extends Model implements * * @var array */ - protected $hidden = ['password', 'remember_token', 'totp_secret']; + protected $hidden = ['password', 'remember_token', 'totp_secret', 'totp_authenticated_at']; /** * Parameters for search querying. diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index d94cd5cac..f9124a718 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -184,14 +184,20 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf /** * {@inheritdoc} */ - public function all() + public function all($paginate = null) { + Assert::nullOrIntegerish($paginate, 'First argument passed to all must be null or integer, received %s.'); + $instance = $this->getBuilder(); if (is_subclass_of(get_called_class(), SearchableInterface::class)) { $instance = $instance->search($this->searchTerm); } - return $instance->get($this->getColumns()); + if (is_null($paginate)) { + return $instance->get($this->getColumns()); + } + + return $instance->paginate($paginate, $this->getColumns()); } /** diff --git a/app/Transformers/Admin/UserTransformer.php b/app/Transformers/Admin/UserTransformer.php index 323e72c4c..90547948a 100644 --- a/app/Transformers/Admin/UserTransformer.php +++ b/app/Transformers/Admin/UserTransformer.php @@ -1,57 +1,37 @@ . - * - * 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\User; -use League\Fractal\TransformerAbstract; +use Pterodactyl\Transformers\ApiTransformer; -class UserTransformer extends TransformerAbstract +class UserTransformer extends ApiTransformer { /** * List of resources that can be included. * * @var array */ - protected $availableIncludes = [ - 'access', - 'servers', - ]; - - /** - * The Illuminate Request object if provided. - * - * @var \Illuminate\Http\Request|bool - */ - protected $request; + protected $availableIncludes = ['servers']; /** * Setup request object for transformer. * - * @param \Illuminate\Http\Request|bool $request + * @param \Illuminate\Http\Request $request */ - public function __construct($request = false) + public function __construct(Request $request) { - 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. * + * @param \Pterodactyl\Models\User $user * @return array */ - public function transform(User $user) + public function transform(User $user): array { return $user->toArray(); } @@ -59,28 +39,21 @@ class UserTransformer extends TransformerAbstract /** * Return the servers associated with this user. * - * @return \Leauge\Fractal\Resource\Collection + * @param \Pterodactyl\Models\User $user + * @return bool|\League\Fractal\Resource\Collection + * + * @throws \Pterodactyl\Exceptions\PterodactylException */ public function includeServers(User $user) { - if ($this->request && ! $this->request->apiKeyHasPermission('server-list')) { - return; + if ($this->authorize('server-list')) { + return false; } - return $this->collection($user->servers, new ServerTransformer($this->request), 'server'); - } - - /** - * Return the servers that this user can access. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeAccess(User $user) - { - if ($this->request && ! $this->request->apiKeyHasPermission('server-list')) { - return; + if (! $user->relationLoaded('servers')) { + $user->load('servers'); } - return $this->collection($user->access()->get(), new ServerTransformer($this->request), 'server'); + return $this->collection($user->getRelation('servers'), new ServerTransformer($this->request), 'server'); } } diff --git a/app/Transformers/ApiTransformer.php b/app/Transformers/ApiTransformer.php new file mode 100644 index 000000000..d2334a079 --- /dev/null +++ b/app/Transformers/ApiTransformer.php @@ -0,0 +1,39 @@ +request->attributes->get('api_key'); + if (! $model->relationLoaded('permissions')) { + throw new PterodactylException('Permissions must be loaded onto a model before passing to transformer authorize function.'); + } + + $count = $model->getRelation('permissions')->filter(function ($model) use ($permission) { + return $model->permission === $permission; + })->count(); + + return $count > 0; + } +} diff --git a/routes/api-admin.php b/routes/api-admin.php index a36adf3b4..abd570ec9 100644 --- a/routes/api-admin.php +++ b/routes/api-admin.php @@ -1,97 +1,19 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ -//Route::get('/', 'CoreController@index'); -// -///* -//|-------------------------------------------------------------------------- -//| Server Controller Routes -//|-------------------------------------------------------------------------- -//| -//| Endpoint: /api/admin/servers -//| -//*/ -//Route::group(['prefix' => '/servers'], function () { -// Route::get('/', 'ServerController@index'); -// Route::get('/{id}', 'ServerController@view'); -// -// Route::post('/', 'ServerController@store'); -// -// Route::put('/{id}/details', 'ServerController@details'); -// Route::put('/{id}/container', 'ServerController@container'); -// Route::put('/{id}/build', 'ServerController@build'); -// Route::put('/{id}/startup', 'ServerController@startup'); -// -// Route::patch('/{id}/install', 'ServerController@install'); -// Route::patch('/{id}/rebuild', 'ServerController@rebuild'); -// Route::patch('/{id}/suspend', 'ServerController@suspend'); -// -// Route::delete('/{id}', 'ServerController@delete'); -//}); -// -///* -//|-------------------------------------------------------------------------- -//| Location Controller Routes -//|-------------------------------------------------------------------------- -//| -//| Endpoint: /api/admin/locations -//| -//*/ -//Route::group(['prefix' => '/locations'], function () { -// Route::get('/', 'LocationController@index'); -//}); -// -///* -//|-------------------------------------------------------------------------- -//| Node Controller Routes -//|-------------------------------------------------------------------------- -//| -//| Endpoint: /api/admin/nodes -//| -//*/ -//Route::group(['prefix' => '/nodes'], function () { -// Route::get('/', 'NodeController@index'); -// Route::get('/{id}', 'NodeController@view'); -// Route::get('/{id}/config', 'NodeController@viewConfig'); -// -// Route::post('/', 'NodeController@store'); -// -// Route::delete('/{id}', 'NodeController@delete'); -//}); -// -///* -//|-------------------------------------------------------------------------- -//| User Controller Routes -//|-------------------------------------------------------------------------- -//| -//| Endpoint: /api/admin/users -//| -//*/ -//Route::group(['prefix' => '/users'], function () { -// Route::get('/', 'UserController@index'); -// Route::get('/{id}', 'UserController@view'); -// -// Route::post('/', 'UserController@store'); -// -// Route::put('/{id}', 'UserController@update'); -// -// Route::delete('/{id}', 'UserController@delete'); -//}); -// -///* -//|-------------------------------------------------------------------------- -//| Service Controller Routes -//|-------------------------------------------------------------------------- -//| -//| Endpoint: /api/admin/services -//| -//*/ -//Route::group(['prefix' => '/services'], function () { -// Route::get('/', 'ServiceController@index'); -// Route::get('/{id}', 'ServiceController@view'); -//}); +|-------------------------------------------------------------------------- +| User Controller Routes +|-------------------------------------------------------------------------- +| +| Endpoint: /api/admin/users +| +*/ +Route::group(['prefix' => '/users'], function () { + Route::get('/', 'Users\UserController@index')->name('api.admin.user.list'); + Route::get('/{id}', 'Users\UserController@view'); + + Route::post('/', 'Users\UserController@store'); + Route::put('/{id}', 'Users\UserController@update'); + + Route::delete('/{id}', 'Users\UserController@delete'); +}); diff --git a/spec/admin/swagger.yaml b/spec/admin/swagger.yaml new file mode 100644 index 000000000..d05039970 --- /dev/null +++ b/spec/admin/swagger.yaml @@ -0,0 +1,73 @@ +swagger: "2.0" +info: + version: 1.0.0 + title: Pterodactyl Admin API Reference + description: Pterodactyl Panel API Documentation + contact: + name: Dane Everitt + url: https://pterodactyl.io + email: support@pterodactyl.io + license: + name: MIT +host: example.com +basePath: /api/admin +schemes: + - http + - https +consumes: + - application/vnd.pterodactyl.v1+json +produces: + - application/json +paths: + /users: + get: + description: | + Returns all users that exist on the Panel. + operationId: findUsers + responses: + "200": + description: OK + schema: + type: object + required: ["data"] + properties: + data: + type: array + items: + $ref: '#/definitions/User' + properties: + id: + type: integer + attributes: + type: object +definitions: + User: + allOf: + - required: + - email + - username + - uuid + properties: + external_id: + type: string + uuid: + type: string + email: + type: string + username: + type: string + name_first: + type: string + name_last: + type: string + language: + type: string + root_admin: + type: boolean + use_totp: + type: boolean + updated_at: + type: string + created_at: + type: string + From a10db204abe816c9017625e1f9c187db07f13391 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 26 Nov 2017 13:26:38 -0600 Subject: [PATCH 07/54] Misc fixes --- CHANGELOG.md | 1 + app/Http/Middleware/API/AuthenticateIPAccess.php | 3 ++- resources/lang/en/base.php | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fd5a82e8..825277fb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * `[beta.2]` — Fixes a bug that would throw a red page of death when submitting an invalid egg variable value for a server in the Admin CP. * `[beta.2]` — Someone found a `@todo` that I never `@todid` and thus database hosts could not be created without being linked to a node. This is fixed... * `[beta.2]` — Fixes bug that caused incorrect rendering of CPU usage on server graphs due to missing variable. + ### Changed * API keys have been changed to only use a single public key passed in a bearer token. All existing keys can continue being used, however only the first 32 characters should be sent. diff --git a/app/Http/Middleware/API/AuthenticateIPAccess.php b/app/Http/Middleware/API/AuthenticateIPAccess.php index b3d1d5c37..aa0af7e2e 100644 --- a/app/Http/Middleware/API/AuthenticateIPAccess.php +++ b/app/Http/Middleware/API/AuthenticateIPAccess.php @@ -28,8 +28,9 @@ class AuthenticateIPAccess return $next($request); } + $find = new IP($request->ip()); foreach ($model->allowed_ips as $ip) { - if (Range::parse($ip)->contains(new IP($request->ip()))) { + if (Range::parse($ip)->contains($find)) { return $next($request); } } diff --git a/resources/lang/en/base.php b/resources/lang/en/base.php index 234452da7..579ac3410 100644 --- a/resources/lang/en/base.php +++ b/resources/lang/en/base.php @@ -33,7 +33,7 @@ return [ 'header_sub' => 'Manage your API access keys.', 'list' => 'API Keys', 'create_new' => 'Create New API key', - 'keypair_created' => 'An API Key-Pair has been generated and is listed below.', + 'keypair_created' => 'An API key has been successfully generated and is listed below.', ], 'new' => [ 'header' => 'New API Key', From 95ba8da99e7e4a93b69cadb7690645563b881cf5 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 16 Dec 2017 10:49:43 -0600 Subject: [PATCH 08/54] Clean changelog --- CHANGELOG.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23ecaeb0f..226b1aaf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,16 +13,12 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * `[beta.2]` — Fixes bug causing schedules to be un-deletable. * `[beta.2]` — Fixes bug that prevented the deletion of nodes due to an allocation deletion cascade issue with the SQL schema. -### Changed -* Revoking the administrative status for an admin will revoke all authentication tokens currently assigned to their account. - ### Added * Added star indicators to user listing in Admin CP to indicate users who are set as a root admin. +* Settings are now editable via the Admin CP and override config values where possible. ### Changed -* API keys have been changed to only use a single public key passed in a bearer token. All existing keys can continue being used, however only the first 32 characters should be sent. - -### Changed +* Revoking the administrative status for an admin will revoke all authentication tokens currently assigned to their account. * API keys have been changed to only use a single public key passed in a bearer token. All existing keys can continue being used, however only the first 32 characters should be sent. ## v0.7.0-beta.2 (Derelict Dermodactylus) From 4a65dff9403c087de6adc38a49d5a0993d0b0830 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 16 Dec 2017 11:31:18 -0600 Subject: [PATCH 09/54] Implement admin user management API routes --- app/Exceptions/Handler.php | 1 + .../API/Admin/Users/UserController.php | 157 ++++++++++++++++-- routes/api-admin.php | 8 +- 3 files changed, 145 insertions(+), 21 deletions(-) diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index ed7c004b2..9f94b003e 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -71,6 +71,7 @@ class Handler extends ExceptionHandler $response = response()->json( [ 'error' => $displayError, + 'type' => (! config('app.debug')) ? null : class_basename($exception), 'http_code' => (method_exists($exception, 'getStatusCode')) ? $exception->getStatusCode() : 500, 'trace' => (! config('app.debug')) ? null : $exception->getTrace(), ], diff --git a/app/Http/Controllers/API/Admin/Users/UserController.php b/app/Http/Controllers/API/Admin/Users/UserController.php index 46243b19c..68b6c0062 100644 --- a/app/Http/Controllers/API/Admin/Users/UserController.php +++ b/app/Http/Controllers/API/Admin/Users/UserController.php @@ -4,20 +4,30 @@ namespace Pterodactyl\Http\Controllers\API\Admin\Users; use Spatie\Fractal\Fractal; use Illuminate\Http\Request; +use Pterodactyl\Models\User; +use Illuminate\Http\Response; +use Illuminate\Http\JsonResponse; use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Services\Users\UserUpdateService; +use Pterodactyl\Services\Users\UserCreationService; +use Pterodactyl\Services\Users\UserDeletionService; use Pterodactyl\Transformers\Admin\UserTransformer; +use Pterodactyl\Http\Requests\Admin\UserFormRequest; use League\Fractal\Pagination\IlluminatePaginatorAdapter; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -use Illuminate\Contracts\Config\Repository as ConfigRepository; -/** - * @SWG\Swagger( - * schemes={"https"}, - * basePath="/api/admin/users" - * ) - */ class UserController extends Controller { + /** + * @var \Pterodactyl\Services\Users\UserCreationService + */ + private $creationService; + + /** + * @var \Pterodactyl\Services\Users\UserDeletionService + */ + private $deletionService; + /** * @var \Spatie\Fractal\Fractal */ @@ -27,47 +37,160 @@ class UserController extends Controller * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface */ private $repository; + /** - * @var \Illuminate\Contracts\Config\Repository + * @var \Pterodactyl\Services\Users\UserUpdateService */ - private $config; + private $updateService; /** * UserController constructor. * - * @param \Illuminate\Contracts\Config\Repository $config * @param \Spatie\Fractal\Fractal $fractal * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository + * @param \Pterodactyl\Services\Users\UserCreationService $creationService + * @param \Pterodactyl\Services\Users\UserDeletionService $deletionService + * @param \Pterodactyl\Services\Users\UserUpdateService $updateService */ public function __construct( - ConfigRepository $config, Fractal $fractal, - UserRepositoryInterface $repository + UserRepositoryInterface $repository, + UserCreationService $creationService, + UserDeletionService $deletionService, + UserUpdateService $updateService ) { + $this->creationService = $creationService; + $this->deletionService = $deletionService; $this->fractal = $fractal; $this->repository = $repository; - $this->config = $config; + $this->updateService = $updateService; } /** - * Handle request to list all users on the panel. + * Handle request to list all users on the panel. Returns a JSONAPI representation + * of a collection of users including any defined relations passed in + * the request. * * @param \Illuminate\Http\Request $request * @return array */ - public function index(Request $request) + public function index(Request $request): array { - $users = $this->repository->all($this->config->get('pterodactyl.paginate.api.users')); + $users = $this->repository->all(config('pterodactyl.paginate.api.users')); $fractal = $this->fractal->collection($users) ->transformWith(new UserTransformer($request)) ->withResourceName('user') ->paginateWith(new IlluminatePaginatorAdapter($users)); - if ($this->config->get('pterodactyl.api.include_on_list') && $request->input('include')) { + if (config('pterodactyl.api.include_on_list') && $request->has('include')) { $fractal->parseIncludes(explode(',', $request->input('include'))); } return $fractal->toArray(); } + + /** + * Handle a request to view a single user. Includes any relations that + * were defined in the request. + * + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\User $user + * @return array + */ + public function view(Request $request, User $user): array + { + $fractal = $this->fractal->item($user) + ->transformWith(new UserTransformer($request)) + ->withResourceName('user'); + + if ($request->has('include')) { + $fractal->parseIncludes(explode(',', $request->input('include'))); + } + + return $fractal->toArray(); + } + + /** + * Update an existing user on the system and return the response. Returns the + * updated user model response on success. Supports handling of token revocation + * errors when switching a user from an admin to a normal user. + * + * Revocation errors are returned under the 'revocation_errors' key in the response + * meta. If there are no errors this is an empty array. + * + * @param \Pterodactyl\Http\Requests\Admin\UserFormRequest $request + * @param \Pterodactyl\Models\User $user + * @return array + * + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function update(UserFormRequest $request, User $user): array + { + $this->updateService->setUserLevel(User::USER_LEVEL_ADMIN); + $collection = $this->updateService->handle($user, $request->normalize()); + + $errors = []; + if (! empty($collection->get('exceptions'))) { + foreach ($collection->get('exceptions') as $node => $exception) { + /** @var \GuzzleHttp\Exception\RequestException $exception */ + /** @var \GuzzleHttp\Psr7\Response|null $response */ + $response = method_exists($exception, 'getResponse') ? $exception->getResponse() : null; + $message = trans('admin/server.exceptions.daemon_exception', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ]); + + $errors[] = ['message' => $message, 'node' => $node]; + } + } + + return $this->fractal->item($collection->get('user')) + ->transformWith(new UserTransformer($request)) + ->withResourceName('user') + ->addMeta([ + 'revocation_errors' => $errors, + ]) + ->toArray(); + } + + /** + * Store a new user on the system. Returns the created user and a HTTP/201 + * header on successful creation. + * + * @param \Pterodactyl\Http\Requests\Admin\UserFormRequest $request + * @return \Illuminate\Http\JsonResponse + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function store(UserFormRequest $request): JsonResponse + { + $user = $this->creationService->handle($request->normalize()); + + return $this->fractal->item($user) + ->transformWith(new UserTransformer($request)) + ->withResourceName('user') + ->addMeta([ + 'link' => route('api.admin.user.view', ['user' => $user->id]), + ]) + ->respond(201); + } + + /** + * Handle a request to delete a user from the Panel. Returns a HTTP/204 response + * on successful deletion. + * + * @param \Pterodactyl\Models\User $user + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function delete(User $user): Response + { + $this->deletionService->handle($user); + + return response('', 204); + } } diff --git a/routes/api-admin.php b/routes/api-admin.php index abd570ec9..884b00dbc 100644 --- a/routes/api-admin.php +++ b/routes/api-admin.php @@ -10,10 +10,10 @@ */ Route::group(['prefix' => '/users'], function () { Route::get('/', 'Users\UserController@index')->name('api.admin.user.list'); - Route::get('/{id}', 'Users\UserController@view'); + Route::get('/{user}', 'Users\UserController@view')->name('api.admin.user.view'); - Route::post('/', 'Users\UserController@store'); - Route::put('/{id}', 'Users\UserController@update'); + Route::post('/', 'Users\UserController@store')->name('api.admin.user.store'); + Route::put('/{user}', 'Users\UserController@update')->name('api.admin.user.update'); - Route::delete('/{id}', 'Users\UserController@delete'); + Route::delete('/{user}', 'Users\UserController@delete')->name('api.admin.user.delete'); }); From 0dcf2aaed69e9220be2ed6e2c4ce344161d396f8 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 16 Dec 2017 12:20:09 -0600 Subject: [PATCH 10/54] Inital upgrade to 5.5 This simply updates dependencies and gets all of the providers and config files updated based on what the laravel/laravel currently ships with --- CHANGELOG.md | 2 + app/Console/Kernel.php | 34 +- app/Http/Kernel.php | 8 +- app/Http/Middleware/TrustProxies.php | 29 + app/Providers/AppServiceProvider.php | 13 - app/Providers/EventServiceProvider.php | 8 - composer.json | 45 +- composer.lock | 1333 ++++++++++++++---------- config/app.php | 45 +- config/cache.php | 5 +- config/database.php | 2 +- config/filesystems.php | 12 +- config/session.php | 19 +- database/factories/ModelFactory.php | 38 +- phpunit.xml | 2 +- public/.htaccess | 13 +- public/index.php | 7 +- 17 files changed, 936 insertions(+), 679 deletions(-) create mode 100644 app/Http/Middleware/TrustProxies.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b2a85057..e9b6bcc73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. ### Changed * Revoking the administrative status for an admin will revoke all authentication tokens currently assigned to their account. +* Updated core framework to Laravel 5.5. This includes many dependency updates. +* Certain AWS specific environment keys were changed, this should have minimal impact on users unless you specifically enabled AWS specific features. The renames are: `AWS_KEY -> AWS_ACCESS_KEY_ID`, `AWS_SECRET -> AWS_SECRET_ACCESS_KEY`, `AWS_REGION -> AWS_DEFAULT_REGION` ### Added * Added star indicators to user listing in Admin CP to indicate users who are set as a root admin. diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 468561830..c87cd5394 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -3,41 +3,17 @@ namespace Pterodactyl\Console; use Illuminate\Console\Scheduling\Schedule; -use Pterodactyl\Console\Commands\InfoCommand; -use Pterodactyl\Console\Commands\User\MakeUserCommand; -use Pterodactyl\Console\Commands\User\DeleteUserCommand; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; -use Pterodactyl\Console\Commands\Server\RebuildServerCommand; -use Pterodactyl\Console\Commands\Location\MakeLocationCommand; -use Pterodactyl\Console\Commands\User\DisableTwoFactorCommand; -use Pterodactyl\Console\Commands\Environment\AppSettingsCommand; -use Pterodactyl\Console\Commands\Location\DeleteLocationCommand; -use Pterodactyl\Console\Commands\Schedule\ProcessRunnableCommand; -use Pterodactyl\Console\Commands\Environment\EmailSettingsCommand; -use Pterodactyl\Console\Commands\Environment\DatabaseSettingsCommand; -use Pterodactyl\Console\Commands\Maintenance\CleanServiceBackupFilesCommand; class Kernel extends ConsoleKernel { /** - * The Artisan commands provided by your application. - * - * @var array + * Register the commands for the application. */ - protected $commands = [ - AppSettingsCommand::class, - CleanServiceBackupFilesCommand::class, - DatabaseSettingsCommand::class, - DeleteLocationCommand::class, - DeleteUserCommand::class, - DisableTwoFactorCommand::class, - EmailSettingsCommand::class, - InfoCommand::class, - MakeLocationCommand::class, - MakeUserCommand::class, - ProcessRunnableCommand::class, - RebuildServerCommand::class, - ]; + protected function commands() + { + $this->load(__DIR__ . '/Commands'); + } /** * Define the application's command schedule. diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 13a7b29c0..9f4184884 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -2,10 +2,10 @@ namespace Pterodactyl\Http; -use Fideloper\Proxy\TrustProxies; use Illuminate\Auth\Middleware\Authorize; use Illuminate\Auth\Middleware\Authenticate; use Pterodactyl\Http\Middleware\TrimStrings; +use Pterodactyl\Http\Middleware\TrustProxies; use Illuminate\Session\Middleware\StartSession; use Pterodactyl\Http\Middleware\EncryptCookies; use Pterodactyl\Http\Middleware\VerifyCsrfToken; @@ -23,6 +23,7 @@ use Pterodactyl\Http\Middleware\RedirectIfAuthenticated; use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; use Pterodactyl\Http\Middleware\API\AuthenticateIPAccess; use Pterodactyl\Http\Middleware\Daemon\DaemonAuthenticate; +use Illuminate\Foundation\Http\Middleware\ValidatePostSize; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Pterodactyl\Http\Middleware\API\HasPermissionToResource; use Pterodactyl\Http\Middleware\Server\AuthenticateAsSubuser; @@ -31,6 +32,7 @@ use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication; 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\DaemonAuthenticate as OldDaemonAuthenticate; class Kernel extends HttpKernel @@ -42,9 +44,9 @@ class Kernel extends HttpKernel */ protected $middleware = [ CheckForMaintenanceMode::class, - EncryptCookies::class, - AddQueuedCookiesToResponse::class, + ValidatePostSize::class, TrimStrings::class, + ConvertEmptyStringsToNull::class, TrustProxies::class, ]; diff --git a/app/Http/Middleware/TrustProxies.php b/app/Http/Middleware/TrustProxies.php new file mode 100644 index 000000000..f44f1d6eb --- /dev/null +++ b/app/Http/Middleware/TrustProxies.php @@ -0,0 +1,29 @@ + 'FORWARDED', + Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR', + Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST', + Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT', + Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO', + ]; +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index b3a7c33ea..fdc7e2ac1 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -12,8 +12,6 @@ use Illuminate\Support\ServiceProvider; use Pterodactyl\Observers\UserObserver; use Pterodactyl\Observers\ServerObserver; use Pterodactyl\Observers\SubuserObserver; -use Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider; -use Barryvdh\Debugbar\ServiceProvider as DebugbarServiceProvider; class AppServiceProvider extends ServiceProvider { @@ -32,17 +30,6 @@ class AppServiceProvider extends ServiceProvider View::share('appIsGit', $this->versionData()['is_git'] ?? false); } - /** - * Register any application services. - */ - public function register() - { - if ($this->app->environment() !== 'production') { - $this->app->register(DebugbarServiceProvider::class); - $this->app->register(IdeHelperServiceProvider::class); - } - } - /** * Return version information for the footer. * diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index c7c928f1a..2ecc663fe 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -12,12 +12,4 @@ class EventServiceProvider extends ServiceProvider * @var array */ protected $listen = []; - - /** - * Register any other events for your application. - */ - public function boot() - { - parent::boot(); - } } diff --git a/composer.json b/composer.json index 6b1271f84..414ea2238 100644 --- a/composer.json +++ b/composer.json @@ -15,43 +15,40 @@ "ext-mbstring": "*", "ext-pdo_mysql": "*", "ext-zip": "*", - "appstract/laravel-blade-directives": "^0.6.0", - "aws/aws-sdk-php": "^3.29", - "daneeveritt/login-notifications": "^1.0", + "appstract/laravel-blade-directives": "^0.7", + "aws/aws-sdk-php": "^3.48", "doctrine/dbal": "^2.5", - "edvinaskrucas/settings": "^2.0", "fideloper/proxy": "^3.3", - "guzzlehttp/guzzle": "~6.3.0", + "guzzlehttp/guzzle": "^6.3", "hashids/hashids": "^2.0", "igaster/laravel-theme": "^1.16", "laracasts/utilities": "^3.0", - "laravel/framework": "5.4.27", - "laravel/tinker": "1.0.1", - "lord/laroute": "~2.4.5", + "laravel/framework": "5.5.*", + "laravel/tinker": "^1.0", + "lord/laroute": "^2.4", "matriphe/iso-639": "^1.2", "mtdowling/cron-expression": "^1.2", "nesbot/carbon": "^1.22", - "nicolaslopezj/searchable": "^1.9", "pragmarx/google2fa": "^2.0", "predis/predis": "^1.1", "prologue/alerts": "^0.4", "ramsey/uuid": "^3.7", "s1lentium/iptools": "^1.1", - "sofa/eloquence": "~5.4.1", - "spatie/laravel-fractal": "^4.0", - "watson/validating": "^3.0", + "sofa/eloquence-base": "v5.5", + "sofa/eloquence-validable": "v5.5", + "spatie/laravel-fractal": "^5.3", "webmozart/assert": "^1.2", - "webpatser/laravel-uuid": "^2.0", "znck/belongs-to-through": "^2.3" }, "require-dev": { - "barryvdh/laravel-debugbar": "^2.4", + "barryvdh/laravel-debugbar": "^3.1", "barryvdh/laravel-ide-helper": "^2.4", + "filp/whoops": "^2.1", "friendsofphp/php-cs-fixer": "^2.8.0", "fzaninotto/faker": "^1.6", - "mockery/mockery": "^0.9", - "php-mock/php-mock-phpunit": "^1.1", - "phpunit/phpunit": "^5.7" + "mockery/mockery": "^1.0", + "php-mock/php-mock-phpunit": "^2.0", + "phpunit/phpunit": "^6.5" }, "autoload": { "classmap": [ @@ -70,13 +67,15 @@ } }, "scripts": { - "post-install-cmd": [ - "Illuminate\\Foundation\\ComposerScripts::postInstall", - "php artisan optimize" + "post-root-package-install": [ + "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" ], - "post-update-cmd": [ - "Illuminate\\Foundation\\ComposerScripts::postUpdate", - "php artisan optimize" + "post-create-project-cmd": [ + "@php artisan key:generate" + ], + "post-autoload-dump": [ + "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", + "@php artisan package:discover" ] }, "prefer-stable": true, diff --git a/composer.lock b/composer.lock index 139e3c1c3..9faac0ced 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "bd42f43877e96cca4d4af755c590eb25", + "content-hash": "adc287e0991b3755ac9384a9eb9e58e0", "packages": [ { "name": "appstract/laravel-blade-directives", - "version": "0.6.0", + "version": "0.7.0", "source": { "type": "git", "url": "https://github.com/appstract/laravel-blade-directives.git", - "reference": "3b0e146b3f2511d69e51160e7312ed5e57616460" + "reference": "8e8627f191e968ae48a3ce0a7e9118f497a705fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appstract/laravel-blade-directives/zipball/3b0e146b3f2511d69e51160e7312ed5e57616460", - "reference": "3b0e146b3f2511d69e51160e7312ed5e57616460", + "url": "https://api.github.com/repos/appstract/laravel-blade-directives/zipball/8e8627f191e968ae48a3ce0a7e9118f497a705fb", + "reference": "8e8627f191e968ae48a3ce0a7e9118f497a705fb", "shasum": "" }, "require": { @@ -57,20 +57,20 @@ "appstract", "laravel-blade-directives" ], - "time": "2017-10-13T08:41:27+00:00" + "time": "2017-11-09T19:31:07+00:00" }, { "name": "aws/aws-sdk-php", - "version": "3.38.1", + "version": "3.48.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "9f704274f4748d2039a16d45b3388ed8dde74e89" + "reference": "f5aef5671cd7f6e3b9ede0a3ff17c131cc05f4d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/9f704274f4748d2039a16d45b3388ed8dde74e89", - "reference": "9f704274f4748d2039a16d45b3388ed8dde74e89", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f5aef5671cd7f6e3b9ede0a3ff17c131cc05f4d3", + "reference": "f5aef5671cd7f6e3b9ede0a3ff17c131cc05f4d3", "shasum": "" }, "require": { @@ -92,7 +92,7 @@ "ext-dom": "*", "ext-openssl": "*", "nette/neon": "^2.3", - "phpunit/phpunit": "^4.8.35|^5.4.0", + "phpunit/phpunit": "^4.8.35|^5.4.3", "psr/cache": "^1.0" }, "suggest": { @@ -137,52 +137,7 @@ "s3", "sdk" ], - "time": "2017-11-09T19:15:59+00:00" - }, - { - "name": "daneeveritt/login-notifications", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/DaneEveritt/login-notifications.git", - "reference": "a12e9b25e9a5e42d3f25c16579ba6dc2b8aba910" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/DaneEveritt/login-notifications/zipball/a12e9b25e9a5e42d3f25c16579ba6dc2b8aba910", - "reference": "a12e9b25e9a5e42d3f25c16579ba6dc2b8aba910", - "shasum": "" - }, - "require": { - "laravel/framework": "~5.3.0|~5.4.0", - "nesbot/carbon": "1.22.*", - "php": "^5.6|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "DaneEveritt\\LoginNotifications\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Dane Everitt", - "email": "dane@daneeveritt.com" - } - ], - "description": "Login notifications for Laravel", - "keywords": [ - "email", - "events", - "laravel", - "login", - "notifications" - ], - "time": "2017-04-14T20:57:26+00:00" + "time": "2017-12-15T19:49:31+00:00" }, { "name": "dnoegel/php-xdg-base-dir", @@ -688,22 +643,82 @@ "time": "2014-09-09T13:34:57+00:00" }, { - "name": "erusev/parsedown", - "version": "1.6.3", + "name": "egulias/email-validator", + "version": "2.1.3", "source": { "type": "git", - "url": "https://github.com/erusev/parsedown.git", - "reference": "728952b90a333b5c6f77f06ea9422b94b585878d" + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "1bec00a10039b823cc94eef4eddd47dcd3b2ca04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/erusev/parsedown/zipball/728952b90a333b5c6f77f06ea9422b94b585878d", - "reference": "728952b90a333b5c6f77f06ea9422b94b585878d", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/1bec00a10039b823cc94eef4eddd47dcd3b2ca04", + "reference": "1bec00a10039b823cc94eef4eddd47dcd3b2ca04", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1.0.1", + "php": ">= 5.5" + }, + "require-dev": { + "dominicsayers/isemail": "dev-master", + "phpunit/phpunit": "^4.8.35", + "satooshi/php-coveralls": "^1.0.1" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "EmailValidator" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "time": "2017-11-15T23:40:40+00:00" + }, + { + "name": "erusev/parsedown", + "version": "1.6.4", + "source": { + "type": "git", + "url": "https://github.com/erusev/parsedown.git", + "reference": "fbe3fe878f4fe69048bb8a52783a09802004f548" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/fbe3fe878f4fe69048bb8a52783a09802004f548", + "reference": "fbe3fe878f4fe69048bb8a52783a09802004f548", "shasum": "" }, "require": { "php": ">=5.3.0" }, + "require-dev": { + "phpunit/phpunit": "^4.8.35" + }, "type": "library", "autoload": { "psr-0": { @@ -727,7 +742,7 @@ "markdown", "parser" ], - "time": "2017-05-14T14:47:48+00:00" + "time": "2017-11-14T20:44:03+00:00" }, { "name": "fideloper/proxy", @@ -1235,39 +1250,40 @@ }, { "name": "laravel/framework", - "version": "v5.4.27", + "version": "v5.5.25", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "66f5e1b37cbd66e730ea18850ded6dc0ad570404" + "reference": "0a5b6112f325c56ae5a6679c08a0a10723153fe0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/66f5e1b37cbd66e730ea18850ded6dc0ad570404", - "reference": "66f5e1b37cbd66e730ea18850ded6dc0ad570404", + "url": "https://api.github.com/repos/laravel/framework/zipball/0a5b6112f325c56ae5a6679c08a0a10723153fe0", + "reference": "0a5b6112f325c56ae5a6679c08a0a10723153fe0", "shasum": "" }, "require": { - "doctrine/inflector": "~1.0", + "doctrine/inflector": "~1.1", "erusev/parsedown": "~1.6", "ext-mbstring": "*", "ext-openssl": "*", "league/flysystem": "~1.0", - "monolog/monolog": "~1.11", + "monolog/monolog": "~1.12", "mtdowling/cron-expression": "~1.0", "nesbot/carbon": "~1.20", - "paragonie/random_compat": "~1.4|~2.0", - "php": ">=5.6.4", + "php": ">=7.0", + "psr/container": "~1.0", + "psr/simple-cache": "^1.0", "ramsey/uuid": "~3.0", - "swiftmailer/swiftmailer": "~5.4", - "symfony/console": "~3.2", - "symfony/debug": "~3.2", - "symfony/finder": "~3.2", - "symfony/http-foundation": "~3.2", - "symfony/http-kernel": "~3.2", - "symfony/process": "~3.2", - "symfony/routing": "~3.2", - "symfony/var-dumper": "~3.2", + "swiftmailer/swiftmailer": "~6.0", + "symfony/console": "~3.3", + "symfony/debug": "~3.3", + "symfony/finder": "~3.3", + "symfony/http-foundation": "~3.3", + "symfony/http-kernel": "~3.3", + "symfony/process": "~3.3", + "symfony/routing": "~3.3", + "symfony/var-dumper": "~3.3", "tijsverkoyen/css-to-inline-styles": "~2.2", "vlucas/phpdotenv": "~2.2" }, @@ -1284,7 +1300,6 @@ "illuminate/database": "self.version", "illuminate/encryption": "self.version", "illuminate/events": "self.version", - "illuminate/exception": "self.version", "illuminate/filesystem": "self.version", "illuminate/hashing": "self.version", "illuminate/http": "self.version", @@ -1306,33 +1321,38 @@ "require-dev": { "aws/aws-sdk-php": "~3.0", "doctrine/dbal": "~2.5", - "mockery/mockery": "~0.9.4", + "filp/whoops": "^2.1.4", + "mockery/mockery": "~1.0", + "orchestra/testbench-core": "3.5.*", "pda/pheanstalk": "~3.0", - "phpunit/phpunit": "~5.7", - "predis/predis": "~1.0", - "symfony/css-selector": "~3.2", - "symfony/dom-crawler": "~3.2" + "phpunit/phpunit": "~6.0", + "predis/predis": "^1.1.1", + "symfony/css-selector": "~3.3", + "symfony/dom-crawler": "~3.3" }, "suggest": { "aws/aws-sdk-php": "Required to use the SQS queue driver and SES mail driver (~3.0).", "doctrine/dbal": "Required to rename columns and drop SQLite columns (~2.5).", + "ext-pcntl": "Required to use all features of the queue worker.", + "ext-posix": "Required to use all features of the queue worker.", "fzaninotto/faker": "Required to use the eloquent factory builder (~1.4).", "guzzlehttp/guzzle": "Required to use the Mailgun and Mandrill mail drivers and the ping methods on schedules (~6.0).", "laravel/tinker": "Required to use the tinker console command (~1.0).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (~1.0).", + "league/flysystem-cached-adapter": "Required to use Flysystem caching (~1.0).", "league/flysystem-rackspace": "Required to use the Flysystem Rackspace driver (~1.0).", "nexmo/client": "Required to use the Nexmo transport (~1.0).", "pda/pheanstalk": "Required to use the beanstalk queue driver (~3.0).", "predis/predis": "Required to use the redis cache and queue drivers (~1.0).", - "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (~2.0).", - "symfony/css-selector": "Required to use some of the crawler integration testing tools (~3.2).", - "symfony/dom-crawler": "Required to use most of the crawler integration testing tools (~3.2).", - "symfony/psr-http-message-bridge": "Required to psr7 bridging features (0.2.*)." + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (~3.0).", + "symfony/css-selector": "Required to use some of the crawler integration testing tools (~3.3).", + "symfony/dom-crawler": "Required to use most of the crawler integration testing tools (~3.3).", + "symfony/psr-http-message-bridge": "Required to psr7 bridging features (~1.0)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.4-dev" + "dev-master": "5.5-dev" } }, "autoload": { @@ -1360,20 +1380,20 @@ "framework", "laravel" ], - "time": "2017-06-15T19:08:25+00:00" + "time": "2017-12-11T14:59:28+00:00" }, { "name": "laravel/tinker", - "version": "v1.0.1", + "version": "v1.0.2", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "7eb2e281395131897407285672ef5532e87e17f9" + "reference": "203978fd67f118902acff95925847e70b72e3daf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/7eb2e281395131897407285672ef5532e87e17f9", - "reference": "7eb2e281395131897407285672ef5532e87e17f9", + "url": "https://api.github.com/repos/laravel/tinker/zipball/203978fd67f118902acff95925847e70b72e3daf", + "reference": "203978fd67f118902acff95925847e70b72e3daf", "shasum": "" }, "require": { @@ -1423,7 +1443,7 @@ "laravel", "psysh" ], - "time": "2017-06-01T16:31:26+00:00" + "time": "2017-07-13T13:11:05+00:00" }, { "name": "league/flysystem", @@ -1897,52 +1917,6 @@ ], "time": "2017-01-16T07:55:07+00:00" }, - { - "name": "nicolaslopezj/searchable", - "version": "1.9.6", - "source": { - "type": "git", - "url": "https://github.com/nicolaslopezj/searchable.git", - "reference": "c067f11a993c274c9da0d4a2e70ca07614bb92da" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nicolaslopezj/searchable/zipball/c067f11a993c274c9da0d4a2e70ca07614bb92da", - "reference": "c067f11a993c274c9da0d4a2e70ca07614bb92da", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "illuminate/database": "4.2.x|~5.0", - "php": ">=5.4.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Nicolaslopezj\\Searchable\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Lopez", - "email": "nicolaslopezj@me.com" - } - ], - "description": "Eloquent model search trait.", - "keywords": [ - "database", - "eloquent", - "laravel", - "model", - "search", - "searchable" - ], - "time": "2017-08-09T04:37:57+00:00" - }, { "name": "nikic/php-parser", "version": "v3.1.2", @@ -2265,6 +2239,55 @@ ], "time": "2017-01-24T13:22:25+00:00" }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14T16:28:37+00:00" + }, { "name": "psr/http-message", "version": "1.0.1", @@ -2363,17 +2386,65 @@ "time": "2016-10-10T12:19:37+00:00" }, { - "name": "psy/psysh", - "version": "v0.8.14", + "name": "psr/simple-cache", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/bobthecow/psysh.git", - "reference": "91e53c16560bdb8b9592544bb38429ae00d6baee" + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "753fa598e8f3b9966c886fe13f370baa45ef0e24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/91e53c16560bdb8b9592544bb38429ae00d6baee", - "reference": "91e53c16560bdb8b9592544bb38429ae00d6baee", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/753fa598e8f3b9966c886fe13f370baa45ef0e24", + "reference": "753fa598e8f3b9966c886fe13f370baa45ef0e24", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "time": "2017-01-02T13:31:39+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.8.16", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "d4c8eab0683dc056f2ca54ca67f5388527c068b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/d4c8eab0683dc056f2ca54ca67f5388527c068b1", + "reference": "d4c8eab0683dc056f2ca54ca67f5388527c068b1", "shasum": "" }, "require": { @@ -2381,14 +2452,13 @@ "jakub-onderka/php-console-highlighter": "0.3.*", "nikic/php-parser": "~1.3|~2.0|~3.0", "php": ">=5.3.9", - "symfony/console": "~2.3.10|^2.4.2|~3.0", - "symfony/var-dumper": "~2.7|~3.0" + "symfony/console": "~2.3.10|^2.4.2|~3.0|~4.0", + "symfony/var-dumper": "~2.7|~3.0|~4.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~1.11", "hoa/console": "~3.16|~1.14", - "phpunit/phpunit": "~4.4|~5.0", - "symfony/finder": "~2.1|~3.0" + "phpunit/phpunit": "^4.8.35|^5.4.3", + "symfony/finder": "~2.1|~3.0|~4.0" }, "suggest": { "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", @@ -2433,7 +2503,7 @@ "interactive", "shell" ], - "time": "2017-11-04T16:06:49+00:00" + "time": "2017-12-10T21:49:27+00:00" }, { "name": "ramsey/uuid", @@ -2568,23 +2638,23 @@ "time": "2016-08-21T15:57:09+00:00" }, { - "name": "sofa/eloquence", - "version": "5.4.1", + "name": "sofa/eloquence-base", + "version": "v5.5", "source": { "type": "git", - "url": "https://github.com/jarektkaczyk/eloquence.git", - "reference": "6f821bcf24950b58e633cb0cac7bbee6acb0f34a" + "url": "https://github.com/jarektkaczyk/eloquence-base.git", + "reference": "41e9b10073d0592b37437cdd06eea40a2b86f3e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jarektkaczyk/eloquence/zipball/6f821bcf24950b58e633cb0cac7bbee6acb0f34a", - "reference": "6f821bcf24950b58e633cb0cac7bbee6acb0f34a", + "url": "https://api.github.com/repos/jarektkaczyk/eloquence-base/zipball/41e9b10073d0592b37437cdd06eea40a2b86f3e0", + "reference": "41e9b10073d0592b37437cdd06eea40a2b86f3e0", "shasum": "" }, "require": { - "illuminate/database": "5.4.*", - "php": ">=5.6.4", - "sofa/hookable": "5.4.*" + "illuminate/database": "5.5.*", + "php": ">=7.0.0", + "sofa/hookable": "5.5.*" }, "require-dev": { "mockery/mockery": "0.9.4", @@ -2592,6 +2662,13 @@ "squizlabs/php_codesniffer": "2.3.3" }, "type": "library", + "extra": { + "laravel": { + "providers": [ + "Sofa\\Eloquence\\BaseServiceProvider" + ] + } + }, "autoload": { "psr-4": { "Sofa\\Eloquence\\": "src" @@ -2608,7 +2685,7 @@ { "name": "Jarek Tkaczyk", "email": "jarek@softonsofa.com", - "homepage": "http://softonsofa.com/", + "homepage": "https://softonsofa.com/", "role": "Developer" } ], @@ -2621,24 +2698,76 @@ "mutable", "searchable" ], - "time": "2017-04-22T14:38:11+00:00" + "time": "2017-10-13T14:26:50+00:00" }, { - "name": "sofa/hookable", - "version": "5.4.1", + "name": "sofa/eloquence-validable", + "version": "v5.5", "source": { "type": "git", - "url": "https://github.com/jarektkaczyk/hookable.git", - "reference": "1791d001bdf483136a11b3ea600462f446b82401" + "url": "https://github.com/jarektkaczyk/eloquence-validable.git", + "reference": "ac93ec8180558d3c70328de166c33a765732bb12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jarektkaczyk/hookable/zipball/1791d001bdf483136a11b3ea600462f446b82401", - "reference": "1791d001bdf483136a11b3ea600462f446b82401", + "url": "https://api.github.com/repos/jarektkaczyk/eloquence-validable/zipball/ac93ec8180558d3c70328de166c33a765732bb12", + "reference": "ac93ec8180558d3c70328de166c33a765732bb12", "shasum": "" }, "require": { - "illuminate/database": "5.3.*|5.4.*", + "php": ">=7.0.0", + "sofa/eloquence-base": "5.5.*" + }, + "require-dev": { + "mockery/mockery": "0.9.4", + "phpunit/phpunit": "4.5.0", + "squizlabs/php_codesniffer": "2.3.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Sofa\\Eloquence\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jarek Tkaczyk", + "email": "jarek@softonsofa.com", + "homepage": "https://softonsofa.com/", + "role": "Developer" + } + ], + "description": "Flexible Searchable, Mappable, Metable, Validation and more extensions for Laravel Eloquent ORM.", + "keywords": [ + "eloquent", + "laravel", + "mappable", + "metable", + "mutable", + "searchable" + ], + "time": "2017-10-13T14:42:08+00:00" + }, + { + "name": "sofa/hookable", + "version": "v5.5.1", + "source": { + "type": "git", + "url": "https://github.com/jarektkaczyk/hookable.git", + "reference": "7eb58b5cadeebca4ef74d5bd742dd4ff93348524" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jarektkaczyk/hookable/zipball/7eb58b5cadeebca4ef74d5bd742dd4ff93348524", + "reference": "7eb58b5cadeebca4ef74d5bd742dd4ff93348524", + "shasum": "" + }, + "require": { + "illuminate/database": "5.3.*|5.4.*|5.5.*", "php": ">=5.6.4" }, "require-dev": { @@ -2667,7 +2796,7 @@ "eloquent", "laravel" ], - "time": "2017-05-27T15:48:52+00:00" + "time": "2017-11-17T14:11:31+00:00" }, { "name": "spatie/fractalistic", @@ -2722,27 +2851,27 @@ }, { "name": "spatie/laravel-fractal", - "version": "4.5.0", + "version": "5.3.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-fractal.git", - "reference": "445364122d4e6d6da6f599939962e689668c2b5f" + "reference": "f395eb645cd454b209bc628f974ed137d16e03da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-fractal/zipball/445364122d4e6d6da6f599939962e689668c2b5f", - "reference": "445364122d4e6d6da6f599939962e689668c2b5f", + "url": "https://api.github.com/repos/spatie/laravel-fractal/zipball/f395eb645cd454b209bc628f974ed137d16e03da", + "reference": "f395eb645cd454b209bc628f974ed137d16e03da", "shasum": "" }, "require": { - "illuminate/contracts": "~5.1.0|~5.2.0|~5.3.0|~5.4.0", - "illuminate/support": "~5.1.0|~5.2.0|~5.3.0|~5.4.0", - "php": "^5.6|^7.0", + "illuminate/contracts": "~5.5.0", + "illuminate/support": "~5.5.0", + "php": "^7.0", "spatie/fractalistic": "^2.5" }, "require-dev": { - "orchestra/testbench": "~3.2.0|3.3.0|~3.4.0", - "phpunit/phpunit": "^5.7.5" + "orchestra/testbench": "~3.5.0", + "phpunit/phpunit": "^6.2" }, "type": "library", "extra": { @@ -2786,33 +2915,34 @@ "spatie", "transform" ], - "time": "2017-08-22T17:52:05+00:00" + "time": "2017-11-28T16:33:26+00:00" }, { "name": "swiftmailer/swiftmailer", - "version": "v5.4.8", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/swiftmailer/swiftmailer.git", - "reference": "9a06dc570a0367850280eefd3f1dc2da45aef517" + "reference": "412333372fb6c8ffb65496a2bbd7321af75733fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/9a06dc570a0367850280eefd3f1dc2da45aef517", - "reference": "9a06dc570a0367850280eefd3f1dc2da45aef517", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/412333372fb6c8ffb65496a2bbd7321af75733fc", + "reference": "412333372fb6c8ffb65496a2bbd7321af75733fc", "shasum": "" }, "require": { - "php": ">=5.3.3" + "egulias/email-validator": "~2.0", + "php": ">=7.0.0" }, "require-dev": { "mockery/mockery": "~0.9.1", - "symfony/phpunit-bridge": "~3.2" + "symfony/phpunit-bridge": "~3.3@dev" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.4-dev" + "dev-master": "6.0-dev" } }, "autoload": { @@ -2834,13 +2964,13 @@ } ], "description": "Swiftmailer, free feature-rich PHP mailer", - "homepage": "http://swiftmailer.org", + "homepage": "http://swiftmailer.symfony.com", "keywords": [ "email", "mail", "mailer" ], - "time": "2017-05-01T15:54:03+00:00" + "time": "2017-09-30T22:39:41+00:00" }, { "name": "symfony/console", @@ -3795,56 +3925,6 @@ ], "time": "2016-09-01T10:05:43+00:00" }, - { - "name": "watson/validating", - "version": "3.1.2", - "source": { - "type": "git", - "url": "https://github.com/dwightwatson/validating.git", - "reference": "22edd06d45893f5d4f79c9e901bd7fbce174a79f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/dwightwatson/validating/zipball/22edd06d45893f5d4f79c9e901bd7fbce174a79f", - "reference": "22edd06d45893f5d4f79c9e901bd7fbce174a79f", - "shasum": "" - }, - "require": { - "illuminate/contracts": ">=5.3", - "illuminate/database": ">=5.3", - "illuminate/events": ">=5.3", - "illuminate/support": ">=5.3", - "illuminate/validation": ">=5.3", - "php": ">=5.4.0" - }, - "require-dev": { - "mockery/mockery": "0.9.*", - "phpunit/phpunit": "~4.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Watson\\Validating\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Dwight Watson", - "email": "dwight@studiousapp.com" - } - ], - "description": "Eloquent model validating trait.", - "keywords": [ - "eloquent", - "laravel", - "validation" - ], - "time": "2017-11-06T21:35:49+00:00" - }, { "name": "webmozart/assert", "version": "1.2.0", @@ -3895,60 +3975,6 @@ ], "time": "2016-11-23T20:04:58+00:00" }, - { - "name": "webpatser/laravel-uuid", - "version": "2.2.1", - "source": { - "type": "git", - "url": "https://github.com/webpatser/laravel-uuid.git", - "reference": "c87d5c631938edad7aae96d27881e3ea3de23d80" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webpatser/laravel-uuid/zipball/c87d5c631938edad7aae96d27881e3ea3de23d80", - "reference": "c87d5c631938edad7aae96d27881e3ea3de23d80", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "fzaninotto/faker": "1.5.*", - "phpunit/phpunit": "4.7.*" - }, - "suggest": { - "paragonie/random_compat": "A random_bytes Php 5.x polyfill." - }, - "type": "library", - "extra": { - "laravel": { - "aliases": { - "Uuid": "Webpatser\\Uuid\\Uuid" - } - } - }, - "autoload": { - "psr-0": { - "Webpatser\\Uuid": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christoph Kempen", - "email": "christoph@downsized.nl" - } - ], - "description": "Class to generate a UUID according to the RFC 4122 standard. Support for version 1, 3, 4 and 5 UUID are built-in.", - "homepage": "https://github.com/webpatser/uuid", - "keywords": [ - "UUID RFC4122" - ], - "time": "2017-09-10T21:34:32+00:00" - }, { "name": "znck/belongs-to-through", "version": "v2.3.1", @@ -4005,25 +4031,44 @@ "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v2.4.3", + "version": "v3.1.0", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "d7c88f08131f6404cb714f3f6cf0642f6afa3903" + "reference": "01a859752094e00aa8548832312366753272f8af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/d7c88f08131f6404cb714f3f6cf0642f6afa3903", - "reference": "d7c88f08131f6404cb714f3f6cf0642f6afa3903", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/01a859752094e00aa8548832312366753272f8af", + "reference": "01a859752094e00aa8548832312366753272f8af", "shasum": "" }, "require": { - "illuminate/support": "5.1.*|5.2.*|5.3.*|5.4.*|5.5.*", - "maximebf/debugbar": "~1.13.0", - "php": ">=5.5.9", - "symfony/finder": "~2.7|~3.0" + "illuminate/routing": "5.5.x", + "illuminate/session": "5.5.x", + "illuminate/support": "5.5.x", + "maximebf/debugbar": "~1.14.0", + "php": ">=7.0", + "symfony/debug": "^3", + "symfony/finder": "^3" + }, + "require-dev": { + "illuminate/framework": "5.5.x" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + }, + "laravel": { + "providers": [ + "Barryvdh\\Debugbar\\ServiceProvider" + ], + "aliases": { + "Debugbar": "Barryvdh\\Debugbar\\Facade" + } + } + }, "autoload": { "psr-4": { "Barryvdh\\Debugbar\\": "src/" @@ -4050,7 +4095,7 @@ "profiler", "webprofiler" ], - "time": "2017-07-21T11:56:48+00:00" + "time": "2017-09-18T13:32:46+00:00" }, { "name": "barryvdh/laravel-ide-helper", @@ -4291,17 +4336,78 @@ "time": "2015-06-14T21:17:01+00:00" }, { - "name": "friendsofphp/php-cs-fixer", - "version": "v2.8.1", + "name": "filp/whoops", + "version": "2.1.14", "source": { "type": "git", - "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "04f71e56e03ba2627e345e8c949c80dcef0e683e" + "url": "https://github.com/filp/whoops.git", + "reference": "c6081b8838686aa04f1e83ba7e91f78b7b2a23e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/04f71e56e03ba2627e345e8c949c80dcef0e683e", - "reference": "04f71e56e03ba2627e345e8c949c80dcef0e683e", + "url": "https://api.github.com/repos/filp/whoops/zipball/c6081b8838686aa04f1e83ba7e91f78b7b2a23e6", + "reference": "c6081b8838686aa04f1e83ba7e91f78b7b2a23e6", + "shasum": "" + }, + "require": { + "php": "^5.5.9 || ^7.0", + "psr/log": "^1.0.1" + }, + "require-dev": { + "mockery/mockery": "0.9.*", + "phpunit/phpunit": "^4.8.35 || ^5.7", + "symfony/var-dumper": "^2.6 || ^3.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "time": "2017-11-23T18:22:44+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v2.9.0", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", + "reference": "454ddbe65da6a9297446f442bad244e8a99a9a38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/454ddbe65da6a9297446f442bad244e8a99a9a38", + "reference": "454ddbe65da6a9297446f442bad244e8a99a9a38", "shasum": "" }, "require": { @@ -4328,7 +4434,8 @@ "require-dev": { "johnkary/phpunit-speedtrap": "^1.1 || ^2.0@dev", "justinrainbow/json-schema": "^5.0", - "php-coveralls/php-coveralls": "^1.0.2", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.0", "php-cs-fixer/accessible-object": "^1.0", "phpunit/phpunit": "^5.7.23 || ^6.4.3", "symfony/phpunit-bridge": "^3.2.2 || ^4.0" @@ -4368,7 +4475,7 @@ } ], "description": "A tool to automatically fix PHP code style", - "time": "2017-11-09T13:31:39+00:00" + "time": "2017-12-08T16:36:20+00:00" }, { "name": "fzaninotto/faker", @@ -4471,20 +4578,20 @@ }, { "name": "hamcrest/hamcrest-php", - "version": "v1.2.2", + "version": "v2.0.0", "source": { "type": "git", "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "b37020aa976fa52d3de9aa904aa2522dc518f79c" + "reference": "776503d3a8e85d4f9a1148614f95b7a608b046ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/b37020aa976fa52d3de9aa904aa2522dc518f79c", - "reference": "b37020aa976fa52d3de9aa904aa2522dc518f79c", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/776503d3a8e85d4f9a1148614f95b7a608b046ad", + "reference": "776503d3a8e85d4f9a1148614f95b7a608b046ad", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": "^5.3|^7.0" }, "replace": { "cordoval/hamcrest-php": "*", @@ -4493,15 +4600,18 @@ }, "require-dev": { "phpunit/php-file-iterator": "1.3.3", - "satooshi/php-coveralls": "dev-master" + "phpunit/phpunit": "~4.0", + "satooshi/php-coveralls": "^1.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, "autoload": { "classmap": [ "hamcrest" - ], - "files": [ - "hamcrest/Hamcrest.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -4512,20 +4622,20 @@ "keywords": [ "test" ], - "time": "2015-05-11T14:41:42+00:00" + "time": "2016-01-20T08:20:44+00:00" }, { "name": "maximebf/debugbar", - "version": "1.13.1", + "version": "v1.14.1", "source": { "type": "git", "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "afee79a236348e39a44cb837106b7c5b4897ac2a" + "reference": "64251a392344e3d22f3d21c3b7c531ba96eb01d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/afee79a236348e39a44cb837106b7c5b4897ac2a", - "reference": "afee79a236348e39a44cb837106b7c5b4897ac2a", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/64251a392344e3d22f3d21c3b7c531ba96eb01d2", + "reference": "64251a392344e3d22f3d21c3b7c531ba96eb01d2", "shasum": "" }, "require": { @@ -4544,7 +4654,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.13-dev" + "dev-master": "1.14-dev" } }, "autoload": { @@ -4573,34 +4683,34 @@ "debug", "debugbar" ], - "time": "2017-01-05T08:46:19+00:00" + "time": "2017-09-13T12:19:36+00:00" }, { "name": "mockery/mockery", - "version": "0.9.9", + "version": "1.0", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "6fdb61243844dc924071d3404bb23994ea0b6856" + "reference": "1bac8c362b12f522fdd1f1fa3556284c91affa38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/6fdb61243844dc924071d3404bb23994ea0b6856", - "reference": "6fdb61243844dc924071d3404bb23994ea0b6856", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1bac8c362b12f522fdd1f1fa3556284c91affa38", + "reference": "1bac8c362b12f522fdd1f1fa3556284c91affa38", "shasum": "" }, "require": { - "hamcrest/hamcrest-php": "~1.1", + "hamcrest/hamcrest-php": "~2.0", "lib-pcre": ">=7.0", - "php": ">=5.3.2" + "php": ">=5.6.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "~5.7|~6.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "0.9.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { @@ -4625,7 +4735,7 @@ } ], "description": "Mockery is a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Its core goal is to offer a test double framework with a succinct API capable of clearly defining all possible object operations and interactions using a human readable Domain Specific Language (DSL). Designed as a drop in alternative to PHPUnit's phpunit-mock-objects library, Mockery is easy to integrate with PHPUnit and can operate alongside phpunit-mock-objects without the World ending.", - "homepage": "http://github.com/padraic/mockery", + "homepage": "http://github.com/mockery/mockery", "keywords": [ "BDD", "TDD", @@ -4638,7 +4748,7 @@ "test double", "testing" ], - "time": "2017-02-28T12:52:32+00:00" + "time": "2017-10-06T16:20:43+00:00" }, { "name": "myclabs/deep-copy", @@ -4685,6 +4795,108 @@ ], "time": "2017-10-19T19:58:43+00:00" }, + { + "name": "phar-io/manifest", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^1.0.1", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "time": "2017-03-05T18:14:27+00:00" + }, + { + "name": "phar-io/version", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "time": "2017-03-05T17:38:23+00:00" + }, { "name": "php-cs-fixer/diff", "version": "v1.2.0", @@ -4735,30 +4947,29 @@ }, { "name": "php-mock/php-mock", - "version": "1.0.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/php-mock/php-mock.git", - "reference": "bfa2d17d64dbf129073a7ba2051a96ce52749570" + "reference": "22d297231118e6fd5b9db087fbe1ef866c2b95d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-mock/php-mock/zipball/bfa2d17d64dbf129073a7ba2051a96ce52749570", - "reference": "bfa2d17d64dbf129073a7ba2051a96ce52749570", + "url": "https://api.github.com/repos/php-mock/php-mock/zipball/22d297231118e6fd5b9db087fbe1ef866c2b95d2", + "reference": "22d297231118e6fd5b9db087fbe1ef866c2b95d2", "shasum": "" }, "require": { - "php": ">=5.5", + "php": ">=5.6", "phpunit/php-text-template": "^1" }, "replace": { "malkusch/php-mock": "*" }, "require-dev": { - "phpunit/phpunit": "^4|^5" + "phpunit/phpunit": "^5.7" }, "suggest": { - "php-mock/php-mock-mockery": "Allows using PHPMockery for Mockery integration", "php-mock/php-mock-phpunit": "Allows integration into PHPUnit testcase with the trait PHPMock." }, "type": "library", @@ -4766,7 +4977,7 @@ "psr-4": { "phpmock\\": [ "classes/", - "tests/unit/" + "tests/" ] } }, @@ -4793,25 +5004,25 @@ "test", "test double" ], - "time": "2015-11-11T22:37:09+00:00" + "time": "2017-02-17T20:52:52+00:00" }, { "name": "php-mock/php-mock-integration", - "version": "1.0.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/php-mock/php-mock-integration.git", - "reference": "e83fb65dd20cd3cf250d554cbd4682b96b684f4b" + "reference": "5a0d7d7755f823bc2a230cfa45058b40f9013bc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-mock/php-mock-integration/zipball/e83fb65dd20cd3cf250d554cbd4682b96b684f4b", - "reference": "e83fb65dd20cd3cf250d554cbd4682b96b684f4b", + "url": "https://api.github.com/repos/php-mock/php-mock-integration/zipball/5a0d7d7755f823bc2a230cfa45058b40f9013bc4", + "reference": "5a0d7d7755f823bc2a230cfa45058b40f9013bc4", "shasum": "" }, "require": { - "php": ">=5.5", - "php-mock/php-mock": "^1", + "php": ">=5.6", + "php-mock/php-mock": "^2", "phpunit/php-text-template": "^1" }, "require-dev": { @@ -4846,29 +5057,26 @@ "test", "test double" ], - "time": "2015-10-26T21:21:42+00:00" + "time": "2017-02-17T21:31:34+00:00" }, { "name": "php-mock/php-mock-phpunit", - "version": "1.1.2", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/php-mock/php-mock-phpunit.git", - "reference": "359e3038c016cee4c8f8db6387bcab3fcdebada0" + "reference": "173781abdc632c59200253e12e2b991ae6a4574f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-mock/php-mock-phpunit/zipball/359e3038c016cee4c8f8db6387bcab3fcdebada0", - "reference": "359e3038c016cee4c8f8db6387bcab3fcdebada0", + "url": "https://api.github.com/repos/php-mock/php-mock-phpunit/zipball/173781abdc632c59200253e12e2b991ae6a4574f", + "reference": "173781abdc632c59200253e12e2b991ae6a4574f", "shasum": "" }, "require": { - "php": ">=5.5", - "php-mock/php-mock-integration": "^1", - "phpunit/phpunit": "^4.0.0 || ^5.0.0" - }, - "conflict": { - "phpunit/phpunit-mock-objects": "3.2.0" + "php": ">=7", + "php-mock/php-mock-integration": "^2", + "phpunit/phpunit": "^6" }, "type": "library", "autoload": { @@ -4900,7 +5108,7 @@ "test", "test double" ], - "time": "2016-06-15T23:36:13+00:00" + "time": "2017-02-17T22:44:38+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -4958,29 +5166,35 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "4.1.1", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "2d3d238c433cf69caeb4842e97a3223a116f94b2" + "reference": "66465776cfc249844bde6d117abff1d22e06c2da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/2d3d238c433cf69caeb4842e97a3223a116f94b2", - "reference": "2d3d238c433cf69caeb4842e97a3223a116f94b2", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/66465776cfc249844bde6d117abff1d22e06c2da", + "reference": "66465776cfc249844bde6d117abff1d22e06c2da", "shasum": "" }, "require": { "php": "^7.0", - "phpdocumentor/reflection-common": "^1.0@dev", + "phpdocumentor/reflection-common": "^1.0.0", "phpdocumentor/type-resolver": "^0.4.0", "webmozart/assert": "^1.0" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^4.4" + "doctrine/instantiator": "~1.0.5", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^6.4" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, "autoload": { "psr-4": { "phpDocumentor\\Reflection\\": [ @@ -4999,7 +5213,7 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-08-30T18:51:59+00:00" + "time": "2017-11-27T17:38:31+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -5050,16 +5264,16 @@ }, { "name": "phpspec/prophecy", - "version": "v1.7.2", + "version": "1.7.3", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6" + "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", - "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf", + "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf", "shasum": "" }, "require": { @@ -5071,7 +5285,7 @@ }, "require-dev": { "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8 || ^5.6.5" + "phpunit/phpunit": "^4.8.35 || ^5.7" }, "type": "library", "extra": { @@ -5109,44 +5323,44 @@ "spy", "stub" ], - "time": "2017-09-04T11:05:03+00:00" + "time": "2017-11-24T13:59:53+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "4.0.8", + "version": "5.3.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d" + "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d", - "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/661f34d0bd3f1a7225ef491a70a020ad23a057a1", + "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1", "shasum": "" }, "require": { "ext-dom": "*", "ext-xmlwriter": "*", - "php": "^5.6 || ^7.0", - "phpunit/php-file-iterator": "^1.3", - "phpunit/php-text-template": "^1.2", - "phpunit/php-token-stream": "^1.4.2 || ^2.0", - "sebastian/code-unit-reverse-lookup": "^1.0", - "sebastian/environment": "^1.3.2 || ^2.0", - "sebastian/version": "^1.0 || ^2.0" + "php": "^7.0", + "phpunit/php-file-iterator": "^1.4.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^2.0.1", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" }, "require-dev": { - "ext-xdebug": "^2.1.4", - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^6.0" }, "suggest": { - "ext-xdebug": "^2.5.1" + "ext-xdebug": "^2.5.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0.x-dev" + "dev-master": "5.3.x-dev" } }, "autoload": { @@ -5161,7 +5375,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -5172,20 +5386,20 @@ "testing", "xunit" ], - "time": "2017-04-02T07:44:40+00:00" + "time": "2017-12-06T09:29:45+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "1.4.2", + "version": "1.4.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", "shasum": "" }, "require": { @@ -5219,7 +5433,7 @@ "filesystem", "iterator" ], - "time": "2016-10-03T07:40:28+00:00" + "time": "2017-11-27T13:52:08+00:00" }, { "name": "phpunit/php-text-template", @@ -5313,16 +5527,16 @@ }, { "name": "phpunit/php-token-stream", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0" + "reference": "791198a2c6254db10131eecfe8c06670700904db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9a02332089ac48e704c70f6cefed30c224e3c0b0", - "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", + "reference": "791198a2c6254db10131eecfe8c06670700904db", "shasum": "" }, "require": { @@ -5358,20 +5572,20 @@ "keywords": [ "tokenizer" ], - "time": "2017-08-20T05:47:52+00:00" + "time": "2017-11-27T05:48:46+00:00" }, { "name": "phpunit/phpunit", - "version": "5.7.23", + "version": "6.5.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "78532d5269d984660080d8e0f4c99c5c2ea65ffe" + "reference": "1b2f933d5775f9237369deaa2d2bfbf9d652be4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/78532d5269d984660080d8e0f4c99c5c2ea65ffe", - "reference": "78532d5269d984660080d8e0f4c99c5c2ea65ffe", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1b2f933d5775f9237369deaa2d2bfbf9d652be4c", + "reference": "1b2f933d5775f9237369deaa2d2bfbf9d652be4c", "shasum": "" }, "require": { @@ -5380,33 +5594,35 @@ "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", - "myclabs/deep-copy": "~1.3", - "php": "^5.6 || ^7.0", - "phpspec/prophecy": "^1.6.2", - "phpunit/php-code-coverage": "^4.0.4", - "phpunit/php-file-iterator": "~1.4", - "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "^3.2", - "sebastian/comparator": "^1.2.4", - "sebastian/diff": "^1.4.3", - "sebastian/environment": "^1.3.4 || ^2.0", - "sebastian/exporter": "~2.0", - "sebastian/global-state": "^1.1", - "sebastian/object-enumerator": "~2.0", - "sebastian/resource-operations": "~1.0", - "sebastian/version": "~1.0.3|~2.0", - "symfony/yaml": "~2.1|~3.0" + "myclabs/deep-copy": "^1.6.1", + "phar-io/manifest": "^1.0.1", + "phar-io/version": "^1.0", + "php": "^7.0", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^5.3", + "phpunit/php-file-iterator": "^1.4.3", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^1.0.9", + "phpunit/phpunit-mock-objects": "^5.0.5", + "sebastian/comparator": "^2.1", + "sebastian/diff": "^2.0", + "sebastian/environment": "^3.1", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^1.0", + "sebastian/version": "^2.0.1" }, "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2" + "phpdocumentor/reflection-docblock": "3.0.2", + "phpunit/dbunit": "<3.0" }, "require-dev": { "ext-pdo": "*" }, "suggest": { "ext-xdebug": "*", - "phpunit/php-invoker": "~1.1" + "phpunit/php-invoker": "^1.1" }, "bin": [ "phpunit" @@ -5414,7 +5630,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.7.x-dev" + "dev-master": "6.5.x-dev" } }, "autoload": { @@ -5440,33 +5656,33 @@ "testing", "xunit" ], - "time": "2017-10-15T06:13:55+00:00" + "time": "2017-12-10T08:06:19+00:00" }, { "name": "phpunit/phpunit-mock-objects", - "version": "3.4.4", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "a23b761686d50a560cc56233b9ecf49597cc9118" + "reference": "283b9f4f670e3a6fd6c4ff95c51a952eb5c75933" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118", - "reference": "a23b761686d50a560cc56233b9ecf49597cc9118", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/283b9f4f670e3a6fd6c4ff95c51a952eb5c75933", + "reference": "283b9f4f670e3a6fd6c4ff95c51a952eb5c75933", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.6 || ^7.0", - "phpunit/php-text-template": "^1.2", - "sebastian/exporter": "^1.2 || ^2.0" + "doctrine/instantiator": "^1.0.5", + "php": "^7.0", + "phpunit/php-text-template": "^1.2.1", + "sebastian/exporter": "^3.1" }, "conflict": { - "phpunit/phpunit": "<5.4.0" + "phpunit/phpunit": "<6.0" }, "require-dev": { - "phpunit/phpunit": "^5.4" + "phpunit/phpunit": "^6.5" }, "suggest": { "ext-soap": "*" @@ -5474,7 +5690,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2.x-dev" + "dev-master": "5.0.x-dev" } }, "autoload": { @@ -5489,7 +5705,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -5499,7 +5715,7 @@ "mock", "xunit" ], - "time": "2017-06-30T09:13:00+00:00" + "time": "2017-12-10T08:01:53+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -5548,30 +5764,30 @@ }, { "name": "sebastian/comparator", - "version": "1.2.4", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" + "reference": "1174d9018191e93cb9d719edec01257fc05f8158" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1174d9018191e93cb9d719edec01257fc05f8158", + "reference": "1174d9018191e93cb9d719edec01257fc05f8158", "shasum": "" }, "require": { - "php": ">=5.3.3", - "sebastian/diff": "~1.2", - "sebastian/exporter": "~1.2 || ~2.0" + "php": "^7.0", + "sebastian/diff": "^2.0", + "sebastian/exporter": "^3.1" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^6.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "2.1.x-dev" } }, "autoload": { @@ -5602,38 +5818,38 @@ } ], "description": "Provides the functionality to compare PHP values for equality", - "homepage": "http://www.github.com/sebastianbergmann/comparator", + "homepage": "https://github.com/sebastianbergmann/comparator", "keywords": [ "comparator", "compare", "equality" ], - "time": "2017-01-29T09:50:25+00:00" + "time": "2017-11-03T07:16:52+00:00" }, { "name": "sebastian/diff", - "version": "1.4.3", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^6.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -5660,32 +5876,32 @@ "keywords": [ "diff" ], - "time": "2017-05-22T07:24:03+00:00" + "time": "2017-08-03T08:09:46+00:00" }, { "name": "sebastian/environment", - "version": "2.0.0", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^5.0" + "phpunit/phpunit": "^6.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.1.x-dev" } }, "autoload": { @@ -5710,34 +5926,34 @@ "environment", "hhvm" ], - "time": "2016-11-26T07:53:53+00:00" + "time": "2017-07-01T08:51:00+00:00" }, { "name": "sebastian/exporter", - "version": "2.0.0", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4" + "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", - "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", + "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", "shasum": "" }, "require": { - "php": ">=5.3.3", - "sebastian/recursion-context": "~2.0" + "php": "^7.0", + "sebastian/recursion-context": "^3.0" }, "require-dev": { "ext-mbstring": "*", - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.1.x-dev" } }, "autoload": { @@ -5777,27 +5993,27 @@ "export", "exporter" ], - "time": "2016-11-19T08:54:04+00:00" + "time": "2017-04-03T13:19:02+00:00" }, { "name": "sebastian/global-state", - "version": "1.1.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.2" + "phpunit/phpunit": "^6.0" }, "suggest": { "ext-uopz": "*" @@ -5805,7 +6021,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -5828,33 +6044,34 @@ "keywords": [ "global state" ], - "time": "2015-10-12T03:26:01+00:00" + "time": "2017-04-27T15:39:26+00:00" }, { "name": "sebastian/object-enumerator", - "version": "2.0.1", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7" + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1311872ac850040a79c3c058bea3e22d0f09cbb7", - "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", "shasum": "" }, "require": { - "php": ">=5.6", - "sebastian/recursion-context": "~2.0" + "php": "^7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" }, "require-dev": { - "phpunit/phpunit": "~5" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -5874,32 +6091,77 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2017-02-18T15:18:39+00:00" + "time": "2017-08-03T12:35:26+00:00" }, { - "name": "sebastian/recursion-context", - "version": "2.0.0", + "name": "sebastian/object-reflector", + "version": "1.1.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a" + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "773f97c67f28de00d397be301821b06708fca0be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a", - "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", + "reference": "773f97c67f28de00d397be301821b06708fca0be", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "time": "2017-03-29T09:07:27+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -5927,7 +6189,7 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2016-11-19T07:33:16+00:00" + "time": "2017-03-03T06:23:57+00:00" }, { "name": "sebastian/resource-operations", @@ -6337,59 +6599,44 @@ "time": "2017-04-12T14:14:56+00:00" }, { - "name": "symfony/yaml", - "version": "v3.3.6", + "name": "theseer/tokenizer", + "version": "1.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed" + "url": "https://github.com/theseer/tokenizer.git", + "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/ddc23324e6cfe066f3dd34a37ff494fa80b617ed", - "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b", + "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b", "shasum": "" }, "require": { - "php": ">=5.5.9" - }, - "require-dev": { - "symfony/console": "~2.8|~3.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" } ], - "description": "Symfony Yaml Component", - "homepage": "https://symfony.com", - "time": "2017-07-23T12:43:26+00:00" + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "time": "2017-04-07T12:08:54+00:00" } ], "aliases": [], diff --git a/config/app.php b/config/app.php index d56b80634..5ffe58f21 100644 --- a/config/app.php +++ b/config/app.php @@ -1,10 +1,15 @@ env('APP_ENV', 'production'), + /* + |-------------------------------------------------------------------------- + | Application Version + |-------------------------------------------------------------------------- + | This value is set when creating a Pterodactyl release. You should not + | change this value if you are not maintaining your own internal versions. + */ - 'version' => env('APP_VERSION', 'canary'), + 'version' => 'canary', /* |-------------------------------------------------------------------------- @@ -15,8 +20,22 @@ return [ | framework needs to place the application's name in a notification or | any other location as required by the application or its packages. */ + 'name' => env('APP_NAME', 'Pterodactyl'), + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services your application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + /* |-------------------------------------------------------------------------- | Application Debug Mode @@ -93,7 +112,7 @@ return [ | */ - 'key' => env('APP_KEY', 'SomeRandomString3232RandomString'), + 'key' => env('APP_KEY'), 'cipher' => 'AES-256-CBC', @@ -112,7 +131,7 @@ return [ 'log' => env('APP_LOG', 'daily'), - 'log_level' => env('APP_LOG_LEVEL', 'debug'), + 'log_level' => env('APP_LOG_LEVEL', 'info'), /* |-------------------------------------------------------------------------- @@ -141,6 +160,7 @@ return [ Illuminate\Foundation\Providers\FoundationServiceProvider::class, Illuminate\Hashing\HashServiceProvider::class, Illuminate\Mail\MailServiceProvider::class, + Illuminate\Notifications\NotificationServiceProvider::class, Illuminate\Pagination\PaginationServiceProvider::class, Illuminate\Pipeline\PipelineServiceProvider::class, Illuminate\Queue\QueueServiceProvider::class, @@ -149,12 +169,6 @@ return [ Illuminate\Session\SessionServiceProvider::class, Illuminate\Validation\ValidationServiceProvider::class, Illuminate\View\ViewServiceProvider::class, - Illuminate\Notifications\NotificationServiceProvider::class, - - /* - * Package Service Providers... - */ - Laravel\Tinker\TinkerServiceProvider::class, /* * Application Service Providers... @@ -175,12 +189,7 @@ return [ */ igaster\laravelTheme\themeServiceProvider::class, Prologue\Alerts\AlertsServiceProvider::class, - Fideloper\Proxy\TrustedProxyServiceProvider::class, - Laracasts\Utilities\JavaScript\JavaScriptServiceProvider::class, Lord\Laroute\LarouteServiceProvider::class, - Spatie\Fractal\FractalServiceProvider::class, - Sofa\Eloquence\ServiceProvider::class, - Appstract\BladeDirectives\BladeDirectivesServiceProvider::class, ], /* @@ -205,17 +214,14 @@ return [ 'Carbon' => Carbon\Carbon::class, 'Config' => Illuminate\Support\Facades\Config::class, 'Cookie' => Illuminate\Support\Facades\Cookie::class, - 'Cron' => Cron\CronExpression::class, 'Crypt' => Illuminate\Support\Facades\Crypt::class, 'DB' => Illuminate\Support\Facades\DB::class, 'Eloquent' => Illuminate\Database\Eloquent\Model::class, 'Event' => Illuminate\Support\Facades\Event::class, 'File' => Illuminate\Support\Facades\File::class, - 'Fractal' => Spatie\Fractal\FractalFacade::class, 'Gate' => Illuminate\Support\Facades\Gate::class, 'Hash' => Illuminate\Support\Facades\Hash::class, 'Input' => Illuminate\Support\Facades\Input::class, - 'Inspiring' => Illuminate\Foundation\Inspiring::class, 'Javascript' => Laracasts\Utilities\JavaScript\JavaScriptFacade::class, 'Lang' => Illuminate\Support\Facades\Lang::class, 'Log' => Illuminate\Support\Facades\Log::class, @@ -233,7 +239,6 @@ return [ 'Storage' => Illuminate\Support\Facades\Storage::class, 'Theme' => igaster\laravelTheme\Facades\Theme::class, 'URL' => Illuminate\Support\Facades\URL::class, - 'Uuid' => Webpatser\Uuid\Uuid::class, 'Validator' => Illuminate\Support\Facades\Validator::class, 'View' => Illuminate\Support\Facades\View::class, ], diff --git a/config/cache.php b/config/cache.php index 6109216c7..86bbeb61e 100644 --- a/config/cache.php +++ b/config/cache.php @@ -10,6 +10,8 @@ return [ | using this caching library. This connection is used when another is | not explicitly specified when executing a given caching function. | + | Supported: "apc", "array", "database", "file", "memcached", "redis" + | */ 'default' => env('CACHE_DRIVER', 'file'), @@ -39,6 +41,7 @@ return [ 'table' => 'cache', 'connection' => null, ], + 'file' => [ 'driver' => 'file', 'path' => storage_path('framework/cache/data'), @@ -80,5 +83,5 @@ return [ | */ - 'prefix' => 'pterodactyl', + 'prefix' => env('CACHE_PREFIX', str_slug(env('APP_NAME', 'pterodactyl'), '_') . '_cache'), ]; diff --git a/config/database.php b/config/database.php index ee865cf6e..15d708f16 100644 --- a/config/database.php +++ b/config/database.php @@ -75,7 +75,7 @@ return [ 'host' => env('REDIS_HOST', 'localhost'), 'password' => env('REDIS_PASSWORD', null), 'port' => env('REDIS_PORT', 6379), - 'database' => 0, + 'database' => env('REDIS_DATBASE', 0), ], ], ]; diff --git a/config/filesystems.php b/config/filesystems.php index 305142930..809742bed 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -10,11 +10,9 @@ return [ | by the framework. A "local" driver, as well as a variety of cloud | based drivers are available for your choosing. Just store away! | - | Supported: "local", "ftp", "s3", "rackspace" - | */ - 'default' => 'local', + 'default' => env('FILESYSTEM_DRIVER', 'local'), /* |-------------------------------------------------------------------------- @@ -27,7 +25,7 @@ return [ | */ - 'cloud' => 's3', + 'cloud' => env('FILESYSTEM_CLOUD', 's3'), /* |-------------------------------------------------------------------------- @@ -56,9 +54,9 @@ return [ 's3' => [ 'driver' => 's3', - 'key' => env('AWS_KEY'), - 'secret' => env('AWS_SECRET'), - 'region' => env('AWS_REGION'), + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), 'bucket' => env('AWS_BUCKET'), ], ], diff --git a/config/session.php b/config/session.php index 08d97fd56..837809f66 100644 --- a/config/session.php +++ b/config/session.php @@ -28,7 +28,7 @@ return [ | */ - 'lifetime' => 10080, + 'lifetime' => env('SESSION_LIFETIME', 10080), 'expire_on_close' => false, @@ -121,7 +121,7 @@ return [ | */ - 'cookie' => 'pterodactyl_session', + 'cookie' => env('SESSION_COOKIE', str_slug(env('APP_NAME', 'pterodactyl'), '_') . '_session'), /* |-------------------------------------------------------------------------- @@ -174,4 +174,19 @@ return [ */ 'http_only' => true, + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | do not enable this as other CSRF protection services are in place. + | + | Supported: "lax", "strict" + | + */ + + 'same_site' => null, ]; diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index e117b59bb..c5501d119 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -1,5 +1,7 @@ define(Pterodactyl\Models\Server::class, function (Faker\Generator $faker) { +$factory->define(Pterodactyl\Models\Server::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), 'node_id' => $faker->randomNumber(), @@ -39,7 +39,7 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $fa ]; }); -$factory->define(Pterodactyl\Models\User::class, function (Faker\Generator $faker) { +$factory->define(Pterodactyl\Models\User::class, function (Faker $faker) { static $password; return [ @@ -63,7 +63,7 @@ $factory->state(Pterodactyl\Models\User::class, 'admin', function () { ]; }); -$factory->define(Pterodactyl\Models\Location::class, function (Faker\Generator $faker) { +$factory->define(Pterodactyl\Models\Location::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), 'short' => $faker->domainWord, @@ -71,7 +71,7 @@ $factory->define(Pterodactyl\Models\Location::class, function (Faker\Generator $ ]; }); -$factory->define(Pterodactyl\Models\Node::class, function (Faker\Generator $faker) { +$factory->define(Pterodactyl\Models\Node::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), 'uuid' => $faker->unique()->uuid, @@ -92,7 +92,7 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker\Generator $fake ]; }); -$factory->define(Pterodactyl\Models\Nest::class, function (Faker\Generator $faker) { +$factory->define(Pterodactyl\Models\Nest::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), 'uuid' => $faker->unique()->uuid, @@ -102,7 +102,7 @@ $factory->define(Pterodactyl\Models\Nest::class, function (Faker\Generator $fake ]; }); -$factory->define(Pterodactyl\Models\Egg::class, function (Faker\Generator $faker) { +$factory->define(Pterodactyl\Models\Egg::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), 'uuid' => $faker->unique()->uuid, @@ -113,7 +113,7 @@ $factory->define(Pterodactyl\Models\Egg::class, function (Faker\Generator $faker ]; }); -$factory->define(Pterodactyl\Models\EggVariable::class, function (Faker\Generator $faker) { +$factory->define(Pterodactyl\Models\EggVariable::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), 'name' => $faker->firstName, @@ -134,7 +134,7 @@ $factory->state(Pterodactyl\Models\EggVariable::class, 'editable', function () { return ['user_editable' => 1]; }); -$factory->define(Pterodactyl\Models\Pack::class, function (Faker\Generator $faker) { +$factory->define(Pterodactyl\Models\Pack::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), 'egg_id' => $faker->randomNumber(), @@ -148,7 +148,7 @@ $factory->define(Pterodactyl\Models\Pack::class, function (Faker\Generator $fake ]; }); -$factory->define(Pterodactyl\Models\Subuser::class, function (Faker\Generator $faker) { +$factory->define(Pterodactyl\Models\Subuser::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), 'user_id' => $faker->randomNumber(), @@ -156,7 +156,7 @@ $factory->define(Pterodactyl\Models\Subuser::class, function (Faker\Generator $f ]; }); -$factory->define(Pterodactyl\Models\Allocation::class, function (Faker\Generator $faker) { +$factory->define(Pterodactyl\Models\Allocation::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), 'node_id' => $faker->randomNumber(), @@ -165,7 +165,7 @@ $factory->define(Pterodactyl\Models\Allocation::class, function (Faker\Generator ]; }); -$factory->define(Pterodactyl\Models\DatabaseHost::class, function (Faker\Generator $faker) { +$factory->define(Pterodactyl\Models\DatabaseHost::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), 'name' => $faker->colorName, @@ -177,7 +177,7 @@ $factory->define(Pterodactyl\Models\DatabaseHost::class, function (Faker\Generat ]; }); -$factory->define(Pterodactyl\Models\Database::class, function (Faker\Generator $faker) { +$factory->define(Pterodactyl\Models\Database::class, function (Faker $faker) { static $password; return [ @@ -192,7 +192,7 @@ $factory->define(Pterodactyl\Models\Database::class, function (Faker\Generator $ ]; }); -$factory->define(Pterodactyl\Models\Schedule::class, function (Faker\Generator $faker) { +$factory->define(Pterodactyl\Models\Schedule::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), 'server_id' => $faker->randomNumber(), @@ -200,7 +200,7 @@ $factory->define(Pterodactyl\Models\Schedule::class, function (Faker\Generator $ ]; }); -$factory->define(Pterodactyl\Models\Task::class, function (Faker\Generator $faker) { +$factory->define(Pterodactyl\Models\Task::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), 'schedule_id' => $faker->randomNumber(), @@ -212,7 +212,7 @@ $factory->define(Pterodactyl\Models\Task::class, function (Faker\Generator $fake ]; }); -$factory->define(Pterodactyl\Models\DaemonKey::class, function (Faker\Generator $faker) { +$factory->define(Pterodactyl\Models\DaemonKey::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), 'server_id' => $faker->randomNumber(), @@ -222,7 +222,7 @@ $factory->define(Pterodactyl\Models\DaemonKey::class, function (Faker\Generator ]; }); -$factory->define(Pterodactyl\Models\APIKey::class, function (Faker\Generator $faker) { +$factory->define(Pterodactyl\Models\APIKey::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), 'user_id' => $faker->randomNumber(), @@ -233,7 +233,7 @@ $factory->define(Pterodactyl\Models\APIKey::class, function (Faker\Generator $fa ]; }); -$factory->define(Pterodactyl\Models\APIPermission::class, function (Faker\Generator $faker) { +$factory->define(Pterodactyl\Models\APIPermission::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), 'key_id' => $faker->randomNumber(), diff --git a/phpunit.xml b/phpunit.xml index 89b3c0b22..26ecd5b4f 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,7 +1,7 @@ - Options -MultiViews + Options -MultiViews -Indexes RewriteEngine On + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + # Redirect Trailing Slashes If Not A Folder... RewriteCond %{REQUEST_FILENAME} !-d - RewriteRule ^(.*)/$ /$1 [L,R=301] - - # Prevent stripping authorization headers - RewriteCond %{HTTP:Authorization} ^(.*) - RewriteRule .* - [e=HTTP_AUTHORIZATION:%1] + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] # Handle Front Controller... RewriteCond %{REQUEST_FILENAME} !-d diff --git a/public/index.php b/public/index.php index f2bd8213e..233cba743 100644 --- a/public/index.php +++ b/public/index.php @@ -3,8 +3,9 @@ /** * Laravel - A PHP Framework For Web Artisans. * - * @author Taylor Otwell + * @author Taylor Otwell */ +define('LARAVEL_START', microtime(true)); /* |-------------------------------------------------------------------------- @@ -14,11 +15,11 @@ | Composer provides a convenient, automatically generated class loader for | our application. We just need to utilize it! We'll simply require it | into the script here so that we don't have to worry about manual -| loading any of our classes later on. It feels nice to relax. +| loading any of our classes later on. It feels great to relax. | */ -require __DIR__ . '/../bootstrap/autoload.php'; +require __DIR__ . '/../vendor/autoload.php'; /* |-------------------------------------------------------------------------- From 3c48947f9ddb91c8004482e4d0abdabe4889e309 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 16 Dec 2017 13:15:09 -0600 Subject: [PATCH 11/54] Fix known issues from the upgrade guide --- .../CleanServiceBackupFilesCommand.php | 27 +++------ app/Http/Controllers/Admin/PackController.php | 2 +- .../Controllers/Admin/ServersController.php | 2 +- app/Http/Controllers/Auth/LoginController.php | 5 +- app/Http/Middleware/VerifyReCaptcha.php | 2 +- .../Admin/DatabaseHostFormRequest.php | 2 +- app/Models/Pack.php | 26 --------- app/Policies/ServerPolicy.php | 13 +++++ app/Providers/SettingsServiceProvider.php | 18 ++++++ .../pterodactyl/admin/packs/view.blade.php | 7 --- .../CleanServiceBackupFilesCommandTest.php | 55 +++++++++---------- 11 files changed, 71 insertions(+), 88 deletions(-) diff --git a/app/Console/Commands/Maintenance/CleanServiceBackupFilesCommand.php b/app/Console/Commands/Maintenance/CleanServiceBackupFilesCommand.php index f3982921e..4811a222e 100644 --- a/app/Console/Commands/Maintenance/CleanServiceBackupFilesCommand.php +++ b/app/Console/Commands/Maintenance/CleanServiceBackupFilesCommand.php @@ -1,27 +1,16 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Console\Commands\Maintenance; use Carbon\Carbon; use Illuminate\Console\Command; +use Symfony\Component\Finder\SplFileInfo; use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; class CleanServiceBackupFilesCommand extends Command { const BACKUP_THRESHOLD_MINUTES = 5; - /** - * @var \Carbon\Carbon - */ - protected $carbon; - /** * @var string */ @@ -40,14 +29,12 @@ class CleanServiceBackupFilesCommand extends Command /** * CleanServiceBackupFilesCommand constructor. * - * @param \Carbon\Carbon $carbon * @param \Illuminate\Contracts\Filesystem\Factory $filesystem */ - public function __construct(Carbon $carbon, FilesystemFactory $filesystem) + public function __construct(FilesystemFactory $filesystem) { parent::__construct(); - $this->carbon = $carbon; $this->disk = $filesystem->disk(); } @@ -58,11 +45,11 @@ class CleanServiceBackupFilesCommand extends Command { $files = $this->disk->files('services/.bak'); - collect($files)->each(function ($file) { - $lastModified = $this->carbon->timestamp($this->disk->lastModified($file)); - if ($lastModified->diffInMinutes($this->carbon->now()) > self::BACKUP_THRESHOLD_MINUTES) { - $this->disk->delete($file); - $this->info(trans('command/messages.maintenance.deleting_service_backup', ['file' => $file])); + collect($files)->each(function (SplFileInfo $file) { + $lastModified = Carbon::createFromTimestamp($this->disk->lastModified($file->getPath())); + if ($lastModified->diffInMinutes(Carbon::now()) > self::BACKUP_THRESHOLD_MINUTES) { + $this->disk->delete($file->getPath()); + $this->info(trans('command/messages.maintenance.deleting_service_backup', ['file' => $file->getFilename()])); } }); } diff --git a/app/Http/Controllers/Admin/PackController.php b/app/Http/Controllers/Admin/PackController.php index ae6c86fda..fed86f3a0 100644 --- a/app/Http/Controllers/Admin/PackController.php +++ b/app/Http/Controllers/Admin/PackController.php @@ -163,7 +163,7 @@ class PackController extends Controller */ public function store(PackFormRequest $request) { - if ($request->has('from_template')) { + if ($request->filled('from_template')) { $pack = $this->templateUploadService->handle($request->input('egg_id'), $request->file('file_upload')); } else { $pack = $this->creationService->handle($request->normalize(), $request->file('file_upload')); diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 1cd63a20d..427bb7d20 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -524,7 +524,7 @@ class ServersController extends Controller */ public function delete(Request $request, Server $server) { - $this->deletionService->withForce($request->has('force_delete'))->handle($server); + $this->deletionService->withForce($request->filled('force_delete'))->handle($server); $this->alert->success(trans('admin/server.alerts.server_deleted'))->flash(); return redirect()->route('admin.servers'); diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 8e67d9f1a..127802d22 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -107,6 +107,8 @@ class LoginController extends Controller * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response + * + * @throws \Illuminate\Validation\ValidationException */ public function login(Request $request) { @@ -115,8 +117,7 @@ class LoginController extends Controller if ($this->hasTooManyLoginAttempts($request)) { $this->fireLockoutEvent($request); - - return $this->sendLockoutResponse($request); + $this->sendLockoutResponse($request); } try { diff --git a/app/Http/Middleware/VerifyReCaptcha.php b/app/Http/Middleware/VerifyReCaptcha.php index 16c3a2f59..7464e854b 100644 --- a/app/Http/Middleware/VerifyReCaptcha.php +++ b/app/Http/Middleware/VerifyReCaptcha.php @@ -39,7 +39,7 @@ class VerifyReCaptcha return $next($request); } - if ($request->has('g-recaptcha-response')) { + if ($request->filled('g-recaptcha-response')) { $client = new Client(); $res = $client->post($this->config->get('recaptcha.domain'), [ 'form_params' => [ diff --git a/app/Http/Requests/Admin/DatabaseHostFormRequest.php b/app/Http/Requests/Admin/DatabaseHostFormRequest.php index 1feb42ed1..76a2f1e55 100644 --- a/app/Http/Requests/Admin/DatabaseHostFormRequest.php +++ b/app/Http/Requests/Admin/DatabaseHostFormRequest.php @@ -18,7 +18,7 @@ class DatabaseHostFormRequest extends AdminFormRequest */ public function rules() { - if (! $this->has('node_id')) { + if (! $this->filled('node_id')) { $this->merge(['node_id' => null]); } diff --git a/app/Models/Pack.php b/app/Models/Pack.php index a029b3614..5d172c252 100644 --- a/app/Models/Pack.php +++ b/app/Models/Pack.php @@ -9,8 +9,6 @@ namespace Pterodactyl\Models; -use File; -use Storage; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; @@ -88,30 +86,6 @@ class Pack extends Model implements CleansAttributes, ValidableContract 'version' => 2, ]; - /** - * Returns all of the archived files for a given pack. - * - * @param bool $collection - * @return \Illuminate\Support\Collection|object - * @deprecated - */ - public function files($collection = false) - { - $files = collect(Storage::files('packs/' . $this->uuid)); - - $files = $files->map(function ($item) { - $path = storage_path('app/' . $item); - - return (object) [ - 'name' => basename($item), - 'hash' => sha1_file($path), - 'size' => File::humanReadableSize($path), - ]; - }); - - return ($collection) ? $files : (object) $files->all(); - } - /** * Gets egg associated with a service pack. * diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php index c5e1860c4..9b4db6f05 100644 --- a/app/Policies/ServerPolicy.php +++ b/app/Policies/ServerPolicy.php @@ -51,4 +51,17 @@ class ServerPolicy return $this->checkPermission($user, $server, $ability); } + + /** + * This is a horrendous hack to avoid Laravel's "smart" behavior that does + * not call the before() function if there isn't a function matching the + * policy permission. + * + * @param string $name + * @param mixed $arguments + */ + public function __call($name, $arguments) + { + // do nothing + } } diff --git a/app/Providers/SettingsServiceProvider.php b/app/Providers/SettingsServiceProvider.php index dc9e9cdc6..40802f5c6 100644 --- a/app/Providers/SettingsServiceProvider.php +++ b/app/Providers/SettingsServiceProvider.php @@ -87,6 +87,24 @@ class SettingsServiceProvider extends ServiceProvider } } + switch (strtolower($value)) { + case 'true': + case '(true)': + $value = true; + break; + case 'false': + case '(false)': + $value = false; + break; + case 'empty': + case '(empty)': + $value = ''; + break; + case 'null': + case '(null)': + $value = null; + } + $config->set(str_replace(':', '.', $key), $value); } } diff --git a/resources/themes/pterodactyl/admin/packs/view.blade.php b/resources/themes/pterodactyl/admin/packs/view.blade.php index e9e5804e5..d9bb50390 100644 --- a/resources/themes/pterodactyl/admin/packs/view.blade.php +++ b/resources/themes/pterodactyl/admin/packs/view.blade.php @@ -113,13 +113,6 @@ SHA1 Hash File Size - @foreach($pack->files() as $file) - - {{ $file->name }} - {{ $file->hash }} - {{ $file->size }} - - @endforeach
+
+
+
+@endsection + +@section('footer-scripts') + @parent + +@endsection diff --git a/resources/themes/pterodactyl/admin/api/new.blade.php b/resources/themes/pterodactyl/admin/api/new.blade.php new file mode 100644 index 000000000..bf590d7c0 --- /dev/null +++ b/resources/themes/pterodactyl/admin/api/new.blade.php @@ -0,0 +1,70 @@ +@extends('layouts.admin') + +@section('title') + Application API +@endsection + +@section('content-header') +

Application APICreate a new application API key.

+ +@endsection + +@section('content') +
+
+
+
+
+

Select Permissions

+
+
+ + @foreach($resources as $resource) + + + + + + + @endforeach +
{{ title_case($resource) }} + + + + + + + + +
+
+
+
+
+
+
+
+ + +
+

Once you have assigned permissions and created this set of credentials you will be unable to come back and edit it. If you need to make changes down the road you will need to create a new set of credentials.

+
+ +
+
+
+
+@endsection + +@section('footer-scripts') + @parent + +@endsection diff --git a/resources/themes/pterodactyl/layouts/admin.blade.php b/resources/themes/pterodactyl/layouts/admin.blade.php index 6f70c196f..47ea53a2d 100644 --- a/resources/themes/pterodactyl/layouts/admin.blade.php +++ b/resources/themes/pterodactyl/layouts/admin.blade.php @@ -85,6 +85,11 @@ Settings +
  • + + Application API + +
  • MANAGEMENT
  • diff --git a/routes/admin.php b/routes/admin.php index 01b20bc6d..ffcc01566 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -2,6 +2,23 @@ Route::get('/', 'BaseController@index')->name('admin.index'); +/* +|-------------------------------------------------------------------------- +| Location Controller Routes +|-------------------------------------------------------------------------- +| +| Endpoint: /admin/api +| +*/ +Route::group(['prefix' => 'api'], function () { + Route::get('/', 'ApplicationApiController@index')->name('admin.api.index'); + Route::get('/new', 'ApplicationApiController@create')->name('admin.api.new'); + + Route::post('/new', 'ApplicationApiController@store'); + + Route::delete('/revoke/{identifier}', 'ApplicationApiController@delete')->name('admin.api.delete'); +}); + /* |-------------------------------------------------------------------------- | Location Controller Routes From c59d3a96aafcd45de1cd501bf833094586c77a29 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 18 Jan 2018 21:41:45 -0600 Subject: [PATCH 35/54] Add test for new middleware --- .../Api/Admin/AuthenticateUserTest.php | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/Unit/Http/Middleware/Api/Admin/AuthenticateUserTest.php diff --git a/tests/Unit/Http/Middleware/Api/Admin/AuthenticateUserTest.php b/tests/Unit/Http/Middleware/Api/Admin/AuthenticateUserTest.php new file mode 100644 index 000000000..fb243a78f --- /dev/null +++ b/tests/Unit/Http/Middleware/Api/Admin/AuthenticateUserTest.php @@ -0,0 +1,53 @@ +setRequestUserModel(null); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test that a non-admin user results an an exception. + * + * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + */ + public function testNonAdminUser() + { + $this->generateRequestUserModel(['root_admin' => false]); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test that an admin user continues though the middleware. + */ + public function testAdminUser() + { + $this->generateRequestUserModel(['root_admin' => true]); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Return an instance of the middleware for testing. + * + * @return \Pterodactyl\Http\Middleware\Api\Admin\AuthenticateUser + */ + private function getMiddleware(): AuthenticateUser + { + return new AuthenticateUser; + } +} From bdadec002c2dcd6565bb9c50200d6c88f2802a35 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 18 Jan 2018 21:56:12 -0600 Subject: [PATCH 36/54] Push updates to server transformer --- .../Api/Admin/ServerTransformer.php | 138 +++++++++--------- 1 file changed, 73 insertions(+), 65 deletions(-) diff --git a/app/Transformers/Api/Admin/ServerTransformer.php b/app/Transformers/Api/Admin/ServerTransformer.php index 7cb722537..7e5b9249d 100644 --- a/app/Transformers/Api/Admin/ServerTransformer.php +++ b/app/Transformers/Api/Admin/ServerTransformer.php @@ -2,11 +2,12 @@ namespace Pterodactyl\Transformers\Api\Admin; -use Illuminate\Http\Request; use Pterodactyl\Models\Server; -use League\Fractal\TransformerAbstract; +use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Transformers\Admin\PackTransformer; +use Pterodactyl\Transformers\Admin\ServerVariableTransformer; -class ServerTransformer extends TransformerAbstract +class ServerTransformer extends BaseTransformer { /** * List of resources that can be included. @@ -18,40 +19,20 @@ class ServerTransformer extends TransformerAbstract 'user', 'subusers', 'pack', - 'service', - 'option', + 'nest', + 'egg', 'variables', 'location', 'node', ]; - /** - * 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 server array. * + * @param \Pterodactyl\Models\Server $server * @return array */ - public function transform(Server $server) + public function transform(Server $server): array { return collect($server->toArray())->only($server->getTableColumns())->toArray(); } @@ -59,126 +40,153 @@ class ServerTransformer extends TransformerAbstract /** * Return a generic array of allocations for this server. * - * @return \Leauge\Fractal\Resource\Collection + * @param \Pterodactyl\Models\Server $server + * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource */ public function includeAllocations(Server $server) { - if ($this->request && ! $this->request->apiKeyHasPermission('server-view')) { - return; + if (! $this->authorize(AdminAcl::RESOURCE_ALLOCATIONS)) { + return $this->null(); } - return $this->collection($server->allocations, new AllocationTransformer($this->request, 'server'), 'allocation'); + $server->loadMissing('allocations'); + + return $this->collection($server->getRelation('allocations'), $this->makeTransformer(AllocationTransformer::class), 'allocation'); } /** * Return a generic array of data about subusers for this server. * - * @return \Leauge\Fractal\Resource\Collection + * @param \Pterodactyl\Models\Server $server + * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource */ public function includeSubusers(Server $server) { - if ($this->request && ! $this->request->apiKeyHasPermission('server-view')) { - return; + if (! $this->authorize(AdminAcl::RESOURCE_USERS)) { + return $this->null(); } - return $this->collection($server->subusers, new SubuserTransformer($this->request), 'subuser'); + $server->loadMissing('subusers'); + + return $this->collection($server->getRelation('subusers'), $this->makeTransformer(UserTransformer::class), 'user'); } /** * Return a generic array of data about subusers for this server. * - * @return \Leauge\Fractal\Resource\Item + * @param \Pterodactyl\Models\Server $server + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource */ public function includeUser(Server $server) { - if ($this->request && ! $this->request->apiKeyHasPermission('user-view')) { - return; + if (! $this->authorize(AdminAcl::RESOURCE_USERS)) { + return $this->null(); } - return $this->item($server->user, new UserTransformer($this->request), 'user'); + $server->loadMissing('user'); + + return $this->item($server->getRelation('user'), $this->makeTransformer(UserTransformer::class), 'user'); } /** * Return a generic array with pack information for this server. * - * @return \Leauge\Fractal\Resource\Item + * @param \Pterodactyl\Models\Server $server + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource */ public function includePack(Server $server) { - if ($this->request && ! $this->request->apiKeyHasPermission('pack-view')) { - return; + if (! $this->authorize(AdminAcl::RESOURCE_PACKS)) { + return $this->null(); } - return $this->item($server->pack, new PackTransformer($this->request), 'pack'); + $server->loadMissing('pack'); + + return $this->item($server->getRelation('pack'), $this->makeTransformer(PackTransformer::class), 'pack'); } /** - * Return a generic array with service information for this server. + * Return a generic array with nest information for this server. * - * @return \Leauge\Fractal\Resource\Item + * @param \Pterodactyl\Models\Server $server + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource */ - public function includeService(Server $server) + public function includeNest(Server $server) { - if ($this->request && ! $this->request->apiKeyHasPermission('service-view')) { - return; + if (! $this->authorize(AdminAcl::RESOURCE_NESTS)) { + return $this->null(); } - return $this->item($server->service, new ServiceTransformer($this->request), 'service'); + $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. * - * @return \Leauge\Fractal\Resource\Item + * @param \Pterodactyl\Models\Server $server + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource */ public function includeOption(Server $server) { - if ($this->request && ! $this->request->apiKeyHasPermission('option-view')) { - return; + if (! $this->authorize(AdminAcl::RESOURCE_EGGS)) { + return $this->null(); } - return $this->item($server->option, new OptionTransformer($this->request), 'option'); + $server->loadMissing('egg'); + + return $this->item($server->getRelation('egg'), $this->makeTransformer(EggTransformer::class), 'egg'); } /** * Return a generic array of data about subusers for this server. * - * @return \Leauge\Fractal\Resource\Collection + * @param \Pterodactyl\Models\Server $server + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource */ public function includeVariables(Server $server) { - if ($this->request && ! $this->request->apiKeyHasPermission('server-view')) { - return; + if (! $this->authorize(AdminAcl::RESOURCE_SERVERS)) { + return $this->null(); } - return $this->collection($server->variables, new ServerVariableTransformer($this->request), 'server_variable'); + $server->loadMissing('variables'); + + return $this->item($server->getRelation('variables'), $this->makeTransformer(ServerVariableTransformer::class), 'server_variable'); } /** * Return a generic array with pack information for this server. * - * @return \Leauge\Fractal\Resource\Item + * @param \Pterodactyl\Models\Server $server + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource */ public function includeLocation(Server $server) { - if ($this->request && ! $this->request->apiKeyHasPermission('location-list')) { - return; + if (! $this->authorize(AdminAcl::RESOURCE_LOCATIONS)) { + return $this->null(); } - return $this->item($server->location, new LocationTransformer($this->request), 'location'); + $server->loadMissing('location'); + + return $this->item($server->getRelation('location'), $this->makeTransformer(LocationTransformer::class), 'location'); } /** * Return a generic array with pack information for this server. * - * @return \Leauge\Fractal\Resource\Item|void + * @param \Pterodactyl\Models\Server $server + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource */ public function includeNode(Server $server) { - if ($this->request && ! $this->request->apiKeyHasPermission('node-view')) { - return; + if (! $this->authorize(AdminAcl::RESOURCE_NODES)) { + return $this->null(); } - return $this->item($server->node, new NodeTransformer($this->request), 'node'); + $server->loadMissing('node'); + + return $this->item($server->getRelation('node'), $this->makeTransformer(NodeTransformer::class), 'node'); } } From 0e7f8cedf024cc1af93fc45b07666420a3fde672 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 19 Jan 2018 19:58:57 -0600 Subject: [PATCH 37/54] Reorganize API files --- .../API/Remote/EggInstallController.php | 2 +- .../API/Remote/EggRetrievalController.php | 2 +- .../Controllers/API/Remote/SftpController.php | 2 +- .../API/Remote/ValidateKeyController.php | 2 +- .../Application/ApplicationApiController.php | 1 + .../Locations/LocationController.php | 24 +++++----- .../Nodes/AllocationController.php | 16 +++---- .../Application}/Nodes/NodeController.php | 28 ++++++------ .../Application/Servers/ServerController.php | 44 +++++++++++++++++++ .../Application}/Users/UserController.php | 28 ++++++------ app/Http/Kernel.php | 8 ++-- .../AuthenticateIPAccess.php | 2 +- .../AuthenticateKey.php | 2 +- .../AuthenticateUser.php | 2 +- .../SetSessionDriver.php | 2 +- .../Allocations/DeleteAllocationRequest.php | 4 +- .../Allocations/GetAllocationsRequest.php | 4 +- .../Application}/ApiAdminRequest.php | 8 ++-- .../Locations/DeleteLocationRequest.php | 4 +- .../Locations/GetLocationRequest.php | 4 +- .../Locations/GetLocationsRequest.php | 4 +- .../Locations/StoreLocationRequest.php | 4 +- .../Locations/UpdateLocationRequest.php | 4 +- .../Application}/Nodes/DeleteNodeRequest.php | 4 +- .../Application}/Nodes/GetNodeRequest.php | 4 +- .../Application}/Nodes/GetNodesRequest.php | 4 +- .../Application}/Nodes/StoreNodeRequest.php | 4 +- .../Application}/Nodes/UpdateNodeRequest.php | 2 +- .../Application}/Users/DeleteUserRequest.php | 4 +- .../Application}/Users/GetUserRequest.php | 2 +- .../Application}/Users/GetUsersRequest.php | 4 +- .../Application}/Users/StoreUserRequest.php | 4 +- .../Application}/Users/UpdateUserRequest.php | 2 +- .../Remote/SftpAuthenticationFormRequest.php | 0 app/Providers/RouteServiceProvider.php | 8 ++-- public/js/laroute.js | 2 +- routes/{api-admin.php => api-application.php} | 6 +-- .../Api/Admin/AuthenticateIPAccessTest.php | 4 +- .../Api/Admin/AuthenticateKeyTest.php | 4 +- .../Api/Admin/AuthenticateUserTest.php | 4 +- .../Api/Admin/SetSessionDriverTest.php | 4 +- 41 files changed, 156 insertions(+), 111 deletions(-) create mode 100644 app/Http/Controllers/Api/Application/ApplicationApiController.php rename app/Http/Controllers/{API/Admin => Api/Application}/Locations/LocationController.php (84%) rename app/Http/Controllers/{API/Admin => Api/Application}/Nodes/AllocationController.php (82%) rename app/Http/Controllers/{API/Admin => Api/Application}/Nodes/NodeController.php (84%) create mode 100644 app/Http/Controllers/Api/Application/Servers/ServerController.php rename app/Http/Controllers/{API/Admin => Api/Application}/Users/UserController.php (87%) rename app/Http/Middleware/Api/{Admin => Application}/AuthenticateIPAccess.php (95%) rename app/Http/Middleware/Api/{Admin => Application}/AuthenticateKey.php (98%) rename app/Http/Middleware/Api/{Admin => Application}/AuthenticateUser.php (92%) rename app/Http/Middleware/Api/{Admin => Application}/SetSessionDriver.php (95%) rename app/Http/Requests/{API/Admin => Api/Application}/Allocations/DeleteAllocationRequest.php (87%) rename app/Http/Requests/{API/Admin => Api/Application}/Allocations/GetAllocationsRequest.php (82%) rename app/Http/Requests/{API/Admin => Api/Application}/ApiAdminRequest.php (91%) rename app/Http/Requests/{API/Admin => Api/Application}/Locations/DeleteLocationRequest.php (82%) rename app/Http/Requests/{API/Admin => Api/Application}/Locations/GetLocationRequest.php (73%) rename app/Http/Requests/{API/Admin => Api/Application}/Locations/GetLocationsRequest.php (68%) rename app/Http/Requests/{API/Admin => Api/Application}/Locations/StoreLocationRequest.php (86%) rename app/Http/Requests/{API/Admin => Api/Application}/Locations/UpdateLocationRequest.php (83%) rename app/Http/Requests/{API/Admin => Api/Application}/Nodes/DeleteNodeRequest.php (83%) rename app/Http/Requests/{API/Admin => Api/Application}/Nodes/GetNodeRequest.php (74%) rename app/Http/Requests/{API/Admin => Api/Application}/Nodes/GetNodesRequest.php (68%) rename app/Http/Requests/{API/Admin => Api/Application}/Nodes/StoreNodeRequest.php (94%) rename app/Http/Requests/{API/Admin => Api/Application}/Nodes/UpdateNodeRequest.php (93%) rename app/Http/Requests/{API/Admin => Api/Application}/Users/DeleteUserRequest.php (82%) rename app/Http/Requests/{API/Admin => Api/Application}/Users/GetUserRequest.php (85%) rename app/Http/Requests/{API/Admin => Api/Application}/Users/GetUsersRequest.php (68%) rename app/Http/Requests/{API/Admin => Api/Application}/Users/StoreUserRequest.php (89%) rename app/Http/Requests/{API/Admin => Api/Application}/Users/UpdateUserRequest.php (93%) rename app/Http/Requests/{API => Api}/Remote/SftpAuthenticationFormRequest.php (100%) rename routes/{api-admin.php => api-application.php} (96%) diff --git a/app/Http/Controllers/API/Remote/EggInstallController.php b/app/Http/Controllers/API/Remote/EggInstallController.php index 98c83b00c..65aedfbff 100644 --- a/app/Http/Controllers/API/Remote/EggInstallController.php +++ b/app/Http/Controllers/API/Remote/EggInstallController.php @@ -1,6 +1,6 @@ fractal = $fractal; + $this->repository = $repository; + } + + public function index(Request $request): array + { + $servers = $this->repository->paginated(50); + + return $this->fractal->collection($servers) + ->transformWith((new ServerTransformer)->setKey()) + ->withResourceName('server') + ->toArray(); + } +} diff --git a/app/Http/Controllers/API/Admin/Users/UserController.php b/app/Http/Controllers/Api/Application/Users/UserController.php similarity index 87% rename from app/Http/Controllers/API/Admin/Users/UserController.php rename to app/Http/Controllers/Api/Application/Users/UserController.php index 3cb435b66..48017ffc7 100644 --- a/app/Http/Controllers/API/Admin/Users/UserController.php +++ b/app/Http/Controllers/Api/Application/Users/UserController.php @@ -1,6 +1,6 @@ key(), $this->resource, $this->permission); + return AdminAcl::check($this->key(), $this->resource, $this->permission); } /** diff --git a/app/Http/Requests/API/Admin/Locations/DeleteLocationRequest.php b/app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php similarity index 82% rename from app/Http/Requests/API/Admin/Locations/DeleteLocationRequest.php rename to app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php index eb2ebba51..ee788fa74 100644 --- a/app/Http/Requests/API/Admin/Locations/DeleteLocationRequest.php +++ b/app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php @@ -1,10 +1,10 @@ namespace($this->namespace . '\Server') ->group(base_path('routes/server.php')); - Route::middleware(['api'])->prefix('/api/admin') - ->namespace($this->namespace . '\API\Admin') - ->group(base_path('routes/api-admin.php')); + Route::middleware(['api'])->prefix('/api/application') + ->namespace($this->namespace . '\Api\Application') + ->group(base_path('routes/api-application.php')); Route::middleware(['daemon'])->prefix('/api/remote') - ->namespace($this->namespace . '\API\Remote') + ->namespace($this->namespace . '\Api\Remote') ->group(base_path('routes/api-remote.php')); Route::middleware(['web', 'daemon-old'])->prefix('/daemon') diff --git a/public/js/laroute.js b/public/js/laroute.js index 89d746704..bd13dfa2f 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":["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\AccountKeyController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/api\/new","name":"account.api.new","action":"Pterodactyl\Http\Controllers\Base\AccountKeyController@create"},{"host":null,"methods":["POST"],"uri":"account\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Base\AccountKeyController@store"},{"host":null,"methods":["DELETE"],"uri":"account\/api\/revoke\/{identifier}","name":"account.api.revoke","action":"Pterodactyl\Http\Controllers\Base\AccountKeyController@revoke"},{"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\ApplicationApiController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/api\/new","name":"admin.api.new","action":"Pterodactyl\Http\Controllers\Admin\ApplicationApiController@create"},{"host":null,"methods":["POST"],"uri":"admin\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ApplicationApiController@store"},{"host":null,"methods":["DELETE"],"uri":"admin\/api\/revoke\/{identifier}","name":"admin.api.delete","action":"Pterodactyl\Http\Controllers\Admin\ApplicationApiController@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\/admin\/users","name":"api.admin.user.list","action":"Pterodactyl\Http\Controllers\API\Admin\Users\UserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/users\/{user}","name":"api.admin.user.view","action":"Pterodactyl\Http\Controllers\API\Admin\Users\UserController@view"},{"host":null,"methods":["POST"],"uri":"api\/admin\/users","name":"api.admin.user.store","action":"Pterodactyl\Http\Controllers\API\Admin\Users\UserController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/admin\/users\/{user}","name":"api.admin.user.update","action":"Pterodactyl\Http\Controllers\API\Admin\Users\UserController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/admin\/users\/{user}","name":"api.admin.user.delete","action":"Pterodactyl\Http\Controllers\API\Admin\Users\UserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/nodes","name":"api.admin.node.list","action":"Pterodactyl\Http\Controllers\API\Admin\Nodes\NodeController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/nodes\/{node}","name":"api.admin.node.view","action":"Pterodactyl\Http\Controllers\API\Admin\Nodes\NodeController@view"},{"host":null,"methods":["POST"],"uri":"api\/admin\/nodes","name":"api.admin.node.store","action":"Pterodactyl\Http\Controllers\API\Admin\Nodes\NodeController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/admin\/nodes\/{node}","name":"api.admin.node.update","action":"Pterodactyl\Http\Controllers\API\Admin\Nodes\NodeController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/admin\/nodes\/{node}","name":"api.admin.node.delete","action":"Pterodactyl\Http\Controllers\API\Admin\Nodes\NodeController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/nodes\/{node}\/allocations","name":"api.admin.node.allocations.list","action":"Pterodactyl\Http\Controllers\API\Admin\Nodes\AllocationController@index"},{"host":null,"methods":["DELETE"],"uri":"api\/admin\/nodes\/{node}\/allocations\/{allocation}","name":"api.admin.node.allocations.delete","action":"Pterodactyl\Http\Controllers\API\Admin\Nodes\AllocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/locations","name":"api.admin.location.list","action":"Pterodactyl\Http\Controllers\API\Admin\Locations\LocationController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/locations\/{location}","name":"api.admin.location.view","action":"Pterodactyl\Http\Controllers\API\Admin\Locations\LocationController@view"},{"host":null,"methods":["POST"],"uri":"api\/admin\/locations","name":"api.admin.location.store","action":"Pterodactyl\Http\Controllers\API\Admin\Locations\LocationController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/admin\/locations\/{location}","name":"api.admin.location.update","action":"Pterodactyl\Http\Controllers\API\Admin\Locations\LocationController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/admin\/locations\/{location}","name":"api.admin.location.delete","action":"Pterodactyl\Http\Controllers\API\Admin\Locations\LocationController@delete"},{"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":["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\AccountKeyController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/api\/new","name":"account.api.new","action":"Pterodactyl\Http\Controllers\Base\AccountKeyController@create"},{"host":null,"methods":["POST"],"uri":"account\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Base\AccountKeyController@store"},{"host":null,"methods":["DELETE"],"uri":"account\/api\/revoke\/{identifier}","name":"account.api.revoke","action":"Pterodactyl\Http\Controllers\Base\AccountKeyController@revoke"},{"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\ApplicationApiController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/api\/new","name":"admin.api.new","action":"Pterodactyl\Http\Controllers\Admin\ApplicationApiController@create"},{"host":null,"methods":["POST"],"uri":"admin\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ApplicationApiController@store"},{"host":null,"methods":["DELETE"],"uri":"admin\/api\/revoke\/{identifier}","name":"admin.api.delete","action":"Pterodactyl\Http\Controllers\Admin\ApplicationApiController@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\/admin\/users","name":"api.admin.user.list","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/users\/{user}","name":"api.admin.user.view","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@view"},{"host":null,"methods":["POST"],"uri":"api\/admin\/users","name":"api.admin.user.store","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/admin\/users\/{user}","name":"api.admin.user.update","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/admin\/users\/{user}","name":"api.admin.user.delete","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/nodes","name":"api.admin.node.list","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/nodes\/{node}","name":"api.admin.node.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@view"},{"host":null,"methods":["POST"],"uri":"api\/admin\/nodes","name":"api.admin.node.store","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/admin\/nodes\/{node}","name":"api.admin.node.update","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/admin\/nodes\/{node}","name":"api.admin.node.delete","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/nodes\/{node}\/allocations","name":"api.admin.node.allocations.list","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@index"},{"host":null,"methods":["DELETE"],"uri":"api\/admin\/nodes\/{node}\/allocations\/{allocation}","name":"api.admin.node.allocations.delete","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/locations","name":"api.admin.location.list","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/admin\/locations\/{location}","name":"api.admin.location.view","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@view"},{"host":null,"methods":["POST"],"uri":"api\/admin\/locations","name":"api.admin.location.store","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/admin\/locations\/{location}","name":"api.admin.location.update","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/admin\/locations\/{location}","name":"api.admin.location.delete","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@delete"},{"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/routes/api-admin.php b/routes/api-application.php similarity index 96% rename from routes/api-admin.php rename to routes/api-application.php index 6b04b9cf6..f98c797e6 100644 --- a/routes/api-admin.php +++ b/routes/api-application.php @@ -10,7 +10,7 @@ use Pterodactyl\Models\Allocation; | User Controller Routes |-------------------------------------------------------------------------- | -| Endpoint: /api/admin/users +| Endpoint: /api/application/users | */ Route::group(['prefix' => '/users'], function () { @@ -32,7 +32,7 @@ Route::group(['prefix' => '/users'], function () { | Node Controller Routes |-------------------------------------------------------------------------- | -| Endpoint: /api/admin/nodes +| Endpoint: /api/application/nodes | */ Route::group(['prefix' => '/nodes'], function () { @@ -64,7 +64,7 @@ Route::group(['prefix' => '/nodes'], function () { | Location Controller Routes |-------------------------------------------------------------------------- | -| Endpoint: /api/admin/locations +| Endpoint: /api/application/locations | */ Route::group(['prefix' => '/locations'], function () { diff --git a/tests/Unit/Http/Middleware/Api/Admin/AuthenticateIPAccessTest.php b/tests/Unit/Http/Middleware/Api/Admin/AuthenticateIPAccessTest.php index ee5ac6f20..ffbf77517 100644 --- a/tests/Unit/Http/Middleware/Api/Admin/AuthenticateIPAccessTest.php +++ b/tests/Unit/Http/Middleware/Api/Admin/AuthenticateIPAccessTest.php @@ -4,7 +4,7 @@ namespace Tests\Unit\Http\Middleware\Api\Admin; use Pterodactyl\Models\ApiKey; use Tests\Unit\Http\Middleware\MiddlewareTestCase; -use Pterodactyl\Http\Middleware\Api\Admin\AuthenticateIPAccess; +use Pterodactyl\Http\Middleware\Api\Application\AuthenticateIPAccess; class AuthenticateIPAccessTest extends MiddlewareTestCase { @@ -65,7 +65,7 @@ class AuthenticateIPAccessTest extends MiddlewareTestCase /** * Return an instance of the middleware to be used when testing. * - * @return \Pterodactyl\Http\Middleware\Api\Admin\AuthenticateIPAccess + * @return \Pterodactyl\Http\Middleware\Api\Application\AuthenticateIPAccess */ private function getMiddleware(): AuthenticateIPAccess { diff --git a/tests/Unit/Http/Middleware/Api/Admin/AuthenticateKeyTest.php b/tests/Unit/Http/Middleware/Api/Admin/AuthenticateKeyTest.php index e0de17a45..0827eef3c 100644 --- a/tests/Unit/Http/Middleware/Api/Admin/AuthenticateKeyTest.php +++ b/tests/Unit/Http/Middleware/Api/Admin/AuthenticateKeyTest.php @@ -9,9 +9,9 @@ use Illuminate\Auth\AuthManager; use Illuminate\Contracts\Encryption\Encrypter; use Tests\Unit\Http\Middleware\MiddlewareTestCase; use Symfony\Component\HttpKernel\Exception\HttpException; -use Pterodactyl\Http\Middleware\Api\Admin\AuthenticateKey; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; +use Pterodactyl\Http\Middleware\Api\Application\AuthenticateKey; class AuthenticateKeyTest extends MiddlewareTestCase { @@ -117,7 +117,7 @@ class AuthenticateKeyTest extends MiddlewareTestCase /** * Return an instance of the middleware with mocked dependencies for testing. * - * @return \Pterodactyl\Http\Middleware\Api\Admin\AuthenticateKey + * @return \Pterodactyl\Http\Middleware\Api\Application\AuthenticateKey */ private function getMiddleware(): AuthenticateKey { diff --git a/tests/Unit/Http/Middleware/Api/Admin/AuthenticateUserTest.php b/tests/Unit/Http/Middleware/Api/Admin/AuthenticateUserTest.php index fb243a78f..fa31eaa99 100644 --- a/tests/Unit/Http/Middleware/Api/Admin/AuthenticateUserTest.php +++ b/tests/Unit/Http/Middleware/Api/Admin/AuthenticateUserTest.php @@ -3,7 +3,7 @@ namespace Tests\Unit\Http\Middleware\Api\Admin; use Tests\Unit\Http\Middleware\MiddlewareTestCase; -use Pterodactyl\Http\Middleware\Api\Admin\AuthenticateUser; +use Pterodactyl\Http\Middleware\Api\Application\AuthenticateUser; class AuthenticateUserTest extends MiddlewareTestCase { @@ -44,7 +44,7 @@ class AuthenticateUserTest extends MiddlewareTestCase /** * Return an instance of the middleware for testing. * - * @return \Pterodactyl\Http\Middleware\Api\Admin\AuthenticateUser + * @return \Pterodactyl\Http\Middleware\Api\Application\AuthenticateUser */ private function getMiddleware(): AuthenticateUser { diff --git a/tests/Unit/Http/Middleware/Api/Admin/SetSessionDriverTest.php b/tests/Unit/Http/Middleware/Api/Admin/SetSessionDriverTest.php index b49ee214a..4ad220863 100644 --- a/tests/Unit/Http/Middleware/Api/Admin/SetSessionDriverTest.php +++ b/tests/Unit/Http/Middleware/Api/Admin/SetSessionDriverTest.php @@ -7,7 +7,7 @@ use Barryvdh\Debugbar\LaravelDebugbar; use Illuminate\Contracts\Config\Repository; use Illuminate\Contracts\Foundation\Application; use Tests\Unit\Http\Middleware\MiddlewareTestCase; -use Pterodactyl\Http\Middleware\Api\Admin\SetSessionDriver; +use Pterodactyl\Http\Middleware\Api\Application\SetSessionDriver; class SetSessionDriverTest extends MiddlewareTestCase { @@ -60,7 +60,7 @@ class SetSessionDriverTest extends MiddlewareTestCase /** * Return an instance of the middleware with mocked dependencies for testing. * - * @return \Pterodactyl\Http\Middleware\Api\Admin\SetSessionDriver + * @return \Pterodactyl\Http\Middleware\Api\Application\SetSessionDriver */ private function getMiddleware(): SetSessionDriver { From 06335a1e47787a33ec41e706fd8d505058dc00b4 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 19 Jan 2018 20:00:28 -0600 Subject: [PATCH 38/54] Update test namespace --- .../Api/{Admin => Application}/AuthenticateIPAccessTest.php | 2 +- .../Api/{Admin => Application}/AuthenticateKeyTest.php | 2 +- .../Api/{Admin => Application}/AuthenticateUserTest.php | 2 +- .../Api/{Admin => Application}/SetSessionDriverTest.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename tests/Unit/Http/Middleware/Api/{Admin => Application}/AuthenticateIPAccessTest.php (97%) rename tests/Unit/Http/Middleware/Api/{Admin => Application}/AuthenticateKeyTest.php (98%) rename tests/Unit/Http/Middleware/Api/{Admin => Application}/AuthenticateUserTest.php (96%) rename tests/Unit/Http/Middleware/Api/{Admin => Application}/SetSessionDriverTest.php (97%) diff --git a/tests/Unit/Http/Middleware/Api/Admin/AuthenticateIPAccessTest.php b/tests/Unit/Http/Middleware/Api/Application/AuthenticateIPAccessTest.php similarity index 97% rename from tests/Unit/Http/Middleware/Api/Admin/AuthenticateIPAccessTest.php rename to tests/Unit/Http/Middleware/Api/Application/AuthenticateIPAccessTest.php index ffbf77517..cf23d0292 100644 --- a/tests/Unit/Http/Middleware/Api/Admin/AuthenticateIPAccessTest.php +++ b/tests/Unit/Http/Middleware/Api/Application/AuthenticateIPAccessTest.php @@ -1,6 +1,6 @@ Date: Fri, 19 Jan 2018 20:01:56 -0600 Subject: [PATCH 39/54] Sneaky files --- app/Http/Controllers/API/Remote/SftpController.php | 4 ++-- .../Requests/Api/Remote/SftpAuthenticationFormRequest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/API/Remote/SftpController.php b/app/Http/Controllers/API/Remote/SftpController.php index bc99a7305..04fa432ce 100644 --- a/app/Http/Controllers/API/Remote/SftpController.php +++ b/app/Http/Controllers/API/Remote/SftpController.php @@ -9,7 +9,7 @@ use Pterodactyl\Http\Controllers\Controller; use Illuminate\Foundation\Auth\ThrottlesLogins; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Services\Sftp\AuthenticateUsingPasswordService; -use Pterodactyl\Http\Requests\API\Remote\SftpAuthenticationFormRequest; +use Pterodactyl\Http\Requests\Api\Remote\SftpAuthenticationFormRequest; class SftpController extends Controller { @@ -34,7 +34,7 @@ class SftpController extends Controller * Authenticate a set of credentials and return the associated server details * for a SFTP connection on the daemon. * - * @param \Pterodactyl\Http\Requests\API\Remote\SftpAuthenticationFormRequest $request + * @param \Pterodactyl\Http\Requests\Api\Remote\SftpAuthenticationFormRequest $request * @return \Illuminate\Http\JsonResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException diff --git a/app/Http/Requests/Api/Remote/SftpAuthenticationFormRequest.php b/app/Http/Requests/Api/Remote/SftpAuthenticationFormRequest.php index cb2af93ee..041ff197f 100644 --- a/app/Http/Requests/Api/Remote/SftpAuthenticationFormRequest.php +++ b/app/Http/Requests/Api/Remote/SftpAuthenticationFormRequest.php @@ -1,6 +1,6 @@ Date: Fri, 19 Jan 2018 21:47:06 -0600 Subject: [PATCH 40/54] Make server listing and single server view API endpoints work --- .../Spatie/Fractalistic/Fractal.php | 44 +++++++++ ...ionApiController.php => ApiController.php} | 2 +- .../Locations/LocationController.php | 2 +- .../Nodes/AllocationController.php | 12 +-- .../Api/Application/Nodes/NodeController.php | 2 +- .../Application/Servers/ServerController.php | 53 ++++++---- .../Api/Application/Users/UserController.php | 2 +- ...=> DeleteAllocationRequest.phpApplication} | 4 +- .../Allocations/GetAllocationsRequest.php | 4 +- ...nRequest.php => ApplicationApiRequest.php} | 2 +- .../Locations/DeleteLocationRequest.php | 4 +- .../Locations/GetLocationsRequest.php | 4 +- .../Locations/StoreLocationRequest.php | 4 +- .../Application/Nodes/DeleteNodeRequest.php | 4 +- .../Api/Application/Nodes/GetNodeRequest.php | 3 +- .../Api/Application/Nodes/GetNodesRequest.php | 4 +- .../Application/Nodes/StoreNodeRequest.php | 4 +- .../Application/Servers/GetServerRequest.php | 32 ++++++ .../Application/Servers/GetServersRequest.php | 29 ++++++ .../Application/Users/DeleteUserRequest.php | 4 +- .../Api/Application/Users/GetUsersRequest.php | 4 +- .../Application/Users/StoreUserRequest.php | 4 +- app/Models/Server.php | 6 ++ .../Api/Admin/ServerVariableTransformer.php | 69 ------------- .../AllocationTransformer.php | 2 +- .../BaseTransformer.php | 17 +++- .../Application/EggVariableTransformer.php | 13 +++ .../LocationTransformer.php | 2 +- .../NodeTransformer.php | 2 +- .../OptionTransformer.php | 0 .../PackTransformer.php | 0 .../ServerTransformer.php | 99 ++++++++++++++----- .../Application/ServerVariableTransformer.php | 44 +++++++++ .../ServiceTransformer.php | 0 .../ServiceVariableTransformer.php | 0 .../SubuserTransformer.php | 0 .../UserTransformer.php | 2 +- routes/admin.php | 8 +- routes/api-application.php | 52 ++++++---- 39 files changed, 367 insertions(+), 176 deletions(-) create mode 100644 app/Extensions/Spatie/Fractalistic/Fractal.php rename app/Http/Controllers/Admin/{ApplicationApiController.php => ApiController.php} (98%) rename app/Http/Requests/Api/Application/Allocations/{DeleteAllocationRequest.php => DeleteAllocationRequest.phpApplication} (87%) rename app/Http/Requests/Api/Application/{ApiAdminRequest.php => ApplicationApiRequest.php} (97%) create mode 100644 app/Http/Requests/Api/Application/Servers/GetServerRequest.php create mode 100644 app/Http/Requests/Api/Application/Servers/GetServersRequest.php delete mode 100644 app/Transformers/Api/Admin/ServerVariableTransformer.php rename app/Transformers/Api/{Admin => Application}/AllocationTransformer.php (97%) rename app/Transformers/Api/{Admin => Application}/BaseTransformer.php (80%) create mode 100644 app/Transformers/Api/Application/EggVariableTransformer.php rename app/Transformers/Api/{Admin => Application}/LocationTransformer.php (96%) rename app/Transformers/Api/{Admin => Application}/NodeTransformer.php (98%) rename app/Transformers/Api/{Admin => Application}/OptionTransformer.php (100%) rename app/Transformers/Api/{Admin => Application}/PackTransformer.php (100%) rename app/Transformers/Api/{Admin => Application}/ServerTransformer.php (62%) create mode 100644 app/Transformers/Api/Application/ServerVariableTransformer.php rename app/Transformers/Api/{Admin => Application}/ServiceTransformer.php (100%) rename app/Transformers/Api/{Admin => Application}/ServiceVariableTransformer.php (100%) rename app/Transformers/Api/{Admin => Application}/SubuserTransformer.php (100%) rename app/Transformers/Api/{Admin => Application}/UserTransformer.php (95%) diff --git a/app/Extensions/Spatie/Fractalistic/Fractal.php b/app/Extensions/Spatie/Fractalistic/Fractal.php new file mode 100644 index 000000000..cbe8f46b5 --- /dev/null +++ b/app/Extensions/Spatie/Fractalistic/Fractal.php @@ -0,0 +1,44 @@ +serializer)) { + $this->serializer = new JsonApiSerializer; + } + + // Automatically set the paginator on the response object if the + // data being provided implements a paginator. + if (is_null($this->paginator) && $this->data instanceof LengthAwarePaginator) { + $this->paginator = new IlluminatePaginatorAdapter($this->data); + } + + // Automatically set the resource name if the response data is a model + // and the resource name is available on the model. + if (is_null($this->resourceName) && $this->data instanceof Model) { + if (defined(get_class($this->data) . '::RESOURCE_NAME')) { + $this->resourceName = constant(get_class($this->data) . '::RESOURCE_NAME'); + } + } + + return parent::createData(); + } +} diff --git a/app/Http/Controllers/Admin/ApplicationApiController.php b/app/Http/Controllers/Admin/ApiController.php similarity index 98% rename from app/Http/Controllers/Admin/ApplicationApiController.php rename to app/Http/Controllers/Admin/ApiController.php index 10eb1e47c..bbf4bcbf8 100644 --- a/app/Http/Controllers/Admin/ApplicationApiController.php +++ b/app/Http/Controllers/Admin/ApiController.php @@ -14,7 +14,7 @@ use Pterodactyl\Services\Api\KeyCreationService; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; use Pterodactyl\Http\Requests\Admin\Api\StoreApplicationApiKeyRequest; -class ApplicationApiController extends Controller +class ApiController extends Controller { /** * @var \Prologue\Alerts\AlertsMessageBag diff --git a/app/Http/Controllers/Api/Application/Locations/LocationController.php b/app/Http/Controllers/Api/Application/Locations/LocationController.php index c051c267a..5af4c0782 100644 --- a/app/Http/Controllers/Api/Application/Locations/LocationController.php +++ b/app/Http/Controllers/Api/Application/Locations/LocationController.php @@ -11,8 +11,8 @@ use League\Fractal\Pagination\IlluminatePaginatorAdapter; use Pterodactyl\Services\Locations\LocationUpdateService; use Pterodactyl\Services\Locations\LocationCreationService; use Pterodactyl\Services\Locations\LocationDeletionService; -use Pterodactyl\Transformers\Api\Admin\LocationTransformer; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Transformers\Api\Application\LocationTransformer; use Pterodactyl\Http\Requests\Api\Application\Locations\GetLocationsRequest; use Pterodactyl\Http\Requests\Api\Application\Locations\DeleteLocationRequest; use Pterodactyl\Http\Requests\Api\Application\Locations\UpdateLocationRequest; diff --git a/app/Http/Controllers/Api/Application/Nodes/AllocationController.php b/app/Http/Controllers/Api/Application/Nodes/AllocationController.php index 3c770a145..969d4b55d 100644 --- a/app/Http/Controllers/Api/Application/Nodes/AllocationController.php +++ b/app/Http/Controllers/Api/Application/Nodes/AllocationController.php @@ -8,11 +8,11 @@ use Illuminate\Http\Response; use Pterodactyl\Models\Allocation; use Pterodactyl\Http\Controllers\Controller; use League\Fractal\Pagination\IlluminatePaginatorAdapter; -use Pterodactyl\Transformers\Api\Admin\AllocationTransformer; use Pterodactyl\Services\Allocations\AllocationDeletionService; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Transformers\Api\Application\AllocationTransformer; use Pterodactyl\Http\Requests\Api\Application\Allocations\GetAllocationsRequest; -use Pterodactyl\Http\Requests\Api\Application\Allocations\DeleteAllocationRequest; +use Pterodactyl\Http\Requests\Api\Application\Allocations\DeleteAllocationRequestApplication; class AllocationController extends Controller { @@ -66,14 +66,14 @@ class AllocationController extends Controller /** * Delete a specific allocation from the Panel. * - * @param \Pterodactyl\Http\Requests\Api\Application\Allocations\DeleteAllocationRequest $request - * @param \Pterodactyl\Models\Node $node - * @param \Pterodactyl\Models\Allocation $allocation + * @param \Pterodactyl\Http\Requests\Api\Application\Allocations\DeleteAllocationRequestApplication $request + * @param \Pterodactyl\Models\Node $node + * @param \Pterodactyl\Models\Allocation $allocation * @return \Illuminate\Http\Response * * @throws \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException */ - public function delete(DeleteAllocationRequest $request, Node $node, Allocation $allocation): Response + public function delete(DeleteAllocationRequestApplication $request, Node $node, Allocation $allocation): Response { $this->deletionService->handle($allocation); diff --git a/app/Http/Controllers/Api/Application/Nodes/NodeController.php b/app/Http/Controllers/Api/Application/Nodes/NodeController.php index 1abbbe2c0..cf115db96 100644 --- a/app/Http/Controllers/Api/Application/Nodes/NodeController.php +++ b/app/Http/Controllers/Api/Application/Nodes/NodeController.php @@ -10,9 +10,9 @@ use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Nodes\NodeUpdateService; use Pterodactyl\Services\Nodes\NodeCreationService; use Pterodactyl\Services\Nodes\NodeDeletionService; -use Pterodactyl\Transformers\Api\Admin\NodeTransformer; use League\Fractal\Pagination\IlluminatePaginatorAdapter; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Transformers\Api\Application\NodeTransformer; use Pterodactyl\Http\Requests\Api\Application\Nodes\GetNodeRequest; use Pterodactyl\Http\Requests\Api\Application\Nodes\GetNodesRequest; use Pterodactyl\Http\Requests\Api\Application\Nodes\StoreNodeRequest; diff --git a/app/Http/Controllers/Api/Application/Servers/ServerController.php b/app/Http/Controllers/Api/Application/Servers/ServerController.php index 90a5ea39a..f1d7398a3 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerController.php @@ -1,20 +1,18 @@ fractal = $fractal; + parent::__construct($fractal, $request); + $this->repository = $repository; } - public function index(Request $request): array + /** + * Return all of the servers that currently exist on the Panel. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\GetServersRequest $request + * @return array + */ + public function index(GetServersRequest $request): array { - $servers = $this->repository->paginated(50); + $servers = $this->repository->setSearchTerm($request->input('search'))->paginated(50); return $this->fractal->collection($servers) - ->transformWith((new ServerTransformer)->setKey()) - ->withResourceName('server') + ->transformWith($this->getTransformer(ServerTransformer::class)) + ->toArray(); + } + + /** + * Show a single server transformed for the application API. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\GetServerRequest $request + * @param \Pterodactyl\Models\Server $server + * @return array + */ + public function view(GetServerRequest $request, Server $server): array + { + return $this->fractal->item($server) + ->transformWith($this->getTransformer(ServerTransformer::class)) ->toArray(); } } diff --git a/app/Http/Controllers/Api/Application/Users/UserController.php b/app/Http/Controllers/Api/Application/Users/UserController.php index 48017ffc7..35c534536 100644 --- a/app/Http/Controllers/Api/Application/Users/UserController.php +++ b/app/Http/Controllers/Api/Application/Users/UserController.php @@ -11,9 +11,9 @@ use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Users\UserUpdateService; use Pterodactyl\Services\Users\UserCreationService; use Pterodactyl\Services\Users\UserDeletionService; -use Pterodactyl\Transformers\Api\Admin\UserTransformer; use League\Fractal\Pagination\IlluminatePaginatorAdapter; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Transformers\Api\Application\UserTransformer; use Pterodactyl\Http\Requests\Api\Application\Users\GetUserRequest; use Pterodactyl\Http\Requests\Api\Application\Users\GetUsersRequest; use Pterodactyl\Http\Requests\Api\Application\Users\StoreUserRequest; diff --git a/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php b/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.phpApplication similarity index 87% rename from app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php rename to app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.phpApplication index 1ee934a82..21da6bfb3 100644 --- a/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php +++ b/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.phpApplication @@ -5,9 +5,9 @@ namespace Pterodactyl\Http\Requests\Api\Application\Allocations; use Pterodactyl\Models\Node; use Pterodactyl\Models\Allocation; use Pterodactyl\Services\Acl\Api\AdminAcl; -use Pterodactyl\Http\Requests\Api\Application\ApiAdminRequest; +use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; -class DeleteAllocationRequest extends ApiAdminRequest +class DeleteAllocationRequest extends ApplicationApiRequest { /** * @var string diff --git a/app/Http/Requests/Api/Application/Allocations/GetAllocationsRequest.php b/app/Http/Requests/Api/Application/Allocations/GetAllocationsRequest.php index f7af3edab..150bdb95e 100644 --- a/app/Http/Requests/Api/Application/Allocations/GetAllocationsRequest.php +++ b/app/Http/Requests/Api/Application/Allocations/GetAllocationsRequest.php @@ -4,9 +4,9 @@ namespace Pterodactyl\Http\Requests\Api\Application\Allocations; use Pterodactyl\Models\Node; use Pterodactyl\Services\Acl\Api\AdminAcl; -use Pterodactyl\Http\Requests\Api\Application\ApiAdminRequest; +use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; -class GetAllocationsRequest extends ApiAdminRequest +class GetAllocationsRequest extends ApplicationApiRequest { /** * @var string diff --git a/app/Http/Requests/Api/Application/ApiAdminRequest.php b/app/Http/Requests/Api/Application/ApplicationApiRequest.php similarity index 97% rename from app/Http/Requests/Api/Application/ApiAdminRequest.php rename to app/Http/Requests/Api/Application/ApplicationApiRequest.php index 267d2a54d..2cf7c8478 100644 --- a/app/Http/Requests/Api/Application/ApiAdminRequest.php +++ b/app/Http/Requests/Api/Application/ApplicationApiRequest.php @@ -8,7 +8,7 @@ use Illuminate\Foundation\Http\FormRequest; use Pterodactyl\Exceptions\PterodactylException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -abstract class ApiAdminRequest extends FormRequest +abstract class ApplicationApiRequest extends FormRequest { /** * The resource that should be checked when performing the authorization diff --git a/app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php b/app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php index ee788fa74..d1863eea7 100644 --- a/app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php +++ b/app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php @@ -4,9 +4,9 @@ namespace Pterodactyl\Http\Requests\Api\Application\Locations; use Pterodactyl\Models\Location; use Pterodactyl\Services\Acl\Api\AdminAcl; -use Pterodactyl\Http\Requests\Api\Application\ApiAdminRequest; +use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; -class DeleteLocationRequest extends ApiAdminRequest +class DeleteLocationRequest extends ApplicationApiRequest { /** * @var string diff --git a/app/Http/Requests/Api/Application/Locations/GetLocationsRequest.php b/app/Http/Requests/Api/Application/Locations/GetLocationsRequest.php index ceb21c5b6..5edf00462 100644 --- a/app/Http/Requests/Api/Application/Locations/GetLocationsRequest.php +++ b/app/Http/Requests/Api/Application/Locations/GetLocationsRequest.php @@ -3,9 +3,9 @@ namespace Pterodactyl\Http\Requests\Api\Application\Locations; use Pterodactyl\Services\Acl\Api\AdminAcl; -use Pterodactyl\Http\Requests\Api\Application\ApiAdminRequest; +use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; -class GetLocationsRequest extends ApiAdminRequest +class GetLocationsRequest extends ApplicationApiRequest { /** * @var string diff --git a/app/Http/Requests/Api/Application/Locations/StoreLocationRequest.php b/app/Http/Requests/Api/Application/Locations/StoreLocationRequest.php index 50683acb9..5a71883a3 100644 --- a/app/Http/Requests/Api/Application/Locations/StoreLocationRequest.php +++ b/app/Http/Requests/Api/Application/Locations/StoreLocationRequest.php @@ -4,9 +4,9 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Locations; use Pterodactyl\Models\Location; use Pterodactyl\Services\Acl\Api\AdminAcl; -use Pterodactyl\Http\Requests\Api\Application\ApiAdminRequest; +use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; -class StoreLocationRequest extends ApiAdminRequest +class StoreLocationRequest extends ApplicationApiRequest { /** * @var string diff --git a/app/Http/Requests/Api/Application/Nodes/DeleteNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/DeleteNodeRequest.php index b7dabe2d5..77dd24eac 100644 --- a/app/Http/Requests/Api/Application/Nodes/DeleteNodeRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/DeleteNodeRequest.php @@ -4,9 +4,9 @@ namespace Pterodactyl\Http\Requests\Api\Application\Nodes; use Pterodactyl\Models\Node; use Pterodactyl\Services\Acl\Api\AdminAcl; -use Pterodactyl\Http\Requests\Api\Application\ApiAdminRequest; +use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; -class DeleteNodeRequest extends ApiAdminRequest +class DeleteNodeRequest extends ApplicationApiRequest { /** * @var string diff --git a/app/Http/Requests/Api/Application/Nodes/GetNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/GetNodeRequest.php index 41d30043e..fbf957edd 100644 --- a/app/Http/Requests/Api/Application/Nodes/GetNodeRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/GetNodeRequest.php @@ -3,9 +3,8 @@ namespace Pterodactyl\Http\Requests\Api\Application\Nodes; use Pterodactyl\Models\Node; -use Pterodactyl\Http\Requests\Api\Application\ApiAdminRequest; -class GetNodeRequest extends ApiAdminRequest +class GetNodeRequest extends GetNodesRequest { /** * Determine if the requested node exists on the Panel. diff --git a/app/Http/Requests/Api/Application/Nodes/GetNodesRequest.php b/app/Http/Requests/Api/Application/Nodes/GetNodesRequest.php index b6bc1db7e..fc5f5a38e 100644 --- a/app/Http/Requests/Api/Application/Nodes/GetNodesRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/GetNodesRequest.php @@ -3,9 +3,9 @@ namespace Pterodactyl\Http\Requests\Api\Application\Nodes; use Pterodactyl\Services\Acl\Api\AdminAcl; -use Pterodactyl\Http\Requests\Api\Application\ApiAdminRequest; +use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; -class GetNodesRequest extends ApiAdminRequest +class GetNodesRequest extends ApplicationApiRequest { /** * @var string diff --git a/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php index d1d720f04..20a29f199 100644 --- a/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php @@ -4,9 +4,9 @@ namespace Pterodactyl\Http\Requests\Api\Application\Nodes; use Pterodactyl\Models\Node; use Pterodactyl\Services\Acl\Api\AdminAcl; -use Pterodactyl\Http\Requests\Api\Application\ApiAdminRequest; +use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; -class StoreNodeRequest extends ApiAdminRequest +class StoreNodeRequest extends ApplicationApiRequest { /** * @var string diff --git a/app/Http/Requests/Api/Application/Servers/GetServerRequest.php b/app/Http/Requests/Api/Application/Servers/GetServerRequest.php new file mode 100644 index 000000000..42428ece7 --- /dev/null +++ b/app/Http/Requests/Api/Application/Servers/GetServerRequest.php @@ -0,0 +1,32 @@ +route()->parameter('server'); + + return $server instanceof Server && $server->exists; + } +} diff --git a/app/Http/Requests/Api/Application/Servers/GetServersRequest.php b/app/Http/Requests/Api/Application/Servers/GetServersRequest.php new file mode 100644 index 000000000..922610385 --- /dev/null +++ b/app/Http/Requests/Api/Application/Servers/GetServersRequest.php @@ -0,0 +1,29 @@ + 'string|max:100', + ]; + } +} diff --git a/app/Http/Requests/Api/Application/Users/DeleteUserRequest.php b/app/Http/Requests/Api/Application/Users/DeleteUserRequest.php index f46e97937..571b29c63 100644 --- a/app/Http/Requests/Api/Application/Users/DeleteUserRequest.php +++ b/app/Http/Requests/Api/Application/Users/DeleteUserRequest.php @@ -4,9 +4,9 @@ namespace Pterodactyl\Http\Requests\Api\Application\Users; use Pterodactyl\Models\User; use Pterodactyl\Services\Acl\Api\AdminAcl; -use Pterodactyl\Http\Requests\Api\Application\ApiAdminRequest; +use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; -class DeleteUserRequest extends ApiAdminRequest +class DeleteUserRequest extends ApplicationApiRequest { /** * @var string diff --git a/app/Http/Requests/Api/Application/Users/GetUsersRequest.php b/app/Http/Requests/Api/Application/Users/GetUsersRequest.php index c0043f08f..8736a8e9d 100644 --- a/app/Http/Requests/Api/Application/Users/GetUsersRequest.php +++ b/app/Http/Requests/Api/Application/Users/GetUsersRequest.php @@ -3,9 +3,9 @@ namespace Pterodactyl\Http\Requests\Api\Application\Users; use Pterodactyl\Services\Acl\Api\AdminAcl as Acl; -use Pterodactyl\Http\Requests\Api\Application\ApiAdminRequest; +use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; -class GetUsersRequest extends ApiAdminRequest +class GetUsersRequest extends ApplicationApiRequest { /** * @var string diff --git a/app/Http/Requests/Api/Application/Users/StoreUserRequest.php b/app/Http/Requests/Api/Application/Users/StoreUserRequest.php index 00a2c668b..e6416826c 100644 --- a/app/Http/Requests/Api/Application/Users/StoreUserRequest.php +++ b/app/Http/Requests/Api/Application/Users/StoreUserRequest.php @@ -4,9 +4,9 @@ namespace Pterodactyl\Http\Requests\Api\Application\Users; use Pterodactyl\Models\User; use Pterodactyl\Services\Acl\Api\AdminAcl; -use Pterodactyl\Http\Requests\Api\Application\ApiAdminRequest; +use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; -class StoreUserRequest extends ApiAdminRequest +class StoreUserRequest extends ApplicationApiRequest { /** * @var string diff --git a/app/Models/Server.php b/app/Models/Server.php index 9f81584c8..01c5eef15 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -22,6 +22,12 @@ class Server extends Model implements CleansAttributes, ValidableContract { use BelongsToThrough, Eloquence, Notifiable, Validable; + /** + * The resource name for this model when it is transformed into an + * API representation using fractal. + */ + const RESOURCE_NAME = 'server'; + /** * The table associated with the model. * diff --git a/app/Transformers/Api/Admin/ServerVariableTransformer.php b/app/Transformers/Api/Admin/ServerVariableTransformer.php deleted file mode 100644 index 6f526f3f4..000000000 --- a/app/Transformers/Api/Admin/ServerVariableTransformer.php +++ /dev/null @@ -1,69 +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\ServerVariable; -use League\Fractal\TransformerAbstract; - -class ServerVariableTransformer extends TransformerAbstract -{ - /** - * List of resources that can be included. - * - * @var array - */ - protected $availableIncludes = ['parent']; - - /** - * 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 server variable array. - * - * @return array - */ - public function transform(ServerVariable $variable) - { - return $variable->toArray(); - } - - /** - * Return the parent service variable data. - * - * @return \Leauge\Fractal\Resource\Item - */ - public function includeParent(ServerVariable $variable) - { - if ($this->request && ! $this->request->apiKeyHasPermission('option-view')) { - return; - } - - return $this->item($variable->variable, new ServiceVariableTransformer($this->request), 'variable'); - } -} diff --git a/app/Transformers/Api/Admin/AllocationTransformer.php b/app/Transformers/Api/Application/AllocationTransformer.php similarity index 97% rename from app/Transformers/Api/Admin/AllocationTransformer.php rename to app/Transformers/Api/Application/AllocationTransformer.php index a1766ffd7..10d08def6 100644 --- a/app/Transformers/Api/Admin/AllocationTransformer.php +++ b/app/Transformers/Api/Application/AllocationTransformer.php @@ -1,6 +1,6 @@ call([$this, 'handle']); + } + } + /** * Set the HTTP request class being used for this request. * @@ -59,11 +70,11 @@ abstract class BaseTransformer extends TransformerAbstract * * @param string $abstract * @param array $parameters - * @return \Pterodactyl\Transformers\Api\Admin\BaseTransformer + * @return \Pterodactyl\Transformers\Api\Application\BaseTransformer */ protected function makeTransformer(string $abstract, array $parameters = []): self { - /** @var \Pterodactyl\Transformers\Api\Admin\BaseTransformer $transformer */ + /** @var \Pterodactyl\Transformers\Api\Application\BaseTransformer $transformer */ $transformer = Container::getInstance()->makeWith($abstract, $parameters); $transformer->setKey($this->getKey()); diff --git a/app/Transformers/Api/Application/EggVariableTransformer.php b/app/Transformers/Api/Application/EggVariableTransformer.php new file mode 100644 index 000000000..357f7de1b --- /dev/null +++ b/app/Transformers/Api/Application/EggVariableTransformer.php @@ -0,0 +1,13 @@ +toArray(); + } +} diff --git a/app/Transformers/Api/Admin/LocationTransformer.php b/app/Transformers/Api/Application/LocationTransformer.php similarity index 96% rename from app/Transformers/Api/Admin/LocationTransformer.php rename to app/Transformers/Api/Application/LocationTransformer.php index 81fa9d928..0c29f1801 100644 --- a/app/Transformers/Api/Admin/LocationTransformer.php +++ b/app/Transformers/Api/Application/LocationTransformer.php @@ -1,6 +1,6 @@ environmentService = $environmentService; + } + /** * Return a generic transformed server array. * * @param \Pterodactyl\Models\Server $server * @return array + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function transform(Server $server): array { - return collect($server->toArray())->only($server->getTableColumns())->toArray(); + return [ + 'id' => $server->getKey(), + 'uuid' => $server->uuid, + 'identifier' => $server->uuidShort, + 'name' => $server->name, + 'description' => $server->description, + 'suspended' => (bool) $server->suspended, + 'limits' => [ + 'memory' => $server->memory, + 'swap' => $server->swap, + 'disk' => $server->disk, + 'io' => $server->io, + 'cpu' => $server->cpu, + ], + 'user' => $server->owner_id, + 'node' => $server->node_id, + 'allocation' => $server->allocation_id, + 'nest' => $server->nest_id, + 'egg' => $server->egg_id, + 'pack' => $server->pack_id, + 'container' => [ + 'startup_command' => $server->startup, + 'image' => $server->image, + 'installed' => (int) $server->installed === 1, + 'environment' => $this->environmentService->handle($server), + ], + 'created_at' => Chronos::createFromFormat(Chronos::DEFAULT_TO_STRING_FORMAT, $server->created_at)->setTimezone('UTC')->toIso8601String(), + 'updated_at' => Chronos::createFromFormat(Chronos::DEFAULT_TO_STRING_FORMAT, $server->updated_at)->setTimezone('UTC')->toIso8601String(), + ]; } /** @@ -94,16 +139,16 @@ class ServerTransformer extends BaseTransformer * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource */ - 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'); +// +// return $this->item($server->getRelation('pack'), $this->makeTransformer(PackTransformer::class), 'pack'); +// } /** * Return a generic array with nest information for this server. @@ -111,16 +156,16 @@ class ServerTransformer extends BaseTransformer * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource */ - 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. @@ -136,14 +181,14 @@ class ServerTransformer extends BaseTransformer $server->loadMissing('egg'); - return $this->item($server->getRelation('egg'), $this->makeTransformer(EggTransformer::class), 'egg'); + return $this->item($server->getRelation('egg'), $this->makeTransformer(EggVariableTransformer::class), 'egg'); } /** * Return a generic array of data about subusers for this server. * * @param \Pterodactyl\Models\Server $server - * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource */ public function includeVariables(Server $server) { @@ -153,7 +198,7 @@ class ServerTransformer extends BaseTransformer $server->loadMissing('variables'); - return $this->item($server->getRelation('variables'), $this->makeTransformer(ServerVariableTransformer::class), 'server_variable'); + return $this->collection($server->getRelation('variables'), $this->makeTransformer(ServerVariableTransformer::class), 'server_variable'); } /** diff --git a/app/Transformers/Api/Application/ServerVariableTransformer.php b/app/Transformers/Api/Application/ServerVariableTransformer.php new file mode 100644 index 000000000..b965bc861 --- /dev/null +++ b/app/Transformers/Api/Application/ServerVariableTransformer.php @@ -0,0 +1,44 @@ +toArray(); + } + + /** + * Return the parent service variable data. + * + * @param \Pterodactyl\Models\ServerVariable $variable + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + */ + public function includeParent(ServerVariable $variable) + { + if (! $this->authorize(AdminAcl::RESOURCE_EGGS)) { + return $this->null(); + } + + $variable->loadMissing('variable'); + + return $this->item($variable->getRelation('variable'), $this->makeTransformer(EggVariableTransformer::class), 'variable'); + } +} diff --git a/app/Transformers/Api/Admin/ServiceTransformer.php b/app/Transformers/Api/Application/ServiceTransformer.php similarity index 100% rename from app/Transformers/Api/Admin/ServiceTransformer.php rename to app/Transformers/Api/Application/ServiceTransformer.php diff --git a/app/Transformers/Api/Admin/ServiceVariableTransformer.php b/app/Transformers/Api/Application/ServiceVariableTransformer.php similarity index 100% rename from app/Transformers/Api/Admin/ServiceVariableTransformer.php rename to app/Transformers/Api/Application/ServiceVariableTransformer.php diff --git a/app/Transformers/Api/Admin/SubuserTransformer.php b/app/Transformers/Api/Application/SubuserTransformer.php similarity index 100% rename from app/Transformers/Api/Admin/SubuserTransformer.php rename to app/Transformers/Api/Application/SubuserTransformer.php diff --git a/app/Transformers/Api/Admin/UserTransformer.php b/app/Transformers/Api/Application/UserTransformer.php similarity index 95% rename from app/Transformers/Api/Admin/UserTransformer.php rename to app/Transformers/Api/Application/UserTransformer.php index 54a38469e..b6aac29b9 100644 --- a/app/Transformers/Api/Admin/UserTransformer.php +++ b/app/Transformers/Api/Application/UserTransformer.php @@ -1,6 +1,6 @@ name('admin.index'); | */ Route::group(['prefix' => 'api'], function () { - Route::get('/', 'ApplicationApiController@index')->name('admin.api.index'); - Route::get('/new', 'ApplicationApiController@create')->name('admin.api.new'); + Route::get('/', 'ApiController@index')->name('admin.api.index'); + Route::get('/new', 'ApiController@create')->name('admin.api.new'); - Route::post('/new', 'ApplicationApiController@store'); + Route::post('/new', 'ApiController@store'); - Route::delete('/revoke/{identifier}', 'ApplicationApiController@delete')->name('admin.api.delete'); + Route::delete('/revoke/{identifier}', 'ApiController@delete')->name('admin.api.delete'); }); /* diff --git a/routes/api-application.php b/routes/api-application.php index f98c797e6..9ba57216e 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -2,6 +2,7 @@ use Pterodactyl\Models\Node; use Pterodactyl\Models\User; +use Pterodactyl\Models\Server; use Pterodactyl\Models\Location; use Pterodactyl\Models\Allocation; @@ -18,13 +19,13 @@ Route::group(['prefix' => '/users'], function () { return User::find($value) ?? new User; }); - Route::get('/', 'Users\UserController@index')->name('api.admin.user.list'); - Route::get('/{user}', 'Users\UserController@view')->name('api.admin.user.view'); + Route::get('/', 'Users\UserController@index')->name('api.application.users'); + Route::get('/{user}', 'Users\UserController@view')->name('api.applications.users.view'); - Route::post('/', 'Users\UserController@store')->name('api.admin.user.store'); - Route::patch('/{user}', 'Users\UserController@update')->name('api.admin.user.update'); + Route::post('/', 'Users\UserController@store'); + Route::patch('/{user}', 'Users\UserController@update'); - Route::delete('/{user}', 'Users\UserController@delete')->name('api.admin.user.delete'); + Route::delete('/{user}', 'Users\UserController@delete'); }); /* @@ -40,22 +41,22 @@ Route::group(['prefix' => '/nodes'], function () { return Node::find($value) ?? new Node; }); - Route::get('/', 'Nodes\NodeController@index')->name('api.admin.node.list'); - Route::get('/{node}', 'Nodes\NodeController@view')->name('api.admin.node.view'); + Route::get('/', 'Nodes\NodeController@index')->name('api.application.nodes'); + Route::get('/{node}', 'Nodes\NodeController@view')->name('api.application.nodes.view'); - Route::post('/', 'Nodes\NodeController@store')->name('api.admin.node.store'); - Route::patch('/{node}', 'Nodes\NodeController@update')->name('api.admin.node.update'); + Route::post('/', 'Nodes\NodeController@store'); + Route::patch('/{node}', 'Nodes\NodeController@update'); - Route::delete('/{node}', 'Nodes\NodeController@delete')->name('api.admin.node.delete'); + Route::delete('/{node}', 'Nodes\NodeController@delete'); Route::group(['prefix' => '/{node}/allocations'], function () { Route::bind('allocation', function ($value) { return Allocation::find($value) ?? new Allocation; }); - Route::get('/', 'Nodes\AllocationController@index')->name('api.admin.node.allocations.list'); + Route::get('/', 'Nodes\AllocationController@index')->name('api.application.allocations'); - Route::delete('/{allocation}', 'Nodes\AllocationController@delete')->name('api.admin.node.allocations.delete'); + Route::delete('/{allocation}', 'Nodes\AllocationController@delete')->name('api.application.allocations.view'); }); }); @@ -72,11 +73,28 @@ Route::group(['prefix' => '/locations'], function () { return Location::find($value) ?? new Location; }); - Route::get('/', 'Locations\LocationController@index')->name('api.admin.location.list'); - Route::get('/{location}', 'Locations\LocationController@view')->name('api.admin.location.view'); + Route::get('/', 'Locations\LocationController@index')->name('api.applications.locations'); + Route::get('/{location}', 'Locations\LocationController@view')->name('api.application.locations.view'); - Route::post('/', 'Locations\LocationController@store')->name('api.admin.location.store'); - Route::patch('/{location}', 'Locations\LocationController@update')->name('api.admin.location.update'); + Route::post('/', 'Locations\LocationController@store'); + Route::patch('/{location}', 'Locations\LocationController@update'); - Route::delete('/{location}', 'Locations\LocationController@delete')->name('api.admin.location.delete'); + Route::delete('/{location}', 'Locations\LocationController@delete'); +}); + +/* +|-------------------------------------------------------------------------- +| Location Controller Routes +|-------------------------------------------------------------------------- +| +| Endpoint: /api/application/servers +| +*/ +Route::group(['prefix' => '/servers'], function () { + Route::bind('location', function ($value) { + return Server::find($value) ?? new Location; + }); + + Route::get('/', 'Servers\ServerController@index')->name('api.application.servers'); + Route::get('/{server}', 'Servers\ServerController@view')->name('api.application.servers'); }); From 3724559468ef54be42f2efb8661d6d7763bf8ddf Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 19 Jan 2018 21:48:26 -0600 Subject: [PATCH 41/54] Forgotten changes --- .../Application/ApplicationApiController.php | 55 +++++++++++++++++++ routes/api-application.php | 6 +- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Api/Application/ApplicationApiController.php b/app/Http/Controllers/Api/Application/ApplicationApiController.php index b3d9bbc7f..3285a5bf9 100644 --- a/app/Http/Controllers/Api/Application/ApplicationApiController.php +++ b/app/Http/Controllers/Api/Application/ApplicationApiController.php @@ -1 +1,56 @@ fractal = $fractal; + $this->request = $request; + + // Parse all of the includes to use on this request. + $includes = collect(explode(',', $request->input('include', '')))->map(function ($value) { + return trim($value); + })->filter()->toArray(); + + $this->fractal->parseIncludes($includes); + $this->fractal->limitRecursion(2); + } + + /** + * Return an instance of an application transformer. + * + * @param string $abstract + * @return \Pterodactyl\Transformers\Api\Application\BaseTransformer + */ + public function getTransformer(string $abstract) + { + /** @var \Pterodactyl\Transformers\Api\Application\BaseTransformer $transformer */ + $transformer = Container::getInstance()->make($abstract); + $transformer->setKey($this->request->attributes->get('api_key')); + + return $transformer; + } +} diff --git a/routes/api-application.php b/routes/api-application.php index 9ba57216e..504b17bc8 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -84,15 +84,15 @@ Route::group(['prefix' => '/locations'], function () { /* |-------------------------------------------------------------------------- -| Location Controller Routes +| Server Controller Routes |-------------------------------------------------------------------------- | | Endpoint: /api/application/servers | */ Route::group(['prefix' => '/servers'], function () { - Route::bind('location', function ($value) { - return Server::find($value) ?? new Location; + Route::bind('server', function ($value) { + return Server::find($value) ?? new Server; }); Route::get('/', 'Servers\ServerController@index')->name('api.application.servers'); From 17544481b59cc286a980a6212e6c2d3f15d5f044 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 20 Jan 2018 13:48:02 -0600 Subject: [PATCH 42/54] More server management via the API --- .../Application/ApplicationApiController.php | 34 ++++-- .../Application/Servers/ServerController.php | 41 +++++-- .../Servers/ServerManagementController.php | 115 ++++++++++++++++++ ...rverRequest.php => ServerWriteRequest.php} | 4 +- app/Services/Servers/SuspensionService.php | 15 ++- routes/api-application.php | 10 +- 6 files changed, 193 insertions(+), 26 deletions(-) create mode 100644 app/Http/Controllers/Api/Application/Servers/ServerManagementController.php rename app/Http/Requests/Api/Application/Servers/{GetServerRequest.php => ServerWriteRequest.php} (86%) diff --git a/app/Http/Controllers/Api/Application/ApplicationApiController.php b/app/Http/Controllers/Api/Application/ApplicationApiController.php index 3285a5bf9..3a6473553 100644 --- a/app/Http/Controllers/Api/Application/ApplicationApiController.php +++ b/app/Http/Controllers/Api/Application/ApplicationApiController.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Http\Controllers\Api\Application; use Illuminate\Http\Request; +use Illuminate\Http\Response; use Illuminate\Container\Container; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Extensions\Spatie\Fractalistic\Fractal; @@ -21,17 +22,13 @@ abstract class ApplicationApiController extends Controller /** * ApplicationApiController constructor. - * - * @param \Pterodactyl\Extensions\Spatie\Fractalistic\Fractal $fractal - * @param \Illuminate\Http\Request $request */ - public function __construct(Fractal $fractal, Request $request) + public function __construct() { - $this->fractal = $fractal; - $this->request = $request; + Container::getInstance()->call([$this, 'loadDependencies']); // Parse all of the includes to use on this request. - $includes = collect(explode(',', $request->input('include', '')))->map(function ($value) { + $includes = collect(explode(',', $this->request->input('include', '')))->map(function ($value) { return trim($value); })->filter()->toArray(); @@ -39,6 +36,19 @@ abstract class ApplicationApiController extends Controller $this->fractal->limitRecursion(2); } + /** + * Perform dependency injection of certain classes needed for core functionality + * without littering the constructors of classes that extend this abstract. + * + * @param \Pterodactyl\Extensions\Spatie\Fractalistic\Fractal $fractal + * @param \Illuminate\Http\Request $request + */ + public function loadDependencies(Fractal $fractal, Request $request) + { + $this->fractal = $fractal; + $this->request = $request; + } + /** * Return an instance of an application transformer. * @@ -53,4 +63,14 @@ abstract class ApplicationApiController extends Controller return $transformer; } + + /** + * Return a HTTP/204 response for the API. + * + * @return \Illuminate\Http\Response + */ + protected function returnNoContent(): Response + { + return new Response('', Response::HTTP_NO_CONTENT); + } } diff --git a/app/Http/Controllers/Api/Application/Servers/ServerController.php b/app/Http/Controllers/Api/Application/Servers/ServerController.php index f1d7398a3..98d90f284 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerController.php @@ -2,17 +2,22 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Servers; -use Illuminate\Http\Request; +use Illuminate\Http\Response; use Pterodactyl\Models\Server; -use Pterodactyl\Extensions\Spatie\Fractalistic\Fractal; +use Pterodactyl\Services\Servers\ServerDeletionService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Transformers\Api\Application\ServerTransformer; -use Pterodactyl\Http\Requests\Api\Application\Servers\GetServerRequest; use Pterodactyl\Http\Requests\Api\Application\Servers\GetServersRequest; +use Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; class ServerController extends ApplicationApiController { + /** + * @var \Pterodactyl\Services\Servers\ServerDeletionService + */ + private $deletionService; + /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ @@ -21,14 +26,14 @@ class ServerController extends ApplicationApiController /** * ServerController constructor. * - * @param \Pterodactyl\Extensions\Spatie\Fractalistic\Fractal $fractal - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Services\Servers\ServerDeletionService $deletionService * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository */ - public function __construct(Fractal $fractal, Request $request, ServerRepositoryInterface $repository) + public function __construct(ServerDeletionService $deletionService, ServerRepositoryInterface $repository) { - parent::__construct($fractal, $request); + parent::__construct(); + $this->deletionService = $deletionService; $this->repository = $repository; } @@ -50,14 +55,30 @@ class ServerController extends ApplicationApiController /** * Show a single server transformed for the application API. * - * @param \Pterodactyl\Http\Requests\Api\Application\Servers\GetServerRequest $request - * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest $request + * @param \Pterodactyl\Models\Server $server * @return array */ - public function view(GetServerRequest $request, Server $server): array + public function view(ServerWriteRequest $request, Server $server): array { return $this->fractal->item($server) ->transformWith($this->getTransformer(ServerTransformer::class)) ->toArray(); } + + /** + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest $request + * @param \Pterodactyl\Models\Server $server + * @param string $force + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function delete(ServerWriteRequest $request, Server $server, string $force = ''): Response + { + $this->deletionService->withForce($force === 'force')->handle($server); + + return $this->returnNoContent(); + } } diff --git a/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php b/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php new file mode 100644 index 000000000..4379616fa --- /dev/null +++ b/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php @@ -0,0 +1,115 @@ +rebuildService = $rebuildService; + $this->reinstallServerService = $reinstallServerService; + $this->suspensionService = $suspensionService; + } + + /** + * Suspend a server on the Panel. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest $request + * @param \Pterodactyl\Models\Server $server + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function suspend(ServerWriteRequest $request, Server $server): Response + { + $this->suspensionService->toggle($server, SuspensionService::ACTION_SUSPEND); + + return $this->returnNoContent(); + } + + /** + * Unsuspend a server on the Panel. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest $request + * @param \Pterodactyl\Models\Server $server + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function unsuspend(ServerWriteRequest $request, Server $server): Response + { + $this->suspensionService->toggle($server, SuspensionService::ACTION_UNSUSPEND); + + return $this->returnNoContent(); + } + + /** + * Mark a server as needing to be reinstalled. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest $request + * @param \Pterodactyl\Models\Server $server + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function reinstall(ServerWriteRequest $request, Server $server): Response + { + $this->reinstallServerService->reinstall($server); + + return $this->returnNoContent(); + } + + /** + * Mark a server as needing its container rebuilt the next time it is started. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest $request + * @param \Pterodactyl\Models\Server $server + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + */ + public function rebuild(ServerWriteRequest $request, Server $server): Response + { + $this->rebuildService->handle($server); + + return $this->returnNoContent(); + } +} diff --git a/app/Http/Requests/Api/Application/Servers/GetServerRequest.php b/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php similarity index 86% rename from app/Http/Requests/Api/Application/Servers/GetServerRequest.php rename to app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php index 42428ece7..728b1ce52 100644 --- a/app/Http/Requests/Api/Application/Servers/GetServerRequest.php +++ b/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php @@ -6,7 +6,7 @@ use Pterodactyl\Models\Server; use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; -class GetServerRequest extends ApplicationApiRequest +class ServerWriteRequest extends ApplicationApiRequest { /** * @var string @@ -16,7 +16,7 @@ class GetServerRequest extends ApplicationApiRequest /** * @var int */ - protected $permission = AdminAcl::READ; + protected $permission = AdminAcl::WRITE; /** * Determine if the requested server exists on the Panel. diff --git a/app/Services/Servers/SuspensionService.php b/app/Services/Servers/SuspensionService.php index 1feb2887d..896cfeb17 100644 --- a/app/Services/Servers/SuspensionService.php +++ b/app/Services/Servers/SuspensionService.php @@ -19,6 +19,9 @@ use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonS class SuspensionService { + const ACTION_SUSPEND = 'suspend'; + const ACTION_UNSUSPEND = 'unsuspend'; + /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface */ @@ -70,29 +73,29 @@ class SuspensionService * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function toggle($server, $action = 'suspend') + public function toggle($server, $action = self::ACTION_SUSPEND) { if (! $server instanceof Server) { $server = $this->repository->find($server); } - if (! in_array($action, ['suspend', 'unsuspend'])) { + if (! in_array($action, [self::ACTION_SUSPEND, self::ACTION_UNSUSPEND])) { throw new \InvalidArgumentException(sprintf( - 'Action must be either suspend or unsuspend, %s passed.', + 'Action must be either ' . self::ACTION_SUSPEND . ' or ' . self::ACTION_UNSUSPEND . ', %s passed.', $action )); } if ( - $action === 'suspend' && $server->suspended || - $action === 'unsuspend' && ! $server->suspended + $action === self::ACTION_SUSPEND && $server->suspended || + $action === self::ACTION_UNSUSPEND && ! $server->suspended ) { return true; } $this->database->beginTransaction(); $this->repository->withoutFreshModel()->update($server->id, [ - 'suspended' => $action === 'suspend', + 'suspended' => $action === self::ACTION_SUSPEND, ]); try { diff --git a/routes/api-application.php b/routes/api-application.php index 504b17bc8..f6d2577c0 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -96,5 +96,13 @@ Route::group(['prefix' => '/servers'], function () { }); Route::get('/', 'Servers\ServerController@index')->name('api.application.servers'); - Route::get('/{server}', 'Servers\ServerController@view')->name('api.application.servers'); + Route::get('/{server}', 'Servers\ServerController@view')->name('api.application.servers.view'); + + Route::post('/{server}/suspend', 'Servers\ServerManagementController@suspend')->name('api.application.servers.suspend'); + Route::post('/{server}/unsuspend', 'Servers\ServerManagementController@unsuspend')->name('api.application.servers.unsuspend'); + Route::post('/{server}/reinstall', 'Servers\ServerManagementController@reinstall')->name('api.application.servers.reinstall'); + Route::post('/{server}/rebuild', 'Servers\ServerManagementController@rebuild')->name('api.application.servers.rebuild'); + + Route::delete('/{server}', 'Servers\ServerController@delete'); + Route::delete('/{server}/{force?}', 'Servers\ServerController@delete'); }); From 3e327b8b0e661f2d1063c365c797eb74b93561bf Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 20 Jan 2018 15:33:04 -0600 Subject: [PATCH 43/54] Use more logical route binding to not reveal resources on the API unless authenticated. --- app/Http/Kernel.php | 3 +- .../Middleware/Api/ApiSubstituteBindings.php | 70 +++++++++++++++++++ app/Providers/RouteServiceProvider.php | 8 --- routes/api-application.php | 26 ------- 4 files changed, 72 insertions(+), 35 deletions(-) create mode 100644 app/Http/Middleware/Api/ApiSubstituteBindings.php diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 3b6966064..1d33e210d 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -19,6 +19,7 @@ use Pterodactyl\Http\Middleware\AccessingValidServer; use Illuminate\View\Middleware\ShareErrorsFromSession; use Pterodactyl\Http\Middleware\RedirectIfAuthenticated; use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; +use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings; use Illuminate\Foundation\Http\Middleware\ValidatePostSize; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Pterodactyl\Http\Middleware\Server\AuthenticateAsSubuser; @@ -68,7 +69,7 @@ class Kernel extends HttpKernel ], 'api' => [ 'throttle:120,1', - SubstituteBindings::class, + ApiSubstituteBindings::class, SetSessionDriver::class, AuthenticateKey::class, AuthenticateUser::class, diff --git a/app/Http/Middleware/Api/ApiSubstituteBindings.php b/app/Http/Middleware/Api/ApiSubstituteBindings.php new file mode 100644 index 000000000..430d4597b --- /dev/null +++ b/app/Http/Middleware/Api/ApiSubstituteBindings.php @@ -0,0 +1,70 @@ +route(); + + $this->router->substituteBindings($route); + $this->resolveForRoute($route); + + return $next($request); + } + + /** + * Resolve the implicit route bindings for the given route. This function + * overrides Laravel's default inn \Illuminate\Routing\ImplictRouteBinding + * to not throw a 404 error when a model is not found. + * + * If a model is not found using the provided information, the binding is + * replaced with null which is then checked in the form requests on API + * routes. This avoids a potential imformation leak on API routes for + * unauthenticated users. + * + * @param \Illuminate\Routing\Route $route + */ + protected function resolveForRoute($route) + { + $parameters = $route->parameters(); + + // Avoid having to copy and paste the entirety of that class into this middleware + // by using reflection to access a protected method. + $reflection = new ReflectionMethod(ImplicitRouteBinding::class, 'getParameterName'); + $reflection->setAccessible(true); + + foreach ($route->signatureParameters(UrlRoutable::class) as $parameter) { + if (! $parameterName = $reflection->invokeArgs(null, [$parameter->name, $parameters])) { + continue; + } + + $parameterValue = $parameters[$parameterName]; + + if ($parameterValue instanceof UrlRoutable) { + continue; + } + + // Try to find an existing model, if one is not found simply bind the + // parameter as null. + $instance = Container::getInstance()->make($parameter->getClass()->name); + $route->setParameter($parameterName, $instance->resolveRouteBinding($parameterValue)); + } + } +} diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 130b7ba72..f9f6ac31d 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -16,14 +16,6 @@ class RouteServiceProvider extends ServiceProvider */ protected $namespace = 'Pterodactyl\Http\Controllers'; - /** - * Define your route model bindings, pattern filters, etc. - */ - public function boot() - { - parent::boot(); - } - /** * Define the routes for the application. */ diff --git a/routes/api-application.php b/routes/api-application.php index f6d2577c0..83ea5a2a0 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -1,11 +1,5 @@ '/users'], function () { - Route::bind('user', function ($value) { - return User::find($value) ?? new User; - }); - Route::get('/', 'Users\UserController@index')->name('api.application.users'); Route::get('/{user}', 'Users\UserController@view')->name('api.applications.users.view'); @@ -37,10 +27,6 @@ Route::group(['prefix' => '/users'], function () { | */ Route::group(['prefix' => '/nodes'], function () { - Route::bind('node', function ($value) { - return Node::find($value) ?? new Node; - }); - Route::get('/', 'Nodes\NodeController@index')->name('api.application.nodes'); Route::get('/{node}', 'Nodes\NodeController@view')->name('api.application.nodes.view'); @@ -50,10 +36,6 @@ Route::group(['prefix' => '/nodes'], function () { Route::delete('/{node}', 'Nodes\NodeController@delete'); Route::group(['prefix' => '/{node}/allocations'], function () { - Route::bind('allocation', function ($value) { - return Allocation::find($value) ?? new Allocation; - }); - Route::get('/', 'Nodes\AllocationController@index')->name('api.application.allocations'); Route::delete('/{allocation}', 'Nodes\AllocationController@delete')->name('api.application.allocations.view'); @@ -69,10 +51,6 @@ Route::group(['prefix' => '/nodes'], function () { | */ Route::group(['prefix' => '/locations'], function () { - Route::bind('location', function ($value) { - return Location::find($value) ?? new Location; - }); - Route::get('/', 'Locations\LocationController@index')->name('api.applications.locations'); Route::get('/{location}', 'Locations\LocationController@view')->name('api.application.locations.view'); @@ -91,10 +69,6 @@ Route::group(['prefix' => '/locations'], function () { | */ Route::group(['prefix' => '/servers'], function () { - Route::bind('server', function ($value) { - return Server::find($value) ?? new Server; - }); - Route::get('/', 'Servers\ServerController@index')->name('api.application.servers'); Route::get('/{server}', 'Servers\ServerController@view')->name('api.application.servers.view'); From 17f6f3eeb650b88ffad301f86eb992214e7c4a59 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 20 Jan 2018 16:03:23 -0600 Subject: [PATCH 44/54] Add server details modification endpoint to API. --- .../Controllers/Admin/ServersController.php | 2 +- .../Servers/ServerDetailsController.php | 47 ++++ .../Servers/UpdateServerDetailsRequest.php | 53 ++++ app/Models/Server.php | 2 +- .../Servers/DetailsModificationService.php | 94 ++------ routes/api-application.php | 2 + .../DetailsModificationServiceTest.php | 227 +++--------------- 7 files changed, 160 insertions(+), 267 deletions(-) create mode 100644 app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php create mode 100644 app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index c3e33b964..75063335e 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -401,7 +401,7 @@ class ServersController extends Controller */ public function setDetails(Request $request, Server $server) { - $this->detailsModificationService->edit($server, $request->only([ + $this->detailsModificationService->handle($server, $request->only([ 'owner_id', 'name', 'description', ])); diff --git a/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php b/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php new file mode 100644 index 000000000..638948590 --- /dev/null +++ b/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php @@ -0,0 +1,47 @@ +modificationService = $modificationService; + } + + /** + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\UpdateServerDetailsRequest $request + * @param \Pterodactyl\Models\Server $server + * @return array + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function details(UpdateServerDetailsRequest $request, Server $server): array + { + $server = $this->modificationService->returnUpdatedModel()->handle($server, $request->validated()); + + return $this->fractal->item($server) + ->transformWith($this->getTransformer(ServerTransformer::class)) + ->toArray(); + } +} diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php new file mode 100644 index 000000000..011293b76 --- /dev/null +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php @@ -0,0 +1,53 @@ +route()->parameter('server')->id); + + return [ + 'name' => $rules['name'], + 'user' => $rules['owner_id'], + 'description' => $rules['description'], + ]; + } + + /** + * Convert the posted data into the correct format that is expected + * by the application. + * + * @return array + */ + public function validated(): array + { + return [ + 'name' => $this->input('name'), + 'owner_id' => $this->input('user'), + 'description' => $this->input('description'), + ]; + } + + /** + * Rename some of the attributes in error messages to clarify the field + * being discussed. + * + * @return array + */ + public function attributes(): array + { + return [ + 'user' => 'User ID', + 'name' => 'Server Name', + ]; + } +} diff --git a/app/Models/Server.php b/app/Models/Server.php index 01c5eef15..d09291e56 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -80,7 +80,7 @@ class Server extends Model implements CleansAttributes, ValidableContract */ protected static $dataIntegrityRules = [ 'owner_id' => 'exists:users,id', - 'name' => 'regex:/^([\w .-]{1,200})$/', + 'name' => 'string|min:1|max:255', 'node_id' => 'exists:nodes,id', 'description' => 'string', 'memory' => 'numeric|min:0', diff --git a/app/Services/Servers/DetailsModificationService.php b/app/Services/Servers/DetailsModificationService.php index 5ba4f779b..cabebf254 100644 --- a/app/Services/Servers/DetailsModificationService.php +++ b/app/Services/Servers/DetailsModificationService.php @@ -1,55 +1,37 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Servers; -use Illuminate\Log\Writer; use Pterodactyl\Models\Server; -use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Traits\Services\ReturnsUpdatedModels; use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService; use Pterodactyl\Services\DaemonKeys\DaemonKeyDeletionService; -use Pterodactyl\Repositories\Daemon\ServerRepository as DaemonServerRepository; class DetailsModificationService { + use ReturnsUpdatedModels; + /** * @var \Illuminate\Database\ConnectionInterface */ - protected $connection; - - /** - * @var \Pterodactyl\Repositories\Daemon\ServerRepository - */ - protected $daemonServerRepository; + private $connection; /** * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService */ - protected $keyCreationService; + private $keyCreationService; /** * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyDeletionService */ - protected $keyDeletionService; + private $keyDeletionService; /** * @var \Pterodactyl\Repositories\Eloquent\ServerRepository */ - protected $repository; - - /** - * @var \Illuminate\Log\Writer - */ - protected $writer; + private $repository; /** * DetailsModificationService constructor. @@ -57,92 +39,48 @@ class DetailsModificationService * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService $keyCreationService * @param \Pterodactyl\Services\DaemonKeys\DaemonKeyDeletionService $keyDeletionService - * @param \Pterodactyl\Repositories\Daemon\ServerRepository $daemonServerRepository * @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository - * @param \Illuminate\Log\Writer $writer */ public function __construct( ConnectionInterface $connection, DaemonKeyCreationService $keyCreationService, DaemonKeyDeletionService $keyDeletionService, - DaemonServerRepository $daemonServerRepository, - ServerRepository $repository, - Writer $writer + ServerRepository $repository ) { $this->connection = $connection; - $this->daemonServerRepository = $daemonServerRepository; $this->keyCreationService = $keyCreationService; $this->keyDeletionService = $keyDeletionService; $this->repository = $repository; - $this->writer = $writer; } /** * Update the details for a single server instance. * - * @param int|\Pterodactyl\Models\Server $server - * @param array $data + * @param \Pterodactyl\Models\Server $server + * @param array $data + * @return bool|\Pterodactyl\Models\Server * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function edit($server, array $data) + public function handle(Server $server, array $data) { - if (! $server instanceof Server) { - $server = $this->repository->find($server); - } - $this->connection->beginTransaction(); - $this->repository->withoutFreshModel()->update($server->id, [ + + $response = $this->repository->setFreshModel($this->getUpdatedModel())->update($server->id, [ 'owner_id' => array_get($data, 'owner_id'), 'name' => array_get($data, 'name'), 'description' => array_get($data, 'description', ''), ], true, true); - if (array_get($data, 'owner_id') != $server->owner_id) { + if ((int) array_get($data, 'owner_id', 0) !== (int) $server->owner_id) { $this->keyDeletionService->handle($server, $server->owner_id); $this->keyCreationService->handle($server->id, array_get($data, 'owner_id')); } $this->connection->commit(); - } - /** - * Update the docker container for a specified server. - * - * @param int|\Pterodactyl\Models\Server $server - * @param string $image - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function setDockerImage($server, $image) - { - if (! $server instanceof Server) { - $server = $this->repository->find($server); - } - - $this->connection->beginTransaction(); - $this->repository->withoutFreshModel()->update($server->id, ['image' => $image]); - - try { - $this->daemonServerRepository->setServer($server)->update([ - 'build' => [ - 'image' => $image, - ], - ]); - } catch (RequestException $exception) { - $this->connection->rollBack(); - $response = $exception->getResponse(); - $this->writer->warning($exception); - - throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ - 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), - ])); - } - - $this->connection->commit(); + return $response; } } diff --git a/routes/api-application.php b/routes/api-application.php index 83ea5a2a0..f98b3739b 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -72,6 +72,8 @@ Route::group(['prefix' => '/servers'], function () { Route::get('/', 'Servers\ServerController@index')->name('api.application.servers'); Route::get('/{server}', 'Servers\ServerController@view')->name('api.application.servers.view'); + Route::patch('/{server}/details', 'Servers\ServerDetailsController@details')->name('api.application.servers.details'); + Route::post('/{server}/suspend', 'Servers\ServerManagementController@suspend')->name('api.application.servers.suspend'); Route::post('/{server}/unsuspend', 'Servers\ServerManagementController@unsuspend')->name('api.application.servers.unsuspend'); Route::post('/{server}/reinstall', 'Servers\ServerManagementController@reinstall')->name('api.application.servers.reinstall'); diff --git a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php index 410ec5b95..bd049d018 100644 --- a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php +++ b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php @@ -1,71 +1,37 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Services\Servers; -use Exception; use Mockery as m; use Tests\TestCase; -use Illuminate\Log\Writer; -use GuzzleHttp\Psr7\Response; use Pterodactyl\Models\Server; -use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\PterodactylException; use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Services\Servers\DetailsModificationService; use Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService; use Pterodactyl\Services\DaemonKeys\DaemonKeyDeletionService; -use Pterodactyl\Repositories\Daemon\ServerRepository as DaemonServerRepository; class DetailsModificationServiceTest extends TestCase { /** * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock */ - protected $connection; - - /** - * @var \Pterodactyl\Repositories\Daemon\ServerRepository|\Mockery\Mock - */ - protected $daemonServerRepository; - - /** - * @var \GuzzleHttp\Exception\RequestException|\Mockery\Mock - */ - protected $exception; + private $connection; /** * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService|\Mockery\Mock */ - protected $keyCreationService; + private $keyCreationService; /** * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyDeletionService|\Mockery\Mock */ - protected $keyDeletionService; + private $keyDeletionService; /** * @var \Pterodactyl\Repositories\Eloquent\ServerRepository|\Mockery\Mock */ - protected $repository; - - /** - * @var \Pterodactyl\Services\Servers\DetailsModificationService - */ - protected $service; - - /** - * @var \Illuminate\Log\Writer|\Mockery\Mock - */ - protected $writer; + private $repository; /** * Setup tests. @@ -75,72 +41,49 @@ class DetailsModificationServiceTest extends TestCase parent::setUp(); $this->connection = m::mock(ConnectionInterface::class); - $this->exception = m::mock(RequestException::class)->makePartial(); - $this->daemonServerRepository = m::mock(DaemonServerRepository::class); $this->keyCreationService = m::mock(DaemonKeyCreationService::class); $this->keyDeletionService = m::mock(DaemonKeyDeletionService::class); $this->repository = m::mock(ServerRepository::class); - $this->writer = m::mock(Writer::class); - - $this->service = new DetailsModificationService( - $this->connection, - $this->keyCreationService, - $this->keyDeletionService, - $this->daemonServerRepository, - $this->repository, - $this->writer - ); } /** * Test basic updating of core variables when a model is provided. */ - public function testEditShouldSkipDatabaseSearchIfModelIsPassed() + public function testDetailsAreEdited() { - $server = factory(Server::class)->make([ - 'owner_id' => 1, - ]); + $server = factory(Server::class)->make(['owner_id' => 1]); $data = ['owner_id' => 1, 'name' => 'New Name', 'description' => 'New Description']; - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($server->id, [ + $this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull(); + $this->repository->shouldReceive('setFreshModel')->once()->with(false)->andReturnSelf(); + $this->repository->shouldReceive('update')->once()->with($server->id, [ 'owner_id' => $data['owner_id'], 'name' => $data['name'], 'description' => $data['description'], - ], true, true)->once()->andReturnNull(); + ], true, true)->andReturn(true); - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->once()->withNoArgs()->andReturnNull(); - $this->service->edit($server, $data); - $this->assertTrue(true); + $response = $this->getService()->handle($server, $data); + $this->assertTrue($response); } /** - * Test that repository attempts to find model in database if no model is passed. + * Test that a model is returned if requested. */ - public function testEditShouldGetModelFromRepositoryIfNotPassed() + public function testModelIsReturned() { - $server = factory(Server::class)->make([ - 'owner_id' => 1, - ]); + $server = factory(Server::class)->make(['owner_id' => 1]); - $data = ['owner_id' => 1, 'name' => 'New Name', 'description' => 'New Description']; + $this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull(); + $this->repository->shouldReceive('setFreshModel')->once()->with(true)->andReturnSelf(); + $this->repository->shouldReceive('update')->once()->andReturn($server); - $this->repository->shouldReceive('find')->with($server->id)->once()->andReturn($server); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($server->id, [ - 'owner_id' => $data['owner_id'], - 'name' => $data['name'], - 'description' => $data['description'], - ], true, true)->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->once()->withNoArgs()->andReturnNull(); - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $this->service->edit($server->id, $data); - $this->assertTrue(true); + $response = $this->getService()->returnUpdatedModel()->handle($server, ['owner_id' => 1]); + $this->assertInstanceOf(Server::class, $response); } /** @@ -150,128 +93,38 @@ class DetailsModificationServiceTest extends TestCase { $server = factory(Server::class)->make([ 'owner_id' => 1, - 'node_id' => 1, ]); $data = ['owner_id' => 2, 'name' => 'New Name', 'description' => 'New Description']; - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($server->id, [ + $this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull(); + $this->repository->shouldReceive('setFreshModel')->once()->with(false)->andReturnSelf(); + $this->repository->shouldReceive('update')->once()->with($server->id, [ 'owner_id' => $data['owner_id'], 'name' => $data['name'], 'description' => $data['description'], - ], true, true)->once()->andReturnNull(); + ], true, true)->andReturn(true); - $this->keyDeletionService->shouldReceive('handle')->with($server, $server->owner_id)->once()->andReturnNull(); - $this->keyCreationService->shouldReceive('handle')->with($server->id, $data['owner_id']); - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + $this->keyDeletionService->shouldReceive('handle')->once()->with($server, $server->owner_id)->andReturnNull(); + $this->keyCreationService->shouldReceive('handle')->once()->with($server->id, $data['owner_id'])->andReturnNull(); + $this->connection->shouldReceive('commit')->once()->withNoArgs()->andReturnNull(); - $this->service->edit($server, $data); - $this->assertTrue(true); + $response = $this->getService()->handle($server, $data); + $this->assertTrue($response); } /** - * Test that the docker image for a server can be updated if a model is provided. - */ - public function testDockerImageCanBeUpdatedWhenAServerModelIsProvided() - { - $server = factory(Server::class)->make(['node_id' => 1]); - - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($server->id, [ - 'image' => 'new/image', - ])->once()->andReturnNull(); - - $this->daemonServerRepository->shouldReceive('setServer')->with($server)->once()->andReturnSelf() - ->shouldReceive('update')->with([ - 'build' => [ - 'image' => 'new/image', - ], - ])->once()->andReturn(new Response); - - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $this->service->setDockerImage($server, 'new/image'); - $this->assertTrue(true); - } - - /** - * Test that the docker image for a server can be updated if a model is provided. - */ - public function testDockerImageCanBeUpdatedWhenNoModelIsProvided() - { - $server = factory(Server::class)->make(['node_id' => 1]); - - $this->repository->shouldReceive('find')->with($server->id)->once()->andReturn($server); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($server->id, [ - 'image' => 'new/image', - ])->once()->andReturnNull(); - - $this->daemonServerRepository->shouldReceive('setServer')->with($server)->once()->andReturnSelf() - ->shouldReceive('update')->with([ - 'build' => [ - 'image' => 'new/image', - ], - ])->once()->andReturn(new Response); - - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $this->service->setDockerImage($server->id, 'new/image'); - $this->assertTrue(true); - } - - /** - * Test that an exception thrown by Guzzle is rendered as a displayable exception. - */ - public function testExceptionThrownByGuzzleWhenSettingDockerImageShouldBeRenderedAsADisplayableException() - { - $server = factory(Server::class)->make(['node_id' => 1]); - - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($server->id, [ - 'image' => 'new/image', - ])->once()->andReturnNull(); - - $this->daemonServerRepository->shouldReceive('setServer')->andThrow($this->exception); - $this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); - $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400); - - $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); - - try { - $this->service->setDockerImage($server, 'new/image'); - } catch (PterodactylException $exception) { - $this->assertInstanceOf(DisplayException::class, $exception); - $this->assertEquals( - trans('admin/server.exceptions.daemon_exception', ['code' => 400]), - $exception->getMessage() - ); - } - } - - /** - * Test that an exception not thrown by Guzzle is not transformed to a displayable exception. + * Return an instance of the service with mocked dependencies for testing. * - * @expectedException \Exception + * @return \Pterodactyl\Services\Servers\DetailsModificationService */ - public function testExceptionNotThrownByGuzzleWhenSettingDockerImageShouldNotBeRenderedAsADisplayableException() + private function getService(): DetailsModificationService { - $server = factory(Server::class)->make(['node_id' => 1]); - - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($server->id, [ - 'image' => 'new/image', - ])->once()->andReturnNull(); - - $this->daemonServerRepository->shouldReceive('setNode')->andThrow(new Exception()); - - $this->service->setDockerImage($server, 'new/image'); + return new DetailsModificationService( + $this->connection, + $this->keyCreationService, + $this->keyDeletionService, + $this->repository + ); } } From faaf27632cd6ca3102de37ed6a86fc21d7ff7558 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 21 Jan 2018 14:37:57 -0600 Subject: [PATCH 45/54] Fix behavior on automatic resource name setter --- app/Extensions/Spatie/Fractalistic/Fractal.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/Extensions/Spatie/Fractalistic/Fractal.php b/app/Extensions/Spatie/Fractalistic/Fractal.php index cbe8f46b5..9ac0d588c 100644 --- a/app/Extensions/Spatie/Fractalistic/Fractal.php +++ b/app/Extensions/Spatie/Fractalistic/Fractal.php @@ -39,6 +39,15 @@ class Fractal extends SpatieFractal } } + if (is_null($this->resourceName) && $this->data instanceof LengthAwarePaginator) { + $item = collect($this->data->items())->first(); + if ($item instanceof Model) { + if (defined(get_class($item) . '::RESOURCE_NAME')) { + $this->resourceName = constant(get_class($item) . '::RESOURCE_NAME'); + } + } + } + return parent::createData(); } } From d3dba3fcf96134342520a6d5bc6563676aa852fb Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 21 Jan 2018 14:45:20 -0600 Subject: [PATCH 46/54] Fix bug when modifying server descriptions --- .../Api/Application/Servers/UpdateServerDetailsRequest.php | 2 +- app/Services/Servers/DetailsModificationService.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php index 011293b76..4b75138b9 100644 --- a/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php @@ -18,7 +18,7 @@ class UpdateServerDetailsRequest extends ServerWriteRequest return [ 'name' => $rules['name'], 'user' => $rules['owner_id'], - 'description' => $rules['description'], + 'description' => array_merge(['nullable'], $rules['description']), ]; } diff --git a/app/Services/Servers/DetailsModificationService.php b/app/Services/Servers/DetailsModificationService.php index cabebf254..78d8eb31e 100644 --- a/app/Services/Servers/DetailsModificationService.php +++ b/app/Services/Servers/DetailsModificationService.php @@ -71,7 +71,7 @@ class DetailsModificationService $response = $this->repository->setFreshModel($this->getUpdatedModel())->update($server->id, [ 'owner_id' => array_get($data, 'owner_id'), 'name' => array_get($data, 'name'), - 'description' => array_get($data, 'description', ''), + 'description' => array_get($data, 'description') ?? '', ], true, true); if ((int) array_get($data, 'owner_id', 0) !== (int) $server->owner_id) { From aca0819bcd07b03785b66bb02baee8f4951dc8e4 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 21 Jan 2018 16:02:03 -0600 Subject: [PATCH 47/54] Add server build management to API --- .../AllocationRepositoryInterface.php | 17 ++ app/Exceptions/DisplayException.php | 9 +- app/Exceptions/Handler.php | 2 + .../Servers/ServerDetailsController.php | 41 ++++- .../UpdateServerBuildConfigurationRequest.php | 61 +++++++ .../Eloquent/AllocationRepository.php | 28 ++- .../Servers/BuildModificationService.php | 171 ++++++------------ routes/api-application.php | 1 + 8 files changed, 202 insertions(+), 128 deletions(-) create mode 100644 app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php diff --git a/app/Contracts/Repository/AllocationRepositoryInterface.php b/app/Contracts/Repository/AllocationRepositoryInterface.php index acfea56ea..47dfe9475 100644 --- a/app/Contracts/Repository/AllocationRepositoryInterface.php +++ b/app/Contracts/Repository/AllocationRepositoryInterface.php @@ -40,4 +40,21 @@ interface AllocationRepositoryInterface extends RepositoryInterface * @return \Illuminate\Support\Collection */ public function getUniqueAllocationIpsForNode(int $node): Collection; + + /** + * Return all of the allocations that exist for a node that are not currently + * allocated. + * + * @param int $node + * @return array + */ + public function getUnassignedAllocationIds(int $node): array; + + /** + * Get an array of all allocations that are currently assigned to a given server. + * + * @param int $server + * @return array + */ + public function getAssignedAllocationIds(int $server): array; } diff --git a/app/Exceptions/DisplayException.php b/app/Exceptions/DisplayException.php index a74f9be26..e6488f89c 100644 --- a/app/Exceptions/DisplayException.php +++ b/app/Exceptions/DisplayException.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Exceptions; @@ -66,7 +59,7 @@ class DisplayException extends PterodactylException if ($request->expectsJson()) { return response()->json(Handler::convertToArray($this, [ 'detail' => $this->getMessage(), - ]), method_exists($this, 'getStatusCode') ? $this->getStatusCode() : Response::HTTP_INTERNAL_SERVER_ERROR); + ]), method_exists($this, 'getStatusCode') ? $this->getStatusCode() : Response::HTTP_BAD_REQUEST); } app()->make(AlertsMessageBag::class)->danger($this->getMessage())->flash(); diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 450328096..952d94335 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -36,6 +36,8 @@ class Handler extends ExceptionHandler * @var array */ protected $dontFlash = [ + 'token', + 'secret', 'password', 'password_confirmation', ]; diff --git a/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php b/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php index 638948590..1a9cb889e 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php @@ -3,31 +3,42 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Servers; use Pterodactyl\Models\Server; +use Pterodactyl\Services\Servers\BuildModificationService; use Pterodactyl\Services\Servers\DetailsModificationService; use Pterodactyl\Transformers\Api\Application\ServerTransformer; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; use Pterodactyl\Http\Requests\Api\Application\Servers\UpdateServerDetailsRequest; +use Pterodactyl\Http\Requests\Api\Application\Servers\UpdateServerBuildConfigurationRequest; class ServerDetailsController extends ApplicationApiController { + /** + * @var \Pterodactyl\Services\Servers\BuildModificationService + */ + private $buildModificationService; + /** * @var \Pterodactyl\Services\Servers\DetailsModificationService */ - private $modificationService; + private $detailsModificationService; /** * ServerDetailsController constructor. * - * @param \Pterodactyl\Services\Servers\DetailsModificationService $modificationService + * @param \Pterodactyl\Services\Servers\BuildModificationService $buildModificationService + * @param \Pterodactyl\Services\Servers\DetailsModificationService $detailsModificationService */ - public function __construct(DetailsModificationService $modificationService) + public function __construct(BuildModificationService $buildModificationService, DetailsModificationService $detailsModificationService) { parent::__construct(); - $this->modificationService = $modificationService; + $this->buildModificationService = $buildModificationService; + $this->detailsModificationService = $detailsModificationService; } /** + * Update the details for a specific server. + * * @param \Pterodactyl\Http\Requests\Api\Application\Servers\UpdateServerDetailsRequest $request * @param \Pterodactyl\Models\Server $server * @return array @@ -38,7 +49,27 @@ class ServerDetailsController extends ApplicationApiController */ public function details(UpdateServerDetailsRequest $request, Server $server): array { - $server = $this->modificationService->returnUpdatedModel()->handle($server, $request->validated()); + $server = $this->detailsModificationService->returnUpdatedModel()->handle($server, $request->validated()); + + return $this->fractal->item($server) + ->transformWith($this->getTransformer(ServerTransformer::class)) + ->toArray(); + } + + /** + * Update the build details for a specific server. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\UpdateServerBuildConfigurationRequest $request + * @param \Pterodactyl\Models\Server $server + * @return array + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function build(UpdateServerBuildConfigurationRequest $request, Server $server): array + { + $server = $this->buildModificationService->handle($server, $request->validated()); return $this->fractal->item($server) ->transformWith($this->getTransformer(ServerTransformer::class)) diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php new file mode 100644 index 000000000..893ff5ff7 --- /dev/null +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php @@ -0,0 +1,61 @@ +route()->parameter('server')->id); + + return [ + 'allocation' => $rules['allocation_id'], + 'memory' => $rules['memory'], + 'swap' => $rules['swap'], + 'io' => $rules['io'], + 'cpu' => $rules['cpu'], + 'disk' => $rules['disk'], + 'add_allocations' => 'bail|array', + 'add_allocations.*' => 'integer', + 'remove_allocations' => 'bail|array', + 'remove_allocations.*' => 'integer', + ]; + } + + /** + * Convert the allocation field into the expected format for the service handler. + * + * @return array + */ + public function validated() + { + $data = parent::validated(); + + $data['allocation_id'] = $data['allocation']; + unset($data['allocation']); + + return $data; + } + + /** + * Custom attributes to use in error message responses. + * + * @return array + */ + public function attributes() + { + return [ + 'add_allocations' => 'allocations to add', + 'remove_allocations' => 'allocations to remove', + 'add_allocations.*' => 'allocation to add', + 'remove_allocations.*' => 'allocation to remove', + ]; + } +} diff --git a/app/Repositories/Eloquent/AllocationRepository.php b/app/Repositories/Eloquent/AllocationRepository.php index f4830678d..82fe860d8 100644 --- a/app/Repositories/Eloquent/AllocationRepository.php +++ b/app/Repositories/Eloquent/AllocationRepository.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Repositories\Eloquent; -use Pterodactyl\Models\Node; use Illuminate\Support\Collection; use Pterodactyl\Models\Allocation; use Illuminate\Contracts\Pagination\LengthAwarePaginator; @@ -68,4 +67,31 @@ class AllocationRepository extends EloquentRepository implements AllocationRepos ->orderByRaw('INET_ATON(ip) ASC') ->get($this->getColumns()); } + + /** + * Return all of the allocations that exist for a node that are not currently + * allocated. + * + * @param int $node + * @return array + */ + public function getUnassignedAllocationIds(int $node): array + { + $results = $this->getBuilder()->select('id')->whereNull('server_id')->where('node_id', $node)->get(); + + return $results->pluck('id')->toArray(); + } + + /** + * Get an array of all allocations that are currently assigned to a given server. + * + * @param int $server + * @return array + */ + public function getAssignedAllocationIds(int $server): array + { + $results = $this->getBuilder()->select('id')->where('server_id', $server)->get(); + + return $results->pluck('id')->toArray(); + } } diff --git a/app/Services/Servers/BuildModificationService.php b/app/Services/Servers/BuildModificationService.php index e952941c8..8924b2a04 100644 --- a/app/Services/Servers/BuildModificationService.php +++ b/app/Services/Servers/BuildModificationService.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Services\Servers; -use Illuminate\Log\Writer; use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; @@ -10,6 +9,7 @@ use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class BuildModificationService @@ -17,105 +17,58 @@ class BuildModificationService /** * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface */ - protected $allocationRepository; - - /** - * @var array - */ - protected $build = []; - - /** - * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface - */ - protected $daemonServerRepository; + private $allocationRepository; /** * @var \Illuminate\Database\ConnectionInterface */ - protected $database; + private $connection; /** - * @var null|int + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface */ - protected $firstAllocationId = null; + private $daemonServerRepository; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ - protected $repository; - - /** - * @var \Illuminate\Log\Writer - */ - protected $writer; + private $repository; /** * BuildModificationService constructor. * * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository - * @param \Illuminate\Database\ConnectionInterface $database + * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository - * @param \Illuminate\Log\Writer $writer */ public function __construct( AllocationRepositoryInterface $allocationRepository, - ConnectionInterface $database, + ConnectionInterface $connection, DaemonServerRepositoryInterface $daemonServerRepository, - ServerRepositoryInterface $repository, - Writer $writer + ServerRepositoryInterface $repository ) { $this->allocationRepository = $allocationRepository; $this->daemonServerRepository = $daemonServerRepository; - $this->database = $database; + $this->connection = $connection; $this->repository = $repository; - $this->writer = $writer; - } - - /** - * Set build array parameters. - * - * @param string $key - * @param mixed $value - */ - public function setBuild($key, $value) - { - $this->build[$key] = $value; - } - - /** - * Return the build array or an item out of the build array. - * - * @param string|null $attribute - * @return array|mixed|null - */ - public function getBuild($attribute = null) - { - if (is_null($attribute)) { - return $this->build; - } - - return array_get($this->build, $attribute); } /** * Change the build details for a specified server. * - * @param int|\Pterodactyl\Models\Server $server - * @param array $data + * @param \Pterodactyl\Models\Server $server + * @param array $data + * @return \Pterodactyl\Models\Server * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function handle($server, array $data) + public function handle(Server $server, array $data) { - if (! $server instanceof Server) { - $server = $this->repository->find($server); - } - - $data['allocation_id'] = array_get($data, 'allocation_id', $server->allocation_id); - $this->database->beginTransaction(); + $build = []; + $this->connection->beginTransaction(); $this->processAllocations($server, $data); if (isset($data['allocation_id']) && $data['allocation_id'] != $server->allocation_id) { @@ -128,108 +81,98 @@ class BuildModificationService throw new DisplayException(trans('admin/server.exceptions.default_allocation_not_found')); } - $this->setBuild('default', ['ip' => $allocation->ip, 'port' => $allocation->port]); + $build['default'] = ['ip' => $allocation->ip, 'port' => $allocation->port]; } - $server = $this->repository->update($server->id, [ - 'memory' => (int) array_get($data, 'memory', $server->memory), - 'swap' => (int) array_get($data, 'swap', $server->swap), - 'io' => (int) array_get($data, 'io', $server->io), - 'cpu' => (int) array_get($data, 'cpu', $server->cpu), - 'disk' => (int) array_get($data, 'disk', $server->disk), - 'allocation_id' => array_get($data, 'allocation_id', $server->allocation_id), + $server = $this->repository->withFreshModel()->update($server->id, [ + 'memory' => array_get($data, 'memory'), + 'swap' => array_get($data, 'swap'), + 'io' => array_get($data, 'io'), + 'cpu' => array_get($data, 'cpu'), + 'disk' => array_get($data, 'disk'), + 'allocation_id' => array_get($data, 'allocation_id'), ]); - $allocations = $this->allocationRepository->findWhere([ - ['server_id', '=', $server->id], - ]); + $allocations = $this->allocationRepository->findWhere([['server_id', '=', $server->id]]); - $this->setBuild('memory', (int) $server->memory); - $this->setBuild('swap', (int) $server->swap); - $this->setBuild('io', (int) $server->io); - $this->setBuild('cpu', (int) $server->cpu); - $this->setBuild('disk', (int) $server->disk); - $this->setBuild('ports|overwrite', $allocations->groupBy('ip')->map(function ($item) { + $build['memory'] = (int) $server->memory; + $build['swap'] = (int) $server->swap; + $build['io'] = (int) $server->io; + $build['cpu'] = (int) $server->cpu; + $build['disk'] = (int) $server->disk; + $build['ports|overwrite'] = $allocations->groupBy('ip')->map(function ($item) { return $item->pluck('port'); - })->toArray()); + })->toArray(); try { - $this->daemonServerRepository->setServer($server)->update([ - 'build' => $this->getBuild(), - ]); - - $this->database->commit(); + $this->daemonServerRepository->setServer($server)->update(['build' => $build]); + $this->connection->commit(); } catch (RequestException $exception) { - $response = $exception->getResponse(); - $this->writer->warning($exception); - - throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ - 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), - ])); + throw new DaemonConnectionException($exception); } + + return $server; } /** - * Process the allocations being assigned in the data and ensure they are available for a server. + * Process the allocations being assigned in the data and ensure they + * are available for a server. * * @param \Pterodactyl\Models\Server $server * @param array $data * * @throws \Pterodactyl\Exceptions\DisplayException */ - public function processAllocations(Server $server, array &$data) + private function processAllocations(Server $server, array &$data) { + $firstAllocationId = null; + if (! array_key_exists('add_allocations', $data) && ! array_key_exists('remove_allocations', $data)) { return; } - // Loop through allocations to add. + // Handle the addition of allocations to this server. if (array_key_exists('add_allocations', $data) && ! empty($data['add_allocations'])) { - $unassigned = $this->allocationRepository->findWhere([ - ['server_id', '=', null], - ['node_id', '=', $server->node_id], - ])->pluck('id')->toArray(); + $unassigned = $this->allocationRepository->getUnassignedAllocationIds($server->node_id); + $updateIds = []; foreach ($data['add_allocations'] as $allocation) { if (! in_array($allocation, $unassigned)) { continue; } - $this->firstAllocationId = $this->firstAllocationId ?? $allocation; - $toUpdate[] = [$allocation]; + $firstAllocationId = $firstAllocationId ?? $allocation; + $updateIds[] = $allocation; } - if (isset($toUpdate)) { - $this->allocationRepository->updateWhereIn('id', $toUpdate, ['server_id' => $server->id]); - unset($toUpdate); + if (! empty($updateIds)) { + $this->allocationRepository->updateWhereIn('id', $updateIds, ['server_id' => $server->id]); } } - // Loop through allocations to remove. + // Handle removal of allocations from this server. if (array_key_exists('remove_allocations', $data) && ! empty($data['remove_allocations'])) { - $assigned = $this->allocationRepository->findWhere([ - ['server_id', '=', $server->id], - ])->pluck('id')->toArray(); + $assigned = $this->allocationRepository->getAssignedAllocationIds($server->id); + $updateIds = []; foreach ($data['remove_allocations'] as $allocation) { if (! in_array($allocation, $assigned)) { continue; } if ($allocation == $data['allocation_id']) { - if (is_null($this->firstAllocationId)) { + if (is_null($firstAllocationId)) { throw new DisplayException(trans('admin/server.exceptions.no_new_default_allocation')); } - $data['allocation_id'] = $this->firstAllocationId; + $data['allocation_id'] = $firstAllocationId; } - $toUpdate[] = [$allocation]; + $updateIds[] = $allocation; } - if (isset($toUpdate)) { - $this->allocationRepository->updateWhereIn('id', $toUpdate, ['server_id' => null]); - unset($toUpdate); + if (! empty($updateIds)) { + $this->allocationRepository->updateWhereIn('id', $updateIds, ['server_id' => null]); } } } diff --git a/routes/api-application.php b/routes/api-application.php index f98b3739b..72576b920 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -73,6 +73,7 @@ Route::group(['prefix' => '/servers'], function () { Route::get('/{server}', 'Servers\ServerController@view')->name('api.application.servers.view'); Route::patch('/{server}/details', 'Servers\ServerDetailsController@details')->name('api.application.servers.details'); + Route::patch('/{server}/build', 'Servers\ServerDetailsController@build')->name('api.application.servers.build'); Route::post('/{server}/suspend', 'Servers\ServerManagementController@suspend')->name('api.application.servers.suspend'); Route::post('/{server}/unsuspend', 'Servers\ServerManagementController@unsuspend')->name('api.application.servers.unsuspend'); From 2bd691efad22419c094cc71bbdcc9902c997b75f Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 25 Jan 2018 21:26:06 -0600 Subject: [PATCH 48/54] Add database list endpoint, add more resource name magic --- .../Spatie/Fractalistic/Fractal.php | 25 ++--- .../Application/ApplicationApiController.php | 2 +- .../Servers/DatabaseController.php | 44 ++++++++ app/Models/APILog.php | 7 -- app/Models/Allocation.php | 13 +-- app/Models/ApiKey.php | 6 +- app/Models/Checksum.php | 38 ------- app/Models/DaemonKey.php | 22 ---- app/Models/Database.php | 13 +-- app/Models/DatabaseHost.php | 13 +-- app/Models/Egg.php | 13 +-- app/Models/EggVariable.php | 13 +-- app/Models/Location.php | 6 + app/Models/Nest.php | 13 +-- app/Models/Node.php | 13 +-- app/Models/Pack.php | 13 +-- app/Models/Permission.php | 13 +-- app/Models/Schedule.php | 13 +-- app/Models/Server.php | 7 -- app/Models/ServerVariable.php | 13 +-- app/Models/Subuser.php | 13 +-- app/Models/Task.php | 13 +-- app/Models/User.php | 6 + app/Services/Acl/Api/AdminAcl.php | 3 +- .../Api/Application/AllocationTransformer.php | 10 ++ .../Api/Application/BaseTransformer.php | 7 ++ .../Application/DatabaseHostTransformer.php | 69 ++++++++++++ .../Application/EggVariableTransformer.php | 11 ++ .../Api/Application/LocationTransformer.php | 10 ++ .../Api/Application/NodeTransformer.php | 10 ++ .../Application/ServerDatabaseTransformer.php | 104 ++++++++++++++++++ .../Api/Application/ServerTransformer.php | 10 ++ .../Application/ServerVariableTransformer.php | 10 ++ .../Api/Application/UserTransformer.php | 10 ++ ...1_11_213943_AddApiKeyPermissionColumns.php | 6 +- routes/api-application.php | 11 ++ 36 files changed, 416 insertions(+), 187 deletions(-) create mode 100644 app/Http/Controllers/Api/Application/Servers/DatabaseController.php delete mode 100644 app/Models/Checksum.php create mode 100644 app/Transformers/Api/Application/DatabaseHostTransformer.php create mode 100644 app/Transformers/Api/Application/ServerDatabaseTransformer.php diff --git a/app/Extensions/Spatie/Fractalistic/Fractal.php b/app/Extensions/Spatie/Fractalistic/Fractal.php index 9ac0d588c..753cd4758 100644 --- a/app/Extensions/Spatie/Fractalistic/Fractal.php +++ b/app/Extensions/Spatie/Fractalistic/Fractal.php @@ -2,7 +2,7 @@ namespace Pterodactyl\Extensions\Spatie\Fractalistic; -use Illuminate\Database\Eloquent\Model; +use League\Fractal\TransformerAbstract; use League\Fractal\Serializer\JsonApiSerializer; use Spatie\Fractalistic\Fractal as SpatieFractal; use Illuminate\Contracts\Pagination\LengthAwarePaginator; @@ -31,21 +31,14 @@ class Fractal extends SpatieFractal $this->paginator = new IlluminatePaginatorAdapter($this->data); } - // Automatically set the resource name if the response data is a model - // and the resource name is available on the model. - if (is_null($this->resourceName) && $this->data instanceof Model) { - if (defined(get_class($this->data) . '::RESOURCE_NAME')) { - $this->resourceName = constant(get_class($this->data) . '::RESOURCE_NAME'); - } - } - - if (is_null($this->resourceName) && $this->data instanceof LengthAwarePaginator) { - $item = collect($this->data->items())->first(); - if ($item instanceof Model) { - if (defined(get_class($item) . '::RESOURCE_NAME')) { - $this->resourceName = constant(get_class($item) . '::RESOURCE_NAME'); - } - } + // If the resource name is not set attempt to pull it off the transformer + // itself and set it automatically. + if ( + is_null($this->resourceName) + && $this->transformer instanceof TransformerAbstract + && method_exists($this->transformer, 'getResourceName') + ) { + $this->resourceName = $this->transformer->getResourceName(); } return parent::createData(); diff --git a/app/Http/Controllers/Api/Application/ApplicationApiController.php b/app/Http/Controllers/Api/Application/ApplicationApiController.php index 3a6473553..c932c3644 100644 --- a/app/Http/Controllers/Api/Application/ApplicationApiController.php +++ b/app/Http/Controllers/Api/Application/ApplicationApiController.php @@ -16,7 +16,7 @@ abstract class ApplicationApiController extends Controller private $request; /** - * @var \Spatie\Fractalistic\Fractal + * @var \Pterodactyl\Extensions\Spatie\Fractalistic\Fractal */ protected $fractal; diff --git a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php new file mode 100644 index 000000000..e1fec4943 --- /dev/null +++ b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php @@ -0,0 +1,44 @@ +repository = $repository; + } + + /** + * Return a listing of all databases currently available to a single + * server. + * + * @param \Pterodactyl\Models\Server $server + * @return array + */ + public function index(Server $server): array + { + $databases = $this->repository->getDatabasesForServer($server->id); + + return $this->fractal->collection($databases) + ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) + ->toArray(); + } +} diff --git a/app/Models/APILog.php b/app/Models/APILog.php index a0dd5f508..359daa4ed 100644 --- a/app/Models/APILog.php +++ b/app/Models/APILog.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Models; diff --git a/app/Models/Allocation.php b/app/Models/Allocation.php index dfa84c017..5921c0a2b 100644 --- a/app/Models/Allocation.php +++ b/app/Models/Allocation.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Models; @@ -19,6 +12,12 @@ class Allocation extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; + /** + * The resource name for this model when it is transformed into an + * API representation using fractal. + */ + const RESOURCE_NAME = 'allocation'; + /** * The table associated with the model. * diff --git a/app/Models/ApiKey.php b/app/Models/ApiKey.php index 8379134ca..bd05454dd 100644 --- a/app/Models/ApiKey.php +++ b/app/Models/ApiKey.php @@ -50,7 +50,8 @@ class ApiKey extends Model implements CleansAttributes, ValidableContract 'user_id' => 'int', 'r_' . AdminAcl::RESOURCE_USERS => 'int', 'r_' . AdminAcl::RESOURCE_ALLOCATIONS => 'int', - 'r_' . AdminAcl::RESOURCE_DATABASES => 'int', + 'r_' . AdminAcl::RESOURCE_DATABASE_HOSTS => 'int', + 'r_' . AdminAcl::RESOURCE_SERVER_DATABASES => 'int', 'r_' . AdminAcl::RESOURCE_EGGS => 'int', 'r_' . AdminAcl::RESOURCE_LOCATIONS => 'int', 'r_' . AdminAcl::RESOURCE_NESTS => 'int', @@ -108,7 +109,8 @@ class ApiKey extends Model implements CleansAttributes, ValidableContract 'last_used_at' => 'nullable|date', 'r_' . AdminAcl::RESOURCE_USERS => 'integer|min:0|max:3', 'r_' . AdminAcl::RESOURCE_ALLOCATIONS => 'integer|min:0|max:3', - 'r_' . AdminAcl::RESOURCE_DATABASES => 'integer|min:0|max:3', + 'r_' . AdminAcl::RESOURCE_DATABASE_HOSTS => 'integer|min:0|max:3', + 'r_' . AdminAcl::RESOURCE_SERVER_DATABASES => 'integer|min:0|max:3', 'r_' . AdminAcl::RESOURCE_EGGS => 'integer|min:0|max:3', 'r_' . AdminAcl::RESOURCE_LOCATIONS => 'integer|min:0|max:3', 'r_' . AdminAcl::RESOURCE_NESTS => 'integer|min:0|max:3', diff --git a/app/Models/Checksum.php b/app/Models/Checksum.php deleted file mode 100644 index 231b07c7f..000000000 --- a/app/Models/Checksum.php +++ /dev/null @@ -1,38 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Models; - -use Illuminate\Database\Eloquent\Model; - -class Checksum extends Model -{ - /** - * The table associated with the model. - * - * @var string - */ - protected $table = 'checksums'; - - /** - * Fields that are not mass assignable. - * - * @var array - */ - protected $guarded = ['id', 'created_at', 'updated_at']; - - /** - * Cast values to correct type. - * - * @var array - */ - protected $casts = [ - 'service' => 'integer', - ]; -} diff --git a/app/Models/DaemonKey.php b/app/Models/DaemonKey.php index 59cab2354..c4c2940a9 100644 --- a/app/Models/DaemonKey.php +++ b/app/Models/DaemonKey.php @@ -1,26 +1,4 @@ . - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ namespace Pterodactyl\Models; diff --git a/app/Models/Database.php b/app/Models/Database.php index 17d79c753..9ff1d8c17 100644 --- a/app/Models/Database.php +++ b/app/Models/Database.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Models; @@ -19,6 +12,12 @@ class Database extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; + /** + * The resource name for this model when it is transformed into an + * API representation using fractal. + */ + const RESOURCE_NAME = 'server_database'; + /** * The table associated with the model. * diff --git a/app/Models/DatabaseHost.php b/app/Models/DatabaseHost.php index 2fb339add..f42f2650d 100644 --- a/app/Models/DatabaseHost.php +++ b/app/Models/DatabaseHost.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Models; @@ -19,6 +12,12 @@ class DatabaseHost extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; + /** + * The resource name for this model when it is transformed into an + * API representation using fractal. + */ + const RESOURCE_NAME = 'database_host'; + /** * The table associated with the model. * diff --git a/app/Models/Egg.php b/app/Models/Egg.php index 1c1b9e815..c9b2d9767 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Models; @@ -19,6 +12,12 @@ class Egg extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; + /** + * The resource name for this model when it is transformed into an + * API representation using fractal. + */ + const RESOURCE_NAME = 'egg'; + /** * The table associated with the model. * diff --git a/app/Models/EggVariable.php b/app/Models/EggVariable.php index 5341dd0dd..44b074bd1 100644 --- a/app/Models/EggVariable.php +++ b/app/Models/EggVariable.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Models; @@ -19,6 +12,12 @@ class EggVariable extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; + /** + * The resource name for this model when it is transformed into an + * API representation using fractal. + */ + const RESOURCE_NAME = 'egg_variable'; + /** * Reserved environment variable names. * diff --git a/app/Models/Location.php b/app/Models/Location.php index 30808c0dd..c680a54da 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -12,6 +12,12 @@ class Location extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; + /** + * The resource name for this model when it is transformed into an + * API representation using fractal. + */ + const RESOURCE_NAME = 'location'; + /** * The table associated with the model. * diff --git a/app/Models/Nest.php b/app/Models/Nest.php index 3631bc6e3..a6ab112e6 100644 --- a/app/Models/Nest.php +++ b/app/Models/Nest.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Models; @@ -19,6 +12,12 @@ class Nest extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; + /** + * The resource name for this model when it is transformed into an + * API representation using fractal. + */ + const RESOURCE_NAME = 'nest'; + /** * The table associated with the model. * diff --git a/app/Models/Node.php b/app/Models/Node.php index cc22a724e..ad8a9fe64 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Models; @@ -20,6 +13,12 @@ class Node extends Model implements CleansAttributes, ValidableContract { use Eloquence, Notifiable, Validable; + /** + * The resource name for this model when it is transformed into an + * API representation using fractal. + */ + const RESOURCE_NAME = 'node'; + const DAEMON_SECRET_LENGTH = 36; /** diff --git a/app/Models/Pack.php b/app/Models/Pack.php index 5d172c252..657d2f1d0 100644 --- a/app/Models/Pack.php +++ b/app/Models/Pack.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Models; @@ -19,6 +12,12 @@ class Pack extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; + /** + * The resource name for this model when it is transformed into an + * API representation using fractal. + */ + const RESOURCE_NAME = 'pack'; + /** * The table associated with the model. * diff --git a/app/Models/Permission.php b/app/Models/Permission.php index 61b67e487..6c2d94ab1 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Models; @@ -19,6 +12,12 @@ class Permission extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; + /** + * The resource name for this model when it is transformed into an + * API representation using fractal. + */ + const RESOURCE_NAME = 'subuser_permission'; + /** * Should timestamps be used on this model. * diff --git a/app/Models/Schedule.php b/app/Models/Schedule.php index 215bb6d9c..83971d797 100644 --- a/app/Models/Schedule.php +++ b/app/Models/Schedule.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Models; @@ -19,6 +12,12 @@ class Schedule extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; + /** + * The resource name for this model when it is transformed into an + * API representation using fractal. + */ + const RESOURCE_NAME = 'server_schedule'; + /** * The table associated with the model. * diff --git a/app/Models/Server.php b/app/Models/Server.php index d09291e56..0d029cc61 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Models; diff --git a/app/Models/ServerVariable.php b/app/Models/ServerVariable.php index a17683ed6..f706d5942 100644 --- a/app/Models/ServerVariable.php +++ b/app/Models/ServerVariable.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Models; @@ -13,6 +6,12 @@ use Illuminate\Database\Eloquent\Model; class ServerVariable extends Model { + /** + * The resource name for this model when it is transformed into an + * API representation using fractal. + */ + const RESOURCE_NAME = 'server_variable'; + /** * The table associated with the model. * diff --git a/app/Models/Subuser.php b/app/Models/Subuser.php index bcf5837fb..93c62217a 100644 --- a/app/Models/Subuser.php +++ b/app/Models/Subuser.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Models; @@ -20,6 +13,12 @@ class Subuser extends Model implements CleansAttributes, ValidableContract { use Eloquence, Notifiable, Validable; + /** + * The resource name for this model when it is transformed into an + * API representation using fractal. + */ + const RESOURCE_NAME = 'server_subuser'; + /** * The table associated with the model. * diff --git a/app/Models/Task.php b/app/Models/Task.php index 82323e075..28c8e3237 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Models; @@ -20,6 +13,12 @@ class Task extends Model implements CleansAttributes, ValidableContract { use BelongsToThrough, Eloquence, Validable; + /** + * The resource name for this model when it is transformed into an + * API representation using fractal. + */ + const RESOURCE_NAME = 'schedule_task'; + /** * The table associated with the model. * diff --git a/app/Models/User.php b/app/Models/User.php index 82cec4ddc..29754eff8 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -37,6 +37,12 @@ class User extends Model implements const FILTER_LEVEL_ADMIN = 2; const FILTER_LEVEL_SUBUSER = 3; + /** + * The resource name for this model when it is transformed into an + * API representation using fractal. + */ + const RESOURCE_NAME = 'user'; + /** * Level of servers to display when using access() on a user. * diff --git a/app/Services/Acl/Api/AdminAcl.php b/app/Services/Acl/Api/AdminAcl.php index 701264405..54bd594fd 100644 --- a/app/Services/Acl/Api/AdminAcl.php +++ b/app/Services/Acl/Api/AdminAcl.php @@ -32,7 +32,8 @@ class AdminAcl const RESOURCE_LOCATIONS = 'locations'; const RESOURCE_NESTS = 'nests'; const RESOURCE_EGGS = 'eggs'; - const RESOURCE_DATABASES = 'databases'; + const RESOURCE_DATABASE_HOSTS = 'database_hosts'; + const RESOURCE_SERVER_DATABASES = 'server_databases'; const RESOURCE_PACKS = 'packs'; /** diff --git a/app/Transformers/Api/Application/AllocationTransformer.php b/app/Transformers/Api/Application/AllocationTransformer.php index 10d08def6..cce6acc86 100644 --- a/app/Transformers/Api/Application/AllocationTransformer.php +++ b/app/Transformers/Api/Application/AllocationTransformer.php @@ -14,6 +14,16 @@ class AllocationTransformer extends BaseTransformer */ protected $availableIncludes = ['node', 'server']; + /** + * Return the resource name for the JSONAPI output. + * + * @return string + */ + public function getResourceName(): string + { + return Allocation::RESOURCE_NAME; + } + /** * Return a generic transformed allocation array. * diff --git a/app/Transformers/Api/Application/BaseTransformer.php b/app/Transformers/Api/Application/BaseTransformer.php index 8046df1e6..c2fdf6137 100644 --- a/app/Transformers/Api/Application/BaseTransformer.php +++ b/app/Transformers/Api/Application/BaseTransformer.php @@ -17,6 +17,13 @@ abstract class BaseTransformer extends TransformerAbstract */ private $key; + /** + * Return the resource name for the JSONAPI output. + * + * @return string + */ + abstract public function getResourceName(): string; + /** * BaseTransformer constructor. */ diff --git a/app/Transformers/Api/Application/DatabaseHostTransformer.php b/app/Transformers/Api/Application/DatabaseHostTransformer.php new file mode 100644 index 000000000..ef7d575bf --- /dev/null +++ b/app/Transformers/Api/Application/DatabaseHostTransformer.php @@ -0,0 +1,69 @@ + $model->id, + 'name' => $model->name, + 'host' => $model->host, + 'port' => $model->port, + 'username' => $model->username, + 'node' => $model->node_id, + 'created_at' => Chronos::createFromFormat(Chronos::DEFAULT_TO_STRING_FORMAT, $model->created_at) + ->setTimezone(config('app.timezone')) + ->toIso8601String(), + 'updated_at' => Chronos::createFromFormat(Chronos::DEFAULT_TO_STRING_FORMAT, $model->updated_at) + ->setTimezone(config('app.timezone')) + ->toIso8601String(), + ]; + } + + /** + * Include the databases associated with this host. + * + * @param \Pterodactyl\Models\DatabaseHost $model + * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + */ + public function includeDatabases(DatabaseHost $model) + { + if (! $this->authorize(AdminAcl::RESOURCE_SERVER_DATABASES)) { + return $this->null(); + } + + $model->loadMissing('databases'); + + return $this->collection($model->getRelation('databases'), $this->makeTransformer(ServerDatabaseTransformer::class), Database::RESOURCE_NAME); + } +} diff --git a/app/Transformers/Api/Application/EggVariableTransformer.php b/app/Transformers/Api/Application/EggVariableTransformer.php index 357f7de1b..decb038ab 100644 --- a/app/Transformers/Api/Application/EggVariableTransformer.php +++ b/app/Transformers/Api/Application/EggVariableTransformer.php @@ -2,10 +2,21 @@ namespace Pterodactyl\Transformers\Api\Application; +use Pterodactyl\Models\Egg; use Pterodactyl\Models\EggVariable; class EggVariableTransformer extends BaseTransformer { + /** + * Return the resource name for the JSONAPI output. + * + * @return string + */ + public function getResourceName(): string + { + return Egg::RESOURCE_NAME; + } + public function transform(EggVariable $model) { return $model->toArray(); diff --git a/app/Transformers/Api/Application/LocationTransformer.php b/app/Transformers/Api/Application/LocationTransformer.php index 0c29f1801..d003053d6 100644 --- a/app/Transformers/Api/Application/LocationTransformer.php +++ b/app/Transformers/Api/Application/LocationTransformer.php @@ -14,6 +14,16 @@ class LocationTransformer extends BaseTransformer */ protected $availableIncludes = ['nodes', 'servers']; + /** + * Return the resource name for the JSONAPI output. + * + * @return string + */ + public function getResourceName(): string + { + return Location::RESOURCE_NAME; + } + /** * Return a generic transformed pack array. * diff --git a/app/Transformers/Api/Application/NodeTransformer.php b/app/Transformers/Api/Application/NodeTransformer.php index 8a52a8687..d47183fc7 100644 --- a/app/Transformers/Api/Application/NodeTransformer.php +++ b/app/Transformers/Api/Application/NodeTransformer.php @@ -14,6 +14,16 @@ class NodeTransformer extends BaseTransformer */ protected $availableIncludes = ['allocations', 'location', 'servers']; + /** + * Return the resource name for the JSONAPI output. + * + * @return string + */ + public function getResourceName(): string + { + return Node::RESOURCE_NAME; + } + /** * Return a node transformed into a format that can be consumed by the * external administrative API. diff --git a/app/Transformers/Api/Application/ServerDatabaseTransformer.php b/app/Transformers/Api/Application/ServerDatabaseTransformer.php new file mode 100644 index 000000000..c374798eb --- /dev/null +++ b/app/Transformers/Api/Application/ServerDatabaseTransformer.php @@ -0,0 +1,104 @@ +encrypter = $encrypter; + } + + /** + * Return the resource name for the JSONAPI output. + * + * @return string + */ + public function getResourceName(): string + { + return Database::RESOURCE_NAME; + } + + /** + * Transform a database model in a representation for the application API. + * + * @param \Pterodactyl\Models\Database $model + * @return array + */ + public function transform(Database $model): array + { + return [ + 'id' => $model->id, + 'server' => $model->server_id, + 'host' => $model->database_host_id, + 'database' => $model->database, + 'username' => $model->username, + 'remote' => $model->remote, + 'created_at' => Chronos::createFromFormat(Chronos::DEFAULT_TO_STRING_FORMAT, $model->created_at) + ->setTimezone(config('app.timezone')) + ->toIso8601String(), + 'updated_at' => Chronos::createFromFormat(Chronos::DEFAULT_TO_STRING_FORMAT, $model->updated_at) + ->setTimezone(config('app.timezone')) + ->toIso8601String(), + ]; + } + + /** + * Include the database password in the request. + * + * @param \Pterodactyl\Models\Database $model + * @return \League\Fractal\Resource\Item + */ + public function includePassword(Database $model): Item + { + return $this->item($model, function (Database $model) { + return [ + 'id' => $model->id, + 'password' => $this->encrypter->decrypt($model->password), + ]; + }, 'database_password'); + } + + /** + * Return the database host relationship for this server database. + * + * @param \Pterodactyl\Models\Database $model + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + */ + public function includeHost(Database $model) + { + if (! $this->authorize(AdminAcl::RESOURCE_DATABASE_HOSTS)) { + return $this->null(); + } + + $model->loadMissing('host'); + + return $this->item( + $model->getRelation('host'), + $this->makeTransformer(DatabaseHostTransformer::class), + DatabaseHost::RESOURCE_NAME + ); + } +} diff --git a/app/Transformers/Api/Application/ServerTransformer.php b/app/Transformers/Api/Application/ServerTransformer.php index e3c8cc56e..914449ef2 100644 --- a/app/Transformers/Api/Application/ServerTransformer.php +++ b/app/Transformers/Api/Application/ServerTransformer.php @@ -41,6 +41,16 @@ class ServerTransformer extends BaseTransformer $this->environmentService = $environmentService; } + /** + * Return the resource name for the JSONAPI output. + * + * @return string + */ + public function getResourceName(): string + { + return Server::RESOURCE_NAME; + } + /** * Return a generic transformed server array. * diff --git a/app/Transformers/Api/Application/ServerVariableTransformer.php b/app/Transformers/Api/Application/ServerVariableTransformer.php index b965bc861..5ce592b5f 100644 --- a/app/Transformers/Api/Application/ServerVariableTransformer.php +++ b/app/Transformers/Api/Application/ServerVariableTransformer.php @@ -14,6 +14,16 @@ class ServerVariableTransformer extends BaseTransformer */ protected $availableIncludes = ['parent']; + /** + * Return the resource name for the JSONAPI output. + * + * @return string + */ + public function getResourceName(): string + { + return ServerVariable::RESOURCE_NAME; + } + /** * Return a generic transformed server variable array. * diff --git a/app/Transformers/Api/Application/UserTransformer.php b/app/Transformers/Api/Application/UserTransformer.php index b6aac29b9..d3d680ed7 100644 --- a/app/Transformers/Api/Application/UserTransformer.php +++ b/app/Transformers/Api/Application/UserTransformer.php @@ -14,6 +14,16 @@ class UserTransformer extends BaseTransformer */ protected $availableIncludes = ['servers']; + /** + * Return the resource name for the JSONAPI output. + * + * @return string + */ + public function getResourceName(): string + { + return User::RESOURCE_NAME; + } + /** * Return a generic transformed subuser array. * diff --git a/database/migrations/2018_01_11_213943_AddApiKeyPermissionColumns.php b/database/migrations/2018_01_11_213943_AddApiKeyPermissionColumns.php index 3948972c4..cd6b60e10 100644 --- a/database/migrations/2018_01_11_213943_AddApiKeyPermissionColumns.php +++ b/database/migrations/2018_01_11_213943_AddApiKeyPermissionColumns.php @@ -23,7 +23,8 @@ class AddApiKeyPermissionColumns extends Migration $table->unsignedTinyInteger('r_locations')->default(0); $table->unsignedTinyInteger('r_nests')->default(0); $table->unsignedTinyInteger('r_eggs')->default(0); - $table->unsignedTinyInteger('r_databases')->default(0); + $table->unsignedTinyInteger('r_database_hosts')->default(0); + $table->unsignedTinyInteger('r_server_databases')->default(0); $table->unsignedTinyInteger('r_packs')->default(0); }); } @@ -52,7 +53,8 @@ class AddApiKeyPermissionColumns extends Migration 'r_locations', 'r_nests', 'r_eggs', - 'r_databases', + 'r_database_hosts', + 'r_server_databases', 'r_packs', ]); }); diff --git a/routes/api-application.php b/routes/api-application.php index 72576b920..1b72ef2da 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -82,4 +82,15 @@ Route::group(['prefix' => '/servers'], function () { Route::delete('/{server}', 'Servers\ServerController@delete'); Route::delete('/{server}/{force?}', 'Servers\ServerController@delete'); + + // Database Management Endpoint + Route::group(['prefix' => '/{server}/databases'], function () { + Route::get('/', 'Servers\DatabaseController@index')->name('api.application.servers.databases'); + Route::get('/{database}', 'Servers\DatabaseController@view')->name('api.application.servers.databases.view'); + + Route::post('/', 'Servers\DatabaseController@store'); + Route::patch('/{database}', 'Servers\DatabaseController@update'); + + Route::delete('/{database}', 'Servers\DatabaseController@delete'); + }); }); From de07b3cc7fb5dde2c26a35d4ee6931bdf21526c0 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 25 Jan 2018 22:34:53 -0600 Subject: [PATCH 49/54] Add server database management support to API. --- app/Exceptions/DisplayException.php | 8 ++ .../Spatie/Fractalistic/Fractal.php | 2 +- .../Servers/DatabaseController.php | 104 +++++++++++++++++- .../Middleware/Api/ApiSubstituteBindings.php | 56 ++-------- .../Api/Application/ApplicationApiRequest.php | 14 ++- .../Databases/GetServerDatabaseRequest.php | 32 ++++++ .../Databases/GetServerDatabasesRequest.php | 19 ++++ .../Databases/ServerDatabaseWriteRequest.php | 13 +++ .../Databases/StoreServerDatabaseRequest.php | 61 ++++++++++ routes/api-application.php | 2 +- 10 files changed, 257 insertions(+), 54 deletions(-) create mode 100644 app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabaseRequest.php create mode 100644 app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabasesRequest.php create mode 100644 app/Http/Requests/Api/Application/Servers/Databases/ServerDatabaseWriteRequest.php create mode 100644 app/Http/Requests/Api/Application/Servers/Databases/StoreServerDatabaseRequest.php diff --git a/app/Exceptions/DisplayException.php b/app/Exceptions/DisplayException.php index e6488f89c..e3a839c5e 100644 --- a/app/Exceptions/DisplayException.php +++ b/app/Exceptions/DisplayException.php @@ -46,6 +46,14 @@ class DisplayException extends PterodactylException return $this->level; } + /** + * @return int + */ + public function getStatusCode() + { + return Response::HTTP_BAD_REQUEST; + } + /** * Render the exception to the user by adding a flashed message to the session * and then redirecting them back to the page that they came from. If the diff --git a/app/Extensions/Spatie/Fractalistic/Fractal.php b/app/Extensions/Spatie/Fractalistic/Fractal.php index 753cd4758..35a8b8f3b 100644 --- a/app/Extensions/Spatie/Fractalistic/Fractal.php +++ b/app/Extensions/Spatie/Fractalistic/Fractal.php @@ -3,8 +3,8 @@ namespace Pterodactyl\Extensions\Spatie\Fractalistic; use League\Fractal\TransformerAbstract; +use Spatie\Fractal\Fractal as SpatieFractal; use League\Fractal\Serializer\JsonApiSerializer; -use Spatie\Fractalistic\Fractal as SpatieFractal; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use League\Fractal\Pagination\IlluminatePaginatorAdapter; diff --git a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php index e1fec4943..41d637489 100644 --- a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php +++ b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php @@ -2,13 +2,32 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Servers; +use Illuminate\Http\Response; use Pterodactyl\Models\Server; +use Pterodactyl\Models\Database; +use Illuminate\Http\JsonResponse; +use Pterodactyl\Services\Databases\DatabasePasswordService; +use Pterodactyl\Services\Databases\DatabaseManagementService; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Transformers\Api\Application\ServerDatabaseTransformer; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; +use Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabaseRequest; +use Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabasesRequest; +use Pterodactyl\Http\Requests\Api\Application\Servers\Databases\ServerDatabaseWriteRequest; +use Pterodactyl\Http\Requests\Api\Application\Servers\Databases\StoreServerDatabaseRequest; class DatabaseController extends ApplicationApiController { + /** + * @var \Pterodactyl\Services\Databases\DatabaseManagementService + */ + private $databaseManagementService; + + /** + * @var \Pterodactyl\Services\Databases\DatabasePasswordService + */ + private $databasePasswordService; + /** * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface */ @@ -17,12 +36,19 @@ class DatabaseController extends ApplicationApiController /** * DatabaseController constructor. * + * @param \Pterodactyl\Services\Databases\DatabaseManagementService $databaseManagementService + * @param \Pterodactyl\Services\Databases\DatabasePasswordService $databasePasswordService * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository */ - public function __construct(DatabaseRepositoryInterface $repository) - { + public function __construct( + DatabaseManagementService $databaseManagementService, + DatabasePasswordService $databasePasswordService, + DatabaseRepositoryInterface $repository + ) { parent::__construct(); + $this->databaseManagementService = $databaseManagementService; + $this->databasePasswordService = $databasePasswordService; $this->repository = $repository; } @@ -30,10 +56,11 @@ class DatabaseController extends ApplicationApiController * Return a listing of all databases currently available to a single * server. * - * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabasesRequest $request + * @param \Pterodactyl\Models\Server $server * @return array */ - public function index(Server $server): array + public function index(GetServerDatabasesRequest $request, Server $server): array { $databases = $this->repository->getDatabasesForServer($server->id); @@ -41,4 +68,73 @@ class DatabaseController extends ApplicationApiController ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) ->toArray(); } + + /** + * Return a single server database. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabaseRequest $request + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Database $database + * @return array + */ + public function view(GetServerDatabaseRequest $request, Server $server, Database $database): array + { + return $this->fractal->item($database) + ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) + ->toArray(); + } + + /** + * Reset the password for a specific server database. + * + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Database $database + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function resetPassword(ServerDatabaseWriteRequest $request, Server $server, Database $database): Response + { + $this->databasePasswordService->handle($database, str_random(24)); + + return response('', 204); + } + + /** + * Create a new database on the Panel for a given server. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\StoreServerDatabaseRequest $request + * @param \Pterodactyl\Models\Server $server + * @return \Illuminate\Http\JsonResponse + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function store(StoreServerDatabaseRequest $request, Server $server): JsonResponse + { + $database = $this->databaseManagementService->create($server->id, $request->validated()); + + return $this->fractal->item($database) + ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) + ->respond(201); + } + + /** + * Delete a specific database from the Panel. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\ServerDatabaseWriteRequest $request + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Database $database + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function delete(ServerDatabaseWriteRequest $request, Server $server, Database $database): Response + { + $this->databaseManagementService->delete($database->id); + + return response('', 204); + } } diff --git a/app/Http/Middleware/Api/ApiSubstituteBindings.php b/app/Http/Middleware/Api/ApiSubstituteBindings.php index 430d4597b..f12cc91a1 100644 --- a/app/Http/Middleware/Api/ApiSubstituteBindings.php +++ b/app/Http/Middleware/Api/ApiSubstituteBindings.php @@ -3,11 +3,8 @@ namespace Pterodactyl\Http\Middleware\Api; use Closure; -use ReflectionMethod; -use Illuminate\Container\Container; -use Illuminate\Routing\ImplicitRouteBinding; -use Illuminate\Contracts\Routing\UrlRoutable; use Illuminate\Routing\Middleware\SubstituteBindings; +use Illuminate\Database\Eloquent\ModelNotFoundException; class ApiSubstituteBindings extends SubstituteBindings { @@ -24,47 +21,18 @@ class ApiSubstituteBindings extends SubstituteBindings $route = $request->route(); $this->router->substituteBindings($route); - $this->resolveForRoute($route); + + // Attempt to resolve bindings for this route. If one of the models + // cannot be resolved do not immediately return a 404 error. Set a request + // attribute that can be checked in the base API request class to only + // trigger a 404 after validating that the API key making the request is valid + // and even has permission to access the requested resource. + try { + $this->router->substituteImplicitBindings($route); + } catch (ModelNotFoundException $exception) { + $request->attributes->set('is_missing_model', true); + } return $next($request); } - - /** - * Resolve the implicit route bindings for the given route. This function - * overrides Laravel's default inn \Illuminate\Routing\ImplictRouteBinding - * to not throw a 404 error when a model is not found. - * - * If a model is not found using the provided information, the binding is - * replaced with null which is then checked in the form requests on API - * routes. This avoids a potential imformation leak on API routes for - * unauthenticated users. - * - * @param \Illuminate\Routing\Route $route - */ - protected function resolveForRoute($route) - { - $parameters = $route->parameters(); - - // Avoid having to copy and paste the entirety of that class into this middleware - // by using reflection to access a protected method. - $reflection = new ReflectionMethod(ImplicitRouteBinding::class, 'getParameterName'); - $reflection->setAccessible(true); - - foreach ($route->signatureParameters(UrlRoutable::class) as $parameter) { - if (! $parameterName = $reflection->invokeArgs(null, [$parameter->name, $parameters])) { - continue; - } - - $parameterValue = $parameters[$parameterName]; - - if ($parameterValue instanceof UrlRoutable) { - continue; - } - - // Try to find an existing model, if one is not found simply bind the - // parameter as null. - $instance = Container::getInstance()->make($parameter->getClass()->name); - $route->setParameter($parameterName, $instance->resolveRouteBinding($parameterValue)); - } - } } diff --git a/app/Http/Requests/Api/Application/ApplicationApiRequest.php b/app/Http/Requests/Api/Application/ApplicationApiRequest.php index 2cf7c8478..3d110d52c 100644 --- a/app/Http/Requests/Api/Application/ApplicationApiRequest.php +++ b/app/Http/Requests/Api/Application/ApplicationApiRequest.php @@ -73,7 +73,7 @@ abstract class ApplicationApiRequest extends FormRequest return $this->attributes->get('api_key'); } - /** + /* * Determine if the request passes the authorization check as well * as the exists check. * @@ -81,18 +81,24 @@ abstract class ApplicationApiRequest extends FormRequest * * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException */ + + /** + * @return bool + */ protected function passesAuthorization() { - $passes = parent::passesAuthorization(); + if (! parent::passesAuthorization()) { + return false; + } // Only let the user know that a resource does not exist if they are // authenticated to access the endpoint. This avoids exposing that // an item exists (or does not exist) to the user until they can prove // that they have permission to know about it. - if ($passes && ! $this->resourceExists()) { + if ($this->attributes->get('is_missing_model', false) || ! $this->resourceExists()) { throw new NotFoundHttpException('The requested resource does not exist on this server.'); } - return $passes; + return true; } } diff --git a/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabaseRequest.php b/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabaseRequest.php new file mode 100644 index 000000000..e398a5bbf --- /dev/null +++ b/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabaseRequest.php @@ -0,0 +1,32 @@ +route()->parameter('server'); + $database = $this->route()->parameter('database'); + + return $database->server_id === $server->id; + } +} diff --git a/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabasesRequest.php b/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabasesRequest.php new file mode 100644 index 000000000..3e6cfc6fe --- /dev/null +++ b/app/Http/Requests/Api/Application/Servers/Databases/GetServerDatabasesRequest.php @@ -0,0 +1,19 @@ + 'required|string|min:1|max:24', + 'remote' => 'required|string|min:1', + 'host' => 'required|integer|exists:database_hosts,id', + ]; + } + + /** + * Return data formatted in the correct format for the service to consume. + * + * @return array + */ + public function validated() + { + return [ + 'database' => $this->input('database'), + 'remote' => $this->input('remote'), + 'database_host_id' => $this->input('host'), + ]; + } + + /** + * Format error messages in a more understandable format for API output. + * + * @return array + */ + public function attributes() + { + return [ + 'host' => 'Database Host Server ID', + 'remote' => 'Remote Connection String', + 'database' => 'Database Name', + ]; + } +} diff --git a/routes/api-application.php b/routes/api-application.php index 1b72ef2da..85566a68e 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -89,7 +89,7 @@ Route::group(['prefix' => '/servers'], function () { Route::get('/{database}', 'Servers\DatabaseController@view')->name('api.application.servers.databases.view'); Route::post('/', 'Servers\DatabaseController@store'); - Route::patch('/{database}', 'Servers\DatabaseController@update'); + Route::post('/{database}/reset-password', 'Servers\DatabaseController@resetPassword'); Route::delete('/{database}', 'Servers\DatabaseController@delete'); }); From 8afced34108395317b83b3ac1b6f5189b28375bb Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 27 Jan 2018 12:38:56 -0600 Subject: [PATCH 50/54] Add nests & eggs Cleanup middleware handling and parameters on controllers... --- .../Serializers/PterodactylSerializer.php | 72 +++++++++ .../Spatie/Fractalistic/Fractal.php | 4 +- .../Locations/LocationController.php | 49 +++--- .../Api/Application/Nests/EggController.php | 61 +++++++ .../Api/Application/Nests/NestController.php | 57 +++++++ .../Nodes/AllocationController.php | 62 +++++--- .../Api/Application/Nodes/NodeController.php | 52 +++--- .../Servers/DatabaseController.php | 36 ++--- .../Application/Servers/ServerController.php | 5 +- .../Servers/ServerDetailsController.php | 18 ++- .../Servers/ServerManagementController.php | 27 ++-- .../Api/Application/Users/UserController.php | 49 ++---- .../Middleware/Api/ApiSubstituteBindings.php | 36 +++++ ...pplication => DeleteAllocationRequest.php} | 0 .../Allocations/StoreAllocationRequest.php | 46 ++++++ .../Api/Application/ApplicationApiRequest.php | 21 +++ .../Application/Nests/Eggs/GetEggRequest.php | 29 ++++ .../Application/Nests/Eggs/GetEggsRequest.php | 19 +++ .../Api/Application/Nests/GetNestsRequest.php | 19 +++ .../Api/Application/EggTransformer.php | 150 ++++++++++++++++++ .../Api/Application/NestTransformer.php | 63 ++++++++ .../Api/Application/OptionTransformer.php | 116 -------------- .../Application/ServerDatabaseTransformer.php | 1 - .../Api/Application/ServiceTransformer.php | 101 ------------ .../ServiceVariableTransformer.php | 69 -------- routes/api-application.php | 21 +++ 26 files changed, 737 insertions(+), 446 deletions(-) create mode 100644 app/Extensions/League/Fractal/Serializers/PterodactylSerializer.php create mode 100644 app/Http/Controllers/Api/Application/Nests/EggController.php create mode 100644 app/Http/Controllers/Api/Application/Nests/NestController.php rename app/Http/Requests/Api/Application/Allocations/{DeleteAllocationRequest.phpApplication => DeleteAllocationRequest.php} (100%) create mode 100644 app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php create mode 100644 app/Http/Requests/Api/Application/Nests/Eggs/GetEggRequest.php create mode 100644 app/Http/Requests/Api/Application/Nests/Eggs/GetEggsRequest.php create mode 100644 app/Http/Requests/Api/Application/Nests/GetNestsRequest.php create mode 100644 app/Transformers/Api/Application/EggTransformer.php create mode 100644 app/Transformers/Api/Application/NestTransformer.php delete mode 100644 app/Transformers/Api/Application/OptionTransformer.php delete mode 100644 app/Transformers/Api/Application/ServiceTransformer.php delete mode 100644 app/Transformers/Api/Application/ServiceVariableTransformer.php diff --git a/app/Extensions/League/Fractal/Serializers/PterodactylSerializer.php b/app/Extensions/League/Fractal/Serializers/PterodactylSerializer.php new file mode 100644 index 000000000..9599ca2c9 --- /dev/null +++ b/app/Extensions/League/Fractal/Serializers/PterodactylSerializer.php @@ -0,0 +1,72 @@ + $resourceKey, + 'attributes' => $data, + ]; + } + + /** + * Serialize a collection. + * + * @param string $resourceKey + * @param array $data + * @return array + */ + public function collection($resourceKey, array $data) + { + $response = []; + foreach ($data as $datum) { + $response[] = $this->item($resourceKey, $datum); + } + + return [ + 'object' => 'list', + 'data' => $response, + ]; + } + + /** + * Serialize a null resource. + * + * @return array + */ + public function null() + { + return [ + 'object' => 'null_resource', + 'attributes' => null, + ]; + } + + /** + * Merge the included resources with the parent resource being serialized. + * + * @param array $transformedData + * @param array $includedData + * @return array + */ + public function mergeIncludes($transformedData, $includedData) + { + foreach ($includedData as $key => $datum) { + $transformedData['relationships'][$key] = $datum; + } + + return $transformedData; + } +} diff --git a/app/Extensions/Spatie/Fractalistic/Fractal.php b/app/Extensions/Spatie/Fractalistic/Fractal.php index 35a8b8f3b..5bb0dd52b 100644 --- a/app/Extensions/Spatie/Fractalistic/Fractal.php +++ b/app/Extensions/Spatie/Fractalistic/Fractal.php @@ -4,9 +4,9 @@ namespace Pterodactyl\Extensions\Spatie\Fractalistic; use League\Fractal\TransformerAbstract; use Spatie\Fractal\Fractal as SpatieFractal; -use League\Fractal\Serializer\JsonApiSerializer; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use League\Fractal\Pagination\IlluminatePaginatorAdapter; +use Pterodactyl\Extensions\League\Fractal\Serializers\PterodactylSerializer; class Fractal extends SpatieFractal { @@ -22,7 +22,7 @@ class Fractal extends SpatieFractal { // Set the serializer by default. if (is_null($this->serializer)) { - $this->serializer = new JsonApiSerializer; + $this->serializer = new PterodactylSerializer; } // Automatically set the paginator on the response object if the diff --git a/app/Http/Controllers/Api/Application/Locations/LocationController.php b/app/Http/Controllers/Api/Application/Locations/LocationController.php index 5af4c0782..42bb2a08c 100644 --- a/app/Http/Controllers/Api/Application/Locations/LocationController.php +++ b/app/Http/Controllers/Api/Application/Locations/LocationController.php @@ -2,22 +2,20 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Locations; -use Spatie\Fractal\Fractal; use Illuminate\Http\Response; use Pterodactyl\Models\Location; use Illuminate\Http\JsonResponse; -use Pterodactyl\Http\Controllers\Controller; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; use Pterodactyl\Services\Locations\LocationUpdateService; use Pterodactyl\Services\Locations\LocationCreationService; use Pterodactyl\Services\Locations\LocationDeletionService; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Transformers\Api\Application\LocationTransformer; +use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; use Pterodactyl\Http\Requests\Api\Application\Locations\GetLocationsRequest; use Pterodactyl\Http\Requests\Api\Application\Locations\DeleteLocationRequest; use Pterodactyl\Http\Requests\Api\Application\Locations\UpdateLocationRequest; -class LocationController extends Controller +class LocationController extends ApplicationApiController { /** * @var \Pterodactyl\Services\Locations\LocationCreationService @@ -29,11 +27,6 @@ class LocationController extends Controller */ private $deletionService; - /** - * @var \Spatie\Fractal\Fractal - */ - private $fractal; - /** * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface */ @@ -47,22 +40,21 @@ class LocationController extends Controller /** * LocationController constructor. * - * @param \Spatie\Fractal\Fractal $fractal * @param \Pterodactyl\Services\Locations\LocationCreationService $creationService * @param \Pterodactyl\Services\Locations\LocationDeletionService $deletionService * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $repository * @param \Pterodactyl\Services\Locations\LocationUpdateService $updateService */ public function __construct( - Fractal $fractal, LocationCreationService $creationService, LocationDeletionService $deletionService, LocationRepositoryInterface $repository, LocationUpdateService $updateService ) { + parent::__construct(); + $this->creationService = $creationService; $this->deletionService = $deletionService; - $this->fractal = $fractal; $this->repository = $repository; $this->updateService = $updateService; } @@ -78,9 +70,7 @@ class LocationController extends Controller $locations = $this->repository->paginated(100); return $this->fractal->collection($locations) - ->transformWith((new LocationTransformer)->setKey($request->key())) - ->withResourceName('location') - ->paginateWith(new IlluminatePaginatorAdapter($locations)) + ->transformWith($this->getTransformer(LocationTransformer::class)) ->toArray(); } @@ -88,14 +78,12 @@ class LocationController extends Controller * Return a single location. * * @param \Pterodactyl\Http\Controllers\Api\Application\Locations\GetLocationRequest $request - * @param \Pterodactyl\Models\Location $location * @return array */ - public function view(GetLocationRequest $request, Location $location): array + public function view(GetLocationRequest $request): array { - return $this->fractal->item($location) - ->transformWith((new LocationTransformer)->setKey($request->key())) - ->withResourceName('location') + return $this->fractal->item($request->getModel(Location::class)) + ->transformWith($this->getTransformer(LocationTransformer::class)) ->toArray(); } @@ -113,8 +101,12 @@ class LocationController extends Controller $location = $this->creationService->handle($request->validated()); return $this->fractal->item($location) - ->transformWith((new LocationTransformer)->setKey($request->key())) - ->withResourceName('location') + ->transformWith($this->getTransformer(LocationTransformer::class)) + ->addMeta([ + 'resource' => route('api.application.locations.view', [ + 'location' => $location->id, + ]), + ]) ->respond(201); } @@ -122,19 +114,17 @@ class LocationController extends Controller * Update a location on the Panel and return the updated record to the user. * * @param \Pterodactyl\Http\Requests\Api\Application\Locations\UpdateLocationRequest $request - * @param \Pterodactyl\Models\Location $location * @return array * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function update(UpdateLocationRequest $request, Location $location): array + public function update(UpdateLocationRequest $request): array { - $location = $this->updateService->handle($location, $request->validated()); + $location = $this->updateService->handle($request->getModel(Location::class), $request->validated()); return $this->fractal->item($location) - ->transformWith((new LocationTransformer)->setKey($request->key())) - ->withResourceName('location') + ->transformWith($this->getTransformer(LocationTransformer::class)) ->toArray(); } @@ -142,14 +132,13 @@ class LocationController extends Controller * Delete a location from the Panel. * * @param \Pterodactyl\Http\Requests\Api\Application\Locations\DeleteLocationRequest $request - * @param \Pterodactyl\Models\Location $location * @return \Illuminate\Http\Response * * @throws \Pterodactyl\Exceptions\Service\Location\HasActiveNodesException */ - public function delete(DeleteLocationRequest $request, Location $location): Response + public function delete(DeleteLocationRequest $request): Response { - $this->deletionService->handle($location); + $this->deletionService->handle($request->getModel(Location::class)); return response('', 204); } diff --git a/app/Http/Controllers/Api/Application/Nests/EggController.php b/app/Http/Controllers/Api/Application/Nests/EggController.php new file mode 100644 index 000000000..21ce4ec9f --- /dev/null +++ b/app/Http/Controllers/Api/Application/Nests/EggController.php @@ -0,0 +1,61 @@ +repository = $repository; + } + + /** + * Return all eggs that exist for a given nest. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Nests\Eggs\GetEggsRequest $request + * @return array + */ + public function index(GetEggsRequest $request): array + { + $eggs = $this->repository->findWhere([ + ['nest_id', '=', $request->getModel(Nest::class)->id], + ]); + + return $this->fractal->collection($eggs) + ->transformWith($this->getTransformer(EggTransformer::class)) + ->toArray(); + } + + /** + * Return a single egg that exists on the specified nest. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Nests\Eggs\GetEggRequest $request + * @return array + */ + public function view(GetEggRequest $request): array + { + return $this->fractal->item($request->getModel(Egg::class)) + ->transformWith($this->getTransformer(EggTransformer::class)) + ->toArray(); + } +} diff --git a/app/Http/Controllers/Api/Application/Nests/NestController.php b/app/Http/Controllers/Api/Application/Nests/NestController.php new file mode 100644 index 000000000..adeacc56c --- /dev/null +++ b/app/Http/Controllers/Api/Application/Nests/NestController.php @@ -0,0 +1,57 @@ +repository = $repository; + } + + /** + * Return all Nests that exist on the Panel. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Nests\GetNestsRequest $request + * @return array + */ + public function index(GetNestsRequest $request): array + { + $nests = $this->repository->paginated(50); + + return $this->fractal->collection($nests) + ->transformWith($this->getTransformer(NestTransformer::class)) + ->toArray(); + } + + /** + * Return information about a single Nest model. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Nests\GetNestsRequest $request + * @return array + */ + public function view(GetNestsRequest $request): array + { + return $this->fractal->item($request->getModel(Nest::class)) + ->transformWith($this->getTransformer(NestTransformer::class)) + ->toArray(); + } +} diff --git a/app/Http/Controllers/Api/Application/Nodes/AllocationController.php b/app/Http/Controllers/Api/Application/Nodes/AllocationController.php index 969d4b55d..ef35e91c5 100644 --- a/app/Http/Controllers/Api/Application/Nodes/AllocationController.php +++ b/app/Http/Controllers/Api/Application/Nodes/AllocationController.php @@ -2,20 +2,25 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Nodes; -use Spatie\Fractal\Fractal; use Pterodactyl\Models\Node; use Illuminate\Http\Response; use Pterodactyl\Models\Allocation; -use Pterodactyl\Http\Controllers\Controller; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; +use Pterodactyl\Services\Allocations\AssignmentService; use Pterodactyl\Services\Allocations\AllocationDeletionService; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Transformers\Api\Application\AllocationTransformer; +use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; use Pterodactyl\Http\Requests\Api\Application\Allocations\GetAllocationsRequest; -use Pterodactyl\Http\Requests\Api\Application\Allocations\DeleteAllocationRequestApplication; +use Pterodactyl\Http\Requests\Api\Application\Allocations\StoreAllocationRequest; +use Pterodactyl\Http\Requests\Api\Application\Allocations\DeleteAllocationRequest; -class AllocationController extends Controller +class AllocationController extends ApplicationApiController { + /** + * @var \Pterodactyl\Services\Allocations\AssignmentService + */ + private $assignmentService; + /** * @var \Pterodactyl\Services\Allocations\AllocationDeletionService */ @@ -34,14 +39,19 @@ class AllocationController extends Controller /** * AllocationController constructor. * + * @param \Pterodactyl\Services\Allocations\AssignmentService $assignmentService * @param \Pterodactyl\Services\Allocations\AllocationDeletionService $deletionService * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $repository - * @param \Spatie\Fractal\Fractal $fractal */ - public function __construct(AllocationDeletionService $deletionService, AllocationRepositoryInterface $repository, Fractal $fractal) - { + public function __construct( + AssignmentService $assignmentService, + AllocationDeletionService $deletionService, + AllocationRepositoryInterface $repository + ) { + parent::__construct(); + + $this->assignmentService = $assignmentService; $this->deletionService = $deletionService; - $this->fractal = $fractal; $this->repository = $repository; } @@ -49,33 +59,45 @@ class AllocationController extends Controller * Return all of the allocations that exist for a given node. * * @param \Pterodactyl\Http\Requests\Api\Application\Allocations\GetAllocationsRequest $request - * @param \Pterodactyl\Models\Node $node * @return array */ - public function index(GetAllocationsRequest $request, Node $node): array + public function index(GetAllocationsRequest $request): array { - $allocations = $this->repository->getPaginatedAllocationsForNode($node->id, 100); + $allocations = $this->repository->getPaginatedAllocationsForNode( + $request->getModel(Node::class)->id, 50 + ); return $this->fractal->collection($allocations) - ->transformWith((new AllocationTransformer)->setKey($request->key())) - ->withResourceName('allocation') - ->paginateWith(new IlluminatePaginatorAdapter($allocations)) + ->transformWith($this->getTransformer(AllocationTransformer::class)) ->toArray(); } + /** + * Store new allocations for a given node. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Allocations\StoreAllocationRequest $request + * @return array + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function store(StoreAllocationRequest $request): array + { + $this->assignmentService->handle($request->getModel(Node::class), $request->validated()); + + return response('', 204); + } + /** * Delete a specific allocation from the Panel. * - * @param \Pterodactyl\Http\Requests\Api\Application\Allocations\DeleteAllocationRequestApplication $request - * @param \Pterodactyl\Models\Node $node - * @param \Pterodactyl\Models\Allocation $allocation + * @param \Pterodactyl\Http\Requests\Api\Application\Allocations\DeleteAllocationRequest $request * @return \Illuminate\Http\Response * * @throws \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException */ - public function delete(DeleteAllocationRequestApplication $request, Node $node, Allocation $allocation): Response + public function delete(DeleteAllocationRequest $request): Response { - $this->deletionService->handle($allocation); + $this->deletionService->handle($request->getModel(Allocation::class)); return response('', 204); } diff --git a/app/Http/Controllers/Api/Application/Nodes/NodeController.php b/app/Http/Controllers/Api/Application/Nodes/NodeController.php index cf115db96..2e1c76a5c 100644 --- a/app/Http/Controllers/Api/Application/Nodes/NodeController.php +++ b/app/Http/Controllers/Api/Application/Nodes/NodeController.php @@ -2,15 +2,12 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Nodes; -use Spatie\Fractal\Fractal; use Pterodactyl\Models\Node; use Illuminate\Http\Response; use Illuminate\Http\JsonResponse; -use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Nodes\NodeUpdateService; use Pterodactyl\Services\Nodes\NodeCreationService; use Pterodactyl\Services\Nodes\NodeDeletionService; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Transformers\Api\Application\NodeTransformer; use Pterodactyl\Http\Requests\Api\Application\Nodes\GetNodeRequest; @@ -18,8 +15,9 @@ use Pterodactyl\Http\Requests\Api\Application\Nodes\GetNodesRequest; use Pterodactyl\Http\Requests\Api\Application\Nodes\StoreNodeRequest; use Pterodactyl\Http\Requests\Api\Application\Nodes\DeleteNodeRequest; use Pterodactyl\Http\Requests\Api\Application\Nodes\UpdateNodeRequest; +use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; -class NodeController extends Controller +class NodeController extends ApplicationApiController { /** * @var \Pterodactyl\Services\Nodes\NodeCreationService @@ -31,11 +29,6 @@ class NodeController extends Controller */ private $deletionService; - /** - * @var \Spatie\Fractal\Fractal - */ - private $fractal; - /** * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface */ @@ -49,20 +42,19 @@ class NodeController extends Controller /** * NodeController constructor. * - * @param \Spatie\Fractal\Fractal $fractal * @param \Pterodactyl\Services\Nodes\NodeCreationService $creationService * @param \Pterodactyl\Services\Nodes\NodeDeletionService $deletionService * @param \Pterodactyl\Services\Nodes\NodeUpdateService $updateService * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository */ public function __construct( - Fractal $fractal, NodeCreationService $creationService, NodeDeletionService $deletionService, NodeUpdateService $updateService, NodeRepositoryInterface $repository ) { - $this->fractal = $fractal; + parent::__construct(); + $this->repository = $repository; $this->creationService = $creationService; $this->deletionService = $deletionService; @@ -77,12 +69,10 @@ class NodeController extends Controller */ public function index(GetNodesRequest $request): array { - $nodes = $this->repository->paginated(100); + $nodes = $this->repository->paginated(50); return $this->fractal->collection($nodes) - ->transformWith((new NodeTransformer)->setKey($request->key())) - ->withResourceName('node') - ->paginateWith(new IlluminatePaginatorAdapter($nodes)) + ->transformWith($this->getTransformer(NodeTransformer::class)) ->toArray(); } @@ -90,14 +80,12 @@ class NodeController extends Controller * Return data for a single instance of a node. * * @param \Pterodactyl\Http\Requests\Api\Application\Nodes\GetNodeRequest $request - * @param \Pterodactyl\Models\Node $node * @return array */ - public function view(GetNodeRequest $request, Node $node): array + public function view(GetNodeRequest $request): array { - return $this->fractal->item($node) - ->transformWith((new NodeTransformer)->setKey($request->key())) - ->withResourceName('node') + return $this->fractal->item($request->getModel(Node::class)) + ->transformWith($this->getTransformer(NodeTransformer::class)) ->toArray(); } @@ -115,10 +103,11 @@ class NodeController extends Controller $node = $this->creationService->handle($request->validated()); return $this->fractal->item($node) - ->transformWith((new NodeTransformer)->setKey($request->key())) - ->withResourceName('node') + ->transformWith($this->getTransformer(NodeTransformer::class)) ->addMeta([ - 'link' => route('api.admin.node.view', ['node' => $node->id]), + 'resource' => route('api.application.nodes.view', [ + 'node' => $node->id, + ]), ]) ->respond(201); } @@ -127,20 +116,20 @@ class NodeController extends Controller * Update an existing node on the Panel. * * @param \Pterodactyl\Http\Requests\Api\Application\Nodes\UpdateNodeRequest $request - * @param \Pterodactyl\Models\Node $node * @return array * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function update(UpdateNodeRequest $request, Node $node): array + public function update(UpdateNodeRequest $request): array { - $node = $this->updateService->returnUpdatedModel()->handle($node, $request->validated()); + $node = $this->updateService->returnUpdatedModel()->handle( + $request->getModel(Node::class), $request->validated() + ); return $this->fractal->item($node) - ->transformWith((new NodeTransformer)->setKey($request->key())) - ->withResourceName('node') + ->transformWith($this->getTransformer(NodeTransformer::class)) ->toArray(); } @@ -149,14 +138,13 @@ class NodeController extends Controller * currently attached to it. * * @param \Pterodactyl\Http\Requests\Api\Application\Nodes\DeleteNodeRequest $request - * @param \Pterodactyl\Models\Node $node * @return \Illuminate\Http\Response * * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException */ - public function delete(DeleteNodeRequest $request, Node $node): Response + public function delete(DeleteNodeRequest $request): Response { - $this->deletionService->handle($node); + $this->deletionService->handle($request->getModel(Node::class)); return response('', 204); } diff --git a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php index 41d637489..05512a4ee 100644 --- a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php +++ b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php @@ -57,12 +57,11 @@ class DatabaseController extends ApplicationApiController * server. * * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabasesRequest $request - * @param \Pterodactyl\Models\Server $server * @return array */ - public function index(GetServerDatabasesRequest $request, Server $server): array + public function index(GetServerDatabasesRequest $request): array { - $databases = $this->repository->getDatabasesForServer($server->id); + $databases = $this->repository->getDatabasesForServer($request->getModel(Server::class)->id); return $this->fractal->collection($databases) ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) @@ -73,13 +72,11 @@ class DatabaseController extends ApplicationApiController * Return a single server database. * * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabaseRequest $request - * @param \Pterodactyl\Models\Server $server - * @param \Pterodactyl\Models\Database $database * @return array */ - public function view(GetServerDatabaseRequest $request, Server $server, Database $database): array + public function view(GetServerDatabaseRequest $request): array { - return $this->fractal->item($database) + return $this->fractal->item($request->getModel(Database::class)) ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) ->toArray(); } @@ -87,16 +84,15 @@ class DatabaseController extends ApplicationApiController /** * Reset the password for a specific server database. * - * @param \Pterodactyl\Models\Server $server - * @param \Pterodactyl\Models\Database $database + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\ServerDatabaseWriteRequest $request * @return \Illuminate\Http\Response * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function resetPassword(ServerDatabaseWriteRequest $request, Server $server, Database $database): Response + public function resetPassword(ServerDatabaseWriteRequest $request): Response { - $this->databasePasswordService->handle($database, str_random(24)); + $this->databasePasswordService->handle($request->getModel(Database::class), str_random(24)); return response('', 204); } @@ -105,35 +101,39 @@ class DatabaseController extends ApplicationApiController * Create a new database on the Panel for a given server. * * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\StoreServerDatabaseRequest $request - * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\JsonResponse * * @throws \Exception * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function store(StoreServerDatabaseRequest $request, Server $server): JsonResponse + public function store(StoreServerDatabaseRequest $request): JsonResponse { + $server = $request->getModel(Server::class); $database = $this->databaseManagementService->create($server->id, $request->validated()); return $this->fractal->item($database) ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) + ->addMeta([ + 'resource' => route('api.application.servers.databases.view', [ + 'server' => $server->id, + 'database' => $database->id, + ]), + ]) ->respond(201); } /** - * Delete a specific database from the Panel. + * Handle a request to delete a specific server database from the Panel. * * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\ServerDatabaseWriteRequest $request - * @param \Pterodactyl\Models\Server $server - * @param \Pterodactyl\Models\Database $database * @return \Illuminate\Http\Response * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function delete(ServerDatabaseWriteRequest $request, Server $server, Database $database): Response + public function delete(ServerDatabaseWriteRequest $request): Response { - $this->databaseManagementService->delete($database->id); + $this->databaseManagementService->delete($request->getModel(Database::class)->id); return response('', 204); } diff --git a/app/Http/Controllers/Api/Application/Servers/ServerController.php b/app/Http/Controllers/Api/Application/Servers/ServerController.php index 98d90f284..9d9a7474f 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerController.php @@ -56,12 +56,11 @@ class ServerController extends ApplicationApiController * Show a single server transformed for the application API. * * @param \Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest $request - * @param \Pterodactyl\Models\Server $server * @return array */ - public function view(ServerWriteRequest $request, Server $server): array + public function view(ServerWriteRequest $request): array { - return $this->fractal->item($server) + return $this->fractal->item($request->getModel(Server::class)) ->transformWith($this->getTransformer(ServerTransformer::class)) ->toArray(); } diff --git a/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php b/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php index 1a9cb889e..e544c138a 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php @@ -28,8 +28,10 @@ class ServerDetailsController extends ApplicationApiController * @param \Pterodactyl\Services\Servers\BuildModificationService $buildModificationService * @param \Pterodactyl\Services\Servers\DetailsModificationService $detailsModificationService */ - public function __construct(BuildModificationService $buildModificationService, DetailsModificationService $detailsModificationService) - { + public function __construct( + BuildModificationService $buildModificationService, + DetailsModificationService $detailsModificationService + ) { parent::__construct(); $this->buildModificationService = $buildModificationService; @@ -40,16 +42,17 @@ class ServerDetailsController extends ApplicationApiController * Update the details for a specific server. * * @param \Pterodactyl\Http\Requests\Api\Application\Servers\UpdateServerDetailsRequest $request - * @param \Pterodactyl\Models\Server $server * @return array * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function details(UpdateServerDetailsRequest $request, Server $server): array + public function details(UpdateServerDetailsRequest $request): array { - $server = $this->detailsModificationService->returnUpdatedModel()->handle($server, $request->validated()); + $server = $this->detailsModificationService->returnUpdatedModel()->handle( + $request->getModel(Server::class), $request->validated() + ); return $this->fractal->item($server) ->transformWith($this->getTransformer(ServerTransformer::class)) @@ -60,16 +63,15 @@ class ServerDetailsController extends ApplicationApiController * Update the build details for a specific server. * * @param \Pterodactyl\Http\Requests\Api\Application\Servers\UpdateServerBuildConfigurationRequest $request - * @param \Pterodactyl\Models\Server $server * @return array * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function build(UpdateServerBuildConfigurationRequest $request, Server $server): array + public function build(UpdateServerBuildConfigurationRequest $request): array { - $server = $this->buildModificationService->handle($server, $request->validated()); + $server = $this->buildModificationService->handle($request->getModel(Server::class), $request->validated()); return $this->fractal->item($server) ->transformWith($this->getTransformer(ServerTransformer::class)) diff --git a/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php b/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php index 4379616fa..9ab324d7e 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php @@ -34,8 +34,11 @@ class ServerManagementController extends ApplicationApiController * @param \Pterodactyl\Services\Servers\ReinstallServerService $reinstallServerService * @param \Pterodactyl\Services\Servers\SuspensionService $suspensionService */ - public function __construct(ContainerRebuildService $rebuildService, ReinstallServerService $reinstallServerService, SuspensionService $suspensionService) - { + public function __construct( + ContainerRebuildService $rebuildService, + ReinstallServerService $reinstallServerService, + SuspensionService $suspensionService + ) { parent::__construct(); $this->rebuildService = $rebuildService; @@ -47,16 +50,15 @@ class ServerManagementController extends ApplicationApiController * Suspend a server on the Panel. * * @param \Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest $request - * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\Response * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function suspend(ServerWriteRequest $request, Server $server): Response + public function suspend(ServerWriteRequest $request): Response { - $this->suspensionService->toggle($server, SuspensionService::ACTION_SUSPEND); + $this->suspensionService->toggle($request->getModel(Server::class), SuspensionService::ACTION_SUSPEND); return $this->returnNoContent(); } @@ -65,16 +67,15 @@ class ServerManagementController extends ApplicationApiController * Unsuspend a server on the Panel. * * @param \Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest $request - * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\Response * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function unsuspend(ServerWriteRequest $request, Server $server): Response + public function unsuspend(ServerWriteRequest $request): Response { - $this->suspensionService->toggle($server, SuspensionService::ACTION_UNSUSPEND); + $this->suspensionService->toggle($request->getModel(Server::class), SuspensionService::ACTION_UNSUSPEND); return $this->returnNoContent(); } @@ -83,16 +84,15 @@ class ServerManagementController extends ApplicationApiController * Mark a server as needing to be reinstalled. * * @param \Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest $request - * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\Response * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function reinstall(ServerWriteRequest $request, Server $server): Response + public function reinstall(ServerWriteRequest $request): Response { - $this->reinstallServerService->reinstall($server); + $this->reinstallServerService->reinstall($request->getModel(Server::class)); return $this->returnNoContent(); } @@ -101,14 +101,13 @@ class ServerManagementController extends ApplicationApiController * Mark a server as needing its container rebuilt the next time it is started. * * @param \Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest $request - * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\Response * * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException */ - public function rebuild(ServerWriteRequest $request, Server $server): Response + public function rebuild(ServerWriteRequest $request): Response { - $this->rebuildService->handle($server); + $this->rebuildService->handle($request->getModel(Server::class)); return $this->returnNoContent(); } diff --git a/app/Http/Controllers/Api/Application/Users/UserController.php b/app/Http/Controllers/Api/Application/Users/UserController.php index 35c534536..96d8c986f 100644 --- a/app/Http/Controllers/Api/Application/Users/UserController.php +++ b/app/Http/Controllers/Api/Application/Users/UserController.php @@ -2,16 +2,12 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Users; -use Spatie\Fractal\Fractal; -use Illuminate\Http\Request; use Pterodactyl\Models\User; use Illuminate\Http\Response; use Illuminate\Http\JsonResponse; -use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Users\UserUpdateService; use Pterodactyl\Services\Users\UserCreationService; use Pterodactyl\Services\Users\UserDeletionService; -use League\Fractal\Pagination\IlluminatePaginatorAdapter; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Transformers\Api\Application\UserTransformer; use Pterodactyl\Http\Requests\Api\Application\Users\GetUserRequest; @@ -19,8 +15,9 @@ use Pterodactyl\Http\Requests\Api\Application\Users\GetUsersRequest; use Pterodactyl\Http\Requests\Api\Application\Users\StoreUserRequest; use Pterodactyl\Http\Requests\Api\Application\Users\DeleteUserRequest; use Pterodactyl\Http\Requests\Api\Application\Users\UpdateUserRequest; +use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; -class UserController extends Controller +class UserController extends ApplicationApiController { /** * @var \Pterodactyl\Services\Users\UserCreationService @@ -32,11 +29,6 @@ class UserController extends Controller */ private $deletionService; - /** - * @var \Spatie\Fractal\Fractal - */ - private $fractal; - /** * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface */ @@ -50,22 +42,21 @@ class UserController extends Controller /** * UserController constructor. * - * @param \Spatie\Fractal\Fractal $fractal * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository * @param \Pterodactyl\Services\Users\UserCreationService $creationService * @param \Pterodactyl\Services\Users\UserDeletionService $deletionService * @param \Pterodactyl\Services\Users\UserUpdateService $updateService */ public function __construct( - Fractal $fractal, UserRepositoryInterface $repository, UserCreationService $creationService, UserDeletionService $deletionService, UserUpdateService $updateService ) { + parent::__construct(); + $this->creationService = $creationService; $this->deletionService = $deletionService; - $this->fractal = $fractal; $this->repository = $repository; $this->updateService = $updateService; } @@ -83,9 +74,7 @@ class UserController extends Controller $users = $this->repository->paginated(100); return $this->fractal->collection($users) - ->transformWith((new UserTransformer)->setKey($request->key())) - ->withResourceName('user') - ->paginateWith(new IlluminatePaginatorAdapter($users)) + ->transformWith($this->getTransformer(UserTransformer::class)) ->toArray(); } @@ -94,14 +83,12 @@ class UserController extends Controller * were defined in the request. * * @param \Pterodactyl\Http\Requests\Api\Application\Users\GetUserRequest $request - * @param \Pterodactyl\Models\User $user * @return array */ - public function view(GetUserRequest $request, User $user): array + public function view(GetUserRequest $request): array { - return $this->fractal->item($user) - ->transformWith((new UserTransformer)->setKey($request->key())) - ->withResourceName('user') + return $this->fractal->item($request->getModel(User::class)) + ->transformWith($this->getTransformer(UserTransformer::class)) ->toArray(); } @@ -114,16 +101,15 @@ class UserController extends Controller * meta. If there are no errors this is an empty array. * * @param \Pterodactyl\Http\Requests\Api\Application\Users\UpdateUserRequest $request - * @param \Pterodactyl\Models\User $user * @return array * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function update(UpdateUserRequest $request, User $user): array + public function update(UpdateUserRequest $request): array { $this->updateService->setUserLevel(User::USER_LEVEL_ADMIN); - $collection = $this->updateService->handle($user, $request->validated()); + $collection = $this->updateService->handle($request->getModel(User::class), $request->validated()); $errors = []; if (! empty($collection->get('exceptions'))) { @@ -140,8 +126,7 @@ class UserController extends Controller } $response = $this->fractal->item($collection->get('model')) - ->transformWith((new UserTransformer)->setKey($request->key())) - ->withResourceName('user'); + ->transformWith($this->getTransformer(UserTransformer::class)); if (count($errors) > 0) { $response->addMeta([ @@ -167,10 +152,11 @@ class UserController extends Controller $user = $this->creationService->handle($request->validated()); return $this->fractal->item($user) - ->transformWith((new UserTransformer)->setKey($request->key())) - ->withResourceName('user') + ->transformWith($this->getTransformer(UserTransformer::class)) ->addMeta([ - 'link' => route('api.admin.user.view', ['user' => $user->id]), + 'resource' => route('api.application.users.view', [ + 'user' => $user->id, + ]), ]) ->respond(201); } @@ -180,14 +166,13 @@ class UserController extends Controller * on successful deletion. * * @param \Pterodactyl\Http\Requests\Api\Application\Users\DeleteUserRequest $request - * @param \Pterodactyl\Models\User $user * @return \Illuminate\Http\Response * * @throws \Pterodactyl\Exceptions\DisplayException */ - public function delete(DeleteUserRequest $request, User $user): Response + public function delete(DeleteUserRequest $request): Response { - $this->deletionService->handle($user); + $this->deletionService->handle($request->getModel(User::class)); return response('', 204); } diff --git a/app/Http/Middleware/Api/ApiSubstituteBindings.php b/app/Http/Middleware/Api/ApiSubstituteBindings.php index f12cc91a1..41ca8ee70 100644 --- a/app/Http/Middleware/Api/ApiSubstituteBindings.php +++ b/app/Http/Middleware/Api/ApiSubstituteBindings.php @@ -3,11 +3,33 @@ namespace Pterodactyl\Http\Middleware\Api; use Closure; +use Pterodactyl\Models\Egg; +use Pterodactyl\Models\Nest; +use Pterodactyl\Models\Node; +use Pterodactyl\Models\Server; +use Pterodactyl\Models\Database; +use Pterodactyl\Models\Location; +use Pterodactyl\Models\Allocation; use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Database\Eloquent\ModelNotFoundException; class ApiSubstituteBindings extends SubstituteBindings { + /** + * Mappings to automatically assign route parameters to a model. + * + * @var array + */ + protected static $mappings = [ + 'allocation' => Allocation::class, + 'database' => Database::class, + 'egg' => Egg::class, + 'location' => Location::class, + 'nest' => Nest::class, + 'node' => Node::class, + 'server' => Server::class, + ]; + /** * Perform substitution of route parameters without triggering * a 404 error if a model is not found. @@ -20,6 +42,10 @@ class ApiSubstituteBindings extends SubstituteBindings { $route = $request->route(); + foreach (self::$mappings as $key => $model) { + $this->router->model($key, $model); + } + $this->router->substituteBindings($route); // Attempt to resolve bindings for this route. If one of the models @@ -35,4 +61,14 @@ class ApiSubstituteBindings extends SubstituteBindings return $next($request); } + + /** + * Return the registered mappings. + * + * @return array + */ + public static function getMappings() + { + return self::$mappings; + } } diff --git a/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.phpApplication b/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php similarity index 100% rename from app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.phpApplication rename to app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php diff --git a/app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php b/app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php new file mode 100644 index 000000000..2cf2ae004 --- /dev/null +++ b/app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php @@ -0,0 +1,46 @@ + 'required|string', + 'alias' => 'sometimes|nullable|string|max:255', + 'ports' => 'required|array', + 'ports.*' => 'string', + ]; + } + + /** + * @return array + */ + public function validated() + { + $data = parent::validated(); + + return [ + 'allocation_ip' => $data['ip'], + 'allocation_ports' => $data['ports'], + 'allocation_alias' => $data['alias'], + ]; + } +} diff --git a/app/Http/Requests/Api/Application/ApplicationApiRequest.php b/app/Http/Requests/Api/Application/ApplicationApiRequest.php index 3d110d52c..aeb03dfbe 100644 --- a/app/Http/Requests/Api/Application/ApplicationApiRequest.php +++ b/app/Http/Requests/Api/Application/ApplicationApiRequest.php @@ -3,9 +3,11 @@ 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; +use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; abstract class ApplicationApiRequest extends FormRequest @@ -73,6 +75,25 @@ abstract class ApplicationApiRequest extends FormRequest return $this->attributes->get('api_key'); } + /** + * Grab a model from the route parameters. If no model exists under + * the specified key a default response is returned. + * + * @param string $model + * @param mixed $default + * @return mixed + */ + public function getModel(string $model, $default = null) + { + $parameterKey = array_get(array_flip(ApiSubstituteBindings::getMappings()), $model); + + if (! is_null($parameterKey)) { + $model = $this->route()->parameter($parameterKey); + } + + return $model ?? $default; + } + /* * Determine if the request passes the authorization check as well * as the exists check. diff --git a/app/Http/Requests/Api/Application/Nests/Eggs/GetEggRequest.php b/app/Http/Requests/Api/Application/Nests/Eggs/GetEggRequest.php new file mode 100644 index 000000000..b3f1a08a0 --- /dev/null +++ b/app/Http/Requests/Api/Application/Nests/Eggs/GetEggRequest.php @@ -0,0 +1,29 @@ +getModel('nest')->id === $this->getModel('egg')->nest_id; + } +} diff --git a/app/Http/Requests/Api/Application/Nests/Eggs/GetEggsRequest.php b/app/Http/Requests/Api/Application/Nests/Eggs/GetEggsRequest.php new file mode 100644 index 000000000..a6aadf904 --- /dev/null +++ b/app/Http/Requests/Api/Application/Nests/Eggs/GetEggsRequest.php @@ -0,0 +1,19 @@ + $model->id, + 'uuid' => $model->uuid, + 'nest' => $model->nest_id, + 'author' => $model->author, + 'description' => $model->description, + 'docker_image' => $model->docker_image, + 'config' => [ + 'files' => json_decode($model->config_files), + 'startup' => json_decode($model->config_startup), + 'stop' => $model->config_stop, + 'logs' => json_decode($model->config_logs), + 'extends' => $model->config_from, + ], + 'startup' => $model->startup, + 'script' => [ + 'privileged' => $model->script_is_privileged, + 'install' => $model->script_install, + 'entry' => $model->script_entry, + 'container' => $model->script_container, + 'extends' => $model->copy_script_from, + ], + $model->getCreatedAtColumn() => $this->formatTimestamp($model->created_at), + $model->getUpdatedAtColumn() => $this->formatTimestamp($model->updated_at), + ]; + } + + /** + * Include the Nest relationship for the given Egg in the transformation. + * + * @param \Pterodactyl\Models\Egg $model + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + */ + public function includeNest(Egg $model) + { + if (! $this->authorize(AdminAcl::RESOURCE_NESTS)) { + return $this->null(); + } + + $model->loadMissing('nest'); + + return $this->item($model->getRelation('nest'), $this->makeTransformer(NestTransformer::class), Nest::RESOURCE_NAME); + } + + /** + * Include the Servers relationship for the given Egg in the transformation. + * + * @param \Pterodactyl\Models\Egg $model + * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + */ + public function includeServers(Egg $model) + { + if (! $this->authorize(AdminAcl::RESOURCE_SERVERS)) { + return $this->null(); + } + + $model->loadMissing('servers'); + + return $this->collection($model->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), Server::RESOURCE_NAME); + } + + /** + * Include more detailed information about the configuration if this Egg is + * extending another. + * + * @param \Pterodactyl\Models\Egg $model + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + */ + public function includeConfig(Egg $model) + { + if (is_null($model->config_from)) { + return $this->null(); + } + + $model->loadMissing('configFrom'); + + return $this->item($model, function (Egg $model) { + return [ + 'files' => json_decode($model->inherit_config_files), + 'startup' => json_decode($model->inherit_config_startup), + 'stop' => $model->inherit_config_stop, + 'logs' => json_decode($model->inherit_config_logs), + ]; + }); + } + + /** + * Include more detailed information about the script configuration if the + * Egg is extending another. + * + * @param \Pterodactyl\Models\Egg $model + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + */ + public function includeScript(Egg $model) + { + if (is_null($model->copy_script_from)) { + return $this->null(); + } + + $model->loadMissing('scriptFrom'); + + return $this->item($model, function (Egg $model) { + return [ + 'privileged' => $model->script_is_privileged, + 'install' => $model->copy_script_install, + 'entry' => $model->copy_script_entry, + 'container' => $model->copy_script_container, + ]; + }); + } +} diff --git a/app/Transformers/Api/Application/NestTransformer.php b/app/Transformers/Api/Application/NestTransformer.php new file mode 100644 index 000000000..9517af61d --- /dev/null +++ b/app/Transformers/Api/Application/NestTransformer.php @@ -0,0 +1,63 @@ +toArray(); + + $response[$model->getUpdatedAtColumn()] = $this->formatTimestamp($model->updated_at); + $response[$model->getCreatedAtColumn()] = $this->formatTimestamp($model->created_at); + + return $response; + } + + /** + * Include the Eggs relationship on the given Nest model transformation. + * + * @param \Pterodactyl\Models\Nest $model + * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + */ + public function includeEggs(Nest $model) + { + if (! $this->authorize(AdminAcl::RESOURCE_EGGS)) { + return $this->null(); + } + + $model->loadMissing('eggs'); + + return $this->collection($model->getRelation('eggs'), $this->makeTransformer(EggTransformer::class), Egg::RESOURCE_NAME); + } +} diff --git a/app/Transformers/Api/Application/OptionTransformer.php b/app/Transformers/Api/Application/OptionTransformer.php deleted file mode 100644 index 089f880ec..000000000 --- a/app/Transformers/Api/Application/OptionTransformer.php +++ /dev/null @@ -1,116 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\Admin; - -use Pterodactyl\Models\Egg; -use Illuminate\Http\Request; -use League\Fractal\TransformerAbstract; - -class OptionTransformer extends TransformerAbstract -{ - /** - * List of resources that can be included. - * - * @var array - */ - protected $availableIncludes = [ - 'service', - 'packs', - 'servers', - 'variables', - ]; - - /** - * 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 service option array. - * - * @return array - */ - public function transform(Egg $option) - { - return $option->toArray(); - } - - /** - * Return the parent service for this service option. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeService(Egg $option) - { - if ($this->request && ! $this->request->apiKeyHasPermission('service-view')) { - return; - } - - return $this->item($option->service, new ServiceTransformer($this->request), 'service'); - } - - /** - * Return the packs associated with this service option. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includePacks(Egg $option) - { - if ($this->request && ! $this->request->apiKeyHasPermission('pack-list')) { - return; - } - - return $this->collection($option->packs, new PackTransformer($this->request), 'pack'); - } - - /** - * Return the servers associated with this service option. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeServers(Egg $option) - { - if ($this->request && ! $this->request->apiKeyHasPermission('server-list')) { - return; - } - - return $this->collection($option->servers, new ServerTransformer($this->request), 'server'); - } - - /** - * Return the variables for this service option. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeVariables(Egg $option) - { - if ($this->request && ! $this->request->apiKeyHasPermission('option-view')) { - return; - } - - return $this->collection($option->variables, new ServiceVariableTransformer($this->request), 'variable'); - } -} diff --git a/app/Transformers/Api/Application/ServerDatabaseTransformer.php b/app/Transformers/Api/Application/ServerDatabaseTransformer.php index c374798eb..c71627296 100644 --- a/app/Transformers/Api/Application/ServerDatabaseTransformer.php +++ b/app/Transformers/Api/Application/ServerDatabaseTransformer.php @@ -75,7 +75,6 @@ class ServerDatabaseTransformer extends BaseTransformer { return $this->item($model, function (Database $model) { return [ - 'id' => $model->id, 'password' => $this->encrypter->decrypt($model->password), ]; }, 'database_password'); diff --git a/app/Transformers/Api/Application/ServiceTransformer.php b/app/Transformers/Api/Application/ServiceTransformer.php deleted file mode 100644 index 5f8497cf7..000000000 --- a/app/Transformers/Api/Application/ServiceTransformer.php +++ /dev/null @@ -1,101 +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\Nest; -use League\Fractal\TransformerAbstract; - -class ServiceTransformer extends TransformerAbstract -{ - /** - * List of resources that can be included. - * - * @var array - */ - protected $availableIncludes = [ - 'options', - 'servers', - 'packs', - ]; - - /** - * 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 service array. - * - * @return array - */ - public function transform(Nest $service) - { - return $service->toArray(); - } - - /** - * Return the the service options. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeOptions(Nest $service) - { - if ($this->request && ! $this->request->apiKeyHasPermission('option-list')) { - return; - } - - return $this->collection($service->options, new OptionTransformer($this->request), 'option'); - } - - /** - * Return the servers associated with this service. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeServers(Nest $service) - { - if ($this->request && ! $this->request->apiKeyHasPermission('server-list')) { - return; - } - - return $this->collection($service->servers, new ServerTransformer($this->request), 'server'); - } - - /** - * Return the packs associated with this service. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includePacks(Nest $service) - { - if ($this->request && ! $this->request->apiKeyHasPermission('pack-list')) { - return; - } - - return $this->collection($service->packs, new PackTransformer($this->request), 'pack'); - } -} diff --git a/app/Transformers/Api/Application/ServiceVariableTransformer.php b/app/Transformers/Api/Application/ServiceVariableTransformer.php deleted file mode 100644 index 49f5e449a..000000000 --- a/app/Transformers/Api/Application/ServiceVariableTransformer.php +++ /dev/null @@ -1,69 +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\EggVariable; -use League\Fractal\TransformerAbstract; - -class ServiceVariableTransformer extends TransformerAbstract -{ - /** - * List of resources that can be included. - * - * @var array - */ - protected $availableIncludes = ['variables']; - - /** - * 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 server variable array. - * - * @return array - */ - public function transform(EggVariable $variable) - { - return $variable->toArray(); - } - - /** - * Return the server variables associated with this variable. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeVariables(EggVariable $variable) - { - if ($this->request && ! $this->request->apiKeyHasPermission('server-view')) { - return; - } - - return $this->collection($variable->serverVariable, new ServerVariableTransformer($this->request), 'server_variable'); - } -} diff --git a/routes/api-application.php b/routes/api-application.php index 85566a68e..9649a4306 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -38,6 +38,8 @@ Route::group(['prefix' => '/nodes'], function () { Route::group(['prefix' => '/{node}/allocations'], function () { Route::get('/', 'Nodes\AllocationController@index')->name('api.application.allocations'); + Route::post('/', 'Nodes\AllocationController@store'); + Route::delete('/{allocation}', 'Nodes\AllocationController@delete')->name('api.application.allocations.view'); }); }); @@ -94,3 +96,22 @@ Route::group(['prefix' => '/servers'], function () { Route::delete('/{database}', 'Servers\DatabaseController@delete'); }); }); + +/* +|-------------------------------------------------------------------------- +| Nest Controller Routes +|-------------------------------------------------------------------------- +| +| Endpoint: /api/application/nests +| +*/ +Route::group(['prefix' => '/nests'], function () { + Route::get('/', 'Nests\NestController@index')->name('api.application.nests'); + Route::get('/{nest}', 'Nests\NestController@view')->name('api.application.nests.view'); + + // Egg Management Endpoint + Route::group(['prefix' => '/{nest}/eggs'], function () { + Route::get('/', 'Nests\EggController@index')->name('api.application.nests.eggs'); + Route::get('/{egg}', 'Nests\EggController@view')->name('api.application.nests.eggs.view'); + }); +}); From 97ee95b4da30225534853c4bc3ba59efbcb21476 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 27 Jan 2018 13:26:43 -0600 Subject: [PATCH 51/54] Fix some error handling --- app/Exceptions/Handler.php | 4 ++-- .../Api/Application/Nodes/AllocationController.php | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 952d94335..111d2a6b8 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -83,14 +83,14 @@ class Handler extends ExceptionHandler $cleaned[] = snake_case($reason); } - return [$field => $cleaned]; + return [str_replace('.', '_', $field) => $cleaned]; })->toArray(); $errors = collect($exception->errors())->map(function ($errors, $field) use ($codes) { $response = []; foreach ($errors as $key => $error) { $response[] = [ - 'code' => array_get($codes, $field . '.' . $key), + 'code' => array_get($codes, str_replace('.', '_', $field) . '.' . $key), 'detail' => $error, 'source' => ['field' => $field], ]; diff --git a/app/Http/Controllers/Api/Application/Nodes/AllocationController.php b/app/Http/Controllers/Api/Application/Nodes/AllocationController.php index ef35e91c5..b63f3e203 100644 --- a/app/Http/Controllers/Api/Application/Nodes/AllocationController.php +++ b/app/Http/Controllers/Api/Application/Nodes/AllocationController.php @@ -26,11 +26,6 @@ class AllocationController extends ApplicationApiController */ private $deletionService; - /** - * @var \Spatie\Fractal\Fractal - */ - private $fractal; - /** * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface */ From 5ed164e13ea57644b87114403ca2c3a5dfcfd275 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 28 Jan 2018 17:14:14 -0600 Subject: [PATCH 52/54] Implement server creation though the API. Also implements auto-deployment to specific locations and ports. --- .../AllocationRepositoryInterface.php | 22 +++ .../Repository/NodeRepositoryInterface.php | 12 ++ .../NoViableAllocationException.php | 9 + .../Deployment/NoViableNodeException.php | 9 + .../Controllers/Admin/ServersController.php | 7 +- .../Application/Servers/ServerController.php | 40 +++- .../Servers/StoreServerRequest.php | 148 +++++++++++++++ app/Models/Objects/DeploymentObject.php | 78 ++++++++ app/Models/Server.php | 7 +- .../Eloquent/AllocationRepository.php | 78 ++++++++ app/Repositories/Eloquent/NodeRepository.php | 25 +++ .../Allocations/AssignmentService.php | 2 +- .../Deployment/AllocationSelectionService.php | 123 ++++++++++++ .../Deployment/FindViableNodesService.php | 121 ++++++++++++ .../Servers/ServerCreationService.php | 179 +++++++++++++----- .../Servers/ServerDeletionService.php | 15 +- .../Servers/VariableValidatorService.php | 38 ++-- app/Traits/Services/HasUserLevels.php | 3 + resources/lang/en/exceptions.php | 4 + .../pterodactyl/admin/servers/new.blade.php | 8 +- routes/api-application.php | 1 + .../Servers/ServerCreationServiceTest.php | 54 +++--- .../Servers/ServerDeletionServiceTest.php | 156 ++++++--------- .../Servers/VariableValidatorServiceTest.php | 11 +- 24 files changed, 927 insertions(+), 223 deletions(-) create mode 100644 app/Exceptions/Service/Deployment/NoViableAllocationException.php create mode 100644 app/Exceptions/Service/Deployment/NoViableNodeException.php create mode 100644 app/Http/Requests/Api/Application/Servers/StoreServerRequest.php create mode 100644 app/Models/Objects/DeploymentObject.php create mode 100644 app/Services/Deployment/AllocationSelectionService.php create mode 100644 app/Services/Deployment/FindViableNodesService.php diff --git a/app/Contracts/Repository/AllocationRepositoryInterface.php b/app/Contracts/Repository/AllocationRepositoryInterface.php index 47dfe9475..ef69965b1 100644 --- a/app/Contracts/Repository/AllocationRepositoryInterface.php +++ b/app/Contracts/Repository/AllocationRepositoryInterface.php @@ -57,4 +57,26 @@ interface AllocationRepositoryInterface extends RepositoryInterface * @return array */ public function getAssignedAllocationIds(int $server): array; + + /** + * Return a concated result set of node ips that already have at least one + * server assigned to that IP. This allows for filtering out sets for + * dedicated allocation IPs. + * + * If an array of nodes is passed the results will be limited to allocations + * in those nodes. + * + * @param array $nodes + * @return array + */ + public function getDiscardableDedicatedAllocations(array $nodes = []): array; + + /** + * Return a single allocation from those meeting the requirements. + * + * @param array $nodes + * @param array $ports + * @param bool $dedicated + * @return \Pterodactyl\Models\Allocation|null + public function getRandomAllocation(array $nodes, array $ports, bool $dedicated = false); } diff --git a/app/Contracts/Repository/NodeRepositoryInterface.php b/app/Contracts/Repository/NodeRepositoryInterface.php index 49db33be8..0ebcbe3a0 100644 --- a/app/Contracts/Repository/NodeRepositoryInterface.php +++ b/app/Contracts/Repository/NodeRepositoryInterface.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Contracts\Repository; +use Generator; use Pterodactyl\Models\Node; use Illuminate\Support\Collection; use Illuminate\Contracts\Pagination\LengthAwarePaginator; @@ -62,4 +63,15 @@ interface NodeRepositoryInterface extends RepositoryInterface, SearchableInterfa * @return \Illuminate\Support\Collection */ public function getNodesForServerCreation(): Collection; + + /** + * Return the IDs of all nodes that exist in the provided locations and have the space + * available to support the additional disk and memory provided. + * + * @param array $locations + * @param int $disk + * @param int $memory + * @return \Generator + */ + public function getNodesWithResourceUse(array $locations, int $disk, int $memory): Generator; } diff --git a/app/Exceptions/Service/Deployment/NoViableAllocationException.php b/app/Exceptions/Service/Deployment/NoViableAllocationException.php new file mode 100644 index 000000000..5a8871c8c --- /dev/null +++ b/app/Exceptions/Service/Deployment/NoViableAllocationException.php @@ -0,0 +1,9 @@ +service->create($request->except('_token')); + $server = $this->service->handle($request->except('_token')); $this->alert->success(trans('admin/server.alerts.server_created'))->flash(); return redirect()->route('admin.servers.view', $server->id); diff --git a/app/Http/Controllers/Api/Application/Servers/ServerController.php b/app/Http/Controllers/Api/Application/Servers/ServerController.php index 9d9a7474f..fd6d62f89 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerController.php @@ -4,15 +4,23 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Servers; use Illuminate\Http\Response; use Pterodactyl\Models\Server; +use Illuminate\Http\JsonResponse; +use Pterodactyl\Services\Servers\ServerCreationService; use Pterodactyl\Services\Servers\ServerDeletionService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Transformers\Api\Application\ServerTransformer; use Pterodactyl\Http\Requests\Api\Application\Servers\GetServersRequest; use Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest; +use Pterodactyl\Http\Requests\Api\Application\Servers\StoreServerRequest; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; class ServerController extends ApplicationApiController { + /** + * @var \Pterodactyl\Services\Servers\ServerCreationService + */ + private $creationService; + /** * @var \Pterodactyl\Services\Servers\ServerDeletionService */ @@ -26,13 +34,18 @@ class ServerController extends ApplicationApiController /** * ServerController constructor. * + * @param \Pterodactyl\Services\Servers\ServerCreationService $creationService * @param \Pterodactyl\Services\Servers\ServerDeletionService $deletionService * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository */ - public function __construct(ServerDeletionService $deletionService, ServerRepositoryInterface $repository) - { + public function __construct( + ServerCreationService $creationService, + ServerDeletionService $deletionService, + ServerRepositoryInterface $repository + ) { parent::__construct(); + $this->creationService = $creationService; $this->deletionService = $deletionService; $this->repository = $repository; } @@ -52,6 +65,29 @@ class ServerController extends ApplicationApiController ->toArray(); } + /** + * Create a new server on the system. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\StoreServerRequest $request + * @return \Illuminate\Http\JsonResponse + * + * @throws \Illuminate\Validation\ValidationException + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException + * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException + */ + public function store(StoreServerRequest $request): JsonResponse + { + $server = $this->creationService->handle($request->validated(), $request->getDeploymentObject()); + + return $this->fractal->item($server) + ->transformWith($this->getTransformer(ServerTransformer::class)) + ->respond(201); + } + /** * Show a single server transformed for the application API. * diff --git a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php new file mode 100644 index 000000000..2c95ab52b --- /dev/null +++ b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php @@ -0,0 +1,148 @@ + $rules['name'], + 'description' => array_merge(['nullable'], $rules['description']), + 'user' => $rules['owner_id'], + 'egg' => $rules['egg_id'], + 'pack' => $rules['pack_id'], + 'docker_image' => $rules['image'], + 'startup' => $rules['startup'], + 'environment' => 'required|array', + 'skip_scripts' => 'sometimes|boolean', + + // Resource limitations + 'limits' => 'required|array', + 'limits.memory' => $rules['memory'], + 'limits.swap' => $rules['swap'], + 'limits.disk' => $rules['disk'], + 'limits.io' => $rules['io'], + 'limits.cpu' => $rules['cpu'], + + // Automatic deployment rules + 'deploy' => 'sometimes|required|array', + 'deploy.locations' => 'array', + 'deploy.locations.*' => 'integer|min:1', + 'deploy.dedicated_ip' => 'required_with:deploy,boolean', + 'deploy.port_range' => 'array', + 'deploy.port_range.*' => 'string', + + 'start_on_completion' => 'sometimes|boolean', + ]; + } + + /** + * Normalize the data into a format that can be consumed by the service. + * + * @return array + */ + public function validated() + { + $data = parent::validated(); + + return [ + 'name' => array_get($data, 'name'), + 'description' => array_get($data, 'description'), + 'owner_id' => array_get($data, 'user'), + 'egg_id' => array_get($data, 'egg'), + 'pack_id' => array_get($data, 'pack'), + 'image' => array_get($data, 'docker_image'), + 'startup' => array_get($data, 'startup'), + 'environment' => array_get($data, 'environment'), + 'memory' => array_get($data, 'limits.memory'), + 'swap' => array_get($data, 'limits.swap'), + 'disk' => array_get($data, 'limits.disk'), + 'io' => array_get($data, 'limits.io'), + 'cpu' => array_get($data, 'limits.cpu'), + 'skip_scripts' => array_get($data, 'skip_scripts', false), + 'allocation_id' => array_get($data, 'allocation.default'), + 'allocation_additional' => array_get($data, 'allocation.additional'), + 'start_on_completion' => array_get($data, 'start_on_completion', false), + ]; + } + + /* + * Run validation after the rules above have been applied. + * + * @param \Illuminate\Contracts\Validation\Validator $validator + */ + public function withValidator(Validator $validator) + { + $validator->sometimes('allocation.default', [ + 'required', 'integer', 'bail', + Rule::exists('allocations', 'id')->where(function ($query) { + $query->where('node_id', $this->input('node_id')); + $query->whereNull('server_id'); + }), + ], function ($input) { + return ! ($input->deploy); + }); + + $validator->sometimes('allocation.additional.*', [ + 'integer', + Rule::exists('allocations', 'id')->where(function ($query) { + $query->where('node_id', $this->input('node_id')); + $query->whereNull('server_id'); + }), + ], function ($input) { + return ! ($input->deploy); + }); + + $validator->sometimes('deploy.locations', 'present', function ($input) { + return $input->deploy; + }); + + $validator->sometimes('deploy.port_range', 'present', function ($input) { + return $input->deploy; + }); + } + + /** + * Return a deployment object that can be passed to the server creation service. + * + * @return \Pterodactyl\Models\Objects\DeploymentObject|null + */ + public function getDeploymentObject() + { + if (is_null($this->input('deploy'))) { + return null; + } + + $object = new DeploymentObject; + $object->setDedicated($this->input('deploy.dedicated_ip', false)); + $object->setLocations($this->input('deploy.locations', [])); + $object->setPorts($this->input('deploy.port_range', [])); + + return $object; + } +} diff --git a/app/Models/Objects/DeploymentObject.php b/app/Models/Objects/DeploymentObject.php new file mode 100644 index 000000000..52857410f --- /dev/null +++ b/app/Models/Objects/DeploymentObject.php @@ -0,0 +1,78 @@ +dedicated; + } + + /** + * @param bool $dedicated + * @return $this + */ + public function setDedicated(bool $dedicated) + { + $this->dedicated = $dedicated; + + return $this; + } + + /** + * @return array + */ + public function getLocations(): array + { + return $this->locations; + } + + /** + * @param array $locations + * @return $this + */ + public function setLocations(array $locations) + { + $this->locations = $locations; + + return $this; + } + + /** + * @return array + */ + public function getPorts(): array + { + return $this->ports; + } + + /** + * @param array $ports + * @return $this + */ + public function setPorts(array $ports) + { + $this->ports = $ports; + + return $this; + } +} diff --git a/app/Models/Server.php b/app/Models/Server.php index 0d029cc61..ac3bd64f3 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -66,13 +66,15 @@ class Server extends Model implements CleansAttributes, ValidableContract 'allocation_id' => 'required', 'pack_id' => 'sometimes', 'skip_scripts' => 'sometimes', + 'image' => 'required', + 'startup' => 'required', ]; /** * @var array */ protected static $dataIntegrityRules = [ - 'owner_id' => 'exists:users,id', + 'owner_id' => 'integer|exists:users,id', 'name' => 'string|min:1|max:255', 'node_id' => 'exists:nodes,id', 'description' => 'string', @@ -85,8 +87,9 @@ class Server extends Model implements CleansAttributes, ValidableContract 'nest_id' => 'exists:nests,id', 'egg_id' => 'exists:eggs,id', 'pack_id' => 'nullable|numeric|min:0', - 'startup' => 'nullable|string', + 'startup' => 'string', 'skip_scripts' => 'boolean', + 'image' => 'string|max:255', ]; /** diff --git a/app/Repositories/Eloquent/AllocationRepository.php b/app/Repositories/Eloquent/AllocationRepository.php index 82fe860d8..a47134e4c 100644 --- a/app/Repositories/Eloquent/AllocationRepository.php +++ b/app/Repositories/Eloquent/AllocationRepository.php @@ -4,6 +4,7 @@ namespace Pterodactyl\Repositories\Eloquent; use Illuminate\Support\Collection; use Pterodactyl\Models\Allocation; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; @@ -94,4 +95,81 @@ class AllocationRepository extends EloquentRepository implements AllocationRepos return $results->pluck('id')->toArray(); } + + /** + * Return a concated result set of node ips that already have at least one + * server assigned to that IP. This allows for filtering out sets for + * dedicated allocation IPs. + * + * If an array of nodes is passed the results will be limited to allocations + * in those nodes. + * + * @param array $nodes + * @return array + */ + public function getDiscardableDedicatedAllocations(array $nodes = []): array + { + $instance = $this->getBuilder()->select( + $this->getBuilder()->raw('CONCAT_WS("-", node_id, ip) as result') + ); + + if (! empty($nodes)) { + $instance->whereIn('node_id', $nodes); + } + + $results = $instance->whereNotNull('server_id') + ->groupBy($this->getBuilder()->raw('CONCAT(node_id, ip)')) + ->get(); + + return $results->pluck('result')->toArray(); + } + + /** + * Return a single allocation from those meeting the requirements. + * + * @param array $nodes + * @param array $ports + * @param bool $dedicated + * @return \Pterodactyl\Models\Allocation|null + */ + public function getRandomAllocation(array $nodes, array $ports, bool $dedicated = false) + { + $instance = $this->getBuilder()->whereNull('server_id'); + + if (! empty($nodes)) { + $instance->whereIn('node_id', $nodes); + } + + if (! empty($ports)) { + $instance->where(function (Builder $query) use ($ports) { + $whereIn = []; + foreach ($ports as $port) { + if (is_array($port)) { + $query->orWhereBetween('port', $port); + continue; + } + + $whereIn[] = $port; + } + + if (! empty($whereIn)) { + $query->orWhereIn('port', $whereIn); + } + }); + } + + // If this allocation should not be shared with any other servers get + // the data and modify the query as necessary, + if ($dedicated) { + $discard = $this->getDiscardableDedicatedAllocations($nodes); + + if (! empty($discard)) { + $instance->whereNotIn( + $this->getBuilder()->raw('CONCAT_WS("-", node_id, ip)'), $discard + ); + } + } + + return $instance->inRandomOrder()->first(); + } } diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index 61d93927e..8e0b44ca7 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Repositories\Eloquent; +use Generator; use Pterodactyl\Models\Node; use Illuminate\Support\Collection; use Pterodactyl\Repositories\Concerns\Searchable; @@ -157,4 +158,28 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa ]; })->values(); } + + /** + * Return the IDs of all nodes that exist in the provided locations and have the space + * available to support the additional disk and memory provided. + * + * @param array $locations + * @param int $disk + * @param int $memory + * @return \Generator + */ + public function getNodesWithResourceUse(array $locations, int $disk, int $memory): Generator + { + $instance = $this->getBuilder() + ->select(['nodes.id', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate']) + ->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk') + ->join('servers', 'servers.node_id', '=', 'nodes.id') + ->where('nodes.public', 1); + + if (! empty($locations)) { + $instance->whereIn('nodes.location_id', $locations); + } + + return $instance->cursor(); + } } diff --git a/app/Services/Allocations/AssignmentService.php b/app/Services/Allocations/AssignmentService.php index 3a9cc776b..10d58ef40 100644 --- a/app/Services/Allocations/AssignmentService.php +++ b/app/Services/Allocations/AssignmentService.php @@ -70,7 +70,7 @@ class AssignmentService $this->connection->beginTransaction(); foreach (Network::parse(gethostbyname($data['allocation_ip'])) as $ip) { foreach ($data['allocation_ports'] as $port) { - if (! ctype_digit($port) && ! preg_match(self::PORT_RANGE_REGEX, $port)) { + if (! is_digit($port) && ! preg_match(self::PORT_RANGE_REGEX, $port)) { throw new DisplayException(trans('exceptions.allocations.invalid_mapping', ['port' => $port])); } diff --git a/app/Services/Deployment/AllocationSelectionService.php b/app/Services/Deployment/AllocationSelectionService.php new file mode 100644 index 000000000..633ba1f5e --- /dev/null +++ b/app/Services/Deployment/AllocationSelectionService.php @@ -0,0 +1,123 @@ +repository = $repository; + } + + /** + * Toggle if the selected allocation should be the only allocation belonging + * to the given IP address. If true an allocation will not be selected if an IP + * already has another server set to use on if its allocations. + * + * @param bool $dedicated + * @return $this + */ + public function setDedicated(bool $dedicated) + { + $this->dedicated = $dedicated; + + return $this; + } + + /** + * A list of node IDs that should be used when selecting an allocation. If empty, all + * nodes will be used to filter with. + * + * @param array $nodes + * @return $this + */ + public function setNodes(array $nodes) + { + $this->nodes = $nodes; + + return $this; + } + + /** + * An array of individual ports or port ranges to use when selecting an allocation. If + * empty, all ports will be considered when finding an allocation. If set, only ports appearing + * in the array or range will be used. + * + * @param array $ports + * @return $this + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function setPorts(array $ports) + { + $stored = []; + foreach ($ports as $port) { + if (is_digit($port)) { + $stored[] = $port; + } + + // Ranges are stored in the ports array as an array which can be + // better processed in the repository. + if (preg_match(AssignmentService::PORT_RANGE_REGEX, $port, $matches)) { + if (abs($matches[2] - $matches[1]) > AssignmentService::PORT_RANGE_LIMIT) { + throw new DisplayException(trans('exceptions.allocations.too_many_ports')); + } + + $stored[] = [$matches[1], $matches[2]]; + } + } + + $this->ports = $stored; + + return $this; + } + + /** + * Return a single allocation that should be used as the default allocation for a server. + * + * @return \Pterodactyl\Models\Allocation + * + * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException + */ + public function handle(): Allocation + { + $allocation = $this->repository->getRandomAllocation($this->nodes, $this->ports, $this->dedicated); + + if (is_null($allocation)) { + throw new NoViableAllocationException(trans('exceptions.deployment.no_viable_allocations')); + } + + return $allocation; + } +} diff --git a/app/Services/Deployment/FindViableNodesService.php b/app/Services/Deployment/FindViableNodesService.php new file mode 100644 index 000000000..973d7fc71 --- /dev/null +++ b/app/Services/Deployment/FindViableNodesService.php @@ -0,0 +1,121 @@ +repository = $repository; + } + + /** + * Set the locations that should be searched through to locate available nodes. + * + * @param array $locations + * @return $this + */ + public function setLocations(array $locations): self + { + $this->locations = $locations; + + return $this; + } + + /** + * Set the amount of disk that will be used by the server being created. Nodes will be + * filtered out if they do not have enough available free disk space for this server + * to be placed on. + * + * @param int $disk + * @return $this + */ + public function setDisk(int $disk): self + { + $this->disk = $disk; + + return $this; + } + + /** + * Set the amount of memory that this server will be using. As with disk space, nodes that + * do not have enough free memory will be filtered out. + * + * @param int $memory + * @return $this + */ + public function setMemory(int $memory): self + { + $this->memory = $memory; + + return $this; + } + + /** + * Returns an array of nodes that meet the provided requirements and can then + * be passed to the AllocationSelectionService to return a single allocation. + * + * This functionality is used for automatic deployments of servers and will + * attempt to find all nodes in the defined locations that meet the disk and + * memory availability requirements. Any nodes not meeting those requirements + * are tossed out, as are any nodes marked as non-public, meaning automatic + * deployments should not be done aganist them. + * + * @return int[] + * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException + */ + public function handle(): array + { + Assert::integer($this->disk, 'Calls to ' . __METHOD__ . ' must have the disk space set as an integer, received %s'); + Assert::integer($this->memory, 'Calls to ' . __METHOD__ . ' must have the memory usage set as an integer, received %s'); + + $nodes = $this->repository->getNodesWithResourceUse($this->locations, $this->disk, $this->memory); + $viable = []; + + foreach ($nodes as $node) { + $memoryLimit = $node->memory * (1 + ($node->memory_overallocate / 100)); + $diskLimit = $node->disk * (1 + ($node->disk_overallocate / 100)); + + if (($node->sum_memory + $this->memory) > $memoryLimit || ($node->sum_disk + $this->disk) > $diskLimit) { + continue; + } + + $viable[] = $node->id; + } + + if (empty($viable)) { + throw new NoViableNodeException(trans('exceptions.deployment.no_viable_nodes')); + } + + return $viable; + } +} diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index 1859ec77a..4b2f01bd1 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -5,11 +5,16 @@ namespace Pterodactyl\Services\Servers; use Ramsey\Uuid\Uuid; use Pterodactyl\Models\Node; use Pterodactyl\Models\User; +use Pterodactyl\Models\Server; +use Illuminate\Support\Collection; +use Pterodactyl\Models\Allocation; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; -use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Models\Objects\DeploymentObject; +use Pterodactyl\Services\Deployment\FindViableNodesService; +use Pterodactyl\Contracts\Repository\EggRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Services\Deployment\AllocationSelectionService; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; @@ -22,6 +27,11 @@ class ServerCreationService */ private $allocationRepository; + /** + * @var \Pterodactyl\Services\Deployment\AllocationSelectionService + */ + private $allocationSelectionService; + /** * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService */ @@ -38,9 +48,14 @@ class ServerCreationService private $daemonServerRepository; /** - * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface */ - private $nodeRepository; + private $eggRepository; + + /** + * @var \Pterodactyl\Services\Deployment\FindViableNodesService + */ + private $findViableNodesService; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface @@ -52,11 +67,6 @@ class ServerCreationService */ private $serverVariableRepository; - /** - * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface - */ - private $userRepository; - /** * @var \Pterodactyl\Services\Servers\VariableValidatorService */ @@ -66,60 +76,139 @@ class ServerCreationService * CreationService constructor. * * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository + * @param \Pterodactyl\Services\Deployment\AllocationSelectionService $allocationSelectionService * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository - * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $nodeRepository + * @param \Pterodactyl\Contracts\Repository\EggRepositoryInterface $eggRepository + * @param \Pterodactyl\Services\Deployment\FindViableNodesService $findViableNodesService * @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $configurationStructureService * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository - * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository * @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService */ public function __construct( AllocationRepositoryInterface $allocationRepository, + AllocationSelectionService $allocationSelectionService, ConnectionInterface $connection, DaemonServerRepositoryInterface $daemonServerRepository, - NodeRepositoryInterface $nodeRepository, + EggRepositoryInterface $eggRepository, + FindViableNodesService $findViableNodesService, ServerConfigurationStructureService $configurationStructureService, ServerRepositoryInterface $repository, ServerVariableRepositoryInterface $serverVariableRepository, - UserRepositoryInterface $userRepository, VariableValidatorService $validatorService ) { + $this->allocationSelectionService = $allocationSelectionService; $this->allocationRepository = $allocationRepository; $this->configurationStructureService = $configurationStructureService; $this->connection = $connection; $this->daemonServerRepository = $daemonServerRepository; - $this->nodeRepository = $nodeRepository; + $this->eggRepository = $eggRepository; + $this->findViableNodesService = $findViableNodesService; $this->repository = $repository; $this->serverVariableRepository = $serverVariableRepository; - $this->userRepository = $userRepository; $this->validatorService = $validatorService; } /** - * Create a server on both the panel and daemon. + * Create a server on the Panel and trigger a request to the Daemon to begin the server + * creation process. * - * @param array $data - * @return mixed + * @param array $data + * @param \Pterodactyl\Models\Objects\DeploymentObject|null $deployment + * @return \Pterodactyl\Models\Server * * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Illuminate\Validation\ValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - * @throws \Illuminate\Validation\ValidationException + * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException */ - public function create(array $data) + public function handle(array $data, DeploymentObject $deployment = null): Server { - // @todo auto-deployment - $this->connection->beginTransaction(); - $server = $this->repository->create([ + + // If a deployment object has been passed we need to get the allocation + // that the server should use, and assign the node from that allocation. + if ($deployment instanceof DeploymentObject) { + $allocation = $this->configureDeployment($data, $deployment); + $data['allocation_id'] = $allocation->id; + $data['node_id'] = $allocation->node_id; + } + + if (is_null(array_get($data, 'nest_id'))) { + $egg = $this->eggRepository->setColumns(['id', 'nest_id'])->find(array_get($data, 'egg_id')); + $data['nest_id'] = $egg->nest_id; + } + + $eggVariableData = $this->validatorService + ->setUserLevel(User::USER_LEVEL_ADMIN) + ->handle(array_get($data, 'egg_id'), array_get($data, 'environment', [])); + + // Create the server and assign any additional allocations to it. + $server = $this->createModel($data); + $this->storeAssignedAllocations($server, $data); + $this->storeEggVariables($server, $eggVariableData); + + $structure = $this->configurationStructureService->handle($server); + + try { + $this->daemonServerRepository->setServer($server)->create($structure, [ + 'start_on_completion' => (bool) array_get($data, 'start_on_completion', false), + ]); + + $this->connection->commit(); + } catch (RequestException $exception) { + $this->connection->rollBack(); + throw new DaemonConnectionException($exception); + } + + return $server; + } + + /** + * Gets an allocation to use for automatic deployment. + * + * @param array $data + * @param \Pterodactyl\Models\Objects\DeploymentObject $deployment + * + * @return \Pterodactyl\Models\Allocation + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException + * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException + */ + private function configureDeployment(array $data, DeploymentObject $deployment): Allocation + { + $nodes = $this->findViableNodesService->setLocations($deployment->getLocations()) + ->setDisk(array_get($data, 'disk')) + ->setMemory(array_get($data, 'memory')) + ->handle(); + + return $this->allocationSelectionService->setDedicated($deployment->isDedicated()) + ->setNodes($nodes) + ->setPorts($deployment->getPorts()) + ->handle(); + } + + /** + * Store the server in the database and return the model. + * + * @param array $data + * @return \Pterodactyl\Models\Server + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + private function createModel(array $data): Server + { + return $this->repository->create([ 'uuid' => Uuid::uuid4()->toString(), 'uuidShort' => str_random(8), 'node_id' => array_get($data, 'node_id'), 'name' => array_get($data, 'name'), 'description' => array_get($data, 'description') ?? '', - 'skip_scripts' => isset($data['skip_scripts']), + 'skip_scripts' => array_get($data, 'skip_scripts') ?? isset($data['skip_scripts']), 'suspended' => false, 'owner_id' => array_get($data, 'owner_id'), 'memory' => array_get($data, 'memory'), @@ -134,22 +223,35 @@ class ServerCreationService 'pack_id' => (! isset($data['pack_id']) || $data['pack_id'] == 0) ? null : $data['pack_id'], 'startup' => array_get($data, 'startup'), 'daemonSecret' => str_random(Node::DAEMON_SECRET_LENGTH), - 'image' => array_get($data, 'docker_image'), + 'image' => array_get($data, 'image'), ]); + } - // Process allocations and assign them to the server in the database. + /** + * Configure the allocations assigned to this server. + * + * @param \Pterodactyl\Models\Server $server + * @param array $data + */ + private function storeAssignedAllocations(Server $server, array $data) + { $records = [$data['allocation_id']]; if (isset($data['allocation_additional']) && is_array($data['allocation_additional'])) { $records = array_merge($records, $data['allocation_additional']); } $this->allocationRepository->assignAllocationsToServer($server->id, $records); + } - // Process the passed variables and store them in the database. - $this->validatorService->setUserLevel(User::USER_LEVEL_ADMIN); - $results = $this->validatorService->handle(array_get($data, 'egg_id'), array_get($data, 'environment', [])); - - $records = $results->map(function ($result) use ($server) { + /** + * Process environment variables passed for this server and store them in the database. + * + * @param \Pterodactyl\Models\Server $server + * @param \Illuminate\Support\Collection $variables + */ + private function storeEggVariables(Server $server, Collection $variables) + { + $records = $variables->map(function ($result) use ($server) { return [ 'server_id' => $server->id, 'variable_id' => $result->id, @@ -160,20 +262,5 @@ class ServerCreationService if (! empty($records)) { $this->serverVariableRepository->insert($records); } - $structure = $this->configurationStructureService->handle($server); - - // Create the server on the daemon & commit it to the database. - $node = $this->nodeRepository->find($server->node_id); - try { - $this->daemonServerRepository->setNode($node)->create($structure, [ - 'start_on_completion' => (bool) array_get($data, 'start_on_completion', false), - ]); - $this->connection->commit(); - } catch (RequestException $exception) { - $this->connection->rollBack(); - throw new DaemonConnectionException($exception); - } - - return $server; } } diff --git a/app/Services/Servers/ServerDeletionService.php b/app/Services/Servers/ServerDeletionService.php index e6c098165..37481c27e 100644 --- a/app/Services/Servers/ServerDeletionService.php +++ b/app/Services/Servers/ServerDeletionService.php @@ -13,10 +13,10 @@ use Illuminate\Log\Writer; use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Services\Databases\DatabaseManagementService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; +use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class ServerDeletionService @@ -101,28 +101,21 @@ class ServerDeletionService * @param int|\Pterodactyl\Models\Server $server * * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function handle($server) { - if (! $server instanceof Server) { - $server = $this->repository->setColumns(['id', 'node_id', 'uuid'])->find($server); - } - try { $this->daemonServerRepository->setServer($server)->delete(); } catch (RequestException $exception) { $response = $exception->getResponse(); if (is_null($response) || (! is_null($response) && $response->getStatusCode() !== 404)) { - $this->writer->warning($exception); - // If not forcing the deletion, throw an exception, otherwise just log it and // continue with server deletion process in the panel. if (! $this->force) { - throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ - 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), - ])); + throw new DaemonConnectionException($exception); + } else { + $this->writer->warning($exception); } } } diff --git a/app/Services/Servers/VariableValidatorService.php b/app/Services/Servers/VariableValidatorService.php index 31ca3728b..54183f492 100644 --- a/app/Services/Servers/VariableValidatorService.php +++ b/app/Services/Servers/VariableValidatorService.php @@ -73,29 +73,27 @@ class VariableValidatorService public function handle(int $egg, array $fields = []): Collection { $variables = $this->optionVariableRepository->findWhere([['egg_id', '=', $egg]]); - $messages = $this->validator->make([], []); - $response = $variables->map(function ($item) use ($fields, $messages) { - // Skip doing anything if user is not an admin and - // variable is not user viewable or editable. + $data = $rules = $customAttributes = []; + foreach ($variables as $variable) { + $data['environment'][$variable->env_variable] = array_get($fields, $variable->env_variable); + $rules['environment.' . $variable->env_variable] = $variable->rules; + $customAttributes['environment.' . $variable->env_variable] = trans('validation.internal.variable_value', ['env' => $variable->name]); + } + + $validator = $this->validator->make($data, $rules, [], $customAttributes); + if ($validator->fails()) { + throw new ValidationException($validator); + } + + $response = $variables->filter(function ($item) { + // Skip doing anything if user is not an admin and variable is not user viewable or editable. if (! $this->isUserLevel(User::USER_LEVEL_ADMIN) && (! $item->user_editable || ! $item->user_viewable)) { return false; } - $v = $this->validator->make([ - 'variable_value' => array_get($fields, $item->env_variable), - ], [ - 'variable_value' => $item->rules, - ], [], [ - 'variable_value' => trans('validation.internal.variable_value', ['env' => $item->name]), - ]); - - if ($v->fails()) { - foreach ($v->getMessageBag()->all() as $message) { - $messages->getMessageBag()->add($item->env_variable, $message); - } - } - + return true; + })->map(function ($item) use ($fields) { return (object) [ 'id' => $item->id, 'key' => $item->env_variable, @@ -105,10 +103,6 @@ class VariableValidatorService return is_object($item); }); - if (! empty($messages->getMessageBag()->all())) { - throw new ValidationException($messages); - } - return $response; } } diff --git a/app/Traits/Services/HasUserLevels.php b/app/Traits/Services/HasUserLevels.php index d2d95e233..29e49e8e6 100644 --- a/app/Traits/Services/HasUserLevels.php +++ b/app/Traits/Services/HasUserLevels.php @@ -15,10 +15,13 @@ trait HasUserLevels * Set the access level for running this function. * * @param int $level + * @return $this */ public function setUserLevel(int $level) { $this->userLevel = $level; + + return $this; } /** diff --git a/resources/lang/en/exceptions.php b/resources/lang/en/exceptions.php index 373451a7b..712ad92d7 100644 --- a/resources/lang/en/exceptions.php +++ b/resources/lang/en/exceptions.php @@ -56,4 +56,8 @@ return [ 'users' => [ 'node_revocation_failed' => 'Failed to revoke keys on Node #:node. :error', ], + 'deployment' => [ + 'no_viable_nodes' => 'No nodes satisfying the requirements specified for automatic deployment could be found.', + 'no_viable_allocations' => 'No allocations satisfying the requirements for automatic deployment were found.', + ], ]; diff --git a/resources/themes/pterodactyl/admin/servers/new.blade.php b/resources/themes/pterodactyl/admin/servers/new.blade.php index bbed3de6c..bfb6760b4 100644 --- a/resources/themes/pterodactyl/admin/servers/new.blade.php +++ b/resources/themes/pterodactyl/admin/servers/new.blade.php @@ -91,12 +91,6 @@

    Additional allocations to assign to this server on creation.

  • - @@ -202,7 +196,7 @@
    - +

    This is the default Docker image that will be used to run this server.

    diff --git a/routes/api-application.php b/routes/api-application.php index 9649a4306..35cda6fe8 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -77,6 +77,7 @@ Route::group(['prefix' => '/servers'], function () { Route::patch('/{server}/details', 'Servers\ServerDetailsController@details')->name('api.application.servers.details'); Route::patch('/{server}/build', 'Servers\ServerDetailsController@build')->name('api.application.servers.build'); + Route::post('/', 'Servers\ServerController@store'); Route::post('/{server}/suspend', 'Servers\ServerManagementController@suspend')->name('api.application.servers.suspend'); Route::post('/{server}/unsuspend', 'Servers\ServerManagementController@unsuspend')->name('api.application.servers.unsuspend'); Route::post('/{server}/reinstall', 'Servers\ServerManagementController@reinstall')->name('api.application.servers.reinstall'); diff --git a/tests/Unit/Services/Servers/ServerCreationServiceTest.php b/tests/Unit/Services/Servers/ServerCreationServiceTest.php index 51ed61912..5f6c61d1a 100644 --- a/tests/Unit/Services/Servers/ServerCreationServiceTest.php +++ b/tests/Unit/Services/Servers/ServerCreationServiceTest.php @@ -9,16 +9,15 @@ use Pterodactyl\Models\User; use Tests\Traits\MocksUuids; use Pterodactyl\Models\Server; use Tests\Traits\MocksRequestException; -use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Pterodactyl\Exceptions\PterodactylException; use Pterodactyl\Services\Servers\ServerCreationService; use Pterodactyl\Services\Servers\VariableValidatorService; -use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Services\Deployment\FindViableNodesService; +use Pterodactyl\Contracts\Repository\EggRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Services\Deployment\AllocationSelectionService; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; -use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Services\Servers\ServerConfigurationStructureService; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; @@ -35,6 +34,11 @@ class ServerCreationServiceTest extends TestCase */ private $allocationRepository; + /** + * @var \Pterodactyl\Services\Deployment\AllocationSelectionService + */ + private $allocationSelectionService; + /** * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService|\Mockery\Mock */ @@ -51,14 +55,14 @@ class ServerCreationServiceTest extends TestCase private $daemonServerRepository; /** - * @var \GuzzleHttp\Exception\RequestException|\Mockery\Mock + * @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface */ - private $exception; + private $eggRepository; /** - * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface|\Mockery\Mock + * @var \Pterodactyl\Services\Deployment\FindViableNodesService */ - private $nodeRepository; + private $findViableNodesService; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock @@ -88,11 +92,12 @@ class ServerCreationServiceTest extends TestCase parent::setUp(); $this->allocationRepository = m::mock(AllocationRepositoryInterface::class); + $this->allocationSelectionService = m::mock(AllocationSelectionService::class); $this->configurationStructureService = m::mock(ServerConfigurationStructureService::class); $this->connection = m::mock(ConnectionInterface::class); $this->daemonServerRepository = m::mock(DaemonServerRepositoryInterface::class); - $this->exception = m::mock(RequestException::class); - $this->nodeRepository = m::mock(NodeRepositoryInterface::class); + $this->eggRepository = m::mock(EggRepositoryInterface::class); + $this->findViableNodesService = m::mock(FindViableNodesService::class); $this->repository = m::mock(ServerRepositoryInterface::class); $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); $this->userRepository = m::mock(UserRepositoryInterface::class); @@ -119,7 +124,7 @@ class ServerCreationServiceTest extends TestCase $this->allocationRepository->shouldReceive('assignAllocationsToServer')->with($model->id, [$model->allocation_id])->once()->andReturn(1); - $this->validatorService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_ADMIN)->once()->andReturnNull(); + $this->validatorService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_ADMIN)->once()->andReturnSelf(); $this->validatorService->shouldReceive('handle')->with($model->egg_id, [])->once()->andReturn( collect([(object) ['id' => 123, 'value' => 'var1-value']]) ); @@ -133,20 +138,19 @@ class ServerCreationServiceTest extends TestCase ])->once()->andReturn(true); $this->configurationStructureService->shouldReceive('handle')->with($model)->once()->andReturn(['test' => 'struct']); - $node = factory(Node::class)->make(); - $this->nodeRepository->shouldReceive('find')->with($model->node_id)->once()->andReturn($node); - - $this->daemonServerRepository->shouldReceive('setNode')->with($node)->once()->andReturnSelf(); + $this->daemonServerRepository->shouldReceive('setServer')->with($model)->once()->andReturnSelf(); $this->daemonServerRepository->shouldReceive('create')->with(['test' => 'struct'], ['start_on_completion' => false])->once(); $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - $response = $this->getService()->create($model->toArray()); + $response = $this->getService()->handle($model->toArray()); $this->assertSame($model, $response); } /** * Test handling of node timeout or other daemon error. + * + * @expectedException \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException */ public function testExceptionShouldBeThrownIfTheRequestFails() { @@ -159,21 +163,14 @@ class ServerCreationServiceTest extends TestCase $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->repository->shouldReceive('create')->once()->andReturn($model); $this->allocationRepository->shouldReceive('assignAllocationsToServer')->once()->andReturn(1); - $this->validatorService->shouldReceive('setUserLevel')->once()->andReturnNull(); + $this->validatorService->shouldReceive('setUserLevel')->once()->andReturnSelf(); $this->validatorService->shouldReceive('handle')->once()->andReturn(collect([])); $this->configurationStructureService->shouldReceive('handle')->once()->andReturn([]); - $node = factory(Node::class)->make(); - $this->nodeRepository->shouldReceive('find')->with($model->node_id)->once()->andReturn($node); - $this->daemonServerRepository->shouldReceive('setNode')->with($node)->once()->andThrow($this->exception); + $this->daemonServerRepository->shouldReceive('setServer')->with($model)->once()->andThrow($this->getExceptionMock()); $this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); - try { - $this->getService()->create($model->toArray()); - } catch (PterodactylException $exception) { - $this->assertInstanceOf(DaemonConnectionException::class, $exception); - $this->assertInstanceOf(RequestException::class, $exception->getPrevious()); - } + $this->getService()->handle($model->toArray()); } /** @@ -185,13 +182,14 @@ class ServerCreationServiceTest extends TestCase { return new ServerCreationService( $this->allocationRepository, + $this->allocationSelectionService, $this->connection, $this->daemonServerRepository, - $this->nodeRepository, + $this->eggRepository, + $this->findViableNodesService, $this->configurationStructureService, $this->repository, $this->serverVariableRepository, - $this->userRepository, $this->validatorService ); } diff --git a/tests/Unit/Services/Servers/ServerDeletionServiceTest.php b/tests/Unit/Services/Servers/ServerDeletionServiceTest.php index eb5ffc244..d93d2e985 100644 --- a/tests/Unit/Services/Servers/ServerDeletionServiceTest.php +++ b/tests/Unit/Services/Servers/ServerDeletionServiceTest.php @@ -1,23 +1,14 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Services\Servers; -use Exception; use Mockery as m; use Tests\TestCase; use Illuminate\Log\Writer; use GuzzleHttp\Psr7\Response; use Pterodactyl\Models\Server; -use GuzzleHttp\Exception\RequestException; +use Tests\Traits\MocksRequestException; use Illuminate\Database\ConnectionInterface; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Services\Servers\ServerDeletionService; use Pterodactyl\Services\Databases\DatabaseManagementService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; @@ -26,50 +17,37 @@ use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonS class ServerDeletionServiceTest extends TestCase { - /** - * @var \Illuminate\Database\ConnectionInterface - */ - protected $connection; + use MocksRequestException; /** - * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock */ - protected $daemonServerRepository; + private $connection; /** - * @var \Pterodactyl\Services\Databases\DatabaseManagementService + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface|\Mockery\Mock */ - protected $databaseManagementService; + private $daemonServerRepository; /** - * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface + * @var \Pterodactyl\Services\Databases\DatabaseManagementService|\Mockery\Mock */ - protected $databaseRepository; + private $databaseManagementService; /** - * @var \GuzzleHttp\Exception\RequestException + * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface|\Mockery\Mock */ - protected $exception; + private $databaseRepository; /** - * @var \Pterodactyl\Models\Server + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock */ - protected $model; + private $repository; /** - * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + * @var \Illuminate\Log\Writer|\Mockery\Mock */ - protected $repository; - - /** - * @var \Pterodactyl\Services\Servers\ServerDeletionService - */ - protected $service; - - /** - * @var \Illuminate\Log\Writer - */ - protected $writer; + private $writer; /** * Setup tests. @@ -82,19 +60,8 @@ class ServerDeletionServiceTest extends TestCase $this->daemonServerRepository = m::mock(DaemonServerRepositoryInterface::class); $this->databaseRepository = m::mock(DatabaseRepositoryInterface::class); $this->databaseManagementService = m::mock(DatabaseManagementService::class); - $this->exception = m::mock(RequestException::class); - $this->model = factory(Server::class)->make(); $this->repository = m::mock(ServerRepositoryInterface::class); $this->writer = m::mock(Writer::class); - - $this->service = new ServerDeletionService( - $this->connection, - $this->daemonServerRepository, - $this->databaseRepository, - $this->databaseManagementService, - $this->repository, - $this->writer - ); } /** @@ -102,7 +69,7 @@ class ServerDeletionServiceTest extends TestCase */ public function testForceParameterCanBeSet() { - $response = $this->service->withForce(true); + $response = $this->getService()->withForce(true); $this->assertInstanceOf(ServerDeletionService::class, $response); } @@ -112,20 +79,22 @@ class ServerDeletionServiceTest extends TestCase */ public function testServerCanBeDeletedWithoutForce() { - $this->daemonServerRepository->shouldReceive('setServer')->with($this->model)->once()->andReturnSelf() - ->shouldReceive('delete')->withNoArgs()->once()->andReturn(new Response); + $model = factory(Server::class)->make(); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->databaseRepository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf() - ->shouldReceive('findWhere')->with([ - ['server_id', '=', $this->model->id], - ])->once()->andReturn(collect([(object) ['id' => 50]])); + $this->daemonServerRepository->shouldReceive('setServer')->once()->with($model)->andReturnSelf(); + $this->daemonServerRepository->shouldReceive('delete')->once()->withNoArgs()->andReturn(new Response); - $this->databaseManagementService->shouldReceive('delete')->with(50)->once()->andReturnNull(); - $this->repository->shouldReceive('delete')->with($this->model->id)->once()->andReturn(1); - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + $this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull(); + $this->databaseRepository->shouldReceive('setColumns')->once()->with('id')->andReturnSelf(); + $this->databaseRepository->shouldReceive('findWhere')->once()->with([ + ['server_id', '=', $model->id], + ])->andReturn(collect([(object) ['id' => 50]])); - $this->service->handle($this->model); + $this->databaseManagementService->shouldReceive('delete')->once()->with(50)->andReturnNull(); + $this->repository->shouldReceive('delete')->once()->with($model->id)->andReturn(1); + $this->connection->shouldReceive('commit')->once()->withNoArgs()->andReturnNull(); + + $this->getService()->handle($model); } /** @@ -133,64 +102,55 @@ class ServerDeletionServiceTest extends TestCase */ public function testServerShouldBeDeletedEvenWhenFailureOccursIfForceIsSet() { - $this->daemonServerRepository->shouldReceive('setServer')->with($this->model)->once()->andReturnSelf() - ->shouldReceive('delete')->withNoArgs()->once()->andThrow($this->exception); + $this->configureExceptionMock(); + $model = factory(Server::class)->make(); + + $this->daemonServerRepository->shouldReceive('setServer')->once()->with($model)->andReturnSelf(); + $this->daemonServerRepository->shouldReceive('delete')->once()->withNoArgs()->andThrow($this->getExceptionMock()); - $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->databaseRepository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf() - ->shouldReceive('findWhere')->with([ - ['server_id', '=', $this->model->id], - ])->once()->andReturn(collect([(object) ['id' => 50]])); + $this->databaseRepository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf(); + $this->databaseRepository->shouldReceive('findWhere')->with([ + ['server_id', '=', $model->id], + ])->once()->andReturn(collect([(object) ['id' => 50]])); $this->databaseManagementService->shouldReceive('delete')->with(50)->once()->andReturnNull(); - $this->repository->shouldReceive('delete')->with($this->model->id)->once()->andReturn(1); + $this->repository->shouldReceive('delete')->with($model->id)->once()->andReturn(1); $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - $this->service->withForce()->handle($this->model); + $this->getService()->withForce()->handle($model); } /** * Test that an exception is thrown if a server cannot be deleted from the node and force is not set. + * + * @expectedException \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException */ public function testExceptionShouldBeThrownIfDaemonReturnsAnErrorAndForceIsNotSet() { - $this->daemonServerRepository->shouldReceive('setServer->delete')->once()->andThrow($this->exception); - $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); - $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); + $this->configureExceptionMock(); + $model = factory(Server::class)->make(); - try { - $this->service->handle($this->model); - } catch (Exception $exception) { - $this->assertInstanceOf(DisplayException::class, $exception); - $this->assertEquals(trans('admin/server.exceptions.daemon_exception', [ - 'code' => 'E_CONN_REFUSED', - ]), $exception->getMessage()); - } + $this->daemonServerRepository->shouldReceive('setServer->delete')->once()->andThrow($this->getExceptionMock()); + + $this->getService()->handle($model); } /** - * Test that an integer can be passed in place of the Server model. + * Return an instance of the class with mocked dependencies. + * + * @return \Pterodactyl\Services\Servers\ServerDeletionService */ - public function testIntegerCanBePassedInPlaceOfServerModel() + private function getService(): ServerDeletionService { - $this->repository->shouldReceive('setColumns')->with(['id', 'node_id', 'uuid'])->once()->andReturnSelf() - ->shouldReceive('find')->with($this->model->id)->once()->andReturn($this->model); - - $this->daemonServerRepository->shouldReceive('setServer')->with($this->model)->once()->andReturnSelf() - ->shouldReceive('delete')->withNoArgs()->once()->andReturn(new Response); - - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->databaseRepository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf() - ->shouldReceive('findWhere')->with([ - ['server_id', '=', $this->model->id], - ])->once()->andReturn(collect([(object) ['id' => 50]])); - - $this->databaseManagementService->shouldReceive('delete')->with(50)->once()->andReturnNull(); - $this->repository->shouldReceive('delete')->with($this->model->id)->once()->andReturn(1); - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $this->service->handle($this->model->id); + return new ServerDeletionService( + $this->connection, + $this->daemonServerRepository, + $this->databaseRepository, + $this->databaseManagementService, + $this->repository, + $this->writer + ); } } diff --git a/tests/Unit/Services/Servers/VariableValidatorServiceTest.php b/tests/Unit/Services/Servers/VariableValidatorServiceTest.php index 5f5294e53..5af49f436 100644 --- a/tests/Unit/Services/Servers/VariableValidatorServiceTest.php +++ b/tests/Unit/Services/Servers/VariableValidatorServiceTest.php @@ -128,10 +128,13 @@ class VariableValidatorServiceTest extends TestCase $messages = $exception->validator->getMessageBag()->all(); $this->assertNotEmpty($messages); - $this->assertSame(1, count($messages)); - $this->assertSame(trans('validation.required', [ - 'attribute' => trans('validation.internal.variable_value', ['env' => $variables[0]->name]), - ]), $messages[0]); + $this->assertSame(4, count($messages)); + + for ($i = 0; $i < 4; $i++) { + $this->assertSame(trans('validation.required', [ + 'attribute' => trans('validation.internal.variable_value', ['env' => $variables[$i]->name]), + ]), $messages[$i]); + } } } From 1be78054812c07733788f2fadb97e56cd539dd92 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 28 Jan 2018 17:14:34 -0600 Subject: [PATCH 53/54] Add line? --- app/Contracts/Repository/AllocationRepositoryInterface.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Contracts/Repository/AllocationRepositoryInterface.php b/app/Contracts/Repository/AllocationRepositoryInterface.php index ef69965b1..1331e906f 100644 --- a/app/Contracts/Repository/AllocationRepositoryInterface.php +++ b/app/Contracts/Repository/AllocationRepositoryInterface.php @@ -78,5 +78,6 @@ interface AllocationRepositoryInterface extends RepositoryInterface * @param array $ports * @param bool $dedicated * @return \Pterodactyl\Models\Allocation|null + */ public function getRandomAllocation(array $nodes, array $ports, bool $dedicated = false); } From c599112021a9090a40e893329889c58ba6d3f664 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 30 Jan 2018 20:36:59 -0600 Subject: [PATCH 54/54] Finalize server management API --- .../Connection/DaemonConnectionException.php | 3 +- .../Application/Servers/StartupController.php | 49 +++++++++++++++++ .../Servers/UpdateServerStartupRequest.php | 55 +++++++++++++++++++ .../Servers/StartupModificationService.php | 29 ++++++++-- routes/api-application.php | 1 + .../StartupModificationServiceTest.php | 43 +++++++++++---- 6 files changed, 163 insertions(+), 17 deletions(-) create mode 100644 app/Http/Controllers/Api/Application/Servers/StartupController.php create mode 100644 app/Http/Requests/Api/Application/Servers/UpdateServerStartupRequest.php diff --git a/app/Exceptions/Http/Connection/DaemonConnectionException.php b/app/Exceptions/Http/Connection/DaemonConnectionException.php index d6e0ed724..f2892789c 100644 --- a/app/Exceptions/Http/Connection/DaemonConnectionException.php +++ b/app/Exceptions/Http/Connection/DaemonConnectionException.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Exceptions\Http\Connection; +use Illuminate\Http\Response; use GuzzleHttp\Exception\GuzzleException; use Pterodactyl\Exceptions\DisplayException; @@ -10,7 +11,7 @@ class DaemonConnectionException extends DisplayException /** * @var int */ - private $statusCode = 500; + private $statusCode = Response::HTTP_GATEWAY_TIMEOUT; /** * Throw a displayable exception caused by a daemon connection error. diff --git a/app/Http/Controllers/Api/Application/Servers/StartupController.php b/app/Http/Controllers/Api/Application/Servers/StartupController.php new file mode 100644 index 000000000..e6b8015d8 --- /dev/null +++ b/app/Http/Controllers/Api/Application/Servers/StartupController.php @@ -0,0 +1,49 @@ +modificationService = $modificationService; + } + + /** + * Update the startup and environment settings for a specific server. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\UpdateServerStartupRequest $request + * @return array + * + * @throws \Illuminate\Validation\ValidationException + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function index(UpdateServerStartupRequest $request): array + { + $server = $this->modificationService->handle($request->getModel(Server::class), $request->validated()); + + return $this->fractal->item($server) + ->transformWith($this->getTransformer(ServerTransformer::class)) + ->toArray(); + } +} diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerStartupRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerStartupRequest.php new file mode 100644 index 000000000..d337cb4dd --- /dev/null +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerStartupRequest.php @@ -0,0 +1,55 @@ +getModel(Server::class)->id); + + return [ + 'startup' => $data['startup'], + 'environment' => 'present|array', + 'egg' => $data['egg_id'], + 'pack' => $data['pack_id'], + 'image' => $data['image'], + 'skip_scripts' => 'present|boolean', + ]; + } + + /** + * Return the validated data in a format that is expected by the service. + * + * @return array + */ + public function validated() + { + $data = parent::validated(); + + return collect($data)->only(['startup', 'environment', 'skip_scripts'])->merge([ + 'egg_id' => array_get($data, 'egg'), + 'pack_id' => array_get($data, 'pack'), + 'docker_image' => array_get($data, 'image'), + ])->toArray(); + } +} diff --git a/app/Services/Servers/StartupModificationService.php b/app/Services/Servers/StartupModificationService.php index 76a10ad0d..4e954ae1f 100644 --- a/app/Services/Servers/StartupModificationService.php +++ b/app/Services/Servers/StartupModificationService.php @@ -7,6 +7,7 @@ use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Traits\Services\HasUserLevels; +use Pterodactyl\Contracts\Repository\EggRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; @@ -26,6 +27,11 @@ class StartupModificationService */ private $connection; + /** + * @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface + */ + private $eggRepository; + /** * @var \Pterodactyl\Services\Servers\EnvironmentService */ @@ -51,6 +57,7 @@ class StartupModificationService * * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository + * @param \Pterodactyl\Contracts\Repository\EggRepositoryInterface $eggRepository * @param \Pterodactyl\Services\Servers\EnvironmentService $environmentService * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository @@ -59,6 +66,7 @@ class StartupModificationService public function __construct( ConnectionInterface $connection, DaemonServerRepositoryInterface $daemonServerRepository, + EggRepositoryInterface $eggRepository, EnvironmentService $environmentService, ServerRepositoryInterface $repository, ServerVariableRepositoryInterface $serverVariableRepository, @@ -66,6 +74,7 @@ class StartupModificationService ) { $this->daemonServerRepository = $daemonServerRepository; $this->connection = $connection; + $this->eggRepository = $eggRepository; $this->environmentService = $environmentService; $this->repository = $repository; $this->serverVariableRepository = $serverVariableRepository; @@ -77,13 +86,14 @@ class StartupModificationService * * @param \Pterodactyl\Models\Server $server * @param array $data + * @return \Pterodactyl\Models\Server * * @throws \Illuminate\Validation\ValidationException + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException */ - public function handle(Server $server, array $data) + public function handle(Server $server, array $data): Server { $this->connection->beginTransaction(); if (! is_null(array_get($data, 'environment'))) { @@ -119,6 +129,8 @@ class StartupModificationService } $this->connection->commit(); + + return $server; } /** @@ -133,13 +145,22 @@ class StartupModificationService */ private function updateAdministrativeSettings(array $data, Server &$server, array &$daemonData) { + if ( + is_digit(array_get($data, 'egg_id')) + && $data['egg_id'] != $server->egg_id + && is_null(array_get($data, 'nest_id')) + ) { + $egg = $this->eggRepository->setColumns(['id', 'nest_id'])->find($data['egg_id']); + $data['nest_id'] = $egg->nest_id; + } + $server = $this->repository->update($server->id, [ 'installed' => 0, 'startup' => array_get($data, 'startup', $server->startup), 'nest_id' => array_get($data, 'nest_id', $server->nest_id), 'egg_id' => array_get($data, 'egg_id', $server->egg_id), 'pack_id' => array_get($data, 'pack_id', $server->pack_id) > 0 ? array_get($data, 'pack_id', $server->pack_id) : null, - 'skip_scripts' => isset($data['skip_scripts']), + 'skip_scripts' => array_get($data, 'skip_scripts') ?? isset($data['skip_scripts']), 'image' => array_get($data, 'docker_image', $server->image), ]); @@ -147,7 +168,7 @@ class StartupModificationService 'build' => ['image' => $server->image], 'service' => array_merge( $this->repository->getDaemonServiceData($server, true), - ['skip_scripts' => isset($data['skip_scripts'])] + ['skip_scripts' => $server->skip_scripts] ), ]); } diff --git a/routes/api-application.php b/routes/api-application.php index 35cda6fe8..049ba391b 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -76,6 +76,7 @@ Route::group(['prefix' => '/servers'], function () { Route::patch('/{server}/details', 'Servers\ServerDetailsController@details')->name('api.application.servers.details'); Route::patch('/{server}/build', 'Servers\ServerDetailsController@build')->name('api.application.servers.build'); + Route::patch('/{server}/startup', 'Servers\StartupController@index')->name('api.application.servers.startup'); Route::post('/', 'Servers\ServerController@store'); Route::post('/{server}/suspend', 'Servers\ServerManagementController@suspend')->name('api.application.servers.suspend'); diff --git a/tests/Unit/Services/Servers/StartupModificationServiceTest.php b/tests/Unit/Services/Servers/StartupModificationServiceTest.php index fe8392cfe..99453e515 100644 --- a/tests/Unit/Services/Servers/StartupModificationServiceTest.php +++ b/tests/Unit/Services/Servers/StartupModificationServiceTest.php @@ -1,22 +1,17 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Services\Servers; use Mockery as m; use Tests\TestCase; +use Pterodactyl\Models\Egg; use Pterodactyl\Models\User; use GuzzleHttp\Psr7\Response; use Pterodactyl\Models\Server; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Services\Servers\EnvironmentService; use Pterodactyl\Services\Servers\VariableValidatorService; +use Pterodactyl\Contracts\Repository\EggRepositoryInterface; use Pterodactyl\Services\Servers\StartupModificationService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; @@ -34,6 +29,11 @@ class StartupModificationServiceTest extends TestCase */ private $connection; + /** + * @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock + */ + private $eggRepository; + /** * @var \Pterodactyl\Services\Servers\EnvironmentService|\Mockery\Mock */ @@ -63,6 +63,7 @@ class StartupModificationServiceTest extends TestCase $this->daemonServerRepository = m::mock(DaemonServerRepository::class); $this->connection = m::mock(ConnectionInterface::class); + $this->eggRepository = m::mock(EggRepositoryInterface::class); $this->environmentService = m::mock(EnvironmentService::class); $this->repository = m::mock(ServerRepositoryInterface::class); $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); @@ -96,8 +97,10 @@ class StartupModificationServiceTest extends TestCase $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - $this->getService()->handle($model, ['egg_id' => 123, 'environment' => ['test' => 'abcd1234']]); - $this->assertTrue(true); + $response = $this->getService()->handle($model, ['egg_id' => 123, 'environment' => ['test' => 'abcd1234']]); + + $this->assertInstanceOf(Server::class, $response); + $this->assertSame($model, $response); } /** @@ -110,6 +113,11 @@ class StartupModificationServiceTest extends TestCase 'image' => 'docker:image', ]); + $eggModel = factory(Egg::class)->make([ + 'id' => 456, + 'nest_id' => 12345, + ]); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->validatorService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_ADMIN)->once()->andReturnNull(); $this->validatorService->shouldReceive('handle')->with(456, ['test' => 'abcd1234'])->once()->andReturn( @@ -122,9 +130,12 @@ class StartupModificationServiceTest extends TestCase 'variable_id' => 1, ], ['variable_value' => 'stored-value'])->once()->andReturnNull(); + $this->eggRepository->shouldReceive('setColumns->find')->once()->with($eggModel->id)->andReturn($eggModel); + $this->repository->shouldReceive('update')->with($model->id, m::subset([ 'installed' => 0, - 'egg_id' => 456, + 'nest_id' => $eggModel->nest_id, + 'egg_id' => $eggModel->id, 'pack_id' => 789, 'image' => 'docker:image', ]))->once()->andReturn($model); @@ -152,8 +163,15 @@ class StartupModificationServiceTest extends TestCase $service = $this->getService(); $service->setUserLevel(User::USER_LEVEL_ADMIN); - $service->handle($model, ['docker_image' => 'docker:image', 'egg_id' => 456, 'pack_id' => 789, 'environment' => ['test' => 'abcd1234']]); - $this->assertTrue(true); + $response = $service->handle($model, [ + 'docker_image' => 'docker:image', + 'egg_id' => $eggModel->id, + 'pack_id' => 789, + 'environment' => ['test' => 'abcd1234'], + ]); + + $this->assertInstanceOf(Server::class, $response); + $this->assertSame($model, $response); } /** @@ -166,6 +184,7 @@ class StartupModificationServiceTest extends TestCase return new StartupModificationService( $this->connection, $this->daemonServerRepository, + $this->eggRepository, $this->environmentService, $this->repository, $this->serverVariableRepository,