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:
parent
d21f70c04b
commit
15289b76a7
10 changed files with 220 additions and 68 deletions
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
35
app/Traits/Services/ReturnsUpdatedModels.php
Normal file
35
app/Traits/Services/ReturnsUpdatedModels.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in a new issue