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 Throwable;
use Illuminate\Http\Response;
use Prologue\Alerts\AlertsMessageBag;
class DisplayException extends PterodactylException
@ -65,7 +66,7 @@ class DisplayException extends PterodactylException
if ($request->expectsJson()) {
return response()->json(Handler::convertToArray($this, [
'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();

View file

@ -1,16 +1,17 @@
<?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;
use Illuminate\Http\Response;
use Pterodactyl\Exceptions\DisplayException;
class HasActiveServersException extends DisplayException
{
/**
* @return int
*/
public function getStatusCode()
{
return Response::HTTP_BAD_REQUEST;
}
}

View file

@ -1,16 +1,17 @@
<?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;
use Illuminate\Http\Response;
use Pterodactyl\Exceptions\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 Illuminate\Http\Request;
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 Pterodactyl\Transformers\Api\Admin\NodeTransformer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
class NodeController extends Controller
{
/**
* @var \Pterodactyl\Services\Nodes\NodeCreationService
*/
private $creationService;
/**
* @var \Pterodactyl\Services\Nodes\NodeDeletionService
*/
private $deletionService;
/**
* @var \Spatie\Fractal\Fractal
*/
@ -22,16 +38,32 @@ class NodeController extends Controller
*/
private $repository;
/**
* @var \Pterodactyl\Services\Nodes\NodeUpdateService
*/
private $updateService;
/**
* 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, NodeRepositoryInterface $repository)
{
public function __construct(
Fractal $fractal,
NodeCreationService $creationService,
NodeDeletionService $deletionService,
NodeUpdateService $updateService,
NodeRepositoryInterface $repository
) {
$this->fractal = $fractal;
$this->repository = $repository;
$this->creationService = $creationService;
$this->deletionService = $deletionService;
$this->updateService = $updateService;
}
/**
@ -67,4 +99,63 @@ class NodeController extends Controller
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))
->withResourceName('user')
->addMeta([
->withResourceName('user');
if (count($errors) > 0) {
$response->addMeta([
'revocation_errors' => $errors,
])
->toArray();
]);
}
return $response->toArray();
}
/**

View file

@ -4,11 +4,13 @@ namespace Pterodactyl\Models;
use Sofa\Eloquence\Eloquence;
use Sofa\Eloquence\Validable;
use Illuminate\Validation\Rules\In;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Sofa\Eloquence\Contracts\CleansAttributes;
use Illuminate\Auth\Passwords\CanResetPassword;
use Pterodactyl\Traits\Helpers\AvailableLanguages;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Sofa\Eloquence\Contracts\Validable as ValidableContract;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
@ -23,7 +25,9 @@ class User extends Model implements
CleansAttributes,
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_ADMIN = 1;
@ -138,11 +142,23 @@ class User extends Model implements
'name_last' => 'string|between:1,255',
'password' => 'nullable|string',
'root_admin' => 'boolean',
'language' => 'string|between:2,5',
'language' => 'string',
'use_totp' => 'boolean',
'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.
*

View file

@ -9,82 +9,71 @@
namespace Pterodactyl\Services\Nodes;
use Illuminate\Log\Writer;
use Pterodactyl\Models\Node;
use GuzzleHttp\Exception\RequestException;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Traits\Services\ReturnsUpdatedModels;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface;
class NodeUpdateService
{
use ReturnsUpdatedModels;
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface
*/
protected $configRepository;
private $configRepository;
/**
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
*/
protected $repository;
/**
* @var \Illuminate\Log\Writer
*/
protected $writer;
private $repository;
/**
* UpdateService constructor.
*
* @param \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface $configurationRepository
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
* @param \Illuminate\Log\Writer $writer
*/
public function __construct(
ConfigurationRepositoryInterface $configurationRepository,
NodeRepositoryInterface $repository,
Writer $writer
NodeRepositoryInterface $repository
) {
$this->configRepository = $configurationRepository;
$this->repository = $repository;
$this->writer = $writer;
}
/**
* Update the configuration values for a given node on the machine.
*
* @param int|\Pterodactyl\Models\Node $node
* @param array $data
* @return mixed
* @param \Pterodactyl\Models\Node $node
* @param array $data
* @return \Pterodactyl\Models\Node|mixed
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @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'))) {
$data['daemonSecret'] = str_random(NodeCreationService::DAEMON_SECRET_LENGTH);
$data['daemonSecret'] = str_random(Node::DAEMON_SECRET_LENGTH);
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 {
$this->configRepository->setNode($node->id)->update();
} catch (RequestException $exception) {
$response = $exception->getResponse();
$this->writer->warning($exception);
throw new DisplayException(trans('exceptions.node.daemon_off_config_updated', [
'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(),
]));
throw new DaemonConnectionException($exception);
}
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::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');
});
/*
|--------------------------------------------------------------------------
| Node Controller Routes
|--------------------------------------------------------------------------
|
| Endpoint: /api/admin/nodes
|
*/
Route::group(['prefix' => '/nodes'], function () {
Route::get('/', 'Nodes\NodeController@index')->name('api.admin.node.list');
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')
->expects($this->once())->willReturn('random_string');
$this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->node->id, [
'name' => 'NewName',
'daemonSecret' => 'random_string',
])->andReturn(true);
$this->repository->->shouldReceive('update')->with($this->node->id, [
'name' => 'NewName',
'daemonSecret' => 'random_string',
])->andReturn($this->node);
$this->configRepository->shouldReceive('setNode')->with($this->node->id)->once()->andReturnSelf()
->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()
{
$this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->node->id, [
'name' => 'NewName',
])->andReturn(true);
$this->repository->shouldReceive('update')->with($this->node->id, [
'name' => 'NewName',
])->andReturn($this->node);
$this->configRepository->shouldReceive('setNode')->with($this->node->id)->once()->andReturnSelf()
->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()
{
$this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf()
->shouldReceive('update')->with($this->node->id, [
$this->repository->->shouldReceive('update')->with($this->node->id, [
'name' => 'NewName',
])->andReturn(true);