Finish first round of User/Node API additions

Will still need some tweaking and improvements to allow everything to be used.
This commit is contained in:
Dane Everitt 2018-01-01 15:11:44 -06:00
parent d21f70c04b
commit 15289b76a7
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
10 changed files with 220 additions and 68 deletions

View file

@ -11,6 +11,7 @@ namespace Pterodactyl\Exceptions;
use Log; use Log;
use Throwable; use Throwable;
use Illuminate\Http\Response;
use Prologue\Alerts\AlertsMessageBag; use Prologue\Alerts\AlertsMessageBag;
class DisplayException extends PterodactylException class DisplayException extends PterodactylException
@ -65,7 +66,7 @@ class DisplayException extends PterodactylException
if ($request->expectsJson()) { if ($request->expectsJson()) {
return response()->json(Handler::convertToArray($this, [ return response()->json(Handler::convertToArray($this, [
'detail' => $this->getMessage(), 'detail' => $this->getMessage(),
]), method_exists($this, 'getStatusCode') ? $this->getStatusCode() : 500); ]), method_exists($this, 'getStatusCode') ? $this->getStatusCode() : Response::HTTP_INTERNAL_SERVER_ERROR);
} }
app()->make(AlertsMessageBag::class)->danger($this->getMessage())->flash(); app()->make(AlertsMessageBag::class)->danger($this->getMessage())->flash();

View file

@ -1,16 +1,17 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Exceptions\Service; namespace Pterodactyl\Exceptions\Service;
use Illuminate\Http\Response;
use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayException;
class HasActiveServersException extends DisplayException class HasActiveServersException extends DisplayException
{ {
/**
* @return int
*/
public function getStatusCode()
{
return Response::HTTP_BAD_REQUEST;
}
} }

View file

@ -1,16 +1,17 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Exceptions\Service\Location; namespace Pterodactyl\Exceptions\Service\Location;
use Illuminate\Http\Response;
use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayException;
class HasActiveNodesException extends DisplayException class HasActiveNodesException extends DisplayException
{ {
/**
* @return int
*/
public function getStatusCode()
{
return Response::HTTP_BAD_REQUEST;
}
} }

View file

@ -5,13 +5,29 @@ namespace Pterodactyl\Http\Controllers\API\Admin\Nodes;
use Spatie\Fractal\Fractal; use Spatie\Fractal\Fractal;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Pterodactyl\Models\Node; use Pterodactyl\Models\Node;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Http\Controllers\Controller; 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 Pterodactyl\Transformers\Api\Admin\NodeTransformer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter; use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
class NodeController extends Controller class NodeController extends Controller
{ {
/**
* @var \Pterodactyl\Services\Nodes\NodeCreationService
*/
private $creationService;
/**
* @var \Pterodactyl\Services\Nodes\NodeDeletionService
*/
private $deletionService;
/** /**
* @var \Spatie\Fractal\Fractal * @var \Spatie\Fractal\Fractal
*/ */
@ -22,16 +38,32 @@ class NodeController extends Controller
*/ */
private $repository; private $repository;
/**
* @var \Pterodactyl\Services\Nodes\NodeUpdateService
*/
private $updateService;
/** /**
* NodeController constructor. * NodeController constructor.
* *
* @param \Spatie\Fractal\Fractal $fractal * @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 * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
*/ */
public function __construct(Fractal $fractal, NodeRepositoryInterface $repository) public function __construct(
{ Fractal $fractal,
NodeCreationService $creationService,
NodeDeletionService $deletionService,
NodeUpdateService $updateService,
NodeRepositoryInterface $repository
) {
$this->fractal = $fractal; $this->fractal = $fractal;
$this->repository = $repository; $this->repository = $repository;
$this->creationService = $creationService;
$this->deletionService = $deletionService;
$this->updateService = $updateService;
} }
/** /**
@ -67,4 +99,63 @@ class NodeController extends Controller
return $fractal->toArray(); return $fractal->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
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function store(NodeFormRequest $request): JsonResponse
{
$node = $this->creationService->handle($request->normalize());
return $this->fractal->item($node)
->transformWith(new NodeTransformer($request))
->withResourceName('node')
->addMeta([
'link' => route('api.admin.node.view', ['node' => $node->id]),
])
->respond(201);
}
/**
* Update an existing node on the Panel.
*
* @param \Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest $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
{
$node = $this->updateService->returnUpdatedModel()->handle($node, $request->normalize());
return $this->fractal->item($node)
->transformWith(new NodeTransformer($request))
->withResourceName('node')
->toArray();
}
/**
* Deletes a given node from the Panel as long as there are no servers
* currently attached to it.
*
* @param \Pterodactyl\Models\Node $node
* @return \Illuminate\Http\Response
*
* @throws \Pterodactyl\Exceptions\Service\HasActiveServersException
*/
public function delete(Node $node): Response
{
$this->deletionService->handle($node);
return response('', 201);
}
} }

