diff --git a/app/Console/Commands/Migration/CleanOrphanedApiKeysCommand.php b/app/Console/Commands/Migration/CleanOrphanedApiKeysCommand.php new file mode 100644 index 000000000..b9e007ee7 --- /dev/null +++ b/app/Console/Commands/Migration/CleanOrphanedApiKeysCommand.php @@ -0,0 +1,58 @@ +repository = $repository; + } + + /** + * Delete all orphaned API keys from the database when upgrading from 0.6 to 0.7. + * + * @return null|void + */ + public function handle() + { + $count = $this->repository->findCountWhere([['key_type', '=', ApiKey::TYPE_NONE]]); + $continue = $this->confirm( + 'This action will remove ' . $count . ' keys from the database. Are you sure you wish to continue?', false + ); + + if (! $continue) { + return null; + } + + $this->info('Deleting keys...'); + $this->repository->deleteWhere([['key_type', '=', ApiKey::TYPE_NONE]]); + $this->info('Keys were successfully deleted.'); + } +} diff --git a/app/Contracts/Repository/AllocationRepositoryInterface.php b/app/Contracts/Repository/AllocationRepositoryInterface.php index 22ca07656..1331e906f 100644 --- a/app/Contracts/Repository/AllocationRepositoryInterface.php +++ b/app/Contracts/Repository/AllocationRepositoryInterface.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Contracts\Repository; use Illuminate\Support\Collection; +use Illuminate\Contracts\Pagination\LengthAwarePaginator; interface AllocationRepositoryInterface extends RepositoryInterface { @@ -23,6 +24,15 @@ interface AllocationRepositoryInterface extends RepositoryInterface */ public function getAllocationsForNode(int $node): Collection; + /** + * Return all of the allocations for a node in a paginated format. + * + * @param int $node + * @param int $perPage + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public function getPaginatedAllocationsForNode(int $node, int $perPage = 100): LengthAwarePaginator; + /** * Return all of the unique IPs that exist for a given node. * @@ -30,4 +40,44 @@ 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; + + /** + * 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/ApiKeyRepositoryInterface.php b/app/Contracts/Repository/ApiKeyRepositoryInterface.php index 2fce09cd2..3eb03880b 100644 --- a/app/Contracts/Repository/ApiKeyRepositoryInterface.php +++ b/app/Contracts/Repository/ApiKeyRepositoryInterface.php @@ -1,24 +1,43 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Contracts\Repository; -use Pterodactyl\Models\APIKey; +use Pterodactyl\Models\User; +use Illuminate\Support\Collection; interface ApiKeyRepositoryInterface extends RepositoryInterface { /** - * Load permissions for a key onto the model. + * Get all of the account API keys that exist for a specific user. * - * @param \Pterodactyl\Models\APIKey $model - * @param bool $refresh - * @return \Pterodactyl\Models\APIKey + * @param \Pterodactyl\Models\User $user + * @return \Illuminate\Support\Collection */ - public function loadPermissions(APIKey $model, bool $refresh = false): APIKey; + public function getAccountKeys(User $user): Collection; + + /** + * Get all of the application API keys that exist for a specific user. + * + * @param \Pterodactyl\Models\User $user + * @return \Illuminate\Support\Collection + */ + public function getApplicationKeys(User $user): Collection; + + /** + * Delete an account API key from the panel for a specific user. + * + * @param \Pterodactyl\Models\User $user + * @param string $identifier + * @return int + */ + public function deleteAccountKey(User $user, string $identifier): int; + + /** + * Delete an application API key from the panel for a specific user. + * + * @param \Pterodactyl\Models\User $user + * @param string $identifier + * @return int + */ + public function deleteApplicationKey(User $user, string $identifier): int; } 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/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php index 028058af4..4a098c34f 100644 --- a/app/Contracts/Repository/RepositoryInterface.php +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Contracts\Repository; use Illuminate\Support\Collection; +use Illuminate\Contracts\Pagination\LengthAwarePaginator; interface RepositoryInterface { @@ -175,6 +176,14 @@ interface RepositoryInterface */ public function all(): Collection; + /** + * Return a paginated result set using a search term if set on the repository. + * + * @param int $perPage + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public function paginated(int $perPage): LengthAwarePaginator; + /** * Insert a single or multiple records into the database at once skipping * validation and mass assignment checking. diff --git a/app/Exceptions/DisplayException.php b/app/Exceptions/DisplayException.php index aa18a1c1b..e3a839c5e 100644 --- a/app/Exceptions/DisplayException.php +++ b/app/Exceptions/DisplayException.php @@ -1,19 +1,16 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Exceptions; use Log; use Throwable; +use Illuminate\Http\Response; +use Prologue\Alerts\AlertsMessageBag; class DisplayException extends PterodactylException { + const LEVEL_DEBUG = 'debug'; + const LEVEL_INFO = 'info'; const LEVEL_WARNING = 'warning'; const LEVEL_ERROR = 'error'; @@ -32,13 +29,13 @@ class DisplayException extends PterodactylException */ public function __construct($message, Throwable $previous = null, $level = self::LEVEL_ERROR, $code = 0) { - $this->level = $level; + parent::__construct($message, $code, $previous); if (! is_null($previous)) { Log::{$level}($previous); } - parent::__construct($message, $code, $previous); + $this->level = $level; } /** @@ -48,4 +45,33 @@ 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 + * request originated from an API hit, return the error in JSONAPI spec format. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse + */ + public function render($request) + { + if ($request->expectsJson()) { + return response()->json(Handler::convertToArray($this, [ + 'detail' => $this->getMessage(), + ]), method_exists($this, 'getStatusCode') ? $this->getStatusCode() : Response::HTTP_BAD_REQUEST); + } + + app()->make(AlertsMessageBag::class)->danger($this->getMessage())->flash(); + + return redirect()->back()->withInput(); + } } diff --git a/app/Exceptions/DisplayValidationException.php b/app/Exceptions/DisplayValidationException.php deleted file mode 100644 index 38ae082f6..000000000 --- a/app/Exceptions/DisplayValidationException.php +++ /dev/null @@ -1,14 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Exceptions; - -class DisplayValidationException extends DisplayException -{ -} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index ed7c004b2..111d2a6b8 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -3,13 +3,11 @@ namespace Pterodactyl\Exceptions; use Exception; -use Prologue\Alerts\Facades\Alert; use Illuminate\Auth\AuthenticationException; use Illuminate\Session\TokenMismatchException; use Illuminate\Validation\ValidationException; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Database\Eloquent\ModelNotFoundException; -use Pterodactyl\Exceptions\Model\DataValidationException; use Symfony\Component\HttpKernel\Exception\HttpException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; @@ -25,8 +23,6 @@ class Handler extends ExceptionHandler AuthenticationException::class, AuthorizationException::class, DisplayException::class, - DataValidationException::class, - DisplayValidationException::class, HttpException::class, ModelNotFoundException::class, RecordNotFoundException::class, @@ -34,6 +30,18 @@ class Handler extends ExceptionHandler ValidationException::class, ]; + /** + * A list of the inputs that are never flashed for validation exceptions. + * + * @var array + */ + protected $dontFlash = [ + 'token', + 'secret', + 'password', + 'password_confirmation', + ]; + /** * Report or log an exception. * @@ -53,40 +61,78 @@ class Handler extends ExceptionHandler * * @param \Illuminate\Http\Request $request * @param \Exception $exception - * @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response + * @return \Symfony\Component\HttpFoundation\Response * * @throws \Exception */ public function render($request, Exception $exception) { - if ($request->expectsJson() || $request->isJson() || $request->is(...config('pterodactyl.json_routes'))) { - $exception = $this->prepareException($exception); + return parent::render($request, $exception); + } - if (config('app.debug') || $this->isHttpException($exception) || $exception instanceof DisplayException) { - $displayError = $exception->getMessage(); - } else { - $displayError = 'An unhandled exception was encountered with this request.'; + /** + * @param \Illuminate\Http\Request $request + * @param \Illuminate\Validation\ValidationException $exception + * @return \Illuminate\Http\JsonResponse + */ + public function invalidJson($request, ValidationException $exception) + { + $codes = collect($exception->validator->failed())->mapWithKeys(function ($reasons, $field) { + $cleaned = []; + foreach ($reasons as $reason => $attrs) { + $cleaned[] = snake_case($reason); } - $response = response()->json( - [ - 'error' => $displayError, - 'http_code' => (method_exists($exception, 'getStatusCode')) ? $exception->getStatusCode() : 500, - 'trace' => (! config('app.debug')) ? null : $exception->getTrace(), + 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, str_replace('.', '_', $field) . '.' . $key), + 'detail' => $error, + 'source' => ['field' => $field], + ]; + } + + return $response; + })->flatMap(function ($errors) { + return $errors; + })->toArray(); + + return response()->json(['errors' => $errors], $exception->status); + } + + /** + * Return the exception as a JSONAPI representation for use on API requests. + * + * @param \Exception $exception + * @param array $override + * @return array + */ + public static function convertToArray(Exception $exception, array $override = []): array + { + $error = [ + 'code' => class_basename($exception), + 'status' => method_exists($exception, 'getStatusCode') ? strval($exception->getStatusCode()) : '500', + 'detail' => 'An error was encountered while processing this request.', + ]; + + if (config('app.debug')) { + $error = array_merge($error, [ + 'detail' => $exception->getMessage(), + 'source' => [ + 'line' => $exception->getLine(), + 'file' => str_replace(base_path(), '', $exception->getFile()), ], - $this->isHttpException($exception) ? $exception->getStatusCode() : 500, - $this->isHttpException($exception) ? $exception->getHeaders() : [], - JSON_UNESCAPED_SLASHES - ); - - parent::report($exception); - } elseif ($exception instanceof DisplayException) { - Alert::danger($exception->getMessage())->flash(); - - return redirect()->back()->withInput(); + 'meta' => [ + 'trace' => explode("\n", $exception->getTraceAsString()), + ], + ]); } - return (isset($response)) ? $response : parent::render($request, $exception); + return ['errors' => [array_merge($error, $override)]]; } /** @@ -104,4 +150,16 @@ class Handler extends ExceptionHandler return redirect()->guest(route('auth.login')); } + + /** + * Converts an exception into an array to render in the response. Overrides + * Laravel's built-in converter to output as a JSONAPI spec compliant object. + * + * @param \Exception $exception + * @return array + */ + protected function convertExceptionToArray(Exception $exception) + { + return self::convertToArray($exception); + } } 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/Exceptions/Model/DataValidationException.php b/app/Exceptions/Model/DataValidationException.php index 186f6e7b9..5840e6d9c 100644 --- a/app/Exceptions/Model/DataValidationException.php +++ b/app/Exceptions/Model/DataValidationException.php @@ -1,20 +1,21 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Exceptions\Model; use Illuminate\Contracts\Validation\Validator; -use Illuminate\Validation\ValidationException; +use Pterodactyl\Exceptions\PterodactylException; use Illuminate\Contracts\Support\MessageProvider; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; -class DataValidationException extends ValidationException implements MessageProvider +class DataValidationException extends PterodactylException implements HttpExceptionInterface, MessageProvider { + /** + * The validator instance. + * + * @var \Illuminate\Contracts\Validation\Validator + */ + public $validator; + /** * DataValidationException constructor. * @@ -22,14 +23,38 @@ class DataValidationException extends ValidationException implements MessageProv */ public function __construct(Validator $validator) { - parent::__construct($validator); + parent::__construct( + 'Data integrity exception encountered while performing database write operation. ' . $validator->errors()->toJson() + ); + + $this->validator = $validator; } /** + * Return the validator message bag. + * * @return \Illuminate\Support\MessageBag */ public function getMessageBag() { return $this->validator->errors(); } + + /** + * Return the status code for this request. + * + * @return int + */ + public function getStatusCode() + { + return 500; + } + + /** + * @return array + */ + public function getHeaders() + { + return []; + } } diff --git a/app/Exceptions/PterodactylException.php b/app/Exceptions/PterodactylException.php index 1005aab58..451ae92cb 100644 --- a/app/Exceptions/PterodactylException.php +++ b/app/Exceptions/PterodactylException.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Exceptions; diff --git a/app/Exceptions/Repository/Daemon/InvalidPowerSignalException.php b/app/Exceptions/Repository/Daemon/InvalidPowerSignalException.php index be44103ff..9c05f70cc 100644 --- a/app/Exceptions/Repository/Daemon/InvalidPowerSignalException.php +++ b/app/Exceptions/Repository/Daemon/InvalidPowerSignalException.php @@ -1,14 +1,9 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Exceptions\Repository\Daemon; -class InvalidPowerSignalException extends \Exception +use Pterodactyl\Exceptions\Repository\RepositoryException; + +class InvalidPowerSignalException extends RepositoryException { } diff --git a/app/Exceptions/Repository/DuplicateDatabaseNameException.php b/app/Exceptions/Repository/DuplicateDatabaseNameException.php index 9b09602f6..114695282 100644 --- a/app/Exceptions/Repository/DuplicateDatabaseNameException.php +++ b/app/Exceptions/Repository/DuplicateDatabaseNameException.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Exceptions\Repository; diff --git a/app/Exceptions/Repository/RecordNotFoundException.php b/app/Exceptions/Repository/RecordNotFoundException.php index 82f032ce3..f449faa42 100644 --- a/app/Exceptions/Repository/RecordNotFoundException.php +++ b/app/Exceptions/Repository/RecordNotFoundException.php @@ -1,21 +1,20 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Exceptions\Repository; -class RecordNotFoundException extends \Exception +class RecordNotFoundException extends RepositoryException { /** - * @return int + * Handle request to render this exception to a user. Returns the default + * 404 page view. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response */ - public function getStatusCode() + public function render($request) { - return 404; + if (! config('app.debug')) { + return response()->view('errors.404', [], 404); + } } } diff --git a/app/Exceptions/Repository/RepositoryException.php b/app/Exceptions/Repository/RepositoryException.php index 439ea4e43..d362cd423 100644 --- a/app/Exceptions/Repository/RepositoryException.php +++ b/app/Exceptions/Repository/RepositoryException.php @@ -1,14 +1,9 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Exceptions\Repository; -class RepositoryException extends \Exception +use Pterodactyl\Exceptions\PterodactylException; + +class RepositoryException extends PterodactylException { } diff --git a/app/Exceptions/Service/Allocation/ServerUsingAllocationException.php b/app/Exceptions/Service/Allocation/ServerUsingAllocationException.php new file mode 100644 index 000000000..93018ec90 --- /dev/null +++ b/app/Exceptions/Service/Allocation/ServerUsingAllocationException.php @@ -0,0 +1,9 @@ +. - * - * 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; + } } diff --git a/app/Exceptions/Service/Location/HasActiveNodesException.php b/app/Exceptions/Service/Location/HasActiveNodesException.php index 7960a3387..1270807b8 100644 --- a/app/Exceptions/Service/Location/HasActiveNodesException.php +++ b/app/Exceptions/Service/Location/HasActiveNodesException.php @@ -1,16 +1,17 @@ . - * - * 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; + } } diff --git a/app/Exceptions/Service/Node/ConfigurationNotPersistedException.php b/app/Exceptions/Service/Node/ConfigurationNotPersistedException.php new file mode 100644 index 000000000..e34e5290b --- /dev/null +++ b/app/Exceptions/Service/Node/ConfigurationNotPersistedException.php @@ -0,0 +1,9 @@ + $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 new file mode 100644 index 000000000..5bb0dd52b --- /dev/null +++ b/app/Extensions/Spatie/Fractalistic/Fractal.php @@ -0,0 +1,46 @@ +serializer)) { + $this->serializer = new PterodactylSerializer; + } + + // 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); + } + + // 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/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 @@ alert = $alert; + $this->keyCreationService = $keyCreationService; + $this->repository = $repository; + } + + /** + * Render view showing all of a user's application API keys. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\View\View + */ + public function index(Request $request): View + { + return view('admin.api.index', [ + 'keys' => $this->repository->getApplicationKeys($request->user()), + ]); + } + + /** + * Render view allowing an admin to create a new application API key. + * + * @return \Illuminate\View\View + */ + public function create(): View + { + $resources = AdminAcl::getResourceList(); + sort($resources); + + return view('admin.api.new', [ + 'resources' => $resources, + 'permissions' => [ + 'r' => AdminAcl::READ, + 'rw' => AdminAcl::READ | AdminAcl::WRITE, + 'n' => AdminAcl::NONE, + ], + ]); + } + + /** + * Store the new key and redirect the user back to the application key listing. + * + * @param \Pterodactyl\Http\Requests\Admin\Api\StoreApplicationApiKeyRequest $request + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function store(StoreApplicationApiKeyRequest $request): RedirectResponse + { + $this->keyCreationService->setKeyType(ApiKey::TYPE_APPLICATION)->handle([ + 'memo' => $request->input('memo'), + 'user_id' => $request->user()->id, + ], $request->getKeyPermissions()); + + $this->alert->success('A new application API key has been generated for your account.')->flash(); + + return redirect()->route('admin.api.index'); + } + + /** + * Delete an application API key from the database. + * + * @param \Illuminate\Http\Request $request + * @param string $identifier + * @return \Illuminate\Http\Response + */ + public function delete(Request $request, string $identifier): Response + { + $this->repository->deleteApplicationKey($request->user(), $identifier); + + return response('', 204); + } +} diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index e5d50fb40..511eb4393 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -12,6 +12,8 @@ namespace Pterodactyl\Http\Controllers\Admin; use Javascript; use Illuminate\Http\Request; use Pterodactyl\Models\Node; +use Illuminate\Http\Response; +use Pterodactyl\Models\Allocation; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Nodes\NodeUpdateService; @@ -23,6 +25,7 @@ use Pterodactyl\Services\Helpers\SoftwareVersionService; use Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Http\Requests\Admin\Node\AllocationFormRequest; +use Pterodactyl\Services\Allocations\AllocationDeletionService; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Http\Requests\Admin\Node\AllocationAliasFormRequest; @@ -78,11 +81,16 @@ class NodesController extends Controller * @var \Pterodactyl\Services\Helpers\SoftwareVersionService */ protected $versionService; + /** + * @var \Pterodactyl\Services\Allocations\AllocationDeletionService + */ + private $allocationDeletionService; /** * NodesController constructor. * * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Services\Allocations\AllocationDeletionService $allocationDeletionService * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository * @param \Pterodactyl\Services\Allocations\AssignmentService $assignmentService * @param \Illuminate\Cache\Repository $cache @@ -95,6 +103,7 @@ class NodesController extends Controller */ public function __construct( AlertsMessageBag $alert, + AllocationDeletionService $allocationDeletionService, AllocationRepositoryInterface $allocationRepository, AssignmentService $assignmentService, CacheRepository $cache, @@ -106,6 +115,7 @@ class NodesController extends Controller SoftwareVersionService $versionService ) { $this->alert = $alert; + $this->allocationDeletionService = $allocationDeletionService; $this->allocationRepository = $allocationRepository; $this->assignmentService = $assignmentService; $this->cache = $cache; @@ -262,17 +272,14 @@ class NodesController extends Controller /** * Removes a single allocation from a node. * - * @param int $node - * @param int $allocation - * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse + * @param \Pterodactyl\Models\Allocation $allocation + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException */ - public function allocationRemoveSingle($node, $allocation) + public function allocationRemoveSingle(Allocation $allocation): Response { - $this->allocationRepository->deleteWhere([ - ['id', '=', $allocation], - ['node_id', '=', $node], - ['server_id', '=', null], - ]); + $this->allocationDeletionService->handle($allocation); return response('', 204); } diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index c3e33b964..5ec2c14d9 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -251,14 +251,17 @@ class ServersController extends Controller * @param \Pterodactyl\Http\Requests\Admin\ServerFormRequest $request * @return \Illuminate\Http\RedirectResponse * + * @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 \Illuminate\Validation\ValidationException + * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException + * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException */ public function store(ServerFormRequest $request) { - $server = $this->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); @@ -401,7 +404,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/ApplicationApiController.php b/app/Http/Controllers/Api/Application/ApplicationApiController.php new file mode 100644 index 000000000..c932c3644 --- /dev/null +++ b/app/Http/Controllers/Api/Application/ApplicationApiController.php @@ -0,0 +1,76 @@ +call([$this, 'loadDependencies']); + + // Parse all of the includes to use on this request. + $includes = collect(explode(',', $this->request->input('include', '')))->map(function ($value) { + return trim($value); + })->filter()->toArray(); + + $this->fractal->parseIncludes($includes); + $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. + * + * @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; + } + + /** + * 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/Locations/LocationController.php b/app/Http/Controllers/Api/Application/Locations/LocationController.php new file mode 100644 index 000000000..42bb2a08c --- /dev/null +++ b/app/Http/Controllers/Api/Application/Locations/LocationController.php @@ -0,0 +1,145 @@ +creationService = $creationService; + $this->deletionService = $deletionService; + $this->repository = $repository; + $this->updateService = $updateService; + } + + /** + * Return all of the locations currently registered on the Panel. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Locations\GetLocationsRequest $request + * @return array + */ + public function index(GetLocationsRequest $request): array + { + $locations = $this->repository->paginated(100); + + return $this->fractal->collection($locations) + ->transformWith($this->getTransformer(LocationTransformer::class)) + ->toArray(); + } + + /** + * Return a single location. + * + * @param \Pterodactyl\Http\Controllers\Api\Application\Locations\GetLocationRequest $request + * @return array + */ + public function view(GetLocationRequest $request): array + { + return $this->fractal->item($request->getModel(Location::class)) + ->transformWith($this->getTransformer(LocationTransformer::class)) + ->toArray(); + } + + /** + * Store a new location on the Panel and return a HTTP/201 response code with the + * new location attached. + * + * @param \Pterodactyl\Http\Controllers\Api\Application\Locations\StoreLocationRequest $request + * @return \Illuminate\Http\JsonResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function store(StoreLocationRequest $request): JsonResponse + { + $location = $this->creationService->handle($request->validated()); + + return $this->fractal->item($location) + ->transformWith($this->getTransformer(LocationTransformer::class)) + ->addMeta([ + 'resource' => route('api.application.locations.view', [ + 'location' => $location->id, + ]), + ]) + ->respond(201); + } + + /** + * Update a location on the Panel and return the updated record to the user. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Locations\UpdateLocationRequest $request + * @return array + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function update(UpdateLocationRequest $request): array + { + $location = $this->updateService->handle($request->getModel(Location::class), $request->validated()); + + return $this->fractal->item($location) + ->transformWith($this->getTransformer(LocationTransformer::class)) + ->toArray(); + } + + /** + * Delete a location from the Panel. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Locations\DeleteLocationRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Service\Location\HasActiveNodesException + */ + public function delete(DeleteLocationRequest $request): Response + { + $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 new file mode 100644 index 000000000..b63f3e203 --- /dev/null +++ b/app/Http/Controllers/Api/Application/Nodes/AllocationController.php @@ -0,0 +1,99 @@ +assignmentService = $assignmentService; + $this->deletionService = $deletionService; + $this->repository = $repository; + } + + /** + * Return all of the allocations that exist for a given node. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Allocations\GetAllocationsRequest $request + * @return array + */ + public function index(GetAllocationsRequest $request): array + { + $allocations = $this->repository->getPaginatedAllocationsForNode( + $request->getModel(Node::class)->id, 50 + ); + + return $this->fractal->collection($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\DeleteAllocationRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException + */ + public function delete(DeleteAllocationRequest $request): Response + { + $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 new file mode 100644 index 000000000..2e1c76a5c --- /dev/null +++ b/app/Http/Controllers/Api/Application/Nodes/NodeController.php @@ -0,0 +1,151 @@ +repository = $repository; + $this->creationService = $creationService; + $this->deletionService = $deletionService; + $this->updateService = $updateService; + } + + /** + * Return all of the nodes currently available on the Panel. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Nodes\GetNodesRequest $request + * @return array + */ + public function index(GetNodesRequest $request): array + { + $nodes = $this->repository->paginated(50); + + return $this->fractal->collection($nodes) + ->transformWith($this->getTransformer(NodeTransformer::class)) + ->toArray(); + } + + /** + * Return data for a single instance of a node. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Nodes\GetNodeRequest $request + * @return array + */ + public function view(GetNodeRequest $request): array + { + return $this->fractal->item($request->getModel(Node::class)) + ->transformWith($this->getTransformer(NodeTransformer::class)) + ->toArray(); + } + + /** + * Create a new node on the Panel. Returns the created node and a HTTP/201 + * status response on success. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Nodes\StoreNodeRequest $request + * @return \Illuminate\Http\JsonResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function store(StoreNodeRequest $request): JsonResponse + { + $node = $this->creationService->handle($request->validated()); + + return $this->fractal->item($node) + ->transformWith($this->getTransformer(NodeTransformer::class)) + ->addMeta([ + 'resource' => route('api.application.nodes.view', [ + 'node' => $node->id, + ]), + ]) + ->respond(201); + } + + /** + * Update an existing node on the Panel. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Nodes\UpdateNodeRequest $request + * @return array + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function update(UpdateNodeRequest $request): array + { + $node = $this->updateService->returnUpdatedModel()->handle( + $request->getModel(Node::class), $request->validated() + ); + + return $this->fractal->item($node) + ->transformWith($this->getTransformer(NodeTransformer::class)) + ->toArray(); + } + + /** + * Deletes a given node from the Panel as long as there are no servers + * currently attached to it. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Nodes\DeleteNodeRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException + */ + public function delete(DeleteNodeRequest $request): Response + { + $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 new file mode 100644 index 000000000..05512a4ee --- /dev/null +++ b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php @@ -0,0 +1,140 @@ +databaseManagementService = $databaseManagementService; + $this->databasePasswordService = $databasePasswordService; + $this->repository = $repository; + } + + /** + * Return a listing of all databases currently available to a single + * server. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabasesRequest $request + * @return array + */ + public function index(GetServerDatabasesRequest $request): array + { + $databases = $this->repository->getDatabasesForServer($request->getModel(Server::class)->id); + + return $this->fractal->collection($databases) + ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) + ->toArray(); + } + + /** + * Return a single server database. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabaseRequest $request + * @return array + */ + public function view(GetServerDatabaseRequest $request): array + { + return $this->fractal->item($request->getModel(Database::class)) + ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) + ->toArray(); + } + + /** + * Reset the password for a specific server 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): Response + { + $this->databasePasswordService->handle($request->getModel(Database::class), 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 + * @return \Illuminate\Http\JsonResponse + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + 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); + } + + /** + * Handle a request to delete a specific server database from the Panel. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\ServerDatabaseWriteRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function delete(ServerDatabaseWriteRequest $request): Response + { + $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 new file mode 100644 index 000000000..fd6d62f89 --- /dev/null +++ b/app/Http/Controllers/Api/Application/Servers/ServerController.php @@ -0,0 +1,119 @@ +creationService = $creationService; + $this->deletionService = $deletionService; + $this->repository = $repository; + } + + /** + * 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->setSearchTerm($request->input('search'))->paginated(50); + + return $this->fractal->collection($servers) + ->transformWith($this->getTransformer(ServerTransformer::class)) + ->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. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest $request + * @return array + */ + public function view(ServerWriteRequest $request): array + { + return $this->fractal->item($request->getModel(Server::class)) + ->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/ServerDetailsController.php b/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php new file mode 100644 index 000000000..e544c138a --- /dev/null +++ b/app/Http/Controllers/Api/Application/Servers/ServerDetailsController.php @@ -0,0 +1,80 @@ +buildModificationService = $buildModificationService; + $this->detailsModificationService = $detailsModificationService; + } + + /** + * Update the details for a specific server. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\UpdateServerDetailsRequest $request + * @return array + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function details(UpdateServerDetailsRequest $request): array + { + $server = $this->detailsModificationService->returnUpdatedModel()->handle( + $request->getModel(Server::class), $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 + * @return array + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function build(UpdateServerBuildConfigurationRequest $request): array + { + $server = $this->buildModificationService->handle($request->getModel(Server::class), $request->validated()); + + return $this->fractal->item($server) + ->transformWith($this->getTransformer(ServerTransformer::class)) + ->toArray(); + } +} 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..9ab324d7e --- /dev/null +++ b/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php @@ -0,0 +1,114 @@ +rebuildService = $rebuildService; + $this->reinstallServerService = $reinstallServerService; + $this->suspensionService = $suspensionService; + } + + /** + * Suspend a server on the Panel. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function suspend(ServerWriteRequest $request): Response + { + $this->suspensionService->toggle($request->getModel(Server::class), SuspensionService::ACTION_SUSPEND); + + return $this->returnNoContent(); + } + + /** + * Unsuspend a server on the Panel. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function unsuspend(ServerWriteRequest $request): Response + { + $this->suspensionService->toggle($request->getModel(Server::class), SuspensionService::ACTION_UNSUSPEND); + + return $this->returnNoContent(); + } + + /** + * Mark a server as needing to be reinstalled. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function reinstall(ServerWriteRequest $request): Response + { + $this->reinstallServerService->reinstall($request->getModel(Server::class)); + + 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 + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + */ + public function rebuild(ServerWriteRequest $request): Response + { + $this->rebuildService->handle($request->getModel(Server::class)); + + return $this->returnNoContent(); + } +} 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/Controllers/Api/Application/Users/UserController.php b/app/Http/Controllers/Api/Application/Users/UserController.php new file mode 100644 index 000000000..96d8c986f --- /dev/null +++ b/app/Http/Controllers/Api/Application/Users/UserController.php @@ -0,0 +1,179 @@ +creationService = $creationService; + $this->deletionService = $deletionService; + $this->repository = $repository; + $this->updateService = $updateService; + } + + /** + * Handle request to list all users on the panel. Returns a JSON-API representation + * of a collection of users including any defined relations passed in + * the request. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Users\GetUsersRequest $request + * @return array + */ + public function index(GetUsersRequest $request): array + { + $users = $this->repository->paginated(100); + + return $this->fractal->collection($users) + ->transformWith($this->getTransformer(UserTransformer::class)) + ->toArray(); + } + + /** + * Handle a request to view a single user. Includes any relations that + * were defined in the request. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Users\GetUserRequest $request + * @return array + */ + public function view(GetUserRequest $request): array + { + return $this->fractal->item($request->getModel(User::class)) + ->transformWith($this->getTransformer(UserTransformer::class)) + ->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\Api\Application\Users\UpdateUserRequest $request + * @return array + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function update(UpdateUserRequest $request): array + { + $this->updateService->setUserLevel(User::USER_LEVEL_ADMIN); + $collection = $this->updateService->handle($request->getModel(User::class), $request->validated()); + + $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]; + } + } + + $response = $this->fractal->item($collection->get('model')) + ->transformWith($this->getTransformer(UserTransformer::class)); + + if (count($errors) > 0) { + $response->addMeta([ + 'revocation_errors' => $errors, + ]); + } + + return $response->toArray(); + } + + /** + * Store a new user on the system. Returns the created user and a HTTP/201 + * header on successful creation. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Users\StoreUserRequest $request + * @return \Illuminate\Http\JsonResponse + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function store(StoreUserRequest $request): JsonResponse + { + $user = $this->creationService->handle($request->validated()); + + return $this->fractal->item($user) + ->transformWith($this->getTransformer(UserTransformer::class)) + ->addMeta([ + 'resource' => route('api.application.users.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\Http\Requests\Api\Application\Users\DeleteUserRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function delete(DeleteUserRequest $request): Response + { + $this->deletionService->handle($request->getModel(User::class)); + + return response('', 204); + } +} diff --git a/app/Http/Controllers/Base/APIController.php b/app/Http/Controllers/Base/AccountKeyController.php similarity index 58% rename from app/Http/Controllers/Base/APIController.php rename to app/Http/Controllers/Base/AccountKeyController.php index c73661777..04563ca8a 100644 --- a/app/Http/Controllers/Base/APIController.php +++ b/app/Http/Controllers/Base/AccountKeyController.php @@ -2,15 +2,17 @@ namespace Pterodactyl\Http\Controllers\Base; +use Illuminate\View\View; use Illuminate\Http\Request; +use Illuminate\Http\Response; +use Pterodactyl\Models\ApiKey; use Prologue\Alerts\AlertsMessageBag; -use Pterodactyl\Models\APIPermission; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Api\KeyCreationService; -use Pterodactyl\Http\Requests\Base\ApiKeyFormRequest; +use Pterodactyl\Http\Requests\Base\StoreAccountKeyRequest; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; -class APIController extends Controller +class AccountKeyController extends Controller { /** * @var \Prologue\Alerts\AlertsMessageBag @@ -45,57 +47,44 @@ class APIController extends Controller } /** - * Display base API index page. + * Display a listing of all account API keys. * * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function index(Request $request) + public function index(Request $request): View { return view('base.api.index', [ - 'keys' => $this->repository->findWhere([['user_id', '=', $request->user()->id]]), + 'keys' => $this->repository->getAccountKeys($request->user()), ]); } /** - * Display API key creation page. + * Display account API key creation page. * * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function create(Request $request) + public function create(Request $request): View { - return view('base.api.new', [ - 'permissions' => [ - 'user' => collect(APIPermission::CONST_PERMISSIONS)->pull('_user'), - 'admin' => ! $request->user()->root_admin ? null : collect(APIPermission::CONST_PERMISSIONS)->except('_user')->toArray(), - ], - ]); + return view('base.api.new'); } /** - * Handle saving new API key. + * Handle saving new account API key. * - * @param \Pterodactyl\Http\Requests\Base\ApiKeyFormRequest $request + * @param \Pterodactyl\Http\Requests\Base\StoreAccountKeyRequest $request * @return \Illuminate\Http\RedirectResponse * - * @throws \Exception * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function store(ApiKeyFormRequest $request) + public function store(StoreAccountKeyRequest $request) { - $adminPermissions = []; - if ($request->user()->root_admin) { - $adminPermissions = $request->input('admin_permissions', []); - } - - $secret = $this->keyService->handle([ + $this->keyService->setKeyType(ApiKey::TYPE_ACCOUNT)->handle([ 'user_id' => $request->user()->id, 'allowed_ips' => $request->input('allowed_ips'), 'memo' => $request->input('memo'), - ], $request->input('permissions', []), $adminPermissions); + ]); $this->alert->success(trans('base.api.index.keypair_created'))->flash(); @@ -103,18 +92,15 @@ class APIController extends Controller } /** - * @param \Illuminate\Http\Request $request - * @param string $key - * @return \Illuminate\Http\Response + * Delete an account API key from the Panel via an AJAX request. * - * @throws \Exception + * @param \Illuminate\Http\Request $request + * @param string $identifier + * @return \Illuminate\Http\Response */ - public function revoke(Request $request, $key) + public function revoke(Request $request, string $identifier): Response { - $this->repository->deleteWhere([ - ['user_id', '=', $request->user()->id], - ['token', '=', $key], - ]); + $this->repository->deleteAccountKey($request->user(), $identifier); return response('', 204); } diff --git a/app/Http/Controllers/Server/Files/RemoteRequestController.php b/app/Http/Controllers/Server/Files/RemoteRequestController.php index f792e3ccd..ab58037d0 100644 --- a/app/Http/Controllers/Server/Files/RemoteRequestController.php +++ b/app/Http/Controllers/Server/Files/RemoteRequestController.php @@ -68,7 +68,7 @@ class RemoteRequestController extends Controller try { $listing = $this->repository->setServer($server)->setToken($request->attributes->get('server_token'))->getDirectory($requestDirectory); } catch (RequestException $exception) { - throw new DaemonConnectionException($exception); + throw new DaemonConnectionException($exception, true); } return view('server.files.list', [ diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 9f4184884..1d33e210d 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -14,25 +14,26 @@ use Pterodactyl\Http\Middleware\AdminAuthenticate; 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 Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings; use Illuminate\Foundation\Http\Middleware\ValidatePostSize; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; -use Pterodactyl\Http\Middleware\API\HasPermissionToResource; use Pterodactyl\Http\Middleware\Server\AuthenticateAsSubuser; +use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate; use Pterodactyl\Http\Middleware\Server\SubuserBelongsToServer; use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication; use Pterodactyl\Http\Middleware\Server\DatabaseBelongsToServer; use Pterodactyl\Http\Middleware\Server\ScheduleBelongsToServer; +use Pterodactyl\Http\Middleware\Api\Application\AuthenticateKey; +use Pterodactyl\Http\Middleware\Api\Application\AuthenticateUser; +use Pterodactyl\Http\Middleware\Api\Application\SetSessionDriver; use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode; use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull; +use Pterodactyl\Http\Middleware\Api\Application\AuthenticateIPAccess; use Pterodactyl\Http\Middleware\DaemonAuthenticate as OldDaemonAuthenticate; class Kernel extends HttpKernel @@ -67,10 +68,11 @@ class Kernel extends HttpKernel RequireTwoFactorAuthentication::class, ], 'api' => [ - 'throttle:60,1', - SubstituteBindings::class, + 'throttle:120,1', + ApiSubstituteBindings::class, SetSessionDriver::class, AuthenticateKey::class, + AuthenticateUser::class, AuthenticateIPAccess::class, ], 'daemon' => [ @@ -98,9 +100,6 @@ 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 deleted file mode 100644 index 1d99ffbf7..000000000 --- a/app/Http/Middleware/API/HasPermissionToResource.php +++ /dev/null @@ -1,58 +0,0 @@ -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/Api/ApiSubstituteBindings.php b/app/Http/Middleware/Api/ApiSubstituteBindings.php new file mode 100644 index 000000000..41ca8ee70 --- /dev/null +++ b/app/Http/Middleware/Api/ApiSubstituteBindings.php @@ -0,0 +1,74 @@ + 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. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle($request, Closure $next) + { + $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 + // 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); + } + + /** + * Return the registered mappings. + * + * @return array + */ + public static function getMappings() + { + return self::$mappings; + } +} diff --git a/app/Http/Middleware/API/AuthenticateIPAccess.php b/app/Http/Middleware/Api/Application/AuthenticateIPAccess.php similarity index 95% rename from app/Http/Middleware/API/AuthenticateIPAccess.php rename to app/Http/Middleware/Api/Application/AuthenticateIPAccess.php index aa0af7e2e..6988c637d 100644 --- a/app/Http/Middleware/API/AuthenticateIPAccess.php +++ b/app/Http/Middleware/Api/Application/AuthenticateIPAccess.php @@ -1,6 +1,6 @@ auth = $auth; + $this->encrypter = $encrypter; $this->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. + * is in a valid format and exists in the database. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed * - * @throws \Symfony\Component\HttpKernel\Exception\HttpException - * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function handle(Request $request, Closure $next) { @@ -54,14 +61,26 @@ class AuthenticateKey throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']); } + $raw = $request->bearerToken(); + $identifier = substr($raw, 0, ApiKey::IDENTIFIER_LENGTH); + $token = substr($raw, ApiKey::IDENTIFIER_LENGTH); + try { - $model = $this->repository->findFirstWhere([['token', '=', $request->bearerToken()]]); + $model = $this->repository->findFirstWhere([ + ['identifier', '=', $identifier], + ['key_type', '=', ApiKey::TYPE_APPLICATION], + ]); } catch (RecordNotFoundException $exception) { throw new AccessDeniedHttpException; } + if (! hash_equals($this->encrypter->decrypt($model->token), $token)) { + throw new AccessDeniedHttpException; + } + $this->auth->guard()->loginUsingId($model->user_id); $request->attributes->set('api_key', $model); + $this->repository->withoutFreshModel()->update($model->id, ['last_used_at' => Chronos::now()]); return $next($request); } diff --git a/app/Http/Middleware/Api/Application/AuthenticateUser.php b/app/Http/Middleware/Api/Application/AuthenticateUser.php new file mode 100644 index 000000000..5bbce8296 --- /dev/null +++ b/app/Http/Middleware/Api/Application/AuthenticateUser.php @@ -0,0 +1,27 @@ +user()) || ! $request->user()->root_admin) { + throw new AccessDeniedHttpException; + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/API/SetSessionDriver.php b/app/Http/Middleware/Api/Application/SetSessionDriver.php similarity index 95% rename from app/Http/Middleware/API/SetSessionDriver.php rename to app/Http/Middleware/Api/Application/SetSessionDriver.php index 9cc5d60e3..c4660ec9b 100644 --- a/app/Http/Middleware/API/SetSessionDriver.php +++ b/app/Http/Middleware/Api/Application/SetSessionDriver.php @@ -1,6 +1,6 @@ . - * - * 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\Middleware\Daemon; +namespace Pterodactyl\Http\Middleware\Api\Daemon; use Closure; use Illuminate\Http\Request; diff --git a/app/Http/Requests/Admin/Api/StoreApplicationApiKeyRequest.php b/app/Http/Requests/Admin/Api/StoreApplicationApiKeyRequest.php new file mode 100644 index 000000000..372dd115d --- /dev/null +++ b/app/Http/Requests/Admin/Api/StoreApplicationApiKeyRequest.php @@ -0,0 +1,39 @@ +mapWithKeys(function ($resource) use ($modelRules) { + return [AdminAcl::COLUMN_IDENTIFER . $resource => $modelRules['r_' . $resource]]; + })->merge(['memo' => $modelRules['memo']])->toArray(); + } + + /** + * @return array + */ + public function attributes() + { + return [ + 'memo' => 'Description', + ]; + } + + public function getKeyPermissions(): array + { + return collect($this->validated())->filter(function ($value, $key) { + return substr($key, 0, strlen(AdminAcl::COLUMN_IDENTIFER)) === AdminAcl::COLUMN_IDENTIFER; + })->toArray(); + } +} diff --git a/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php b/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php new file mode 100644 index 000000000..21da6bfb3 --- /dev/null +++ b/app/Http/Requests/Api/Application/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/Application/Allocations/GetAllocationsRequest.php b/app/Http/Requests/Api/Application/Allocations/GetAllocationsRequest.php new file mode 100644 index 000000000..150bdb95e --- /dev/null +++ b/app/Http/Requests/Api/Application/Allocations/GetAllocationsRequest.php @@ -0,0 +1,33 @@ +route()->parameter('node'); + + return $node instanceof Node && $node->exists; + } +} 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 new file mode 100644 index 000000000..aeb03dfbe --- /dev/null +++ b/app/Http/Requests/Api/Application/ApplicationApiRequest.php @@ -0,0 +1,125 @@ +resource)) { + throw new PterodactylException('An ACL resource must be defined on API requests.'); + } + + return AdminAcl::check($this->key(), $this->resource, $this->permission); + } + + /** + * Determine if the requested resource exists on the server. + * + * @return bool + */ + public function resourceExists(): bool + { + return true; + } + + /** + * Default set of rules to apply to API requests. + * + * @return array + */ + public function rules(): array + { + return []; + } + + /** + * Return the API key being used for the request. + * + * @return \Pterodactyl\Models\ApiKey + */ + public function key(): ApiKey + { + 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. + * + * @return bool + * + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + */ + + /** + * @return bool + */ + protected function 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 ($this->attributes->get('is_missing_model', false) || ! $this->resourceExists()) { + throw new NotFoundHttpException('The requested resource does not exist on this server.'); + } + + return true; + } +} diff --git a/app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php b/app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php new file mode 100644 index 000000000..d1863eea7 --- /dev/null +++ b/app/Http/Requests/Api/Application/Locations/DeleteLocationRequest.php @@ -0,0 +1,32 @@ +route()->parameter('location'); + + return $location instanceof Location && $location->exists; + } +} diff --git a/app/Http/Requests/Api/Application/Locations/GetLocationRequest.php b/app/Http/Requests/Api/Application/Locations/GetLocationRequest.php new file mode 100644 index 000000000..fa6c67a86 --- /dev/null +++ b/app/Http/Requests/Api/Application/Locations/GetLocationRequest.php @@ -0,0 +1,21 @@ +route()->parameter('location'); + + return $location instanceof Location && $location->exists; + } +} diff --git a/app/Http/Requests/Api/Application/Locations/GetLocationsRequest.php b/app/Http/Requests/Api/Application/Locations/GetLocationsRequest.php new file mode 100644 index 000000000..5edf00462 --- /dev/null +++ b/app/Http/Requests/Api/Application/Locations/GetLocationsRequest.php @@ -0,0 +1,19 @@ +only([ + 'long', + 'short', + ])->toArray(); + } + + /** + * Rename fields to be more clear in error messages. + * + * @return array + */ + public function attributes() + { + return [ + 'long' => 'Location Description', + 'short' => 'Location Identifier', + ]; + } +} diff --git a/app/Http/Requests/Api/Application/Locations/UpdateLocationRequest.php b/app/Http/Requests/Api/Application/Locations/UpdateLocationRequest.php new file mode 100644 index 000000000..c65a6f904 --- /dev/null +++ b/app/Http/Requests/Api/Application/Locations/UpdateLocationRequest.php @@ -0,0 +1,36 @@ +route()->parameter('location'); + + return $location instanceof Location && $location->exists; + } + + /** + * Rules to validate this request aganist. + * + * @return array + */ + public function rules(): array + { + $locationId = $this->route()->parameter('location')->id; + + return collect(Location::getUpdateRulesForId($locationId))->only([ + 'short', + 'long', + ]); + } +} 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 @@ +route()->parameter('node'); + + return $node instanceof Node && $node->exists; + } +} diff --git a/app/Http/Requests/Api/Application/Nodes/GetNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/GetNodeRequest.php new file mode 100644 index 000000000..fbf957edd --- /dev/null +++ b/app/Http/Requests/Api/Application/Nodes/GetNodeRequest.php @@ -0,0 +1,20 @@ +route()->parameter('node'); + + return $node instanceof Node && $node->exists; + } +} diff --git a/app/Http/Requests/Api/Application/Nodes/GetNodesRequest.php b/app/Http/Requests/Api/Application/Nodes/GetNodesRequest.php new file mode 100644 index 000000000..fc5f5a38e --- /dev/null +++ b/app/Http/Requests/Api/Application/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/Application/Nodes/UpdateNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/UpdateNodeRequest.php new file mode 100644 index 000000000..f6e92f812 --- /dev/null +++ b/app/Http/Requests/Api/Application/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/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/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/Servers/ServerWriteRequest.php b/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php new file mode 100644 index 000000000..728b1ce52 --- /dev/null +++ b/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php @@ -0,0 +1,32 @@ +route()->parameter('server'); + + return $server instanceof Server && $server->exists; + } +} 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/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/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerDetailsRequest.php new file mode 100644 index 000000000..4b75138b9 --- /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' => array_merge(['nullable'], $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/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/Http/Requests/Api/Application/Users/DeleteUserRequest.php b/app/Http/Requests/Api/Application/Users/DeleteUserRequest.php new file mode 100644 index 000000000..571b29c63 --- /dev/null +++ b/app/Http/Requests/Api/Application/Users/DeleteUserRequest.php @@ -0,0 +1,32 @@ +route()->parameter('user'); + + return $user instanceof User && $user->exists; + } +} diff --git a/app/Http/Requests/Api/Application/Users/GetUserRequest.php b/app/Http/Requests/Api/Application/Users/GetUserRequest.php new file mode 100644 index 000000000..0c96e3bb4 --- /dev/null +++ b/app/Http/Requests/Api/Application/Users/GetUserRequest.php @@ -0,0 +1,20 @@ +route()->parameter('user'); + + return $user instanceof User && $user->exists; + } +} diff --git a/app/Http/Requests/Api/Application/Users/GetUsersRequest.php b/app/Http/Requests/Api/Application/Users/GetUsersRequest.php new file mode 100644 index 000000000..8736a8e9d --- /dev/null +++ b/app/Http/Requests/Api/Application/Users/GetUsersRequest.php @@ -0,0 +1,19 @@ +only([ + 'external_id', + 'email', + 'username', + 'name_first', + 'name_last', + 'password', + 'language', + 'root_admin', + ])->toArray(); + } + + /** + * Rename some fields to be more user friendly. + * + * @return array + */ + public function attributes() + { + return [ + 'external_id' => 'Third Party Identifier', + 'name_first' => 'First Name', + 'name_last' => 'Last Name', + 'root_admin' => 'Root Administrator Status', + ]; + } +} diff --git a/app/Http/Requests/Api/Application/Users/UpdateUserRequest.php b/app/Http/Requests/Api/Application/Users/UpdateUserRequest.php new file mode 100644 index 000000000..10b6c54a5 --- /dev/null +++ b/app/Http/Requests/Api/Application/Users/UpdateUserRequest.php @@ -0,0 +1,41 @@ +route()->parameter('user'); + + return $user instanceof User && $user->exists; + } + + /** + * Return the validation rules for this request. + * + * @return array + */ + public function rules(): array + { + $userId = $this->route()->parameter('user')->id; + + return collect(User::getUpdateRulesForId($userId))->only([ + 'external_id', + 'email', + 'username', + 'name_first', + 'name_last', + 'password', + 'language', + 'root_admin', + ])->toArray(); + } +} diff --git a/app/Http/Requests/API/Remote/SftpAuthenticationFormRequest.php b/app/Http/Requests/Api/Remote/SftpAuthenticationFormRequest.php similarity index 82% rename from app/Http/Requests/API/Remote/SftpAuthenticationFormRequest.php rename to app/Http/Requests/Api/Remote/SftpAuthenticationFormRequest.php index 5d82f55c7..041ff197f 100644 --- a/app/Http/Requests/API/Remote/SftpAuthenticationFormRequest.php +++ b/app/Http/Requests/Api/Remote/SftpAuthenticationFormRequest.php @@ -1,10 +1,10 @@ 'required|nullable|string|max:500', + 'allowed_ips' => 'present', + 'allowed_ips.*' => 'sometimes|string', + ]; + } +} diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php deleted file mode 100644 index a9771726f..000000000 --- a/app/Http/Requests/Request.php +++ /dev/null @@ -1,9 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Models; - -use Sofa\Eloquence\Eloquence; -use Sofa\Eloquence\Validable; -use Illuminate\Database\Eloquent\Model; -use Sofa\Eloquence\Contracts\CleansAttributes; -use Sofa\Eloquence\Contracts\Validable as ValidableContract; - -class APIKey extends Model implements CleansAttributes, ValidableContract -{ - use Eloquence, Validable; - - const KEY_LENGTH = 32; - - /** - * The table associated with the model. - * - * @var string - */ - protected $table = 'api_keys'; - - /** - * Cast values to correct type. - * - * @var array - */ - protected $casts = [ - 'allowed_ips' => 'json', - ]; - - /** - * Fields that are not mass assignable. - * - * @var array - */ - protected $guarded = ['id', 'created_at', 'updated_at']; - - /** - * Rules defining what fields must be passed when making a model. - * - * @var array - */ - protected static $applicationRules = [ - 'memo' => 'required', - 'user_id' => 'required', - 'token' => 'required', - ]; - - /** - * Rules to protect aganist invalid data entry to DB. - * - * @var array - */ - protected static $dataIntegrityRules = [ - 'user_id' => 'exists:users,id', - 'token' => 'string|size:32', - 'memo' => 'nullable|string|max:500', - 'allowed_ips' => 'nullable|json', - 'expires_at' => 'nullable|datetime', - ]; - - /** - * Gets the permissions associated with a key. - * - * @return \Illuminate\Database\Eloquent\Relations\HasMany - */ - public function permissions() - { - return $this->hasMany(APIPermission::class, 'key_id'); - } -} 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/APIPermission.php b/app/Models/APIPermission.php deleted file mode 100644 index 1b2eb05e2..000000000 --- a/app/Models/APIPermission.php +++ /dev/null @@ -1,126 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Models; - -use Sofa\Eloquence\Eloquence; -use Sofa\Eloquence\Validable; -use Illuminate\Database\Eloquent\Model; -use Sofa\Eloquence\Contracts\CleansAttributes; -use Sofa\Eloquence\Contracts\Validable as ValidableContract; - -class APIPermission extends Model implements CleansAttributes, ValidableContract -{ - use Eloquence, Validable; - - /** - * List of permissions available for the API. - */ - const CONST_PERMISSIONS = [ - // Items within this block are available to non-adminitrative users. - '_user' => [ - 'server' => [ - 'list', - 'view', - 'power', - 'command', - ], - ], - - // All other pemissions below are administrative actions. - 'server' => [ - 'list', - 'create', - 'view', - 'edit-details', - 'edit-container', - 'edit-build', - 'edit-startup', - 'suspend', - 'install', - 'rebuild', - 'delete', - ], - 'location' => [ - 'list', - ], - 'node' => [ - 'list', - 'view', - 'view-config', - 'create', - 'delete', - ], - 'user' => [ - 'list', - 'view', - 'create', - 'edit', - 'delete', - ], - 'service' => [ - 'list', - 'view', - ], - 'option' => [ - 'list', - 'view', - ], - 'pack' => [ - 'list', - 'view', - ], - ]; - - /** - * The table associated with the model. - * - * @var string - */ - protected $table = 'api_permissions'; - - /** - * Fields that are not mass assignable. - * - * @var array - */ - protected $guarded = ['id']; - - /** - * Cast values to correct type. - * - * @var array - */ - protected $casts = [ - 'key_id' => 'integer', - ]; - - protected static $dataIntegrityRules = [ - 'key_id' => 'required|numeric', - 'permission' => 'required|string|max:200', - ]; - - /** - * Disable timestamps for this table. - * - * @var bool - */ - public $timestamps = false; - - /** - * Return permissions for API. - * - * @return array - * @deprecated - */ - public static function permissions() - { - return []; - } -} diff --git a/app/Models/Allocation.php b/app/Models/Allocation.php index 2fce57e84..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. * @@ -105,4 +104,14 @@ class Allocation extends Model implements CleansAttributes, ValidableContract { return $this->belongsTo(Server::class); } + + /** + * Return the Node model associated with this allocation. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function node() + { + return $this->belongsTo(Node::class); + } } diff --git a/app/Models/ApiKey.php b/app/Models/ApiKey.php new file mode 100644 index 000000000..bd05454dd --- /dev/null +++ b/app/Models/ApiKey.php @@ -0,0 +1,130 @@ + 'json', + 'user_id' => 'int', + 'r_' . AdminAcl::RESOURCE_USERS => 'int', + 'r_' . AdminAcl::RESOURCE_ALLOCATIONS => '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', + 'r_' . AdminAcl::RESOURCE_NODES => 'int', + 'r_' . AdminAcl::RESOURCE_PACKS => 'int', + 'r_' . AdminAcl::RESOURCE_SERVERS => 'int', + ]; + + /** + * Fields that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'identifier', + 'token', + 'allowed_ips', + 'memo', + 'last_used_at', + ]; + + /** + * Fields that should not be included when calling toArray() or toJson() + * on this model. + * + * @var array + */ + protected $hidden = ['token']; + + /** + * Rules defining what fields must be passed when making a model. + * + * @var array + */ + protected static $applicationRules = [ + 'identifier' => 'required', + 'memo' => 'required', + 'user_id' => 'required', + 'token' => 'required', + 'key_type' => 'present', + ]; + + /** + * Rules to protect aganist invalid data entry to DB. + * + * @var array + */ + protected static $dataIntegrityRules = [ + 'user_id' => 'exists:users,id', + 'key_type' => 'integer|min:0|max:4', + 'identifier' => 'string|size:16|unique:api_keys,identifier', + 'token' => 'string', + 'memo' => 'nullable|string|max:500', + 'allowed_ips' => 'nullable|json', + '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_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', + 'r_' . AdminAcl::RESOURCE_NODES => 'integer|min:0|max:3', + 'r_' . AdminAcl::RESOURCE_PACKS => 'integer|min:0|max:3', + 'r_' . AdminAcl::RESOURCE_SERVERS => 'integer|min:0|max:3', + ]; + + /** + * @var array + */ + protected $dates = [ + self::CREATED_AT, + self::UPDATED_AT, + 'last_used_at', + ]; +} 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 62c4eb47d..c680a54da 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.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 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/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/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 9f81584c8..ac3bd64f3 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; @@ -22,6 +15,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. * @@ -67,14 +66,16 @@ 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', - 'name' => 'regex:/^([\w .-]{1,200})$/', + 'owner_id' => 'integer|exists:users,id', + 'name' => 'string|min:1|max:255', 'node_id' => 'exists:nodes,id', 'description' => 'string', 'memory' => 'numeric|min:0', @@ -86,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/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 af574b597..29754eff8 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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; @@ -33,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. * @@ -87,7 +97,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. @@ -122,6 +132,7 @@ class User extends Model implements protected static $applicationRules = [ 'uuid' => 'required', 'email' => 'required', + 'external_id' => 'sometimes', 'username' => 'required', 'name_first' => 'required', 'name_last' => 'required', @@ -138,16 +149,29 @@ class User extends Model implements protected static $dataIntegrityRules = [ 'uuid' => 'string|size:36|unique:users,uuid', 'email' => 'email|unique:users,email', + 'external_id' => 'nullable|string|max:255|unique:users,external_id', 'username' => 'alpha_dash|between:1,255|unique:users,username', 'name_first' => 'string|between:1,255', '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. * diff --git a/app/Policies/APIKeyPolicy.php b/app/Policies/APIKeyPolicy.php deleted file mode 100644 index 69ce45c04..000000000 --- a/app/Policies/APIKeyPolicy.php +++ /dev/null @@ -1,57 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Policies; - -use Cache; -use Carbon; -use Pterodactyl\Models\User; -use Pterodactyl\Models\APIKey as Key; - -class APIKeyPolicy -{ - /** - * Checks if the API key has permission to perform an action. - * - * @param \Pterodactyl\Models\User $user - * @param \Pterodactyl\Models\APIKey $key - * @param string $permission - * @return bool - */ - protected function checkPermission(User $user, Key $key, $permission) - { - // Non-administrative users cannot use administrative routes. - if (! starts_with($key, 'user.') && ! $user->root_admin) { - return false; - } - - // We don't tag this cache key with the user uuid because the key is already unique, - // and multiple users are not defiend for a single key. - $permissions = Cache::remember('APIKeyPolicy.' . $key->public, Carbon::now()->addSeconds(5), function () use ($key) { - return $key->permissions()->get()->transform(function ($item) { - return $item->permission; - })->values(); - }); - - return $permissions->search($permission, true) !== false; - } - - /** - * Determine if a user has permission to perform this action against the system. - * - * @param \Pterodactyl\Models\User $user - * @param string $permission - * @param \Pterodactyl\Models\APIKey $key - * @return bool - */ - public function before(User $user, $permission, Key $key) - { - return $this->checkPermission($user, $key, $permission); - } -} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 0cdb82a29..947750ae8 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -13,7 +13,6 @@ class AuthServiceProvider extends ServiceProvider */ protected $policies = [ 'Pterodactyl\Models\Server' => 'Pterodactyl\Policies\ServerPolicy', - 'Pterodactyl\Models\APIKey' => 'Pterodactyl\Policies\APIKeyPolicy', ]; /** diff --git a/app/Providers/BladeServiceProvider.php b/app/Providers/BladeServiceProvider.php new file mode 100644 index 000000000..97b0df48e --- /dev/null +++ b/app/Providers/BladeServiceProvider.php @@ -0,0 +1,19 @@ +app->make('blade.compiler') + ->directive('datetimeHuman', function ($expression) { + return "setTimezone(config('app.timezone'))->toDateTimeString(); ?>"; + }); + } +} diff --git a/app/Providers/MacroServiceProvider.php b/app/Providers/MacroServiceProvider.php index 014e8f7af..ddfbf7aa8 100644 --- a/app/Providers/MacroServiceProvider.php +++ b/app/Providers/MacroServiceProvider.php @@ -13,7 +13,7 @@ use File; use Cache; use Carbon; use Request; -use Pterodactyl\Models\APIKey; +use Pterodactyl\Models\ApiKey; use Illuminate\Support\ServiceProvider; use Pterodactyl\Services\ApiKeyService; @@ -51,7 +51,7 @@ class MacroServiceProvider extends ServiceProvider 'ApiKeyMacro', 'ApiKeyMacro:Key:' . $parts[0], ])->remember('ApiKeyMacro.' . $parts[0], Carbon::now()->addMinutes(15), function () use ($parts) { - return APIKey::where('public', $parts[0])->first(); + return ApiKey::where('public', $parts[0])->first(); }); } diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index e34520962..aa5fbbaa5 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Providers; @@ -39,7 +32,6 @@ use Pterodactyl\Contracts\Repository\PackRepositoryInterface; use Pterodactyl\Contracts\Repository\TaskRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; -use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Repositories\Eloquent\ServerVariableRepository; @@ -56,7 +48,6 @@ use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; -use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\CommandRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; @@ -73,7 +64,6 @@ class RepositoryServiceProvider extends ServiceProvider // Eloquent Repositories $this->app->bind(AllocationRepositoryInterface::class, AllocationRepository::class); $this->app->bind(ApiKeyRepositoryInterface::class, ApiKeyRepository::class); - $this->app->bind(ApiPermissionRepositoryInterface::class, ApiPermissionRepository::class); $this->app->bind(DaemonKeyRepositoryInterface::class, DaemonKeyRepository::class); $this->app->bind(DatabaseRepositoryInterface::class, DatabaseRepository::class); $this->app->bind(DatabaseHostRepositoryInterface::class, DatabaseHostRepository::class); @@ -93,21 +83,11 @@ class RepositoryServiceProvider extends ServiceProvider $this->app->bind(TaskRepositoryInterface::class, TaskRepository::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class); - $this->app->alias(SettingsRepositoryInterface::class, 'settings'); - // Daemon Repositories - if ($this->app->make('config')->get('pterodactyl.daemon.use_new_daemon')) { - $this->app->bind(ConfigurationRepositoryInterface::class, \Pterodactyl\Repositories\Wings\ConfigurationRepository::class); - $this->app->bind(CommandRepositoryInterface::class, \Pterodactyl\Repositories\Wings\CommandRepository::class); - $this->app->bind(DaemonServerRepositoryInterface::class, \Pterodactyl\Repositories\Wings\ServerRepository::class); - $this->app->bind(FileRepositoryInterface::class, \Pterodactyl\Repositories\Wings\FileRepository::class); - $this->app->bind(PowerRepositoryInterface::class, \Pterodactyl\Repositories\Wings\PowerRepository::class); - } else { - $this->app->bind(ConfigurationRepositoryInterface::class, ConfigurationRepository::class); - $this->app->bind(CommandRepositoryInterface::class, CommandRepository::class); - $this->app->bind(DaemonServerRepositoryInterface::class, DaemonServerRepository::class); - $this->app->bind(FileRepositoryInterface::class, FileRepository::class); - $this->app->bind(PowerRepositoryInterface::class, PowerRepository::class); - } + $this->app->bind(ConfigurationRepositoryInterface::class, ConfigurationRepository::class); + $this->app->bind(CommandRepositoryInterface::class, CommandRepository::class); + $this->app->bind(DaemonServerRepositoryInterface::class, DaemonServerRepository::class); + $this->app->bind(FileRepositoryInterface::class, FileRepository::class); + $this->app->bind(PowerRepositoryInterface::class, PowerRepository::class); } } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index a0f902859..f9f6ac31d 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -16,23 +16,11 @@ 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. */ public function map() { -// 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') ->group(base_path('routes/base.php')); @@ -49,12 +37,12 @@ class RouteServiceProvider extends ServiceProvider ->namespace($this->namespace . '\Server') ->group(base_path('routes/server.php')); - Route::middleware(['api', 'api..user_level:admin'])->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/app/Repositories/Eloquent/AllocationRepository.php b/app/Repositories/Eloquent/AllocationRepository.php index 1a89134ca..a47134e4c 100644 --- a/app/Repositories/Eloquent/AllocationRepository.php +++ b/app/Repositories/Eloquent/AllocationRepository.php @@ -4,6 +4,8 @@ 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; class AllocationRepository extends EloquentRepository implements AllocationRepositoryInterface @@ -41,6 +43,18 @@ class AllocationRepository extends EloquentRepository implements AllocationRepos return $this->getBuilder()->where('node_id', $node)->get($this->getColumns()); } + /** + * Return all of the allocations for a node in a paginated format. + * + * @param int $node + * @param int $perPage + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public function getPaginatedAllocationsForNode(int $node, int $perPage = 100): LengthAwarePaginator + { + return $this->getBuilder()->where('node_id', $node)->paginate($perPage, $this->getColumns()); + } + /** * Return all of the unique IPs that exist for a given node. * @@ -54,4 +68,108 @@ 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(); + } + + /** + * 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/ApiKeyRepository.php b/app/Repositories/Eloquent/ApiKeyRepository.php index f6169c939..7ba0c9982 100644 --- a/app/Repositories/Eloquent/ApiKeyRepository.php +++ b/app/Repositories/Eloquent/ApiKeyRepository.php @@ -2,7 +2,9 @@ namespace Pterodactyl\Repositories\Eloquent; -use Pterodactyl\Models\APIKey; +use Pterodactyl\Models\User; +use Pterodactyl\Models\ApiKey; +use Illuminate\Support\Collection; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; class ApiKeyRepository extends EloquentRepository implements ApiKeyRepositoryInterface @@ -14,22 +16,62 @@ class ApiKeyRepository extends EloquentRepository implements ApiKeyRepositoryInt */ public function model() { - return APIKey::class; + return ApiKey::class; } /** - * Load permissions for a key onto the model. + * Get all of the account API keys that exist for a specific user. * - * @param \Pterodactyl\Models\APIKey $model - * @param bool $refresh - * @return \Pterodactyl\Models\APIKey + * @param \Pterodactyl\Models\User $user + * @return \Illuminate\Support\Collection */ - public function loadPermissions(APIKey $model, bool $refresh = false): APIKey + public function getAccountKeys(User $user): Collection { - if (! $model->relationLoaded('permissions') || $refresh) { - $model->load('permissions'); - } + return $this->getBuilder()->where('user_id', $user->id) + ->where('key_type', ApiKey::TYPE_ACCOUNT) + ->get($this->getColumns()); + } - return $model; + /** + * Get all of the application API keys that exist for a specific user. + * + * @param \Pterodactyl\Models\User $user + * @return \Illuminate\Support\Collection + */ + public function getApplicationKeys(User $user): Collection + { + return $this->getBuilder()->where('user_id', $user->id) + ->where('key_type', ApiKey::TYPE_APPLICATION) + ->get($this->getColumns()); + } + + /** + * Delete an account API key from the panel for a specific user. + * + * @param \Pterodactyl\Models\User $user + * @param string $identifier + * @return int + */ + public function deleteAccountKey(User $user, string $identifier): int + { + return $this->getBuilder()->where('user_id', $user->id) + ->where('key_type', ApiKey::TYPE_ACCOUNT) + ->where('identifier', $identifier) + ->delete(); + } + + /** + * Delete an application API key from the panel for a specific user. + * + * @param \Pterodactyl\Models\User $user + * @param string $identifier + * @return int + */ + public function deleteApplicationKey(User $user, string $identifier): int + { + return $this->getBuilder()->where('user_id', $user->id) + ->where('key_type', ApiKey::TYPE_APPLICATION) + ->where('identifier', $identifier) + ->delete(); } } diff --git a/app/Repositories/Eloquent/ApiPermissionRepository.php b/app/Repositories/Eloquent/ApiPermissionRepository.php deleted file mode 100644 index d94771327..000000000 --- a/app/Repositories/Eloquent/ApiPermissionRepository.php +++ /dev/null @@ -1,19 +0,0 @@ -get($this->getColumns()); } + /** + * Return a paginated result set using a search term if set on the repository. + * + * @param int $perPage + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public function paginated(int $perPage): LengthAwarePaginator + { + $instance = $this->getBuilder(); + if (is_subclass_of(get_called_class(), SearchableInterface::class) && $this->hasSearchTerm()) { + $instance = $instance->search($this->getSearchTerm()); + } + + return $instance->paginate($perPage, $this->getColumns()); + } + /** * Insert a single or multiple records into the database at once skipping * validation and mass assignment checking. 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/Acl/Api/AdminAcl.php b/app/Services/Acl/Api/AdminAcl.php new file mode 100644 index 000000000..54bd594fd --- /dev/null +++ b/app/Services/Acl/Api/AdminAcl.php @@ -0,0 +1,82 @@ +getConstants())->filter(function ($value, $key) { + return substr($key, 0, 9) === 'RESOURCE_'; + })->values()->toArray(); + } +} diff --git a/app/Services/Allocations/AllocationDeletionService.php b/app/Services/Allocations/AllocationDeletionService.php new file mode 100644 index 000000000..5e81a1d2f --- /dev/null +++ b/app/Services/Allocations/AllocationDeletionService.php @@ -0,0 +1,43 @@ +repository = $repository; + } + + /** + * Delete an allocation from the database only if it does not have a server + * that is actively attached to it. + * + * @param \Pterodactyl\Models\Allocation $allocation + * @return int + * + * @throws \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException + */ + public function handle(Allocation $allocation) + { + if (! is_null($allocation->server_id)) { + throw new ServerUsingAllocationException(trans('exceptions.allocations.server_using')); + } + + return $this->repository->delete($allocation->id); + } +} 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/Api/KeyCreationService.php b/app/Services/Api/KeyCreationService.php index 891a32438..4a8efe7b4 100644 --- a/app/Services/Api/KeyCreationService.php +++ b/app/Services/Api/KeyCreationService.php @@ -2,21 +2,21 @@ namespace Pterodactyl\Services\Api; -use Pterodactyl\Models\APIKey; -use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Models\ApiKey; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; class KeyCreationService { /** - * @var \Illuminate\Database\ConnectionInterface + * @var \Illuminate\Contracts\Encryption\Encrypter */ - private $connection; + private $encrypter; /** - * @var \Pterodactyl\Services\Api\PermissionService + * @var int */ - private $permissionService; + private $keyType = ApiKey::TYPE_NONE; /** * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface @@ -27,68 +27,52 @@ class KeyCreationService * ApiKeyService constructor. * * @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository - * @param \Illuminate\Database\ConnectionInterface $connection - * @param \Pterodactyl\Services\Api\PermissionService $permissionService + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter */ - public function __construct( - ApiKeyRepositoryInterface $repository, - ConnectionInterface $connection, - PermissionService $permissionService - ) { + public function __construct(ApiKeyRepositoryInterface $repository, Encrypter $encrypter) + { + $this->encrypter = $encrypter; $this->repository = $repository; - $this->connection = $connection; - $this->permissionService = $permissionService; } /** - * Create a new API Key on the system with the given permissions. + * Set the type of key that should be created. By default an orphaned key will be + * created. These keys cannot be used for anything, and will not render in the UI. + * + * @param int $type + * @return \Pterodactyl\Services\Api\KeyCreationService + */ + public function setKeyType(int $type) + { + $this->keyType = $type; + + return $this; + } + + /** + * Create a new API key for the Panel using the permissions passed in the data request. + * This will automatically generate an identifer and an encrypted token that are + * stored in the database. * * @param array $data * @param array $permissions - * @param array $administrative - * @return \Pterodactyl\Models\APIKey + * @return \Pterodactyl\Models\ApiKey * - * @throws \Exception * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function handle(array $data, array $permissions, array $administrative = []): APIKey + public function handle(array $data, array $permissions = []): ApiKey { - $token = str_random(APIKey::KEY_LENGTH); - $data = array_merge($data, ['token' => $token]); + $data = array_merge($data, [ + 'key_type' => $this->keyType, + 'identifier' => str_random(ApiKey::IDENTIFIER_LENGTH), + 'token' => $this->encrypter->encrypt(str_random(ApiKey::KEY_LENGTH)), + ]); + + if ($this->keyType === ApiKey::TYPE_APPLICATION) { + $data = array_merge($data, $permissions); + } - $this->connection->beginTransaction(); $instance = $this->repository->create($data, true, true); - $nodes = $this->permissionService->getPermissions(); - - foreach ($permissions as $permission) { - @list($block, $search) = explode('-', $permission, 2); - - if ( - (empty($block) || empty($search)) || - ! array_key_exists($block, $nodes['_user']) || - ! in_array($search, $nodes['_user'][$block]) - ) { - continue; - } - - $this->permissionService->create($instance->id, sprintf('user.%s', $permission)); - } - - foreach ($administrative as $permission) { - @list($block, $search) = explode('-', $permission, 2); - - if ( - (empty($block) || empty($search)) || - ! array_key_exists($block, $nodes) || - ! in_array($search, $nodes[$block]) - ) { - continue; - } - - $this->permissionService->create($instance->id, $permission); - } - - $this->connection->commit(); return $instance; } diff --git a/app/Services/Api/PermissionService.php b/app/Services/Api/PermissionService.php deleted file mode 100644 index 24dbf381f..000000000 --- a/app/Services/Api/PermissionService.php +++ /dev/null @@ -1,58 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Services\Api; - -use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; - -class PermissionService -{ - /** - * @var \Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface - */ - protected $repository; - - /** - * ApiPermissionService constructor. - * - * @param \Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface $repository - */ - public function __construct(ApiPermissionRepositoryInterface $repository) - { - $this->repository = $repository; - } - - /** - * Store a permission key in the database. - * - * @param string $key - * @param string $permission - * @return bool - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - */ - public function create($key, $permission) - { - // @todo handle an array of permissions to do a mass assignment? - return $this->repository->withoutFreshModel()->create([ - 'key_id' => $key, - 'permission' => $permission, - ]); - } - - /** - * Return all of the permissions available for an API Key. - * - * @return array - */ - public function getPermissions() - { - return $this->repository->getModel()::CONST_PERMISSIONS; - } -} 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/Locations/LocationDeletionService.php b/app/Services/Locations/LocationDeletionService.php index 34dab547a..5d1495d16 100644 --- a/app/Services/Locations/LocationDeletionService.php +++ b/app/Services/Locations/LocationDeletionService.php @@ -10,6 +10,7 @@ namespace Pterodactyl\Services\Locations; use Webmozart\Assert\Assert; +use Pterodactyl\Models\Location; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Exceptions\Service\Location\HasActiveNodesException; @@ -43,15 +44,16 @@ class LocationDeletionService /** * Delete an existing location. * - * @param int $location + * @param int|\Pterodactyl\Models\Location $location * @return int|null * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Service\Location\HasActiveNodesException */ public function handle($location) { - Assert::integerish($location, 'First argument passed to handle must be numeric, received %s.'); + $location = ($location instanceof Location) ? $location->id : $location; + + Assert::integerish($location, 'First argument passed to handle must be numeric or an instance of ' . Location::class . ', received %s.'); $count = $this->nodeRepository->findCountWhere([['location_id', '=', $location]]); if ($count > 0) { diff --git a/app/Services/Nodes/NodeUpdateService.php b/app/Services/Nodes/NodeUpdateService.php index ce81d6f8a..ef9349a71 100644 --- a/app/Services/Nodes/NodeUpdateService.php +++ b/app/Services/Nodes/NodeUpdateService.php @@ -9,82 +9,92 @@ namespace Pterodactyl\Services\Nodes; -use Illuminate\Log\Writer; use Pterodactyl\Models\Node; +use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\RequestException; -use Pterodactyl\Exceptions\DisplayException; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Traits\Services\ReturnsUpdatedModels; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; +use Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException; use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; class NodeUpdateService { + use ReturnsUpdatedModels; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + private $connection; + /** * @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 \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface $configurationRepository * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository - * @param \Illuminate\Log\Writer $writer */ public function __construct( + ConnectionInterface $connection, ConfigurationRepositoryInterface $configurationRepository, - NodeRepositoryInterface $repository, - Writer $writer + NodeRepositoryInterface $repository ) { + $this->connection = $connection; $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->withoutFreshModel()->update($node->id, $data); + $this->connection->beginTransaction(); + if ($this->getUpdatedModel()) { + $response = $this->repository->update($node->id, $data); + } else { + $response = $this->repository->withoutFreshModel()->update($node->id, $data); + } try { $this->configRepository->setNode($node)->update(); + $this->connection->commit(); } catch (RequestException $exception) { - $response = $exception->getResponse(); - $this->writer->warning($exception); + // Failed to connect to the Daemon. Let's go ahead and save the configuration + // and let the user know they'll need to manually update. + if ($exception instanceof ConnectException) { + $this->connection->commit(); - throw new DisplayException(trans('exceptions.node.daemon_off_config_updated', [ - 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), - ])); + throw new ConfigurationNotPersistedException(trans('exceptions.node.daemon_off_config_updated')); + } + + throw new DaemonConnectionException($exception); } - return $updateResponse; + return $response; } } 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/app/Services/Servers/DetailsModificationService.php b/app/Services/Servers/DetailsModificationService.php index 5ba4f779b..78d8eb31e 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', ''), + '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/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/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/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/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/app/Traits/Services/ReturnsUpdatedModels.php b/app/Traits/Services/ReturnsUpdatedModels.php new file mode 100644 index 000000000..2d5ee64fd --- /dev/null +++ b/app/Traits/Services/ReturnsUpdatedModels.php @@ -0,0 +1,35 @@ +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; + } +} diff --git a/app/Transformers/Admin/AllocationTransformer.php b/app/Transformers/Admin/AllocationTransformer.php deleted file mode 100644 index e1395ea6b..000000000 --- a/app/Transformers/Admin/AllocationTransformer.php +++ /dev/null @@ -1,82 +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\Allocation; -use League\Fractal\TransformerAbstract; - -class AllocationTransformer extends TransformerAbstract -{ - /** - * The filter to be applied to this transformer. - * - * @var bool|string - */ - protected $filter; - - /** - * The Illuminate Request object if provided. - * - * @var \Illuminate\Http\Request|bool - */ - protected $request; - - /** - * Setup request object for transformer. - * - * @param \Illuminate\Http\Request|bool $request - * @param bool $filter - */ - public function __construct($request = false, $filter = false) - { - if (! $request instanceof Request && $request !== false) { - throw new DisplayException('Request passed to constructor must be of type Request or false.'); - } - - $this->request = $request; - $this->filter = $filter; - } - - /** - * Return a generic transformed allocation array. - * - * @return array - */ - public function transform(Allocation $allocation) - { - return $this->transformWithFilter($allocation); - } - - /** - * Determine which transformer filter to apply. - * - * @return array - */ - protected function transformWithFilter(Allocation $allocation) - { - if ($this->filter === 'server') { - return $this->transformForServer($allocation); - } - - return $allocation->toArray(); - } - - /** - * Transform the allocation to only return information not duplicated - * in the server response (discard node_id and server_id). - * - * @return array - */ - protected function transformForServer(Allocation $allocation) - { - return collect($allocation)->only('id', 'ip', 'ip_alias', 'port')->toArray(); - } -} diff --git a/app/Transformers/Admin/LocationTransformer.php b/app/Transformers/Admin/LocationTransformer.php deleted file mode 100644 index 2ae26647d..000000000 --- a/app/Transformers/Admin/LocationTransformer.php +++ /dev/null @@ -1,86 +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\Location; -use League\Fractal\TransformerAbstract; - -class LocationTransformer extends TransformerAbstract -{ - /** - * List of resources that can be included. - * - * @var array - */ - protected $availableIncludes = [ - 'nodes', - 'servers', - ]; - - /** - * 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 pack array. - * - * @return array - */ - public function transform(Location $location) - { - return $location->toArray(); - } - - /** - * Return the nodes associated with this location. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeServers(Location $location) - { - if ($this->request && ! $this->request->apiKeyHasPermission('server-list')) { - return; - } - - return $this->collection($location->servers, new ServerTransformer($this->request), 'server'); - } - - /** - * Return the nodes associated with this location. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeNodes(Location $location) - { - if ($this->request && ! $this->request->apiKeyHasPermission('node-list')) { - return; - } - - return $this->collection($location->nodes, new NodeTransformer($this->request), 'node'); - } -} diff --git a/app/Transformers/Admin/NodeTransformer.php b/app/Transformers/Admin/NodeTransformer.php deleted file mode 100644 index 61abf0c74..000000000 --- a/app/Transformers/Admin/NodeTransformer.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\Node; -use League\Fractal\TransformerAbstract; - -class NodeTransformer extends TransformerAbstract -{ - /** - * List of resources that can be included. - * - * @var array - */ - protected $availableIncludes = [ - 'allocations', - 'location', - 'servers', - ]; - - /** - * 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 pack array. - * - * @return array - */ - public function transform(Node $node) - { - return $node->toArray(); - } - - /** - * Return the nodes associated with this location. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeAllocations(Node $node) - { - if ($this->request && ! $this->request->apiKeyHasPermission('node-view')) { - return; - } - - return $this->collection($node->allocations, new AllocationTransformer($this->request), 'allocation'); - } - - /** - * Return the nodes associated with this location. - * - * @return \Leauge\Fractal\Resource\Item - */ - public function includeLocation(Node $node) - { - if ($this->request && ! $this->request->apiKeyHasPermission('location-list')) { - return; - } - - return $this->item($node->location, new LocationTransformer($this->request), 'location'); - } - - /** - * Return the nodes associated with this location. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeServers(Node $node) - { - if ($this->request && ! $this->request->apiKeyHasPermission('server-list')) { - return; - } - - return $this->collection($node->servers, new ServerTransformer($this->request), 'server'); - } -} diff --git a/app/Transformers/Admin/OptionTransformer.php b/app/Transformers/Admin/OptionTransformer.php deleted file mode 100644 index 089f880ec..000000000 --- a/app/Transformers/Admin/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/Admin/ServerTransformer.php b/app/Transformers/Admin/ServerTransformer.php deleted file mode 100644 index b2fd82747..000000000 --- a/app/Transformers/Admin/ServerTransformer.php +++ /dev/null @@ -1,191 +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\Server; -use League\Fractal\TransformerAbstract; - -class ServerTransformer extends TransformerAbstract -{ - /** - * List of resources that can be included. - * - * @var array - */ - protected $availableIncludes = [ - 'allocations', - 'user', - 'subusers', - 'pack', - 'service', - 'option', - '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. - * - * @return array - */ - public function transform(Server $server) - { - return collect($server->toArray())->only($server->getTableColumns())->toArray(); - } - - /** - * Return a generic array of allocations for this server. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeAllocations(Server $server) - { - if ($this->request && ! $this->request->apiKeyHasPermission('server-view')) { - return; - } - - return $this->collection($server->allocations, new AllocationTransformer($this->request, 'server'), 'allocation'); - } - - /** - * Return a generic array of data about subusers for this server. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeSubusers(Server $server) - { - if ($this->request && ! $this->request->apiKeyHasPermission('server-view')) { - return; - } - - return $this->collection($server->subusers, new SubuserTransformer($this->request), 'subuser'); - } - - /** - * Return a generic array of data about subusers for this server. - * - * @return \Leauge\Fractal\Resource\Item - */ - public function includeUser(Server $server) - { - if ($this->request && ! $this->request->apiKeyHasPermission('user-view')) { - return; - } - - return $this->item($server->user, new UserTransformer($this->request), 'user'); - } - - /** - * Return a generic array with pack information for this server. - * - * @return \Leauge\Fractal\Resource\Item - */ - public function includePack(Server $server) - { - if ($this->request && ! $this->request->apiKeyHasPermission('pack-view')) { - return; - } - - return $this->item($server->pack, new PackTransformer($this->request), 'pack'); - } - - /** - * Return a generic array with service information for this server. - * - * @return \Leauge\Fractal\Resource\Item - */ - public function includeService(Server $server) - { - if ($this->request && ! $this->request->apiKeyHasPermission('service-view')) { - return; - } - - return $this->item($server->service, new ServiceTransformer($this->request), 'service'); - } - - /** - * Return a generic array with service option information for this server. - * - * @return \Leauge\Fractal\Resource\Item - */ - public function includeOption(Server $server) - { - if ($this->request && ! $this->request->apiKeyHasPermission('option-view')) { - return; - } - - return $this->item($server->option, new OptionTransformer($this->request), 'option'); - } - - /** - * Return a generic array of data about subusers for this server. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeVariables(Server $server) - { - if ($this->request && ! $this->request->apiKeyHasPermission('server-view')) { - return; - } - - return $this->collection($server->variables, new ServerVariableTransformer($this->request), 'server_variable'); - } - - /** - * Return a generic array with pack information for this server. - * - * @return \Leauge\Fractal\Resource\Item - */ - public function includeLocation(Server $server) - { - if ($this->request && ! $this->request->apiKeyHasPermission('location-list')) { - return; - } - - return $this->item($server->location, new LocationTransformer($this->request), 'location'); - } - - /** - * Return a generic array with pack information for this server. - * - * @return \Leauge\Fractal\Resource\Item|void - */ - public function includeNode(Server $server) - { - if ($this->request && ! $this->request->apiKeyHasPermission('node-view')) { - return; - } - - return $this->item($server->node, new NodeTransformer($this->request), 'node'); - } -} diff --git a/app/Transformers/Admin/ServerVariableTransformer.php b/app/Transformers/Admin/ServerVariableTransformer.php deleted file mode 100644 index 6f526f3f4..000000000 --- a/app/Transformers/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/Admin/ServiceTransformer.php b/app/Transformers/Admin/ServiceTransformer.php deleted file mode 100644 index 5f8497cf7..000000000 --- a/app/Transformers/Admin/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/Admin/ServiceVariableTransformer.php b/app/Transformers/Admin/ServiceVariableTransformer.php deleted file mode 100644 index 49f5e449a..000000000 --- a/app/Transformers/Admin/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/app/Transformers/Admin/UserTransformer.php b/app/Transformers/Admin/UserTransformer.php deleted file mode 100644 index 323e72c4c..000000000 --- a/app/Transformers/Admin/UserTransformer.php +++ /dev/null @@ -1,86 +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\User; -use League\Fractal\TransformerAbstract; - -class UserTransformer extends TransformerAbstract -{ - /** - * 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; - - /** - * 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 subuser array. - * - * @return array - */ - public function transform(User $user) - { - return $user->toArray(); - } - - /** - * Return the servers associated with this user. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeServers(User $user) - { - if ($this->request && ! $this->request->apiKeyHasPermission('server-list')) { - return; - } - - 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; - } - - return $this->collection($user->access()->get(), new ServerTransformer($this->request), 'server'); - } -} diff --git a/app/Transformers/Api/Application/AllocationTransformer.php b/app/Transformers/Api/Application/AllocationTransformer.php new file mode 100644 index 000000000..cce6acc86 --- /dev/null +++ b/app/Transformers/Api/Application/AllocationTransformer.php @@ -0,0 +1,81 @@ + $allocation->id, + 'ip' => $allocation->ip, + 'alias' => $allocation->ip_alias, + 'port' => $allocation->port, + 'assigned' => ! is_null($allocation->server_id), + ]; + } + + /** + * Load the node relationship onto a given transformation. + * + * @param \Pterodactyl\Models\Allocation $allocation + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + */ + public function includeNode(Allocation $allocation) + { + if (! $this->authorize(AdminAcl::RESOURCE_NODES)) { + return $this->null(); + } + + $allocation->loadMissing('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 \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + */ + public function includeServer(Allocation $allocation) + { + if (! $this->authorize(AdminAcl::RESOURCE_SERVERS)) { + return $this->null(); + } + + $allocation->loadMissing('server'); + + return $this->item( + $allocation->getRelation('server'), $this->makeTransformer(ServerTransformer::class), 'server' + ); + } +} diff --git a/app/Transformers/Api/Application/BaseTransformer.php b/app/Transformers/Api/Application/BaseTransformer.php new file mode 100644 index 000000000..c2fdf6137 --- /dev/null +++ b/app/Transformers/Api/Application/BaseTransformer.php @@ -0,0 +1,103 @@ +call([$this, 'handle']); + } + } + + /** + * Set the HTTP request class being used for this request. + * + * @param \Pterodactyl\Models\ApiKey $key + * @return $this + */ + public function setKey(ApiKey $key) + { + $this->key = $key; + + return $this; + } + + /** + * Return the request instance being used for this transformer. + * + * @return \Pterodactyl\Models\ApiKey + */ + public function getKey(): ApiKey + { + return $this->key; + } + + /** + * Determine if the API key loaded onto the transformer has permission + * to access a different resource. This is used when including other + * models on a transformation request. + * + * @param string $resource + * @return bool + */ + protected function authorize(string $resource): bool + { + return AdminAcl::check($this->getKey(), $resource, AdminAcl::READ); + } + + /** + * Create a new instance of the transformer and pass along the currently + * set API key. + * + * @param string $abstract + * @param array $parameters + * @return \Pterodactyl\Transformers\Api\Application\BaseTransformer + */ + protected function makeTransformer(string $abstract, array $parameters = []): self + { + /** @var \Pterodactyl\Transformers\Api\Application\BaseTransformer $transformer */ + $transformer = Container::getInstance()->makeWith($abstract, $parameters); + $transformer->setKey($this->getKey()); + + 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/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/EggTransformer.php b/app/Transformers/Api/Application/EggTransformer.php new file mode 100644 index 000000000..3454547bd --- /dev/null +++ b/app/Transformers/Api/Application/EggTransformer.php @@ -0,0 +1,150 @@ + $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/EggVariableTransformer.php b/app/Transformers/Api/Application/EggVariableTransformer.php new file mode 100644 index 000000000..decb038ab --- /dev/null +++ b/app/Transformers/Api/Application/EggVariableTransformer.php @@ -0,0 +1,24 @@ +toArray(); + } +} diff --git a/app/Transformers/Api/Application/LocationTransformer.php b/app/Transformers/Api/Application/LocationTransformer.php new file mode 100644 index 000000000..d003053d6 --- /dev/null +++ b/app/Transformers/Api/Application/LocationTransformer.php @@ -0,0 +1,71 @@ +toArray(); + } + + /** + * Return the nodes associated with this location. + * + * @param \Pterodactyl\Models\Location $location + * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + */ + public function includeServers(Location $location) + { + if (! $this->authorize(AdminAcl::RESOURCE_SERVERS)) { + return $this->null(); + } + + $location->loadMissing('servers'); + + return $this->collection($location->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), 'server'); + } + + /** + * Return the nodes associated with this location. + * + * @param \Pterodactyl\Models\Location $location + * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + */ + public function includeNodes(Location $location) + { + if (! $this->authorize(AdminAcl::RESOURCE_NODES)) { + return $this->null(); + } + + $location->loadMissing('nodes'); + + return $this->collection($location->getRelation('nodes'), $this->makeTransformer(NodeTransformer::class), 'node'); + } +} 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/NodeTransformer.php b/app/Transformers/Api/Application/NodeTransformer.php new file mode 100644 index 000000000..d47183fc7 --- /dev/null +++ b/app/Transformers/Api/Application/NodeTransformer.php @@ -0,0 +1,106 @@ +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|\League\Fractal\Resource\NullResource + */ + public function includeAllocations(Node $node) + { + if (! $this->authorize(AdminAcl::RESOURCE_ALLOCATIONS)) { + return $this->null(); + } + + $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 \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + */ + public function includeLocation(Node $node) + { + if (! $this->authorize(AdminAcl::RESOURCE_LOCATIONS)) { + return $this->null(); + } + + $node->loadMissing('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 \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + */ + public function includeServers(Node $node) + { + if (! $this->authorize(AdminAcl::RESOURCE_SERVERS)) { + return $this->null(); + } + + $node->loadMissing('servers'); + + return $this->collection( + $node->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), 'server' + ); + } +} diff --git a/app/Transformers/Admin/PackTransformer.php b/app/Transformers/Api/Application/PackTransformer.php similarity index 100% rename from app/Transformers/Admin/PackTransformer.php rename to app/Transformers/Api/Application/PackTransformer.php diff --git a/app/Transformers/Api/Application/ServerDatabaseTransformer.php b/app/Transformers/Api/Application/ServerDatabaseTransformer.php new file mode 100644 index 000000000..c71627296 --- /dev/null +++ b/app/Transformers/Api/Application/ServerDatabaseTransformer.php @@ -0,0 +1,103 @@ +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 [ + '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 new file mode 100644 index 000000000..914449ef2 --- /dev/null +++ b/app/Transformers/Api/Application/ServerTransformer.php @@ -0,0 +1,247 @@ +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. + * + * @param \Pterodactyl\Models\Server $server + * @return array + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function transform(Server $server): array + { + 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(), + ]; + } + + /** + * Return a generic array of allocations for this server. + * + * @param \Pterodactyl\Models\Server $server + * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + */ + public function includeAllocations(Server $server) + { + if (! $this->authorize(AdminAcl::RESOURCE_ALLOCATIONS)) { + return $this->null(); + } + + $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. + * + * @param \Pterodactyl\Models\Server $server + * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + */ + public function includeSubusers(Server $server) + { + if (! $this->authorize(AdminAcl::RESOURCE_USERS)) { + return $this->null(); + } + + $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. + * + * @param \Pterodactyl\Models\Server $server + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + */ + public function includeUser(Server $server) + { + if (! $this->authorize(AdminAcl::RESOURCE_USERS)) { + return $this->null(); + } + + $server->loadMissing('user'); + + return $this->item($server->getRelation('user'), $this->makeTransformer(UserTransformer::class), 'user'); + } + + /** + * Return a generic array with pack information for this server. + * + * @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'); +// } + + /** + * Return a generic array with nest information for this server. + * + * @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'); +// } + + /** + * Return a generic array with service option information for this server. + * + * @param \Pterodactyl\Models\Server $server + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + */ + public function includeOption(Server $server) + { + if (! $this->authorize(AdminAcl::RESOURCE_EGGS)) { + return $this->null(); + } + + $server->loadMissing('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\Collection|\League\Fractal\Resource\NullResource + */ + public function includeVariables(Server $server) + { + if (! $this->authorize(AdminAcl::RESOURCE_SERVERS)) { + return $this->null(); + } + + $server->loadMissing('variables'); + + return $this->collection($server->getRelation('variables'), $this->makeTransformer(ServerVariableTransformer::class), 'server_variable'); + } + + /** + * Return a generic array with pack information for this server. + * + * @param \Pterodactyl\Models\Server $server + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + */ + public function includeLocation(Server $server) + { + if (! $this->authorize(AdminAcl::RESOURCE_LOCATIONS)) { + return $this->null(); + } + + $server->loadMissing('location'); + + return $this->item($server->getRelation('location'), $this->makeTransformer(LocationTransformer::class), 'location'); + } + + /** + * Return a generic array with pack information for this server. + * + * @param \Pterodactyl\Models\Server $server + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + */ + public function includeNode(Server $server) + { + if (! $this->authorize(AdminAcl::RESOURCE_NODES)) { + return $this->null(); + } + + $server->loadMissing('node'); + + return $this->item($server->getRelation('node'), $this->makeTransformer(NodeTransformer::class), 'node'); + } +} diff --git a/app/Transformers/Api/Application/ServerVariableTransformer.php b/app/Transformers/Api/Application/ServerVariableTransformer.php new file mode 100644 index 000000000..5ce592b5f --- /dev/null +++ b/app/Transformers/Api/Application/ServerVariableTransformer.php @@ -0,0 +1,54 @@ +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/Admin/SubuserTransformer.php b/app/Transformers/Api/Application/SubuserTransformer.php similarity index 100% rename from app/Transformers/Admin/SubuserTransformer.php rename to app/Transformers/Api/Application/SubuserTransformer.php diff --git a/app/Transformers/Api/Application/UserTransformer.php b/app/Transformers/Api/Application/UserTransformer.php new file mode 100644 index 000000000..d3d680ed7 --- /dev/null +++ b/app/Transformers/Api/Application/UserTransformer.php @@ -0,0 +1,54 @@ +toArray(); + } + + /** + * Return the servers associated with this user. + * + * @param \Pterodactyl\Models\User $user + * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + */ + public function includeServers(User $user) + { + if (! $this->authorize(AdminAcl::RESOURCE_SERVERS)) { + return $this->null(); + } + + $user->loadMissing('servers'); + + return $this->collection($user->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), 'server'); + } +} diff --git a/composer.json b/composer.json index 091416ef3..49bc61878 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 d7798684f..1d4eb4aac 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", diff --git a/config/app.php b/config/app.php index 4a8de1e36..9a346918c 100644 --- a/config/app.php +++ b/config/app.php @@ -175,6 +175,7 @@ return [ */ Pterodactyl\Providers\AppServiceProvider::class, Pterodactyl\Providers\AuthServiceProvider::class, + Pterodactyl\Providers\BladeServiceProvider::class, Pterodactyl\Providers\EventServiceProvider::class, Pterodactyl\Providers\HashidsServiceProvider::class, Pterodactyl\Providers\RouteServiceProvider::class, diff --git a/config/laravel-fractal.php b/config/fractal.php similarity index 55% rename from config/laravel-fractal.php rename to config/fractal.php index 92bfe2cea..833cf2b95 100644 --- a/config/laravel-fractal.php +++ b/config/fractal.php @@ -13,4 +13,19 @@ return [ */ 'default_serializer' => League\Fractal\Serializer\JsonApiSerializer::class, + + /* + |-------------------------------------------------------------------------- + | Auto Includes + |-------------------------------------------------------------------------- + | + | If enabled Fractal will automatically add the includes who's + | names are present in the `include` request parameter. + | + */ + + 'auto_includes' => [ + 'enabled' => true, + 'request_key' => 'include', + ], ]; diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 6e585d4d6..bef8ee396 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -223,11 +223,12 @@ $factory->define(Pterodactyl\Models\DaemonKey::class, function (Faker $faker) { ]; }); -$factory->define(Pterodactyl\Models\APIKey::class, function (Faker $faker) { +$factory->define(Pterodactyl\Models\ApiKey::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), 'user_id' => $faker->randomNumber(), - 'token' => str_random(Pterodactyl\Models\APIKey::KEY_LENGTH), + 'identifier' => str_random(Pterodactyl\Models\ApiKey::IDENTIFIER_LENGTH), + 'token' => 'encrypted_string', 'memo' => 'Test Function Key', 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), diff --git a/database/migrations/2018_01_11_213943_AddApiKeyPermissionColumns.php b/database/migrations/2018_01_11_213943_AddApiKeyPermissionColumns.php new file mode 100644 index 000000000..cd6b60e10 --- /dev/null +++ b/database/migrations/2018_01_11_213943_AddApiKeyPermissionColumns.php @@ -0,0 +1,62 @@ +unsignedTinyInteger('r_servers')->default(0); + $table->unsignedTinyInteger('r_nodes')->default(0); + $table->unsignedTinyInteger('r_allocations')->default(0); + $table->unsignedTinyInteger('r_users')->default(0); + $table->unsignedTinyInteger('r_locations')->default(0); + $table->unsignedTinyInteger('r_nests')->default(0); + $table->unsignedTinyInteger('r_eggs')->default(0); + $table->unsignedTinyInteger('r_database_hosts')->default(0); + $table->unsignedTinyInteger('r_server_databases')->default(0); + $table->unsignedTinyInteger('r_packs')->default(0); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::create('api_permissions', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('key_id'); + $table->string('permission'); + + $table->foreign('key_id')->references('id')->on('keys')->onDelete('cascade'); + }); + + Schema::table('api_keys', function (Blueprint $table) { + $table->dropColumn([ + 'r_servers', + 'r_nodes', + 'r_allocations', + 'r_users', + 'r_locations', + 'r_nests', + 'r_eggs', + 'r_database_hosts', + 'r_server_databases', + 'r_packs', + ]); + }); + } +} diff --git a/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php b/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php new file mode 100644 index 000000000..e7fd0c58c --- /dev/null +++ b/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php @@ -0,0 +1,42 @@ +char('identifier', 16)->unique()->after('user_id'); + $table->dropUnique(['token']); + }); + + Schema::table('api_keys', function (Blueprint $table) { + $table->text('token')->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + * @throws \Exception + * @throws \Throwable + */ + public function down() + { + Schema::table('api_keys', function (Blueprint $table) { + $table->dropColumn('identifier'); + $table->string('token', 32)->unique()->change(); + }); + } +} diff --git a/database/migrations/2018_01_13_145209_AddLastUsedAtColumn.php b/database/migrations/2018_01_13_145209_AddLastUsedAtColumn.php new file mode 100644 index 000000000..e0f86b9de --- /dev/null +++ b/database/migrations/2018_01_13_145209_AddLastUsedAtColumn.php @@ -0,0 +1,46 @@ +unsignedTinyInteger('key_type')->after('user_id')->default(0); + $table->timestamp('last_used_at')->after('memo')->nullable(); + $table->dropColumn('expires_at'); + + $table->dropForeign(['user_id']); + }); + + Schema::table('api_keys', function (Blueprint $table) { + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('api_keys', function (Blueprint $table) { + $table->timestamp('expires_at')->after('memo')->nullable(); + $table->dropColumn('last_used_at', 'key_type'); + $table->dropForeign(['user_id']); + }); + + Schema::table('api_keys', function (Blueprint $table) { + $table->foreign('user_id')->references('id')->on('users'); + }); + } +} diff --git a/public/js/laroute.js b/public/js/laroute.js index ea2d1a03d..bd13dfa2f 100644 --- a/public/js/laroute.js +++ b/public/js/laroute.js @@ -5,8 +5,8 @@ var routes = { absolute: false, - rootUrl: 'http://pterodactyl.app', - 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\APIController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/api\/new","name":"account.api.new","action":"Pterodactyl\Http\Controllers\Base\APIController@create"},{"host":null,"methods":["POST"],"uri":"account\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Base\APIController@store"},{"host":null,"methods":["DELETE"],"uri":"account\/api\/revoke\/{key}","name":"account.api.revoke","action":"Pterodactyl\Http\Controllers\Base\APIController@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\/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\/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"}], + 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\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/public/themes/pterodactyl/js/frontend/files/filemanager.min.js b/public/themes/pterodactyl/js/frontend/files/filemanager.min.js index 8f55f8607..9713f6587 100644 --- a/public/themes/pterodactyl/js/frontend/files/filemanager.min.js +++ b/public/themes/pterodactyl/js/frontend/files/filemanager.min.js @@ -1,5 +1,5 @@ 'use strict';var _createClass=function(){function defineProperties(target,props){for(var i=0;i\n \n ';nameBlock.html(attachEditor);var inputField=nameBlock.find('input');var inputLoader=nameBlock.find('.input-loader');inputField.focus();inputField.on('blur keydown',function(e){if(e.type==='keydown'&&e.which===27||e.type==='blur'||e.type==='keydown'&&e.which===13&¤tName===inputField.val()){if(!_.isEmpty(currentLink)){nameBlock.html(currentLink)}else{nameBlock.html(currentName)}inputField.remove();ContextMenu.unbind().run();return}if(e.type==='keydown'&&e.which!==13)return;inputLoader.show();var currentPath=decodeURIComponent(nameBlock.data('path'));$.ajax({type:'POST',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/rename',timeout:10000,data:JSON.stringify({from:''+currentPath+currentName,to:''+currentPath+inputField.val()})}).done(function(data){nameBlock.attr('data-name',inputField.val());if(!_.isEmpty(currentLink)){var newLink=currentLink.attr('href');if(nameBlock.parent().data('type')!=='folder'){newLink=newLink.substr(0,newLink.lastIndexOf('/'))+'/'+inputField.val()}currentLink.attr('href',newLink);nameBlock.html(currentLink.html(inputField.val()))}else{nameBlock.html(inputField.val())}inputField.remove()}).fail(function(jqXHR){console.error(jqXHR);var error='An error occured while trying to process this request.';if(typeof jqXHR.responseJSON!=='undefined'&&typeof jqXHR.responseJSON.error!=='undefined'){error=jqXHR.responseJSON.error}nameBlock.addClass('has-error').delay(2000).queue(function(){nameBlock.removeClass('has-error').dequeue()});inputField.popover({animation:true,placement:'top',content:error,title:'Save Error'}).popover('show')}).always(function(){inputLoader.remove();ContextMenu.unbind().run()})})}},{key:'copy',value:function copy(){var nameBlock=$(this.element).find('td[data-identifier="name"]');var currentName=decodeURIComponent(nameBlock.attr('data-name'));var currentPath=decodeURIComponent(nameBlock.data('path'));swal({type:'input',title:'Copy File',text:'Please enter the new path for the copied file below.',showCancelButton:true,showConfirmButton:true,closeOnConfirm:false,showLoaderOnConfirm:true,inputValue:''+currentPath+currentName},function(val){$.ajax({type:'POST',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/copy',timeout:10000,data:JSON.stringify({from:''+currentPath+currentName,to:''+val})}).done(function(data){swal({type:'success',title:'',text:'File successfully copied.'});Files.list()}).fail(function(jqXHR){console.error(jqXHR);var error='An error occured while trying to process this request.';if(typeof jqXHR.responseJSON!=='undefined'&&typeof jqXHR.responseJSON.error!=='undefined'){error=jqXHR.responseJSON.error}swal({type:'error',title:'',text:error})})})}},{key:'download',value:function download(){var nameBlock=$(this.element).find('td[data-identifier="name"]');var fileName=decodeURIComponent(nameBlock.attr('data-name'));var filePath=decodeURIComponent(nameBlock.data('path'));window.location='/server/'+Pterodactyl.server.uuidShort+'/files/download/'+filePath+fileName}},{key:'delete',value:function _delete(){var nameBlock=$(this.element).find('td[data-identifier="name"]');var delPath=decodeURIComponent(nameBlock.data('path'));var delName=decodeURIComponent(nameBlock.data('name'));swal({type:'warning',title:'',text:'Are you sure you want to delete '+delName+'? There is no reversing this action.',html:true,showCancelButton:true,showConfirmButton:true,closeOnConfirm:false,showLoaderOnConfirm:true},function(){$.ajax({type:'POST',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/delete',timeout:10000,data:JSON.stringify({items:[''+delPath+delName]})}).done(function(data){nameBlock.parent().addClass('warning').delay(200).fadeOut();swal({type:'success',title:'File Deleted'})}).fail(function(jqXHR){console.error(jqXHR);swal({type:'error',title:'Whoops!',html:true,text:'An error occured while attempting to delete this file. Please try again.'})})})}},{key:'toggleMassActions',value:function toggleMassActions(){if($('#file_listing input[type="checkbox"]:checked').length){$('#mass_actions').removeClass('disabled')}else{$('#mass_actions').addClass('disabled')}}},{key:'toggleHighlight',value:function toggleHighlight(event){var parent=$(event.currentTarget);var item=$(event.currentTarget).find('input');if($(item).is(':checked')){$(item).prop('checked',false);parent.removeClass('warning').delay(200)}else{$(item).prop('checked',true);parent.addClass('warning').delay(200)}}},{key:'highlightAll',value:function highlightAll(event){var parent=void 0;var item=$(event.currentTarget).find('input');if($(item).is(':checked')){$('#file_listing input[type=checkbox]').prop('checked',false);$('#file_listing input[data-action="addSelection"]').each(function(){parent=$(this).closest('tr');parent.removeClass('warning').delay(200)})}else{$('#file_listing input[type=checkbox]').prop('checked',true);$('#file_listing input[data-action="addSelection"]').each(function(){parent=$(this).closest('tr');parent.addClass('warning').delay(200)})}}},{key:'deleteSelected',value:function deleteSelected(){var selectedItems=[];var selectedItemsElements=[];var parent=void 0;var nameBlock=void 0;var delLocation=void 0;$('#file_listing input[data-action="addSelection"]:checked').each(function(){parent=$(this).closest('tr');nameBlock=$(parent).find('td[data-identifier="name"]');delLocation=decodeURIComponent(nameBlock.data('path'))+decodeURIComponent(nameBlock.data('name'));selectedItems.push(delLocation);selectedItemsElements.push(parent)});if(selectedItems.length!=0){var formattedItems='';$.each(selectedItems,function(key,value){formattedItems+=''+value+', '});formattedItems=formattedItems.slice(0,-2);swal({type:'warning',title:'',text:'Are you sure you want to delete:'+formattedItems+'? There is no reversing this action.',html:true,showCancelButton:true,showConfirmButton:true,closeOnConfirm:false,showLoaderOnConfirm:true},function(){$.ajax({type:'POST',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/delete',timeout:10000,data:JSON.stringify({items:selectedItems})}).done(function(data){$('#file_listing input:checked').each(function(){$(this).prop('checked',false)});$.each(selectedItemsElements,function(){$(this).addClass('warning').delay(200).fadeOut()});swal({type:'success',title:'Files Deleted'})}).fail(function(jqXHR){console.error(jqXHR);swal({type:'error',title:'Whoops!',html:true,text:'An error occured while attempting to delete these files. Please try again.'})})})}else{swal({type:'warning',title:'',text:'Please select files/folders to delete.'})}}},{key:'decompress',value:function decompress(){var nameBlock=$(this.element).find('td[data-identifier="name"]');var compPath=decodeURIComponent(nameBlock.data('path'));var compName=decodeURIComponent(nameBlock.data('name'));swal({title:' Decompressing...',text:'This might take a few seconds to complete.',html:true,allowOutsideClick:false,allowEscapeKey:false,showConfirmButton:false});$.ajax({type:'POST',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/decompress',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',data:JSON.stringify({files:''+compPath+compName})}).done(function(data){swal.close();Files.list(compPath)}).fail(function(jqXHR){console.error(jqXHR);var error='An error occured while trying to process this request.';if(typeof jqXHR.responseJSON!=='undefined'&&typeof jqXHR.responseJSON.error!=='undefined'){error=jqXHR.responseJSON.error}swal({type:'error',title:'Whoops!',html:true,text:error})})}},{key:'compress',value:function compress(){var nameBlock=$(this.element).find('td[data-identifier="name"]');var compPath=decodeURIComponent(nameBlock.data('path'));var compName=decodeURIComponent(nameBlock.data('name'));$.ajax({type:'POST',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/compress',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',data:JSON.stringify({files:''+compPath+compName,to:compPath.toString()})}).done(function(data){Files.list(compPath,function(err){if(err)return;var fileListing=$('#file_listing').find('[data-name="'+data.saved_as+'"]').parent();fileListing.addClass('success pulsate').delay(3000).queue(function(){fileListing.removeClass('success pulsate').dequeue()})})}).fail(function(jqXHR){console.error(jqXHR);var error='An error occured while trying to process this request.';if(typeof jqXHR.responseJSON!=='undefined'&&typeof jqXHR.responseJSON.error!=='undefined'){error=jqXHR.responseJSON.error}swal({type:'error',title:'Whoops!',html:true,text:error})})}}]);return ActionsClass}(); 'use strict';var _createClass=function(){function defineProperties(target,props){for(var i=0;i New File
  • New Folder
  • '}if(Pterodactyl.permissions.downloadFiles||Pterodactyl.permissions.deleteFiles){buildMenu+='
  • '}if(Pterodactyl.permissions.downloadFiles){buildMenu+=''}if(Pterodactyl.permissions.deleteFiles){buildMenu+='
  • Delete
  • '}buildMenu+='';return buildMenu}},{key:'rightClick',value:function rightClick(){var _this=this;$('[data-action="toggleMenu"]').on('mousedown',function(event){event.preventDefault();if($(document).find('#fileOptionMenu').is(':visible')){$('body').trigger('click');return}_this.showMenu(event)});$('#file_listing > tbody td').on('contextmenu',function(event){_this.showMenu(event)})}},{key:'showMenu',value:function showMenu(event){var _this2=this;var parent=$(event.target).closest('tr');var menu=$(this.makeMenu(parent));if(parent.data('type')==='disabled')return;event.preventDefault();$(menu).appendTo('body');$(menu).data('invokedOn',$(event.target)).show().css({position:'absolute',left:event.pageX-150,top:event.pageY});this.activeLine=parent;this.activeLine.addClass('active');var Actions=new ActionsClass(parent,menu);if(Pterodactyl.permissions.moveFiles){$(menu).find('li[data-action="move"]').unbind().on('click',function(e){e.preventDefault();Actions.move()});$(menu).find('li[data-action="rename"]').unbind().on('click',function(e){e.preventDefault();Actions.rename()})}if(Pterodactyl.permissions.copyFiles){$(menu).find('li[data-action="copy"]').unbind().on('click',function(e){e.preventDefault();Actions.copy()})}if(Pterodactyl.permissions.compressFiles){if(parent.data('type')==='folder'){$(menu).find('li[data-action="compress"]').removeClass('hidden')}$(menu).find('li[data-action="compress"]').unbind().on('click',function(e){e.preventDefault();Actions.compress()})}if(Pterodactyl.permissions.decompressFiles){if(_.without(['application/zip','application/gzip','application/x-gzip'],parent.data('mime')).length<3){$(menu).find('li[data-action="decompress"]').removeClass('hidden')}$(menu).find('li[data-action="decompress"]').unbind().on('click',function(e){e.preventDefault();Actions.decompress()})}if(Pterodactyl.permissions.createFiles){$(menu).find('li[data-action="folder"]').unbind().on('click',function(e){e.preventDefault();Actions.folder()})}if(Pterodactyl.permissions.downloadFiles){if(parent.data('type')==='file'){$(menu).find('li[data-action="download"]').removeClass('hidden')}$(menu).find('li[data-action="download"]').unbind().on('click',function(e){e.preventDefault();Actions.download()})}if(Pterodactyl.permissions.deleteFiles){$(menu).find('li[data-action="delete"]').unbind().on('click',function(e){e.preventDefault();Actions.delete()})}$(window).unbind().on('click',function(event){if($(event.target).is('.disable-menu-hide')){event.preventDefault();return}$(menu).unbind().remove();if(!_.isNull(_this2.activeLine))_this2.activeLine.removeClass('active')})}},{key:'directoryClick',value:function directoryClick(){$('a[data-action="directory-view"]').on('click',function(event){event.preventDefault();var path=$(this).parent().data('path')||'';var name=$(this).parent().data('name')||'';window.location.hash=encodeURIComponent(path+name);Files.list()})}}]);return ContextMenuClass}();window.ContextMenu=new ContextMenuClass; -'use strict';var _typeof=typeof Symbol==='function'&&typeof Symbol.iterator==='symbol'?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==='function'&&obj.constructor===Symbol&&obj!==Symbol.prototype?'symbol':typeof obj};var _createClass=function(){function defineProperties(target,props){for(var i=0;i\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nclass ActionsClass {\n constructor(element, menu) {\n this.element = element;\n this.menu = menu;\n }\n\n destroy() {\n this.element = undefined;\n }\n\n folder(path) {\n let inputValue\n if (path) {\n inputValue = path\n } else {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const currentName = decodeURIComponent(nameBlock.data('name'));\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n\n if ($(this.element).data('type') === 'file') {\n inputValue = currentPath;\n } else {\n inputValue = `${currentPath}${currentName}/`;\n }\n }\n\n swal({\n type: 'input',\n title: 'Create Folder',\n text: 'Please enter the path and folder name below.',\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true,\n inputValue: inputValue\n }, (val) => {\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/folder`,\n timeout: 10000,\n data: JSON.stringify({\n path: val,\n }),\n }).done(data => {\n swal.close();\n Files.list();\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occured while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: '',\n text: error,\n });\n });\n });\n }\n\n move() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const currentName = decodeURIComponent(nameBlock.attr('data-name'));\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n\n swal({\n type: 'input',\n title: 'Move File',\n text: 'Please enter the new path for the file below.',\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true,\n inputValue: `${currentPath}${currentName}`,\n }, (val) => {\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/move`,\n timeout: 10000,\n data: JSON.stringify({\n from: `${currentPath}${currentName}`,\n to: `${val}`,\n }),\n }).done(data => {\n nameBlock.parent().addClass('warning').delay(200).fadeOut();\n swal.close();\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occured while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: '',\n text: error,\n });\n });\n });\n\n }\n\n rename() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const currentLink = nameBlock.find('a');\n const currentName = decodeURIComponent(nameBlock.attr('data-name'));\n const attachEditor = `\n \n \n `;\n\n nameBlock.html(attachEditor);\n const inputField = nameBlock.find('input');\n const inputLoader = nameBlock.find('.input-loader');\n\n inputField.focus();\n inputField.on('blur keydown', e => {\n // Save Field\n if (\n (e.type === 'keydown' && e.which === 27)\n || e.type === 'blur'\n || (e.type === 'keydown' && e.which === 13 && currentName === inputField.val())\n ) {\n if (!_.isEmpty(currentLink)) {\n nameBlock.html(currentLink);\n } else {\n nameBlock.html(currentName);\n }\n inputField.remove();\n ContextMenu.unbind().run();\n return;\n }\n\n if (e.type === 'keydown' && e.which !== 13) return;\n\n inputLoader.show();\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/rename`,\n timeout: 10000,\n data: JSON.stringify({\n from: `${currentPath}${currentName}`,\n to: `${currentPath}${inputField.val()}`,\n }),\n }).done(data => {\n nameBlock.attr('data-name', inputField.val());\n if (!_.isEmpty(currentLink)) {\n let newLink = currentLink.attr('href');\n if (nameBlock.parent().data('type') !== 'folder') {\n newLink = newLink.substr(0, newLink.lastIndexOf('/')) + '/' + inputField.val();\n }\n currentLink.attr('href', newLink);\n nameBlock.html(\n currentLink.html(inputField.val())\n );\n } else {\n nameBlock.html(inputField.val());\n }\n inputField.remove();\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occured while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n nameBlock.addClass('has-error').delay(2000).queue(() => {\n nameBlock.removeClass('has-error').dequeue();\n });\n inputField.popover({\n animation: true,\n placement: 'top',\n content: error,\n title: 'Save Error'\n }).popover('show');\n }).always(() => {\n inputLoader.remove();\n ContextMenu.unbind().run();\n });\n });\n }\n\n copy() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const currentName = decodeURIComponent(nameBlock.attr('data-name'));\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n\n swal({\n type: 'input',\n title: 'Copy File',\n text: 'Please enter the new path for the copied file below.',\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true,\n inputValue: `${currentPath}${currentName}`,\n }, (val) => {\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/copy`,\n timeout: 10000,\n data: JSON.stringify({\n from: `${currentPath}${currentName}`,\n to: `${val}`,\n }),\n }).done(data => {\n swal({\n type: 'success',\n title: '',\n text: 'File successfully copied.'\n });\n Files.list();\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occured while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: '',\n text: error,\n });\n });\n });\n }\n\n download() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const fileName = decodeURIComponent(nameBlock.attr('data-name'));\n const filePath = decodeURIComponent(nameBlock.data('path'));\n\n window.location = `/server/${Pterodactyl.server.uuidShort}/files/download/${filePath}${fileName}`;\n }\n\n delete() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const delPath = decodeURIComponent(nameBlock.data('path'));\n const delName = decodeURIComponent(nameBlock.data('name'));\n\n swal({\n type: 'warning',\n title: '',\n text: 'Are you sure you want to delete ' + delName + '? There is no reversing this action.',\n html: true,\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true\n }, () => {\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/delete`,\n timeout: 10000,\n data: JSON.stringify({\n items: [`${delPath}${delName}`]\n }),\n }).done(data => {\n nameBlock.parent().addClass('warning').delay(200).fadeOut();\n swal({\n type: 'success',\n title: 'File Deleted'\n });\n }).fail(jqXHR => {\n console.error(jqXHR);\n swal({\n type: 'error',\n title: 'Whoops!',\n html: true,\n text: 'An error occured while attempting to delete this file. Please try again.',\n });\n });\n });\n }\n\n toggleMassActions() {\n if ($('#file_listing input[type=\"checkbox\"]:checked').length) {\n $('#mass_actions').removeClass('disabled');\n } else {\n $('#mass_actions').addClass('disabled');\n }\n }\n\n toggleHighlight(event) {\n const parent = $(event.currentTarget);\n const item = $(event.currentTarget).find('input');\n\n if($(item).is(':checked')) {\n $(item).prop('checked', false);\n parent.removeClass('warning').delay(200);\n } else {\n $(item).prop('checked', true);\n parent.addClass('warning').delay(200);\n }\n }\n\n highlightAll(event) {\n let parent;\n const item = $(event.currentTarget).find('input');\n\n if($(item).is(':checked')) {\n $('#file_listing input[type=checkbox]').prop('checked', false);\n $('#file_listing input[data-action=\"addSelection\"]').each(function() {\n parent = $(this).closest('tr');\n parent.removeClass('warning').delay(200);\n });\n } else {\n $('#file_listing input[type=checkbox]').prop('checked', true);\n $('#file_listing input[data-action=\"addSelection\"]').each(function() {\n parent = $(this).closest('tr');\n parent.addClass('warning').delay(200);\n });\n }\n }\n\n deleteSelected() {\n let selectedItems = [];\n let selectedItemsElements = [];\n let parent;\n let nameBlock;\n let delLocation;\n\n $('#file_listing input[data-action=\"addSelection\"]:checked').each(function() {\n parent = $(this).closest('tr');\n nameBlock = $(parent).find('td[data-identifier=\"name\"]');\n delLocation = decodeURIComponent(nameBlock.data('path')) + decodeURIComponent(nameBlock.data('name'));\n\n selectedItems.push(delLocation);\n selectedItemsElements.push(parent);\n });\n\n if (selectedItems.length != 0)\n {\n let formattedItems = \"\";\n $.each(selectedItems, function(key, value) {\n formattedItems += (\"\" + value + \", \");\n })\n\n formattedItems = formattedItems.slice(0, -2);\n\n swal({\n type: 'warning',\n title: '',\n text: 'Are you sure you want to delete:' + formattedItems + '? There is no reversing this action.',\n html: true,\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true\n }, () => {\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/delete`,\n timeout: 10000,\n data: JSON.stringify({\n items: selectedItems\n }),\n }).done(data => {\n $('#file_listing input:checked').each(function() {\n $(this).prop('checked', false);\n });\n\n $.each(selectedItemsElements, function() {\n $(this).addClass('warning').delay(200).fadeOut();\n })\n\n swal({\n type: 'success',\n title: 'Files Deleted'\n });\n }).fail(jqXHR => {\n console.error(jqXHR);\n swal({\n type: 'error',\n title: 'Whoops!',\n html: true,\n text: 'An error occured while attempting to delete these files. Please try again.',\n });\n });\n });\n } else {\n swal({\n type: 'warning',\n title: '',\n text: 'Please select files/folders to delete.',\n });\n }\n }\n\n decompress() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const compPath = decodeURIComponent(nameBlock.data('path'));\n const compName = decodeURIComponent(nameBlock.data('name'));\n\n swal({\n title: ' Decompressing...',\n text: 'This might take a few seconds to complete.',\n html: true,\n allowOutsideClick: false,\n allowEscapeKey: false,\n showConfirmButton: false,\n });\n\n $.ajax({\n type: 'POST',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/decompress`,\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n data: JSON.stringify({\n files: `${compPath}${compName}`\n })\n }).done(data => {\n swal.close();\n Files.list(compPath);\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occured while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: 'Whoops!',\n html: true,\n text: error\n });\n });\n }\n\n compress() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const compPath = decodeURIComponent(nameBlock.data('path'));\n const compName = decodeURIComponent(nameBlock.data('name'));\n\n $.ajax({\n type: 'POST',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/compress`,\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n data: JSON.stringify({\n files: `${compPath}${compName}`,\n to: compPath.toString()\n })\n }).done(data => {\n Files.list(compPath, err => {\n if (err) return;\n const fileListing = $('#file_listing').find(`[data-name=\"${data.saved_as}\"]`).parent();\n fileListing.addClass('success pulsate').delay(3000).queue(() => {\n fileListing.removeClass('success pulsate').dequeue();\n });\n });\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occured while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: 'Whoops!',\n html: true,\n text: error\n });\n });\n }\n}\n","\"use strict\";\n\n// Copyright (c) 2015 - 2017 Dane Everitt \n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nclass ContextMenuClass {\n constructor() {\n this.activeLine = null;\n }\n\n run() {\n this.directoryClick();\n this.rightClick();\n }\n\n makeMenu(parent) {\n $(document).find('#fileOptionMenu').remove();\n if (!_.isNull(this.activeLine)) this.activeLine.removeClass('active');\n\n let newFilePath = $('#file_listing').data('current-dir');\n if (parent.data('type') === 'folder') {\n const nameBlock = parent.find('td[data-identifier=\"name\"]');\n const currentName = decodeURIComponent(nameBlock.attr('data-name'));\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n newFilePath = `${currentPath}${currentName}`;\n }\n\n let buildMenu = '
      ';\n\n if (Pterodactyl.permissions.moveFiles) {\n buildMenu += '
    • Rename
    • \\\n
    • Move
    • ';\n }\n\n if (Pterodactyl.permissions.copyFiles) {\n buildMenu += '
    • Copy
    • ';\n }\n\n if (Pterodactyl.permissions.compressFiles) {\n buildMenu += '
    • Compress
    • ';\n }\n\n if (Pterodactyl.permissions.decompressFiles) {\n buildMenu += '
    • Decompress
    • ';\n }\n\n if (Pterodactyl.permissions.createFiles) {\n buildMenu += '
    • \\\n
    • New File
    • \\\n
    • New Folder
    • ';\n }\n\n if (Pterodactyl.permissions.downloadFiles || Pterodactyl.permissions.deleteFiles) {\n buildMenu += '
    • ';\n }\n\n if (Pterodactyl.permissions.downloadFiles) {\n buildMenu += '
    • Download
    • ';\n }\n\n if (Pterodactyl.permissions.deleteFiles) {\n buildMenu += '
    • Delete
    • ';\n }\n\n buildMenu += '
    ';\n return buildMenu;\n }\n\n rightClick() {\n $('[data-action=\"toggleMenu\"]').on('mousedown', event => {\n event.preventDefault();\n if ($(document).find('#fileOptionMenu').is(':visible')) {\n $('body').trigger('click');\n return;\n }\n this.showMenu(event);\n });\n $('#file_listing > tbody td').on('contextmenu', event => {\n this.showMenu(event);\n });\n }\n\n showMenu(event) {\n const parent = $(event.target).closest('tr');\n const menu = $(this.makeMenu(parent));\n\n if (parent.data('type') === 'disabled') return;\n event.preventDefault();\n\n $(menu).appendTo('body');\n $(menu).data('invokedOn', $(event.target)).show().css({\n position: 'absolute',\n left: event.pageX - 150,\n top: event.pageY,\n });\n\n this.activeLine = parent;\n this.activeLine.addClass('active');\n\n // Handle Events\n const Actions = new ActionsClass(parent, menu);\n if (Pterodactyl.permissions.moveFiles) {\n $(menu).find('li[data-action=\"move\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.move();\n });\n $(menu).find('li[data-action=\"rename\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.rename();\n });\n }\n\n if (Pterodactyl.permissions.copyFiles) {\n $(menu).find('li[data-action=\"copy\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.copy();\n });\n }\n\n if (Pterodactyl.permissions.compressFiles) {\n if (parent.data('type') === 'folder') {\n $(menu).find('li[data-action=\"compress\"]').removeClass('hidden');\n }\n $(menu).find('li[data-action=\"compress\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.compress();\n });\n }\n\n if (Pterodactyl.permissions.decompressFiles) {\n if (_.without(['application/zip', 'application/gzip', 'application/x-gzip'], parent.data('mime')).length < 3) {\n $(menu).find('li[data-action=\"decompress\"]').removeClass('hidden');\n }\n $(menu).find('li[data-action=\"decompress\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.decompress();\n });\n }\n\n if (Pterodactyl.permissions.createFiles) {\n $(menu).find('li[data-action=\"folder\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.folder();\n });\n }\n\n if (Pterodactyl.permissions.downloadFiles) {\n if (parent.data('type') === 'file') {\n $(menu).find('li[data-action=\"download\"]').removeClass('hidden');\n }\n $(menu).find('li[data-action=\"download\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.download();\n });\n }\n\n if (Pterodactyl.permissions.deleteFiles) {\n $(menu).find('li[data-action=\"delete\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.delete();\n });\n }\n\n $(window).unbind().on('click', event => {\n if($(event.target).is('.disable-menu-hide')) {\n event.preventDefault();\n return;\n }\n $(menu).unbind().remove();\n if(!_.isNull(this.activeLine)) this.activeLine.removeClass('active');\n });\n }\n\n directoryClick() {\n $('a[data-action=\"directory-view\"]').on('click', function (event) {\n event.preventDefault();\n\n const path = $(this).parent().data('path') || '';\n const name = $(this).parent().data('name') || '';\n\n window.location.hash = encodeURIComponent(path + name);\n Files.list();\n });\n }\n}\n\nwindow.ContextMenu = new ContextMenuClass;\n","\"use strict\";\n\n// Copyright (c) 2015 - 2017 Dane Everitt \n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nclass FileManager {\n constructor() {\n this.list(this.decodeHash());\n }\n\n list(path, next) {\n if (_.isUndefined(path)) {\n path = this.decodeHash();\n }\n\n this.loader(true);\n $.ajax({\n type: 'POST',\n url: Pterodactyl.meta.directoryList,\n headers: {\n 'X-CSRF-Token': Pterodactyl.meta.csrftoken,\n },\n data: {\n directory: path,\n },\n }).done(data => {\n this.loader(false);\n $('#load_files').slideUp(10).html(data).slideDown(10, () => {\n ContextMenu.run();\n this.reloadFilesButton();\n this.addFolderButton();\n this.selectItem();\n this.selectAll();\n this.selectiveDeletion();\n this.selectRow();\n if (_.isFunction(next)) {\n return next();\n }\n });\n $('#internal_alert').slideUp();\n\n if (typeof Siofu === 'object') {\n Siofu.listenOnInput(document.getElementById(\"files_touch_target\"));\n }\n }).fail(jqXHR => {\n this.loader(false);\n if (_.isFunction(next)) {\n return next(new Error('Failed to load file listing.'));\n }\n\n if ((path !== '' && path !== '/') && jqXHR.status === 404) {\n return this.list('', next);\n }\n\n swal({\n type: 'error',\n title: 'File Error',\n text: jqXHR.responseJSON.error || 'An error occured while attempting to process this request. Please try again.',\n });\n console.error(jqXHR);\n });\n }\n\n loader(show) {\n if (show){\n $('.file-overlay').fadeIn(100);\n } else {\n $('.file-overlay').fadeOut(100);\n }\n }\n\n reloadFilesButton() {\n $('i[data-action=\"reload-files\"]').unbind().on('click', () => {\n $('i[data-action=\"reload-files\"]').addClass('fa-spin');\n this.list();\n });\n }\n\n selectItem() {\n $('[data-action=\"addSelection\"]').on('click', event => {\n event.preventDefault();\n });\n }\n\n selectAll() {\n $('[data-action=\"selectAll\"]').on('click', event => {\n event.preventDefault();\n });\n }\n\n selectiveDeletion() {\n $('[data-action=\"selective-deletion\"]').on('mousedown', event => {\n new ActionsClass().deleteSelected();\n });\n }\n\n addFolderButton() {\n $('[data-action=\"add-folder\"]').unbind().on('click', () => {\n new ActionsClass().folder($('#file_listing').data('current-dir') || '/');\n })\n }\n\n selectRow() {\n $('#file_listing tr').on('mousedown', event => {\n if (event.which === 1) {\n if ($(event.target).is('th') || $(event.target).is('input[data-action=\"selectAll\"]')) {\n new ActionsClass().highlightAll(event);\n } else if ($(event.target).is('td') || $(event.target).is('input[data-action=\"addSelection\"]')) {\n new ActionsClass().toggleHighlight(event);\n }\n\n new ActionsClass().toggleMassActions();\n }\n });\n }\n\n decodeHash() {\n return decodeURIComponent(window.location.hash.substring(1));\n }\n\n}\n\nwindow.Files = new FileManager;\n"]} \ No newline at end of file +{"version":3,"sources":["src/actions.js","src/contextmenu.js","src/index.js"],"names":[],"mappings":"AAAA,a,8oBAqBM,a,YACF,sBAAY,OAAZ,CAAqB,IAArB,CAA2B,oCACvB,KAAK,OAAL,CAAe,OAAf,CACA,KAAK,IAAL,CAAY,IACf,C,kEAES,CACN,KAAK,OAAL,CAAe,SAClB,C,sCAEM,I,CAAM,CACT,GAAI,kBAAJ,CACA,GAAI,IAAJ,CAAU,CACN,WAAa,IAChB,CAFD,IAEO,CACH,GAAM,WAAY,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,4BAArB,CAAlB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAApB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAApB,CAEA,GAAI,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,MAArB,IAAiC,MAArC,CAA6C,CACzC,WAAa,WAChB,CAFD,IAEO,CACH,cAAgB,WAAhB,CAA8B,WAA9B,IACH,CACJ,CAED,KAAK,CACD,KAAM,OADL,CAED,MAAO,eAFN,CAGD,KAAM,8CAHL,CAID,iBAAkB,IAJjB,CAKD,kBAAmB,IALlB,CAMD,eAAgB,KANf,CAOD,oBAAqB,IAPpB,CAQD,WAAY,UARX,CAAL,CASG,SAAC,GAAD,CAAS,CACR,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,QAAS,CACL,iBAAkB,YAAY,MAAZ,CAAmB,YADhC,CAEL,kBAAmB,YAAY,MAAZ,CAAmB,IAFjC,CAFN,CAMH,YAAa,iCANV,CAOH,IAAQ,YAAY,IAAZ,CAAiB,MAAzB,OAAqC,YAAY,IAAZ,CAAiB,IAAtD,KAA8D,YAAY,IAAZ,CAAiB,YAA/E,yBAPG,CAQH,QAAS,KARN,CASH,KAAM,KAAK,SAAL,CAAe,CACjB,KAAM,GADW,CAAf,CATH,CAAP,EAYG,IAZH,CAYQ,cAAQ,CACZ,KAAK,KAAL,GACA,MAAM,IAAN,EACH,CAfD,EAeG,IAfH,CAeQ,eAAS,CACb,QAAQ,KAAR,CAAc,KAAd,EACA,GAAI,OAAQ,wDAAZ,CACA,GAAI,MAAO,OAAM,YAAb,GAA8B,WAA9B,EAA6C,MAAO,OAAM,YAAN,CAAmB,KAA1B,GAAoC,WAArF,CAAkG,CAC9F,MAAQ,MAAM,YAAN,CAAmB,KAC9B,CACD,KAAK,CACD,KAAM,OADL,CAED,MAAO,EAFN,CAGD,KAAM,KAHL,CAAL,CAKH,CA1BD,CA2BH,CArCD,CAsCH,C,mCAEM,CACH,GAAM,WAAY,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,4BAArB,CAAlB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,WAAf,CAAnB,CAApB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAApB,CAEA,KAAK,CACD,KAAM,OADL,CAED,MAAO,WAFN,CAGD,KAAM,+CAHL,CAID,iBAAkB,IAJjB,CAKD,kBAAmB,IALlB,CAMD,eAAgB,KANf,CAOD,oBAAqB,IAPpB,CAQD,cAAe,WAAf,CAA6B,WAR5B,CAAL,CASG,SAAC,GAAD,CAAS,CACR,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,QAAS,CACL,iBAAkB,YAAY,MAAZ,CAAmB,YADhC,CAEL,kBAAmB,YAAY,MAAZ,CAAmB,IAFjC,CAFN,CAMH,YAAa,iCANV,CAOH,IAAQ,YAAY,IAAZ,CAAiB,MAAzB,OAAqC,YAAY,IAAZ,CAAiB,IAAtD,KAA8D,YAAY,IAAZ,CAAiB,YAA/E,uBAPG,CAQH,QAAS,KARN,CASH,KAAM,KAAK,SAAL,CAAe,CACjB,QAAS,WAAT,CAAuB,WADN,CAEjB,MAAO,GAFU,CAAf,CATH,CAAP,EAaG,IAbH,CAaQ,cAAQ,CACZ,UAAU,MAAV,GAAmB,QAAnB,CAA4B,SAA5B,EAAuC,KAAvC,CAA6C,GAA7C,EAAkD,OAAlD,GACA,KAAK,KAAL,EACH,CAhBD,EAgBG,IAhBH,CAgBQ,eAAS,CACb,QAAQ,KAAR,CAAc,KAAd,EACA,GAAI,OAAQ,wDAAZ,CACA,GAAI,MAAO,OAAM,YAAb,GAA8B,WAA9B,EAA6C,MAAO,OAAM,YAAN,CAAmB,KAA1B,GAAoC,WAArF,CAAkG,CAC9F,MAAQ,MAAM,YAAN,CAAmB,KAC9B,CACD,KAAK,CACD,KAAM,OADL,CAED,MAAO,EAFN,CAGD,KAAM,KAHL,CAAL,CAKH,CA3BD,CA4BH,CAtCD,CAwCH,C,uCAEQ,CACL,GAAM,WAAY,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,4BAArB,CAAlB,CACA,GAAM,aAAc,UAAU,IAAV,CAAe,GAAf,CAApB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,WAAf,CAAnB,CAApB,CACA,GAAM,uFACwD,WADxD,4GAAN,CAKA,UAAU,IAAV,CAAe,YAAf,EACA,GAAM,YAAa,UAAU,IAAV,CAAe,OAAf,CAAnB,CACA,GAAM,aAAc,UAAU,IAAV,CAAe,eAAf,CAApB,CAEA,WAAW,KAAX,GACA,WAAW,EAAX,CAAc,cAAd,CAA8B,WAAK,CAE/B,GACK,EAAE,IAAF,GAAW,SAAX,EAAwB,EAAE,KAAF,GAAY,EAArC,EACG,EAAE,IAAF,GAAW,MADd,EAEI,EAAE,IAAF,GAAW,SAAX,EAAwB,EAAE,KAAF,GAAY,EAApC,EAA0C,cAAgB,WAAW,GAAX,EAHlE,CAIE,CACE,GAAI,CAAC,EAAE,OAAF,CAAU,WAAV,CAAL,CAA6B,CACzB,UAAU,IAAV,CAAe,WAAf,CACH,CAFD,IAEO,CACH,UAAU,IAAV,CAAe,WAAf,CACH,CACD,WAAW,MAAX,GACA,YAAY,MAAZ,GAAqB,GAArB,GACA,MACH,CAED,GAAI,EAAE,IAAF,GAAW,SAAX,EAAwB,EAAE,KAAF,GAAY,EAAxC,CAA4C,OAE5C,YAAY,IAAZ,GACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAApB,CAEA,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,QAAS,CACL,iBAAkB,YAAY,MAAZ,CAAmB,YADhC,CAEL,kBAAmB,YAAY,MAAZ,CAAmB,IAFjC,CAFN,CAMH,YAAa,iCANV,CAOH,IAAQ,YAAY,IAAZ,CAAiB,MAAzB,OAAqC,YAAY,IAAZ,CAAiB,IAAtD,KAA8D,YAAY,IAAZ,CAAiB,YAA/E,yBAPG,CAQH,QAAS,KARN,CASH,KAAM,KAAK,SAAL,CAAe,CACjB,QAAS,WAAT,CAAuB,WADN,CAEjB,MAAO,WAAP,CAAqB,WAAW,GAAX,EAFJ,CAAf,CATH,CAAP,EAaG,IAbH,CAaQ,cAAQ,CACZ,UAAU,IAAV,CAAe,WAAf,CAA4B,WAAW,GAAX,EAA5B,EACA,GAAI,CAAC,EAAE,OAAF,CAAU,WAAV,CAAL,CAA6B,CACzB,GAAI,SAAU,YAAY,IAAZ,CAAiB,MAAjB,CAAd,CACA,GAAI,UAAU,MAAV,GAAmB,IAAnB,CAAwB,MAAxB,IAAoC,QAAxC,CAAkD,CAC9C,QAAU,QAAQ,MAAR,CAAe,CAAf,CAAkB,QAAQ,WAAR,CAAoB,GAApB,CAAlB,EAA8C,GAA9C,CAAoD,WAAW,GAAX,EACjE,CACD,YAAY,IAAZ,CAAiB,MAAjB,CAAyB,OAAzB,EACA,UAAU,IAAV,CACI,YAAY,IAAZ,CAAiB,WAAW,GAAX,EAAjB,CADJ,CAGH,CATD,IASO,CACH,UAAU,IAAV,CAAe,WAAW,GAAX,EAAf,CACH,CACD,WAAW,MAAX,EACH,CA5BD,EA4BG,IA5BH,CA4BQ,eAAS,CACb,QAAQ,KAAR,CAAc,KAAd,EACA,GAAI,OAAQ,wDAAZ,CACA,GAAI,MAAO,OAAM,YAAb,GAA8B,WAA9B,EAA6C,MAAO,OAAM,YAAN,CAAmB,KAA1B,GAAoC,WAArF,CAAkG,CAC9F,MAAQ,MAAM,YAAN,CAAmB,KAC9B,CACD,UAAU,QAAV,CAAmB,WAAnB,EAAgC,KAAhC,CAAsC,IAAtC,EAA4C,KAA5C,CAAkD,UAAM,CACpD,UAAU,WAAV,CAAsB,WAAtB,EAAmC,OAAnC,EACH,CAFD,EAGA,WAAW,OAAX,CAAmB,CACf,UAAW,IADI,CAEf,UAAW,KAFI,CAGf,QAAS,KAHM,CAIf,MAAO,YAJQ,CAAnB,EAKG,OALH,CAKW,MALX,CAMH,CA3CD,EA2CG,MA3CH,CA2CU,UAAM,CACZ,YAAY,MAAZ,GACA,YAAY,MAAZ,GAAqB,GAArB,EACH,CA9CD,CA+CH,CArED,CAsEH,C,mCAEM,CACH,GAAM,WAAY,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,4BAArB,CAAlB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,WAAf,CAAnB,CAApB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAApB,CAEA,KAAK,CACD,KAAM,OADL,CAED,MAAO,WAFN,CAGD,KAAM,sDAHL,CAID,iBAAkB,IAJjB,CAKD,kBAAmB,IALlB,CAMD,eAAgB,KANf,CAOD,oBAAqB,IAPpB,CAQD,cAAe,WAAf,CAA6B,WAR5B,CAAL,CASG,SAAC,GAAD,CAAS,CACR,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,QAAS,CACL,iBAAkB,YAAY,MAAZ,CAAmB,YADhC,CAEL,kBAAmB,YAAY,MAAZ,CAAmB,IAFjC,CAFN,CAMH,YAAa,iCANV,CAOH,IAAQ,YAAY,IAAZ,CAAiB,MAAzB,OAAqC,YAAY,IAAZ,CAAiB,IAAtD,KAA8D,YAAY,IAAZ,CAAiB,YAA/E,uBAPG,CAQH,QAAS,KARN,CASH,KAAM,KAAK,SAAL,CAAe,CACjB,QAAS,WAAT,CAAuB,WADN,CAEjB,MAAO,GAFU,CAAf,CATH,CAAP,EAaG,IAbH,CAaQ,cAAQ,CACZ,KAAK,CACD,KAAM,SADL,CAED,MAAO,EAFN,CAGD,KAAM,2BAHL,CAAL,EAKA,MAAM,IAAN,EACH,CApBD,EAoBG,IApBH,CAoBQ,eAAS,CACb,QAAQ,KAAR,CAAc,KAAd,EACA,GAAI,OAAQ,wDAAZ,CACA,GAAI,MAAO,OAAM,YAAb,GAA8B,WAA9B,EAA6C,MAAO,OAAM,YAAN,CAAmB,KAA1B,GAAoC,WAArF,CAAkG,CAC9F,MAAQ,MAAM,YAAN,CAAmB,KAC9B,CACD,KAAK,CACD,KAAM,OADL,CAED,MAAO,EAFN,CAGD,KAAM,KAHL,CAAL,CAKH,CA/BD,CAgCH,CA1CD,CA2CH,C,2CAEU,CACP,GAAM,WAAY,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,4BAArB,CAAlB,CACA,GAAM,UAAW,mBAAmB,UAAU,IAAV,CAAe,WAAf,CAAnB,CAAjB,CACA,GAAM,UAAW,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAAjB,CAEA,OAAO,QAAP,YAA6B,YAAY,MAAZ,CAAmB,SAAhD,oBAA4E,QAA5E,CAAuF,QAC1F,C,wCAEQ,CACL,GAAM,WAAY,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,4BAArB,CAAlB,CACA,GAAM,SAAU,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAAhB,CACA,GAAM,SAAU,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAAhB,CAEA,KAAK,CACD,KAAM,SADL,CAED,MAAO,EAFN,CAGD,KAAM,yCAA2C,OAA3C,CAAqD,8DAH1D,CAID,KAAM,IAJL,CAKD,iBAAkB,IALjB,CAMD,kBAAmB,IANlB,CAOD,eAAgB,KAPf,CAQD,oBAAqB,IARpB,CAAL,CASG,UAAM,CACL,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,QAAS,CACL,iBAAkB,YAAY,MAAZ,CAAmB,YADhC,CAEL,kBAAmB,YAAY,MAAZ,CAAmB,IAFjC,CAFN,CAMH,YAAa,iCANV,CAOH,IAAQ,YAAY,IAAZ,CAAiB,MAAzB,OAAqC,YAAY,IAAZ,CAAiB,IAAtD,KAA8D,YAAY,IAAZ,CAAiB,YAA/E,yBAPG,CAQH,QAAS,KARN,CASH,KAAM,KAAK,SAAL,CAAe,CACjB,MAAO,IAAI,OAAJ,CAAc,OAAd,CADU,CAAf,CATH,CAAP,EAYG,IAZH,CAYQ,cAAQ,CACZ,UAAU,MAAV,GAAmB,QAAnB,CAA4B,SAA5B,EAAuC,KAAvC,CAA6C,GAA7C,EAAkD,OAAlD,GACA,KAAK,CACD,KAAM,SADL,CAED,MAAO,cAFN,CAAL,CAIH,CAlBD,EAkBG,IAlBH,CAkBQ,eAAS,CACb,QAAQ,KAAR,CAAc,KAAd,EACA,KAAK,CACD,KAAM,OADL,CAED,MAAO,SAFN,CAGD,KAAM,IAHL,CAID,KAAM,0EAJL,CAAL,CAMH,CA1BD,CA2BH,CArCD,CAsCH,C,6DAEmB,CAChB,GAAI,EAAE,8CAAF,EAAkD,MAAtD,CAA8D,CAC1D,EAAE,eAAF,EAAmB,WAAnB,CAA+B,UAA/B,CACH,CAFD,IAEO,CACH,EAAE,eAAF,EAAmB,QAAnB,CAA4B,UAA5B,CACH,CACJ,C,wDAEe,K,CAAO,CACnB,GAAM,QAAS,EAAE,MAAM,aAAR,CAAf,CACA,GAAM,MAAO,EAAE,MAAM,aAAR,EAAuB,IAAvB,CAA4B,OAA5B,CAAb,CAEA,GAAG,EAAE,IAAF,EAAQ,EAAR,CAAW,UAAX,CAAH,CAA2B,CACvB,EAAE,IAAF,EAAQ,IAAR,CAAa,SAAb,CAAwB,KAAxB,EACA,OAAO,WAAP,CAAmB,SAAnB,EAA8B,KAA9B,CAAoC,GAApC,CACH,CAHD,IAGO,CACH,EAAE,IAAF,EAAQ,IAAR,CAAa,SAAb,CAAwB,IAAxB,EACA,OAAO,QAAP,CAAgB,SAAhB,EAA2B,KAA3B,CAAiC,GAAjC,CACH,CACJ,C,kDAEY,K,CAAO,CAChB,GAAI,cAAJ,CACA,GAAM,MAAO,EAAE,MAAM,aAAR,EAAuB,IAAvB,CAA4B,OAA5B,CAAb,CAEA,GAAG,EAAE,IAAF,EAAQ,EAAR,CAAW,UAAX,CAAH,CAA2B,CACzB,EAAE,oCAAF,EAAwC,IAAxC,CAA6C,SAA7C,CAAwD,KAAxD,EACA,EAAE,iDAAF,EAAqD,IAArD,CAA0D,UAAW,CACjE,OAAS,EAAE,IAAF,EAAQ,OAAR,CAAgB,IAAhB,CAAT,CACA,OAAO,WAAP,CAAmB,SAAnB,EAA8B,KAA9B,CAAoC,GAApC,CACH,CAHD,CAID,CAND,IAMO,CACL,EAAE,oCAAF,EAAwC,IAAxC,CAA6C,SAA7C,CAAwD,IAAxD,EACA,EAAE,iDAAF,EAAqD,IAArD,CAA0D,UAAW,CACjE,OAAS,EAAE,IAAF,EAAQ,OAAR,CAAgB,IAAhB,CAAT,CACA,OAAO,QAAP,CAAgB,SAAhB,EAA2B,KAA3B,CAAiC,GAAjC,CACH,CAHD,CAID,CACJ,C,uDAEgB,CACb,GAAI,eAAgB,EAApB,CACA,GAAI,uBAAwB,EAA5B,CACA,GAAI,cAAJ,CACA,GAAI,iBAAJ,CACA,GAAI,mBAAJ,CAEA,EAAE,yDAAF,EAA6D,IAA7D,CAAkE,UAAW,CACzE,OAAS,EAAE,IAAF,EAAQ,OAAR,CAAgB,IAAhB,CAAT,CACA,UAAY,EAAE,MAAF,EAAU,IAAV,CAAe,4BAAf,CAAZ,CACA,YAAc,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,EAA6C,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAA3D,CAEA,cAAc,IAAd,CAAmB,WAAnB,EACA,sBAAsB,IAAtB,CAA2B,MAA3B,CACH,CAPD,EASA,GAAI,cAAc,MAAd,EAAwB,CAA5B,CACA,CACI,GAAI,gBAAiB,EAArB,CACA,EAAE,IAAF,CAAO,aAAP,CAAsB,SAAS,GAAT,CAAc,KAAd,CAAqB,CACzC,gBAAmB,SAAW,KAAX,CAAmB,WACvC,CAFD,EAIA,eAAiB,eAAe,KAAf,CAAqB,CAArB,CAAwB,CAAC,CAAzB,CAAjB,CAEA,KAAK,CACD,KAAM,SADL,CAED,MAAO,EAFN,CAGD,KAAM,mCAAqC,cAArC,CAAsD,uDAH3D,CAID,KAAM,IAJL,CAKD,iBAAkB,IALjB,CAMD,kBAAmB,IANlB,CAOD,eAAgB,KAPf,CAQD,oBAAqB,IARpB,CAAL,CASG,UAAM,CACL,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,QAAS,CACL,iBAAkB,YAAY,MAAZ,CAAmB,YADhC,CAEL,kBAAmB,YAAY,MAAZ,CAAmB,IAFjC,CAFN,CAMH,YAAa,iCANV,CAOH,IAAQ,YAAY,IAAZ,CAAiB,MAAzB,OAAqC,YAAY,IAAZ,CAAiB,IAAtD,KAA8D,YAAY,IAAZ,CAAiB,YAA/E,yBAPG,CAQH,QAAS,KARN,CASH,KAAM,KAAK,SAAL,CAAe,CACjB,MAAO,aADU,CAAf,CATH,CAAP,EAYG,IAZH,CAYQ,cAAQ,CACZ,EAAE,6BAAF,EAAiC,IAAjC,CAAsC,UAAW,CAC7C,EAAE,IAAF,EAAQ,IAAR,CAAa,SAAb,CAAwB,KAAxB,CACH,CAFD,EAIA,EAAE,IAAF,CAAO,qBAAP,CAA8B,UAAW,CACrC,EAAE,IAAF,EAAQ,QAAR,CAAiB,SAAjB,EAA4B,KAA5B,CAAkC,GAAlC,EAAuC,OAAvC,EACH,CAFD,EAIA,KAAK,CACD,KAAM,SADL,CAED,MAAO,eAFN,CAAL,CAIH,CAzBD,EAyBG,IAzBH,CAyBQ,eAAS,CACb,QAAQ,KAAR,CAAc,KAAd,EACA,KAAK,CACD,KAAM,OADL,CAED,MAAO,SAFN,CAGD,KAAM,IAHL,CAID,KAAM,4EAJL,CAAL,CAMH,CAjCD,CAkCH,CA5CD,CA6CH,CAtDD,IAsDO,CACH,KAAK,CACH,KAAM,SADH,CAEH,MAAO,EAFJ,CAGH,KAAM,wCAHH,CAAL,CAKH,CACJ,C,+CAEY,CACT,GAAM,WAAY,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,4BAArB,CAAlB,CACA,GAAM,UAAW,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAAjB,CACA,GAAM,UAAW,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAAjB,CAEA,KAAK,CACD,MAAO,wDADN,CAED,KAAM,4CAFL,CAGD,KAAM,IAHL,CAID,kBAAmB,KAJlB,CAKD,eAAgB,KALf,CAMD,kBAAmB,KANlB,CAAL,EASA,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,IAAQ,YAAY,IAAZ,CAAiB,MAAzB,OAAqC,YAAY,IAAZ,CAAiB,IAAtD,KAA8D,YAAY,IAAZ,CAAiB,YAA/E,6BAFG,CAGH,QAAS,CACL,iBAAkB,YAAY,MAAZ,CAAmB,YADhC,CAEL,kBAAmB,YAAY,MAAZ,CAAmB,IAFjC,CAHN,CAOH,YAAa,iCAPV,CAQH,KAAM,KAAK,SAAL,CAAe,CACjB,SAAU,QAAV,CAAqB,QADJ,CAAf,CARH,CAAP,EAWG,IAXH,CAWQ,cAAQ,CACZ,KAAK,KAAL,GACA,MAAM,IAAN,CAAW,QAAX,CACH,CAdD,EAcG,IAdH,CAcQ,eAAS,CACb,QAAQ,KAAR,CAAc,KAAd,EACA,GAAI,OAAQ,wDAAZ,CACA,GAAI,MAAO,OAAM,YAAb,GAA8B,WAA9B,EAA6C,MAAO,OAAM,YAAN,CAAmB,KAA1B,GAAoC,WAArF,CAAkG,CAC9F,MAAQ,MAAM,YAAN,CAAmB,KAC9B,CACD,KAAK,CACD,KAAM,OADL,CAED,MAAO,SAFN,CAGD,KAAM,IAHL,CAID,KAAM,KAJL,CAAL,CAMH,CA1BD,CA2BH,C,2CAEU,CACP,GAAM,WAAY,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,4BAArB,CAAlB,CACA,GAAM,UAAW,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAAjB,CACA,GAAM,UAAW,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAAjB,CAEA,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,IAAQ,YAAY,IAAZ,CAAiB,MAAzB,OAAqC,YAAY,IAAZ,CAAiB,IAAtD,KAA8D,YAAY,IAAZ,CAAiB,YAA/E,2BAFG,CAGH,QAAS,CACL,iBAAkB,YAAY,MAAZ,CAAmB,YADhC,CAEL,kBAAmB,YAAY,MAAZ,CAAmB,IAFjC,CAHN,CAOH,YAAa,iCAPV,CAQH,KAAM,KAAK,SAAL,CAAe,CACjB,SAAU,QAAV,CAAqB,QADJ,CAEjB,GAAI,SAAS,QAAT,EAFa,CAAf,CARH,CAAP,EAYG,IAZH,CAYQ,cAAQ,CACZ,MAAM,IAAN,CAAW,QAAX,CAAqB,aAAO,CACxB,GAAI,GAAJ,CAAS,OACT,GAAM,aAAc,EAAE,eAAF,EAAmB,IAAnB,gBAAuC,KAAK,QAA5C,OAA0D,MAA1D,EAApB,CACA,YAAY,QAAZ,CAAqB,iBAArB,EAAwC,KAAxC,CAA8C,IAA9C,EAAoD,KAApD,CAA0D,UAAM,CAC5D,YAAY,WAAZ,CAAwB,iBAAxB,EAA2C,OAA3C,EACH,CAFD,CAGH,CAND,CAOH,CApBD,EAoBG,IApBH,CAoBQ,eAAS,CACb,QAAQ,KAAR,CAAc,KAAd,EACA,GAAI,OAAQ,wDAAZ,CACA,GAAI,MAAO,OAAM,YAAb,GAA8B,WAA9B,EAA6C,MAAO,OAAM,YAAN,CAAmB,KAA1B,GAAoC,WAArF,CAAkG,CAC9F,MAAQ,MAAM,YAAN,CAAmB,KAC9B,CACD,KAAK,CACD,KAAM,OADL,CAED,MAAO,SAFN,CAGD,KAAM,IAHL,CAID,KAAM,KAJL,CAAL,CAMH,CAhCD,CAiCH,C;;AC3gBL,a,8oBAqBM,iB,YACF,2BAAc,wCACV,KAAK,UAAL,CAAkB,IACrB,C,8DAEK,CACF,KAAK,cAAL,GACA,KAAK,UAAL,EACH,C,0CAEQ,M,CAAQ,CACb,EAAE,QAAF,EAAY,IAAZ,CAAiB,iBAAjB,EAAoC,MAApC,GACA,GAAI,CAAC,EAAE,MAAF,CAAS,KAAK,UAAd,CAAL,CAAgC,KAAK,UAAL,CAAgB,WAAhB,CAA4B,QAA5B,EAEhC,GAAI,aAAc,EAAE,eAAF,EAAmB,IAAnB,CAAwB,aAAxB,CAAlB,CACA,GAAI,OAAO,IAAP,CAAY,MAAZ,IAAwB,QAA5B,CAAsC,CAClC,GAAM,WAAY,OAAO,IAAP,CAAY,4BAAZ,CAAlB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,WAAf,CAAnB,CAApB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAApB,CACA,eAAiB,WAAjB,CAA+B,WAClC,CAED,GAAI,WAAY,kFAAhB,CAEA,GAAI,YAAY,WAAZ,CAAwB,SAA5B,CAAuC,CACnC,WAAa,iPAEhB,CAED,GAAI,YAAY,WAAZ,CAAwB,SAA5B,CAAuC,CACnC,WAAa,kGAChB,CAED,GAAI,YAAY,WAAZ,CAAwB,aAA5B,CAA2C,CACvC,WAAa,kIAChB,CAED,GAAI,YAAY,WAAZ,CAAwB,eAA5B,CAA6C,CACzC,WAAa,8HAChB,CAED,GAAI,YAAY,WAAZ,CAAwB,WAA5B,CAAyC,CACrC,WAAa,+FAC4C,YAAY,MAAZ,CAAmB,SAD/D,CAC0E,kBAD1E,CAC+F,WAD/F,CAC6G,6MAE7H,CAED,GAAI,YAAY,WAAZ,CAAwB,aAAxB,EAAyC,YAAY,WAAZ,CAAwB,WAArE,CAAkF,CAC9E,WAAa,2BAChB,CAED,GAAI,YAAY,WAAZ,CAAwB,aAA5B,CAA2C,CACvC,WAAa,4HAChB,CAED,GAAI,YAAY,WAAZ,CAAwB,WAA5B,CAAyC,CACrC,WAAa,0HAChB,CAED,WAAa,OAAb,CACA,MAAO,UACV,C,+CAEY,gBACT,EAAE,4BAAF,EAAgC,EAAhC,CAAmC,WAAnC,CAAgD,eAAS,CACrD,MAAM,cAAN,GACA,GAAI,EAAE,QAAF,EAAY,IAAZ,CAAiB,iBAAjB,EAAoC,EAApC,CAAuC,UAAvC,CAAJ,CAAwD,CACpD,EAAE,MAAF,EAAU,OAAV,CAAkB,OAAlB,EACA,MACH,CACD,MAAK,QAAL,CAAc,KAAd,CACH,CAPD,EAQA,EAAE,0BAAF,EAA8B,EAA9B,CAAiC,aAAjC,CAAgD,eAAS,CACrD,MAAK,QAAL,CAAc,KAAd,CACH,CAFD,CAGH,C,0CAEQ,K,CAAO,iBACZ,GAAM,QAAS,EAAE,MAAM,MAAR,EAAgB,OAAhB,CAAwB,IAAxB,CAAf,CACA,GAAM,MAAO,EAAE,KAAK,QAAL,CAAc,MAAd,CAAF,CAAb,CAEA,GAAI,OAAO,IAAP,CAAY,MAAZ,IAAwB,UAA5B,CAAwC,OACxC,MAAM,cAAN,GAEA,EAAE,IAAF,EAAQ,QAAR,CAAiB,MAAjB,EACA,EAAE,IAAF,EAAQ,IAAR,CAAa,WAAb,CAA0B,EAAE,MAAM,MAAR,CAA1B,EAA2C,IAA3C,GAAkD,GAAlD,CAAsD,CAClD,SAAU,UADwC,CAElD,KAAM,MAAM,KAAN,CAAc,GAF8B,CAGlD,IAAK,MAAM,KAHuC,CAAtD,EAMA,KAAK,UAAL,CAAkB,MAAlB,CACA,KAAK,UAAL,CAAgB,QAAhB,CAAyB,QAAzB,EAGA,GAAM,SAAU,GAAI,aAAJ,CAAiB,MAAjB,CAAyB,IAAzB,CAAhB,CACA,GAAI,YAAY,WAAZ,CAAwB,SAA5B,CAAuC,CACnC,EAAE,IAAF,EAAQ,IAAR,CAAa,wBAAb,EAAuC,MAAvC,GAAgD,EAAhD,CAAmD,OAAnD,CAA4D,WAAK,CAC7D,EAAE,cAAF,GACA,QAAQ,IAAR,EACH,CAHD,EAIA,EAAE,IAAF,EAAQ,IAAR,CAAa,0BAAb,EAAyC,MAAzC,GAAkD,EAAlD,CAAqD,OAArD,CAA8D,WAAK,CAC/D,EAAE,cAAF,GACA,QAAQ,MAAR,EACH,CAHD,CAIH,CAED,GAAI,YAAY,WAAZ,CAAwB,SAA5B,CAAuC,CACnC,EAAE,IAAF,EAAQ,IAAR,CAAa,wBAAb,EAAuC,MAAvC,GAAgD,EAAhD,CAAmD,OAAnD,CAA4D,WAAK,CAC7D,EAAE,cAAF,GACA,QAAQ,IAAR,EACH,CAHD,CAIH,CAED,GAAI,YAAY,WAAZ,CAAwB,aAA5B,CAA2C,CACvC,GAAI,OAAO,IAAP,CAAY,MAAZ,IAAwB,QAA5B,CAAsC,CAClC,EAAE,IAAF,EAAQ,IAAR,CAAa,4BAAb,EAA2C,WAA3C,CAAuD,QAAvD,CACH,CACD,EAAE,IAAF,EAAQ,IAAR,CAAa,4BAAb,EAA2C,MAA3C,GAAoD,EAApD,CAAuD,OAAvD,CAAgE,WAAK,CACjE,EAAE,cAAF,GACA,QAAQ,QAAR,EACH,CAHD,CAIH,CAED,GAAI,YAAY,WAAZ,CAAwB,eAA5B,CAA6C,CACzC,GAAI,EAAE,OAAF,CAAU,CAAC,iBAAD,CAAoB,kBAApB,CAAwC,oBAAxC,CAAV,CAAyE,OAAO,IAAP,CAAY,MAAZ,CAAzE,EAA8F,MAA9F,CAAuG,CAA3G,CAA8G,CAC1G,EAAE,IAAF,EAAQ,IAAR,CAAa,8BAAb,EAA6C,WAA7C,CAAyD,QAAzD,CACH,CACD,EAAE,IAAF,EAAQ,IAAR,CAAa,8BAAb,EAA6C,MAA7C,GAAsD,EAAtD,CAAyD,OAAzD,CAAkE,WAAK,CACnE,EAAE,cAAF,GACA,QAAQ,UAAR,EACH,CAHD,CAIH,CAED,GAAI,YAAY,WAAZ,CAAwB,WAA5B,CAAyC,CACrC,EAAE,IAAF,EAAQ,IAAR,CAAa,0BAAb,EAAyC,MAAzC,GAAkD,EAAlD,CAAqD,OAArD,CAA8D,WAAK,CAC/D,EAAE,cAAF,GACA,QAAQ,MAAR,EACH,CAHD,CAIH,CAED,GAAI,YAAY,WAAZ,CAAwB,aAA5B,CAA2C,CACvC,GAAI,OAAO,IAAP,CAAY,MAAZ,IAAwB,MAA5B,CAAoC,CAChC,EAAE,IAAF,EAAQ,IAAR,CAAa,4BAAb,EAA2C,WAA3C,CAAuD,QAAvD,CACH,CACD,EAAE,IAAF,EAAQ,IAAR,CAAa,4BAAb,EAA2C,MAA3C,GAAoD,EAApD,CAAuD,OAAvD,CAAgE,WAAK,CACjE,EAAE,cAAF,GACA,QAAQ,QAAR,EACH,CAHD,CAIH,CAED,GAAI,YAAY,WAAZ,CAAwB,WAA5B,CAAyC,CACrC,EAAE,IAAF,EAAQ,IAAR,CAAa,0BAAb,EAAyC,MAAzC,GAAkD,EAAlD,CAAqD,OAArD,CAA8D,WAAK,CAC/D,EAAE,cAAF,GACA,QAAQ,MAAR,EACH,CAHD,CAIH,CAED,EAAE,MAAF,EAAU,MAAV,GAAmB,EAAnB,CAAsB,OAAtB,CAA+B,eAAS,CACpC,GAAG,EAAE,MAAM,MAAR,EAAgB,EAAhB,CAAmB,oBAAnB,CAAH,CAA6C,CACzC,MAAM,cAAN,GACA,MACH,CACD,EAAE,IAAF,EAAQ,MAAR,GAAiB,MAAjB,GACA,GAAG,CAAC,EAAE,MAAF,CAAS,OAAK,UAAd,CAAJ,CAA+B,OAAK,UAAL,CAAgB,WAAhB,CAA4B,QAA5B,CAClC,CAPD,CAQH,C,uDAEgB,CACb,EAAE,iCAAF,EAAqC,EAArC,CAAwC,OAAxC,CAAiD,SAAU,KAAV,CAAiB,CAC9D,MAAM,cAAN,GAEA,GAAM,MAAO,EAAE,IAAF,EAAQ,MAAR,GAAiB,IAAjB,CAAsB,MAAtB,GAAiC,EAA9C,CACA,GAAM,MAAO,EAAE,IAAF,EAAQ,MAAR,GAAiB,IAAjB,CAAsB,MAAtB,GAAiC,EAA9C,CAEA,OAAO,QAAP,CAAgB,IAAhB,CAAuB,mBAAmB,KAAO,IAA1B,CAAvB,CACA,MAAM,IAAN,EACH,CARD,CASH,C,+BAGL,OAAO,WAAP,CAAqB,GAAI,iBAAzB;AC1MA,a,q3BAqBM,Y,YACF,sBAAc,mCACV,KAAK,IAAL,CAAU,KAAK,UAAL,EAAV,CACH,C,0DAEI,I,CAAM,I,CAAM,gBACb,GAAI,EAAE,WAAF,CAAc,IAAd,CAAJ,CAAyB,CACrB,KAAO,KAAK,UAAL,EACV,CAED,KAAK,MAAL,CAAY,IAAZ,EACA,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,IAAK,YAAY,IAAZ,CAAiB,aAFnB,CAGH,QAAS,CACL,eAAgB,YAAY,IAAZ,CAAiB,SAD5B,CAHN,CAMH,KAAM,CACF,UAAW,IADT,CANH,CAAP,EASG,IATH,CASQ,cAAQ,CACZ,MAAK,MAAL,CAAY,KAAZ,EACA,EAAE,aAAF,EAAiB,OAAjB,CAAyB,EAAzB,EAA6B,IAA7B,CAAkC,IAAlC,EAAwC,SAAxC,CAAkD,EAAlD,CAAsD,UAAM,CACxD,YAAY,GAAZ,GACA,MAAK,iBAAL,GACA,MAAK,eAAL,GACA,MAAK,UAAL,GACA,MAAK,SAAL,GACA,MAAK,iBAAL,GACA,MAAK,SAAL,GACA,GAAI,EAAE,UAAF,CAAa,IAAb,CAAJ,CAAwB,CACpB,MAAO,OACV,CACJ,CAXD,EAYA,EAAE,iBAAF,EAAqB,OAArB,GAEA,GAAI,OAAO,MAAP,mCAAO,KAAP,KAAiB,QAArB,CAA+B,CAC3B,MAAM,aAAN,CAAoB,SAAS,cAAT,CAAwB,oBAAxB,CAApB,CACH,CACJ,CA5BD,EA4BG,IA5BH,CA4BQ,eAAS,CACb,MAAK,MAAL,CAAY,KAAZ,EACA,GAAI,EAAE,UAAF,CAAa,IAAb,CAAJ,CAAwB,CACpB,MAAO,MAAK,GAAI,MAAJ,CAAU,8BAAV,CAAL,CACV,CAED,GAAK,OAAS,EAAT,EAAe,OAAS,GAAzB,EAAiC,MAAM,MAAN,GAAiB,GAAtD,CAA2D,CACvD,MAAO,OAAK,IAAL,CAAU,EAAV,CAAc,IAAd,CACV,CAED,KAAK,CACD,KAAM,OADL,CAED,MAAO,YAFN,CAGD,KAAM,MAAM,YAAN,CAAmB,MAAnB,CAA0B,CAA1B,EAA6B,MAA7B,EAAuC,8EAH5C,CAAL,EAKA,QAAQ,KAAR,CAAc,KAAd,CACH,CA5CD,CA6CH,C,sCAEM,I,CAAM,CACT,GAAI,IAAJ,CAAS,CACL,EAAE,eAAF,EAAmB,MAAnB,CAA0B,GAA1B,CACH,CAFD,IAEO,CACH,EAAE,eAAF,EAAmB,OAAnB,CAA2B,GAA3B,CACH,CACJ,C,6DAEmB,iBAChB,EAAE,+BAAF,EAAmC,MAAnC,GAA4C,EAA5C,CAA+C,OAA/C,CAAwD,UAAM,CAC1D,EAAE,+BAAF,EAAmC,QAAnC,CAA4C,SAA5C,EACA,OAAK,IAAL,EACH,CAHD,CAIH,C,+CAEY,CACT,EAAE,8BAAF,EAAkC,EAAlC,CAAqC,OAArC,CAA8C,eAAS,CACnD,MAAM,cAAN,EACH,CAFD,CAGH,C,6CAEW,CACR,EAAE,2BAAF,EAA+B,EAA/B,CAAkC,OAAlC,CAA2C,eAAS,CAChD,MAAM,cAAN,EACH,CAFD,CAGH,C,6DAEmB,CAChB,EAAE,oCAAF,EAAwC,EAAxC,CAA2C,WAA3C,CAAwD,eAAS,CAC7D,GAAI,aAAJ,GAAmB,cAAnB,EACH,CAFD,CAGH,C,yDAEiB,CACd,EAAE,4BAAF,EAAgC,MAAhC,GAAyC,EAAzC,CAA4C,OAA5C,CAAqD,UAAM,CACvD,GAAI,aAAJ,GAAmB,MAAnB,CAA0B,EAAE,eAAF,EAAmB,IAAnB,CAAwB,aAAxB,GAA0C,GAApE,CACH,CAFD,CAGH,C,6CAEW,CACV,EAAE,kBAAF,EAAsB,EAAtB,CAAyB,WAAzB,CAAsC,eAAS,CAC3C,GAAI,MAAM,KAAN,GAAgB,CAApB,CAAuB,CACnB,GAAI,EAAE,MAAM,MAAR,EAAgB,EAAhB,CAAmB,IAAnB,GAA4B,EAAE,MAAM,MAAR,EAAgB,EAAhB,CAAmB,gCAAnB,CAAhC,CAAsF,CAClF,GAAI,aAAJ,GAAmB,YAAnB,CAAgC,KAAhC,CACH,CAFD,IAEO,IAAI,EAAE,MAAM,MAAR,EAAgB,EAAhB,CAAmB,IAAnB,GAA4B,EAAE,MAAM,MAAR,EAAgB,EAAhB,CAAmB,mCAAnB,CAAhC,CAAyF,CAC5F,GAAI,aAAJ,GAAmB,eAAnB,CAAmC,KAAnC,CACH,CAED,GAAI,aAAJ,GAAmB,iBAAnB,EACH,CACJ,CAVD,CAWD,C,+CAEY,CACT,MAAO,oBAAmB,OAAO,QAAP,CAAgB,IAAhB,CAAqB,SAArB,CAA+B,CAA/B,CAAnB,CACV,C,0BAIL,OAAO,KAAP,CAAe,GAAI,YAAnB","file":"filemanager.min.js","sourcesContent":["\"use strict\";\n\n// Copyright (c) 2015 - 2017 Dane Everitt \n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nclass ActionsClass {\n constructor(element, menu) {\n this.element = element;\n this.menu = menu;\n }\n\n destroy() {\n this.element = undefined;\n }\n\n folder(path) {\n let inputValue\n if (path) {\n inputValue = path\n } else {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const currentName = decodeURIComponent(nameBlock.data('name'));\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n\n if ($(this.element).data('type') === 'file') {\n inputValue = currentPath;\n } else {\n inputValue = `${currentPath}${currentName}/`;\n }\n }\n\n swal({\n type: 'input',\n title: 'Create Folder',\n text: 'Please enter the path and folder name below.',\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true,\n inputValue: inputValue\n }, (val) => {\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/folder`,\n timeout: 10000,\n data: JSON.stringify({\n path: val,\n }),\n }).done(data => {\n swal.close();\n Files.list();\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occured while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: '',\n text: error,\n });\n });\n });\n }\n\n move() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const currentName = decodeURIComponent(nameBlock.attr('data-name'));\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n\n swal({\n type: 'input',\n title: 'Move File',\n text: 'Please enter the new path for the file below.',\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true,\n inputValue: `${currentPath}${currentName}`,\n }, (val) => {\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/move`,\n timeout: 10000,\n data: JSON.stringify({\n from: `${currentPath}${currentName}`,\n to: `${val}`,\n }),\n }).done(data => {\n nameBlock.parent().addClass('warning').delay(200).fadeOut();\n swal.close();\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occured while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: '',\n text: error,\n });\n });\n });\n\n }\n\n rename() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const currentLink = nameBlock.find('a');\n const currentName = decodeURIComponent(nameBlock.attr('data-name'));\n const attachEditor = `\n \n \n `;\n\n nameBlock.html(attachEditor);\n const inputField = nameBlock.find('input');\n const inputLoader = nameBlock.find('.input-loader');\n\n inputField.focus();\n inputField.on('blur keydown', e => {\n // Save Field\n if (\n (e.type === 'keydown' && e.which === 27)\n || e.type === 'blur'\n || (e.type === 'keydown' && e.which === 13 && currentName === inputField.val())\n ) {\n if (!_.isEmpty(currentLink)) {\n nameBlock.html(currentLink);\n } else {\n nameBlock.html(currentName);\n }\n inputField.remove();\n ContextMenu.unbind().run();\n return;\n }\n\n if (e.type === 'keydown' && e.which !== 13) return;\n\n inputLoader.show();\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/rename`,\n timeout: 10000,\n data: JSON.stringify({\n from: `${currentPath}${currentName}`,\n to: `${currentPath}${inputField.val()}`,\n }),\n }).done(data => {\n nameBlock.attr('data-name', inputField.val());\n if (!_.isEmpty(currentLink)) {\n let newLink = currentLink.attr('href');\n if (nameBlock.parent().data('type') !== 'folder') {\n newLink = newLink.substr(0, newLink.lastIndexOf('/')) + '/' + inputField.val();\n }\n currentLink.attr('href', newLink);\n nameBlock.html(\n currentLink.html(inputField.val())\n );\n } else {\n nameBlock.html(inputField.val());\n }\n inputField.remove();\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occured while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n nameBlock.addClass('has-error').delay(2000).queue(() => {\n nameBlock.removeClass('has-error').dequeue();\n });\n inputField.popover({\n animation: true,\n placement: 'top',\n content: error,\n title: 'Save Error'\n }).popover('show');\n }).always(() => {\n inputLoader.remove();\n ContextMenu.unbind().run();\n });\n });\n }\n\n copy() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const currentName = decodeURIComponent(nameBlock.attr('data-name'));\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n\n swal({\n type: 'input',\n title: 'Copy File',\n text: 'Please enter the new path for the copied file below.',\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true,\n inputValue: `${currentPath}${currentName}`,\n }, (val) => {\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/copy`,\n timeout: 10000,\n data: JSON.stringify({\n from: `${currentPath}${currentName}`,\n to: `${val}`,\n }),\n }).done(data => {\n swal({\n type: 'success',\n title: '',\n text: 'File successfully copied.'\n });\n Files.list();\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occured while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: '',\n text: error,\n });\n });\n });\n }\n\n download() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const fileName = decodeURIComponent(nameBlock.attr('data-name'));\n const filePath = decodeURIComponent(nameBlock.data('path'));\n\n window.location = `/server/${Pterodactyl.server.uuidShort}/files/download/${filePath}${fileName}`;\n }\n\n delete() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const delPath = decodeURIComponent(nameBlock.data('path'));\n const delName = decodeURIComponent(nameBlock.data('name'));\n\n swal({\n type: 'warning',\n title: '',\n text: 'Are you sure you want to delete ' + delName + '? There is no reversing this action.',\n html: true,\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true\n }, () => {\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/delete`,\n timeout: 10000,\n data: JSON.stringify({\n items: [`${delPath}${delName}`]\n }),\n }).done(data => {\n nameBlock.parent().addClass('warning').delay(200).fadeOut();\n swal({\n type: 'success',\n title: 'File Deleted'\n });\n }).fail(jqXHR => {\n console.error(jqXHR);\n swal({\n type: 'error',\n title: 'Whoops!',\n html: true,\n text: 'An error occured while attempting to delete this file. Please try again.',\n });\n });\n });\n }\n\n toggleMassActions() {\n if ($('#file_listing input[type=\"checkbox\"]:checked').length) {\n $('#mass_actions').removeClass('disabled');\n } else {\n $('#mass_actions').addClass('disabled');\n }\n }\n\n toggleHighlight(event) {\n const parent = $(event.currentTarget);\n const item = $(event.currentTarget).find('input');\n\n if($(item).is(':checked')) {\n $(item).prop('checked', false);\n parent.removeClass('warning').delay(200);\n } else {\n $(item).prop('checked', true);\n parent.addClass('warning').delay(200);\n }\n }\n\n highlightAll(event) {\n let parent;\n const item = $(event.currentTarget).find('input');\n\n if($(item).is(':checked')) {\n $('#file_listing input[type=checkbox]').prop('checked', false);\n $('#file_listing input[data-action=\"addSelection\"]').each(function() {\n parent = $(this).closest('tr');\n parent.removeClass('warning').delay(200);\n });\n } else {\n $('#file_listing input[type=checkbox]').prop('checked', true);\n $('#file_listing input[data-action=\"addSelection\"]').each(function() {\n parent = $(this).closest('tr');\n parent.addClass('warning').delay(200);\n });\n }\n }\n\n deleteSelected() {\n let selectedItems = [];\n let selectedItemsElements = [];\n let parent;\n let nameBlock;\n let delLocation;\n\n $('#file_listing input[data-action=\"addSelection\"]:checked').each(function() {\n parent = $(this).closest('tr');\n nameBlock = $(parent).find('td[data-identifier=\"name\"]');\n delLocation = decodeURIComponent(nameBlock.data('path')) + decodeURIComponent(nameBlock.data('name'));\n\n selectedItems.push(delLocation);\n selectedItemsElements.push(parent);\n });\n\n if (selectedItems.length != 0)\n {\n let formattedItems = \"\";\n $.each(selectedItems, function(key, value) {\n formattedItems += (\"\" + value + \", \");\n })\n\n formattedItems = formattedItems.slice(0, -2);\n\n swal({\n type: 'warning',\n title: '',\n text: 'Are you sure you want to delete:' + formattedItems + '? There is no reversing this action.',\n html: true,\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true\n }, () => {\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/delete`,\n timeout: 10000,\n data: JSON.stringify({\n items: selectedItems\n }),\n }).done(data => {\n $('#file_listing input:checked').each(function() {\n $(this).prop('checked', false);\n });\n\n $.each(selectedItemsElements, function() {\n $(this).addClass('warning').delay(200).fadeOut();\n })\n\n swal({\n type: 'success',\n title: 'Files Deleted'\n });\n }).fail(jqXHR => {\n console.error(jqXHR);\n swal({\n type: 'error',\n title: 'Whoops!',\n html: true,\n text: 'An error occured while attempting to delete these files. Please try again.',\n });\n });\n });\n } else {\n swal({\n type: 'warning',\n title: '',\n text: 'Please select files/folders to delete.',\n });\n }\n }\n\n decompress() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const compPath = decodeURIComponent(nameBlock.data('path'));\n const compName = decodeURIComponent(nameBlock.data('name'));\n\n swal({\n title: ' Decompressing...',\n text: 'This might take a few seconds to complete.',\n html: true,\n allowOutsideClick: false,\n allowEscapeKey: false,\n showConfirmButton: false,\n });\n\n $.ajax({\n type: 'POST',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/decompress`,\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n data: JSON.stringify({\n files: `${compPath}${compName}`\n })\n }).done(data => {\n swal.close();\n Files.list(compPath);\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occured while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: 'Whoops!',\n html: true,\n text: error\n });\n });\n }\n\n compress() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const compPath = decodeURIComponent(nameBlock.data('path'));\n const compName = decodeURIComponent(nameBlock.data('name'));\n\n $.ajax({\n type: 'POST',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/compress`,\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n data: JSON.stringify({\n files: `${compPath}${compName}`,\n to: compPath.toString()\n })\n }).done(data => {\n Files.list(compPath, err => {\n if (err) return;\n const fileListing = $('#file_listing').find(`[data-name=\"${data.saved_as}\"]`).parent();\n fileListing.addClass('success pulsate').delay(3000).queue(() => {\n fileListing.removeClass('success pulsate').dequeue();\n });\n });\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occured while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: 'Whoops!',\n html: true,\n text: error\n });\n });\n }\n}\n","\"use strict\";\n\n// Copyright (c) 2015 - 2017 Dane Everitt \n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nclass ContextMenuClass {\n constructor() {\n this.activeLine = null;\n }\n\n run() {\n this.directoryClick();\n this.rightClick();\n }\n\n makeMenu(parent) {\n $(document).find('#fileOptionMenu').remove();\n if (!_.isNull(this.activeLine)) this.activeLine.removeClass('active');\n\n let newFilePath = $('#file_listing').data('current-dir');\n if (parent.data('type') === 'folder') {\n const nameBlock = parent.find('td[data-identifier=\"name\"]');\n const currentName = decodeURIComponent(nameBlock.attr('data-name'));\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n newFilePath = `${currentPath}${currentName}`;\n }\n\n let buildMenu = '
      ';\n\n if (Pterodactyl.permissions.moveFiles) {\n buildMenu += '
    • Rename
    • \\\n
    • Move
    • ';\n }\n\n if (Pterodactyl.permissions.copyFiles) {\n buildMenu += '
    • Copy
    • ';\n }\n\n if (Pterodactyl.permissions.compressFiles) {\n buildMenu += '
    • Compress
    • ';\n }\n\n if (Pterodactyl.permissions.decompressFiles) {\n buildMenu += '
    • Decompress
    • ';\n }\n\n if (Pterodactyl.permissions.createFiles) {\n buildMenu += '
    • \\\n
    • New File
    • \\\n
    • New Folder
    • ';\n }\n\n if (Pterodactyl.permissions.downloadFiles || Pterodactyl.permissions.deleteFiles) {\n buildMenu += '
    • ';\n }\n\n if (Pterodactyl.permissions.downloadFiles) {\n buildMenu += '
    • Download
    • ';\n }\n\n if (Pterodactyl.permissions.deleteFiles) {\n buildMenu += '
    • Delete
    • ';\n }\n\n buildMenu += '
    ';\n return buildMenu;\n }\n\n rightClick() {\n $('[data-action=\"toggleMenu\"]').on('mousedown', event => {\n event.preventDefault();\n if ($(document).find('#fileOptionMenu').is(':visible')) {\n $('body').trigger('click');\n return;\n }\n this.showMenu(event);\n });\n $('#file_listing > tbody td').on('contextmenu', event => {\n this.showMenu(event);\n });\n }\n\n showMenu(event) {\n const parent = $(event.target).closest('tr');\n const menu = $(this.makeMenu(parent));\n\n if (parent.data('type') === 'disabled') return;\n event.preventDefault();\n\n $(menu).appendTo('body');\n $(menu).data('invokedOn', $(event.target)).show().css({\n position: 'absolute',\n left: event.pageX - 150,\n top: event.pageY,\n });\n\n this.activeLine = parent;\n this.activeLine.addClass('active');\n\n // Handle Events\n const Actions = new ActionsClass(parent, menu);\n if (Pterodactyl.permissions.moveFiles) {\n $(menu).find('li[data-action=\"move\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.move();\n });\n $(menu).find('li[data-action=\"rename\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.rename();\n });\n }\n\n if (Pterodactyl.permissions.copyFiles) {\n $(menu).find('li[data-action=\"copy\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.copy();\n });\n }\n\n if (Pterodactyl.permissions.compressFiles) {\n if (parent.data('type') === 'folder') {\n $(menu).find('li[data-action=\"compress\"]').removeClass('hidden');\n }\n $(menu).find('li[data-action=\"compress\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.compress();\n });\n }\n\n if (Pterodactyl.permissions.decompressFiles) {\n if (_.without(['application/zip', 'application/gzip', 'application/x-gzip'], parent.data('mime')).length < 3) {\n $(menu).find('li[data-action=\"decompress\"]').removeClass('hidden');\n }\n $(menu).find('li[data-action=\"decompress\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.decompress();\n });\n }\n\n if (Pterodactyl.permissions.createFiles) {\n $(menu).find('li[data-action=\"folder\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.folder();\n });\n }\n\n if (Pterodactyl.permissions.downloadFiles) {\n if (parent.data('type') === 'file') {\n $(menu).find('li[data-action=\"download\"]').removeClass('hidden');\n }\n $(menu).find('li[data-action=\"download\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.download();\n });\n }\n\n if (Pterodactyl.permissions.deleteFiles) {\n $(menu).find('li[data-action=\"delete\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.delete();\n });\n }\n\n $(window).unbind().on('click', event => {\n if($(event.target).is('.disable-menu-hide')) {\n event.preventDefault();\n return;\n }\n $(menu).unbind().remove();\n if(!_.isNull(this.activeLine)) this.activeLine.removeClass('active');\n });\n }\n\n directoryClick() {\n $('a[data-action=\"directory-view\"]').on('click', function (event) {\n event.preventDefault();\n\n const path = $(this).parent().data('path') || '';\n const name = $(this).parent().data('name') || '';\n\n window.location.hash = encodeURIComponent(path + name);\n Files.list();\n });\n }\n}\n\nwindow.ContextMenu = new ContextMenuClass;\n","\"use strict\";\n\n// Copyright (c) 2015 - 2017 Dane Everitt \n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nclass FileManager {\n constructor() {\n this.list(this.decodeHash());\n }\n\n list(path, next) {\n if (_.isUndefined(path)) {\n path = this.decodeHash();\n }\n\n this.loader(true);\n $.ajax({\n type: 'POST',\n url: Pterodactyl.meta.directoryList,\n headers: {\n 'X-CSRF-Token': Pterodactyl.meta.csrftoken,\n },\n data: {\n directory: path,\n },\n }).done(data => {\n this.loader(false);\n $('#load_files').slideUp(10).html(data).slideDown(10, () => {\n ContextMenu.run();\n this.reloadFilesButton();\n this.addFolderButton();\n this.selectItem();\n this.selectAll();\n this.selectiveDeletion();\n this.selectRow();\n if (_.isFunction(next)) {\n return next();\n }\n });\n $('#internal_alert').slideUp();\n\n if (typeof Siofu === 'object') {\n Siofu.listenOnInput(document.getElementById(\"files_touch_target\"));\n }\n }).fail(jqXHR => {\n this.loader(false);\n if (_.isFunction(next)) {\n return next(new Error('Failed to load file listing.'));\n }\n\n if ((path !== '' && path !== '/') && jqXHR.status === 404) {\n return this.list('', next);\n }\n\n swal({\n type: 'error',\n title: 'File Error',\n text: jqXHR.responseJSON.errors[0].detail || 'An error occured while attempting to process this request. Please try again.',\n });\n console.error(jqXHR);\n });\n }\n\n loader(show) {\n if (show){\n $('.file-overlay').fadeIn(100);\n } else {\n $('.file-overlay').fadeOut(100);\n }\n }\n\n reloadFilesButton() {\n $('i[data-action=\"reload-files\"]').unbind().on('click', () => {\n $('i[data-action=\"reload-files\"]').addClass('fa-spin');\n this.list();\n });\n }\n\n selectItem() {\n $('[data-action=\"addSelection\"]').on('click', event => {\n event.preventDefault();\n });\n }\n\n selectAll() {\n $('[data-action=\"selectAll\"]').on('click', event => {\n event.preventDefault();\n });\n }\n\n selectiveDeletion() {\n $('[data-action=\"selective-deletion\"]').on('mousedown', event => {\n new ActionsClass().deleteSelected();\n });\n }\n\n addFolderButton() {\n $('[data-action=\"add-folder\"]').unbind().on('click', () => {\n new ActionsClass().folder($('#file_listing').data('current-dir') || '/');\n })\n }\n\n selectRow() {\n $('#file_listing tr').on('mousedown', event => {\n if (event.which === 1) {\n if ($(event.target).is('th') || $(event.target).is('input[data-action=\"selectAll\"]')) {\n new ActionsClass().highlightAll(event);\n } else if ($(event.target).is('td') || $(event.target).is('input[data-action=\"addSelection\"]')) {\n new ActionsClass().toggleHighlight(event);\n }\n\n new ActionsClass().toggleMassActions();\n }\n });\n }\n\n decodeHash() {\n return decodeURIComponent(window.location.hash.substring(1));\n }\n\n}\n\nwindow.Files = new FileManager;\n"]} \ No newline at end of file diff --git a/public/themes/pterodactyl/js/frontend/files/src/index.js b/public/themes/pterodactyl/js/frontend/files/src/index.js index e8011d6cd..b407c0f0b 100644 --- a/public/themes/pterodactyl/js/frontend/files/src/index.js +++ b/public/themes/pterodactyl/js/frontend/files/src/index.js @@ -71,7 +71,7 @@ class FileManager { swal({ type: 'error', title: 'File Error', - text: jqXHR.responseJSON.error || 'An error occured while attempting to process this request. Please try again.', + text: jqXHR.responseJSON.errors[0].detail || 'An error occured while attempting to process this request. Please try again.', }); console.error(jqXHR); }); diff --git a/resources/lang/en/base.php b/resources/lang/en/base.php index 579ac3410..ba63139d3 100644 --- a/resources/lang/en/base.php +++ b/resources/lang/en/base.php @@ -29,183 +29,25 @@ return [ ], 'api' => [ 'index' => [ - 'header' => 'API Access', - 'header_sub' => 'Manage your API access keys.', - 'list' => 'API Keys', + 'list' => 'Your Keys', + 'header' => 'Accout API', + 'header_sub' => 'Manage access keys that allow you to perform actions aganist the panel.', 'create_new' => 'Create New API key', 'keypair_created' => 'An API key has been successfully generated and is listed below.', ], 'new' => [ 'header' => 'New API Key', - 'header_sub' => 'Create a new API access key', + 'header_sub' => 'Create a new account access key.', 'form_title' => 'Details', 'descriptive_memo' => [ - 'title' => 'Descriptive Memo', - 'description' => 'Enter a brief description of what this API key will be used for.', + 'title' => 'Description', + 'description' => 'Enter a brief description of this key that will be useful for reference.', ], 'allowed_ips' => [ 'title' => 'Allowed IPs', 'description' => 'Enter a line delimitated list of IPs that are allowed to access the API using this key. CIDR notation is allowed. Leave blank to allow any IP.', ], ], - 'permissions' => [ - 'user' => [ - 'server_header' => 'User Server Permissions', - 'server' => [ - 'list' => [ - 'title' => 'List Servers', - 'desc' => 'Allows listing of all servers a user owns or has access to as a subuser.', - ], - 'view' => [ - 'title' => 'View Server', - 'desc' => 'Allows viewing of specific server user can access.', - ], - 'power' => [ - 'title' => 'Toggle Power', - 'desc' => 'Allow toggling of power status for a server.', - ], - 'command' => [ - 'title' => 'Send Command', - 'desc' => 'Allow sending of a command to a running server.', - ], - ], - ], - 'admin' => [ - 'server_header' => 'Server Control', - 'server' => [ - 'list' => [ - 'title' => 'List Servers', - 'desc' => 'Allows listing of all servers currently on the system.', - ], - 'view' => [ - 'title' => 'View Server', - 'desc' => 'Allows view of single server including service and details.', - ], - 'delete' => [ - 'title' => 'Delete Server', - 'desc' => 'Allows deletion of a server from the system.', - ], - 'create' => [ - 'title' => 'Create Server', - 'desc' => 'Allows creation of a new server on the system.', - ], - 'edit-details' => [ - 'title' => 'Edit Server Details', - 'desc' => 'Allows editing of server details such as name, owner, description, and secret key.', - ], - 'edit-container' => [ - 'title' => 'Edit Server Container', - 'desc' => 'Allows for modification of the docker container the server runs in.', - ], - 'suspend' => [ - 'title' => 'Suspend Server', - 'desc' => 'Allows for the suspension and unsuspension of a given server.', - ], - 'install' => [ - 'title' => 'Toggle Install Status', - 'desc' => '', - ], - 'rebuild' => [ - 'title' => 'Rebuild Server', - 'desc' => '', - ], - 'edit-build' => [ - 'title' => 'Edit Server Build', - 'desc' => 'Allows editing of server build setting such as CPU and memory allocations.', - ], - 'edit-startup' => [ - 'title' => 'Edit Server Startup', - 'desc' => 'Allows modification of server startup commands and parameters.', - ], - ], - 'location_header' => 'Location Control', - 'location' => [ - 'list' => [ - 'title' => 'List Locations', - 'desc' => 'Allows listing all locations and thier associated nodes.', - ], - ], - 'node_header' => 'Node Control', - 'node' => [ - 'list' => [ - 'title' => 'List Nodes', - 'desc' => 'Allows listing of all nodes currently on the system.', - ], - 'view' => [ - 'title' => 'View Node', - 'desc' => 'Allows viewing details about a specific node including active services.', - ], - 'view-config' => [ - 'title' => 'View Node Configuration', - 'desc' => 'Danger. This allows the viewing of the node configuration file used by the daemon, and exposes secret daemon tokens.', - ], - 'create' => [ - 'title' => 'Create Node', - 'desc' => 'Allows creating a new node on the system.', - ], - 'delete' => [ - 'title' => 'Delete Node', - 'desc' => 'Allows deletion of a node from the system.', - ], - ], - 'user_header' => 'User Control', - 'user' => [ - 'list' => [ - 'title' => 'List Users', - 'desc' => 'Allows listing of all users currently on the system.', - ], - 'view' => [ - 'title' => 'View User', - 'desc' => 'Allows viewing details about a specific user including active services.', - ], - 'create' => [ - 'title' => 'Create User', - 'desc' => 'Allows creating a new user on the system.', - ], - 'edit' => [ - 'title' => 'Update User', - 'desc' => 'Allows modification of user details.', - ], - 'delete' => [ - 'title' => 'Delete User', - 'desc' => 'Allows deleting a user.', - ], - ], - 'service_header' => 'Service Control', - 'service' => [ - 'list' => [ - 'title' => 'List Service', - 'desc' => 'Allows listing of all services configured on the system.', - ], - 'view' => [ - 'title' => 'View Service', - 'desc' => 'Allows listing details about each service on the system including service options and variables.', - ], - ], - 'option_header' => 'Option Control', - 'option' => [ - 'list' => [ - 'title' => 'List Options', - 'desc' => '', - ], - 'view' => [ - 'title' => 'View Option', - 'desc' => '', - ], - ], - 'pack_header' => 'Pack Control', - 'pack' => [ - 'list' => [ - 'title' => 'List Packs', - 'desc' => '', - ], - 'view' => [ - 'title' => 'View Pack', - 'desc' => '', - ], - ], - ], - ], ], 'account' => [ 'details_updated' => 'Your account details have been successfully updated.', diff --git a/resources/lang/en/exceptions.php b/resources/lang/en/exceptions.php index f32b9c71a..712ad92d7 100644 --- a/resources/lang/en/exceptions.php +++ b/resources/lang/en/exceptions.php @@ -1,19 +1,13 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ return [ 'daemon_connection_failed' => 'There was an exception while attempting to communicate with the daemon resulting in a HTTP/:code response code. This exception has been logged.', 'node' => [ 'servers_attached' => 'A node must have no servers linked to it in order to be deleted.', - 'daemon_off_config_updated' => 'The daemon configuration has been updated, however there was an error encountered while attempting to automatically update the configuration file on the Daemon. You will need to manually update the configuration file (core.json) for the daemon to apply these changes. The daemon responded with a HTTP/:code response code and the error has been logged.', + 'daemon_off_config_updated' => 'The daemon configuration has been updated, however there was an error encountered while attempting to automatically update the configuration file on the Daemon. You will need to manually update the configuration file (core.json) for the daemon to apply these changes.', ], 'allocations' => [ + 'server_using' => 'A server is currently assigned to this allocation. An allocation can only be deleted if no server is currently assigned.', 'too_many_ports' => 'Adding more than 1000 ports at a single time is not supported. Please use a smaller range.', 'invalid_mapping' => 'The mapping provided for :port was invalid and could not be processed.', 'cidr_out_of_range' => 'CIDR notation only allows masks between /25 and /32.', @@ -62,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/lang/en/navigation.php b/resources/lang/en/navigation.php index b8f2fb83a..f8a9deebb 100644 --- a/resources/lang/en/navigation.php +++ b/resources/lang/en/navigation.php @@ -6,7 +6,7 @@ return [ 'header' => 'ACCOUNT MANAGEMENT', 'my_account' => 'My Account', 'security_controls' => 'Security Controls', - 'api_access' => 'API Access', + 'api_access' => 'Account API', 'my_servers' => 'My Servers', ], 'server' => [ diff --git a/resources/lang/en/strings.php b/resources/lang/en/strings.php index c9983e643..5b9173866 100644 --- a/resources/lang/en/strings.php +++ b/resources/lang/en/strings.php @@ -83,4 +83,5 @@ return [ 'fri' => 'Friday', 'sat' => 'Saturday', ], + 'last_used' => 'Last Used', ]; diff --git a/resources/themes/pterodactyl/admin/api/index.blade.php b/resources/themes/pterodactyl/admin/api/index.blade.php new file mode 100644 index 000000000..ece51699f --- /dev/null +++ b/resources/themes/pterodactyl/admin/api/index.blade.php @@ -0,0 +1,103 @@ +@extends('layouts.admin') + +@section('title') + Application API +@endsection + +@section('content-header') +

    Application APIControl access credentials for manging this Panel via the API.

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

    Credentials List

    + +
    +
    + + + + + + + + + @foreach($keys as $key) + + + + + + + + @endforeach +
    KeyMemoLast UsedCreated
    {{ $key->identifier }}{{ decrypt($key->token) }}{{ $key->memo }} + @if(!is_null($key->last_used_at)) + @datetimeHuman($key->last_used_at) + @else + — + @endif + @datetimeHuman($key->created_at) + + + +
    +
    +
    +
    +
    +@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/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/resources/themes/pterodactyl/base/api/index.blade.php b/resources/themes/pterodactyl/base/api/index.blade.php index 0d8fc0a3e..e21e5aecc 100644 --- a/resources/themes/pterodactyl/base/api/index.blade.php +++ b/resources/themes/pterodactyl/base/api/index.blade.php @@ -31,28 +31,28 @@ - - - + + + @foreach ($keys as $key) - - - + + @endforeach @@ -84,7 +84,7 @@ }, function () { $.ajax({ method: 'DELETE', - url: Router.route('account.api.revoke', { key: self.data('attr') }), + url: Router.route('account.api.revoke', { identifier: self.data('attr') }), headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' } diff --git a/resources/themes/pterodactyl/base/api/new.blade.php b/resources/themes/pterodactyl/base/api/new.blade.php index 1efe75e86..4c2da25eb 100644 --- a/resources/themes/pterodactyl/base/api/new.blade.php +++ b/resources/themes/pterodactyl/base/api/new.blade.php @@ -1,8 +1,3 @@ -{{-- Pterodactyl - Panel --}} -{{-- Copyright (c) 2015 - 2017 Dane Everitt --}} - -{{-- This software is licensed under the terms of the MIT license. --}} -{{-- https://opensource.org/licenses/MIT --}} @extends('layouts.master') @section('title') @@ -54,13 +49,7 @@
    - -
    +
    {!! csrf_field() !!}
    @@ -69,64 +58,5 @@
    -
    - @foreach($permissions['user'] as $block => $perms) -
    -
    -
    -

    @lang('base.api.permissions.user.' . $block . '_header')

    -
    -
    - @foreach($perms as $permission) -
    -
    - - -
    -

    @lang('base.api.permissions.user.' . $block . '.' . $permission . '.desc')

    -
    - @endforeach -
    -
    -
    - @if ($loop->iteration % 2 === 0) -
    - @endif - @endforeach -
    - @if(Auth::user()->root_admin) -
    - @foreach($permissions['admin'] as $block => $perms) -
    -
    -
    -

    @lang('base.api.permissions.admin.' . $block . '_header')

    -
    -
    - @foreach($perms as $permission) -
    -
    - - -
    -

    @lang('base.api.permissions.admin.' . $block . '.' . $permission . '.desc')

    -
    - @endforeach -
    -
    -
    - @if ($loop->iteration % 3 === 0) -
    - @endif - @if ($loop->iteration % 2 === 0) -
    - @endif - @endforeach -
    - @endif @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..7dfa94f09 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('/', 'ApiController@index')->name('admin.api.index'); + Route::get('/new', 'ApiController@create')->name('admin.api.new'); + + Route::post('/new', 'ApiController@store'); + + Route::delete('/revoke/{identifier}', 'ApiController@delete')->name('admin.api.delete'); +}); + /* |-------------------------------------------------------------------------- | Location Controller Routes diff --git a/routes/api-admin.php b/routes/api-admin.php deleted file mode 100644 index a36adf3b4..000000000 --- a/routes/api-admin.php +++ /dev/null @@ -1,97 +0,0 @@ -. - * - * 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'); -//}); diff --git a/routes/api-application.php b/routes/api-application.php new file mode 100644 index 000000000..049ba391b --- /dev/null +++ b/routes/api-application.php @@ -0,0 +1,119 @@ + '/users'], function () { + 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'); + Route::patch('/{user}', 'Users\UserController@update'); + + Route::delete('/{user}', 'Users\UserController@delete'); +}); + +/* +|-------------------------------------------------------------------------- +| Node Controller Routes +|-------------------------------------------------------------------------- +| +| Endpoint: /api/application/nodes +| +*/ +Route::group(['prefix' => '/nodes'], function () { + 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'); + Route::patch('/{node}', 'Nodes\NodeController@update'); + + Route::delete('/{node}', 'Nodes\NodeController@delete'); + + 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'); + }); +}); + +/* +|-------------------------------------------------------------------------- +| Location Controller Routes +|-------------------------------------------------------------------------- +| +| Endpoint: /api/application/locations +| +*/ +Route::group(['prefix' => '/locations'], function () { + 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'); + Route::patch('/{location}', 'Locations\LocationController@update'); + + Route::delete('/{location}', 'Locations\LocationController@delete'); +}); + +/* +|-------------------------------------------------------------------------- +| Server Controller Routes +|-------------------------------------------------------------------------- +| +| Endpoint: /api/application/servers +| +*/ +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::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'); + 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'); + + // 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::post('/{database}/reset-password', 'Servers\DatabaseController@resetPassword'); + + 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'); + }); +}); diff --git a/routes/base.php b/routes/base.php index 674e6ad6d..c66b33e90 100644 --- a/routes/base.php +++ b/routes/base.php @@ -32,12 +32,12 @@ Route::group(['prefix' => 'account'], function () { | */ Route::group(['prefix' => 'account/api'], function () { - Route::get('/', 'APIController@index')->name('account.api'); - Route::get('/new', 'APIController@create')->name('account.api.new'); + Route::get('/', 'AccountKeyController@index')->name('account.api'); + Route::get('/new', 'AccountKeyController@create')->name('account.api.new'); - Route::post('/new', 'APIController@store'); + Route::post('/new', 'AccountKeyController@store'); - Route::delete('/revoke/{key}', 'APIController@revoke')->name('account.api.revoke'); + Route::delete('/revoke/{identifier}', 'AccountKeyController@revoke')->name('account.api.revoke'); }); /* diff --git a/tests/TestCase.php b/tests/TestCase.php index 5f9ba7482..427744f71 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,6 +2,7 @@ namespace Tests; +use Cake\Chronos\Chronos; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase @@ -18,6 +19,16 @@ abstract class TestCase extends BaseTestCase $this->setKnownUuidFactory(); } + /** + * Tear down tests. + */ + protected function tearDown() + { + parent::tearDown(); + + Chronos::setTestNow(); + } + /** * Handles the known UUID handling in certain unit tests. Use the "KnownUuid" trait * in order to enable this ability. diff --git a/tests/Traits/MocksRequestException.php b/tests/Traits/MocksRequestException.php index 81e0e5414..974fcf0e9 100644 --- a/tests/Traits/MocksRequestException.php +++ b/tests/Traits/MocksRequestException.php @@ -21,29 +21,23 @@ trait MocksRequestException /** * Configure the exception mock to work with the Panel's default exception * handler actions. + * + * @param string $abstract + * @param null $response */ - public function configureExceptionMock() + protected function configureExceptionMock(string $abstract = RequestException::class, $response = null) { - $this->getExceptionMock()->shouldReceive('getResponse')->andReturn($this->exceptionResponse); + $this->getExceptionMock($abstract)->shouldReceive('getResponse')->andReturn(value($response)); } /** * Return a mocked instance of the request exception. * + * @param string $abstract * @return \Mockery\MockInterface */ - private function getExceptionMock(): MockInterface + protected function getExceptionMock(string $abstract = RequestException::class): MockInterface { - return $this->exception ?? $this->exception = Mockery::mock(RequestException::class); - } - - /** - * Set the exception response. - * - * @param mixed $response - */ - protected function setExceptionResponse($response) - { - $this->exceptionResponse = $response; + return $this->exception ?? $this->exception = Mockery::mock($abstract); } } diff --git a/tests/Unit/Http/Controllers/Base/APIControllerTest.php b/tests/Unit/Http/Controllers/Base/AccountKeyControllerTest.php similarity index 56% rename from tests/Unit/Http/Controllers/Base/APIControllerTest.php rename to tests/Unit/Http/Controllers/Base/AccountKeyControllerTest.php index e356c3910..8bb5591b3 100644 --- a/tests/Unit/Http/Controllers/Base/APIControllerTest.php +++ b/tests/Unit/Http/Controllers/Base/AccountKeyControllerTest.php @@ -3,16 +3,15 @@ namespace Tests\Unit\Http\Controllers\Base; use Mockery as m; -use Pterodactyl\Models\User; -use Pterodactyl\Models\APIKey; +use Pterodactyl\Models\ApiKey; use Prologue\Alerts\AlertsMessageBag; 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\Http\Requests\Base\StoreAccountKeyRequest; +use Pterodactyl\Http\Controllers\Base\AccountKeyController; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; -class APIControllerTest extends ControllerTestCase +class AccountKeyControllerTest extends ControllerTestCase { /** * @var \Prologue\Alerts\AlertsMessageBag|\Mockery\Mock @@ -48,7 +47,7 @@ class APIControllerTest extends ControllerTestCase { $model = $this->generateRequestUserModel(); - $this->repository->shouldReceive('findWhere')->with([['user_id', '=', $model->id]])->once()->andReturn(collect(['testkeys'])); + $this->repository->shouldReceive('getAccountKeys')->with($model)->once()->andReturn(collect(['testkeys'])); $response = $this->getController()->index($this->request); $this->assertIsViewResponse($response); @@ -59,51 +58,34 @@ class APIControllerTest extends ControllerTestCase /** * Test the create API view controller. - * - * @dataProvider rootAdminDataProvider */ - public function testCreateController($admin) + public function testCreateController() { - $this->generateRequestUserModel(['root_admin' => $admin]); + $this->generateRequestUserModel(); $response = $this->getController()->create($this->request); $this->assertIsViewResponse($response); - $this->assertViewNameEquals('base.api.new', $response); - $this->assertViewHasKey('permissions.user', $response); - $this->assertViewHasKey('permissions.admin', $response); - - if ($admin) { - $this->assertViewKeyNotEquals('permissions.admin', null, $response); - } else { - $this->assertViewKeyEquals('permissions.admin', null, $response); - } } /** - * Test the store functionality for a non-admin user. - * - * @dataProvider rootAdminDataProvider + * Test the store functionality for a user. */ - public function testStoreController($admin) + public function testStoreController() { - $this->setRequestMockClass(ApiKeyFormRequest::class); - $model = $this->generateRequestUserModel(['root_admin' => $admin]); - $keyModel = factory(APIKey::class)->make(); - - if ($admin) { - $this->request->shouldReceive('input')->with('admin_permissions', [])->once()->andReturn(['admin.permission']); - } + $this->setRequestMockClass(StoreAccountKeyRequest::class); + $model = $this->generateRequestUserModel(); + $keyModel = factory(ApiKey::class)->make(); $this->request->shouldReceive('user')->withNoArgs()->andReturn($model); $this->request->shouldReceive('input')->with('allowed_ips')->once()->andReturnNull(); $this->request->shouldReceive('input')->with('memo')->once()->andReturnNull(); - $this->request->shouldReceive('input')->with('permissions', [])->once()->andReturn(['test.permission']); + $this->keyService->shouldReceive('setKeyType')->with(ApiKey::TYPE_ACCOUNT)->once()->andReturnSelf(); $this->keyService->shouldReceive('handle')->with([ 'user_id' => $model->id, 'allowed_ips' => null, 'memo' => null, - ], ['test.permission'], ($admin) ? ['admin.permission'] : [])->once()->andReturn($keyModel); + ])->once()->andReturn($keyModel); $this->alert->shouldReceive('success')->with(trans('base.api.index.keypair_created'))->once()->andReturnSelf(); $this->alert->shouldReceive('flash')->withNoArgs()->once()->andReturnNull(); @@ -120,34 +102,21 @@ class APIControllerTest extends ControllerTestCase { $model = $this->generateRequestUserModel(); - $this->repository->shouldReceive('deleteWhere')->with([ - ['user_id', '=', $model->id], - ['token', '=', 'testKey123'], - ])->once()->andReturn(1); + $this->repository->shouldReceive('deleteAccountKey')->with($model, 'testIdentifier')->once()->andReturn(1); - $response = $this->getController()->revoke($this->request, 'testKey123'); + $response = $this->getController()->revoke($this->request, 'testIdentifier'); $this->assertIsResponse($response); $this->assertEmpty($response->getContent()); $this->assertResponseCodeEquals(204, $response); } - /** - * Data provider to determine if a user is a root admin. - * - * @return array - */ - public function rootAdminDataProvider() - { - return [[0], [1]]; - } - /** * Return an instance of the controller with mocked dependencies for testing. * - * @return \Pterodactyl\Http\Controllers\Base\APIController + * @return \Pterodactyl\Http\Controllers\Base\AccountKeyController */ - private function getController(): APIController + private function getController(): AccountKeyController { - return new APIController($this->alert, $this->repository, $this->keyService); + return new AccountKeyController($this->alert, $this->repository, $this->keyService); } } diff --git a/tests/Unit/Http/Middleware/API/HasPermissionToResourceTest.php b/tests/Unit/Http/Middleware/API/HasPermissionToResourceTest.php deleted file mode 100644 index 7ef6c3830..000000000 --- a/tests/Unit/Http/Middleware/API/HasPermissionToResourceTest.php +++ /dev/null @@ -1,109 +0,0 @@ -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); - } -} diff --git a/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php b/tests/Unit/Http/Middleware/Api/Application/AuthenticateIPAccessTest.php similarity index 77% rename from tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php rename to tests/Unit/Http/Middleware/Api/Application/AuthenticateIPAccessTest.php index cd122f7cb..cf23d0292 100644 --- a/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php +++ b/tests/Unit/Http/Middleware/Api/Application/AuthenticateIPAccessTest.php @@ -1,27 +1,19 @@ make(['allowed_ips' => []]); + $model = factory(ApiKey::class)->make(['allowed_ips' => []]); $this->setRequestAttribute('api_key', $model); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); @@ -33,7 +25,7 @@ class AuthenticateIPAccessTest extends MiddlewareTestCase */ public function testWithValidIP() { - $model = factory(APIKey::class)->make(['allowed_ips' => ['127.0.0.1']]); + $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'); @@ -46,7 +38,7 @@ class AuthenticateIPAccessTest extends MiddlewareTestCase */ public function testValidIPAganistCIDRRange() { - $model = factory(APIKey::class)->make(['allowed_ips' => ['192.168.1.1/28']]); + $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'); @@ -62,7 +54,7 @@ class AuthenticateIPAccessTest extends MiddlewareTestCase */ public function testWithInvalidIP() { - $model = factory(APIKey::class)->make(['allowed_ips' => ['127.0.0.1']]); + $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'); @@ -73,7 +65,7 @@ class AuthenticateIPAccessTest extends MiddlewareTestCase /** * Return an instance of the middleware to be used when testing. * - * @return \Pterodactyl\Http\Middleware\API\AuthenticateIPAccess + * @return \Pterodactyl\Http\Middleware\Api\Application\AuthenticateIPAccess */ private function getMiddleware(): AuthenticateIPAccess { diff --git a/tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php b/tests/Unit/Http/Middleware/Api/Application/AuthenticateKeyTest.php similarity index 55% rename from tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php rename to tests/Unit/Http/Middleware/Api/Application/AuthenticateKeyTest.php index e2032b588..486222267 100644 --- a/tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php +++ b/tests/Unit/Http/Middleware/Api/Application/AuthenticateKeyTest.php @@ -1,15 +1,17 @@ auth = m::mock(AuthManager::class); + $this->encrypter = m::mock(Encrypter::class); $this->repository = m::mock(ApiKeyRepositoryInterface::class); } @@ -50,11 +59,11 @@ class AuthenticateKeyTest extends MiddlewareTestCase } /** - * Test that an invalid API token throws an exception. + * Test that an invalid API identifer throws an exception. * * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException */ - public function testInvalidTokenThrowsException() + public function testInvalidIdentifier() { $this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn('abcd1234'); $this->repository->shouldReceive('findFirstWhere')->andThrow(new RecordNotFoundException); @@ -67,24 +76,51 @@ class AuthenticateKeyTest extends MiddlewareTestCase */ 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); + $model = factory(ApiKey::class)->make(); + $this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn($model->identifier . 'decrypted'); + $this->repository->shouldReceive('findFirstWhere')->with([ + ['identifier', '=', $model->identifier], + ['key_type', '=', ApiKey::TYPE_APPLICATION], + ])->once()->andReturn($model); + $this->encrypter->shouldReceive('decrypt')->with($model->token)->once()->andReturn('decrypted'); $this->auth->shouldReceive('guard->loginUsingId')->with($model->user_id)->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFreshModel->update')->with($model->id, [ + 'last_used_at' => Chronos::now(), + ])->once()->andReturnNull(); + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); $this->assertEquals($model, $this->request->attributes->get('api_key')); } + /** + * Test that a valid token identifier with an invalid token attached to it + * triggers an exception. + * + * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + */ + public function testInvalidTokenForIdentifier() + { + $model = factory(ApiKey::class)->make(); + + $this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn($model->identifier . 'asdf'); + $this->repository->shouldReceive('findFirstWhere')->with([ + ['identifier', '=', $model->identifier], + ['key_type', '=', ApiKey::TYPE_APPLICATION], + ])->once()->andReturn($model); + $this->encrypter->shouldReceive('decrypt')->with($model->token)->once()->andReturn('decrypted'); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + /** * Return an instance of the middleware with mocked dependencies for testing. * - * @return \Pterodactyl\Http\Middleware\API\AuthenticateKey + * @return \Pterodactyl\Http\Middleware\Api\Application\AuthenticateKey */ private function getMiddleware(): AuthenticateKey { - return new AuthenticateKey($this->repository, $this->auth); + return new AuthenticateKey($this->repository, $this->auth, $this->encrypter); } } diff --git a/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php b/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php new file mode 100644 index 000000000..56c7f5ffb --- /dev/null +++ b/tests/Unit/Http/Middleware/Api/Application/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\Application\AuthenticateUser + */ + private function getMiddleware(): AuthenticateUser + { + return new AuthenticateUser; + } +} diff --git a/tests/Unit/Http/Middleware/API/SetSessionDriverTest.php b/tests/Unit/Http/Middleware/Api/Application/SetSessionDriverTest.php similarity index 90% rename from tests/Unit/Http/Middleware/API/SetSessionDriverTest.php rename to tests/Unit/Http/Middleware/Api/Application/SetSessionDriverTest.php index dd9fa3e04..7804f8209 100644 --- a/tests/Unit/Http/Middleware/API/SetSessionDriverTest.php +++ b/tests/Unit/Http/Middleware/Api/Application/SetSessionDriverTest.php @@ -1,13 +1,13 @@ repository = m::mock(AllocationRepositoryInterface::class); + } + + /** + * Test that an allocation is deleted. + */ + public function testAllocationIsDeleted() + { + $model = factory(Allocation::class)->make(); + + $this->repository->shouldReceive('delete')->with($model->id)->once()->andReturn(1); + + $response = $this->getService()->handle($model); + $this->assertEquals(1, $response); + } + + /** + * Test that an exception gets thrown if an allocation is currently assigned to a server. + * + * @expectedException \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException + */ + public function testExceptionThrownIfAssignedToServer() + { + $model = factory(Allocation::class)->make(['server_id' => 123]); + + $this->getService()->handle($model); + } + + /** + * Return an instance of the service with mocked injections. + * + * @return \Pterodactyl\Services\Allocations\AllocationDeletionService + */ + private function getService(): AllocationDeletionService + { + return new AllocationDeletionService($this->repository); + } +} diff --git a/tests/Unit/Services/Api/KeyCreationServiceTest.php b/tests/Unit/Services/Api/KeyCreationServiceTest.php index 49e0a2fde..613be35f8 100644 --- a/tests/Unit/Services/Api/KeyCreationServiceTest.php +++ b/tests/Unit/Services/Api/KeyCreationServiceTest.php @@ -1,20 +1,12 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ 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 Pterodactyl\Services\Api\PermissionService; +use Pterodactyl\Models\ApiKey; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Services\Api\KeyCreationService; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; @@ -23,14 +15,9 @@ class KeyCreationServiceTest extends TestCase use PHPMock; /** - * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock + * @var \Illuminate\Contracts\Encryption\Encrypter|\Mockery\Mock */ - private $connection; - - /** - * @var \Pterodactyl\Services\Api\PermissionService|\Mockery\Mock - */ - private $permissionService; + private $encrypter; /** * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface|\Mockery\Mock @@ -44,8 +31,7 @@ class KeyCreationServiceTest extends TestCase { parent::setUp(); - $this->connection = m::mock(ConnectionInterface::class); - $this->permissionService = m::mock(PermissionService::class); + $this->encrypter = m::mock(Encrypter::class); $this->repository = m::mock(ApiKeyRepositoryInterface::class); } @@ -54,41 +40,125 @@ class KeyCreationServiceTest extends TestCase */ public function testKeyIsCreated() { - $model = factory(APIKey::class)->make(); + $model = factory(ApiKey::class)->make(); $this->getFunctionMock('\\Pterodactyl\\Services\\Api', 'str_random') - ->expects($this->exactly(1))->with(APIKey::KEY_LENGTH)->willReturn($model->token); + ->expects($this->exactly(2))->willReturnCallback(function ($length) { + return 'str_' . $length; + }); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->encrypter->shouldReceive('encrypt')->with('str_' . ApiKey::KEY_LENGTH)->once()->andReturn($model->token); $this->repository->shouldReceive('create')->with([ 'test-data' => 'test', + 'key_type' => ApiKey::TYPE_NONE, + 'identifier' => 'str_' . ApiKey::IDENTIFIER_LENGTH, 'token' => $model->token, ], true, true)->once()->andReturn($model); - $this->permissionService->shouldReceive('getPermissions')->withNoArgs()->once()->andReturn([ - '_user' => ['server' => ['list', 'multiple-dash-test']], - 'server' => ['create', 'admin-dash-test'], - ]); - - $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->getService()->handle( - ['test-data' => 'test'], - ['invalid-node', 'server-list', 'server-multiple-dash-test'], - ['invalid-node', 'server-create', 'server-admin-dash-test'] - ); + $response = $this->getService()->handle(['test-data' => 'test']); $this->assertNotEmpty($response); - $this->assertInstanceOf(APIKey::class, $response); + $this->assertInstanceOf(ApiKey::class, $response); $this->assertSame($model, $response); } + /** + * Test that an identifier is only set by the function. + */ + public function testIdentifierAndTokenAreOnlySetByFunction() + { + $model = factory(ApiKey::class)->make(); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Api', 'str_random') + ->expects($this->exactly(2))->willReturnCallback(function ($length) { + return 'str_' . $length; + }); + + $this->encrypter->shouldReceive('encrypt')->with('str_' . ApiKey::KEY_LENGTH)->once()->andReturn($model->token); + + $this->repository->shouldReceive('create')->with([ + 'key_type' => ApiKey::TYPE_NONE, + 'identifier' => 'str_' . ApiKey::IDENTIFIER_LENGTH, + 'token' => $model->token, + ], true, true)->once()->andReturn($model); + + $response = $this->getService()->handle(['identifier' => 'customIdentifier', 'token' => 'customToken']); + + $this->assertNotEmpty($response); + $this->assertInstanceOf(ApiKey::class, $response); + $this->assertSame($model, $response); + } + + /** + * Test that permissions passed in are loaded onto the key data. + */ + public function testPermissionsAreRetrievedForApplicationKeys() + { + $model = factory(ApiKey::class)->make(); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Api', 'str_random') + ->expects($this->exactly(2))->willReturnCallback(function ($length) { + return 'str_' . $length; + }); + + $this->encrypter->shouldReceive('encrypt')->with('str_' . ApiKey::KEY_LENGTH)->once()->andReturn($model->token); + + $this->repository->shouldReceive('create')->with([ + 'key_type' => ApiKey::TYPE_APPLICATION, + 'identifier' => 'str_' . ApiKey::IDENTIFIER_LENGTH, + 'token' => $model->token, + 'permission-key' => 'exists', + ], true, true)->once()->andReturn($model); + + $response = $this->getService()->setKeyType(ApiKey::TYPE_APPLICATION)->handle([], ['permission-key' => 'exists']); + + $this->assertNotEmpty($response); + $this->assertInstanceOf(ApiKey::class, $response); + $this->assertSame($model, $response); + } + + /** + * Test that permissions are not retrieved for any key that is not an application key. + * + * @dataProvider keyTypeDataProvider + */ + public function testPermissionsAreNotRetrievedForNonApplicationKeys($keyType) + { + $model = factory(ApiKey::class)->make(); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Api', 'str_random') + ->expects($this->exactly(2))->willReturnCallback(function ($length) { + return 'str_' . $length; + }); + + $this->encrypter->shouldReceive('encrypt')->with('str_' . ApiKey::KEY_LENGTH)->once()->andReturn($model->token); + + $this->repository->shouldReceive('create')->with([ + 'key_type' => $keyType, + 'identifier' => 'str_' . ApiKey::IDENTIFIER_LENGTH, + 'token' => $model->token, + ], true, true)->once()->andReturn($model); + + $response = $this->getService()->setKeyType($keyType)->handle([], ['fake-permission' => 'should-not-exist']); + + $this->assertNotEmpty($response); + $this->assertInstanceOf(ApiKey::class, $response); + $this->assertSame($model, $response); + } + + /** + * Provide key types that are not an application specific key. + * + * @return array + */ + public function keyTypeDataProvider(): array + { + return [ + [ApiKey::TYPE_NONE], [ApiKey::TYPE_ACCOUNT], [ApiKey::TYPE_DAEMON_USER], [ApiKey::TYPE_DAEMON_APPLICATION], + ]; + } + /** * Return an instance of the service with mocked dependencies for testing. * @@ -96,6 +166,6 @@ class KeyCreationServiceTest extends TestCase */ private function getService(): KeyCreationService { - return new KeyCreationService($this->repository, $this->connection, $this->permissionService); + return new KeyCreationService($this->repository, $this->encrypter); } } diff --git a/tests/Unit/Services/Api/PermissionServiceTest.php b/tests/Unit/Services/Api/PermissionServiceTest.php deleted file mode 100644 index 384acec72..000000000 --- a/tests/Unit/Services/Api/PermissionServiceTest.php +++ /dev/null @@ -1,62 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Tests\Unit\Services; - -use Mockery as m; -use Tests\TestCase; -use Pterodactyl\Models\APIPermission; -use Pterodactyl\Services\Api\PermissionService; -use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; - -class PermissionServiceTest extends TestCase -{ - /** - * @var \Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface - */ - protected $repository; - - /** - * @var \Pterodactyl\Services\Api\PermissionService - */ - protected $service; - - /** - * Setup tests. - */ - public function setUp() - { - parent::setUp(); - - $this->repository = m::mock(ApiPermissionRepositoryInterface::class); - $this->service = new PermissionService($this->repository); - } - - /** - * Test that a new API permission can be assigned to a key. - */ - public function test_create_function() - { - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('create')->with(['key_id' => 1, 'permission' => 'test-permission']) - ->once()->andReturn(true); - - $this->assertTrue($this->service->create(1, 'test-permission')); - } - - /** - * Test that function returns an array of all the permissions available as defined on the model. - */ - public function test_get_permissions_function() - { - $this->repository->shouldReceive('getModel')->withNoArgs()->once()->andReturn(new APIPermission()); - - $this->assertEquals(APIPermission::CONST_PERMISSIONS, $this->service->getPermissions()); - } -} diff --git a/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php b/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php index ded8b7326..6dff6e4ba 100644 --- a/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php +++ b/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php @@ -12,49 +12,34 @@ namespace Tests\Unit\Services\Nodes; use Exception; use Mockery as m; use Tests\TestCase; -use Illuminate\Log\Writer; use phpmock\phpunit\PHPMock; use Pterodactyl\Models\Node; use GuzzleHttp\Psr7\Response; -use GuzzleHttp\Exception\RequestException; -use Pterodactyl\Exceptions\DisplayException; +use Tests\Traits\MocksRequestException; +use GuzzleHttp\Exception\ConnectException; +use Illuminate\Database\ConnectionInterface; use Pterodactyl\Services\Nodes\NodeUpdateService; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; class NodeUpdateServiceTest extends TestCase { - use PHPMock; + use PHPMock, MocksRequestException; + + /** + * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock + */ + private $connection; /** * @var \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface|\Mockery\Mock */ - protected $configRepository; - - /** - * @var \GuzzleHttp\Exception\RequestException|\Mockery\Mock - */ - protected $exception; - - /** - * @var \Pterodactyl\Models\Node - */ - protected $node; + private $configRepository; /** * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface|\Mockery\Mock */ - protected $repository; - - /** - * @var \Pterodactyl\Services\Nodes\NodeUpdateService - */ - protected $service; - - /** - * @var \Illuminate\Log\Writer|\Mockery\Mock - */ - protected $writer; + private $repository; /** * Setup tests. @@ -63,18 +48,9 @@ class NodeUpdateServiceTest extends TestCase { parent::setUp(); - $this->node = factory(Node::class)->make(); - + $this->connection = m::mock(ConnectionInterface::class); $this->configRepository = m::mock(ConfigurationRepositoryInterface::class); - $this->exception = m::mock(RequestException::class); $this->repository = m::mock(NodeRepositoryInterface::class); - $this->writer = m::mock(Writer::class); - - $this->service = new NodeUpdateService( - $this->configRepository, - $this->repository, - $this->writer - ); } /** @@ -82,19 +58,23 @@ class NodeUpdateServiceTest extends TestCase */ public function testNodeIsUpdatedAndDaemonSecretIsReset() { + $model = factory(Node::class)->make(); + $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'str_random') ->expects($this->once())->willReturn('random_string'); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($this->node->id, [ - 'name' => 'NewName', - 'daemonSecret' => 'random_string', - ])->andReturn(true); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFreshModel->update')->with($model->id, [ + 'name' => 'NewName', + 'daemonSecret' => 'random_string', + ])->andReturn(true); - $this->configRepository->shouldReceive('setNode')->with($this->node)->once()->andReturnSelf() + $this->configRepository->shouldReceive('setNode')->with($model)->once()->andReturnSelf() ->shouldReceive('update')->withNoArgs()->once()->andReturn(new Response); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - $this->assertTrue($this->service->handle($this->node, ['name' => 'NewName', 'reset_secret' => true])); + $response = $this->getService()->returnUpdatedModel(false)->handle($model, ['name' => 'NewName', 'reset_secret' => true]); + $this->assertTrue($response); } /** @@ -102,57 +82,85 @@ class NodeUpdateServiceTest extends TestCase */ public function testNodeIsUpdatedAndDaemonSecretIsNotChanged() { - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($this->node->id, [ - 'name' => 'NewName', - ])->andReturn(true); + $model = factory(Node::class)->make(); - $this->configRepository->shouldReceive('setNode')->with($this->node)->once()->andReturnSelf() + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFreshModel->update')->with($model->id, [ + 'name' => 'NewName', + ])->andReturn(true); + + $this->configRepository->shouldReceive('setNode')->with($model)->once()->andReturnSelf() ->shouldReceive('update')->withNoArgs()->once()->andReturn(new Response); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - $this->assertTrue($this->service->handle($this->node, ['name' => 'NewName'])); + $response = $this->getService()->returnUpdatedModel(false)->handle($model, ['name' => 'NewName']); + $this->assertTrue($response); + } + + public function testUpdatedModelIsReturned() + { + $model = factory(Node::class)->make(); + $updated = clone $model; + $updated->name = 'NewName'; + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('update')->with($model->id, [ + 'name' => $updated->name, + ])->andReturn($updated); + + $this->configRepository->shouldReceive('setNode')->with($model)->once()->andReturnSelf() + ->shouldReceive('update')->withNoArgs()->once()->andReturn(new Response); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->getService()->returnUpdatedModel()->handle($model, ['name' => $updated->name]); + $this->assertInstanceOf(Node::class, $response); + $this->assertSame($updated, $response); } /** - * Test that an exception caused by the daemon is handled properly. + * Test that an exception caused by a connection error is handled. + * + * @expectedException \Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException */ - public function testExceptionCausedByDaemonIsHandled() + public function testExceptionRelatedToConnection() { - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($this->node->id, [ - 'name' => 'NewName', - ])->andReturn(new Response); + $this->configureExceptionMock(ConnectException::class); + $model = factory(Node::class)->make(); - $this->configRepository->shouldReceive('setNode')->with($this->node)->once()->andThrow($this->exception); - $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); - $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFreshModel->update')->andReturn(new Response); - try { - $this->service->handle($this->node, ['name' => 'NewName']); - } catch (Exception $exception) { - $this->assertInstanceOf(DisplayException::class, $exception); - $this->assertEquals( - trans('exceptions.node.daemon_off_config_updated', ['code' => 400]), - $exception->getMessage() - ); - } + $this->configRepository->shouldReceive('setNode->update')->once()->andThrow($this->getExceptionMock()); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->getService()->handle($model, ['name' => 'NewName']); } /** - * Test that an ID can be passed in place of a model. + * Test that an exception not caused by a daemon connection error is handled. + * + * @expectedException \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException */ - public function testFunctionCanAcceptANodeIdInPlaceOfModel() + public function testExceptionNotRelatedToConnection() { - $this->repository->shouldReceive('find')->with($this->node->id)->once()->andReturn($this->node); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($this->node->id, [ - 'name' => 'NewName', - ])->andReturn(true); + $this->configureExceptionMock(); + $model = factory(Node::class)->make(); - $this->configRepository->shouldReceive('setNode')->with($this->node)->once()->andReturnSelf() - ->shouldReceive('update')->withNoArgs()->once()->andReturn(new Response); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFreshModel->update')->andReturn(new Response); - $this->assertTrue($this->service->handle($this->node->id, ['name' => 'NewName'])); + $this->configRepository->shouldReceive('setNode->update')->once()->andThrow($this->getExceptionMock()); + + $this->getService()->handle($model, ['name' => 'NewName']); + } + + /** + * Return an instance of the service with mocked injections. + * + * @return \Pterodactyl\Services\Nodes\NodeUpdateService + */ + private function getService(): NodeUpdateService + { + return new NodeUpdateService($this->connection, $this->configRepository, $this->repository); } } 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 + ); } } 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/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, 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]); + } } }
  • @lang('strings.public_key') @lang('strings.memo')@lang('strings.public_key')
    {{ $key->token }} {{ $key->memo }}{{ $key->identifier . decrypt($key->token) }} - +