diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c69c8776..947098b6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,24 @@ This file is a running track of new features and fixes to each version of the pa This project follows [Semantic Versioning](http://semver.org) guidelines. +## v0.7.4 (Derelict Dermodactylus) +### Fixed +* Fixes a bug when reinstalling a server that would not mark the server as installing, resulting in some UI issues. +* Handle 404 errors from missing models in the application API bindings correctly. +* Fix validation error returned when no environment variables are passed, even if there are no variables required. +* Fix improper permissions on `PATCH /api/servers//startup` endpoint which was preventing enditing any start variables. +* Should fix migration issues from 0.6 when there are more than API key in the database. + +### Changed +* Changes order that validation of resource existence occurs in API requests to not try and use a non-existent model when validating data. + +### Added +* Adds back client API for sending commands or power toggles to a server though the Panel API: `/api/client/servers/` +* Added proper transformer for Packs and re-enabled missing includes on server. +* Added support for using Filesystem as a caching driver, although not recommended. +* Added support for user management of server databases. +* **Added bulk power management CLI interface to send start, stop, kill, restart actions to servers across configurable nodes.** + ## v0.7.3 (Derelict Dermodactylus) ### Fixed * Fixes server creation API endpoint not passing the provided `external_id` to the creation service. diff --git a/app/Console/Commands/Environment/AppSettingsCommand.php b/app/Console/Commands/Environment/AppSettingsCommand.php index ecf171adc..17c2b5bbc 100644 --- a/app/Console/Commands/Environment/AppSettingsCommand.php +++ b/app/Console/Commands/Environment/AppSettingsCommand.php @@ -22,6 +22,7 @@ class AppSettingsCommand extends Command const ALLOWED_CACHE_DRIVERS = [ 'redis' => 'Redis (recommended)', 'memcached' => 'Memcached', + 'file' => 'Filesystem', ]; const ALLOWED_SESSION_DRIVERS = [ diff --git a/app/Console/Commands/Server/BulkPowerActionCommand.php b/app/Console/Commands/Server/BulkPowerActionCommand.php new file mode 100644 index 000000000..ced90db81 --- /dev/null +++ b/app/Console/Commands/Server/BulkPowerActionCommand.php @@ -0,0 +1,121 @@ +powerRepository = $powerRepository; + $this->repository = $repository; + $this->validator = $validator; + } + + /** + * Handle the bulk power request. + * + * @throws \Illuminate\Validation\ValidationException + * @throws \Pterodactyl\Exceptions\Repository\Daemon\InvalidPowerSignalException + */ + public function handle() + { + $action = $this->argument('action'); + $nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes')); + $servers = empty($this->option('servers')) ? [] : explode(',', $this->option('servers')); + + $validator = $this->validator->make([ + 'action' => $action, + 'nodes' => $nodes, + 'servers' => $servers, + ], [ + 'action' => 'string|in:start,stop,kill,restart', + 'nodes' => 'array', + 'nodes.*' => 'integer|min:1', + 'servers' => 'array', + 'servers.*' => 'integer|min:1', + ]); + + if ($validator->fails()) { + foreach ($validator->getMessageBag()->all() as $message) { + $this->output->error($message); + } + + throw new ValidationException($validator); + } + + $count = $this->repository->getServersForPowerActionCount($servers, $nodes); + if (! $this->confirm(trans('command/messages.server.power.confirm', ['action' => $action, 'count' => $count]))) { + return; + } + + $bar = $this->output->createProgressBar($count); + $servers = $this->repository->getServersForPowerAction($servers, $nodes); + + foreach ($servers as $server) { + $bar->clear(); + + try { + $this->powerRepository->setServer($server)->sendSignal($action); + } catch (RequestException $exception) { + $this->output->error(trans('command/messages.server.power.action_failed', [ + 'name' => $server->name, + 'id' => $server->id, + 'node' => $server->node->name, + 'message' => $exception->getMessage(), + ])); + } + + $bar->advance(); + $bar->display(); + } + + $this->line(''); + } +} diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index 0ca74bf40..983cf7e6e 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -117,4 +117,23 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function getByUuid(string $uuid): Server; + + /** + * Return all of the servers that should have a power action performed aganist them. + * + * @param int[] $servers + * @param int[] $nodes + * @param bool $returnCount + * @return int|\Generator + */ + public function getServersForPowerAction(array $servers = [], array $nodes = [], bool $returnCount = false); + + /** + * Return the total number of servers that will be affected by the query. + * + * @param int[] $servers + * @param int[] $nodes + * @return int + */ + public function getServersForPowerActionCount(array $servers = [], array $nodes = []): int; } diff --git a/app/Exceptions/Service/Database/DatabaseClientFeatureNotEnabledException.php b/app/Exceptions/Service/Database/DatabaseClientFeatureNotEnabledException.php new file mode 100644 index 000000000..809cb4fbf --- /dev/null +++ b/app/Exceptions/Service/Database/DatabaseClientFeatureNotEnabledException.php @@ -0,0 +1,13 @@ +cache->tags(['Node:Configuration'])->put($token, $node->id, 5); + $this->cache->put('Node:Configuration:' . $token, $node->id, 5); return response()->json(['token' => $token]); } diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 6b9d44cbb..8adff82d4 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -498,15 +498,17 @@ class ServersController extends Controller * @param \Illuminate\Http\Request $request * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse + * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @internal param int $id + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function updateBuild(Request $request, Server $server) { $this->buildModificationService->handle($server, $request->only([ 'allocation_id', 'add_allocations', 'remove_allocations', 'memory', 'swap', 'io', 'cpu', 'disk', + 'database_limit', 'allocation_limit', ])); $this->alert->success(trans('admin/server.alerts.build_updated'))->flash(); diff --git a/app/Http/Controllers/Api/Application/ApplicationApiController.php b/app/Http/Controllers/Api/Application/ApplicationApiController.php index c932c3644..bdd5f9e7b 100644 --- a/app/Http/Controllers/Api/Application/ApplicationApiController.php +++ b/app/Http/Controllers/Api/Application/ApplicationApiController.php @@ -3,17 +3,19 @@ namespace Pterodactyl\Http\Controllers\Api\Application; use Illuminate\Http\Request; +use Webmozart\Assert\Assert; use Illuminate\Http\Response; use Illuminate\Container\Container; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Extensions\Spatie\Fractalistic\Fractal; +use Pterodactyl\Transformers\Api\Application\BaseTransformer; abstract class ApplicationApiController extends Controller { /** * @var \Illuminate\Http\Request */ - private $request; + protected $request; /** * @var \Pterodactyl\Extensions\Spatie\Fractalistic\Fractal @@ -61,6 +63,8 @@ abstract class ApplicationApiController extends Controller $transformer = Container::getInstance()->make($abstract); $transformer->setKey($this->request->attributes->get('api_key')); + Assert::isInstanceOf($transformer, BaseTransformer::class); + return $transformer; } diff --git a/app/Http/Controllers/Api/Application/Servers/StartupController.php b/app/Http/Controllers/Api/Application/Servers/StartupController.php index e6b8015d8..0265af464 100644 --- a/app/Http/Controllers/Api/Application/Servers/StartupController.php +++ b/app/Http/Controllers/Api/Application/Servers/StartupController.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Servers; +use Pterodactyl\Models\User; use Pterodactyl\Models\Server; use Pterodactyl\Services\Servers\StartupModificationService; use Pterodactyl\Transformers\Api\Application\ServerTransformer; @@ -40,7 +41,9 @@ class StartupController extends ApplicationApiController */ public function index(UpdateServerStartupRequest $request): array { - $server = $this->modificationService->handle($request->getModel(Server::class), $request->validated()); + $server = $this->modificationService + ->setUserLevel(User::USER_LEVEL_ADMIN) + ->handle($request->getModel(Server::class), $request->validated()); return $this->fractal->item($server) ->transformWith($this->getTransformer(ServerTransformer::class)) diff --git a/app/Http/Controllers/Api/Client/ClientApiController.php b/app/Http/Controllers/Api/Client/ClientApiController.php new file mode 100644 index 000000000..e2d4b3f83 --- /dev/null +++ b/app/Http/Controllers/Api/Client/ClientApiController.php @@ -0,0 +1,29 @@ +make($abstract); + Assert::isInstanceOf($transformer, BaseClientTransformer::class); + + $transformer->setKey($this->request->attributes->get('api_key')); + $transformer->setUser($this->request->user()); + + return $transformer; + } +} diff --git a/app/Http/Controllers/Api/Client/ClientController.php b/app/Http/Controllers/Api/Client/ClientController.php new file mode 100644 index 000000000..d2e1f33a9 --- /dev/null +++ b/app/Http/Controllers/Api/Client/ClientController.php @@ -0,0 +1,44 @@ +repository = $repository; + } + + /** + * Return all of the servers available to the client making the API + * request, including servers the user has access to as a subuser. + * + * @param \Pterodactyl\Http\Requests\Api\Client\GetServersRequest $request + * @return array + */ + public function index(GetServersRequest $request): array + { + $servers = $this->repository->filterUserAccessServers($request->user(), User::FILTER_LEVEL_SUBUSER); + + return $this->fractal->collection($servers) + ->transformWith($this->getTransformer(ServerTransformer::class)) + ->toArray(); + } +} diff --git a/app/Http/Controllers/Api/Client/Servers/CommandController.php b/app/Http/Controllers/Api/Client/Servers/CommandController.php new file mode 100644 index 000000000..8a5b951f5 --- /dev/null +++ b/app/Http/Controllers/Api/Client/Servers/CommandController.php @@ -0,0 +1,74 @@ +keyProviderService = $keyProviderService; + $this->repository = $repository; + } + + /** + * Send a command to a running server. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\SendCommandRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + */ + public function index(SendCommandRequest $request): Response + { + $server = $request->getModel(Server::class); + $token = $this->keyProviderService->handle($server, $request->user()); + + try { + $this->repository->setServer($server) + ->setToken($token) + ->send($request->input('command')); + } catch (RequestException $exception) { + if ($exception instanceof ClientException) { + if ($exception->getResponse() instanceof ResponseInterface && $exception->getResponse()->getStatusCode() === 412) { + throw new PreconditionFailedHttpException('Server is not online.'); + } + } + + throw new DaemonConnectionException($exception); + } + + return $this->returnNoContent(); + } +} diff --git a/app/Http/Controllers/Api/Client/Servers/PowerController.php b/app/Http/Controllers/Api/Client/Servers/PowerController.php new file mode 100644 index 000000000..113b83398 --- /dev/null +++ b/app/Http/Controllers/Api/Client/Servers/PowerController.php @@ -0,0 +1,57 @@ +keyProviderService = $keyProviderService; + $this->repository = $repository; + } + + /** + * Send a power action to a server. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\SendPowerRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Repository\Daemon\InvalidPowerSignalException + */ + public function index(SendPowerRequest $request): Response + { + $server = $request->getModel(Server::class); + $token = $this->keyProviderService->handle($server, $request->user()); + + $this->repository->setServer($server)->setToken($token)->sendSignal($request->input('signal')); + + return $this->returnNoContent(); + } +} diff --git a/app/Http/Controllers/Api/Client/Servers/ServerController.php b/app/Http/Controllers/Api/Client/Servers/ServerController.php new file mode 100644 index 000000000..ce4502e3a --- /dev/null +++ b/app/Http/Controllers/Api/Client/Servers/ServerController.php @@ -0,0 +1,25 @@ +fractal->item($request->getModel(Server::class)) + ->transformWith($this->getTransformer(ServerTransformer::class)) + ->toArray(); + } +} diff --git a/app/Http/Controllers/Api/Remote/FileDownloadController.php b/app/Http/Controllers/Api/Remote/FileDownloadController.php new file mode 100644 index 000000000..fa4818fc9 --- /dev/null +++ b/app/Http/Controllers/Api/Remote/FileDownloadController.php @@ -0,0 +1,50 @@ +cache = $cache; + } + + /** + * Handle a request to authenticate a download using a token and return + * the path of the file to the daemon. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + * + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + */ + public function index(Request $request): JsonResponse + { + $download = $this->cache->pull('Server:Downloads:' . $request->input('token', '')); + + if (is_null($download)) { + throw new NotFoundHttpException('No file was found using the token provided.'); + } + + return response()->json([ + 'path' => array_get($download, 'path'), + 'server' => array_get($download, 'server'), + ]); + } +} diff --git a/app/Http/Controllers/Base/ClientApiController.php b/app/Http/Controllers/Base/ClientApiController.php new file mode 100644 index 000000000..a74c28db8 --- /dev/null +++ b/app/Http/Controllers/Base/ClientApiController.php @@ -0,0 +1,109 @@ +alert = $alert; + $this->creationService = $creationService; + $this->repository = $repository; + } + + /** + * Return all of the API keys available to this user. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\View\View + */ + public function index(Request $request): View + { + return view('base.api.index', [ + 'keys' => $this->repository->getAccountKeys($request->user()), + ]); + } + + /** + * Render UI to allow creation of an API key. + * + * @return \Illuminate\View\View + */ + public function create(): View + { + return view('base.api.new'); + } + + /** + * Create the API key and return the user to the key listing page. + * + * @param \Pterodactyl\Http\Requests\Base\CreateClientApiKeyRequest $request + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function store(CreateClientApiKeyRequest $request): RedirectResponse + { + $allowedIps = null; + if (! is_null($request->input('allowed_ips'))) { + $allowedIps = json_encode(explode(PHP_EOL, $request->input('allowed_ips'))); + } + + $this->creationService->setKeyType(ApiKey::TYPE_ACCOUNT)->handle([ + 'memo' => $request->input('memo'), + 'allowed_ips' => $allowedIps, + 'user_id' => $request->user()->id, + ]); + + $this->alert->success('A new client API key has been generated for your account.')->flash(); + + return redirect()->route('account.api'); + } + + /** + * Delete a client's API key from the panel. + * + * @param \Illuminate\Http\Request $request + * @param $identifier + * @return \Illuminate\Http\Response + */ + public function delete(Request $request, $identifier): Response + { + $this->repository->deleteAccountKey($request->user(), $identifier); + + return response('', 204); + } +} diff --git a/app/Http/Controllers/Daemon/ActionController.php b/app/Http/Controllers/Daemon/ActionController.php index 1c67ec6cd..fef0b35b7 100644 --- a/app/Http/Controllers/Daemon/ActionController.php +++ b/app/Http/Controllers/Daemon/ActionController.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Http\Controllers\Daemon; @@ -17,28 +10,6 @@ use Pterodactyl\Http\Controllers\Controller; class ActionController extends Controller { - /** - * Handles download request from daemon. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse - */ - public function authenticateDownload(Request $request) - { - $download = Cache::tags(['Server:Downloads'])->pull($request->input('token')); - - if (is_null($download)) { - return response()->json([ - 'error' => 'An invalid request token was recieved with this request.', - ], 403); - } - - return response()->json([ - 'path' => $download['path'], - 'server' => $download['server'], - ]); - } - /** * Handles install toggle request from daemon. * @@ -78,7 +49,7 @@ class ActionController extends Controller */ public function configuration(Request $request, $token) { - $nodeId = Cache::tags(['Node:Configuration'])->pull($token); + $nodeId = Cache::pull('Node:Configuration:' . $token); if (is_null($nodeId)) { return response()->json(['error' => 'token_invalid'], 403); } diff --git a/app/Http/Controllers/Server/DatabaseController.php b/app/Http/Controllers/Server/DatabaseController.php index 06636c4c0..be7d501ba 100644 --- a/app/Http/Controllers/Server/DatabaseController.php +++ b/app/Http/Controllers/Server/DatabaseController.php @@ -4,34 +4,76 @@ namespace Pterodactyl\Http\Controllers\Server; use Illuminate\View\View; use Illuminate\Http\Request; +use Illuminate\Http\Response; use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Traits\Controllers\JavascriptInjection; use Pterodactyl\Services\Databases\DatabasePasswordService; +use Pterodactyl\Services\Databases\DatabaseManagementService; +use Pterodactyl\Services\Databases\DeployServerDatabaseService; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; +use Pterodactyl\Http\Requests\Server\Database\StoreServerDatabaseRequest; +use Pterodactyl\Http\Requests\Server\Database\DeleteServerDatabaseRequest; class DatabaseController extends Controller { use JavascriptInjection; + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + private $alert; + + /** + * @var \Pterodactyl\Services\Databases\DeployServerDatabaseService + */ + private $deployServerDatabaseService; + + /** + * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface + */ + private $databaseHostRepository; + + /** + * @var \Pterodactyl\Services\Databases\DatabaseManagementService + */ + private $managementService; + /** * @var \Pterodactyl\Services\Databases\DatabasePasswordService */ - protected $passwordService; + private $passwordService; /** * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface */ - protected $repository; + private $repository; /** * DatabaseController constructor. * - * @param \Pterodactyl\Services\Databases\DatabasePasswordService $passwordService - * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Services\Databases\DeployServerDatabaseService $deployServerDatabaseService + * @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $databaseHostRepository + * @param \Pterodactyl\Services\Databases\DatabaseManagementService $managementService + * @param \Pterodactyl\Services\Databases\DatabasePasswordService $passwordService + * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository */ - public function __construct(DatabasePasswordService $passwordService, DatabaseRepositoryInterface $repository) - { + public function __construct( + AlertsMessageBag $alert, + DeployServerDatabaseService $deployServerDatabaseService, + DatabaseHostRepositoryInterface $databaseHostRepository, + DatabaseManagementService $managementService, + DatabasePasswordService $passwordService, + DatabaseRepositoryInterface $repository + ) { + $this->alert = $alert; + $this->databaseHostRepository = $databaseHostRepository; + $this->deployServerDatabaseService = $deployServerDatabaseService; + $this->managementService = $managementService; $this->passwordService = $passwordService; $this->repository = $repository; } @@ -50,11 +92,42 @@ class DatabaseController extends Controller $this->authorize('view-databases', $server); $this->setRequest($request)->injectJavascript(); + $canCreateDatabase = config('pterodactyl.client_features.databases.enabled'); + $allowRandom = config('pterodactyl.client_features.databases.allow_random'); + + if ($this->databaseHostRepository->findCountWhere([['node_id', '=', $server->node_id]]) === 0) { + if ($canCreateDatabase && ! $allowRandom) { + $canCreateDatabase = false; + } + } + + $databases = $this->repository->getDatabasesForServer($server->id); + return view('server.databases.index', [ - 'databases' => $this->repository->getDatabasesForServer($server->id), + 'allowCreation' => $canCreateDatabase, + 'overLimit' => ! is_null($server->database_limit) && count($databases) >= $server->database_limit, + 'databases' => $databases, ]); } + /** + * Handle a request from a user to create a new database for the server. + * + * @param \Pterodactyl\Http\Requests\Server\Database\StoreServerDatabaseRequest $request + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException + */ + public function store(StoreServerDatabaseRequest $request): RedirectResponse + { + $this->deployServerDatabaseService->handle($request->getServer(), $request->validated()); + + $this->alert->success('Successfully created a new database.')->flash(); + + return redirect()->route('server.databases.index', $request->getServer()->uuidShort); + } + /** * Handle a request to update the password for a specific database. * @@ -74,4 +147,19 @@ class DatabaseController extends Controller return response()->json(['password' => $password]); } + + /** + * Delete a database for this server from the SQL server and Panel database. + * + * @param \Pterodactyl\Http\Requests\Server\Database\DeleteServerDatabaseRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function delete(DeleteServerDatabaseRequest $request): Response + { + $this->managementService->delete($request->attributes->get('database')->id); + + return response('', Response::HTTP_NO_CONTENT); + } } diff --git a/app/Http/Controllers/Server/Files/DownloadController.php b/app/Http/Controllers/Server/Files/DownloadController.php index 79155a63a..04b16d084 100644 --- a/app/Http/Controllers/Server/Files/DownloadController.php +++ b/app/Http/Controllers/Server/Files/DownloadController.php @@ -9,6 +9,7 @@ namespace Pterodactyl\Http\Controllers\Server\Files; +use Ramsey\Uuid\Uuid; use Illuminate\Http\Request; use Illuminate\Cache\Repository; use Illuminate\Http\RedirectResponse; @@ -46,9 +47,10 @@ class DownloadController extends Controller $server = $request->attributes->get('server'); $this->authorize('download-files', $server); - $token = str_random(40); + $token = Uuid::uuid4()->toString(); $node = $server->getRelation('node'); - $this->cache->tags(['Server:Downloads'])->put($token, ['server' => $server->uuid, 'path' => $file], 5); + + $this->cache->put('Server:Downloads:' . $token, ['server' => $server->uuid, 'path' => $file], 5); return redirect(sprintf('%s://%s:%s/v1/server/file/download/%s', $node->scheme, $node->fqdn, $node->daemonListen, $token)); } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 1d33e210d..b6d44530e 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http; +use Pterodactyl\Models\ApiKey; use Illuminate\Auth\Middleware\Authorize; use Illuminate\Auth\Middleware\Authenticate; use Pterodactyl\Http\Middleware\TrimStrings; @@ -14,11 +15,14 @@ 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\Api\ApiSubstituteBindings; use Illuminate\Foundation\Http\Middleware\ValidatePostSize; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; @@ -28,12 +32,10 @@ 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\Api\Client\SubstituteClientApiBindings; +use Pterodactyl\Http\Middleware\Api\Application\AuthenticateApplicationUser; use Pterodactyl\Http\Middleware\DaemonAuthenticate as OldDaemonAuthenticate; class Kernel extends HttpKernel @@ -71,8 +73,15 @@ class Kernel extends HttpKernel 'throttle:120,1', ApiSubstituteBindings::class, SetSessionDriver::class, - AuthenticateKey::class, - AuthenticateUser::class, + 'api..key:' . ApiKey::TYPE_APPLICATION, + AuthenticateApplicationUser::class, + AuthenticateIPAccess::class, + ], + 'client-api' => [ + 'throttle:60,1', + SubstituteClientApiBindings::class, + SetSessionDriver::class, + 'api..key:' . ApiKey::TYPE_ACCOUNT, AuthenticateIPAccess::class, ], 'daemon' => [ @@ -107,5 +116,8 @@ class Kernel extends HttpKernel 'server..database' => DatabaseBelongsToServer::class, 'server..subuser' => SubuserBelongsToServer::class, 'server..schedule' => ScheduleBelongsToServer::class, + + // API Specific Middleware + 'api..key' => AuthenticateKey::class, ]; } diff --git a/app/Http/Middleware/Api/ApiSubstituteBindings.php b/app/Http/Middleware/Api/ApiSubstituteBindings.php index b270be4ca..94af9b1d4 100644 --- a/app/Http/Middleware/Api/ApiSubstituteBindings.php +++ b/app/Http/Middleware/Api/ApiSubstituteBindings.php @@ -32,6 +32,11 @@ class ApiSubstituteBindings extends SubstituteBindings 'user' => User::class, ]; + /** + * @var \Illuminate\Routing\Router + */ + protected $router; + /** * Perform substitution of route parameters without triggering * a 404 error if a model is not found. @@ -45,7 +50,13 @@ class ApiSubstituteBindings extends SubstituteBindings $route = $request->route(); foreach (self::$mappings as $key => $model) { - $this->router->model($key, $model); + if (! is_null($this->router->getBindingCallback($key))) { + continue; + } + + $this->router->model($key, $model, function () use ($request) { + $request->attributes->set('is_missing_model', true); + }); } $this->router->substituteBindings($route); diff --git a/app/Http/Middleware/Api/Application/AuthenticateUser.php b/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php similarity index 95% rename from app/Http/Middleware/Api/Application/AuthenticateUser.php rename to app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php index 7208dbaf9..48da8a74d 100644 --- a/app/Http/Middleware/Api/Application/AuthenticateUser.php +++ b/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php @@ -6,7 +6,7 @@ use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; -class AuthenticateUser +class AuthenticateApplicationUser { /** * Authenticate that the currently authenticated user is an administrator diff --git a/app/Http/Middleware/Api/Application/AuthenticateIPAccess.php b/app/Http/Middleware/Api/AuthenticateIPAccess.php similarity index 77% rename from app/Http/Middleware/Api/Application/AuthenticateIPAccess.php rename to app/Http/Middleware/Api/AuthenticateIPAccess.php index 6988c637d..aed8f53a4 100644 --- a/app/Http/Middleware/Api/Application/AuthenticateIPAccess.php +++ b/app/Http/Middleware/Api/AuthenticateIPAccess.php @@ -1,6 +1,6 @@ ip()); - foreach ($model->allowed_ips as $ip) { + foreach (json_decode($model->allowed_ips) as $ip) { if (Range::parse($ip)->contains($find)) { return $next($request); } } - throw new AccessDeniedHttpException('This IP address does not have permission to access the API using these credentials.'); + throw new AccessDeniedHttpException('This IP address (' . $request->ip() . ') does not have permission to access the API using these credentials.'); } } diff --git a/app/Http/Middleware/Api/Application/AuthenticateKey.php b/app/Http/Middleware/Api/AuthenticateKey.php similarity index 92% rename from app/Http/Middleware/Api/Application/AuthenticateKey.php rename to app/Http/Middleware/Api/AuthenticateKey.php index 30e6236ed..8f400bb4d 100644 --- a/app/Http/Middleware/Api/Application/AuthenticateKey.php +++ b/app/Http/Middleware/Api/AuthenticateKey.php @@ -1,6 +1,6 @@ bearerToken())) { throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']); @@ -68,7 +69,7 @@ class AuthenticateKey try { $model = $this->repository->findFirstWhere([ ['identifier', '=', $identifier], - ['key_type', '=', ApiKey::TYPE_APPLICATION], + ['key_type', '=', $keyType], ]); } catch (RecordNotFoundException $exception) { throw new AccessDeniedHttpException; diff --git a/app/Http/Middleware/Api/Client/AuthenticateClientAccess.php b/app/Http/Middleware/Api/Client/AuthenticateClientAccess.php new file mode 100644 index 000000000..0a006aef0 --- /dev/null +++ b/app/Http/Middleware/Api/Client/AuthenticateClientAccess.php @@ -0,0 +1,27 @@ +user())) { + throw new AccessDeniedHttpException('This account does not have permission to access this resource.'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php new file mode 100644 index 000000000..f8a35fdd8 --- /dev/null +++ b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php @@ -0,0 +1,39 @@ +router->bind('server', function ($value) use ($request) { + try { + return Container::getInstance()->make(ServerRepositoryInterface::class)->findFirstWhere([ + ['uuidShort', '=', $value], + ]); + } catch (RecordNotFoundException $ex) { + $request->attributes->set('is_missing_model', true); + + return null; + } + }); + + return parent::handle($request, $next); + } +} diff --git a/app/Http/Middleware/Api/Application/SetSessionDriver.php b/app/Http/Middleware/Api/SetSessionDriver.php similarity index 95% rename from app/Http/Middleware/Api/Application/SetSessionDriver.php rename to app/Http/Middleware/Api/SetSessionDriver.php index c4660ec9b..c69311a65 100644 --- a/app/Http/Middleware/Api/Application/SetSessionDriver.php +++ b/app/Http/Middleware/Api/SetSessionDriver.php @@ -1,6 +1,6 @@ attributes->get('server'); + $database = $request->input('database') ?? $request->route()->parameter('database'); - $database = $this->repository->find($request->input('database')); + if (! is_digit($database)) { + throw new NotFoundHttpException; + } + + $database = $this->repository->find($database); if (is_null($database) || $database->server_id !== $server->id) { throw new NotFoundHttpException; } diff --git a/app/Http/Requests/Api/Application/ApplicationApiRequest.php b/app/Http/Requests/Api/Application/ApplicationApiRequest.php index ada9b4b00..084a89bdd 100644 --- a/app/Http/Requests/Api/Application/ApplicationApiRequest.php +++ b/app/Http/Requests/Api/Application/ApplicationApiRequest.php @@ -3,7 +3,6 @@ namespace Pterodactyl\Http\Requests\Api\Application; use Pterodactyl\Models\ApiKey; -use Illuminate\Database\Eloquent\Model; use Pterodactyl\Services\Acl\Api\AdminAcl; use Illuminate\Foundation\Http\FormRequest; use Pterodactyl\Exceptions\PterodactylException; @@ -13,6 +12,14 @@ use Symfony\Component\Routing\Exception\InvalidParameterException; abstract class ApplicationApiRequest extends FormRequest { + /** + * Tracks if the request has been validated internally or not to avoid + * making duplicate validation calls. + * + * @var bool + */ + private $hasValidated = false; + /** * The resource that should be checked when performing the authorization * function for this request. @@ -96,6 +103,21 @@ abstract class ApplicationApiRequest extends FormRequest return $this->route()->parameter($parameterKey); } + /** + * Validate that the resource exists and can be accessed prior to booting + * the validator and attempting to use the data. + * + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + protected function prepareForValidation() + { + if (! $this->passesAuthorization()) { + $this->failedAuthorization(); + } + + $this->hasValidated = true; + } + /* * Determine if the request passes the authorization check as well * as the exists check. @@ -110,6 +132,14 @@ abstract class ApplicationApiRequest extends FormRequest */ protected function passesAuthorization() { + // If we have already validated we do not need to call this function + // again. This is needed to work around Laravel's normal auth validation + // that occurs after validating the request params since we are doing auth + // validation in the prepareForValidation() function. + if ($this->hasValidated) { + return true; + } + if (! parent::passesAuthorization()) { return false; } diff --git a/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php b/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php index 728b1ce52..07c201336 100644 --- a/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php +++ b/app/Http/Requests/Api/Application/Servers/ServerWriteRequest.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers; -use Pterodactyl\Models\Server; use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; @@ -17,16 +16,4 @@ class ServerWriteRequest extends ApplicationApiRequest * @var int */ protected $permission = AdminAcl::WRITE; - - /** - * Determine if the requested server exists on the Panel. - * - * @return bool - */ - public function resourceExists(): bool - { - $server = $this->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 index 6d0d2ecf3..d12b738e6 100644 --- a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php +++ b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php @@ -39,7 +39,7 @@ class StoreServerRequest extends ApplicationApiRequest 'pack' => $rules['pack_id'], 'docker_image' => $rules['image'], 'startup' => $rules['startup'], - 'environment' => 'required|array', + 'environment' => 'present|array', 'skip_scripts' => 'sometimes|boolean', // Resource limitations diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php index 893ff5ff7..076abdf4a 100644 --- a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php @@ -13,7 +13,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest */ public function rules(): array { - $rules = Server::getUpdateRulesForId($this->route()->parameter('server')->id); + $rules = Server::getUpdateRulesForId($this->getModel(Server::class)->id); return [ 'allocation' => $rules['allocation_id'], @@ -26,6 +26,9 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest 'add_allocations.*' => 'integer', 'remove_allocations' => 'bail|array', 'remove_allocations.*' => 'integer', + 'feature_limits' => 'required|array', + 'feature_limits.databases' => $rules['database_limit'], + 'feature_limits.allocations' => $rules['allocation_limit'], ]; } @@ -39,7 +42,9 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest $data = parent::validated(); $data['allocation_id'] = $data['allocation']; - unset($data['allocation']); + $data['database_limit'] = $data['feature_limits']['databases']; + $data['allocation_limit'] = $data['feature_limits']['allocations']; + unset($data['allocation'], $data['feature_limits']); return $data; } @@ -56,6 +61,8 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest 'remove_allocations' => 'allocations to remove', 'add_allocations.*' => 'allocation to add', 'remove_allocations.*' => 'allocation to remove', + 'feature_limits.databases' => 'Database Limit', + 'feature_limits.allocations' => 'Allocation Limit', ]; } } diff --git a/app/Http/Requests/Api/Client/ClientApiRequest.php b/app/Http/Requests/Api/Client/ClientApiRequest.php new file mode 100644 index 000000000..ed63ccbf0 --- /dev/null +++ b/app/Http/Requests/Api/Client/ClientApiRequest.php @@ -0,0 +1,19 @@ +user()->can('view-server', $this->getModel(Server::class)); + } +} diff --git a/app/Http/Requests/Api/Client/Servers/SendCommandRequest.php b/app/Http/Requests/Api/Client/Servers/SendCommandRequest.php new file mode 100644 index 000000000..788f97739 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/SendCommandRequest.php @@ -0,0 +1,30 @@ +user()->can('send-command', $this->getModel(Server::class)); + } + + /** + * Rules to validate this request aganist. + * + * @return array + */ + public function rules(): array + { + return [ + 'command' => 'required|string|min:1', + ]; + } +} diff --git a/app/Http/Requests/Api/Client/Servers/SendPowerRequest.php b/app/Http/Requests/Api/Client/Servers/SendPowerRequest.php new file mode 100644 index 000000000..19614d182 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/SendPowerRequest.php @@ -0,0 +1,31 @@ +user()->can('power-' . $this->input('signal', '_undefined'), $this->getModel(Server::class)); + } + + /** + * Rules to validate this request aganist. + * + * @return array + */ + public function rules(): array + { + return [ + 'signal' => 'required|string|in:start,stop,restart,kill', + ]; + } +} diff --git a/app/Http/Requests/Base/CreateClientApiKeyRequest.php b/app/Http/Requests/Base/CreateClientApiKeyRequest.php new file mode 100644 index 000000000..b8e7bbfe2 --- /dev/null +++ b/app/Http/Requests/Base/CreateClientApiKeyRequest.php @@ -0,0 +1,21 @@ + 'required|string|max:255', + 'allowed_ips' => 'nullable|string', + ]; + } +} diff --git a/app/Http/Requests/Server/Database/DeleteServerDatabaseRequest.php b/app/Http/Requests/Server/Database/DeleteServerDatabaseRequest.php new file mode 100644 index 000000000..f2c81d9c8 --- /dev/null +++ b/app/Http/Requests/Server/Database/DeleteServerDatabaseRequest.php @@ -0,0 +1,40 @@ + 'required|string|min:1', + 'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/', + ]; + } +} diff --git a/app/Http/Requests/Server/ServerFormRequest.php b/app/Http/Requests/Server/ServerFormRequest.php index b796a21e0..f59ea3ae3 100644 --- a/app/Http/Requests/Server/ServerFormRequest.php +++ b/app/Http/Requests/Server/ServerFormRequest.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Requests\Server; +use Pterodactyl\Models\Server; use Pterodactyl\Http\Requests\FrontendUserFormRequest; abstract class ServerFormRequest extends FrontendUserFormRequest @@ -24,6 +25,11 @@ abstract class ServerFormRequest extends FrontendUserFormRequest return false; } - return $this->user()->can($this->permission(), $this->attributes->get('server')); + return $this->user()->can($this->permission(), $this->getServer()); + } + + public function getServer(): Server + { + return $this->attributes->get('server'); } } diff --git a/app/Models/Permission.php b/app/Models/Permission.php index c9fd886fe..5d9ea6c72 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -93,6 +93,8 @@ class Permission extends Model implements CleansAttributes, ValidableContract 'database' => [ 'view-databases' => null, 'reset-db-password' => null, + 'delete-database' => null, + 'create-database' => null, ], 'file' => [ 'access-sftp' => null, diff --git a/app/Models/Server.php b/app/Models/Server.php index 458bff1d6..d5c6b3a8d 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -69,6 +69,8 @@ class Server extends Model implements CleansAttributes, ValidableContract 'skip_scripts' => 'sometimes', 'image' => 'required', 'startup' => 'required', + 'database_limit' => 'present', + 'allocation_limit' => 'present', ]; /** @@ -93,6 +95,8 @@ class Server extends Model implements CleansAttributes, ValidableContract 'skip_scripts' => 'boolean', 'image' => 'string|max:255', 'installed' => 'boolean', + 'database_limit' => 'nullable|integer|min:0', + 'allocation_limit' => 'nullable|integer|min:0', ]; /** @@ -116,6 +120,8 @@ class Server extends Model implements CleansAttributes, ValidableContract 'egg_id' => 'integer', 'pack_id' => 'integer', 'installed' => 'integer', + 'database_limit' => 'integer', + 'allocation_limit' => 'integer', ]; /** diff --git a/app/Providers/MacroServiceProvider.php b/app/Providers/MacroServiceProvider.php index ddfbf7aa8..9eae42b81 100644 --- a/app/Providers/MacroServiceProvider.php +++ b/app/Providers/MacroServiceProvider.php @@ -1,21 +1,9 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Providers; -use File; -use Cache; -use Carbon; -use Request; -use Pterodactyl\Models\ApiKey; +use Illuminate\Support\Facades\File; use Illuminate\Support\ServiceProvider; -use Pterodactyl\Services\ApiKeyService; class MacroServiceProvider extends ServiceProvider { @@ -36,35 +24,5 @@ class MacroServiceProvider extends ServiceProvider return round($size, ($i < 2) ? 0 : $precision) . ' ' . $units[$i]; }); - - Request::macro('apiKey', function () { - if (! Request::bearerToken()) { - return false; - } - - $parts = explode('.', Request::bearerToken()); - - if (count($parts) === 2 && strlen($parts[0]) === ApiKeyService::PUB_CRYPTO_BYTES * 2) { - // Because the key itself isn't changing frequently, we simply cache this for - // 15 minutes to speed up the API and keep requests flowing. - return Cache::tags([ - 'ApiKeyMacro', - 'ApiKeyMacro:Key:' . $parts[0], - ])->remember('ApiKeyMacro.' . $parts[0], Carbon::now()->addMinutes(15), function () use ($parts) { - return ApiKey::where('public', $parts[0])->first(); - }); - } - - return false; - }); - - Request::macro('apiKeyHasPermission', function ($permission) { - $key = Request::apiKey(); - if (! $key) { - return false; - } - - return Request::user()->can($permission, $key); - }); } } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index f9f6ac31d..3de307d9a 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -41,6 +41,10 @@ class RouteServiceProvider extends ServiceProvider ->namespace($this->namespace . '\Api\Application') ->group(base_path('routes/api-application.php')); + Route::middleware(['client-api'])->prefix('/api/client') + ->namespace($this->namespace . '\Api\Client') + ->group(base_path('routes/api-client.php')); + Route::middleware(['daemon'])->prefix('/api/remote') ->namespace($this->namespace . '\Api\Remote') ->group(base_path('routes/api-remote.php')); diff --git a/app/Repositories/Daemon/CommandRepository.php b/app/Repositories/Daemon/CommandRepository.php index 31cb6b9b7..7b7577b32 100644 --- a/app/Repositories/Daemon/CommandRepository.php +++ b/app/Repositories/Daemon/CommandRepository.php @@ -12,7 +12,6 @@ class CommandRepository extends BaseRepository implements CommandRepositoryInter * * @param string $command * @return \Psr\Http\Message\ResponseInterface - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function send(string $command): ResponseInterface { diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index 7bca12691..5a53d33f0 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -264,6 +264,45 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt } } + /** + * Return all of the servers that should have a power action performed aganist them. + * + * @param int[] $servers + * @param int[] $nodes + * @param bool $returnCount + * @return int|\Generator + */ + public function getServersForPowerAction(array $servers = [], array $nodes = [], bool $returnCount = false) + { + $instance = $this->getBuilder(); + + if (! empty($nodes) && ! empty($servers)) { + $instance->whereIn('id', $servers)->orWhereIn('node_id', $nodes); + } elseif (empty($nodes) && ! empty($servers)) { + $instance->whereIn('id', $servers); + } elseif (! empty($nodes) && empty($servers)) { + $instance->whereIn('node_id', $nodes); + } + + if ($returnCount) { + return $instance->count(); + } + + return $instance->with('node')->cursor(); + } + + /** + * Return the total number of servers that will be affected by the query. + * + * @param int[] $servers + * @param int[] $nodes + * @return int + */ + public function getServersForPowerActionCount(array $servers = [], array $nodes = []): int + { + return $this->getServersForPowerAction($servers, $nodes, true); + } + /** * Return an array of server IDs that a given user can access based * on owner and subuser permissions. diff --git a/app/Services/Databases/DatabaseManagementService.php b/app/Services/Databases/DatabaseManagementService.php index b05ddc3ff..dc91e11f9 100644 --- a/app/Services/Databases/DatabaseManagementService.php +++ b/app/Services/Databases/DatabaseManagementService.php @@ -13,22 +13,27 @@ class DatabaseManagementService /** * @var \Illuminate\Database\DatabaseManager */ - protected $database; + private $database; /** * @var \Pterodactyl\Extensions\DynamicDatabaseConnection */ - protected $dynamic; + private $dynamic; /** * @var \Illuminate\Contracts\Encryption\Encrypter */ - protected $encrypter; + private $encrypter; /** * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface */ - protected $repository; + private $repository; + + /** + * @var bool + */ + protected $useRandomHost = false; /** * CreationService constructor. @@ -55,7 +60,7 @@ class DatabaseManagementService * * @param int $server * @param array $data - * @return \Illuminate\Database\Eloquent\Model + * @return \Pterodactyl\Models\Database * * @throws \Exception */ diff --git a/app/Services/Databases/DeployServerDatabaseService.php b/app/Services/Databases/DeployServerDatabaseService.php new file mode 100644 index 000000000..c8b5ed179 --- /dev/null +++ b/app/Services/Databases/DeployServerDatabaseService.php @@ -0,0 +1,90 @@ +databaseHostRepository = $databaseHostRepository; + $this->managementService = $managementService; + $this->repository = $repository; + } + + /** + * @param \Pterodactyl\Models\Server $server + * @param array $data + * @return \Pterodactyl\Models\Database + * + * @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException + * @throws \Exception + */ + public function handle(Server $server, array $data): Database + { + if (! config('pterodactyl.client_features.databases.enabled')) { + throw new DatabaseClientFeatureNotEnabledException; + } + + $databases = $this->repository->findCountWhere([['server_id', '=', $server->id]]); + if (! is_null($server->database_limit) && $databases >= $server->database_limit) { + throw new TooManyDatabasesException; + } + + $allowRandom = config('pterodactyl.client_features.databases.allow_random'); + $hosts = $this->databaseHostRepository->setColumns(['id'])->findWhere([ + ['node_id', '=', $server->node_id], + ]); + + if ($hosts->isEmpty() && ! $allowRandom) { + throw new NoSuitableDatabaseHostException; + } + + if ($hosts->isEmpty()) { + $hosts = $this->databaseHostRepository->setColumns(['id'])->all(); + if ($hosts->isEmpty()) { + throw new NoSuitableDatabaseHostException; + } + } + + $host = $hosts->random(); + + return $this->managementService->create($server->id, [ + 'database_host_id' => $host->id, + 'database' => array_get($data, 'database'), + 'remote' => array_get($data, 'remote'), + ]); + } +} diff --git a/app/Services/Servers/BuildModificationService.php b/app/Services/Servers/BuildModificationService.php index 8924b2a04..5d36b4c5c 100644 --- a/app/Services/Servers/BuildModificationService.php +++ b/app/Services/Servers/BuildModificationService.php @@ -91,6 +91,8 @@ class BuildModificationService 'cpu' => array_get($data, 'cpu'), 'disk' => array_get($data, 'disk'), 'allocation_id' => array_get($data, 'allocation_id'), + 'database_limit' => array_get($data, 'database_limit'), + 'allocation_limit' => array_get($data, 'allocation_limit'), ]); $allocations = $this->allocationRepository->findWhere([['server_id', '=', $server->id]]); diff --git a/app/Services/Servers/ReinstallServerService.php b/app/Services/Servers/ReinstallServerService.php index 682813e36..85800473f 100644 --- a/app/Services/Servers/ReinstallServerService.php +++ b/app/Services/Servers/ReinstallServerService.php @@ -66,7 +66,7 @@ class ReinstallServerService $this->database->beginTransaction(); $this->repository->withoutFreshModel()->update($server->id, [ 'installed' => 0, - ]); + ], true, true); try { $this->daemonServerRepository->setServer($server)->reinstall(); diff --git a/app/Transformers/Api/Application/BaseTransformer.php b/app/Transformers/Api/Application/BaseTransformer.php index c2fdf6137..5766088c9 100644 --- a/app/Transformers/Api/Application/BaseTransformer.php +++ b/app/Transformers/Api/Application/BaseTransformer.php @@ -7,6 +7,7 @@ use Pterodactyl\Models\ApiKey; use Illuminate\Container\Container; use League\Fractal\TransformerAbstract; use Pterodactyl\Services\Acl\Api\AdminAcl; +use Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException; abstract class BaseTransformer extends TransformerAbstract { @@ -78,13 +79,19 @@ abstract class BaseTransformer extends TransformerAbstract * @param string $abstract * @param array $parameters * @return \Pterodactyl\Transformers\Api\Application\BaseTransformer + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ - protected function makeTransformer(string $abstract, array $parameters = []): self + protected function makeTransformer(string $abstract, array $parameters = []) { /** @var \Pterodactyl\Transformers\Api\Application\BaseTransformer $transformer */ $transformer = Container::getInstance()->makeWith($abstract, $parameters); $transformer->setKey($this->getKey()); + if (! $transformer instanceof self) { + throw new InvalidTransformerLevelException('Calls to ' . __METHOD__ . ' must return a transformer that is an instance of ' . __CLASS__); + } + return $transformer; } diff --git a/app/Transformers/Api/Application/PackTransformer.php b/app/Transformers/Api/Application/PackTransformer.php index 973002ae8..e77bdd459 100644 --- a/app/Transformers/Api/Application/PackTransformer.php +++ b/app/Transformers/Api/Application/PackTransformer.php @@ -1,90 +1,40 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ -namespace Pterodactyl\Transformers\Admin; +namespace Pterodactyl\Transformers\Api\Application; -use Illuminate\Http\Request; use Pterodactyl\Models\Pack; -use League\Fractal\TransformerAbstract; -class PackTransformer extends TransformerAbstract +class PackTransformer extends BaseTransformer { /** - * List of resources that can be included. + * Return the resource name for the JSONAPI output. * - * @var array + * @return string */ - protected $availableIncludes = [ - 'option', - '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) + public function getResourceName(): string { - if (! $request instanceof Request && $request !== false) { - throw new DisplayException('Request passed to constructor must be of type Request or false.'); - } - - $this->request = $request; + return Pack::RESOURCE_NAME; } /** - * Return a generic transformed pack array. + * Return a transformed User model that can be consumed by external services. * + * @param \Pterodactyl\Models\Pack $pack * @return array */ - public function transform($pack) + public function transform(Pack $pack): array { - if (! $pack instanceof Pack) { - return ['id' => null]; - } - - return $pack->toArray(); - } - - /** - * Return the packs associated with this service. - * - * @return \Leauge\Fractal\Resource\Item - */ - public function includeOption(Pack $pack) - { - if ($this->request && ! $this->request->apiKeyHasPermission('option-view')) { - return; - } - - return $this->item($pack->option, new OptionTransformer($this->request), 'option'); - } - - /** - * Return the packs associated with this service. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeServers(Pack $pack) - { - if ($this->request && ! $this->request->apiKeyHasPermission('server-list')) { - return; - } - - return $this->collection($pack->servers, new ServerTransformer($this->request), 'server'); + return [ + 'id' => $pack->id, + 'uuid' => $pack->uuid, + 'egg' => $pack->egg_id, + 'name' => $pack->name, + 'description' => $pack->description, + 'is_selectable' => (bool) $pack->selectable, + 'is_visible' => (bool) $pack->visible, + 'is_locked' => (bool) $pack->locked, + 'created_at' => $this->formatTimestamp($pack->created_at), + 'updated_at' => $this->formatTimestamp($pack->updated_at), + ]; } } diff --git a/app/Transformers/Api/Application/ServerTransformer.php b/app/Transformers/Api/Application/ServerTransformer.php index 69115d1ed..2a542dbcd 100644 --- a/app/Transformers/Api/Application/ServerTransformer.php +++ b/app/Transformers/Api/Application/ServerTransformer.php @@ -75,6 +75,10 @@ class ServerTransformer extends BaseTransformer 'io' => $server->io, 'cpu' => $server->cpu, ], + 'feature_limits' => [ + 'databases' => $server->database_limit, + 'allocations' => $server->allocation_limit, + ], 'user' => $server->owner_id, 'node' => $server->node_id, 'allocation' => $server->allocation_id, @@ -97,6 +101,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeAllocations(Server $server) { @@ -114,6 +120,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeSubusers(Server $server) { @@ -131,6 +139,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeUser(Server $server) { @@ -148,40 +158,49 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ -// 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'); -// } + public function includePack(Server $server) + { + if (! $this->authorize(AdminAcl::RESOURCE_PACKS)) { + return $this->null(); + } + + $server->loadMissing('pack'); + if (is_null($server->getRelation('pack'))) { + return $this->null(); + } + + 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 + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ -// 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'); -// } + 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 + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeOption(Server $server) { @@ -199,6 +218,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeVariables(Server $server) { @@ -216,6 +237,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeLocation(Server $server) { @@ -233,6 +256,8 @@ class ServerTransformer extends BaseTransformer * * @param \Pterodactyl\Models\Server $server * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeNode(Server $server) { diff --git a/app/Transformers/Api/Application/SubuserTransformer.php b/app/Transformers/Api/Application/SubuserTransformer.php deleted file mode 100644 index 93ed25d52..000000000 --- a/app/Transformers/Api/Application/SubuserTransformer.php +++ /dev/null @@ -1,60 +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\Subuser; -use League\Fractal\TransformerAbstract; - -class SubuserTransformer extends TransformerAbstract -{ - /** - * 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(Subuser $subuser) - { - if ($this->request && ! $this->request->apiKeyHasPermission('server-view')) { - return; - } - - return [ - 'id' => $subuser->id, - 'username' => $subuser->user->username, - 'email' => $subuser->user->email, - '2fa' => (bool) $subuser->user->use_totp, - 'permissions' => $subuser->permissions->pluck('permission'), - 'created_at' => $subuser->created_at, - 'updated_at' => $subuser->updated_at, - ]; - } -} diff --git a/app/Transformers/Api/Client/BaseClientTransformer.php b/app/Transformers/Api/Client/BaseClientTransformer.php new file mode 100644 index 000000000..faa1abbed --- /dev/null +++ b/app/Transformers/Api/Client/BaseClientTransformer.php @@ -0,0 +1,74 @@ +user; + } + + /** + * Set the user model of the user requesting this transformation. + * + * @param \Pterodactyl\Models\User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + + /** + * 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 $ability + * @param \Pterodactyl\Models\Server $server + * @return bool + */ + protected function authorize(string $ability, Server $server = null): bool + { + Assert::isInstanceOf($server, Server::class); + + return $this->getUser()->can($ability, [$server]); + } + + /** + * Create a new instance of the transformer and pass along the currently + * set API key. + * + * @param string $abstract + * @param array $parameters + * @return self + * + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException + */ + protected function makeTransformer(string $abstract, array $parameters = []) + { + $transformer = parent::makeTransformer($abstract, $parameters); + + if (! $transformer instanceof self) { + throw new InvalidTransformerLevelException('Calls to ' . __METHOD__ . ' must return a transformer that is an instance of ' . __CLASS__); + } + + return $transformer; + } +} diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php new file mode 100644 index 000000000..6816d6d74 --- /dev/null +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -0,0 +1,45 @@ + $this->getKey()->user_id === $server->owner_id, + 'identifier' => $server->uuidShort, + 'uuid' => $server->uuid, + 'name' => $server->name, + 'description' => $server->description, + 'limits' => [ + 'memory' => $server->memory, + 'swap' => $server->swap, + 'disk' => $server->disk, + 'io' => $server->io, + 'cpu' => $server->cpu, + ], + 'feature_limits' => [ + 'databases' => $server->database_limit, + 'allocations' => $server->allocation_limit, + ], + ]; + } +} diff --git a/app/Transformers/User/AllocationTransformer.php b/app/Transformers/User/AllocationTransformer.php deleted file mode 100644 index 8ea0c8f91..000000000 --- a/app/Transformers/User/AllocationTransformer.php +++ /dev/null @@ -1,47 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\User; - -use Pterodactyl\Models\Server; -use Pterodactyl\Models\Allocation; -use League\Fractal\TransformerAbstract; - -class AllocationTransformer extends TransformerAbstract -{ - /** - * Server eloquent model. - * - * @return \Pterodactyl\Models\Server - */ - protected $server; - - /** - * Setup allocation transformer with access to server data. - */ - public function __construct(Server $server) - { - $this->server = $server; - } - - /** - * Return a generic transformed allocation array. - * - * @return array - */ - public function transform(Allocation $allocation) - { - return [ - 'id' => $allocation->id, - 'ip' => $allocation->alias, - 'port' => $allocation->port, - 'default' => ($allocation->id === $this->server->allocation_id), - ]; - } -} diff --git a/app/Transformers/User/OverviewTransformer.php b/app/Transformers/User/OverviewTransformer.php deleted file mode 100644 index b59990766..000000000 --- a/app/Transformers/User/OverviewTransformer.php +++ /dev/null @@ -1,35 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\User; - -use Pterodactyl\Models\Server; -use League\Fractal\TransformerAbstract; - -class OverviewTransformer extends TransformerAbstract -{ - /** - * Return a generic transformed server array. - * - * @return array - */ - public function transform(Server $server) - { - return [ - 'id' => $server->uuidShort, - 'uuid' => $server->uuid, - 'name' => $server->name, - 'node' => $server->node->name, - 'ip' => $server->allocation->alias, - 'port' => $server->allocation->port, - 'service' => $server->service->name, - 'option' => $server->option->name, - ]; - } -} diff --git a/app/Transformers/User/ServerTransformer.php b/app/Transformers/User/ServerTransformer.php deleted file mode 100644 index 031ae82f8..000000000 --- a/app/Transformers/User/ServerTransformer.php +++ /dev/null @@ -1,85 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\User; - -use Pterodactyl\Models\Server; -use League\Fractal\TransformerAbstract; - -class ServerTransformer extends TransformerAbstract -{ - /** - * List of resources that can be included. - * - * @var array - */ - protected $availableIncludes = [ - 'allocations', - 'subusers', - 'stats', - ]; - - /** - * Return a generic transformed server array. - * - * @return array - */ - public function transform(Server $server) - { - return [ - 'id' => $server->uuidShort, - 'uuid' => $server->uuid, - 'name' => $server->name, - 'description' => $server->description, - 'node' => $server->node->name, - 'limits' => [ - 'memory' => $server->memory, - 'swap' => $server->swap, - 'disk' => $server->disk, - 'io' => $server->io, - 'cpu' => $server->cpu, - 'oom_disabled' => (bool) $server->oom_disabled, - ], - ]; - } - - /** - * Return a generic array of allocations for this server. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeAllocations(Server $server) - { - $allocations = $server->allocations; - - return $this->collection($allocations, new AllocationTransformer($server), 'allocation'); - } - - /** - * Return a generic array of subusers for this server. - * - * @return \Leauge\Fractal\Resource\Collection - */ - public function includeSubusers(Server $server) - { - $server->load('subusers.permissions', 'subusers.user'); - - return $this->collection($server->subusers, new SubuserTransformer, 'subuser'); - } - - /** - * Return a generic array of allocations for this server. - * - * @return \Leauge\Fractal\Resource\Item - */ - public function includeStats(Server $server) - { - return $this->item($server->guzzleClient(), new StatsTransformer, 'stat'); - } -} diff --git a/app/Transformers/User/StatsTransformer.php b/app/Transformers/User/StatsTransformer.php deleted file mode 100644 index 2a2e1d5ec..000000000 --- a/app/Transformers/User/StatsTransformer.php +++ /dev/null @@ -1,48 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\User; - -use GuzzleHttp\Client; -use League\Fractal\TransformerAbstract; -use GuzzleHttp\Exception\ConnectException; - -class StatsTransformer extends TransformerAbstract -{ - /** - * Return a generic transformed subuser array. - * - * @return array - */ - public function transform(Client $client) - { - try { - $res = $client->request('GET', '/server', ['http_errors' => false]); - - if ($res->getStatusCode() !== 200) { - return [ - 'error' => 'Error: HttpResponseException. Recieved non-200 HTTP status code from daemon: ' . $res->statusCode(), - ]; - } - - $json = json_decode($res->getBody()); - - return [ - 'id' => 1, - 'status' => $json->status, - 'resources' => $json->proc, - ]; - } catch (ConnectException $ex) { - return [ - 'error' => 'Error: ConnectException. Unable to contact the daemon to request server status.', - 'exception' => (config('app.debug')) ? $ex->getMessage() : null, - ]; - } - } -} diff --git a/app/Transformers/User/SubuserTransformer.php b/app/Transformers/User/SubuserTransformer.php deleted file mode 100644 index faac5965c..000000000 --- a/app/Transformers/User/SubuserTransformer.php +++ /dev/null @@ -1,32 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Transformers\User; - -use Pterodactyl\Models\Subuser; -use League\Fractal\TransformerAbstract; - -class SubuserTransformer extends TransformerAbstract -{ - /** - * Return a generic transformed subuser array. - * - * @return array - */ - public function transform(Subuser $subuser) - { - return [ - 'id' => $subuser->id, - 'username' => $subuser->user->username, - 'email' => $subuser->user->email, - '2fa' => (bool) $subuser->user->use_totp, - 'permissions' => $subuser->permissions->pluck('permission'), - ]; - } -} diff --git a/config/app.php b/config/app.php index 50ab9f19c..f95d7162d 100644 --- a/config/app.php +++ b/config/app.php @@ -9,7 +9,7 @@ return [ | change this value if you are not maintaining your own internal versions. */ - 'version' => '0.7.3', + 'version' => '0.7.4', /* |-------------------------------------------------------------------------- diff --git a/config/pterodactyl.php b/config/pterodactyl.php index 523080ae3..e06c709ef 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -163,6 +163,21 @@ return [ 'in_context' => env('PHRASE_IN_CONTEXT', false), ], + /* + |-------------------------------------------------------------------------- + | Language Editor + |-------------------------------------------------------------------------- + | + | Set `PHRASE_IN_CONTEXT` to true to enable the PhaseApp in-context editor + | on this site which allows you to translate the panel, from the panel. + */ + 'client_features' => [ + 'databases' => [ + 'enabled' => env('PTERODACTYL_CLIENT_DATABASES_ENABLED', true), + 'allow_random' => env('PTERODACTYL_CLIENT_DATABASES_ALLOW_RANDOM', true), + ], + ], + /* |-------------------------------------------------------------------------- | File Editor diff --git a/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php b/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php index e7fd0c58c..5dba9c113 100644 --- a/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php +++ b/database/migrations/2018_01_13_142012_SetupTableForKeyEncryption.php @@ -16,7 +16,7 @@ class SetupTableForKeyEncryption extends Migration public function up() { Schema::table('api_keys', function (Blueprint $table) { - $table->char('identifier', 16)->unique()->after('user_id'); + $table->char('identifier', 16)->nullable()->unique()->after('user_id'); $table->dropUnique(['token']); }); diff --git a/database/migrations/2018_03_01_192831_add_database_and_port_limit_columns_to_servers_table.php b/database/migrations/2018_03_01_192831_add_database_and_port_limit_columns_to_servers_table.php new file mode 100644 index 000000000..4e85e8aeb --- /dev/null +++ b/database/migrations/2018_03_01_192831_add_database_and_port_limit_columns_to_servers_table.php @@ -0,0 +1,33 @@ +unsignedInteger('database_limit')->after('installed')->nullable()->default(0); + $table->unsignedInteger('allocation_limit')->after('installed')->nullable()->default(0); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn(['database_limit', 'allocation_limit']); + }); + } +} diff --git a/public/js/laroute.js b/public/js/laroute.js index 05740f254..1ffbc7b1f 100644 --- a/public/js/laroute.js +++ b/public/js/laroute.js @@ -6,7 +6,7 @@ absolute: false, 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":["DELETE"],"uri":"_debugbar\/cache\/{key}\/{tags?}","name":"debugbar.cache.delete","action":"Barryvdh\Debugbar\Controllers\CacheController@delete"},{"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\/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\ApiController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/api\/new","name":"admin.api.new","action":"Pterodactyl\Http\Controllers\Admin\ApiController@create"},{"host":null,"methods":["POST"],"uri":"admin\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ApiController@store"},{"host":null,"methods":["DELETE"],"uri":"admin\/api\/revoke\/{identifier}","name":"admin.api.delete","action":"Pterodactyl\Http\Controllers\Admin\ApiController@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\/application\/users","name":"api.application.users","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users\/{user}","name":"api.application.users.view","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users\/external\/{external_id}","name":"api.application.users.external","action":"Pterodactyl\Http\Controllers\Api\Application\Users\ExternalUserController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/users","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/users\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/users\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes","name":"api.application.nodes","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes\/{node}","name":"api.application.nodes.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/nodes","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/nodes\/{node}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/nodes\/{node}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes\/{node}\/allocations","name":"api.application.allocations","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/nodes\/{node}\/allocations","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@store"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/nodes\/{node}\/allocations\/{allocation}","name":"api.application.allocations.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/locations","name":"api.applications.locations","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/locations\/{location}","name":"api.application.locations.view","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/locations","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/locations\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/locations\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers","name":"api.application.servers","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}","name":"api.application.servers.view","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@view"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/details","name":"api.application.servers.details","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController@details"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/build","name":"api.application.servers.build","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController@build"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/startup","name":"api.application.servers.startup","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\StartupController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@store"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/suspend","name":"api.application.servers.suspend","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@suspend"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/unsuspend","name":"api.application.servers.unsuspend","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@unsuspend"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/reinstall","name":"api.application.servers.reinstall","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@reinstall"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/rebuild","name":"api.application.servers.rebuild","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@rebuild"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@delete"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}\/{force?}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}\/databases","name":"api.application.servers.databases","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}\/databases\/{database}","name":"api.application.servers.databases.view","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/databases","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@store"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/databases\/{database}\/reset-password","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@resetPassword"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}\/databases\/{database}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests","name":"api.application.nests","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\NestController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}","name":"api.application.nests.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\NestController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}\/eggs","name":"api.application.nests.eggs","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\EggController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}\/eggs\/{egg}","name":"api.application.nests.eggs.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\EggController@view"},{"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"}], + 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":["DELETE"],"uri":"_debugbar\/cache\/{key}\/{tags?}","name":"debugbar.cache.delete","action":"Barryvdh\Debugbar\Controllers\CacheController@delete"},{"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\ClientApiController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"account\/api\/new","name":"account.api.new","action":"Pterodactyl\Http\Controllers\Base\ClientApiController@create"},{"host":null,"methods":["POST"],"uri":"account\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Base\ClientApiController@store"},{"host":null,"methods":["DELETE"],"uri":"account\/api\/revoke\/{identifier}","name":"account.api.revoke","action":"Pterodactyl\Http\Controllers\Base\ClientApiController@delete"},{"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\ApiController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"admin\/api\/new","name":"admin.api.new","action":"Pterodactyl\Http\Controllers\Admin\ApiController@create"},{"host":null,"methods":["POST"],"uri":"admin\/api\/new","name":null,"action":"Pterodactyl\Http\Controllers\Admin\ApiController@store"},{"host":null,"methods":["DELETE"],"uri":"admin\/api\/revoke\/{identifier}","name":"admin.api.delete","action":"Pterodactyl\Http\Controllers\Admin\ApiController@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":["POST"],"uri":"server\/{server}\/databases\/new","name":"server.databases.new","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@store"},{"host":null,"methods":["PATCH"],"uri":"server\/{server}\/databases\/password","name":"server.databases.password","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@update"},{"host":null,"methods":["DELETE"],"uri":"server\/{server}\/databases\/delete\/{database}","name":"server.databases.delete","action":"Pterodactyl\Http\Controllers\Server\DatabaseController@delete"},{"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\/application\/users","name":"api.application.users","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users\/{user}","name":"api.application.users.view","action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/users\/external\/{external_id}","name":"api.application.users.external","action":"Pterodactyl\Http\Controllers\Api\Application\Users\ExternalUserController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/users","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/users\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/users\/{user}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Users\UserController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes","name":"api.application.nodes","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes\/{node}","name":"api.application.nodes.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/nodes","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/nodes\/{node}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/nodes\/{node}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\NodeController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nodes\/{node}\/allocations","name":"api.application.allocations","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/nodes\/{node}\/allocations","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@store"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/nodes\/{node}\/allocations\/{allocation}","name":"api.application.allocations.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nodes\AllocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/locations","name":"api.applications.locations","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/locations\/{location}","name":"api.application.locations.view","action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/locations","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@store"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/locations\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@update"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/locations\/{location}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Locations\LocationController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers","name":"api.application.servers","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}","name":"api.application.servers.view","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/external\/{external_id}","name":"api.application.servers.external","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ExternalServerController@index"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/details","name":"api.application.servers.details","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController@details"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/build","name":"api.application.servers.build","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerDetailsController@build"},{"host":null,"methods":["PATCH"],"uri":"api\/application\/servers\/{server}\/startup","name":"api.application.servers.startup","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\StartupController@index"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@store"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/suspend","name":"api.application.servers.suspend","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@suspend"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/unsuspend","name":"api.application.servers.unsuspend","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@unsuspend"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/reinstall","name":"api.application.servers.reinstall","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@reinstall"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/rebuild","name":"api.application.servers.rebuild","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerManagementController@rebuild"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@delete"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}\/{force?}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\ServerController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}\/databases","name":"api.application.servers.databases","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/servers\/{server}\/databases\/{database}","name":"api.application.servers.databases.view","action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@view"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/databases","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@store"},{"host":null,"methods":["POST"],"uri":"api\/application\/servers\/{server}\/databases\/{database}\/reset-password","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@resetPassword"},{"host":null,"methods":["DELETE"],"uri":"api\/application\/servers\/{server}\/databases\/{database}","name":null,"action":"Pterodactyl\Http\Controllers\Api\Application\Servers\DatabaseController@delete"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests","name":"api.application.nests","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\NestController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}","name":"api.application.nests.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\NestController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}\/eggs","name":"api.application.nests.eggs","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\EggController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/application\/nests\/{nest}\/eggs\/{egg}","name":"api.application.nests.eggs.view","action":"Pterodactyl\Http\Controllers\Api\Application\Nests\EggController@view"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/client","name":"api.client.index","action":"Pterodactyl\Http\Controllers\Api\Client\ClientController@index"},{"host":null,"methods":["GET","HEAD"],"uri":"api\/client\/servers\/{server}","name":"api.client.servers.view","action":"Pterodactyl\Http\Controllers\Api\Client\Servers\ServerController@index"},{"host":null,"methods":["POST"],"uri":"api\/client\/servers\/{server}\/command","name":"api.client.servers.command","action":"Pterodactyl\Http\Controllers\Api\Client\Servers\CommandController@index"},{"host":null,"methods":["POST"],"uri":"api\/client\/servers\/{server}\/power","name":"api.client.servers.power","action":"Pterodactyl\Http\Controllers\Api\Client\Servers\PowerController@index"},{"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":["POST"],"uri":"api\/remote\/download-file","name":"api.remote.download_file","action":"Pterodactyl\Http\Controllers\Api\Remote\FileDownloadController@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\/install","name":"daemon.install","action":"Pterodactyl\Http\Controllers\Daemon\ActionController@markInstall"}], prefix: '', route : function (name, parameters, route) { diff --git a/resources/lang/en/command/messages.php b/resources/lang/en/command/messages.php index 77f67c663..4a5250327 100644 --- a/resources/lang/en/command/messages.php +++ b/resources/lang/en/command/messages.php @@ -37,6 +37,10 @@ return [ ], 'server' => [ 'rebuild_failed' => 'Rebuild request for ":name" (#:id) on node ":node" failed with error: :message', + 'power' => [ + 'confirm' => 'You are about to perform a :action aganist :count servers. Do you wish to continue?', + 'action_failed' => 'Power action request for ":name" (#:id) on node ":node" failed with error: :message', + ], ], 'environment' => [ 'mail' => [ diff --git a/resources/lang/en/server.php b/resources/lang/en/server.php index cd89e1f85..8941c4793 100644 --- a/resources/lang/en/server.php +++ b/resources/lang/en/server.php @@ -248,6 +248,14 @@ return [ 'title' => 'Reset Database Password', 'description' => 'Allows a user to reset passwords for databases.', ], + 'delete_database' => [ + 'title' => 'Delete Databases', + 'description' => 'Allows a user to delete databases for this server from the Panel.', + ], + 'create_database' => [ + 'title' => 'Create Database', + 'description' => 'Allows a user to create additional databases for this server.', + ], ], ], 'files' => [ diff --git a/resources/themes/pterodactyl/admin/api/new.blade.php b/resources/themes/pterodactyl/admin/api/new.blade.php index b5876ee8c..b5db6a154 100644 --- a/resources/themes/pterodactyl/admin/api/new.blade.php +++ b/resources/themes/pterodactyl/admin/api/new.blade.php @@ -15,7 +15,7 @@ @section('content')
-
+
diff --git a/resources/themes/pterodactyl/admin/servers/new.blade.php b/resources/themes/pterodactyl/admin/servers/new.blade.php index bfb6760b4..bad452312 100644 --- a/resources/themes/pterodactyl/admin/servers/new.blade.php +++ b/resources/themes/pterodactyl/admin/servers/new.blade.php @@ -111,7 +111,7 @@
- + MB
diff --git a/resources/themes/pterodactyl/admin/servers/view/build.blade.php b/resources/themes/pterodactyl/admin/servers/view/build.blade.php index f6e9e607b..8900bf90a 100644 --- a/resources/themes/pterodactyl/admin/servers/view/build.blade.php +++ b/resources/themes/pterodactyl/admin/servers/view/build.blade.php @@ -89,50 +89,79 @@
-
-
-

Allocation Management

-
-
-
- - -

The default connection address that will be used for this game server.

-
-
- -
- +
+
+
+
+

Application Feature Limits

-

Please note that due to software limitations you cannot assign identical ports on different IPs to the same server.

-
-
- -
- +
+
+
+ +
+ +
+

The total number of databases a user is allowed to create for this server. Leave blank to allow unlimmited.

+
+
+ +
+ +
+

This feature is not currently implemented. The total number of allocations a user is allowed to create for this server. Leave blank to allow unlimited.

+
+
-

Simply select which ports you would like to remove from the list above. If you want to assign a port on a different IP that is already in use you can select it from the left and delete it here.

-
diff --git a/resources/themes/pterodactyl/base/api/index.blade.php b/resources/themes/pterodactyl/base/api/index.blade.php index e21e5aecc..24a717347 100644 --- a/resources/themes/pterodactyl/base/api/index.blade.php +++ b/resources/themes/pterodactyl/base/api/index.blade.php @@ -18,57 +18,70 @@ @endsection @section('content') -
-
-
-
-

@lang('base.api.index.list')

-
- +
+
+
+
+

Credentials List

+
-
-
- - +
+
- - - - + + + + - @foreach ($keys as $key) + @foreach($keys as $key) + - - - - + @endforeach - -
@lang('strings.memo')@lang('strings.public_key')KeyMemoLast UsedCreated
+ + •••••••• + + + {{ $key->memo }}{{ $key->identifier . decrypt($key->token) }} @if(!is_null($key->last_used_at)) @datetimeHuman($key->last_used_at) - @else + @else — @endif - + @datetimeHuman($key->created_at) + + +
+ +
-
@endsection @section('footer-scripts') @parent -@endsection - @section('content') -
-
-
-
-
@lang('base.api.new.form_title')
-
-
-
-
- - -

@lang('base.api.new.descriptive_memo.description')

-
-
- - -

@lang('base.api.new.allowed_ips.description')

-
-
-
-
- {!! csrf_field() !!} - + +
+
+
+
+ +
+

Set an easy to understand description for this API key to help you identify it later on.

-
+
+
+
+
+ + +
+

If you would like to limit this API key to specific IP addresses enter them above, one per line. CIDR notation is allowed for each IP address. Leave blank to allow any IP address.

+
+ +
+
+
- @endsection diff --git a/resources/themes/pterodactyl/layouts/master.blade.php b/resources/themes/pterodactyl/layouts/master.blade.php index 39e5a9dd6..060c76ced 100644 --- a/resources/themes/pterodactyl/layouts/master.blade.php +++ b/resources/themes/pterodactyl/layouts/master.blade.php @@ -101,11 +101,11 @@ @lang('navigation.account.security_controls') - {{--
  • --}} - {{----}} - {{-- @lang('navigation.account.api_access')--}} - {{----}} - {{--
  • --}} +
  • + + @lang('navigation.account.api_access') + +
  • @lang('navigation.account.my_servers') diff --git a/resources/themes/pterodactyl/server/databases/index.blade.php b/resources/themes/pterodactyl/server/databases/index.blade.php index 6ba24c1ad..fb618b64c 100644 --- a/resources/themes/pterodactyl/server/databases/index.blade.php +++ b/resources/themes/pterodactyl/server/databases/index.blade.php @@ -21,15 +21,10 @@ @section('content')
    -
    +
    @if(count($databases) > 0)
    @@ -55,11 +50,20 @@ {{ $database->host->host }}:{{ $database->host->port }} - @can('reset-db-password', $server) + @if(Gate::allows('reset-db-password', $server) || Gate::allows('delete-database', $server)) - + @can('delete-database', $server) + + @endcan + @can('reset-db-password', $server) + + @endcan - @endcan + @endif @endforeach @@ -69,17 +73,49 @@
    @lang('server.config.database.no_dbs') - @if(Auth::user()->root_admin === 1) - @lang('server.config.database.add_db') - @endif
    @endif
    + @if($allowCreation && Gate::allows('create-database', $server)) +
    +
    +
    +

    Create New Database

    +
    + @if($overLimit) +
    +
    + You are currently using {{ count($databases) }} of your {{ $server->database_limit ?? '∞' }} allowed databases. +
    +
    + @else +
    +
    +
    + +
    + s{{ $server->id }}_ + +
    +
    +
    + + +

    This should reflect the IP address that connections are allowed from. Uses standard MySQL notation. If unsure leave as %.

    +
    +
    + +
    + @endif +
    +
    + @endif
    @endsection @@ -126,5 +162,37 @@ }); }); @endcan + @can('delete-database', $server) + $('[data-action="delete-database"]').click(function (event) { + event.preventDefault(); + var self = $(this); + swal({ + title: '', + type: 'warning', + text: 'Are you sure that you want to delete this database? There is no going back, all data will immediately be removed.', + showCancelButton: true, + confirmButtonText: 'Delete', + confirmButtonColor: '#d9534f', + closeOnConfirm: false, + showLoaderOnConfirm: true, + }, function () { + $.ajax({ + method: 'DELETE', + url: Router.route('server.databases.delete', { server: '{{ $server->uuidShort }}', database: self.data('id') }), + headers: { 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content') }, + }).done(function () { + self.parent().parent().slideUp(); + swal.close(); + }).fail(function (jqXHR) { + console.error(jqXHR); + swal({ + type: 'error', + title: 'Whoops!', + text: (typeof jqXHR.responseJSON.error !== 'undefined') ? jqXHR.responseJSON.error : 'An error occured while processing this request.' + }); + }); + }); + }); + @endcan @endsection diff --git a/routes/api-client.php b/routes/api-client.php new file mode 100644 index 000000000..b1f4c1b1b --- /dev/null +++ b/routes/api-client.php @@ -0,0 +1,28 @@ +name('api.client.index'); + +/* +|-------------------------------------------------------------------------- +| Client Control API +|-------------------------------------------------------------------------- +| +| Endpoint: /api/client/servers/{server} +| +*/ +Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateClientAccess::class]], function () { + Route::get('/', 'Servers\ServerController@index')->name('api.client.servers.view'); + + Route::post('/command', 'Servers\CommandController@index')->name('api.client.servers.command'); + Route::post('/power', 'Servers\PowerController@index')->name('api.client.servers.power'); +}); diff --git a/routes/api-remote.php b/routes/api-remote.php index a06a72feb..5566651d4 100644 --- a/routes/api-remote.php +++ b/routes/api-remote.php @@ -1,12 +1,7 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ + Route::get('/authenticate/{token}', 'ValidateKeyController@index')->name('api.remote.authenticate'); +Route::post('/download-file', 'FileDownloadController@index')->name('api.remote.download_file'); Route::group(['prefix' => '/eggs'], function () { Route::get('/', 'EggRetrievalController@index')->name('api.remote.eggs'); diff --git a/routes/api.php b/routes/api.php deleted file mode 100644 index 96dfe5dde..000000000 --- a/routes/api.php +++ /dev/null @@ -1,27 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ -//Route::get('/', 'CoreController@index')->name('api.user'); -// -///* -//|-------------------------------------------------------------------------- -//| Server Controller Routes -//|-------------------------------------------------------------------------- -//| -//| Endpoint: /api/user/server/{server} -//| -//*/ -//Route::group([ -// 'prefix' => '/server/{server}', -// 'middleware' => 'server', -//], function () { -// Route::get('/', 'ServerController@index')->name('api.user.server'); -// -// Route::post('/power', 'ServerController@power')->name('api.user.server.power'); -// Route::post('/command', 'ServerController@command')->name('api.user.server.command'); -//}); diff --git a/routes/base.php b/routes/base.php index 3947b7551..4955afd42 100644 --- a/routes/base.php +++ b/routes/base.php @@ -30,16 +30,15 @@ Route::group(['prefix' => 'account'], function () { | | Endpoint: /account/api | -| Temporarily Disabled */ -//Route::group(['prefix' => 'account/api'], function () { -// Route::get('/', 'AccountKeyController@index')->name('account.api'); -// Route::get('/new', 'AccountKeyController@create')->name('account.api.new'); -// -// Route::post('/new', 'AccountKeyController@store'); -// -// Route::delete('/revoke/{identifier}', 'AccountKeyController@revoke')->name('account.api.revoke'); -//}); +Route::group(['prefix' => 'account/api'], function () { + Route::get('/', 'ClientApiController@index')->name('account.api'); + Route::get('/new', 'ClientApiController@create')->name('account.api.new'); + + Route::post('/new', 'ClientApiController@store'); + + Route::delete('/revoke/{identifier}', 'ClientApiController@delete')->name('account.api.revoke'); +}); /* |-------------------------------------------------------------------------- diff --git a/routes/daemon.php b/routes/daemon.php index b74a005a7..2c8058e36 100644 --- a/routes/daemon.php +++ b/routes/daemon.php @@ -10,5 +10,4 @@ Route::get('/packs/pull/{uuid}', 'PackController@pull')->name('daemon.pack.pull' Route::get('/packs/pull/{uuid}/hash', 'PackController@hash')->name('daemon.pack.hash'); Route::get('/configure/{token}', 'ActionController@configuration')->name('daemon.configuration'); -Route::post('/download', 'ActionController@authenticateDownload')->name('daemon.download'); Route::post('/install', 'ActionController@markInstall')->name('daemon.install'); diff --git a/routes/server.php b/routes/server.php index 85283df9e..a05f4b00b 100644 --- a/routes/server.php +++ b/routes/server.php @@ -38,7 +38,11 @@ Route::group(['prefix' => 'settings'], function () { Route::group(['prefix' => 'databases'], function () { Route::get('/', 'DatabaseController@index')->name('server.databases.index'); + Route::post('/new', 'DatabaseController@store')->name('server.databases.new'); + Route::patch('/password', 'DatabaseController@update')->middleware('server..database')->name('server.databases.password'); + + Route::delete('/delete/{database}', 'DatabaseController@delete')->middleware('server..database')->name('server.databases.delete'); }); /* diff --git a/tests/TestCase.php b/tests/TestCase.php index 427744f71..4d4ce896e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,6 +3,7 @@ namespace Tests; use Cake\Chronos\Chronos; +use Illuminate\Support\Facades\Hash; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase @@ -16,6 +17,7 @@ abstract class TestCase extends BaseTestCase { parent::setUp(); + Hash::setRounds(4); $this->setKnownUuidFactory(); } diff --git a/tests/Unit/Commands/Server/BulkPowerActionCommandTest.php b/tests/Unit/Commands/Server/BulkPowerActionCommandTest.php new file mode 100644 index 000000000..684baf931 --- /dev/null +++ b/tests/Unit/Commands/Server/BulkPowerActionCommandTest.php @@ -0,0 +1,164 @@ +powerRepository = m::mock(PowerRepositoryInterface::class); + $this->repository = m::mock(ServerRepositoryInterface::class); + } + + /** + * Test that an action can be sent to all servers. + */ + public function testSendAction() + { + $servers = factory(Server::class)->times(2)->make(); + + $this->repository->shouldReceive('getServersForPowerActionCount') + ->once() + ->with([], []) + ->andReturn(2); + + $this->repository->shouldReceive('getServersForPowerAction') + ->once() + ->with([], []) + ->andReturn($servers); + + for ($i = 0; $i < count($servers); $i++) { + $this->powerRepository->shouldReceive('setServer->sendSignal') + ->once() + ->with('kill') + ->andReturnNull(); + } + + $display = $this->runCommand($this->getCommand(), ['action' => 'kill'], ['yes']); + + $this->assertNotEmpty($display); + $this->assertContains('2/2', $display); + $this->assertContains(trans('command/messages.server.power.confirm', ['action' => 'kill', 'count' => 2]), $display); + } + + /** + * Test filtering servers and nodes. + */ + public function testSendWithFilters() + { + $server = factory(Server::class)->make(); + + $this->repository->shouldReceive('getServersForPowerActionCount') + ->once() + ->with([1, 2], [3, 4]) + ->andReturn(1); + + $this->repository->shouldReceive('getServersForPowerAction') + ->once() + ->with([1, 2], [3, 4]) + ->andReturn([$server]); + + $this->powerRepository->shouldReceive('setServer->sendSignal') + ->once() + ->with('kill') + ->andReturnNull(); + + $display = $this->runCommand($this->getCommand(), [ + 'action' => 'kill', + '--servers' => '1,2', + '--nodes' => '3,4', + ], ['yes']); + + $this->assertNotEmpty($display); + $this->assertContains('1/1', $display); + $this->assertContains(trans('command/messages.server.power.confirm', ['action' => 'kill', 'count' => 1]), $display); + } + + /** + * Test that sending empty options returns the expected results. + */ + public function testSendWithEmptyOptions() + { + $server = factory(Server::class)->make(); + + $this->repository->shouldReceive('getServersForPowerActionCount') + ->once() + ->with([], []) + ->andReturn(1); + + $this->repository->shouldReceive('getServersForPowerAction')->once()->with([], [])->andReturn([$server]); + $this->powerRepository->shouldReceive('setServer->sendSignal')->once()->with('kill')->andReturnNull(); + + $display = $this->runCommand($this->getCommand(), [ + 'action' => 'kill', + '--servers' => '', + '--nodes' => '', + ], ['yes']); + + $this->assertNotEmpty($display); + $this->assertContains('1/1', $display); + $this->assertContains(trans('command/messages.server.power.confirm', ['action' => 'kill', 'count' => 1]), $display); + } + + /** + * Test that validation occurrs correctly. + * + * @param array $data + * + * @dataProvider validationFailureDataProvider + * @expectedException \Illuminate\Validation\ValidationException + */ + public function testValidationErrors(array $data) + { + $this->runCommand($this->getCommand(), $data); + } + + /** + * Provide invalid data for the command. + * + * @return array + */ + public function validationFailureDataProvider(): array + { + return [ + [['action' => 'hodor']], + [['action' => 'hodor', '--servers' => 'hodor']], + [['action' => 'kill', '--servers' => 'hodor']], + [['action' => 'kill', '--servers' => '1,2,3', '--nodes' => 'hodor']], + [['action' => 'kill', '--servers' => '1,2,3', '--nodes' => '1,2,test']], + ]; + } + + /** + * Return an instance of the command with mocked dependencies. + * + * @return \Pterodactyl\Console\Commands\Server\BulkPowerActionCommand + */ + private function getCommand(): BulkPowerActionCommand + { + return new BulkPowerActionCommand($this->powerRepository, $this->repository, $this->app->make(Factory::class)); + } +} diff --git a/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php b/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php index d44481c1b..b76cbfb86 100644 --- a/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php +++ b/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php @@ -1,17 +1,10 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Http\Controllers\Server\Files; use Mockery as m; -use phpmock\phpunit\PHPMock; use Pterodactyl\Models\Node; +use Tests\Traits\MocksUuids; use Pterodactyl\Models\Server; use Illuminate\Cache\Repository; use Tests\Unit\Http\Controllers\ControllerTestCase; @@ -19,7 +12,7 @@ use Pterodactyl\Http\Controllers\Server\Files\DownloadController; class DownloadControllerTest extends ControllerTestCase { - use PHPMock; + use MocksUuids; /** * @var \Illuminate\Cache\Repository|\Mockery\Mock @@ -48,16 +41,20 @@ class DownloadControllerTest extends ControllerTestCase $this->setRequestAttribute('server', $server); $controller->shouldReceive('authorize')->with('download-files', $server)->once()->andReturnNull(); - $this->getFunctionMock('\\Pterodactyl\\Http\\Controllers\\Server\\Files', 'str_random') - ->expects($this->once())->willReturn('randomString'); - $this->cache->shouldReceive('tags')->with(['Server:Downloads'])->once()->andReturnSelf(); - $this->cache->shouldReceive('put')->with('randomString', ['server' => $server->uuid, 'path' => '/my/file.txt'], 5)->once()->andReturnNull(); + $this->cache->shouldReceive('put') + ->once() + ->with('Server:Downloads:' . $this->getKnownUuid(), ['server' => $server->uuid, 'path' => '/my/file.txt'], 5) + ->andReturnNull(); $response = $controller->index($this->request, $server->uuidShort, '/my/file.txt'); $this->assertIsRedirectResponse($response); $this->assertRedirectUrlEquals(sprintf( - '%s://%s:%s/v1/server/file/download/%s', $server->node->scheme, $server->node->fqdn, $server->node->daemonListen, 'randomString' + '%s://%s:%s/v1/server/file/download/%s', + $server->node->scheme, + $server->node->fqdn, + $server->node->daemonListen, + $this->getKnownUuid() ), $response); } diff --git a/tests/Unit/Http/Middleware/Api/Application/AuthenticateIPAccessTest.php b/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php similarity index 85% rename from tests/Unit/Http/Middleware/Api/Application/AuthenticateIPAccessTest.php rename to tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php index cf23d0292..babd95358 100644 --- a/tests/Unit/Http/Middleware/Api/Application/AuthenticateIPAccessTest.php +++ b/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php @@ -1,10 +1,10 @@ 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'); @@ -38,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'); @@ -54,10 +54,10 @@ 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'); + $this->request->shouldReceive('ip')->withNoArgs()->twice()->andReturn('127.0.0.2'); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); } @@ -65,7 +65,7 @@ class AuthenticateIPAccessTest extends MiddlewareTestCase /** * Return an instance of the middleware to be used when testing. * - * @return \Pterodactyl\Http\Middleware\Api\Application\AuthenticateIPAccess + * @return \Pterodactyl\Http\Middleware\Api\AuthenticateIPAccess */ private function getMiddleware(): AuthenticateIPAccess { diff --git a/tests/Unit/Http/Middleware/Api/Application/AuthenticateKeyTest.php b/tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php similarity index 74% rename from tests/Unit/Http/Middleware/Api/Application/AuthenticateKeyTest.php rename to tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php index 486222267..354838b3f 100644 --- a/tests/Unit/Http/Middleware/Api/Application/AuthenticateKeyTest.php +++ b/tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php @@ -1,6 +1,6 @@ request->shouldReceive('bearerToken')->withNoArgs()->once()->andReturnNull(); try { - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_APPLICATION); } catch (HttpException $exception) { $this->assertEquals(401, $exception->getStatusCode()); $this->assertEquals(['WWW-Authenticate' => 'Bearer'], $exception->getHeaders()); @@ -68,7 +68,7 @@ class AuthenticateKeyTest extends MiddlewareTestCase $this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn('abcd1234'); $this->repository->shouldReceive('findFirstWhere')->andThrow(new RecordNotFoundException); - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_APPLICATION); } /** @@ -90,7 +90,30 @@ class AuthenticateKeyTest extends MiddlewareTestCase 'last_used_at' => Chronos::now(), ])->once()->andReturnNull(); - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_APPLICATION); + $this->assertEquals($model, $this->request->attributes->get('api_key')); + } + + /** + * Test that a valid token can continue past the middleware when set as a user token. + */ + public function testValidTokenWithUserKey() + { + $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_ACCOUNT], + ])->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(), ApiKey::TYPE_ACCOUNT); $this->assertEquals($model, $this->request->attributes->get('api_key')); } @@ -111,13 +134,13 @@ class AuthenticateKeyTest extends MiddlewareTestCase ])->once()->andReturn($model); $this->encrypter->shouldReceive('decrypt')->with($model->token)->once()->andReturn('decrypted'); - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), ApiKey::TYPE_APPLICATION); } /** * Return an instance of the middleware with mocked dependencies for testing. * - * @return \Pterodactyl\Http\Middleware\Api\Application\AuthenticateKey + * @return \Pterodactyl\Http\Middleware\Api\AuthenticateKey */ private function getMiddleware(): AuthenticateKey { diff --git a/tests/Unit/Http/Middleware/Api/Application/SetSessionDriverTest.php b/tests/Unit/Http/Middleware/API/SetSessionDriverTest.php similarity index 90% rename from tests/Unit/Http/Middleware/Api/Application/SetSessionDriverTest.php rename to tests/Unit/Http/Middleware/API/SetSessionDriverTest.php index 7804f8209..0f33f6735 100644 --- a/tests/Unit/Http/Middleware/Api/Application/SetSessionDriverTest.php +++ b/tests/Unit/Http/Middleware/API/SetSessionDriverTest.php @@ -1,13 +1,13 @@ databaseHostRepository = m::mock(DatabaseHostRepositoryInterface::class); + $this->managementService = m::mock(DatabaseManagementService::class); + $this->repository = m::mock(DatabaseRepositoryInterface::class); + + // Set configs for testing instances. + config()->set('pterodactyl.client_features.databases.enabled', true); + config()->set('pterodactyl.client_features.databases.allow_random', true); + } + + /** + * Test handling of non-random hosts when a host is found. + * + * @dataProvider databaseLimitDataProvider + */ + public function testNonRandomFoundHost($limit, $count) + { + config()->set('pterodactyl.client_features.databases.allow_random', false); + + $server = factory(Server::class)->make(['database_limit' => $limit]); + $model = factory(Database::class)->make(); + + $this->repository->shouldReceive('findCountWhere') + ->once() + ->with([['server_id', '=', $server->id]]) + ->andReturn($count); + + $this->databaseHostRepository->shouldReceive('setColumns->findWhere') + ->once() + ->with([['node_id', '=', $server->node_id]]) + ->andReturn(collect([$model])); + + $this->managementService->shouldReceive('create') + ->once() + ->with($server->id, [ + 'database_host_id' => $model->id, + 'database' => 'testdb', + 'remote' => null, + ]) + ->andReturn($model); + + $response = $this->getService()->handle($server, ['database' => 'testdb']); + + $this->assertInstanceOf(Database::class, $response); + $this->assertSame($model, $response); + } + + /** + * Test that an exception is thrown if in non-random mode and no host is found. + * + * @expectedException \Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException + */ + public function testNonRandomNoHost() + { + config()->set('pterodactyl.client_features.databases.allow_random', false); + + $server = factory(Server::class)->make(['database_limit' => 1]); + + $this->repository->shouldReceive('findCountWhere') + ->once() + ->with([['server_id', '=', $server->id]]) + ->andReturn(0); + + $this->databaseHostRepository->shouldReceive('setColumns->findWhere') + ->once() + ->with([['node_id', '=', $server->node_id]]) + ->andReturn(collect()); + + $this->getService()->handle($server, []); + } + + /** + * Test handling of random host selection. + */ + public function testRandomFoundHost() + { + $server = factory(Server::class)->make(['database_limit' => 1]); + $model = factory(Database::class)->make(); + + $this->repository->shouldReceive('findCountWhere') + ->once() + ->with([['server_id', '=', $server->id]]) + ->andReturn(0); + + $this->databaseHostRepository->shouldReceive('setColumns->findWhere') + ->once() + ->with([['node_id', '=', $server->node_id]]) + ->andReturn(collect()); + + $this->databaseHostRepository->shouldReceive('setColumns->all') + ->once() + ->andReturn(collect([$model])); + + $this->managementService->shouldReceive('create') + ->once() + ->with($server->id, [ + 'database_host_id' => $model->id, + 'database' => 'testdb', + 'remote' => null, + ]) + ->andReturn($model); + + $response = $this->getService()->handle($server, ['database' => 'testdb']); + + $this->assertInstanceOf(Database::class, $response); + $this->assertSame($model, $response); + } + + /** + * Test that an exception is thrown when no host is found and random is allowed. + * + * @expectedException \Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException + */ + public function testRandomNoHost() + { + $server = factory(Server::class)->make(['database_limit' => 1]); + + $this->repository->shouldReceive('findCountWhere') + ->once() + ->with([['server_id', '=', $server->id]]) + ->andReturn(0); + + $this->databaseHostRepository->shouldReceive('setColumns->findWhere') + ->once() + ->with([['node_id', '=', $server->node_id]]) + ->andReturn(collect()); + + $this->databaseHostRepository->shouldReceive('setColumns->all') + ->once() + ->andReturn(collect()); + + $this->getService()->handle($server, []); + } + + /** + * Test that a server over the database limit throws an exception. + * + * @dataProvider databaseExceedingLimitDataProvider + * @expectedException \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException + */ + public function testServerOverDatabaseLimit($limit, $count) + { + $server = factory(Server::class)->make(['database_limit' => $limit]); + + $this->repository->shouldReceive('findCountWhere') + ->once() + ->with([['server_id', '=', $server->id]]) + ->andReturn($count); + + $this->getService()->handle($server, []); + } + + /** + * Test that an exception is thrown if the feature is not enabled. + * + * @expectedException \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException + */ + public function testFeatureNotEnabled() + { + config()->set('pterodactyl.client_features.databases.enabled', false); + + $this->getService()->handle(factory(Server::class)->make(), []); + } + + /** + * Provide limits and current database counts for testing. + * + * @return array + */ + public function databaseLimitDataProvider(): array + { + return [ + [null, 10], + [1, 0], + ]; + } + + /** + * Provide data for servers over their database limit. + * + * @return array + */ + public function databaseExceedingLimitDataProvider(): array + { + return [ + [2, 2], + [2, 3], + ]; + } + + /** + * Return an instance of the service with mocked dependencies for testing. + * + * @return \Pterodactyl\Services\Databases\DeployServerDatabaseService + */ + private function getService(): DeployServerDatabaseService + { + return new DeployServerDatabaseService($this->repository, $this->databaseHostRepository, $this->managementService); + } +} diff --git a/tests/Unit/Services/Servers/ReinstallServerServiceTest.php b/tests/Unit/Services/Servers/ReinstallServerServiceTest.php index 349aa571a..f00614f77 100644 --- a/tests/Unit/Services/Servers/ReinstallServerServiceTest.php +++ b/tests/Unit/Services/Servers/ReinstallServerServiceTest.php @@ -81,10 +81,9 @@ class ReinstallServerServiceTest extends TestCase $this->repository->shouldNotReceive('find'); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($this->server->id, [ - 'installed' => 0, - ])->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFreshModel->update')->with($this->server->id, [ + 'installed' => 0, + ], true, true)->once()->andReturnNull(); $this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andReturnSelf() ->shouldReceive('reinstall')->withNoArgs()->once()->andReturn(new Response); @@ -101,10 +100,9 @@ class ReinstallServerServiceTest extends TestCase $this->repository->shouldReceive('find')->with($this->server->id)->once()->andReturn($this->server); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($this->server->id, [ - 'installed' => 0, - ])->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFreshModel->update')->with($this->server->id, [ + 'installed' => 0, + ], true, true)->once()->andReturnNull(); $this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andReturnSelf() ->shouldReceive('reinstall')->withNoArgs()->once()->andReturn(new Response); @@ -121,10 +119,9 @@ class ReinstallServerServiceTest extends TestCase public function testExceptionThrownByGuzzleShouldBeReRenderedAsDisplayable() { $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($this->server->id, [ - 'installed' => 0, - ])->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFreshModel->update')->with($this->server->id, [ + 'installed' => 0, + ], true, true)->once()->andReturnNull(); $this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andThrow($this->exception); @@ -139,10 +136,9 @@ class ReinstallServerServiceTest extends TestCase public function testExceptionNotThrownByGuzzleShouldNotBeTransformedToDisplayable() { $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($this->server->id, [ - 'installed' => 0, - ])->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFreshModel->update')->with($this->server->id, [ + 'installed' => 0, + ], true, true)->once()->andReturnNull(); $this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andThrow(new Exception());