View file

@ -146,13 +146,17 @@ class UserController extends Controller
} }
} }
return $this->fractal->item($collection->get('user')) $response = $this->fractal->item($collection->get('model'))
->transformWith(new UserTransformer($request)) ->transformWith(new UserTransformer($request))
->withResourceName('user') ->withResourceName('user');
->addMeta([
if (count($errors) > 0) {
$response->addMeta([
'revocation_errors' => $errors, 'revocation_errors' => $errors,
]) ]);
->toArray(); }
return $response->toArray();
} }
/** /**

View file

@ -4,11 +4,13 @@ namespace Pterodactyl\Models;
use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Eloquence;
use Sofa\Eloquence\Validable; use Sofa\Eloquence\Validable;
use Illuminate\Validation\Rules\In;
use Illuminate\Auth\Authenticatable; use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\CleansAttributes;
use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Auth\Passwords\CanResetPassword;
use Pterodactyl\Traits\Helpers\AvailableLanguages;
use Illuminate\Foundation\Auth\Access\Authorizable; use Illuminate\Foundation\Auth\Access\Authorizable;
use Sofa\Eloquence\Contracts\Validable as ValidableContract; use Sofa\Eloquence\Contracts\Validable as ValidableContract;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
@ -23,7 +25,9 @@ class User extends Model implements
CleansAttributes, CleansAttributes,
ValidableContract ValidableContract
{ {
use Authenticatable, Authorizable, CanResetPassword, Eloquence, Notifiable, Validable; use Authenticatable, Authorizable, AvailableLanguages, CanResetPassword, Eloquence, Notifiable, Validable {
gatherRules as eloquenceGatherRules;
}
const USER_LEVEL_USER = 0; const USER_LEVEL_USER = 0;
const USER_LEVEL_ADMIN = 1; const USER_LEVEL_ADMIN = 1;
@ -138,11 +142,23 @@ class User extends Model implements
'name_last' => 'string|between:1,255', 'name_last' => 'string|between:1,255',
'password' => 'nullable|string', 'password' => 'nullable|string',
'root_admin' => 'boolean', 'root_admin' => 'boolean',
'language' => 'string|between:2,5', 'language' => 'string',
'use_totp' => 'boolean', 'use_totp' => 'boolean',
'totp_secret' => 'nullable|string', 'totp_secret' => 'nullable|string',
]; ];
/**
* Implement language verification by overriding Eloquence's gather
* rules function.
*/
protected static function gatherRules()
{
$rules = self::eloquenceGatherRules();
$rules['language'][] = new In(array_keys((new self)->getAvailableLanguages()));
return $rules;
}
/** /**
* Send the password reset notification. * Send the password reset notification.
* *

View file

@ -9,82 +9,71 @@
namespace Pterodactyl\Services\Nodes; namespace Pterodactyl\Services\Nodes;
use Illuminate\Log\Writer;
use Pterodactyl\Models\Node; use Pterodactyl\Models\Node;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Traits\Services\ReturnsUpdatedModels;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface;
class NodeUpdateService class NodeUpdateService
{ {
use ReturnsUpdatedModels;
/** /**
* @var \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface * @var \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface
*/ */
protected $configRepository; private $configRepository;
/** /**
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
*/ */
protected $repository; private $repository;
/**
* @var \Illuminate\Log\Writer
*/
protected $writer;
/** /**
* UpdateService constructor. * UpdateService constructor.
* *
* @param \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface $configurationRepository * @param \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface $configurationRepository
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
* @param \Illuminate\Log\Writer $writer
*/ */
public function __construct( public function __construct(
ConfigurationRepositoryInterface $configurationRepository, ConfigurationRepositoryInterface $configurationRepository,
NodeRepositoryInterface $repository, NodeRepositoryInterface $repository
Writer $writer
) { ) {
$this->configRepository = $configurationRepository; $this->configRepository = $configurationRepository;
$this->repository = $repository; $this->repository = $repository;
$this->writer = $writer;
} }
/** /**
* Update the configuration values for a given node on the machine. * Update the configuration values for a given node on the machine.
* *
* @param int|\Pterodactyl\Models\Node $node * @param \Pterodactyl\Models\Node $node
* @param array $data * @param array $data
* @return mixed * @return \Pterodactyl\Models\Node|mixed
* *
* @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function handle($node, array $data) public function handle(Node $node, array $data)
{ {
if (! $node instanceof Node) {
$node = $this->repository->find($node);
}
if (! is_null(array_get($data, 'reset_secret'))) { if (! is_null(array_get($data, 'reset_secret'))) {
$data['daemonSecret'] = str_random(NodeCreationService::DAEMON_SECRET_LENGTH); $data['daemonSecret'] = str_random(Node::DAEMON_SECRET_LENGTH);
unset($data['reset_secret']); unset($data['reset_secret']);
} }
$updateResponse = $this->repository->withoutFresh()->update($node->id, $data); if ($this->getUpdatedModel()) {
$response = $this->repository->update($node->id, $data);
} else {
$response = $this->repository->withoutFresh()->update($node->id, $data);
}
try { try {
$this->configRepository->setNode($node->id)->update(); $this->configRepository->setNode($node->id)->update();
} catch (RequestException $exception) { } catch (RequestException $exception) {
$response = $exception->getResponse(); throw new DaemonConnectionException($exception);
$this->writer->warning($exception);
throw new DisplayException(trans('exceptions.node.daemon_off_config_updated', [
'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(),
]));
} }
return $updateResponse; return $response;
} }
} }

View file

@ -0,0 +1,35 @@
<?php
namespace Pterodactyl\Traits\Services;
trait ReturnsUpdatedModels
{
/**
* @var bool
*/
private $updatedModel = false;
/**
* @return bool
*/
public function getUpdatedModel()
{
return $this->updatedModel;
}
/**
* If called a fresh model will be returned from the database. This is used
* for API calls, but is unnecessary for UI based updates where the page is
* being reloaded and a fresh model will be pulled anyways.
*
* @param bool $toggle
*
* @return $this
*/
public function returnUpdatedModel(bool $toggle = true)
{
$this->updatedModel = $toggle;
return $this;
}
}

