diff --git a/app/Http/Controllers/API/Admin/Nodes/AllocationController.php b/app/Http/Controllers/API/Admin/Nodes/AllocationController.php index 9eb1d73cf8..161283bfc6 100644 --- a/app/Http/Controllers/API/Admin/Nodes/AllocationController.php +++ b/app/Http/Controllers/API/Admin/Nodes/AllocationController.php @@ -3,7 +3,7 @@ namespace Pterodactyl\Http\Controllers\API\Admin\Nodes; use Spatie\Fractal\Fractal; -use Illuminate\Http\Request; +use Pterodactyl\Models\Node; use Illuminate\Http\Response; use Pterodactyl\Models\Allocation; use Pterodactyl\Http\Controllers\Controller; @@ -11,6 +11,8 @@ use Pterodactyl\Transformers\Api\Admin\AllocationTransformer; use Pterodactyl\Services\Allocations\AllocationDeletionService; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Http\Requests\API\Admin\Allocations\GetAllocationsRequest; +use Pterodactyl\Http\Requests\API\Admin\Allocations\DeleteAllocationRequest; class AllocationController extends Controller { @@ -46,16 +48,16 @@ public function __construct(AllocationDeletionService $deletionService, Allocati /** * Return all of the allocations that exist for a given node. * - * @param \Illuminate\Http\Request $request - * @param int $node + * @param \Pterodactyl\Http\Requests\API\Admin\Allocations\GetAllocationsRequest $request + * @param \Pterodactyl\Models\Node $node * @return array */ - public function index(Request $request, int $node): array + public function index(GetAllocationsRequest $request, Node $node): array { - $allocations = $this->repository->getPaginatedAllocationsForNode($node, 100); + $allocations = $this->repository->getPaginatedAllocationsForNode($node->id, 100); return $this->fractal->collection($allocations) - ->transformWith(new AllocationTransformer($request)) + ->transformWith((new AllocationTransformer)->setKey($request->key())) ->withResourceName('allocation') ->paginateWith(new IlluminatePaginatorAdapter($allocations)) ->toArray(); @@ -64,14 +66,14 @@ public function index(Request $request, int $node): array /** * Delete a specific allocation from the Panel. * - * @param \Illuminate\Http\Request $request - * @param int $node - * @param \Pterodactyl\Models\Allocation $allocation + * @param \Pterodactyl\Http\Requests\API\Admin\Allocations\DeleteAllocationRequest $request + * @param \Pterodactyl\Models\Node $node + * @param \Pterodactyl\Models\Allocation $allocation * @return \Illuminate\Http\Response * * @throws \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException */ - public function delete(Request $request, int $node, Allocation $allocation): Response + public function delete(DeleteAllocationRequest $request, Node $node, Allocation $allocation): Response { $this->deletionService->handle($allocation); diff --git a/app/Http/Controllers/API/Admin/Nodes/NodeController.php b/app/Http/Controllers/API/Admin/Nodes/NodeController.php index 23e5408463..89f39fed2f 100644 --- a/app/Http/Controllers/API/Admin/Nodes/NodeController.php +++ b/app/Http/Controllers/API/Admin/Nodes/NodeController.php @@ -3,7 +3,6 @@ namespace Pterodactyl\Http\Controllers\API\Admin\Nodes; use Spatie\Fractal\Fractal; -use Illuminate\Http\Request; use Pterodactyl\Models\Node; use Illuminate\Http\Response; use Illuminate\Http\JsonResponse; @@ -13,8 +12,12 @@ use Pterodactyl\Services\Nodes\NodeDeletionService; use Pterodactyl\Transformers\Api\Admin\NodeTransformer; use League\Fractal\Pagination\IlluminatePaginatorAdapter; -use Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Http\Requests\API\Admin\Nodes\GetNodeRequest; +use Pterodactyl\Http\Requests\API\Admin\Nodes\GetNodesRequest; +use Pterodactyl\Http\Requests\API\Admin\Nodes\StoreNodeRequest; +use Pterodactyl\Http\Requests\API\Admin\Nodes\DeleteNodeRequest; +use Pterodactyl\Http\Requests\API\Admin\Nodes\UpdateNodeRequest; class NodeController extends Controller { @@ -69,52 +72,50 @@ public function __construct( /** * Return all of the nodes currently available on the Panel. * - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Http\Requests\API\Admin\Nodes\GetNodesRequest $request * @return array */ - public function index(Request $request): array + public function index(GetNodesRequest $request): array { $nodes = $this->repository->paginated(100); - $fractal = $this->fractal->collection($nodes) - ->transformWith(new NodeTransformer($request)) + return $this->fractal->collection($nodes) + ->transformWith((new NodeTransformer)->setKey($request->key())) ->withResourceName('node') - ->paginateWith(new IlluminatePaginatorAdapter($nodes)); - - return $fractal->toArray(); + ->paginateWith(new IlluminatePaginatorAdapter($nodes)) + ->toArray(); } /** * Return data for a single instance of a node. * - * @param \Illuminate\Http\Request $request - * @param \Pterodactyl\Models\Node $node + * @param \Pterodactyl\Http\Requests\API\Admin\Nodes\GetNodeRequest $request + * @param \Pterodactyl\Models\Node $node * @return array */ - public function view(Request $request, Node $node): array + public function view(GetNodeRequest $request, Node $node): array { - $fractal = $this->fractal->item($node) - ->transformWith(new NodeTransformer($request)) - ->withResourceName('node'); - - return $fractal->toArray(); + return $this->fractal->item($node) + ->transformWith((new NodeTransformer)->setKey($request->key())) + ->withResourceName('node') + ->toArray(); } /** * Create a new node on the Panel. Returns the created node and a HTTP/201 * status response on success. * - * @param \Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest $request + * @param \Pterodactyl\Http\Requests\API\Admin\Nodes\StoreNodeRequest $request * @return \Illuminate\Http\JsonResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function store(NodeFormRequest $request): JsonResponse + public function store(StoreNodeRequest $request): JsonResponse { - $node = $this->creationService->handle($request->normalize()); + $node = $this->creationService->handle($request->validated()); return $this->fractal->item($node) - ->transformWith(new NodeTransformer($request)) + ->transformWith((new NodeTransformer)->setKey($request->key())) ->withResourceName('node') ->addMeta([ 'link' => route('api.admin.node.view', ['node' => $node->id]), @@ -125,20 +126,20 @@ public function store(NodeFormRequest $request): JsonResponse /** * Update an existing node on the Panel. * - * @param \Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest $request - * @param \Pterodactyl\Models\Node $node + * @param \Pterodactyl\Http\Requests\API\Admin\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(NodeFormRequest $request, Node $node): array + public function update(UpdateNodeRequest $request, Node $node): array { - $node = $this->updateService->returnUpdatedModel()->handle($node, $request->normalize()); + $node = $this->updateService->returnUpdatedModel()->handle($node, $request->validated()); return $this->fractal->item($node) - ->transformWith(new NodeTransformer($request)) + ->transformWith((new NodeTransformer)->setKey($request->key())) ->withResourceName('node') ->toArray(); } @@ -147,15 +148,16 @@ public function update(NodeFormRequest $request, Node $node): array * Deletes a given node from the Panel as long as there are no servers * currently attached to it. * - * @param \Pterodactyl\Models\Node $node + * @param \Pterodactyl\Http\Requests\API\Admin\Nodes\DeleteNodeRequest $request + * @param \Pterodactyl\Models\Node $node * @return \Illuminate\Http\Response * * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException */ - public function delete(Node $node): Response + public function delete(DeleteNodeRequest $request, Node $node): Response { $this->deletionService->handle($node); - return response('', 201); + return response('', 204); } } diff --git a/app/Http/Requests/API/Admin/Allocations/DeleteAllocationRequest.php b/app/Http/Requests/API/Admin/Allocations/DeleteAllocationRequest.php new file mode 100644 index 0000000000..2db4fee430 --- /dev/null +++ b/app/Http/Requests/API/Admin/Allocations/DeleteAllocationRequest.php @@ -0,0 +1,41 @@ +route()->parameter('node'); + $allocation = $this->route()->parameter('allocation'); + + if ($node instanceof Node && $node->exists) { + if ($allocation instanceof Allocation && $allocation->exists && $allocation->node_id === $node->id) { + return true; + } + } + + return false; + } +} diff --git a/app/Http/Requests/API/Admin/Allocations/GetAllocationsRequest.php b/app/Http/Requests/API/Admin/Allocations/GetAllocationsRequest.php new file mode 100644 index 0000000000..cf46e90b9f --- /dev/null +++ b/app/Http/Requests/API/Admin/Allocations/GetAllocationsRequest.php @@ -0,0 +1,33 @@ +route()->parameter('node'); + + return $node instanceof Node && $node->exists; + } +} diff --git a/app/Http/Requests/API/Admin/Nodes/DeleteNodeRequest.php b/app/Http/Requests/API/Admin/Nodes/DeleteNodeRequest.php new file mode 100644 index 0000000000..f92f4752a1 --- /dev/null +++ b/app/Http/Requests/API/Admin/Nodes/DeleteNodeRequest.php @@ -0,0 +1,33 @@ +route()->parameter('node'); + + return $node instanceof Node && $node->exists; + } +} diff --git a/app/Http/Requests/API/Admin/Nodes/GetNodeRequest.php b/app/Http/Requests/API/Admin/Nodes/GetNodeRequest.php new file mode 100644 index 0000000000..5ea5a42822 --- /dev/null +++ b/app/Http/Requests/API/Admin/Nodes/GetNodeRequest.php @@ -0,0 +1,21 @@ +route()->parameter('node'); + + return $node instanceof Node && $node->exists; + } +} diff --git a/app/Http/Requests/API/Admin/Nodes/GetNodesRequest.php b/app/Http/Requests/API/Admin/Nodes/GetNodesRequest.php new file mode 100644 index 0000000000..13530eaaac --- /dev/null +++ b/app/Http/Requests/API/Admin/Nodes/GetNodesRequest.php @@ -0,0 +1,19 @@ +only([ + 'public', + 'name', + 'location_id', + 'fqdn', + 'scheme', + 'behind_proxy', + 'memory', + 'memory_overallocate', + 'disk', + 'disk_overallocation', + 'upload_size', + 'daemonListen', + 'daemonSFTP', + 'daemonBase', + ])->mapWithKeys(function ($value, $key) { + $key = ($key === 'daemonSFTP') ? 'daemonSftp' : $key; + + return [snake_case($key) => $value]; + })->toArray(); + } + + /** + * Fields to rename for clarity in the API response. + * + * @return array + */ + public function attributes() + { + return [ + 'daemon_base' => 'Daemon Base Path', + 'upload_size' => 'File Upload Size Limit', + 'location_id' => 'Location', + 'public' => 'Node Visibility', + ]; + } + + /** + * Change the formatting of some data keys in the validated response data + * to match what the application expects in the services. + * + * @return array + */ + public function validated() + { + $response = parent::validated(); + $response['daemonListen'] = $response['daemon_listen']; + $response['daemonSFTP'] = $response['daemon_sftp']; + $response['daemonBase'] = $response['daemon_base']; + + unset($response['daemon_base'], $response['daemon_listen'], $response['daemon_sftp']); + + return $response; + } +} diff --git a/app/Http/Requests/API/Admin/Nodes/UpdateNodeRequest.php b/app/Http/Requests/API/Admin/Nodes/UpdateNodeRequest.php new file mode 100644 index 0000000000..da635355bd --- /dev/null +++ b/app/Http/Requests/API/Admin/Nodes/UpdateNodeRequest.php @@ -0,0 +1,35 @@ +route()->parameter('node'); + + return $node instanceof Node && $node->exists; + } + + /** + * Apply validation rules to this request. Uses the parent class rules() + * function but passes in the rules for updating rather than creating. + * + * @param array|null $rules + * @return array + */ + public function rules(array $rules = null): array + { + $nodeId = $this->route()->parameter('node')->id; + + return parent::rules(Node::getUpdateRulesForId($nodeId)); + } +} diff --git a/app/Transformers/Api/Admin/AllocationTransformer.php b/app/Transformers/Api/Admin/AllocationTransformer.php index 9997a57853..a1766ffd77 100644 --- a/app/Transformers/Api/Admin/AllocationTransformer.php +++ b/app/Transformers/Api/Admin/AllocationTransformer.php @@ -3,7 +3,7 @@ namespace Pterodactyl\Transformers\Api\Admin; use Pterodactyl\Models\Allocation; -use Pterodactyl\Transformers\Api\BaseTransformer; +use Pterodactyl\Services\Acl\Api\AdminAcl; class AllocationTransformer extends BaseTransformer { @@ -12,10 +12,7 @@ class AllocationTransformer extends BaseTransformer * * @var array */ - protected $availableIncludes = [ - 'node', - 'server', - ]; + protected $availableIncludes = ['node', 'server']; /** * Return a generic transformed allocation array. @@ -38,37 +35,37 @@ public function transform(Allocation $allocation) * Load the node relationship onto a given transformation. * * @param \Pterodactyl\Models\Allocation $allocation - * @return bool|\League\Fractal\Resource\Item - * - * @throws \Pterodactyl\Exceptions\PterodactylException + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource */ public function includeNode(Allocation $allocation) { - if (! $this->authorize('node-view')) { - return false; + if (! $this->authorize(AdminAcl::RESOURCE_NODES)) { + return $this->null(); } $allocation->loadMissing('node'); - return $this->item($allocation->getRelation('node'), new NodeTransformer($this->getRequest()), 'node'); + return $this->item( + $allocation->getRelation('node'), $this->makeTransformer(NodeTransformer::class), 'node' + ); } /** * Load the server relationship onto a given transformation. * * @param \Pterodactyl\Models\Allocation $allocation - * @return bool|\League\Fractal\Resource\Item - * - * @throws \Pterodactyl\Exceptions\PterodactylException + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource */ public function includeServer(Allocation $allocation) { - if (! $this->authorize('server-view')) { - return false; + if (! $this->authorize(AdminAcl::RESOURCE_SERVERS)) { + return $this->null(); } $allocation->loadMissing('server'); - return $this->item($allocation->getRelation('server'), new ServerTransformer($this->getRequest()), 'server'); + return $this->item( + $allocation->getRelation('server'), $this->makeTransformer(ServerTransformer::class), 'server' + ); } } diff --git a/app/Transformers/Api/Admin/BaseTransformer.php b/app/Transformers/Api/Admin/BaseTransformer.php index b4d522635d..7673d034c8 100644 --- a/app/Transformers/Api/Admin/BaseTransformer.php +++ b/app/Transformers/Api/Admin/BaseTransformer.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Transformers\Api\Admin; +use Cake\Chronos\Chronos; use Pterodactyl\Models\APIKey; use Illuminate\Container\Container; use League\Fractal\TransformerAbstract; @@ -9,6 +10,8 @@ abstract class BaseTransformer extends TransformerAbstract { + const RESPONSE_TIMEZONE = 'UTC'; + /** * @var \Pterodactyl\Models\APIKey */ @@ -66,4 +69,17 @@ protected function makeTransformer(string $abstract, array $parameters = []): se return $transformer; } + + /** + * Return an ISO-8601 formatted timestamp to use in the API response. + * + * @param string $timestamp + * @return string + */ + protected function formatTimestamp(string $timestamp): string + { + return Chronos::createFromFormat(Chronos::DEFAULT_TO_STRING_FORMAT, $timestamp) + ->setTimezone(self::RESPONSE_TIMEZONE) + ->toIso8601String(); + } } diff --git a/app/Transformers/Api/Admin/NodeTransformer.php b/app/Transformers/Api/Admin/NodeTransformer.php index e4630699d7..26c028edb5 100644 --- a/app/Transformers/Api/Admin/NodeTransformer.php +++ b/app/Transformers/Api/Admin/NodeTransformer.php @@ -3,7 +3,7 @@ namespace Pterodactyl\Transformers\Api\Admin; use Pterodactyl\Models\Node; -use Pterodactyl\Transformers\Api\BaseTransformer; +use Pterodactyl\Services\Acl\Api\AdminAcl; class NodeTransformer extends BaseTransformer { @@ -15,70 +15,82 @@ class NodeTransformer extends BaseTransformer protected $availableIncludes = ['allocations', 'location', 'servers']; /** - * Return a generic transformed pack array. + * Return a node transformed into a format that can be consumed by the + * external administrative API. * * @param \Pterodactyl\Models\Node $node * @return array */ public function transform(Node $node): array { - return $node->toArray(); + $response = collect($node->toArray())->mapWithKeys(function ($value, $key) { + // I messed up early in 2016 when I named this column as poorly + // as I did. This is the tragic result of my mistakes. + $key = ($key === 'daemonSFTP') ? 'daemonSftp' : $key; + + return [snake_case($key) => $value]; + })->toArray(); + + $response[$node->getUpdatedAtColumn()] = $this->formatTimestamp($node->updated_at); + $response[$node->getCreatedAtColumn()] = $this->formatTimestamp($node->created_at); + + return $response; } /** * Return the nodes associated with this location. * * @param \Pterodactyl\Models\Node $node - * @return \League\Fractal\Resource\Collection + * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource */ public function includeAllocations(Node $node) { - if (! $node->relationLoaded('allocations')) { - $node->load('allocations'); + if (! $this->authorize(AdminAcl::RESOURCE_ALLOCATIONS)) { + return $this->null(); } - return $this->collection($node->getRelation('allocations'), new AllocationTransformer($this->getRequest()), 'allocation'); + $node->loadMissing('allocations'); + + return $this->collection( + $node->getRelation('allocations'), $this->makeTransformer(AllocationTransformer::class), 'allocation' + ); } /** * Return the nodes associated with this location. * * @param \Pterodactyl\Models\Node $node - * @return bool|\League\Fractal\Resource\Item - * - * @throws \Pterodactyl\Exceptions\PterodactylException + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource */ public function includeLocation(Node $node) { - if (! $this->authorize('location-list')) { - return false; + if (! $this->authorize(AdminAcl::RESOURCE_LOCATIONS)) { + return $this->null(); } - if (! $node->relationLoaded('location')) { - $node->load('location'); - } + $node->loadMissing('location'); - return $this->item($node->getRelation('location'), new LocationTransformer($this->getRequest()), 'location'); + return $this->item( + $node->getRelation('location'), $this->makeTransformer(LocationTransformer::class), 'location' + ); } /** * Return the nodes associated with this location. * * @param \Pterodactyl\Models\Node $node - * @return bool|\League\Fractal\Resource\Collection - * - * @throws \Pterodactyl\Exceptions\PterodactylException + * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource */ public function includeServers(Node $node) { - if (! $this->authorize('server-list')) { - return false; + if (! $this->authorize(AdminAcl::RESOURCE_SERVERS)) { + return $this->null(); } - if (! $node->relationLoaded('servers')) { - $node->load('servers'); - } + $node->loadMissing('servers'); - return $this->collection($node->getRelation('servers'), new ServerTransformer($this->getRequest()), 'server'); + return $this->collection( + $node->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), 'server' + ); } } diff --git a/composer.json b/composer.json index 091416ef37..49bc61878f 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "ext-zip": "*", "appstract/laravel-blade-directives": "^0.7", "aws/aws-sdk-php": "^3.48", + "cakephp/chronos": "^1.1", "doctrine/dbal": "^2.5", "fideloper/proxy": "^3.3", "guzzlehttp/guzzle": "^6.3", diff --git a/composer.lock b/composer.lock index d7798684fa..1d4eb4aac3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "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": "5cdbf1cd4e3e64e939ca2201704d0141", + "content-hash": "bf623da6beb7303ec158d9ff3e9dde87", "packages": [ { "name": "appstract/laravel-blade-directives", @@ -139,6 +139,63 @@ ], "time": "2017-12-29T17:28:50+00:00" }, + { + "name": "cakephp/chronos", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/cakephp/chronos.git", + "reference": "56d98330d366a469745848b07540373846c40561" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/chronos/zipball/56d98330d366a469745848b07540373846c40561", + "reference": "56d98330d366a469745848b07540373846c40561", + "shasum": "" + }, + "require": { + "php": "^5.5.9|^7" + }, + "require-dev": { + "athletic/athletic": "~0.1", + "cakephp/cakephp-codesniffer": "~2.3", + "phpbench/phpbench": "@dev", + "phpstan/phpstan": "^0.6.4", + "phpunit/phpunit": "<6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cake\\Chronos\\": "src" + }, + "files": [ + "src/carbon_compat.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "http://nesbot.com" + }, + { + "name": "The CakePHP Team", + "homepage": "http://cakephp.org" + } + ], + "description": "A simple API extension for DateTime.", + "homepage": "http://cakephp.org", + "keywords": [ + "date", + "datetime", + "time" + ], + "time": "2017-12-25T22:42:18+00:00" + }, { "name": "dnoegel/php-xdg-base-dir", "version": "0.1",