View file

@ -13,12 +13,25 @@ Route::group(['prefix' => '/users'], function () {
Route::get('/{user}', 'Users\UserController@view')->name('api.admin.user.view'); Route::get('/{user}', 'Users\UserController@view')->name('api.admin.user.view');
Route::post('/', 'Users\UserController@store')->name('api.admin.user.store'); Route::post('/', 'Users\UserController@store')->name('api.admin.user.store');
Route::put('/{user}', 'Users\UserController@update')->name('api.admin.user.update'); Route::patch('/{user}', 'Users\UserController@update')->name('api.admin.user.update');
Route::delete('/{user}', 'Users\UserController@delete')->name('api.admin.user.delete'); Route::delete('/{user}', 'Users\UserController@delete')->name('api.admin.user.delete');
}); });
/*
|--------------------------------------------------------------------------
| Node Controller Routes
|--------------------------------------------------------------------------
|
| Endpoint: /api/admin/nodes
|
*/
Route::group(['prefix' => '/nodes'], function () { Route::group(['prefix' => '/nodes'], function () {
Route::get('/', 'Nodes\NodeController@index')->name('api.admin.node.list'); Route::get('/', 'Nodes\NodeController@index')->name('api.admin.node.list');
Route::get('/{node}', 'Nodes\NodeController@view')->name('api.admin.node.view'); Route::get('/{node}', 'Nodes\NodeController@view')->name('api.admin.node.view');
Route::post('/', 'Nodes\NodeController@store')->name('api.admin.node.store');
Route::patch('/{node}', 'Nodes\NodeController@update')->name('api.admin.node.update');
Route::delete('/{node}', 'Nodes\NodeController@delete')->name('api.admin.node.delete');
}); });

View file

@ -84,16 +84,17 @@ class NodeUpdateServiceTest extends TestCase
$this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'str_random') $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'str_random')
->expects($this->once())->willReturn('random_string'); ->expects($this->once())->willReturn('random_string');
$this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() $this->repository->->shouldReceive('update')->with($this->node->id, [
->shouldReceive('update')->with($this->node->id, [
'name' => 'NewName', 'name' => 'NewName',
'daemonSecret' => 'random_string', 'daemonSecret' => 'random_string',
])->andReturn(true); ])->andReturn($this->node);
$this->configRepository->shouldReceive('setNode')->with($this->node->id)->once()->andReturnSelf() $this->configRepository->shouldReceive('setNode')->with($this->node->id)->once()->andReturnSelf()
->shouldReceive('update')->withNoArgs()->once()->andReturnNull(); ->shouldReceive('update')->withNoArgs()->once()->andReturnNull();
$this->assertTrue($this->service->handle($this->node, ['name' => 'NewName', 'reset_secret' => true])); $response = $this->service->handle($this->node, ['name' => 'NewName', 'reset_secret' => true]);
$this->assertInstanceOf(Node::class, $response);
$this->assertSame($this->node, $response);
} }
/** /**
@ -101,15 +102,16 @@ class NodeUpdateServiceTest extends TestCase
*/ */
public function testNodeIsUpdatedAndDaemonSecretIsNotChanged() public function testNodeIsUpdatedAndDaemonSecretIsNotChanged()
{ {
$this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() $this->repository->shouldReceive('update')->with($this->node->id, [
->shouldReceive('update')->with($this->node->id, [
'name' => 'NewName', 'name' => 'NewName',
])->andReturn(true); ])->andReturn($this->node);
$this->configRepository->shouldReceive('setNode')->with($this->node->id)->once()->andReturnSelf() $this->configRepository->shouldReceive('setNode')->with($this->node->id)->once()->andReturnSelf()
->shouldReceive('update')->withNoArgs()->once()->andReturnNull(); ->shouldReceive('update')->withNoArgs()->once()->andReturnNull();
$this->assertTrue($this->service->handle($this->node, ['name' => 'NewName'])); $response = $this->service->handle($this->node, ['name' => 'NewName']);
$this->assertInstanceOf(Node::class, $response);
$this->assertSame($this->node, $response);
} }
/** /**
@ -117,8 +119,7 @@ class NodeUpdateServiceTest extends TestCase
*/ */
public function testExceptionCausedByDaemonIsHandled() public function testExceptionCausedByDaemonIsHandled()
{ {
$this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() $this->repository->->shouldReceive('update')->with($this->node->id, [
->shouldReceive('update')->with($this->node->id, [
'name' => 'NewName', 'name' => 'NewName',
])->andReturn(true); ])->andReturn(true);