diff --git a/.babelrc b/.babelrc deleted file mode 100644 index d8a888ed2..000000000 --- a/.babelrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "presets": ["es2015"], - "compact": true, - "minified": true, - "only": "public/themes/pterodactyl/js/frontend/files/src/*.js", - "sourceMaps": "inline", - "comments": false -} diff --git a/.dev/vagrant/provision.sh b/.dev/vagrant/provision.sh index 38dc27ad8..35a2219f0 100644 --- a/.dev/vagrant/provision.sh +++ b/.dev/vagrant/provision.sh @@ -21,6 +21,12 @@ debconf-set-selections <<< 'mariadb-server-5.5 mysql-server/root_password_again # actually install apt-get install -y php7.2 php7.2-cli php7.2-gd php7.2-mysql php7.2-pdo php7.2-mbstring php7.2-tokenizer php7.2-bcmath php7.2-xml php7.2-fpm php7.2-memcached php7.2-curl php7.2-zip php-xdebug mariadb-server nginx curl tar unzip git memcached > /dev/null +echo "Install nodejs and yarn" +curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - +apt-get -y install nodejs yarn > /dev/null + echo "Install composer" curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer diff --git a/.env.dusk b/.env.dusk new file mode 100644 index 000000000..237f61d32 --- /dev/null +++ b/.env.dusk @@ -0,0 +1,26 @@ +APP_ENV=local +APP_DEBUG=false +APP_KEY=NDWgIKKi9ovNK1PXZpzfNVSBdfCXGb5i +APP_JWT_KEY=test1234 +APP_TIMEZONE=America/Los_Angeles +APP_URL=http://pterodactyl.local + +CACHE_DRIVER=file +SESSION_DRIVER=file + +HASHIDS_SALT=IqRr0g82tCTeuyxGs8RV +HASHIDS_LENGTH=8 + +MAIL_DRIVER=log +MAIL_FROM=support@pterodactyl.io +QUEUE_DRIVER=array + +APP_SERVICE_AUTHOR=testing@pterodactyl.io +MAIL_FROM_NAME="Pterodactyl Panel" +RECAPTCHA_ENABLED=false + +DB_CONNECTION=testing +TESTING_DB_HOST=192.168.1.202 +TESTING_DB_DATABASE=panel_test +TESTING_DB_USERNAME=panel_test +TESTING_DB_PASSWORD=Test1234 diff --git a/.env.example b/.env.example index 0b59de0c9..9062de215 100644 --- a/.env.example +++ b/.env.example @@ -28,4 +28,4 @@ MAIL_FROM=no-reply@example.com QUEUE_HIGH=high QUEUE_STANDARD=standard -QUEUE_LOW=low \ No newline at end of file +QUEUE_LOW=low diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md index 6d69080b1..640d6def5 100644 --- a/.github/ISSUE_TEMPLATE/---bug-report.md +++ b/.github/ISSUE_TEMPLATE/---bug-report.md @@ -1,9 +1,17 @@ --- name: "\U0001F41B Bug Report" -about: Create a report to help us resolve a bug or error +about: For reporting code or design bugs with the software. DO NOT REPORT APACHE/NGINX/PHP CONFIGURATION ISSUES. --- +DO NOT REPORT ISSUES CONFIGURING: SSL, PHP, APACHE, NGINX, YOUR MACHINE, SSH, SFTP, ETC. ON THIS GITHUB TRACKER. + +For assistance installating this software, as well as debugging issues with dependencies, please use our discord server: https://discord.gg/pterodactyl + +You MUST complete all of the below information when reporting a bug, failure to do so will result in closure of your issue. PLEASE stop spamming our tracker with "bugs" that are not related to this project. + +**STOP: READ FIRST, AND THEN DELETE THE ABOVE LINES** + **Background (please complete the following information):** * Panel or Daemon: * Version of Panel/Daemon: diff --git a/.github/ISSUE_TEMPLATE/--installation-help.md b/.github/ISSUE_TEMPLATE/--installation-help.md index fa10ca975..4d5d27e71 100644 --- a/.github/ISSUE_TEMPLATE/--installation-help.md +++ b/.github/ISSUE_TEMPLATE/--installation-help.md @@ -6,3 +6,9 @@ about: 'Visit our Discord for installation help: https://pterodactyl.io/discord' We use GitHub issues only to discuss about Pterodactyl bugs and new features. For this kind of questions about using Pterodactyl, please visit our Discord for assistance: https://pterodactyl.io/discord + +DO NOT REPORT ISSUES CONFIGURING: SSL, PHP, APACHE, NGINX, YOUR MACHINE, SSH, SFTP, ETC. ON THIS GITHUB TRACKER. + +For assistance installating this software, as well as debugging issues with dependencies, please use our discord server: https://discord.gg/pterodactyl + +PLEASE stop spamming our tracker with "bugs" that are not related to this project. diff --git a/.gitignore b/.gitignore index 9fc4aee54..3352c6ad9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,15 +7,12 @@ storage/framework/* /.idea /nbproject -package-lock.json -composer.lock node_modules - -_ide_helper_models.php +*.log _ide_helper.php - -sami.phar -/.sami +.phpstorm.meta.php +.php_cs.cache +public/assets/manifest.json # For local development with docker # Remove if we ever put the Dockerfile in the repo @@ -32,3 +29,6 @@ coverage.xml # Vagrant *.log +resources/lang/locales.js +resources/assets/pterodactyl/scripts/helpers/ziggy.js +resources/assets/scripts/helpers/ziggy.js diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 000000000..286cc3c65 --- /dev/null +++ b/BUILDING.md @@ -0,0 +1,57 @@ +# Local Development +Pterodactyl is now powered by Vuejs and Tailwindcss and uses webpack at its core to generate compiled assets. Release +versions of Pterodactyl will include pre-compiled, minified, and hashed assets ready-to-go. + +However, if you are interested in running custom themes or making modifications to the Vue files you'll need a build +system in place to generate these compiled assets. To get your environment setup, you'll first need to install at least Nodejs +`8`, and it is _highly_ recommended that you also install [Yarn](https://yarnpkg.com) to manage your `node_modules`. + +### Install Dependencies +```bash +yarn install +``` + +The command above will download all of the dependencies necessary to get Pterodactyl assets building. After that, its as +simple as running the command below to generate assets while you're developing. + +```bash +# build the compiled assets for development +yarn run build + +# build the assets automatically when files are modified +yarn run watch +``` + + +### Hot Module Reloading +For more advanced users, we also support 'Hot Module Reloading', allowing you to quickly see changes you're making +to the Vue template files without having to reload the page you're on. To Get started with this, you just need +to run the command below. + +```bash +PUBLIC_PATH=http://192.168.1.1:8080 yarn run serve --host 192.168.1.1 +``` + +There are two _very important_ parts of this command to take note of and change for your specific environment. The first +is the `--host` flag, which is required and should point to the machine where the `webpack-serve` server will be running. +The second is the `PUBLIC_PATH` environment variable which is the URL pointing to the HMR server and is appended to all of +the asset URLs used in Pterodactyl. + +#### Vagrant +If you want to use HMR with our Vagrant image, you can use `yarn run v:serve` as a shortcut for the correct parameters. +In order to have proper file change detection you can use the [`vagrant-notify-forwarder`](https://github.com/mhallin/vagrant-notify-forwarder) to notify file events from the host to the VM. +```sh +vagrant plugin install vagrant-notify-forwarder +vagrant reload +``` + +### Building for Production +Once you have your files squared away and ready for the live server, you'll be needing to generate compiled, minified, and +hashed assets to push live. To do so, run the command below: + +```bash +yarn run build:production +``` + +This will generate a production ready `bundle.js` and `bundle.css` as well as a `manifest.json` and store them in +the `/public/assets` directory where they can then be access by clients, and read by the Panel. diff --git a/CHANGELOG.md b/CHANGELOG.md index 62780484e..8f8179562 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,17 +3,60 @@ 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.14 (Derelict Dermodactylus) +### Fixed +* **[SECURITY]** Fixes an XSS vulnerability when performing certain actions in the file manager. +* **[SECURITY]** Attempting to login as a user who has 2FA enabled will no longer request the 2FA token before validating +that their password is correct. This closes a user existence leak that would expose that an account exists if +it had 2FA enabled. + +### Changed +* Support for setting a node to listen on ports lower than 1024. +* QR code URLs are now generated without the use of an external library to reduce the dependency tree. +* Regenerated database passwords now respect the same settings that were used when initially created. +* Cleaned up 2FA QR code generation to use a more up-to-date library and API. +* Console charts now properly start at 0 and scale based on server configuration. No more crazy spikes that +are due to a change of one unit. + +## v0.7.13 (Derelict Dermodactylus) +### Fixed +* Fixes a bug with the location update API endpoint throwing an error due to an unexected response value. +* Fixes bug where node creation API endpoint was not correctly requiring the `disk_overallocate` key. +* Prevents an exception from being thrown when a database with the same name is created on two different hosts. +* Fixes the redis password not saving correctly when setting up the environment from the command line. +* Fixes a bug with transaction handling in many areas of the application that would cause validation error messages +and other session data to not be persisted properly when using the database as the session driver. +* Fix a bug introduced at some point in the past that causes internal data integrity exceptions to not bubble up to +the user correctly, leading to extraneous and confusing exception messages. +* Fixes a bug causing servers to not be marked as having failed installation in some cases. + +### Changed +* `allocation_limit` for servers now defaults to a null value, and is not required in PATCH/POST requests when adding +a server through the API. +* The `PATCH` endpoint for `/api/applications/servers/{server}/build` now accepts an array called `limits` to match +the response from the server `GET` endpoint. + +### Added +* The server listing for a node is now paginated to 25 servers per page to improve performance on large nodes. + ## v0.7.12 (Derelict Dermodactylus) ### Fixed * Fixes an issue with the locations API endpoint referencing an invalid namespace. * Fixes the `store()` function on the locations API not working due to an incorrect return typehint. * Fixes daemon secrets not being able to be reset on a Node. - +* Fixes an issue where files were not editable due to missing URL encoding in the file manager. +* Fixed checking of language changes +* Fixed Spigot egg not building versions other than `latest`. +* Fixed the Forge egg install script. +* Fixes a bug that would ignore the `skip_scripts` setting when creating or editing a server. + ### Updated * Upgraded core to use Laravel `5.7.14`. +* Updated Simplified Chinese translation pack. ### Added * Added support for opening and editing Python files through the web editor. +* Adds Russian translation. ## v0.7.11 (Derelict Dermodactylus) ### Fixed @@ -211,7 +254,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * Nest and Egg listings now show the associated ID in order to make API requests easier. * Added star indicators to user listing in Admin CP to indicate users who are set as a root admin. * Creating a new node will now requires a SSL connection if the Panel is configured to use SSL as well. -* Socketio error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure. +* Connector error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure. * File manager now supports mass deletion option for files and folders. * Support for CS:GO as a default service option selection. * Support for GMOD as a default service option selection. @@ -341,7 +384,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * Changed 2FA login process to be more secure. Previously authentication checking happened on the 2FA post page, now it happens prior and is passed along to the 2FA page to avoid storing any credentials. ### Added -* Socketio error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure. +* Connector error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure. ## v0.7.0-beta.1 (Derelict Dermodactylus) ### Added diff --git a/README.md b/README.md index 5831a8709..8ff13e8c8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Logo Image](https://cdn.pterodactyl.io/logos/Banner%20Logo%20Black@2x.png)](https://pterodactyl.io) +[![Logo Image](https://cdn.pterodactyl.io/logos/new/pterodactyl_logo.png)](https://pterodactyl.io) [![Build status](https://img.shields.io/travis/pterodactyl/panel/develop.svg?style=flat-square)](https://travis-ci.org/pterodactyl/panel) [![StyleCI](https://styleci.io/repos/47508644/shield?branch=develop)](https://styleci.io/repos/47508644) diff --git a/app/Console/Commands/Environment/AppSettingsCommand.php b/app/Console/Commands/Environment/AppSettingsCommand.php index 6b9bce37d..9a10bfd0d 100644 --- a/app/Console/Commands/Environment/AppSettingsCommand.php +++ b/app/Console/Commands/Environment/AppSettingsCommand.php @@ -178,9 +178,7 @@ class AppSettingsCommand extends Command if ($askForRedisPassword) { $this->output->comment(trans('command/messages.environment.app.redis_pass_help')); $this->variables['REDIS_PASSWORD'] = $this->option('redis-pass') ?? $this->output->askHidden( - trans('command/messages.environment.app.redis_password'), function () { - return ''; - } + trans('command/messages.environment.app.redis_password') ); } diff --git a/app/Contracts/Http/ClientPermissionsRequest.php b/app/Contracts/Http/ClientPermissionsRequest.php new file mode 100644 index 000000000..6b863fce9 --- /dev/null +++ b/app/Contracts/Http/ClientPermissionsRequest.php @@ -0,0 +1,15 @@ +make(Connection::class); + + // If we are currently wrapped up inside a transaction, we will roll all the way + // back to the beginning. This needs to happen, otherwise session data does not + // get properly persisted. + // + // This is kind of a hack, and ideally things like this should be handled as + // much as possible at the code level, but there are a lot of spots that do a + // ton of actions and were written before this bug discovery was made. + // + // @see https://github.com/pterodactyl/panel/pull/1468 + if ($connections->transactionLevel()) { + $connections->rollBack(0); + } + + // Because of some breaking change snuck into a Laravel update that didn't get caught + // by any of the tests, exceptions implementing the HttpExceptionInterface get marked + // as being HttpExceptions, but aren't actually implementing the HttpException abstract. + // + // This is incredibly annoying because we can't just temporarily override the handler to + // allow these (at least without taking on a high maintenance cost). Laravel 5.8 fixes this, + // so when we update (or have updated) this code can be removed. + // + // @see https://github.com/laravel/framework/pull/25975 + // @todo remove this code when upgrading to Laravel 5.8 + if ($exception instanceof HttpExceptionInterface && ! $exception instanceof HttpException) { + $exception = new HttpException( + $exception->getStatusCode(), + $exception->getMessage(), + $exception, + $exception->getHeaders(), + $exception->getCode() + ); + } + return parent::render($request, $exception); } @@ -231,7 +268,7 @@ class Handler extends ExceptionHandler protected function unauthenticated($request, AuthenticationException $exception) { if ($request->expectsJson()) { - return response()->json(['error' => 'Unauthenticated.'], 401); + return response()->json(self::convertToArray($exception), 401); } return redirect()->guest(route('auth.login')); diff --git a/app/Exceptions/Repository/RecordNotFoundException.php b/app/Exceptions/Repository/RecordNotFoundException.php index f449faa42..53cfa2222 100644 --- a/app/Exceptions/Repository/RecordNotFoundException.php +++ b/app/Exceptions/Repository/RecordNotFoundException.php @@ -2,19 +2,28 @@ namespace Pterodactyl\Exceptions\Repository; -class RecordNotFoundException extends RepositoryException +use Illuminate\Http\Response; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; + +class RecordNotFoundException extends RepositoryException implements HttpExceptionInterface { /** - * Handle request to render this exception to a user. Returns the default - * 404 page view. + * Returns the status code. * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response + * @return int */ - public function render($request) + public function getStatusCode() { - if (! config('app.debug')) { - return response()->view('errors.404', [], 404); - } + return Response::HTTP_NOT_FOUND; + } + + /** + * Returns response headers. + * + * @return array + */ + public function getHeaders() + { + return []; } } diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index ab1ce5eb7..28266eac5 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -24,6 +24,7 @@ use Pterodactyl\Services\Allocations\AssignmentService; use Pterodactyl\Services\Helpers\SoftwareVersionService; use Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Http\Requests\Admin\Node\AllocationFormRequest; use Pterodactyl\Services\Allocations\AllocationDeletionService; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; @@ -32,6 +33,11 @@ use Pterodactyl\Http\Requests\Admin\Node\AllocationAliasFormRequest; class NodesController extends Controller { + /** + * @var \Pterodactyl\Services\Allocations\AllocationDeletionService + */ + protected $allocationDeletionService; + /** * @var \Prologue\Alerts\AlertsMessageBag */ @@ -72,6 +78,11 @@ class NodesController extends Controller */ protected $repository; + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + /** * @var \Pterodactyl\Services\Nodes\NodeUpdateService */ @@ -81,10 +92,6 @@ class NodesController extends Controller * @var \Pterodactyl\Services\Helpers\SoftwareVersionService */ protected $versionService; - /** - * @var \Pterodactyl\Services\Allocations\AllocationDeletionService - */ - private $allocationDeletionService; /** * NodesController constructor. @@ -98,6 +105,7 @@ class NodesController extends Controller * @param \Pterodactyl\Services\Nodes\NodeDeletionService $deletionService * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository * @param \Pterodactyl\Services\Nodes\NodeUpdateService $updateService * @param \Pterodactyl\Services\Helpers\SoftwareVersionService $versionService */ @@ -111,6 +119,7 @@ class NodesController extends Controller NodeDeletionService $deletionService, LocationRepositoryInterface $locationRepository, NodeRepositoryInterface $repository, + ServerRepositoryInterface $serverRepository, NodeUpdateService $updateService, SoftwareVersionService $versionService ) { @@ -123,6 +132,7 @@ class NodesController extends Controller $this->deletionService = $deletionService; $this->locationRepository = $locationRepository; $this->repository = $repository; + $this->serverRepository = $serverRepository; $this->updateService = $updateService; $this->versionService = $versionService; } @@ -178,8 +188,6 @@ class NodesController extends Controller * * @param \Pterodactyl\Models\Node $node * @return \Illuminate\View\View - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function viewIndex(Node $node) { @@ -235,19 +243,17 @@ class NodesController extends Controller /** * Shows the server listing page for a specific node. * - * @param int $node + * @param \Pterodactyl\Models\Node $node * @return \Illuminate\View\View - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function viewServers($node) + public function viewServers(Node $node) { - $node = $this->repository->getNodeServers($node); + $servers = $this->serverRepository->loadAllServersForNode($node->id, 25); Javascript::put([ 'node' => collect($node->makeVisible('daemonSecret'))->only(['scheme', 'fqdn', 'daemonListen', 'daemonSecret']), ]); - return view('admin.nodes.view.servers', ['node' => $node]); + return view('admin.nodes.view.servers', ['node' => $node, 'servers' => $servers]); } /** diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 161e8aba6..44eebcccf 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -599,7 +599,7 @@ class ServersController extends Controller ['id', '=', $request->input('database')], ]); - $this->databasePasswordService->handle($database, str_random(20)); + $this->databasePasswordService->handle($database, str_random(24)); return response('', 204); } diff --git a/app/Http/Controllers/Api/Application/Locations/LocationController.php b/app/Http/Controllers/Api/Application/Locations/LocationController.php index b829268d7..9220cf358 100644 --- a/app/Http/Controllers/Api/Application/Locations/LocationController.php +++ b/app/Http/Controllers/Api/Application/Locations/LocationController.php @@ -13,8 +13,8 @@ use Pterodactyl\Transformers\Api\Application\LocationTransformer; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; use Pterodactyl\Http\Requests\Api\Application\Locations\GetLocationRequest; use Pterodactyl\Http\Requests\Api\Application\Locations\GetLocationsRequest; -use Pterodactyl\Http\Requests\Api\Application\Locations\DeleteLocationRequest; use Pterodactyl\Http\Requests\Api\Application\Locations\StoreLocationRequest; +use Pterodactyl\Http\Requests\Api\Application\Locations\DeleteLocationRequest; use Pterodactyl\Http\Requests\Api\Application\Locations\UpdateLocationRequest; class LocationController extends ApplicationApiController diff --git a/app/Http/Controllers/Api/Client/AccountController.php b/app/Http/Controllers/Api/Client/AccountController.php new file mode 100644 index 000000000..965d01264 --- /dev/null +++ b/app/Http/Controllers/Api/Client/AccountController.php @@ -0,0 +1,73 @@ +updateService = $updateService; + } + + /** + * @param Request $request + * @return array + */ + public function index(Request $request): array + { + return $this->fractal->item($request->user()) + ->transformWith($this->getTransformer(AccountTransformer::class)) + ->toArray(); + } + + /** + * Update the authenticated user's email address. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Account\UpdateEmailRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function updateEmail(UpdateEmailRequest $request): Response + { + $this->updateService->handle($request->user(), $request->validated()); + + return response('', Response::HTTP_CREATED); + } + + /** + * Update the authenticated user's password. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Account\UpdatePasswordRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function updatePassword(UpdatePasswordRequest $request): Response + { + $this->updateService->handle($request->user(), $request->validated()); + + return response('', Response::HTTP_CREATED); + } +} diff --git a/app/Http/Controllers/Api/Client/ClientController.php b/app/Http/Controllers/Api/Client/ClientController.php index 2dffba8f0..62b5d2c4a 100644 --- a/app/Http/Controllers/Api/Client/ClientController.php +++ b/app/Http/Controllers/Api/Client/ClientController.php @@ -35,7 +35,9 @@ class ClientController extends ClientApiController */ public function index(GetServersRequest $request): array { - $servers = $this->repository->filterUserAccessServers($request->user(), User::FILTER_LEVEL_SUBUSER, config('pterodactyl.paginate.frontend.servers')); + $servers = $this->repository + ->setSearchTerm($request->input('query')) + ->filterUserAccessServers($request->user(), User::FILTER_LEVEL_ALL); return $this->fractal->collection($servers) ->transformWith($this->getTransformer(ServerTransformer::class)) diff --git a/app/Http/Controllers/Api/Client/Servers/DatabaseController.php b/app/Http/Controllers/Api/Client/Servers/DatabaseController.php new file mode 100644 index 000000000..d440f3216 --- /dev/null +++ b/app/Http/Controllers/Api/Client/Servers/DatabaseController.php @@ -0,0 +1,96 @@ +deployDatabaseService = $deployDatabaseService; + $this->repository = $repository; + $this->managementService = $managementService; + } + + /** + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Databases\GetDatabasesRequest $request + * @return array + */ + public function index(GetDatabasesRequest $request): array + { + $databases = $this->repository->getDatabasesForServer($request->getModel(Server::class)->id); + + return $this->fractal->collection($databases) + ->transformWith($this->getTransformer(DatabaseTransformer::class)) + ->toArray(); + } + + /** + * Create a new database for the given server and return it. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Databases\StoreDatabaseRequest $request + * @return array + * + * @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException + */ + public function store(StoreDatabaseRequest $request): array + { + $database = $this->deployDatabaseService->handle($request->getModel(Server::class), $request->validated()); + + return $this->fractal->item($database) + ->parseIncludes(['password']) + ->transformWith($this->getTransformer(DatabaseTransformer::class)) + ->toArray(); + } + + /** + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Databases\DeleteDatabaseRequest $request + * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function delete(DeleteDatabaseRequest $request): Response + { + $this->managementService->delete($request->getModel(Database::class)->id); + + return Response::create('', Response::HTTP_NO_CONTENT); + } +} diff --git a/app/Http/Controllers/Api/Client/Servers/FileController.php b/app/Http/Controllers/Api/Client/Servers/FileController.php new file mode 100644 index 000000000..eac0cd6dc --- /dev/null +++ b/app/Http/Controllers/Api/Client/Servers/FileController.php @@ -0,0 +1,186 @@ +cache = $cache; + $this->config = $config; + $this->fileRepository = $fileRepository; + } + + /** + * Returns a listing of files in a given directory. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\ListFilesRequest $request + * @return \Illuminate\Http\JsonResponse + */ + public function listDirectory(ListFilesRequest $request): JsonResponse + { + return JsonResponse::create([ + 'contents' => $this->fileRepository->setServer($request->getModel(Server::class))->getDirectory( + $request->get('directory') ?? '/' + ), + 'editable' => $this->config->get('pterodactyl.files.editable', []), + ]); + } + + /** + * Return the contents of a specified file for the user. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\GetFileContentsRequest $request + * @return \Illuminate\Http\Response + */ + public function getFileContents(GetFileContentsRequest $request): Response + { + return Response::create( + $this->fileRepository->setServer($request->getModel(Server::class))->getContent( + $request->get('file'), $this->config->get('pterodactyl.files.max_edit_size') + ) + ); + } + + /** + * Writes the contents of the specified file to the server. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\WriteFileContentRequest $request + * @return \Illuminate\Http\Response + */ + public function writeFileContents(WriteFileContentRequest $request): Response + { + $this->fileRepository->setServer($request->getModel(Server::class))->putContent( + $request->get('file'), + $request->getContent() + ); + + return Response::create('', Response::HTTP_NO_CONTENT); + } + + /** + * Creates a new folder on the server. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\CreateFolderRequest $request + * @return \Illuminate\Http\Response + */ + public function createFolder(CreateFolderRequest $request): Response + { + $this->fileRepository + ->setServer($request->getModel(Server::class)) + ->createDirectory($request->input('name'), $request->input('directory', '/')); + + return Response::create('', Response::HTTP_NO_CONTENT); + } + + /** + * Renames a file on the remote machine. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\RenameFileRequest $request + * @return \Illuminate\Http\Response + */ + public function renameFile(RenameFileRequest $request): Response + { + $this->fileRepository + ->setServer($request->getModel(Server::class)) + ->renameFile($request->input('rename_from'), $request->input('rename_to')); + + return Response::create('', Response::HTTP_NO_CONTENT); + } + + /** + * Copies a file on the server. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\CopyFileRequest $request + * @return \Illuminate\Http\Response + */ + public function copyFile(CopyFileRequest $request): Response + { + $this->fileRepository + ->setServer($request->getModel(Server::class)) + ->copyFile($request->input('location')); + + return Response::create('', Response::HTTP_NO_CONTENT); + } + + /** + * Deletes a file or folder from the server. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\DeleteFileRequest $request + * @return \Illuminate\Http\Response + */ + public function delete(DeleteFileRequest $request): Response + { + $this->fileRepository + ->setServer($request->getModel(Server::class)) + ->deleteFile($request->input('location')); + + return Response::create('', Response::HTTP_NO_CONTENT); + } + + /** + * Configure a reference to a file to download in the cache so that when the + * user hits the Daemon and it verifies with the Panel they'll actually be able + * to download that file. + * + * Returns the token that needs to be used when downloading the file. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\DownloadFileRequest $request + * @return \Illuminate\Http\JsonResponse + * @throws \Exception + */ + public function download(DownloadFileRequest $request): JsonResponse + { + /** @var \Pterodactyl\Models\Server $server */ + $server = $request->getModel(Server::class); + $token = Uuid::uuid4()->toString(); + + $this->cache->put( + 'Server:Downloads:' . $token, ['server' => $server->uuid, 'path' => $request->route()->parameter('file')], Carbon::now()->addMinutes(5) + ); + + return JsonResponse::create(['token' => $token]); + } +} diff --git a/app/Http/Controllers/Api/Client/Servers/NetworkController.php b/app/Http/Controllers/Api/Client/Servers/NetworkController.php new file mode 100644 index 000000000..e7ecd1072 --- /dev/null +++ b/app/Http/Controllers/Api/Client/Servers/NetworkController.php @@ -0,0 +1,49 @@ +repository = $repository; + } + + /** + * Lists all of the allocations available to a server and wether or + * not they are currently assigned as the primary for this server. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Network\GetNetworkRequest $request + * @return array + */ + public function index(GetNetworkRequest $request): array + { + $server = $request->getModel(Server::class); + + $allocations = $this->repository->findWhere([ + ['server_id', '=', $server->id], + ]); + + return $this->fractal->collection($allocations) + ->transformWith($this->getTransformer(AllocationTransformer::class)) + ->toArray(); + } +} diff --git a/app/Http/Controllers/Auth/AbstractLoginController.php b/app/Http/Controllers/Auth/AbstractLoginController.php new file mode 100644 index 000000000..108a104e1 --- /dev/null +++ b/app/Http/Controllers/Auth/AbstractLoginController.php @@ -0,0 +1,132 @@ +lockoutTime = $config->get('auth.lockout.time'); + $this->maxLoginAttempts = $config->get('auth.lockout.attempts'); + + $this->auth = $auth; + $this->config = $config; + } + + /** + * Get the failed login response instance. + * + * @param \Illuminate\Http\Request $request + * @param \Illuminate\Contracts\Auth\Authenticatable|null $user + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + protected function sendFailedLoginResponse(Request $request, Authenticatable $user = null) + { + $this->incrementLoginAttempts($request); + $this->fireFailedLoginEvent($user, [ + $this->getField($request->input('user')) => $request->input('user'), + ]); + + if ($request->route()->named('auth.login-checkpoint')) { + throw new DisplayException(trans('auth.two_factor.checkpoint_failed')); + } + + throw new DisplayException(trans('auth.failed')); + } + + /** + * Send the response after the user was authenticated. + * + * @param \Pterodactyl\Models\User $user + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + protected function sendLoginResponse(User $user, Request $request): JsonResponse + { + $request->session()->regenerate(); + $this->clearLoginAttempts($request); + + $this->auth->guard()->login($user, true); + + return JsonResponse::create([ + 'data' => [ + 'complete' => true, + 'intended' => $this->redirectPath(), + 'user' => $user->toVueObject(), + ], + ]); + } + + /** + * Determine if the user is logging in using an email or username,. + * + * @param string $input + * @return string + */ + protected function getField(string $input = null): string + { + return str_contains($input, '@') ? 'email' : 'username'; + } + + /** + * Fire a failed login event. + * + * @param \Illuminate\Contracts\Auth\Authenticatable|null $user + * @param array $credentials + */ + protected function fireFailedLoginEvent(Authenticatable $user = null, array $credentials = []) + { + event(new Failed('auth', $user, $credentials)); + } +} diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index 896489f97..e772321f9 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -3,7 +3,7 @@ namespace Pterodactyl\Http\Controllers\Auth; use Illuminate\Http\Request; -use Illuminate\Http\RedirectResponse; +use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Password; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Events\Auth\FailedPasswordReset; @@ -18,9 +18,9 @@ class ForgotPasswordController extends Controller * * @param \Illuminate\Http\Request * @param string $response - * @return \Illuminate\Http\RedirectResponse + * @return \Illuminate\Http\JsonResponse */ - protected function sendResetLinkFailedResponse(Request $request, $response): RedirectResponse + protected function sendResetLinkFailedResponse(Request $request, $response): JsonResponse { // As noted in #358 we will return success even if it failed // to avoid pointing out that an account does or does not @@ -29,4 +29,18 @@ class ForgotPasswordController extends Controller return $this->sendResetLinkResponse($request, Password::RESET_LINK_SENT); } + + /** + * Get the response for a successful password reset link. + * + * @param \Illuminate\Http\Request $request + * @param string $response + * @return \Illuminate\Http\JsonResponse + */ + protected function sendResetLinkResponse(Request $request, $response): JsonResponse + { + return response()->json([ + 'status' => trans($response), + ]); + } } diff --git a/app/Http/Controllers/Auth/LoginCheckpointController.php b/app/Http/Controllers/Auth/LoginCheckpointController.php new file mode 100644 index 000000000..8af396ede --- /dev/null +++ b/app/Http/Controllers/Auth/LoginCheckpointController.php @@ -0,0 +1,95 @@ +google2FA = $google2FA; + $this->cache = $cache; + $this->repository = $repository; + $this->encrypter = $encrypter; + } + + /** + * Handle a login where the user is required to provide a TOTP authentication + * token. Once a user has reached this stage it is assumed that they have already + * provided a valid username and password. + * + * @param \Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest $request + * @return \Illuminate\Http\JsonResponse + * + * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException + * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException + * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function __invoke(LoginCheckpointRequest $request): JsonResponse + { + try { + $user = $this->repository->find( + $this->cache->pull($request->input('confirmation_token'), 0) + ); + } catch (RecordNotFoundException $exception) { + return $this->sendFailedLoginResponse($request); + } + + $decrypted = $this->encrypter->decrypt($user->totp_secret); + $window = $this->config->get('pterodactyl.auth.2fa.window'); + + if ($this->google2FA->verifyKey($decrypted, $request->input('authentication_code'), $window)) { + return $this->sendLoginResponse($user, $request); + } + + return $this->sendFailedLoginResponse($request, $user); + } +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index d00b22faa..a3419412c 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -2,117 +2,81 @@ namespace Pterodactyl\Http\Controllers\Auth; +use Illuminate\Support\Str; use Illuminate\Http\Request; use Illuminate\Auth\AuthManager; -use PragmaRX\Google2FA\Google2FA; -use Illuminate\Auth\Events\Failed; -use Illuminate\Http\RedirectResponse; -use Pterodactyl\Http\Controllers\Controller; -use Illuminate\Contracts\Auth\Authenticatable; -use Illuminate\Contracts\Encryption\Encrypter; -use Illuminate\Foundation\Auth\AuthenticatesUsers; +use Illuminate\Http\JsonResponse; +use Illuminate\Contracts\View\View; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Contracts\View\Factory as ViewFactory; use Illuminate\Contracts\Cache\Repository as CacheRepository; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; -use Illuminate\Contracts\Config\Repository as ConfigRepository; -class LoginController extends Controller +class LoginController extends AbstractLoginController { - use AuthenticatesUsers; - - const USER_INPUT_FIELD = 'user'; - /** - * @var \Illuminate\Auth\AuthManager + * @var \Illuminate\Contracts\View\Factory */ - private $auth; + private $view; /** * @var \Illuminate\Contracts\Cache\Repository */ private $cache; - /** - * @var \Illuminate\Contracts\Config\Repository - */ - private $config; - - /** - * @var \Illuminate\Contracts\Encryption\Encrypter - */ - private $encrypter; - /** * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface */ private $repository; - /** - * @var \PragmaRX\Google2FA\Google2FA - */ - private $google2FA; - - /** - * Where to redirect users after login / registration. - * - * @var string - */ - protected $redirectTo = '/'; - - /** - * Lockout time for failed login requests. - * - * @var int - */ - protected $lockoutTime; - - /** - * After how many attempts should logins be throttled and locked. - * - * @var int - */ - protected $maxLoginAttempts; - /** * LoginController constructor. * * @param \Illuminate\Auth\AuthManager $auth - * @param \Illuminate\Contracts\Cache\Repository $cache * @param \Illuminate\Contracts\Config\Repository $config - * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter - * @param \PragmaRX\Google2FA\Google2FA $google2FA + * @param \Illuminate\Contracts\Cache\Repository $cache * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository + * @param \Illuminate\Contracts\View\Factory $view */ public function __construct( AuthManager $auth, + Repository $config, CacheRepository $cache, - ConfigRepository $config, - Encrypter $encrypter, - Google2FA $google2FA, - UserRepositoryInterface $repository + UserRepositoryInterface $repository, + ViewFactory $view ) { - $this->auth = $auth; - $this->cache = $cache; - $this->config = $config; - $this->encrypter = $encrypter; - $this->google2FA = $google2FA; - $this->repository = $repository; + parent::__construct($auth, $config); - $this->lockoutTime = $this->config->get('auth.lockout.time'); - $this->maxLoginAttempts = $this->config->get('auth.lockout.attempts'); + $this->view = $view; + $this->cache = $cache; + $this->repository = $repository; + } + + /** + * Handle all incoming requests for the authentication routes and render the + * base authentication view component. Vuejs will take over at this point and + * turn the login area into a SPA. + * + * @return \Illuminate\Contracts\View\View + */ + public function index(): View + { + return $this->view->make('templates/auth.core'); } /** * Handle a login request to the application. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse * + * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Illuminate\Validation\ValidationException */ - public function login(Request $request) + public function login(Request $request): JsonResponse { - $username = $request->input(self::USER_INPUT_FIELD); + $username = $request->input('user'); $useColumn = $this->getField($username); if ($this->hasTooManyLoginAttempts($request)) { @@ -126,122 +90,28 @@ class LoginController extends Controller return $this->sendFailedLoginResponse($request); } - $validCredentials = password_verify($request->input('password'), $user->password); + // Ensure that the account is using a valid username and password before trying to + // continue. Previously this was handled in the 2FA checkpoint, however that has + // a flaw in which you can discover if an account exists simply by seeing if you + // can proceede to the next step in the login process. + if (! password_verify($request->input('password'), $user->password)) { + return $this->sendFailedLoginResponse($request, $user); + } + if ($user->use_totp) { - $token = str_random(64); - $this->cache->put($token, ['user_id' => $user->id, 'valid_credentials' => $validCredentials], 5); + $token = Str::random(64); + $this->cache->put($token, $user->id, 5); - return redirect()->route('auth.totp')->with('authentication_token', $token); - } - - if ($validCredentials) { - $this->auth->guard()->login($user, true); - - return $this->sendLoginResponse($request); - } - - return $this->sendFailedLoginResponse($request, $user); - } - - /** - * Handle a TOTP implementation page. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View - */ - public function totp(Request $request) - { - $token = $request->session()->get('authentication_token'); - if (is_null($token) || $this->auth->guard()->user()) { - return redirect()->route('auth.login'); - } - - return view('auth.totp', ['verify_key' => $token]); - } - - /** - * Handle a login where the user is required to provide a TOTP authentication - * token. In order to add additional layers of security, users are not - * informed of an incorrect password until this stage, forcing them to - * provide a token on each login attempt. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response - */ - public function loginUsingTotp(Request $request) - { - if (is_null($request->input('verify_token'))) { - return $this->sendFailedLoginResponse($request); - } - - try { - $cache = $this->cache->pull($request->input('verify_token'), []); - $user = $this->repository->find(array_get($cache, 'user_id', 0)); - } catch (RecordNotFoundException $exception) { - return $this->sendFailedLoginResponse($request); - } - - if (is_null($request->input('2fa_token')) || ! array_get($cache, 'valid_credentials')) { - return $this->sendFailedLoginResponse($request, $user); - } - - if (! $this->google2FA->verifyKey( - $this->encrypter->decrypt($user->totp_secret), - $request->input('2fa_token'), - $this->config->get('pterodactyl.auth.2fa.window') - )) { - return $this->sendFailedLoginResponse($request, $user); + return JsonResponse::create([ + 'data' => [ + 'complete' => false, + 'confirmation_token' => $token, + ], + ]); } $this->auth->guard()->login($user, true); - return $this->sendLoginResponse($request); - } - - /** - * Get the failed login response instance. - * - * @param \Illuminate\Http\Request $request - * @param \Illuminate\Contracts\Auth\Authenticatable|null $user - * @return \Illuminate\Http\RedirectResponse - */ - protected function sendFailedLoginResponse(Request $request, Authenticatable $user = null): RedirectResponse - { - $this->incrementLoginAttempts($request); - $this->fireFailedLoginEvent($user, [ - $this->getField($request->input(self::USER_INPUT_FIELD)) => $request->input(self::USER_INPUT_FIELD), - ]); - - $errors = [self::USER_INPUT_FIELD => trans('auth.failed')]; - - if ($request->expectsJson()) { - return response()->json($errors, 422); - } - - return redirect()->route('auth.login') - ->withInput($request->only(self::USER_INPUT_FIELD)) - ->withErrors($errors); - } - - /** - * Determine if the user is logging in using an email or username,. - * - * @param string $input - * @return string - */ - private function getField(string $input = null): string - { - return str_contains($input, '@') ? 'email' : 'username'; - } - - /** - * Fire a failed login event. - * - * @param \Illuminate\Contracts\Auth\Authenticatable|null $user - * @param array $credentials - */ - private function fireFailedLoginEvent(Authenticatable $user = null, array $credentials = []) - { - event(new Failed(config('auth.defaults.guard'), $user, $credentials)); + return $this->sendLoginResponse($user, $request); } } diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php index f13511d91..22aee3eb5 100644 --- a/app/Http/Controllers/Auth/ResetPasswordController.php +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -3,13 +3,15 @@ namespace Pterodactyl\Http\Controllers\Auth; use Illuminate\Support\Str; -use Illuminate\Http\Request; -use Prologue\Alerts\AlertsMessageBag; +use Illuminate\Http\JsonResponse; use Illuminate\Contracts\Hashing\Hasher; +use Illuminate\Support\Facades\Password; use Illuminate\Auth\Events\PasswordReset; use Illuminate\Contracts\Events\Dispatcher; +use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; use Illuminate\Foundation\Auth\ResetsPasswords; +use Pterodactyl\Http\Requests\Auth\ResetPasswordRequest; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; class ResetPasswordController extends Controller @@ -28,11 +30,6 @@ class ResetPasswordController extends Controller */ protected $hasTwoFactor = false; - /** - * @var \Prologue\Alerts\AlertsMessageBag - */ - private $alerts; - /** * @var \Illuminate\Contracts\Events\Dispatcher */ @@ -51,31 +48,44 @@ class ResetPasswordController extends Controller /** * ResetPasswordController constructor. * - * @param \Prologue\Alerts\AlertsMessageBag $alerts * @param \Illuminate\Contracts\Events\Dispatcher $dispatcher * @param \Illuminate\Contracts\Hashing\Hasher $hasher * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository */ - public function __construct(AlertsMessageBag $alerts, Dispatcher $dispatcher, Hasher $hasher, UserRepositoryInterface $userRepository) + public function __construct(Dispatcher $dispatcher, Hasher $hasher, UserRepositoryInterface $userRepository) { - $this->alerts = $alerts; $this->dispatcher = $dispatcher; $this->hasher = $hasher; $this->userRepository = $userRepository; } /** - * Return the rules used when validating password reset. + * Reset the given user's password. * - * @return array + * @param \Pterodactyl\Http\Requests\Auth\ResetPasswordRequest $request + * @return \Illuminate\Http\JsonResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException */ - protected function rules(): array + public function __invoke(ResetPasswordRequest $request): JsonResponse { - return [ - 'token' => 'required', - 'email' => 'required|email', - 'password' => 'required|confirmed|min:8', - ]; + // Here we will attempt to reset the user's password. If it is successful we + // will update the password on an actual user model and persist it to the + // database. Otherwise we will parse the error and return the response. + $response = $this->broker()->reset( + $this->credentials($request), function ($user, $password) { + $this->resetPassword($user, $password); + } + ); + + // If the password was successfully reset, we will redirect the user back to + // the application's home authenticated view. If there is an error we can + // redirect them back to where they came from with their error message. + if ($response === Password::PASSWORD_RESET) { + return $this->sendResetResponse(); + } + + throw new DisplayException(trans($response)); } /** @@ -108,19 +118,16 @@ class ResetPasswordController extends Controller } /** - * Get the response for a successful password reset. + * Send a successful password reset response back to the callee. * - * @param \Illuminate\Http\Request $request - * @param string $response - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse + * @return \Illuminate\Http\JsonResponse */ - protected function sendResetResponse(Request $request, $response) + protected function sendResetResponse(): JsonResponse { - if ($this->hasTwoFactor) { - $this->alerts->success('Your password was successfully updated. Please log in to continue.')->flash(); - } - - return redirect($this->hasTwoFactor ? route('auth.login') : $this->redirectPath()) - ->with('status', trans($response)); + return response()->json([ + 'success' => true, + 'redirect_to' => $this->redirectTo, + 'send_to_login' => $this->hasTwoFactor, + ]); } } diff --git a/app/Http/Controllers/Base/AccountController.php b/app/Http/Controllers/Base/AccountController.php index 80811750e..e69de29bb 100644 --- a/app/Http/Controllers/Base/AccountController.php +++ b/app/Http/Controllers/Base/AccountController.php @@ -1,91 +0,0 @@ -alert = $alert; - $this->updateService = $updateService; - $this->sessionGuard = $authManager->guard(); - } - - /** - * Display base account information page. - * - * @return \Illuminate\View\View - */ - public function index() - { - return view('base.account', [ - 'languages' => $this->getAvailableLanguages(true), - ]); - } - - /** - * Update details for a user's account. - * - * @param \Pterodactyl\Http\Requests\Base\AccountDataFormRequest $request - * @return \Illuminate\Http\RedirectResponse - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function update(AccountDataFormRequest $request) - { - // Prevent logging this specific session out when the password is changed. This will - // automatically update the user's password anyways, so no need to do anything else here. - if ($request->input('do_action') === 'password') { - $this->sessionGuard->logoutOtherDevices($request->input('new_password')); - } else { - if ($request->input('do_action') === 'email') { - $data = ['email' => $request->input('new_email')]; - } elseif ($request->input('do_action') === 'identity') { - $data = $request->only(['name_first', 'name_last', 'username', 'language']); - } else { - $data = []; - } - - $this->updateService->setUserLevel(User::USER_LEVEL_USER); - $this->updateService->handle($request->user(), $data); - } - - $this->alert->success(trans('base.account.details_updated'))->flash(); - - return redirect()->route('account'); - } -} diff --git a/app/Http/Controllers/Base/IndexController.php b/app/Http/Controllers/Base/IndexController.php index 20ef370eb..625c9b23e 100644 --- a/app/Http/Controllers/Base/IndexController.php +++ b/app/Http/Controllers/Base/IndexController.php @@ -53,13 +53,13 @@ class IndexController extends Controller * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function getIndex(Request $request) + public function index(Request $request) { $servers = $this->repository->setSearchTerm($request->input('query'))->filterUserAccessServers( $request->user(), User::FILTER_LEVEL_ALL, config('pterodactyl.paginate.frontend.servers') ); - return view('base.index', ['servers' => $servers]); + return view('templates/base.core', ['servers' => $servers]); } /** diff --git a/app/Http/Controllers/Base/SecurityController.php b/app/Http/Controllers/Base/SecurityController.php index 3bb85a0b5..17327b66c 100644 --- a/app/Http/Controllers/Base/SecurityController.php +++ b/app/Http/Controllers/Base/SecurityController.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Http\Controllers\Base; use Illuminate\Http\Request; +use Illuminate\Http\JsonResponse; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Users\TwoFactorSetupService; @@ -62,36 +63,28 @@ class SecurityController extends Controller } /** - * Returns Security Management Page. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View - */ - public function index(Request $request) - { - if ($this->config->get('session.driver') === 'database') { - $activeSessions = $this->repository->getUserSessions($request->user()->id); - } - - return view('base.security', [ - 'sessions' => $activeSessions ?? null, - ]); - } - - /** - * Generates TOTP Secret and returns popup data for user to verify - * that they can generate a valid response. + * Return information about the user's two-factor authentication status. If not enabled setup their + * secret and return information to allow the user to proceede with setup. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse - * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function generateTotp(Request $request) + public function index(Request $request): JsonResponse { - return response()->json([ - 'qrImage' => $this->twoFactorSetupService->handle($request->user()), + if ($request->user()->use_totp) { + return JsonResponse::create([ + 'enabled' => true, + ]); + } + + $response = $this->twoFactorSetupService->handle($request->user()); + + return JsonResponse::create([ + 'enabled' => false, + 'qr_image' => $response, + 'secret' => '', ]); } @@ -99,53 +92,43 @@ class SecurityController extends Controller * Verifies that 2FA token received is valid and will work on the account. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function setTotp(Request $request) + public function store(Request $request): JsonResponse { try { $this->toggleTwoFactorService->handle($request->user(), $request->input('token') ?? ''); - - return response('true'); } catch (TwoFactorAuthenticationTokenInvalid $exception) { - return response('false'); + $error = true; } + + return JsonResponse::create([ + 'success' => ! isset($error), + ]); } /** * Disables TOTP on an account. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse + * @return \Illuminate\Http\JsonResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function disableTotp(Request $request) + public function delete(Request $request): JsonResponse { try { $this->toggleTwoFactorService->handle($request->user(), $request->input('token') ?? '', false); } catch (TwoFactorAuthenticationTokenInvalid $exception) { - $this->alert->danger(trans('base.security.2fa_disable_error'))->flash(); + $error = true; } - return redirect()->route('account.security'); - } - - /** - * Revokes a user session. - * - * @param \Illuminate\Http\Request $request - * @param string $id - * @return \Illuminate\Http\RedirectResponse - */ - public function revoke(Request $request, string $id) - { - $this->repository->deleteUserSession($request->user()->id, $id); - - return redirect()->route('account.security'); + return JsonResponse::create([ + 'success' => ! isset($error), + ]); } } diff --git a/app/Http/Controllers/Daemon/ActionController.php b/app/Http/Controllers/Daemon/ActionController.php index ea4c52533..257d8775a 100644 --- a/app/Http/Controllers/Daemon/ActionController.php +++ b/app/Http/Controllers/Daemon/ActionController.php @@ -5,10 +5,14 @@ namespace Pterodactyl\Http\Controllers\Daemon; use Cache; use Illuminate\Http\Request; use Pterodactyl\Models\Node; +use Illuminate\Http\Response; use Pterodactyl\Models\Server; +use Illuminate\Http\JsonResponse; use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Events\Server\Installed as ServerInstalled; use Illuminate\Contracts\Events\Dispatcher as EventDispatcher; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; class ActionController extends Controller { @@ -16,15 +20,21 @@ class ActionController extends Controller * @var \Illuminate\Contracts\Events\Dispatcher */ private $eventDispatcher; + /** + * @var \Pterodactyl\Repositories\Eloquent\ServerRepository + */ + private $repository; /** * ActionController constructor. * - * @param \Illuminate\Contracts\Events\Dispatcher $eventDispatcher + * @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository + * @param \Illuminate\Contracts\Events\Dispatcher $eventDispatcher */ - public function __construct(EventDispatcher $eventDispatcher) + public function __construct(ServerRepository $repository, EventDispatcher $eventDispatcher) { $this->eventDispatcher = $eventDispatcher; + $this->repository = $repository; } /** @@ -32,34 +42,47 @@ class ActionController extends Controller * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function markInstall(Request $request) + public function markInstall(Request $request): JsonResponse { - $server = Server::where('uuid', $request->input('server'))->with('node')->first(); - if (! $server) { - return response()->json([ + try { + /** @var \Pterodactyl\Models\Server $server */ + $server = $this->repository->findFirstWhere([ + 'uuid' => $request->input('server'), + ]); + } catch (RecordNotFoundException $exception) { + return JsonResponse::create([ 'error' => 'No server by that ID was found on the system.', - ], 422); + ], Response::HTTP_UNPROCESSABLE_ENTITY); + } + + if (! $server->relationLoaded('node')) { + $server->load('node'); } $hmac = $request->input('signed'); $status = $request->input('installed'); - if (! hash_equals(base64_decode($hmac), hash_hmac('sha256', $server->uuid, $server->node->daemonSecret, true))) { - return response()->json([ + if (! hash_equals(base64_decode($hmac), hash_hmac('sha256', $server->uuid, $server->getRelation('node')->daemonSecret, true))) { + return JsonResponse::create([ 'error' => 'Signed HMAC was invalid.', - ], 403); + ], Response::HTTP_FORBIDDEN); } - $server->installed = ($status === 'installed') ? 1 : 2; - $server->save(); + $this->repository->update($server->id, [ + 'installed' => ($status === 'installed') ? 1 : 2, + ], true, true); // Only fire event if server installed successfully. - if ($server->installed === 1) { + if ($status === 'installed') { $this->eventDispatcher->dispatch(new ServerInstalled($server)); } - return response()->json([]); + // Don't use a 204 here, the daemon is hard-checking for a 200 code. + return JsonResponse::create([]); } /** diff --git a/app/Http/Controllers/Server/ConsoleController.php b/app/Http/Controllers/Server/ConsoleController.php deleted file mode 100644 index 0c1959251..000000000 --- a/app/Http/Controllers/Server/ConsoleController.php +++ /dev/null @@ -1,72 +0,0 @@ -config = $config; - } - - /** - * Render server index page with the console and power options. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View - */ - public function index(Request $request): View - { - $server = $request->attributes->get('server'); - - $this->setRequest($request)->injectJavascript([ - 'server' => [ - 'cpu' => $server->cpu, - ], - 'meta' => [ - 'saveFile' => route('server.files.save', $server->uuidShort), - 'csrfToken' => csrf_token(), - ], - 'config' => [ - 'console_count' => $this->config->get('pterodactyl.console.count'), - 'console_freq' => $this->config->get('pterodactyl.console.frequency'), - ], - ]); - - return view('server.index'); - } - - /** - * Render a stand-alone console in the browser. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View - */ - public function console(Request $request): View - { - $this->setRequest($request)->injectJavascript(['config' => [ - 'console_count' => $this->config->get('pterodactyl.console.count'), - 'console_freq' => $this->config->get('pterodactyl.console.frequency'), - ]]); - - return view('server.console'); - } -} diff --git a/app/Http/Controllers/Server/CredentialsController.php b/app/Http/Controllers/Server/CredentialsController.php new file mode 100644 index 000000000..36e6489ee --- /dev/null +++ b/app/Http/Controllers/Server/CredentialsController.php @@ -0,0 +1,48 @@ +keyProviderService = $keyProviderService; + } + + /** + * Return a set of credentials that the currently authenticated user can use to access + * a given server with. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function index(Request $request): JsonResponse + { + /** @var \Pterodactyl\Models\Server $server */ + $server = $request->attributes->get('server'); + $server->loadMissing('node'); + + return JsonResponse::create([ + 'node' => $server->getRelation('node')->getConnectionAddress(), + 'key' => $this->keyProviderService->handle($server, $request->user()), + ]); + } +} diff --git a/app/Http/Controllers/Server/DatabaseController.php b/app/Http/Controllers/Server/DatabaseController.php deleted file mode 100644 index be7d501ba..000000000 --- a/app/Http/Controllers/Server/DatabaseController.php +++ /dev/null @@ -1,165 +0,0 @@ -alert = $alert; - $this->databaseHostRepository = $databaseHostRepository; - $this->deployServerDatabaseService = $deployServerDatabaseService; - $this->managementService = $managementService; - $this->passwordService = $passwordService; - $this->repository = $repository; - } - - /** - * Render the database listing for a server. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View - * - * @throws \Illuminate\Auth\Access\AuthorizationException - */ - public function index(Request $request): View - { - $server = $request->attributes->get('server'); - $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', [ - '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. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse - * - * @throws \Illuminate\Auth\Access\AuthorizationException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function update(Request $request): JsonResponse - { - $this->authorize('reset-db-password', $request->attributes->get('server')); - - $password = str_random(20); - $this->passwordService->handle($request->attributes->get('database'), $password); - - 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/FileController.php b/app/Http/Controllers/Server/FileController.php new file mode 100644 index 000000000..23bba17ee --- /dev/null +++ b/app/Http/Controllers/Server/FileController.php @@ -0,0 +1,57 @@ +fileRepository = $fileRepository; + } + + /** + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + * + * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + */ + public function index(Request $request): JsonResponse + { + $server = $request->attributes->get('server'); + $this->authorize('list-files', $server); + + $requestDirectory = '/' . trim(urldecode($request->route()->parameter('directory', '/')), '/'); + + try { + $contents = $this->fileRepository->setServer($server)->setToken( + $request->attributes->get('server_token') + )->getDirectory($requestDirectory); + } catch (TransferException $exception) { + throw new DaemonConnectionException($exception, true); + } + + return JsonResponse::create([ + 'contents' => $contents, + 'editable' => config('pterodactyl.files.editable'), + 'current_directory' => $requestDirectory, + ]); + } +} diff --git a/app/Http/Controllers/Server/Files/DownloadController.php b/app/Http/Controllers/Server/Files/DownloadController.php deleted file mode 100644 index 04b16d084..000000000 --- a/app/Http/Controllers/Server/Files/DownloadController.php +++ /dev/null @@ -1,57 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Http\Controllers\Server\Files; - -use Ramsey\Uuid\Uuid; -use Illuminate\Http\Request; -use Illuminate\Cache\Repository; -use Illuminate\Http\RedirectResponse; -use Pterodactyl\Http\Controllers\Controller; - -class DownloadController extends Controller -{ - /** - * @var \Illuminate\Cache\Repository - */ - protected $cache; - - /** - * DownloadController constructor. - * - * @param \Illuminate\Cache\Repository $cache - */ - public function __construct(Repository $cache) - { - $this->cache = $cache; - } - - /** - * Setup a unique download link for a user to download a file from. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @param string $file - * @return \Illuminate\Http\RedirectResponse - * - * @throws \Illuminate\Auth\Access\AuthorizationException - */ - public function index(Request $request, string $uuid, string $file): RedirectResponse - { - $server = $request->attributes->get('server'); - $this->authorize('download-files', $server); - - $token = Uuid::uuid4()->toString(); - $node = $server->getRelation('node'); - - $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/Controllers/Server/Files/FileActionsController.php b/app/Http/Controllers/Server/Files/FileActionsController.php deleted file mode 100644 index bd63009a6..000000000 --- a/app/Http/Controllers/Server/Files/FileActionsController.php +++ /dev/null @@ -1,120 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Http\Controllers\Server\Files; - -use Illuminate\View\View; -use Illuminate\Http\Request; -use GuzzleHttp\Exception\RequestException; -use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Traits\Controllers\JavascriptInjection; -use Pterodactyl\Http\Requests\Server\UpdateFileContentsFormRequest; -use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; -use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; - -class FileActionsController extends Controller -{ - use JavascriptInjection; - - /** - * @var \Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface - */ - protected $repository; - - /** - * FileActionsController constructor. - * - * @param \Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface $repository - */ - public function __construct(FileRepositoryInterface $repository) - { - $this->repository = $repository; - } - - /** - * Display server file index list. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View - * - * @throws \Illuminate\Auth\Access\AuthorizationException - */ - public function index(Request $request): View - { - $server = $request->attributes->get('server'); - $this->authorize('list-files', $server); - - $this->setRequest($request)->injectJavascript([ - 'meta' => [ - 'directoryList' => route('server.files.directory-list', $server->uuidShort), - 'csrftoken' => csrf_token(), - ], - 'permissions' => [ - 'moveFiles' => $request->user()->can('move-files', $server), - 'copyFiles' => $request->user()->can('copy-files', $server), - 'compressFiles' => $request->user()->can('compress-files', $server), - 'decompressFiles' => $request->user()->can('decompress-files', $server), - 'createFiles' => $request->user()->can('create-files', $server), - 'downloadFiles' => $request->user()->can('download-files', $server), - 'deleteFiles' => $request->user()->can('delete-files', $server), - ], - ]); - - return view('server.files.index'); - } - - /** - * Render page to manually create a file in the panel. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View - * - * @throws \Illuminate\Auth\Access\AuthorizationException - */ - public function create(Request $request): View - { - $this->authorize('create-files', $request->attributes->get('server')); - $this->setRequest($request)->injectJavascript(); - - return view('server.files.add', [ - 'directory' => (in_array($request->get('dir'), [null, '/', ''])) ? '' : trim($request->get('dir'), '/') . '/', - ]); - } - - /** - * Display a form to allow for editing of a file. - * - * @param \Pterodactyl\Http\Requests\Server\UpdateFileContentsFormRequest $request - * @param string $uuid - * @param string $file - * @return \Illuminate\View\View - * - * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException - */ - public function view(UpdateFileContentsFormRequest $request, string $uuid, string $file): View - { - $server = $request->attributes->get('server'); - - $dirname = str_replace('\\', '/', pathinfo($file, PATHINFO_DIRNAME)); - try { - $content = $this->repository->setServer($server)->setToken($request->attributes->get('server_token'))->getContent($file); - } catch (RequestException $exception) { - throw new DaemonConnectionException($exception); - } - - $this->setRequest($request)->injectJavascript(['stat' => $request->attributes->get('file_stats')]); - - return view('server.files.edit', [ - 'file' => $file, - 'stat' => $request->attributes->get('file_stats'), - 'contents' => $content, - 'directory' => (in_array($dirname, ['.', './', '/'])) ? '/' : trim($dirname, '/') . '/', - ]); - } -} diff --git a/app/Http/Controllers/Server/Files/RemoteRequestController.php b/app/Http/Controllers/Server/Files/RemoteRequestController.php deleted file mode 100644 index ab58037d0..000000000 --- a/app/Http/Controllers/Server/Files/RemoteRequestController.php +++ /dev/null @@ -1,105 +0,0 @@ -config = $config; - $this->repository = $repository; - } - - /** - * Return a listing of a servers file directory. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View - * - * @throws \Illuminate\Auth\Access\AuthorizationException - * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function directory(Request $request): View - { - $server = $request->attributes->get('server'); - $this->authorize('list-files', $server); - - $requestDirectory = '/' . trim(urldecode($request->input('directory', '/')), '/'); - $directory = [ - 'header' => $requestDirectory !== '/' ? $requestDirectory : '', - 'first' => $requestDirectory !== '/', - ]; - - $goBack = explode('/', trim($requestDirectory, '/')); - if (! empty(array_filter($goBack)) && count($goBack) >= 2) { - array_pop($goBack); - - $directory['show'] = true; - $directory['link'] = '/' . implode('/', $goBack); - $directory['link_show'] = implode('/', $goBack) . '/'; - } - - try { - $listing = $this->repository->setServer($server)->setToken($request->attributes->get('server_token'))->getDirectory($requestDirectory); - } catch (RequestException $exception) { - throw new DaemonConnectionException($exception, true); - } - - return view('server.files.list', [ - 'files' => $listing['files'], - 'folders' => $listing['folders'], - 'editableMime' => $this->config->get('pterodactyl.files.editable'), - 'directory' => $directory, - ]); - } - - /** - * Put the contents of a file onto the daemon. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response - * - * @throws \Illuminate\Auth\Access\AuthorizationException - * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException - */ - public function store(Request $request): Response - { - $server = $request->attributes->get('server'); - $this->authorize('save-files', $server); - - try { - $this->repository->setServer($server)->setToken($request->attributes->get('server_token')) - ->putContent($request->input('file'), $request->input('contents') ?? ''); - - return response('', 204); - } catch (RequestException $exception) { - throw new DaemonConnectionException($exception); - } - } -} diff --git a/app/Http/Controllers/Server/Settings/AllocationController.php b/app/Http/Controllers/Server/Settings/AllocationController.php deleted file mode 100644 index 21baf7c0d..000000000 --- a/app/Http/Controllers/Server/Settings/AllocationController.php +++ /dev/null @@ -1,96 +0,0 @@ -defaultAllocationService = $defaultAllocationService; - $this->hashids = $hashids; - $this->repository = $repository; - } - - /** - * Render the allocation management overview page for a server. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View - * - * @throws \Illuminate\Auth\Access\AuthorizationException - */ - public function index(Request $request): View - { - $server = $request->attributes->get('server'); - $this->authorize('view-allocations', $server); - $this->setRequest($request)->injectJavascript(); - - return view('server.settings.allocation', [ - 'allocations' => $this->repository->findWhere([['server_id', '=', $server->id]]), - ]); - } - - /** - * Update the default allocation for a server. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse - * - * @throws \Illuminate\Auth\Access\AuthorizationException - * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function update(Request $request): JsonResponse - { - $server = $request->attributes->get('server'); - $this->authorize('edit-allocation', $server); - - $allocation = $this->hashids->decodeFirst($request->input('allocation'), 0); - - try { - $this->defaultAllocationService->handle($server->id, $allocation); - } catch (AllocationDoesNotBelongToServerException $exception) { - return response()->json(['error' => 'No matching allocation was located for this server.'], 404); - } - - return response()->json(); - } -} diff --git a/app/Http/Controllers/Server/Settings/NameController.php b/app/Http/Controllers/Server/Settings/NameController.php deleted file mode 100644 index 29cdb9ed9..000000000 --- a/app/Http/Controllers/Server/Settings/NameController.php +++ /dev/null @@ -1,59 +0,0 @@ -repository = $repository; - } - - /** - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View - * @throws \Illuminate\Auth\Access\AuthorizationException - */ - public function index(Request $request) - { - $this->authorize('view-name', $request->attributes->get('server')); - $this->setRequest($request)->injectJavascript(); - - return view('server.settings.name'); - } - - /** - * Update the stored name for a specific server. - * - * @param \Pterodactyl\Http\Requests\Server\Settings\ChangeServerNameRequest $request - * @return \Illuminate\Http\RedirectResponse - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function update(ChangeServerNameRequest $request): RedirectResponse - { - $this->repository->update($request->getServer()->id, $request->validated()); - - return redirect()->route('server.settings.name', $request->getServer()->uuidShort); - } -} diff --git a/app/Http/Controllers/Server/Settings/SftpController.php b/app/Http/Controllers/Server/Settings/SftpController.php deleted file mode 100644 index 58b110bd3..000000000 --- a/app/Http/Controllers/Server/Settings/SftpController.php +++ /dev/null @@ -1,29 +0,0 @@ -authorize('access-sftp', $request->attributes->get('server')); - $this->setRequest($request)->injectJavascript(); - - return view('server.settings.sftp'); - } -} diff --git a/app/Http/Controllers/Server/Settings/StartupController.php b/app/Http/Controllers/Server/Settings/StartupController.php deleted file mode 100644 index 8f17022b5..000000000 --- a/app/Http/Controllers/Server/Settings/StartupController.php +++ /dev/null @@ -1,96 +0,0 @@ -alert = $alert; - $this->commandViewService = $commandViewService; - $this->modificationService = $modificationService; - } - - /** - * Render the server startup page. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View - * - * @throws \Illuminate\Auth\Access\AuthorizationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function index(Request $request): View - { - $server = $request->attributes->get('server'); - $this->authorize('view-startup', $server); - $this->setRequest($request)->injectJavascript(); - - $data = $this->commandViewService->handle($server->id); - - return view('server.settings.startup', [ - 'variables' => $data->get('variables'), - 'server_values' => $data->get('server_values'), - 'startup' => $data->get('startup'), - ]); - } - - /** - * Handle request to update the startup variables for a server. Authorization - * is handled in the form request. - * - * @param \Pterodactyl\Http\Requests\Server\UpdateStartupParametersFormRequest $request - * @return \Illuminate\Http\RedirectResponse - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Illuminate\Validation\ValidationException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function update(UpdateStartupParametersFormRequest $request): RedirectResponse - { - $this->modificationService->setUserLevel(User::USER_LEVEL_USER); - $this->modificationService->handle($request->attributes->get('server'), $request->normalize()); - $this->alert->success(trans('server.config.startup.edited'))->flash(); - - return redirect()->route('server.settings.startup', ['server' => $request->attributes->get('server')->uuidShort]); - } -} diff --git a/app/Http/Controllers/Server/SubuserController.php b/app/Http/Controllers/Server/SubuserController.php deleted file mode 100644 index 008cfeaa5..000000000 --- a/app/Http/Controllers/Server/SubuserController.php +++ /dev/null @@ -1,197 +0,0 @@ -alert = $alert; - $this->repository = $repository; - $this->subuserCreationService = $subuserCreationService; - $this->subuserDeletionService = $subuserDeletionService; - $this->subuserUpdateService = $subuserUpdateService; - } - - /** - * Displays the subuser overview index. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View - * - * @throws \Illuminate\Auth\Access\AuthorizationException - */ - public function index(Request $request): View - { - $server = $request->attributes->get('server'); - $this->authorize('list-subusers', $server); - $this->setRequest($request)->injectJavascript(); - - return view('server.users.index', [ - 'subusers' => $this->repository->findWhere([['server_id', '=', $server->id]]), - ]); - } - - /** - * Displays a single subuser overview. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View - * - * @throws \Illuminate\Auth\Access\AuthorizationException - */ - public function view(Request $request): View - { - $server = $request->attributes->get('server'); - $this->authorize('view-subuser', $server); - - $subuser = $this->repository->getWithPermissions($request->attributes->get('subuser')); - $this->setRequest($request)->injectJavascript(); - - return view('server.users.view', [ - 'subuser' => $subuser, - 'permlist' => Permission::getPermissions(), - 'permissions' => $subuser->getRelation('permissions')->mapWithKeys(function ($item) { - return [$item->permission => true]; - }), - ]); - } - - /** - * Handles editing a subuser. - * - * @param \Pterodactyl\Http\Requests\Server\Subuser\SubuserUpdateFormRequest $request - * @param string $uuid - * @param string $hash - * @return \Illuminate\Http\RedirectResponse - * - * @throws \Illuminate\Auth\Access\AuthorizationException - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function update(SubuserUpdateFormRequest $request, string $uuid, string $hash): RedirectResponse - { - $this->subuserUpdateService->handle($request->attributes->get('subuser'), $request->input('permissions', [])); - $this->alert->success(trans('server.users.user_updated'))->flash(); - - return redirect()->route('server.subusers.view', ['uuid' => $uuid, 'subuser' => $hash]); - } - - /** - * Display new subuser creation page. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View - * @throws \Illuminate\Auth\Access\AuthorizationException - */ - public function create(Request $request): View - { - $server = $request->attributes->get('server'); - $this->authorize('create-subuser', $server); - $this->setRequest($request)->injectJavascript(); - - return view('server.users.new', ['permissions' => Permission::getPermissions()]); - } - - /** - * Handles creating a new subuser. - * - * @param \Pterodactyl\Http\Requests\Server\Subuser\SubuserStoreFormRequest $request - * @return \Illuminate\Http\RedirectResponse - * - * @throws \Exception - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - * @throws \Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException - * @throws \Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException - */ - public function store(SubuserStoreFormRequest $request): RedirectResponse - { - $server = $request->attributes->get('server'); - - $subuser = $this->subuserCreationService->handle($server, $request->input('email'), $request->input('permissions', [])); - $this->alert->success(trans('server.users.user_assigned'))->flash(); - - return redirect()->route('server.subusers.view', [ - 'uuid' => $server->uuidShort, - 'id' => $subuser->hashid, - ]); - } - - /** - * Handles deleting a subuser. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response - * - * @throws \Illuminate\Auth\Access\AuthorizationException - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function delete(Request $request): Response - { - $server = $request->attributes->get('server'); - $this->authorize('delete-subuser', $server); - - $this->subuserDeletionService->handle($request->attributes->get('subuser')); - - return response('', 204); - } -} diff --git a/app/Http/Controllers/Server/Tasks/ActionController.php b/app/Http/Controllers/Server/Tasks/ActionController.php index 498db8537..e69de29bb 100644 --- a/app/Http/Controllers/Server/Tasks/ActionController.php +++ b/app/Http/Controllers/Server/Tasks/ActionController.php @@ -1,79 +0,0 @@ -processScheduleService = $processScheduleService; - $this->repository = $repository; - } - - /** - * Toggle a task to be active or inactive for a given server. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response - * - * @throws \Illuminate\Auth\Access\AuthorizationException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function toggle(Request $request): Response - { - $server = $request->attributes->get('server'); - $schedule = $request->attributes->get('schedule'); - $this->authorize('toggle-schedule', $server); - - $this->repository->update($schedule->id, [ - 'is_active' => ! $schedule->is_active, - ]); - - return response('', 204); - } - - /** - * Trigger a schedule to run now. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response - * - * @throws \Illuminate\Auth\Access\AuthorizationException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function trigger(Request $request): Response - { - $server = $request->attributes->get('server'); - $this->authorize('toggle-schedule', $server); - - $this->processScheduleService->handle( - $request->attributes->get('schedule') - ); - - return response('', 204); - } -} diff --git a/app/Http/Controllers/Server/Tasks/TaskManagementController.php b/app/Http/Controllers/Server/Tasks/TaskManagementController.php deleted file mode 100644 index 9805b5cff..000000000 --- a/app/Http/Controllers/Server/Tasks/TaskManagementController.php +++ /dev/null @@ -1,198 +0,0 @@ -alert = $alert; - $this->creationService = $creationService; - $this->hashids = $hashids; - $this->repository = $repository; - $this->updateService = $updateService; - } - - /** - * Display the task page listing. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View - * - * @throws \Illuminate\Auth\Access\AuthorizationException - */ - public function index(Request $request): View - { - $server = $request->attributes->get('server'); - $this->authorize('list-schedules', $server); - $this->setRequest($request)->injectJavascript(); - - return view('server.schedules.index', [ - 'schedules' => $this->repository->findServerSchedules($server->id), - 'actions' => [ - 'command' => trans('server.schedule.actions.command'), - 'power' => trans('server.schedule.actions.power'), - ], - ]); - } - - /** - * Display the task creation page. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View - * - * @throws \Illuminate\Auth\Access\AuthorizationException - */ - public function create(Request $request): View - { - $server = $request->attributes->get('server'); - $this->authorize('create-schedule', $server); - $this->setRequest($request)->injectJavascript(); - - return view('server.schedules.new'); - } - - /** - * Handle request to store a new schedule and tasks in the database. - * - * @param \Pterodactyl\Http\Requests\Server\ScheduleCreationFormRequest $request - * @return \Illuminate\Http\RedirectResponse - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Service\Schedule\Task\TaskIntervalTooLongException - */ - public function store(ScheduleCreationFormRequest $request): RedirectResponse - { - $server = $request->attributes->get('server'); - - $schedule = $this->creationService->handle($server, $request->normalize(), $request->getTasks()); - $this->alert->success(trans('server.schedule.schedule_created'))->flash(); - - return redirect()->route('server.schedules.view', [ - 'server' => $server->uuidShort, - 'schedule' => $schedule->hashid, - ]); - } - - /** - * Return a view to modify a schedule. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View - * - * @throws \Illuminate\Auth\Access\AuthorizationException - */ - public function view(Request $request): View - { - $server = $request->attributes->get('server'); - $schedule = $request->attributes->get('schedule'); - $this->authorize('view-schedule', $server); - - $this->setRequest($request)->injectJavascript([ - 'tasks' => $schedule->getRelation('tasks')->map(function ($task) { - /* @var \Pterodactyl\Models\Task $task */ - return collect($task->toArray())->only('action', 'time_offset', 'payload')->all(); - }), - ]); - - return view('server.schedules.view', ['schedule' => $schedule]); - } - - /** - * Update a specific parent task on the system. - * - * @param \Pterodactyl\Http\Requests\Server\ScheduleCreationFormRequest $request - * @return \Illuminate\Http\RedirectResponse - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - * @throws \Pterodactyl\Exceptions\Service\Schedule\Task\TaskIntervalTooLongException - */ - public function update(ScheduleCreationFormRequest $request): RedirectResponse - { - $server = $request->attributes->get('server'); - $schedule = $request->attributes->get('schedule'); - - $this->updateService->handle($schedule, $request->normalize(), $request->getTasks()); - $this->alert->success(trans('server.schedule.schedule_updated'))->flash(); - - return redirect()->route('server.schedules.view', [ - 'server' => $server->uuidShort, - 'schedule' => $schedule->hashid, - ]); - } - - /** - * Delete a parent task from the Panel. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response - * - * @throws \Illuminate\Auth\Access\AuthorizationException - */ - public function delete(Request $request): Response - { - $server = $request->attributes->get('server'); - $schedule = $request->attributes->get('schedule'); - $this->authorize('delete-schedule', $server); - - $this->repository->delete($schedule->id); - - return response('', 204); - } -} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 85c1190ea..e1e7f0082 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -49,6 +49,7 @@ class Kernel extends HttpKernel */ protected $middleware = [ CheckForMaintenanceMode::class, + EncryptCookies::class, ValidatePostSize::class, TrimStrings::class, ConvertEmptyStringsToNull::class, @@ -62,7 +63,6 @@ class Kernel extends HttpKernel */ protected $middlewareGroups = [ 'web' => [ - EncryptCookies::class, AddQueuedCookiesToResponse::class, StartSession::class, AuthenticateSession::class, @@ -73,7 +73,7 @@ class Kernel extends HttpKernel RequireTwoFactorAuthentication::class, ], 'api' => [ - 'throttle:120,1', + 'throttle:240,1', ApiSubstituteBindings::class, SetSessionDriver::class, 'api..key:' . ApiKey::TYPE_APPLICATION, @@ -81,9 +81,11 @@ class Kernel extends HttpKernel AuthenticateIPAccess::class, ], 'client-api' => [ - 'throttle:60,1', - SubstituteClientApiBindings::class, + 'throttle:240,1', + StartSession::class, SetSessionDriver::class, + AuthenticateSession::class, + SubstituteClientApiBindings::class, 'api..key:' . ApiKey::TYPE_ACCOUNT, AuthenticateIPAccess::class, ], diff --git a/app/Http/Middleware/Api/AuthenticateKey.php b/app/Http/Middleware/Api/AuthenticateKey.php index 8f400bb4d..429c26f90 100644 --- a/app/Http/Middleware/Api/AuthenticateKey.php +++ b/app/Http/Middleware/Api/AuthenticateKey.php @@ -5,6 +5,7 @@ namespace Pterodactyl\Http\Middleware\Api; use Closure; use Cake\Chronos\Chronos; use Illuminate\Http\Request; +use Pterodactyl\Models\User; use Pterodactyl\Models\ApiKey; use Illuminate\Auth\AuthManager; use Illuminate\Contracts\Encryption\Encrypter; @@ -58,13 +59,43 @@ class AuthenticateKey */ public function handle(Request $request, Closure $next, int $keyType) { - if (is_null($request->bearerToken())) { + if (is_null($request->bearerToken()) && is_null($request->user())) { throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']); } $raw = $request->bearerToken(); - $identifier = substr($raw, 0, ApiKey::IDENTIFIER_LENGTH); - $token = substr($raw, ApiKey::IDENTIFIER_LENGTH); + + // This is a request coming through using cookies, we have an authenticated user not using + // an API key. Make some fake API key models and continue on through the process. + if (empty($raw) && $request->user() instanceof User) { + $model = (new ApiKey())->forceFill([ + 'user_id' => $request->user()->id, + 'key_type' => ApiKey::TYPE_ACCOUNT, + ]); + } else { + $model = $this->authenticateApiKey($raw, $keyType); + $this->auth->guard()->loginUsingId($model->user_id); + } + + $request->attributes->set('api_key', $model); + + return $next($request); + } + + /** + * Authenticate an API key. + * + * @param string $key + * @param int $keyType + * @return \Pterodactyl\Models\ApiKey + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + protected function authenticateApiKey(string $key, int $keyType): ApiKey + { + $identifier = substr($key, 0, ApiKey::IDENTIFIER_LENGTH); + $token = substr($key, ApiKey::IDENTIFIER_LENGTH); try { $model = $this->repository->findFirstWhere([ @@ -79,10 +110,8 @@ class AuthenticateKey throw new AccessDeniedHttpException; } - $this->auth->guard()->loginUsingId($model->user_id); - $request->attributes->set('api_key', $model); $this->repository->withoutFreshModel()->update($model->id, ['last_used_at' => Chronos::now()]); - return $next($request); + return $model; } } diff --git a/app/Http/Middleware/Api/Client/AuthenticateClientAccess.php b/app/Http/Middleware/Api/Client/AuthenticateClientAccess.php deleted file mode 100644 index e048b5869..000000000 --- a/app/Http/Middleware/Api/Client/AuthenticateClientAccess.php +++ /dev/null @@ -1,60 +0,0 @@ -keyProviderService = $keyProviderService; - } - - /** - * Authenticate that the currently authenticated user has permission - * to access the specified server. This only checks that the user is an - * admin, owner, or a subuser. You'll need to do more specific checks in - * the API calls to determine if they can perform different actions. - * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @return mixed - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - */ - public function handle(Request $request, Closure $next) - { - if (is_null($request->user())) { - throw new AccessDeniedHttpException('A request must be made using an authenticated client.'); - } - - /** @var \Pterodactyl\Models\Server $server */ - $server = $request->route()->parameter('server'); - - try { - $token = $this->keyProviderService->handle($server, $request->user()); - } catch (RecordNotFoundException $exception) { - throw new NotFoundHttpException('The requested server could not be located.'); - } - - $request->attributes->set('server_token', $token); - - return $next($request); - } -} diff --git a/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php b/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php new file mode 100644 index 000000000..4214791ba --- /dev/null +++ b/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php @@ -0,0 +1,57 @@ +repository = $repository; + } + + /** + * Authenticate that this server exists and is not suspended or marked as installing. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle(Request $request, Closure $next) + { + $server = $request->route()->parameter('server'); + + if (! $server instanceof Server) { + throw new NotFoundHttpException(trans('exceptions.api.resource_not_found')); + } + + if ($server->suspended) { + throw new AccessDeniedHttpException('Cannot access a server that is marked as being suspended.'); + } + + if (! $server->isInstalled()) { + throw new ConflictHttpException('Server has not completed the installation process.'); + } + + $request->attributes->set('server', $server); + + return $next($request); + } +} diff --git a/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php index f8a35fdd8..5ad94cc90 100644 --- a/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php +++ b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php @@ -4,9 +4,11 @@ namespace Pterodactyl\Http\Middleware\Api\Client; use Closure; use Illuminate\Container\Container; +use Pterodactyl\Contracts\Extensions\HashidsInterface; use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; class SubstituteClientApiBindings extends ApiSubstituteBindings { @@ -24,8 +26,13 @@ class SubstituteClientApiBindings extends ApiSubstituteBindings // column rather than the default 'id'. $this->router->bind('server', function ($value) use ($request) { try { + $column = 'uuidShort'; + if (preg_match('/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i', $value)) { + $column = 'uuid'; + } + return Container::getInstance()->make(ServerRepositoryInterface::class)->findFirstWhere([ - ['uuidShort', '=', $value], + [$column, '=', $value], ]); } catch (RecordNotFoundException $ex) { $request->attributes->set('is_missing_model', true); @@ -34,6 +41,20 @@ class SubstituteClientApiBindings extends ApiSubstituteBindings } }); + $this->router->bind('database', function ($value) use ($request) { + try { + $id = Container::getInstance()->make(HashidsInterface::class)->decodeFirst($value); + + return Container::getInstance()->make(DatabaseRepositoryInterface::class)->findFirstWhere([ + ['id', '=', $id], + ]); + } catch (RecordNotFoundException $exception) { + $request->attributes->set('is_missing_model', true); + + return null; + } + }); + return parent::handle($request, $next); } } diff --git a/app/Http/Middleware/Api/SetSessionDriver.php b/app/Http/Middleware/Api/SetSessionDriver.php index 3d5c16617..8ce50a8b5 100644 --- a/app/Http/Middleware/Api/SetSessionDriver.php +++ b/app/Http/Middleware/Api/SetSessionDriver.php @@ -4,17 +4,10 @@ namespace Pterodactyl\Http\Middleware\Api; use Closure; use Illuminate\Http\Request; -use Barryvdh\Debugbar\LaravelDebugbar; -use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Config\Repository as ConfigRepository; class SetSessionDriver { - /** - * @var \Illuminate\Contracts\Foundation\Application - */ - private $app; - /** * @var \Illuminate\Contracts\Config\Repository */ @@ -23,12 +16,10 @@ class SetSessionDriver /** * SetSessionDriver constructor. * - * @param \Illuminate\Contracts\Foundation\Application $app - * @param \Illuminate\Contracts\Config\Repository $config + * @param \Illuminate\Contracts\Config\Repository $config */ - public function __construct(Application $app, ConfigRepository $config) + public function __construct(ConfigRepository $config) { - $this->app = $app; $this->config = $config; } @@ -41,10 +32,6 @@ class SetSessionDriver */ public function handle(Request $request, Closure $next) { - if ($this->config->get('app.debug')) { - $this->app->make(LaravelDebugbar::class)->disable(); - } - $this->config->set('session.driver', 'array'); return $next($request); diff --git a/app/Http/Middleware/RequireTwoFactorAuthentication.php b/app/Http/Middleware/RequireTwoFactorAuthentication.php index aee1cf068..486b8e3cc 100644 --- a/app/Http/Middleware/RequireTwoFactorAuthentication.php +++ b/app/Http/Middleware/RequireTwoFactorAuthentication.php @@ -10,6 +10,7 @@ namespace Pterodactyl\Http\Middleware; use Closure; +use Illuminate\Support\Str; use Illuminate\Http\Request; use Prologue\Alerts\AlertsMessageBag; @@ -24,27 +25,12 @@ class RequireTwoFactorAuthentication */ private $alert; - /** - * The names of routes that should be accessible without 2FA enabled. - * - * @var array - */ - protected $except = [ - 'account.security', - 'account.security.revoke', - 'account.security.totp', - 'account.security.totp.set', - 'account.security.totp.disable', - 'auth.totp', - 'auth.logout', - ]; - /** * The route to redirect a user to to enable 2FA. * * @var string */ - protected $redirectRoute = 'account.security'; + protected $redirectRoute = 'account'; /** * RequireTwoFactorAuthentication constructor. @@ -69,7 +55,8 @@ class RequireTwoFactorAuthentication return $next($request); } - if (in_array($request->route()->getName(), $this->except)) { + $current = $request->route()->getName(); + if (in_array($current, ['auth', 'account']) || Str::startsWith($current, ['auth.', 'account.'])) { return $next($request); } diff --git a/app/Http/Middleware/Server/AccessingValidServer.php b/app/Http/Middleware/Server/AccessingValidServer.php index ea29e0b99..894dcaa1e 100644 --- a/app/Http/Middleware/Server/AccessingValidServer.php +++ b/app/Http/Middleware/Server/AccessingValidServer.php @@ -5,7 +5,6 @@ namespace Pterodactyl\Http\Middleware\Server; use Closure; use Illuminate\Http\Request; use Pterodactyl\Models\Server; -use Illuminate\Contracts\Session\Session; use Illuminate\Contracts\Routing\ResponseFactory; use Illuminate\Contracts\Config\Repository as ConfigRepository; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; @@ -29,29 +28,21 @@ class AccessingValidServer */ private $response; - /** - * @var \Illuminate\Contracts\Session\Session - */ - private $session; - /** * AccessingValidServer constructor. * * @param \Illuminate\Contracts\Config\Repository $config * @param \Illuminate\Contracts\Routing\ResponseFactory $response * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository - * @param \Illuminate\Contracts\Session\Session $session */ public function __construct( ConfigRepository $config, ResponseFactory $response, - ServerRepositoryInterface $repository, - Session $session + ServerRepositoryInterface $repository ) { $this->config = $config; $this->repository = $repository; $this->response = $response; - $this->session = $session; } /** @@ -61,7 +52,6 @@ class AccessingValidServer * @param \Closure $next * @return \Illuminate\Http\Response|mixed * - * @throws \Illuminate\Auth\AuthenticationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException @@ -90,10 +80,6 @@ class AccessingValidServer return $this->response->view('errors.installing', [], 409); } - // Store the server in the session. - // @todo remove from session. use request attributes. - $this->session->now('server_data.model', $server); - // Add server to the request attributes. This will replace sessions // as files are updated. $request->attributes->set('server', $server); diff --git a/app/Http/Requests/Admin/Servers/Databases/StoreServerDatabaseRequest.php b/app/Http/Requests/Admin/Servers/Databases/StoreServerDatabaseRequest.php index 7f08ecd08..ba8b76a83 100644 --- a/app/Http/Requests/Admin/Servers/Databases/StoreServerDatabaseRequest.php +++ b/app/Http/Requests/Admin/Servers/Databases/StoreServerDatabaseRequest.php @@ -2,6 +2,8 @@ namespace Pterodactyl\Http\Requests\Admin\Servers\Databases; +use Illuminate\Validation\Rule; +use Illuminate\Database\Query\Builder; use Pterodactyl\Http\Requests\Admin\AdminFormRequest; class StoreServerDatabaseRequest extends AdminFormRequest @@ -14,7 +16,15 @@ class StoreServerDatabaseRequest extends AdminFormRequest public function rules(): array { return [ - 'database' => 'required|string|min:1|max:24', + 'database' => [ + 'required', + 'string', + 'min:1', + 'max:24', + Rule::unique('databases')->where(function (Builder $query) { + $query->where('database_host_id', $this->input('database_host_id') ?? 0); + }), + ], 'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/', 'database_host_id' => 'required|integer|exists:database_hosts,id', ]; diff --git a/app/Http/Requests/Api/Application/ApplicationApiRequest.php b/app/Http/Requests/Api/Application/ApplicationApiRequest.php index ca5f40dd0..2471a8fdc 100644 --- a/app/Http/Requests/Api/Application/ApplicationApiRequest.php +++ b/app/Http/Requests/Api/Application/ApplicationApiRequest.php @@ -126,10 +126,6 @@ abstract class ApplicationApiRequest extends FormRequest * * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException */ - - /** - * @return bool - */ protected function passesAuthorization() { // If we have already validated we do not need to call this function diff --git a/app/Http/Requests/Api/Application/Locations/UpdateLocationRequest.php b/app/Http/Requests/Api/Application/Locations/UpdateLocationRequest.php index b29c1c334..2d83ab08f 100644 --- a/app/Http/Requests/Api/Application/Locations/UpdateLocationRequest.php +++ b/app/Http/Requests/Api/Application/Locations/UpdateLocationRequest.php @@ -3,7 +3,6 @@ namespace Pterodactyl\Http\Requests\Api\Application\Locations; use Pterodactyl\Models\Location; -use Pterodactyl\Http\Controllers\Api\Application\Locations\StoreLocationRequest; class UpdateLocationRequest extends StoreLocationRequest { @@ -31,6 +30,6 @@ class UpdateLocationRequest extends StoreLocationRequest return collect(Location::getUpdateRulesForId($locationId))->only([ 'short', 'long', - ]); + ])->toArray(); } } diff --git a/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php index 0f39fd2f8..37dd32585 100644 --- a/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php @@ -36,7 +36,7 @@ class StoreNodeRequest extends ApplicationApiRequest 'memory', 'memory_overallocate', 'disk', - 'disk_overallocation', + 'disk_overallocate', 'upload_size', 'daemonListen', 'daemonSFTP', diff --git a/app/Http/Requests/Api/Application/Servers/Databases/StoreServerDatabaseRequest.php b/app/Http/Requests/Api/Application/Servers/Databases/StoreServerDatabaseRequest.php index ee4264a9d..c2dbfe14a 100644 --- a/app/Http/Requests/Api/Application/Servers/Databases/StoreServerDatabaseRequest.php +++ b/app/Http/Requests/Api/Application/Servers/Databases/StoreServerDatabaseRequest.php @@ -2,6 +2,8 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers\Databases; +use Illuminate\Validation\Rule; +use Illuminate\Database\Query\Builder; use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; @@ -25,7 +27,15 @@ class StoreServerDatabaseRequest extends ApplicationApiRequest public function rules(): array { return [ - 'database' => 'required|string|min:1|max:24', + 'database' => [ + 'required', + 'string', + 'min:1', + 'max:24', + Rule::unique('databases')->where(function (Builder $query) { + $query->where('database_host_id', $this->input('host') ?? 0); + }), + ], 'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/', 'host' => 'required|integer|exists:database_hosts,id', ]; diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php index d3d4a8b21..07bbc3281 100644 --- a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Http\Requests\Api\Application\Servers; use Pterodactyl\Models\Server; +use Illuminate\Support\Collection; class UpdateServerBuildConfigurationRequest extends ServerWriteRequest { @@ -17,15 +18,29 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest return [ 'allocation' => $rules['allocation_id'], - 'memory' => $rules['memory'], - 'swap' => $rules['swap'], - 'io' => $rules['io'], - 'cpu' => $rules['cpu'], - 'disk' => $rules['disk'], + + 'limits' => 'sometimes|array', + 'limits.memory' => $this->requiredToOptional('memory', $rules['memory'], true), + 'limits.swap' => $this->requiredToOptional('swap', $rules['swap'], true), + 'limits.io' => $this->requiredToOptional('io', $rules['io'], true), + 'limits.cpu' => $this->requiredToOptional('cpu', $rules['cpu'], true), + 'limits.disk' => $this->requiredToOptional('disk', $rules['disk'], true), + + // Legacy rules to maintain backwards compatable API support without requiring + // a major version bump. + // + // @see https://github.com/pterodactyl/panel/issues/1500 + 'memory' => $this->requiredToOptional('memory', $rules['memory']), + 'swap' => $this->requiredToOptional('swap', $rules['swap']), + 'io' => $this->requiredToOptional('io', $rules['io']), + 'cpu' => $this->requiredToOptional('cpu', $rules['cpu']), + 'disk' => $this->requiredToOptional('disk', $rules['disk']), + 'add_allocations' => 'bail|array', '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'], @@ -46,6 +61,15 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest $data['allocation_limit'] = $data['feature_limits']['allocations']; unset($data['allocation'], $data['feature_limits']); + // Adjust the limits field to match what is expected by the model. + if (! empty($data['limits'])) { + foreach ($data['limits'] as $key => $value) { + $data[$key] = $value; + } + + unset($data['limits']); + } + return $data; } @@ -65,4 +89,30 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest 'feature_limits.allocations' => 'Allocation Limit', ]; } + + /** + * Converts existing rules for certain limits into a format that maintains backwards + * compatability with the old API endpoint while also supporting a more correct API + * call. + * + * @param string $field + * @param array $rules + * @param bool $limits + * @return array + * + * @see https://github.com/pterodactyl/panel/issues/1500 + */ + protected function requiredToOptional(string $field, array $rules, bool $limits = false) + { + if (! in_array('required', $rules)) { + return $rules; + } + + return (new Collection($rules)) + ->filter(function ($value) { + return $value !== 'required'; + }) + ->prepend($limits ? 'required_with:limits' : 'required_without:limits') + ->toArray(); + } } diff --git a/app/Http/Requests/Api/Client/Account/UpdateEmailRequest.php b/app/Http/Requests/Api/Client/Account/UpdateEmailRequest.php new file mode 100644 index 000000000..a990e79ba --- /dev/null +++ b/app/Http/Requests/Api/Client/Account/UpdateEmailRequest.php @@ -0,0 +1,39 @@ +input('password'), $this->user()->password)) { + throw new InvalidPasswordProvidedException(trans('validation.internal.invalid_password')); + } + + return true; + } + + /** + * @return array + */ + public function rules(): array + { + $rules = User::getUpdateRulesForId($this->user()->id); + + return ['email' => $rules['email']]; + } +} diff --git a/app/Http/Requests/Api/Client/Account/UpdatePasswordRequest.php b/app/Http/Requests/Api/Client/Account/UpdatePasswordRequest.php new file mode 100644 index 000000000..12079f681 --- /dev/null +++ b/app/Http/Requests/Api/Client/Account/UpdatePasswordRequest.php @@ -0,0 +1,39 @@ +input('current_password'), $this->user()->password)) { + throw new InvalidPasswordProvidedException(trans('validation.internal.invalid_password')); + } + + return true; + } + + /** + * @return array + */ + public function rules(): array + { + $rules = User::getUpdateRulesForId($this->user()->id); + + return ['password' => array_merge($rules['password'], ['confirmed'])]; + } +} diff --git a/app/Http/Requests/Api/Client/ClientApiRequest.php b/app/Http/Requests/Api/Client/ClientApiRequest.php index 92402e513..468b294f7 100644 --- a/app/Http/Requests/Api/Client/ClientApiRequest.php +++ b/app/Http/Requests/Api/Client/ClientApiRequest.php @@ -2,18 +2,23 @@ namespace Pterodactyl\Http\Requests\Api\Client; +use Pterodactyl\Models\Server; +use Pterodactyl\Contracts\Http\ClientPermissionsRequest; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; abstract class ClientApiRequest extends ApplicationApiRequest { /** - * Determine if the current user is authorized to perform - * the requested action against the API. + * Determine if the current user is authorized to perform the requested action against the API. * * @return bool */ public function authorize(): bool { + if ($this instanceof ClientPermissionsRequest || method_exists($this, 'permission')) { + return $this->user()->can($this->permission(), $this->getModel(Server::class)); + } + return true; } } diff --git a/app/Http/Requests/Api/Client/Servers/Databases/DeleteDatabaseRequest.php b/app/Http/Requests/Api/Client/Servers/Databases/DeleteDatabaseRequest.php new file mode 100644 index 000000000..b43dd0d5f --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Databases/DeleteDatabaseRequest.php @@ -0,0 +1,27 @@ +getModel(Server::class)->id === $this->getModel(Database::class)->server_id; + } +} diff --git a/app/Http/Requests/Api/Client/Servers/Databases/GetDatabasesRequest.php b/app/Http/Requests/Api/Client/Servers/Databases/GetDatabasesRequest.php new file mode 100644 index 000000000..0e90d80bb --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Databases/GetDatabasesRequest.php @@ -0,0 +1,17 @@ + 'required|alpha_dash|min:1|max:100', + 'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/', + ]; + } +} diff --git a/app/Http/Requests/Api/Client/Servers/Files/CopyFileRequest.php b/app/Http/Requests/Api/Client/Servers/Files/CopyFileRequest.php new file mode 100644 index 000000000..4382dd018 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Files/CopyFileRequest.php @@ -0,0 +1,27 @@ + 'required|string', + ]; + } +} diff --git a/app/Http/Requests/Api/Client/Servers/Files/CreateFolderRequest.php b/app/Http/Requests/Api/Client/Servers/Files/CreateFolderRequest.php new file mode 100644 index 000000000..71ec0f94d --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Files/CreateFolderRequest.php @@ -0,0 +1,30 @@ +user()->can('create-files', $this->getModel(Server::class)); + } + + /** + * @return array + */ + public function rules(): array + { + return [ + 'root' => 'sometimes|nullable|string', + 'name' => 'required|string', + ]; + } +} diff --git a/app/Http/Requests/Api/Client/Servers/Files/DeleteFileRequest.php b/app/Http/Requests/Api/Client/Servers/Files/DeleteFileRequest.php new file mode 100644 index 000000000..62820b428 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Files/DeleteFileRequest.php @@ -0,0 +1,27 @@ + 'required|string', + ]; + } +} diff --git a/app/Http/Requests/Api/Client/Servers/Files/DownloadFileRequest.php b/app/Http/Requests/Api/Client/Servers/Files/DownloadFileRequest.php new file mode 100644 index 000000000..a67a6efb6 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Files/DownloadFileRequest.php @@ -0,0 +1,20 @@ +user()->can('download-files', $this->getModel(Server::class)); + } +} diff --git a/app/Http/Requests/Api/Client/Servers/Files/GetFileContentsRequest.php b/app/Http/Requests/Api/Client/Servers/Files/GetFileContentsRequest.php new file mode 100644 index 000000000..7c596c8d4 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Files/GetFileContentsRequest.php @@ -0,0 +1,31 @@ + 'required|string', + ]; + } +} diff --git a/app/Http/Requests/Api/Client/Servers/Files/ListFilesRequest.php b/app/Http/Requests/Api/Client/Servers/Files/ListFilesRequest.php new file mode 100644 index 000000000..306447cde --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Files/ListFilesRequest.php @@ -0,0 +1,30 @@ +user()->can('list-files', $this->getModel(Server::class)); + } + + /** + * @return array + */ + public function rules(): array + { + return [ + 'directory' => 'sometimes|nullable|string', + ]; + } +} diff --git a/app/Http/Requests/Api/Client/Servers/Files/RenameFileRequest.php b/app/Http/Requests/Api/Client/Servers/Files/RenameFileRequest.php new file mode 100644 index 000000000..a1c399453 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Files/RenameFileRequest.php @@ -0,0 +1,31 @@ + 'string|required', + 'rename_to' => 'string|required', + ]; + } +} diff --git a/app/Http/Requests/Api/Client/Servers/Files/WriteFileContentRequest.php b/app/Http/Requests/Api/Client/Servers/Files/WriteFileContentRequest.php new file mode 100644 index 000000000..31404938a --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Files/WriteFileContentRequest.php @@ -0,0 +1,35 @@ + 'required|string', + ]; + } +} diff --git a/app/Http/Requests/Api/Client/Servers/Network/GetNetworkRequest.php b/app/Http/Requests/Api/Client/Servers/Network/GetNetworkRequest.php new file mode 100644 index 000000000..14fd18337 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Network/GetNetworkRequest.php @@ -0,0 +1,20 @@ +user()->can('view-allocations', $this->getModel(Server::class)); + } +} diff --git a/app/Http/Requests/Auth/LoginCheckpointRequest.php b/app/Http/Requests/Auth/LoginCheckpointRequest.php new file mode 100644 index 000000000..158f5c465 --- /dev/null +++ b/app/Http/Requests/Auth/LoginCheckpointRequest.php @@ -0,0 +1,31 @@ + 'required|string', + 'authentication_code' => 'required|numeric', + ]; + } +} diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php new file mode 100644 index 000000000..7fed20939 --- /dev/null +++ b/app/Http/Requests/Auth/LoginRequest.php @@ -0,0 +1,27 @@ + 'required|string|min:1', + 'password' => 'required|string', + ]; + } +} diff --git a/app/Http/Requests/Auth/ResetPasswordRequest.php b/app/Http/Requests/Auth/ResetPasswordRequest.php new file mode 100644 index 000000000..e06883c2a --- /dev/null +++ b/app/Http/Requests/Auth/ResetPasswordRequest.php @@ -0,0 +1,28 @@ + 'required|string', + 'email' => 'required|email', + 'password' => 'required|string|confirmed|min:8', + ]; + } +} diff --git a/app/Http/Requests/Base/AccountDataFormRequest.php b/app/Http/Requests/Base/AccountDataFormRequest.php index 0d24b7a16..474d85acb 100644 --- a/app/Http/Requests/Base/AccountDataFormRequest.php +++ b/app/Http/Requests/Base/AccountDataFormRequest.php @@ -28,7 +28,7 @@ class AccountDataFormRequest extends FrontendUserFormRequest // Verify password matches when changing password or email. if (in_array($this->input('do_action'), ['password', 'email'])) { if (! password_verify($this->input('current_password'), $this->user()->password)) { - throw new InvalidPasswordProvidedException(trans('base.account.invalid_password')); + throw new InvalidPasswordProvidedException(trans('validation.internal.invalid_password')); } } @@ -59,6 +59,7 @@ class AccountDataFormRequest extends FrontendUserFormRequest 'name_first' => array_get($modelRules, 'name_first'), 'name_last' => array_get($modelRules, 'name_last'), 'username' => array_get($modelRules, 'username'), + 'language' => array_get($modelRules, 'language'), ]; break; default: diff --git a/app/Http/ViewComposers/AssetComposer.php b/app/Http/ViewComposers/AssetComposer.php new file mode 100644 index 000000000..41e5f7e10 --- /dev/null +++ b/app/Http/ViewComposers/AssetComposer.php @@ -0,0 +1,34 @@ +assetHashService = $assetHashService; + } + + /** + * Provide access to the asset service in the views. + * + * @param \Illuminate\View\View $view + */ + public function compose(View $view) + { + $view->with('asset', $this->assetHashService); + } +} diff --git a/app/Models/Node.php b/app/Models/Node.php index 80c84904f..256431cea 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -110,8 +110,8 @@ class Node extends Model implements CleansAttributes, ValidableContract 'disk' => 'numeric|min:1', 'disk_overallocate' => 'numeric|min:-1', 'daemonBase' => 'regex:/^([\/][\d\w.\-\/]+)$/', - 'daemonSFTP' => 'numeric|between:1024,65535', - 'daemonListen' => 'numeric|between:1024,65535', + 'daemonSFTP' => 'numeric|between:1,65535', + 'daemonListen' => 'numeric|between:1,65535', 'maintenance_mode' => 'boolean', 'upload_size' => 'int|between:1,1024', ]; @@ -132,6 +132,16 @@ class Node extends Model implements CleansAttributes, ValidableContract 'maintenance_mode' => false, ]; + /** + * Get the connection address to use when making calls to this node. + * + * @return string + */ + public function getConnectionAddress(): string + { + return sprintf('%s://%s:%s', $this->scheme, $this->fqdn, $this->daemonListen); + } + /** * Returns the configuration in JSON format. * diff --git a/app/Models/Server.php b/app/Models/Server.php index 5d42cf86b..af80a05b3 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -63,7 +63,7 @@ class Server extends Model implements CleansAttributes, ValidableContract 'image' => 'required', 'startup' => 'required', 'database_limit' => 'present', - 'allocation_limit' => 'present', + 'allocation_limit' => 'sometimes', ]; /** @@ -87,7 +87,7 @@ class Server extends Model implements CleansAttributes, ValidableContract 'startup' => 'string', 'skip_scripts' => 'boolean', 'image' => 'string|max:255', - 'installed' => 'boolean', + 'installed' => 'in:0,1,2', 'database_limit' => 'nullable|integer|min:0', 'allocation_limit' => 'nullable|integer|min:0', ]; @@ -143,6 +143,14 @@ class Server extends Model implements CleansAttributes, ValidableContract return Schema::getColumnListing($this->getTable()); } + /** + * @return bool + */ + public function isInstalled(): bool + { + return $this->installed === 1; + } + /** * Gets the user who owns the server. * diff --git a/app/Models/User.php b/app/Models/User.php index 6732b23af..28f785e01 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,6 +5,7 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Pterodactyl\Rules\Username; +use Illuminate\Support\Collection; use Illuminate\Validation\Rules\In; use Illuminate\Auth\Authenticatable; use Illuminate\Database\Eloquent\Model; @@ -177,6 +178,16 @@ class User extends Model implements return $rules; } + /** + * Return the user model in a format that can be passed over to Vue templates. + * + * @return array + */ + public function toVueObject(): array + { + return (new Collection($this->toArray()))->except(['id', 'external_id'])->toArray(); + } + /** * Send the password reset notification. * diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index aa5fbbaa5..9813baa2e 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -3,7 +3,7 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; -use Pterodactyl\Repositories\Daemon\FileRepository; +use Pterodactyl\Repositories\Wings\FileRepository; use Pterodactyl\Repositories\Daemon\PowerRepository; use Pterodactyl\Repositories\Eloquent\EggRepository; use Pterodactyl\Repositories\Eloquent\NestRepository; diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index f0e978116..2ee187c69 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -22,20 +22,21 @@ class RouteServiceProvider extends ServiceProvider public function map() { Route::middleware(['web', 'auth', 'csrf']) - ->namespace($this->namespace . '\Base') - ->group(base_path('routes/base.php')); + ->namespace($this->namespace . '\Base') + ->group(base_path('routes/base.php')); Route::middleware(['web', 'auth', 'admin', 'csrf'])->prefix('/admin') - ->namespace($this->namespace . '\Admin') - ->group(base_path('routes/admin.php')); + ->namespace($this->namespace . '\Admin') + ->group(base_path('routes/admin.php')); Route::middleware(['web', 'csrf'])->prefix('/auth') - ->namespace($this->namespace . '\Auth') - ->group(base_path('routes/auth.php')); + ->namespace($this->namespace . '\Auth') + ->group(base_path('routes/auth.php')); - Route::middleware(['web', 'csrf', 'auth', 'server', 'subuser.auth', 'node.maintenance'])->prefix('/server/{server}') - ->namespace($this->namespace . '\Server') - ->group(base_path('routes/server.php')); + Route::middleware(['web', 'csrf', 'auth', 'server', 'subuser.auth', 'node.maintenance']) + ->prefix('/api/server/{server}') + ->namespace($this->namespace . '\Server') + ->group(base_path('routes/server.php')); Route::middleware(['api'])->prefix('/api/application') ->namespace($this->namespace . '\Api\Application') @@ -50,7 +51,7 @@ class RouteServiceProvider extends ServiceProvider ->group(base_path('routes/api-remote.php')); Route::middleware(['web', 'daemon-old'])->prefix('/daemon') - ->namespace($this->namespace . '\Daemon') - ->group(base_path('routes/daemon.php')); + ->namespace($this->namespace . '\Daemon') + ->group(base_path('routes/daemon.php')); } } diff --git a/app/Providers/ViewComposerServiceProvider.php b/app/Providers/ViewComposerServiceProvider.php index ab8c9e164..9490234f3 100644 --- a/app/Providers/ViewComposerServiceProvider.php +++ b/app/Providers/ViewComposerServiceProvider.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; +use Pterodactyl\Http\ViewComposers\AssetComposer; use Pterodactyl\Http\ViewComposers\ServerListComposer; use Pterodactyl\Http\ViewComposers\Server\ServerDataComposer; @@ -13,6 +14,8 @@ class ViewComposerServiceProvider extends ServiceProvider */ public function boot() { + $this->app->make('view')->composer('*', AssetComposer::class); + $this->app->make('view')->composer('server.*', ServerDataComposer::class); // Add data to make the sidebar work when viewing a server. diff --git a/app/Repositories/Daemon/FileRepository.php b/app/Repositories/Daemon/FileRepository.php index 46117f3c5..7c01f8dc8 100644 --- a/app/Repositories/Daemon/FileRepository.php +++ b/app/Repositories/Daemon/FileRepository.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Repositories\Daemon; use stdClass; +use RuntimeException; use Psr\Http\Message\ResponseInterface; use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; @@ -84,33 +85,20 @@ class FileRepository extends BaseRepository implements FileRepositoryInterface { $response = $this->getHttpClient()->request('GET', sprintf('server/directory/%s', rawurlencode($path))); - $contents = json_decode($response->getBody()); - $files = $folders = []; + return json_decode($response->getBody()); + } - foreach ($contents as $value) { - if ($value->directory) { - array_push($folders, [ - 'entry' => $value->name, - 'directory' => trim($path, '/'), - 'size' => null, - 'date' => strtotime($value->modified), - 'mime' => $value->mime, - ]); - } elseif ($value->file) { - array_push($files, [ - 'entry' => $value->name, - 'directory' => trim($path, '/'), - 'extension' => str_replace('\\', '/', pathinfo($value->name, PATHINFO_EXTENSION)), - 'size' => human_readable($value->size), - 'date' => strtotime($value->modified), - 'mime' => $value->mime, - ]); - } - } - - return [ - 'files' => $files, - 'folders' => $folders, - ]; + /** + * Creates a new directory for the server in the given $path. + * + * @param string $name + * @param string $path + * @return \Psr\Http\Message\ResponseInterface + * + * @throws \RuntimeException + */ + public function createDirectory(string $name, string $path): ResponseInterface + { + throw new RuntimeException('Not implemented.'); } } diff --git a/app/Repositories/Eloquent/DatabaseRepository.php b/app/Repositories/Eloquent/DatabaseRepository.php index d3b615b92..a159324b9 100644 --- a/app/Repositories/Eloquent/DatabaseRepository.php +++ b/app/Repositories/Eloquent/DatabaseRepository.php @@ -76,7 +76,7 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor */ public function getDatabasesForServer(int $server): Collection { - return $this->getBuilder()->where('server_id', $server)->get($this->getColumns()); + return $this->getBuilder()->with('host')->where('server_id', $server)->get($this->getColumns()); } /** diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index 4f59fddce..3b86f2ee1 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -6,10 +6,8 @@ use Generator; use Pterodactyl\Models\Node; use Illuminate\Support\Collection; use Pterodactyl\Repositories\Concerns\Searchable; -use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; -use Pterodactyl\Exceptions\Repository\RecordNotFoundException; class NodeRepository extends EloquentRepository implements NodeRepositoryInterface { @@ -140,25 +138,6 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa return $node; } - /** - * Return a node with all of the servers attached to that node. - * - * @param int $id - * @return \Pterodactyl\Models\Node - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function getNodeServers(int $id): Node - { - try { - return $this->getBuilder()->with([ - 'servers.user', 'servers.nest', 'servers.egg', - ])->findOrFail($id, $this->getColumns()); - } catch (ModelNotFoundException $exception) { - throw new RecordNotFoundException; - } - } - /** * Return a collection of nodes for all locations to use in server creation UI. * diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index ccb69af5f..f806c0f98 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Repositories\Eloquent; +use Pterodactyl\Models\Node; use Pterodactyl\Models\User; use Webmozart\Assert\Assert; use Pterodactyl\Models\Server; @@ -358,4 +359,20 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt { return $this->getBuilder()->where('suspended', true)->count(); } + + /** + * Returns all of the servers that exist for a given node in a paginated response. + * + * @param int $node + * @param int $limit + * + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public function loadAllServersForNode(int $node, int $limit): LengthAwarePaginator + { + return $this->getBuilder() + ->with(['user', 'nest', 'egg']) + ->where('node_id', '=', $node) + ->paginate($limit); + } } diff --git a/app/Repositories/Wings/BaseWingsRepository.php b/app/Repositories/Wings/BaseWingsRepository.php new file mode 100644 index 000000000..b22f8f8dd --- /dev/null +++ b/app/Repositories/Wings/BaseWingsRepository.php @@ -0,0 +1,33 @@ +getConfig(), [ + 'base_uri' => $this->getNode()->getConnectionAddress(), + 'headers' => [ + 'Authorization' => 'Bearer ' . ($this->getToken() ?? $this->getNode()->daemonSecret), + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + ])); + } +} diff --git a/app/Repositories/Wings/FileRepository.php b/app/Repositories/Wings/FileRepository.php new file mode 100644 index 000000000..5b0a8dabe --- /dev/null +++ b/app/Repositories/Wings/FileRepository.php @@ -0,0 +1,173 @@ +getHttpClient()->get( + sprintf('/api/servers/%s/files/contents', $this->getServer()->uuid), + [ + 'query' => ['file' => $path], + ] + ); + + $length = (int) $response->getHeader('Content-Length')[0] ?? 0; + + if ($notLargerThan && $length > $notLargerThan) { + throw new FileSizeTooLargeException( + trans('server.files.exceptions.max_size') + ); + } + + return $response->getBody()->__toString(); + } + + /** + * Save new contents to a given file. This works for both creating and updating + * a file. + * + * @param string $path + * @param string $content + * @return \Psr\Http\Message\ResponseInterface + * + * @throws \GuzzleHttp\Exception\TransferException + */ + public function putContent(string $path, string $content): ResponseInterface + { + return $this->getHttpClient()->post( + sprintf('/api/servers/%s/files/write', $this->getServer()->uuid), + [ + 'query' => ['file' => $path], + 'body' => $content, + ] + ); + } + + /** + * Return a directory listing for a given path. + * + * @param string $path + * @return array + * + * @throws \GuzzleHttp\Exception\TransferException + */ + public function getDirectory(string $path): array + { + $response = $this->getHttpClient()->get( + sprintf('/api/servers/%s/files/list-directory', $this->getServer()->uuid), + [ + 'query' => ['directory' => $path], + ] + ); + + return json_decode($response->getBody(), true); + } + + /** + * Creates a new directory for the server in the given $path. + * + * @param string $name + * @param string $path + * @return \Psr\Http\Message\ResponseInterface + */ + public function createDirectory(string $name, string $path): ResponseInterface + { + return $this->getHttpClient()->post( + sprintf('/api/servers/%s/files/create-directory', $this->getServer()->uuid), + [ + 'json' => [ + 'name' => $name, + 'path' => $path, + ], + ] + ); + } + + /** + * Renames or moves a file on the remote machine. + * + * @param string $from + * @param string $to + * @return \Psr\Http\Message\ResponseInterface + */ + public function renameFile(string $from, string $to): ResponseInterface + { + return $this->getHttpClient()->put( + sprintf('/api/servers/%s/files/rename', $this->getServer()->uuid), + [ + 'json' => [ + 'rename_from' => $from, + 'rename_to' => $to, + ], + ] + ); + } + + /** + * Copy a given file and give it a unique name. + * + * @param string $location + * @return \Psr\Http\Message\ResponseInterface + */ + public function copyFile(string $location): ResponseInterface + { + return $this->getHttpClient()->post( + sprintf('/api/servers/%s/files/copy', $this->getServer()->uuid), + [ + 'json' => [ + 'location' => $location, + ], + ] + ); + } + + /** + * Delete a file or folder for the server. + * + * @param string $location + * @return \Psr\Http\Message\ResponseInterface + */ + public function deleteFile(string $location): ResponseInterface + { + return $this->getHttpClient()->post( + sprintf('/api/servers/%s/files/delete', $this->getServer()->uuid), + [ + 'json' => [ + 'location' => $location, + ], + ] + ); + } +} diff --git a/app/Services/Databases/DatabaseManagementService.php b/app/Services/Databases/DatabaseManagementService.php index dc91e11f9..d7c942cba 100644 --- a/app/Services/Databases/DatabaseManagementService.php +++ b/app/Services/Databases/DatabaseManagementService.php @@ -69,7 +69,7 @@ class DatabaseManagementService $data['server_id'] = $server; $data['database'] = sprintf('s%d_%s', $server, $data['database']); $data['username'] = sprintf('u%d_%s', $server, str_random(10)); - $data['password'] = $this->encrypter->encrypt(str_random(16)); + $data['password'] = $this->encrypter->encrypt(str_random(24)); $this->database->beginTransaction(); try { diff --git a/app/Services/Helpers/AssetHashService.php b/app/Services/Helpers/AssetHashService.php new file mode 100644 index 000000000..33945578c --- /dev/null +++ b/app/Services/Helpers/AssetHashService.php @@ -0,0 +1,141 @@ +application = $application; + $this->cache = $cache; + $this->filesystem = $filesystem->createLocalDriver(['root' => public_path()]); + } + + /** + * Modify a URL to append the asset hash. + * + * @param string $resource + * @return string + * + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + public function url(string $resource): string + { + $file = last(explode('/', $resource)); + $data = array_get($this->manifest(), $file, $file); + + return str_replace($file, array_get($data, 'src', $file), $resource); + } + + /** + * Return the data integrity hash for a resource. + * + * @param string $resource + * @return string + * + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + public function integrity(string $resource): string + { + $file = last(explode('/', $resource)); + $data = array_get($this->manifest(), $file, $file); + + return array_get($data, 'integrity', ''); + } + + /** + * Return a built CSS import using the provided URL. + * + * @param string $resource + * @return string + * + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + public function css(string $resource): string + { + return ''; + } + + /** + * Return a built JS import using the provided URL. + * + * @param string $resource + * @return string + * + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + public function js(string $resource): string + { + return ''; + } + + /** + * Get the asset manifest and store it in the cache for quicker lookups. + * + * @return array + * + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + protected function manifest(): array + { + if (! is_null(self::$manifest)) { + return self::$manifest; + } + + // Skip checking the cache if we are not in production. + if ($this->application->environment() === 'production') { + $stored = $this->cache->get('Core:AssetManifest'); + if (! is_null($stored)) { + return self::$manifest = $stored; + } + } + + $contents = json_decode($this->filesystem->get(self::MANIFEST_PATH), true); + $this->cache->put('Core:AssetManifest', $contents, 1440); + + return self::$manifest = $contents; + } +} diff --git a/app/Services/Users/TwoFactorSetupService.php b/app/Services/Users/TwoFactorSetupService.php index 4d2ecff8a..7afb5f2bc 100644 --- a/app/Services/Users/TwoFactorSetupService.php +++ b/app/Services/Users/TwoFactorSetupService.php @@ -1,22 +1,18 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Users; +use Exception; +use RuntimeException; use Pterodactyl\Models\User; -use PragmaRX\Google2FA\Google2FA; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Illuminate\Contracts\Config\Repository as ConfigRepository; class TwoFactorSetupService { + const VALID_BASE32_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + /** * @var \Illuminate\Contracts\Config\Repository */ @@ -27,11 +23,6 @@ class TwoFactorSetupService */ private $encrypter; - /** - * @var \PragmaRX\Google2FA\Google2FA - */ - private $google2FA; - /** * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface */ @@ -42,24 +33,22 @@ class TwoFactorSetupService * * @param \Illuminate\Contracts\Config\Repository $config * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter - * @param \PragmaRX\Google2FA\Google2FA $google2FA * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository */ public function __construct( ConfigRepository $config, Encrypter $encrypter, - Google2FA $google2FA, UserRepositoryInterface $repository ) { $this->config = $config; $this->encrypter = $encrypter; - $this->google2FA = $google2FA; $this->repository = $repository; } /** * Generate a 2FA token and store it in the database before returning the - * QR code image. + * QR code URL. This URL will need to be attached to a QR generating service in + * order to function. * * @param \Pterodactyl\Models\User $user * @return string @@ -69,13 +58,26 @@ class TwoFactorSetupService */ public function handle(User $user): string { - $secret = $this->google2FA->generateSecretKey($this->config->get('pterodactyl.auth.2fa.bytes')); - $image = $this->google2FA->getQRCodeGoogleUrl($this->config->get('app.name'), $user->email, $secret); + $secret = ''; + try { + for ($i = 0; $i < $this->config->get('pterodactyl.auth.2fa.bytes', 16); $i++) { + $secret .= substr(self::VALID_BASE32_CHARACTERS, random_int(0, 31), 1); + } + } catch (Exception $exception) { + throw new RuntimeException($exception->getMessage(), 0, $exception); + } $this->repository->withoutFreshModel()->update($user->id, [ 'totp_secret' => $this->encrypter->encrypt($secret), ]); - return $image; + $company = $this->config->get('app.name'); + + return sprintf( + 'otpauth://totp/%1$s:%2$s?secret=%3$s&issuer=%1$s', + rawurlencode($company), + rawurlencode($user->email), + rawurlencode($secret) + ); } } diff --git a/app/Transformers/Api/Client/AccountTransformer.php b/app/Transformers/Api/Client/AccountTransformer.php new file mode 100644 index 000000000..2cdc92e34 --- /dev/null +++ b/app/Transformers/Api/Client/AccountTransformer.php @@ -0,0 +1,37 @@ + $model->id, + 'admin' => $model->root_admin, + 'username' => $model->username, + 'email' => $model->email, + 'first_name' => $model->name_first, + 'last_name' => $model->name_last, + 'language' => $model->language, + ]; + } +} diff --git a/app/Transformers/Api/Client/AllocationTransformer.php b/app/Transformers/Api/Client/AllocationTransformer.php new file mode 100644 index 000000000..055afdae3 --- /dev/null +++ b/app/Transformers/Api/Client/AllocationTransformer.php @@ -0,0 +1,36 @@ +loadMissing('server'); + + return [ + 'ip' => $model->ip, + 'alias' => $model->ip_alias, + 'port' => $model->port, + 'default' => $model->getRelation('server')->allocation_id === $model->id, + ]; + } +} diff --git a/app/Transformers/Api/Client/DatabaseTransformer.php b/app/Transformers/Api/Client/DatabaseTransformer.php new file mode 100644 index 000000000..c7b3177cc --- /dev/null +++ b/app/Transformers/Api/Client/DatabaseTransformer.php @@ -0,0 +1,78 @@ +encrypter = $encrypter; + $this->hashids = $hashids; + } + + /** + * @return string + */ + public function getResourceName(): string + { + return Database::RESOURCE_NAME; + } + + /** + * @param \Pterodactyl\Models\Database $model + * @return array + */ + public function transform(Database $model): array + { + $model->loadMissing('host'); + + return [ + 'id' => $this->hashids->encode($model->id), + 'host' => [ + 'address' => $model->getRelation('host')->host, + 'port' => $model->getRelation('host')->port, + ], + 'name' => $model->database, + 'username' => $model->username, + 'connections_from' => $model->remote, + ]; + } + + /** + * Include the database password in the request. + * + * @param \Pterodactyl\Models\Database $model + * @return \League\Fractal\Resource\Item + */ + public function includePassword(Database $model): Item + { + return $this->item($model, function (Database $model) { + return [ + 'password' => $this->encrypter->decrypt($model->password), + ]; + }, 'database_password'); + } +} diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php index 6816d6d74..64282686a 100644 --- a/app/Transformers/Api/Client/ServerTransformer.php +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -28,7 +28,12 @@ class ServerTransformer extends BaseClientTransformer 'identifier' => $server->uuidShort, 'uuid' => $server->uuid, 'name' => $server->name, + 'node' => $server->node->name, 'description' => $server->description, + 'allocation' => [ + 'ip' => $server->allocation->alias, + 'port' => $server->allocation->port, + ], 'limits' => [ 'memory' => $server->memory, 'swap' => $server->swap, diff --git a/app/Transformers/Api/Client/StatsTransformer.php b/app/Transformers/Api/Client/StatsTransformer.php index 01d8e3f20..d3e66eb9a 100644 --- a/app/Transformers/Api/Client/StatsTransformer.php +++ b/app/Transformers/Api/Client/StatsTransformer.php @@ -3,6 +3,8 @@ namespace Pterodactyl\Transformers\Api\Client; use Pterodactyl\Models\Server; +use GuzzleHttp\Exception\RequestException; +use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface; class StatsTransformer extends BaseClientTransformer @@ -36,6 +38,8 @@ class StatsTransformer extends BaseClientTransformer * * @param \Pterodactyl\Models\Server $model * @return array + * + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException */ public function transform(Server $model) { @@ -61,7 +65,10 @@ class StatsTransformer extends BaseClientTransformer 'disk' => [ 'current' => round(object_get($object, 'proc.disk.used', 0)), 'limit' => floatval($model->disk), + 'io' => $model->io, ], + 'installed' => $model->installed === 1, + 'suspended' => (bool) $model->suspended, ]; } diff --git a/codecov.yml b/codecov.yml index f173c829e..fdc1d707a 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,5 +2,4 @@ coverage: status: project: off patch: off -comment: - layout: "diff" +comment: false diff --git a/composer.json b/composer.json index 14d11cc6b..e0bbc6203 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "doctrine/dbal": "^2.5", "fideloper/proxy": "^4.0", "guzzlehttp/guzzle": "^6.3", - "hashids/hashids": "^2.0", + "hashids/hashids": "^3.0", "igaster/laravel-theme": "^2.0.6", "laracasts/utilities": "^3.0", "laravel/framework": "~5.7.14", @@ -29,7 +29,7 @@ "lord/laroute": "^2.4", "matriphe/iso-639": "^1.2", "nesbot/carbon": "^1.22", - "pragmarx/google2fa": "^2.0", + "pragmarx/google2fa": "^5.0", "predis/predis": "^1.1", "prologue/alerts": "^0.4", "ramsey/uuid": "^3.7", @@ -37,8 +37,8 @@ "sofa/eloquence-base": "v5.6.2", "sofa/eloquence-validable": "v5.6", "spatie/laravel-fractal": "^5.4", - "webmozart/assert": "^1.2", - "znck/belongs-to-through": "^2.3" + "staudenmeir/belongs-to-through": "~2.3.0", + "webmozart/assert": "^1.2" }, "require-dev": { "barryvdh/laravel-debugbar": "^3.2", @@ -47,10 +47,13 @@ "filp/whoops": "^2.1", "friendsofphp/php-cs-fixer": "^2.11.1", "fzaninotto/faker": "^1.6", + "laravel/dusk": "^3.0", + "martinlindhe/laravel-vue-i18n-generator": "^0.1.28", "mockery/mockery": "^1.0", "nunomaduro/collision": "^2.0", "php-mock/php-mock-phpunit": "^2.1", - "phpunit/phpunit": "~7.0" + "phpunit/phpunit": "~7.0", + "tightenco/ziggy": "^0.5.0" }, "autoload": { "classmap": [ @@ -65,6 +68,7 @@ }, "autoload-dev": { "psr-4": { + "Pterodactyl\\Tests\\Browser\\": "tests/Browser", "Pterodactyl\\Tests\\Integration\\": "tests/Integration", "Tests\\": "tests/" } diff --git a/composer.lock b/composer.lock index 93547e2dd..e0372b8be 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "17de367358e9573d913860f1b446e9a7", + "content-hash": "d16e45e7d2f27afca05914fa627726e7", "packages": [ { "name": "appstract/laravel-blade-directives", @@ -61,16 +61,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.74.1", + "version": "3.81.6", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "e02575af8021ee57b818107c1fd8759110374044" + "reference": "0e07e92ec7042316ffc79ef290cf7bbb7d7be24b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e02575af8021ee57b818107c1fd8759110374044", - "reference": "e02575af8021ee57b818107c1fd8759110374044", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/0e07e92ec7042316ffc79ef290cf7bbb7d7be24b", + "reference": "0e07e92ec7042316ffc79ef290cf7bbb7d7be24b", "shasum": "" }, "require": { @@ -140,7 +140,56 @@ "s3", "sdk" ], - "time": "2018-11-21T19:18:43+00:00" + "time": "2018-12-14T22:50:05+00:00" + }, + { + "name": "bacon/bacon-qr-code", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "eaac909da3ccc32b748a65b127acd8918f58d9b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/eaac909da3ccc32b748a65b127acd8918f58d9b0", + "reference": "eaac909da3ccc32b748a65b127acd8918f58d9b0", + "shasum": "" + }, + "require": { + "dasprid/enum": "^1.0", + "ext-iconv": "*", + "php": "^7.1" + }, + "require-dev": { + "phly/keep-a-changelog": "^1.4", + "phpunit/phpunit": "^6.4", + "squizlabs/php_codesniffer": "^3.1" + }, + "suggest": { + "ext-imagick": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-4": { + "BaconQrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "http://www.dasprids.de", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "time": "2018-04-25T17:53:56+00:00" }, { "name": "cakephp/chronos", @@ -199,6 +248,48 @@ ], "time": "2018-10-18T22:02:21+00:00" }, + { + "name": "dasprid/enum", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "631ef6e638e9494b0310837fa531bedd908fc22b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/631ef6e638e9494b0310837fa531bedd908fc22b", + "reference": "631ef6e638e9494b0310837fa531bedd908fc22b", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "^6.4", + "squizlabs/php_codesniffer": "^3.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "time": "2017-10-25T22:45:27+00:00" + }, { "name": "dnoegel/php-xdg-base-dir", "version": "0.1", @@ -309,16 +400,16 @@ }, { "name": "doctrine/dbal", - "version": "v2.8.0", + "version": "v2.9.1", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "5140a64c08b4b607b9bedaae0cedd26f04a0e621" + "reference": "ec74d6e300d78fbc896669c3ca57ef9719adc9c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/5140a64c08b4b607b9bedaae0cedd26f04a0e621", - "reference": "5140a64c08b4b607b9bedaae0cedd26f04a0e621", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/ec74d6e300d78fbc896669c3ca57ef9719adc9c6", + "reference": "ec74d6e300d78fbc896669c3ca57ef9719adc9c6", "shasum": "" }, "require": { @@ -328,11 +419,10 @@ "php": "^7.1" }, "require-dev": { - "doctrine/coding-standard": "^4.0", + "doctrine/coding-standard": "^5.0", "jetbrains/phpstorm-stubs": "^2018.1.2", "phpstan/phpstan": "^0.10.1", - "phpunit/phpunit": "^7.1.2", - "phpunit/phpunit-mock-objects": "!=3.2.4,!=3.2.5", + "phpunit/phpunit": "^7.4", "symfony/console": "^2.0.5|^3.0|^4.0", "symfony/phpunit-bridge": "^3.4.5|^4.0.5" }, @@ -345,13 +435,13 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8.x-dev", + "dev-master": "2.9.x-dev", "dev-develop": "3.0.x-dev" } }, "autoload": { - "psr-0": { - "Doctrine\\DBAL\\": "lib/" + "psr-4": { + "Doctrine\\DBAL\\": "lib/Doctrine/DBAL" } }, "notification-url": "https://packagist.org/downloads/", @@ -376,15 +466,19 @@ "email": "jonwage@gmail.com" } ], - "description": "Database Abstraction Layer", - "homepage": "http://www.doctrine-project.org", + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", "keywords": [ + "abstraction", "database", "dbal", + "mysql", "persistence", + "pgsql", + "php", "queryobject" ], - "time": "2018-07-13T03:16:35+00:00" + "time": "2018-12-14T04:51:13+00:00" }, { "name": "doctrine/event-manager", @@ -632,16 +726,16 @@ }, { "name": "egulias/email-validator", - "version": "2.1.6", + "version": "2.1.7", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "0578b32b30b22de3e8664f797cf846fc9246f786" + "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/0578b32b30b22de3e8664f797cf846fc9246f786", - "reference": "0578b32b30b22de3e8664f797cf846fc9246f786", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/709f21f92707308cdf8f9bcfa1af4cb26586521e", + "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e", "shasum": "" }, "require": { @@ -685,7 +779,7 @@ "validation", "validator" ], - "time": "2018-09-25T20:47:26+00:00" + "time": "2018-12-04T22:38:24+00:00" }, { "name": "erusev/parsedown", @@ -905,32 +999,33 @@ }, { "name": "guzzlehttp/psr7", - "version": "1.4.2", + "version": "1.5.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" + "reference": "9f83dded91781a01c63574e387eaa769be769115" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115", + "reference": "9f83dded91781a01c63574e387eaa769be769115", "shasum": "" }, "require": { "php": ">=5.4.0", - "psr/http-message": "~1.0" + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5" }, "provide": { "psr/http-message-implementation": "1.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -960,13 +1055,14 @@ "keywords": [ "http", "message", + "psr-7", "request", "response", "stream", "uri", "url" ], - "time": "2017-03-20T17:10:46+00:00" + "time": "2018-12-04T20:46:45+00:00" }, { "name": "hashids/hashids", @@ -1244,16 +1340,16 @@ }, { "name": "laravel/framework", - "version": "v5.7.14", + "version": "v5.7.17", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "ad6c1fe1e455c0f73a431928282704879ccbd856" + "reference": "a15898e2de5f5ae5548a09349c558ef0435d495a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/ad6c1fe1e455c0f73a431928282704879ccbd856", - "reference": "ad6c1fe1e455c0f73a431928282704879ccbd856", + "url": "https://api.github.com/repos/laravel/framework/zipball/a15898e2de5f5ae5548a09349c558ef0435d495a", + "reference": "a15898e2de5f5ae5548a09349c558ef0435d495a", "shasum": "" }, "require": { @@ -1262,6 +1358,8 @@ "erusev/parsedown": "^1.7", "ext-mbstring": "*", "ext-openssl": "*", + "laravel/nexmo-notification-channel": "^1.0", + "laravel/slack-notification-channel": "^1.0", "league/flysystem": "^1.0.8", "monolog/monolog": "^1.12", "nesbot/carbon": "^1.26.3", @@ -1384,7 +1482,121 @@ "framework", "laravel" ], - "time": "2018-11-21T13:46:08+00:00" + "time": "2018-12-12T14:38:09+00:00" + }, + { + "name": "laravel/nexmo-notification-channel", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/nexmo-notification-channel.git", + "reference": "03edd42a55b306ff980c9950899d5a2b03260d48" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/nexmo-notification-channel/zipball/03edd42a55b306ff980c9950899d5a2b03260d48", + "reference": "03edd42a55b306ff980c9950899d5a2b03260d48", + "shasum": "" + }, + "require": { + "nexmo/client": "^1.0", + "php": "^7.1.3" + }, + "require-dev": { + "illuminate/notifications": "~5.7", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + }, + "laravel": { + "providers": [ + "Illuminate\\Notifications\\NexmoChannelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Notifications\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Nexmo Notification Channel for laravel.", + "keywords": [ + "laravel", + "nexmo", + "notifications" + ], + "time": "2018-12-04T12:57:08+00:00" + }, + { + "name": "laravel/slack-notification-channel", + "version": "v1.0.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/slack-notification-channel.git", + "reference": "6e164293b754a95f246faf50ab2bbea3e4923cc9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/slack-notification-channel/zipball/6e164293b754a95f246faf50ab2bbea3e4923cc9", + "reference": "6e164293b754a95f246faf50ab2bbea3e4923cc9", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0", + "php": "^7.1.3" + }, + "require-dev": { + "illuminate/notifications": "~5.7", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + }, + "laravel": { + "providers": [ + "Illuminate\\Notifications\\SlackChannelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Notifications\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Slack Notification Channel for laravel.", + "keywords": [ + "laravel", + "notifications", + "slack" + ], + "time": "2018-12-12T13:12:06+00:00" }, { "name": "laravel/tinker", @@ -1449,6 +1661,64 @@ ], "time": "2018-10-12T19:39:35+00:00" }, + { + "name": "lcobucci/jwt", + "version": "3.2.5", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "82be04b4753f8b7693b62852b7eab30f97524f9b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/82be04b4753f8b7693b62852b7eab30f97524f9b", + "reference": "82be04b4753f8b7693b62852b7eab30f97524f9b", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=5.5" + }, + "require-dev": { + "mdanter/ecc": "~0.3.1", + "mikey179/vfsstream": "~1.5", + "phpmd/phpmd": "~2.2", + "phpunit/php-invoker": "~1.1", + "phpunit/phpunit": "~4.5", + "squizlabs/php_codesniffer": "~2.3" + }, + "suggest": { + "mdanter/ecc": "Required to use Elliptic Curves based algorithms." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Otávio Cobucci Oblonczyk", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "time": "2018-11-11T12:22:26+00:00" + }, { "name": "league/flysystem", "version": "1.0.49", @@ -1883,6 +2153,54 @@ ], "time": "2018-11-22T18:23:02+00:00" }, + { + "name": "nexmo/client", + "version": "1.5.2", + "source": { + "type": "git", + "url": "https://github.com/Nexmo/nexmo-php.git", + "reference": "f192c84ec3cc3e2657fc754e0da1a17e39ac5542" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nexmo/nexmo-php/zipball/f192c84ec3cc3e2657fc754e0da1a17e39ac5542", + "reference": "f192c84ec3cc3e2657fc754e0da1a17e39ac5542", + "shasum": "" + }, + "require": { + "lcobucci/jwt": "^3.2", + "php": ">=5.6", + "php-http/client-implementation": "^1.0", + "php-http/guzzle6-adapter": "^1.0", + "zendframework/zend-diactoros": "^1.3" + }, + "require-dev": { + "estahn/phpunit-json-assertions": "^1.0.0", + "php-http/mock-client": "^0.3.0", + "phpunit/phpunit": "^5.7", + "squizlabs/php_codesniffer": "^3.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Nexmo\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tim Lytle", + "email": "tim@nexmo.com", + "homepage": "http://twitter.com/tjlytle", + "role": "Developer" + } + ], + "description": "PHP Client for using Nexmo's API.", + "time": "2018-11-14T14:12:22+00:00" + }, { "name": "nikic/php-parser", "version": "v4.1.0", @@ -1936,16 +2254,16 @@ }, { "name": "opis/closure", - "version": "3.1.1", + "version": "3.1.2", "source": { "type": "git", "url": "https://github.com/opis/closure.git", - "reference": "d3209e46ad6c69a969b705df0738fd0dbe26ef9e" + "reference": "de00c69a2328d3ee5baa71fc584dc643222a574c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opis/closure/zipball/d3209e46ad6c69a969b705df0738fd0dbe26ef9e", - "reference": "d3209e46ad6c69a969b705df0738fd0dbe26ef9e", + "url": "https://api.github.com/repos/opis/closure/zipball/de00c69a2328d3ee5baa71fc584dc643222a574c", + "reference": "de00c69a2328d3ee5baa71fc584dc643222a574c", "shasum": "" }, "require": { @@ -1953,7 +2271,7 @@ }, "require-dev": { "jeremeamia/superclosure": "^2.0", - "phpunit/phpunit": "^4.0" + "phpunit/phpunit": "^4.0|^5.0|^6.0|^7.0" }, "type": "library", "extra": { @@ -1993,7 +2311,7 @@ "serialization", "serialize" ], - "time": "2018-10-02T13:36:53+00:00" + "time": "2018-12-16T21:48:23+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -2107,32 +2425,194 @@ "time": "2018-07-04T16:31:37+00:00" }, { - "name": "pragmarx/google2fa", - "version": "v2.0.7", + "name": "php-http/guzzle6-adapter", + "version": "v1.1.1", "source": { "type": "git", - "url": "https://github.com/antonioribeiro/google2fa.git", - "reference": "5a818bda62fab0c0a79060b06d50d50b5525d631" + "url": "https://github.com/php-http/guzzle6-adapter.git", + "reference": "a56941f9dc6110409cfcddc91546ee97039277ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/5a818bda62fab0c0a79060b06d50d50b5525d631", - "reference": "5a818bda62fab0c0a79060b06d50d50b5525d631", + "url": "https://api.github.com/repos/php-http/guzzle6-adapter/zipball/a56941f9dc6110409cfcddc91546ee97039277ab", + "reference": "a56941f9dc6110409cfcddc91546ee97039277ab", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0", + "php": ">=5.5.0", + "php-http/httplug": "^1.0" + }, + "provide": { + "php-http/async-client-implementation": "1.0", + "php-http/client-implementation": "1.0" + }, + "require-dev": { + "ext-curl": "*", + "php-http/adapter-integration-tests": "^0.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Adapter\\Guzzle6\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + }, + { + "name": "David de Boer", + "email": "david@ddeboer.nl" + } + ], + "description": "Guzzle 6 HTTP Adapter", + "homepage": "http://httplug.io", + "keywords": [ + "Guzzle", + "http" + ], + "time": "2016-05-10T06:13:32+00:00" + }, + { + "name": "php-http/httplug", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "1c6381726c18579c4ca2ef1ec1498fdae8bdf018" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/1c6381726c18579c4ca2ef1ec1498fdae8bdf018", + "reference": "1c6381726c18579c4ca2ef1ec1498fdae8bdf018", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "php-http/promise": "^1.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "henrikbjorn/phpspec-code-coverage": "^1.0", + "phpspec/phpspec": "^2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "time": "2016-08-31T08:30:17+00:00" + }, + { + "name": "php-http/promise", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "dc494cdc9d7160b9a09bd5573272195242ce7980" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/dc494cdc9d7160b9a09bd5573272195242ce7980", + "reference": "dc494cdc9d7160b9a09bd5573272195242ce7980", + "shasum": "" + }, + "require-dev": { + "henrikbjorn/phpspec-code-coverage": "^1.0", + "phpspec/phpspec": "^2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + }, + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "time": "2016-01-26T13:27:02+00:00" + }, + { + "name": "pragmarx/google2fa", + "version": "v5.0.0", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "17c969c82f427dd916afe4be50bafc6299aef1b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/17c969c82f427dd916afe4be50bafc6299aef1b4", + "reference": "17c969c82f427dd916afe4be50bafc6299aef1b4", "shasum": "" }, "require": { "paragonie/constant_time_encoding": "~1.0|~2.0", - "paragonie/random_compat": "~1.4|~2.0", + "paragonie/random_compat": ">=1", "php": ">=5.4", "symfony/polyfill-php56": "~1.2" }, "require-dev": { - "bacon/bacon-qr-code": "~1.0", "phpunit/phpunit": "~4|~5|~6" }, - "suggest": { - "bacon/bacon-qr-code": "Required to generate inline QR Codes." - }, "type": "library", "extra": { "component": "package", @@ -2148,7 +2628,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { @@ -2162,10 +2642,67 @@ "2fa", "Authentication", "Two Factor Authentication", - "google2fa", - "laravel" + "google2fa" ], - "time": "2018-01-06T16:21:07+00:00" + "time": "2019-03-19T22:44:16+00:00" + }, + { + "name": "pragmarx/google2fa-qrcode", + "version": "v1.0.3", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa-qrcode.git", + "reference": "fd5ff0531a48b193a659309cc5fb882c14dbd03f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa-qrcode/zipball/fd5ff0531a48b193a659309cc5fb882c14dbd03f", + "reference": "fd5ff0531a48b193a659309cc5fb882c14dbd03f", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "~1.0|~2.0", + "php": ">=5.4", + "pragmarx/google2fa": ">=4.0" + }, + "require-dev": { + "khanamiryan/qrcode-detector-decoder": "^1.0", + "phpunit/phpunit": "~4|~5|~6|~7" + }, + "type": "library", + "extra": { + "component": "package", + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "PragmaRX\\Google2FAQRCode\\": "src/", + "PragmaRX\\Google2FAQRCode\\Tests\\": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "QR Code package for Google2FA", + "keywords": [ + "2fa", + "Authentication", + "Two Factor Authentication", + "google2fa", + "qr code", + "qrcode" + ], + "time": "2019-03-20T16:42:58+00:00" }, { "name": "predis/predis", @@ -2545,6 +3082,46 @@ ], "time": "2018-10-13T15:16:03+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "~3.7.0", + "satooshi/php-coveralls": ">=1.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2016-02-11T07:05:27+00:00" + }, { "name": "ramsey/uuid", "version": "3.8.0", @@ -2958,6 +3535,58 @@ ], "time": "2018-09-28T16:17:34+00:00" }, + { + "name": "staudenmeir/belongs-to-through", + "version": "v2.3.2", + "source": { + "type": "git", + "url": "https://github.com/staudenmeir/belongs-to-through.git", + "reference": "2ba1ff76353058d2b4d395e725617d97fd103ab0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staudenmeir/belongs-to-through/zipball/2ba1ff76353058d2b4d395e725617d97fd103ab0", + "reference": "2ba1ff76353058d2b4d395e725617d97fd103ab0", + "shasum": "" + }, + "require": { + "illuminate/database": "~5.0" + }, + "require-dev": { + "fabpot/php-cs-fixer": "^1.11", + "orchestra/testbench": "~3.0", + "phpunit/php-code-coverage": "^3.3", + "phpunit/phpunit": "~5.0", + "satooshi/php-coveralls": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Znck\\Eloquent\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rahul Kadyan", + "email": "hi@znck.me" + } + ], + "description": "Adds belongsToThrough relation to laravel models", + "homepage": "https://github.com/staudenmeir/belongs-to-through", + "keywords": [ + "belongsToThrough", + "eloquent", + "laravel", + "model", + "models", + "znck" + ], + "time": "2019-02-01T14:33:18+00:00" + }, { "name": "swiftmailer/swiftmailer", "version": "v6.1.3", @@ -3019,20 +3648,21 @@ }, { "name": "symfony/console", - "version": "v4.1.7", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "432122af37d8cd52fba1b294b11976e0d20df595" + "reference": "4dff24e5d01e713818805c1862d2e3f901ee7dd0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/432122af37d8cd52fba1b294b11976e0d20df595", - "reference": "432122af37d8cd52fba1b294b11976e0d20df595", + "url": "https://api.github.com/repos/symfony/console/zipball/4dff24e5d01e713818805c1862d2e3f901ee7dd0", + "reference": "4dff24e5d01e713818805c1862d2e3f901ee7dd0", "shasum": "" }, "require": { "php": "^7.1.3", + "symfony/contracts": "^1.0", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { @@ -3056,7 +3686,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -3083,20 +3713,88 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2018-10-31T09:30:44+00:00" + "time": "2018-11-27T07:40:44+00:00" }, { - "name": "symfony/css-selector", - "version": "v4.1.7", + "name": "symfony/contracts", + "version": "v1.0.2", "source": { "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "d67de79a70a27d93c92c47f37ece958bf8de4d8a" + "url": "https://github.com/symfony/contracts.git", + "reference": "1aa7ab2429c3d594dd70689604b5cf7421254cdf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/d67de79a70a27d93c92c47f37ece958bf8de4d8a", - "reference": "d67de79a70a27d93c92c47f37ece958bf8de4d8a", + "url": "https://api.github.com/repos/symfony/contracts/zipball/1aa7ab2429c3d594dd70689604b5cf7421254cdf", + "reference": "1aa7ab2429c3d594dd70689604b5cf7421254cdf", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "require-dev": { + "psr/cache": "^1.0", + "psr/container": "^1.0" + }, + "suggest": { + "psr/cache": "When using the Cache contracts", + "psr/container": "When using the Service contracts", + "symfony/cache-contracts-implementation": "", + "symfony/service-contracts-implementation": "", + "symfony/translation-contracts-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\": "" + }, + "exclude-from-classmap": [ + "**/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A set of abstractions extracted out of the Symfony components", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "time": "2018-12-05T08:06:11+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v4.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "aa9fa526ba1b2ec087ffdfb32753803d999fcfcd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/aa9fa526ba1b2ec087ffdfb32753803d999fcfcd", + "reference": "aa9fa526ba1b2ec087ffdfb32753803d999fcfcd", "shasum": "" }, "require": { @@ -3105,7 +3803,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -3136,20 +3834,20 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2018-10-02T16:36:10+00:00" + "time": "2018-11-11T19:52:12+00:00" }, { "name": "symfony/debug", - "version": "v4.1.7", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "19090917b848a799cbae4800abf740fe4eb71c1d" + "reference": "e0a2b92ee0b5b934f973d90c2f58e18af109d276" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/19090917b848a799cbae4800abf740fe4eb71c1d", - "reference": "19090917b848a799cbae4800abf740fe4eb71c1d", + "url": "https://api.github.com/repos/symfony/debug/zipball/e0a2b92ee0b5b934f973d90c2f58e18af109d276", + "reference": "e0a2b92ee0b5b934f973d90c2f58e18af109d276", "shasum": "" }, "require": { @@ -3165,7 +3863,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -3192,24 +3890,25 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2018-10-31T09:09:42+00:00" + "time": "2018-11-28T18:24:18+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.1.7", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "552541dad078c85d9414b09c041ede488b456cd5" + "reference": "921f49c3158a276d27c0d770a5a347a3b718b328" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/552541dad078c85d9414b09c041ede488b456cd5", - "reference": "552541dad078c85d9414b09c041ede488b456cd5", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/921f49c3158a276d27c0d770a5a347a3b718b328", + "reference": "921f49c3158a276d27c0d770a5a347a3b718b328", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": "^7.1.3", + "symfony/contracts": "^1.0" }, "conflict": { "symfony/dependency-injection": "<3.4" @@ -3228,7 +3927,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -3255,20 +3954,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2018-10-10T13:52:42+00:00" + "time": "2018-12-01T08:52:38+00:00" }, { "name": "symfony/finder", - "version": "v4.1.7", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "1f17195b44543017a9c9b2d437c670627e96ad06" + "reference": "e53d477d7b5c4982d0e1bfd2298dbee63d01441d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/1f17195b44543017a9c9b2d437c670627e96ad06", - "reference": "1f17195b44543017a9c9b2d437c670627e96ad06", + "url": "https://api.github.com/repos/symfony/finder/zipball/e53d477d7b5c4982d0e1bfd2298dbee63d01441d", + "reference": "e53d477d7b5c4982d0e1bfd2298dbee63d01441d", "shasum": "" }, "require": { @@ -3277,7 +3976,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -3304,20 +4003,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2018-10-03T08:47:56+00:00" + "time": "2018-11-11T19:52:12+00:00" }, { "name": "symfony/http-foundation", - "version": "v4.1.7", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "82d494c1492b0dd24bbc5c2d963fb02eb44491af" + "reference": "1b31f3017fadd8cb05cf2c8aebdbf3b12a943851" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/82d494c1492b0dd24bbc5c2d963fb02eb44491af", - "reference": "82d494c1492b0dd24bbc5c2d963fb02eb44491af", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/1b31f3017fadd8cb05cf2c8aebdbf3b12a943851", + "reference": "1b31f3017fadd8cb05cf2c8aebdbf3b12a943851", "shasum": "" }, "require": { @@ -3331,7 +4030,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -3358,25 +4057,26 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2018-10-31T09:09:42+00:00" + "time": "2018-11-26T10:55:26+00:00" }, { "name": "symfony/http-kernel", - "version": "v4.1.7", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "958be64ab13b65172ad646ef5ae20364c2305fae" + "reference": "b39ceffc0388232c309cbde3a7c3685f2ec0a624" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/958be64ab13b65172ad646ef5ae20364c2305fae", - "reference": "958be64ab13b65172ad646ef5ae20364c2305fae", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b39ceffc0388232c309cbde3a7c3685f2ec0a624", + "reference": "b39ceffc0388232c309cbde3a7c3685f2ec0a624", "shasum": "" }, "require": { "php": "^7.1.3", "psr/log": "~1.0", + "symfony/contracts": "^1.0.2", "symfony/debug": "~3.4|~4.0", "symfony/event-dispatcher": "~4.1", "symfony/http-foundation": "^4.1.1", @@ -3384,7 +4084,8 @@ }, "conflict": { "symfony/config": "<3.4", - "symfony/dependency-injection": "<4.1", + "symfony/dependency-injection": "<4.2", + "symfony/translation": "<4.2", "symfony/var-dumper": "<4.1.1", "twig/twig": "<1.34|<2.4,>=2" }, @@ -3397,7 +4098,7 @@ "symfony/config": "~3.4|~4.0", "symfony/console": "~3.4|~4.0", "symfony/css-selector": "~3.4|~4.0", - "symfony/dependency-injection": "^4.1", + "symfony/dependency-injection": "^4.2", "symfony/dom-crawler": "~3.4|~4.0", "symfony/expression-language": "~3.4|~4.0", "symfony/finder": "~3.4|~4.0", @@ -3405,7 +4106,7 @@ "symfony/routing": "~3.4|~4.0", "symfony/stopwatch": "~3.4|~4.0", "symfony/templating": "~3.4|~4.0", - "symfony/translation": "~3.4|~4.0", + "symfony/translation": "~4.2", "symfony/var-dumper": "^4.1.1" }, "suggest": { @@ -3418,7 +4119,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -3445,7 +4146,7 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2018-11-03T11:11:23+00:00" + "time": "2018-12-06T17:39:52+00:00" }, { "name": "symfony/polyfill-ctype", @@ -3729,16 +4430,16 @@ }, { "name": "symfony/process", - "version": "v4.1.7", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "3e83acef94d979b1de946599ef86b3a352abcdc9" + "reference": "2b341009ccec76837a7f46f59641b431e4d4c2b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/3e83acef94d979b1de946599ef86b3a352abcdc9", - "reference": "3e83acef94d979b1de946599ef86b3a352abcdc9", + "url": "https://api.github.com/repos/symfony/process/zipball/2b341009ccec76837a7f46f59641b431e4d4c2b0", + "reference": "2b341009ccec76837a7f46f59641b431e4d4c2b0", "shasum": "" }, "require": { @@ -3747,7 +4448,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -3774,34 +4475,34 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2018-10-14T20:48:13+00:00" + "time": "2018-11-20T16:22:05+00:00" }, { "name": "symfony/routing", - "version": "v4.1.7", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "d4a3c14cfbe6b9c05a1d6e948654022d4d1ad3fd" + "reference": "649460207e77da6c545326c7f53618d23ad2c866" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/d4a3c14cfbe6b9c05a1d6e948654022d4d1ad3fd", - "reference": "d4a3c14cfbe6b9c05a1d6e948654022d4d1ad3fd", + "url": "https://api.github.com/repos/symfony/routing/zipball/649460207e77da6c545326c7f53618d23ad2c866", + "reference": "649460207e77da6c545326c7f53618d23ad2c866", "shasum": "" }, "require": { "php": "^7.1.3" }, "conflict": { - "symfony/config": "<3.4", + "symfony/config": "<4.2", "symfony/dependency-injection": "<3.4", "symfony/yaml": "<3.4" }, "require-dev": { "doctrine/annotations": "~1.0", "psr/log": "~1.0", - "symfony/config": "~3.4|~4.0", + "symfony/config": "~4.2", "symfony/dependency-injection": "~3.4|~4.0", "symfony/expression-language": "~3.4|~4.0", "symfony/http-foundation": "~3.4|~4.0", @@ -3818,7 +4519,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -3851,24 +4552,25 @@ "uri", "url" ], - "time": "2018-10-28T18:38:52+00:00" + "time": "2018-12-03T22:08:12+00:00" }, { "name": "symfony/translation", - "version": "v4.1.7", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "aa04dc1c75b7d3da7bd7003104cd0cfc5dff635c" + "reference": "c0e2191e9bed845946ab3d99767513b56ca7dcd6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/aa04dc1c75b7d3da7bd7003104cd0cfc5dff635c", - "reference": "aa04dc1c75b7d3da7bd7003104cd0cfc5dff635c", + "url": "https://api.github.com/repos/symfony/translation/zipball/c0e2191e9bed845946ab3d99767513b56ca7dcd6", + "reference": "c0e2191e9bed845946ab3d99767513b56ca7dcd6", "shasum": "" }, "require": { "php": "^7.1.3", + "symfony/contracts": "^1.0.2", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { @@ -3876,6 +4578,9 @@ "symfony/dependency-injection": "<3.4", "symfony/yaml": "<3.4" }, + "provide": { + "symfony/translation-contracts-implementation": "1.0" + }, "require-dev": { "psr/log": "~1.0", "symfony/config": "~3.4|~4.0", @@ -3893,7 +4598,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -3920,20 +4625,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2018-10-28T18:38:52+00:00" + "time": "2018-12-06T10:45:32+00:00" }, { "name": "symfony/var-dumper", - "version": "v4.1.7", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "60319b45653580b0cdacca499344577d87732f16" + "reference": "db61258540350725f4beb6b84006e32398acd120" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/60319b45653580b0cdacca499344577d87732f16", - "reference": "60319b45653580b0cdacca499344577d87732f16", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/db61258540350725f4beb6b84006e32398acd120", + "reference": "db61258540350725f4beb6b84006e32398acd120", "shasum": "" }, "require": { @@ -3961,7 +4666,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -3995,7 +4700,7 @@ "debug", "dump" ], - "time": "2018-10-02T16:36:10+00:00" + "time": "2018-11-25T12:50:42+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -4145,56 +4850,68 @@ "time": "2018-01-29T19:49:41+00:00" }, { - "name": "znck/belongs-to-through", - "version": "v2.3.1", + "name": "zendframework/zend-diactoros", + "version": "1.8.6", "source": { "type": "git", - "url": "https://github.com/znck/belongs-to-through.git", - "reference": "8ac53e9134072902a8d3f3e18c327d4a8fd70d3d" + "url": "https://github.com/zendframework/zend-diactoros.git", + "reference": "20da13beba0dde8fb648be3cc19765732790f46e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/znck/belongs-to-through/zipball/8ac53e9134072902a8d3f3e18c327d4a8fd70d3d", - "reference": "8ac53e9134072902a8d3f3e18c327d4a8fd70d3d", + "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/20da13beba0dde8fb648be3cc19765732790f46e", + "reference": "20da13beba0dde8fb648be3cc19765732790f46e", "shasum": "" }, "require": { - "illuminate/database": "~5.0" + "php": "^5.6 || ^7.0", + "psr/http-message": "^1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" }, "require-dev": { - "fabpot/php-cs-fixer": "^1.11", - "orchestra/testbench": "~3.0", - "phpunit/php-code-coverage": "^3.3", - "phpunit/phpunit": "~5.0", - "satooshi/php-coveralls": "^1.0" + "ext-dom": "*", + "ext-libxml": "*", + "php-http/psr7-integration-tests": "dev-master", + "phpunit/phpunit": "^5.7.16 || ^6.0.8 || ^7.2.7", + "zendframework/zend-coding-standard": "~1.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8.x-dev", + "dev-develop": "1.9.x-dev", + "dev-release-2.0": "2.0.x-dev" + } + }, "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/marshal_uri_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php" + ], "psr-4": { - "Znck\\Eloquent\\": "src/" + "Zend\\Diactoros\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-2-Clause" ], - "authors": [ - { - "name": "Rahul Kadyan", - "email": "hi@znck.me" - } - ], - "description": "Adds belongsToThrough relation to laravel models", - "homepage": "https://github.com/znck/belongs-to-through", + "description": "PSR HTTP Message implementations", + "homepage": "https://github.com/zendframework/zend-diactoros", "keywords": [ - "belongsToThrough", - "eloquent", - "laravel", - "model", - "models", - "znck" + "http", + "psr", + "psr-7" ], - "time": "2017-07-23T13:11:16+00:00" + "time": "2018-09-05T19:29:37+00:00" } ], "packages-dev": [ @@ -4543,16 +5260,16 @@ }, { "name": "composer/composer", - "version": "1.7.3", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "e965b9aaa8854c3067f1ed2ae45f436572d73eb7" + "reference": "d8aef3af866b28786ce9b8647e52c42496436669" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/e965b9aaa8854c3067f1ed2ae45f436572d73eb7", - "reference": "e965b9aaa8854c3067f1ed2ae45f436572d73eb7", + "url": "https://api.github.com/repos/composer/composer/zipball/d8aef3af866b28786ce9b8647e52c42496436669", + "reference": "d8aef3af866b28786ce9b8647e52c42496436669", "shasum": "" }, "require": { @@ -4588,7 +5305,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7-dev" + "dev-master": "1.8-dev" } }, "autoload": { @@ -4619,7 +5336,7 @@ "dependency", "package" ], - "time": "2018-11-01T09:05:06+00:00" + "time": "2018-12-03T09:31:16+00:00" }, { "name": "composer/semver", @@ -4746,16 +5463,16 @@ }, { "name": "composer/xdebug-handler", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "b8e9745fb9b06ea6664d8872c4505fb16df4611c" + "reference": "dc523135366eb68f22268d069ea7749486458562" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/b8e9745fb9b06ea6664d8872c4505fb16df4611c", - "reference": "b8e9745fb9b06ea6664d8872c4505fb16df4611c", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/dc523135366eb68f22268d069ea7749486458562", + "reference": "dc523135366eb68f22268d069ea7749486458562", "shasum": "" }, "require": { @@ -4786,7 +5503,7 @@ "Xdebug", "performance" ], - "time": "2018-08-31T19:07:57+00:00" + "time": "2018-11-29T10:59:02+00:00" }, { "name": "doctrine/annotations", @@ -4910,6 +5627,66 @@ ], "time": "2017-07-22T11:58:36+00:00" }, + { + "name": "facebook/webdriver", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/facebook/php-webdriver.git", + "reference": "bd8c740097eb9f2fc3735250fc1912bc811a954e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/bd8c740097eb9f2fc3735250fc1912bc811a954e", + "reference": "bd8c740097eb9f2fc3735250fc1912bc811a954e", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-zip": "*", + "php": "^5.6 || ~7.0", + "symfony/process": "^2.8 || ^3.1 || ^4.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.0", + "jakub-onderka/php-parallel-lint": "^0.9.2", + "php-coveralls/php-coveralls": "^2.0", + "php-mock/php-mock-phpunit": "^1.1", + "phpunit/phpunit": "^5.7", + "sebastian/environment": "^1.3.4 || ^2.0 || ^3.0", + "squizlabs/php_codesniffer": "^2.6", + "symfony/var-dumper": "^3.3 || ^4.0" + }, + "suggest": { + "ext-SimpleXML": "For Firefox profile creation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-community": "1.5-dev" + } + }, + "autoload": { + "psr-4": { + "Facebook\\WebDriver\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "A PHP client for Selenium WebDriver", + "homepage": "https://github.com/facebook/php-webdriver", + "keywords": [ + "facebook", + "php", + "selenium", + "webdriver" + ], + "time": "2018-05-16T17:37:13+00:00" + }, { "name": "filp/whoops", "version": "2.3.1", @@ -5283,6 +6060,120 @@ ], "time": "2018-02-14T22:26:30+00:00" }, + { + "name": "laravel/dusk", + "version": "v3.0.10", + "source": { + "type": "git", + "url": "https://github.com/laravel/dusk.git", + "reference": "8bfb9f01ae09d1d9ca3ce53e36b1f020c0f8dc8f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/dusk/zipball/8bfb9f01ae09d1d9ca3ce53e36b1f020c0f8dc8f", + "reference": "8bfb9f01ae09d1d9ca3ce53e36b1f020c0f8dc8f", + "shasum": "" + }, + "require": { + "facebook/webdriver": "~1.3", + "illuminate/console": "~5.6", + "illuminate/support": "~5.6", + "nesbot/carbon": "~1.20", + "php": ">=7.1.0", + "symfony/console": "~4.0", + "symfony/process": "~4.0" + }, + "require-dev": { + "mockery/mockery": "~1.0", + "phpunit/phpunit": "~7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Dusk\\DuskServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Dusk\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Dusk provides simple end-to-end testing and browser automation.", + "keywords": [ + "laravel", + "testing", + "webdriver" + ], + "time": "2018-08-06T20:22:46+00:00" + }, + { + "name": "martinlindhe/laravel-vue-i18n-generator", + "version": "0.1.35", + "source": { + "type": "git", + "url": "https://github.com/martinlindhe/laravel-vue-i18n-generator.git", + "reference": "55c2d33c0bd3c681f38dc683b6f4ac600b1da557" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/martinlindhe/laravel-vue-i18n-generator/zipball/55c2d33c0bd3c681f38dc683b6f4ac600b1da557", + "reference": "55c2d33c0bd3c681f38dc683b6f4ac600b1da557", + "shasum": "" + }, + "require": { + "illuminate/console": "~5.1.0|~5.2.0|~5.3.0|~5.4.0|~5.5.0|~5.6.0|~5.7.0", + "illuminate/support": "~5.1.0|~5.2.0|~5.3.0|~5.4.0|~5.5.0|~5.6.0|~5.7.0", + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.7" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "MartinLindhe\\VueInternationalizationGenerator\\GeneratorProvider" + ] + } + }, + "autoload": { + "psr-4": { + "MartinLindhe\\VueInternationalizationGenerator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Martin Lindhe", + "email": "martin@ubique.se" + } + ], + "description": "Generates a vue-i18n compatible include file from your Laravel translations.", + "homepage": "http://github.com/martinlindhe/laravel-vue-i18n-generator", + "keywords": [ + "laravel", + "vue-i18n" + ], + "time": "2018-12-07T16:53:14+00:00" + }, { "name": "maximebf/debugbar", "version": "v1.15.0", @@ -6311,16 +7202,16 @@ }, { "name": "phpunit/phpunit", - "version": "7.4.4", + "version": "7.5.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b1be2c8530c4c29c3519a052c9fb6cee55053bbd" + "reference": "c23d78776ad415d5506e0679723cb461d71f488f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b1be2c8530c4c29c3519a052c9fb6cee55053bbd", - "reference": "b1be2c8530c4c29c3519a052c9fb6cee55053bbd", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c23d78776ad415d5506e0679723cb461d71f488f", + "reference": "c23d78776ad415d5506e0679723cb461d71f488f", "shasum": "" }, "require": { @@ -6341,7 +7232,7 @@ "phpunit/php-timer": "^2.0", "sebastian/comparator": "^3.0", "sebastian/diff": "^3.0", - "sebastian/environment": "^3.1 || ^4.0", + "sebastian/environment": "^4.0", "sebastian/exporter": "^3.1", "sebastian/global-state": "^2.0", "sebastian/object-enumerator": "^3.0.3", @@ -6365,7 +7256,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "7.4-dev" + "dev-master": "7.5-dev" } }, "autoload": { @@ -6391,7 +7282,7 @@ "testing", "xunit" ], - "time": "2018-11-14T16:52:02+00:00" + "time": "2018-12-12T07:20:32+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -7051,16 +7942,16 @@ }, { "name": "symfony/filesystem", - "version": "v4.1.7", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "fd7bd6535beb1f0a0a9e3ee960666d0598546981" + "reference": "2f4c8b999b3b7cadb2a69390b01af70886753710" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/fd7bd6535beb1f0a0a9e3ee960666d0598546981", - "reference": "fd7bd6535beb1f0a0a9e3ee960666d0598546981", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/2f4c8b999b3b7cadb2a69390b01af70886753710", + "reference": "2f4c8b999b3b7cadb2a69390b01af70886753710", "shasum": "" }, "require": { @@ -7070,7 +7961,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -7097,20 +7988,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2018-10-30T13:18:25+00:00" + "time": "2018-11-11T19:52:12+00:00" }, { "name": "symfony/options-resolver", - "version": "v4.1.7", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "40f0e40d37c1c8a762334618dea597d64bbb75ff" + "reference": "a9c38e8a3da2c03b3e71fdffa6efb0bda51390ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/40f0e40d37c1c8a762334618dea597d64bbb75ff", - "reference": "40f0e40d37c1c8a762334618dea597d64bbb75ff", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/a9c38e8a3da2c03b3e71fdffa6efb0bda51390ba", + "reference": "a9c38e8a3da2c03b3e71fdffa6efb0bda51390ba", "shasum": "" }, "require": { @@ -7119,7 +8010,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -7151,7 +8042,7 @@ "configuration", "options" ], - "time": "2018-09-18T12:45:12+00:00" + "time": "2018-11-11T19:52:12+00:00" }, { "name": "symfony/polyfill-php70", @@ -7214,25 +8105,26 @@ }, { "name": "symfony/stopwatch", - "version": "v4.1.7", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "5bfc064125b73ff81229e19381ce1c34d3416f4b" + "reference": "ec076716412274e51f8a7ea675d9515e5c311123" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5bfc064125b73ff81229e19381ce1c34d3416f4b", - "reference": "5bfc064125b73ff81229e19381ce1c34d3416f4b", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/ec076716412274e51f8a7ea675d9515e5c311123", + "reference": "ec076716412274e51f8a7ea675d9515e5c311123", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": "^7.1.3", + "symfony/contracts": "^1.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -7259,20 +8151,20 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2018-10-02T12:40:59+00:00" + "time": "2018-11-11T19:52:12+00:00" }, { "name": "symfony/yaml", - "version": "v4.1.7", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "367e689b2fdc19965be435337b50bc8adf2746c9" + "reference": "c41175c801e3edfda90f32e292619d10c27103d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/367e689b2fdc19965be435337b50bc8adf2746c9", - "reference": "367e689b2fdc19965be435337b50bc8adf2746c9", + "url": "https://api.github.com/repos/symfony/yaml/zipball/c41175c801e3edfda90f32e292619d10c27103d7", + "reference": "c41175c801e3edfda90f32e292619d10c27103d7", "shasum": "" }, "require": { @@ -7291,7 +8183,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -7318,7 +8210,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2018-10-02T16:36:10+00:00" + "time": "2018-11-11T19:52:12+00:00" }, { "name": "theseer/tokenizer", @@ -7359,6 +8251,57 @@ ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "time": "2017-04-07T12:08:54+00:00" + }, + { + "name": "tightenco/ziggy", + "version": "v0.5.0", + "source": { + "type": "git", + "url": "https://github.com/tightenco/ziggy.git", + "reference": "a289378d08ee16f573140820eae002cd1616e9ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tightenco/ziggy/zipball/a289378d08ee16f573140820eae002cd1616e9ec", + "reference": "a289378d08ee16f573140820eae002cd1616e9ec", + "shasum": "" + }, + "require": { + "laravel/framework": "^5.4.29" + }, + "require-dev": { + "mikey179/vfsstream": "^1.6", + "orchestra/testbench": "~3.4" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Tightenco\\Ziggy\\ZiggyServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Tightenco\\Ziggy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Stauffer", + "email": "matt@tighten.co" + }, + { + "name": "Daniel Coulbourne", + "email": "daniel@tighten.co" + } + ], + "description": "Generates a Blade directive exporting all of your named Laravel routes. Also provides a nice route() helper function in JavaScript.", + "time": "2017-10-30T07:55:42+00:00" } ], "aliases": [], diff --git a/config/app.php b/config/app.php index fccb8b888..4082feaa0 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' => 'canary', + 'version' => '0.7.14', /* |-------------------------------------------------------------------------- diff --git a/config/pterodactyl.php b/config/pterodactyl.php index ba488dad2..4379c753b 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -1,6 +1,5 @@ env('SESSION_LIFETIME', 10080), + 'lifetime' => env('SESSION_LIFETIME', 720), 'expire_on_close' => false, diff --git a/config/vue-i18n-generator.php b/config/vue-i18n-generator.php new file mode 100644 index 000000000..92a20fd73 --- /dev/null +++ b/config/vue-i18n-generator.php @@ -0,0 +1,50 @@ + '/resources/lang', + + /* + |-------------------------------------------------------------------------- + | Laravel translation files + |-------------------------------------------------------------------------- + | + | You can choose which translation files to be generated. + | Note: leave this empty for all the translation files to be generated. + | + */ + + 'langFiles' => [], + + /* + |-------------------------------------------------------------------------- + | Output file + |-------------------------------------------------------------------------- + | + | The javascript path where I will place the generated file. + | Note: the path will be prepended to point to the App directory. + | + */ + 'jsPath' => '/resources/lang/i18n', + 'jsFile' => '/resources/lang/locales.js', + + /* + |-------------------------------------------------------------------------- + | i18n library + |-------------------------------------------------------------------------- + | + | Specify the library you use for localization. + | Options are vue-i18n or vuex-i18n. + | + */ + 'i18nLib' => 'vuex-i18n', +]; diff --git a/database/migrations/2019_03_02_142328_set_allocation_limit_default_null.php b/database/migrations/2019_03_02_142328_set_allocation_limit_default_null.php new file mode 100644 index 000000000..d91ce6372 --- /dev/null +++ b/database/migrations/2019_03_02_142328_set_allocation_limit_default_null.php @@ -0,0 +1,32 @@ +unsignedInteger('allocation_limit')->nullable()->default(null)->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('servers', function (Blueprint $table) { + $table->unsignedInteger('allocation_limit')->nullable()->default(0)->change(); + }); + } +} diff --git a/database/migrations/2019_03_02_151321_fix_unique_index_to_account_for_host.php b/database/migrations/2019_03_02_151321_fix_unique_index_to_account_for_host.php new file mode 100644 index 000000000..fe5f85f88 --- /dev/null +++ b/database/migrations/2019_03_02_151321_fix_unique_index_to_account_for_host.php @@ -0,0 +1,40 @@ +dropUnique(['database']); + $table->dropUnique(['username']); + + $table->unique(['database_host_id', 'database']); + $table->unique(['database_host_id', 'username']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('databases', function (Blueprint $table) { + $table->dropUnique(['database_host_id', 'database']); + $table->dropUnique(['database_host_id', 'username']); + + $table->unique(['database']); + $table->unique(['username']); + }); + } +} diff --git a/database/seeds/NestSeeder.php b/database/seeds/NestSeeder.php index 38b221846..fdd775645 100644 --- a/database/seeds/NestSeeder.php +++ b/database/seeds/NestSeeder.php @@ -17,7 +17,7 @@ class NestSeeder extends Seeder private $repository; /** - * MinecraftNestSeeder constructor. + * NestSeeder constructor. * * @param \Pterodactyl\Services\Nests\NestCreationService $creationService * @param \Pterodactyl\Contracts\Repository\NestRepositoryInterface $repository diff --git a/database/seeds/eggs/minecraft/egg-bungeecord.json b/database/seeds/eggs/minecraft/egg-bungeecord.json index 1f60064ac..d527024f3 100644 --- a/database/seeds/eggs/minecraft/egg-bungeecord.json +++ b/database/seeds/eggs/minecraft/egg-bungeecord.json @@ -18,7 +18,7 @@ "scripts": { "installation": { "script": "#!\/bin\/ash\n# Bungeecord Installation Script\n#\n# Server Files: \/mnt\/server\napk update\napk add curl\n\ncd \/mnt\/server\n\nif [ -z \"${BUNGEE_VERSION}\" ] || [ \"${BUNGEE_VERSION}\" == \"latest\" ]; then\n BUNGEE_VERSION=\"lastStableBuild\"\nfi\n\ncurl -o ${SERVER_JARFILE} https:\/\/ci.md-5.net\/job\/BungeeCord\/${BUNGEE_VERSION}\/artifact\/bootstrap\/target\/BungeeCord.jar", - "container": "alpine:3.4", + "container": "alpine:3.9", "entrypoint": "ash" } }, @@ -42,4 +42,4 @@ "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" } ] -} \ No newline at end of file +} diff --git a/database/seeds/eggs/minecraft/egg-forge-minecraft.json b/database/seeds/eggs/minecraft/egg-forge-minecraft.json index 11be0aefb..2e8b43bfe 100644 --- a/database/seeds/eggs/minecraft/egg-forge-minecraft.json +++ b/database/seeds/eggs/minecraft/egg-forge-minecraft.json @@ -3,7 +3,7 @@ "meta": { "version": "PTDL_v1" }, - "exported_at": "2018-06-25T15:47:07-04:00", + "exported_at": "2019-02-07T07:47:35-05:00", "name": "Forge Minecraft", "author": "support@pterodactyl.io", "description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.", @@ -17,8 +17,8 @@ }, "scripts": { "installation": { - "script": "#!\/bin\/ash\r\n# Forge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napk update\r\napk add curl\r\n\r\nif [ -z \"$MC_VERSION\" ] || [ \"$MC_VERSION\" == \"latest\" ]; then\r\n FORGE_VERSION=$(curl -sl http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/ | grep -A1 Latest | grep -o -e '[1]\\.[0-9][0-9]]\\?\\.\\?[0-9]\\?[0-9] - [0-9][0-9]\\.[0-9][0-9]\\.[0-9]\\?[0-9]\\.[0-9][0-9][0-9][0-9]' | sed 's\/ \/\/g')\r\nelse\r\n FORGE_VERSION=$(curl -sl http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/index_$MC_VERSION.html | grep -A1 Latest | grep -o -e '[1]\\.[0-9][0-9]]\\?\\.\\?[0-9]\\?[0-9] - [0-9][0-9]\\.[0-9][0-9]\\.[0-9]\\?[0-9]\\.[0-9][0-9][0-9][0-9]' | sed 's\/ \/\/g')\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\necho -e \"\\nDownloading Forge Version $FORGE_VERSION\\n\"\r\ncurl -sS http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/$FORGE_VERSION\/forge-$FORGE_VERSION-installer.jar -o installer.jar\r\ncurl -sS http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/$FORGE_VERSION\/forge-$FORGE_VERSION-universal.jar -o $SERVER_JARFILE\r\n\r\necho -e \"\\nInstalling forge server usint the installer jar file.\\n\"\r\njava -jar installer.jar --installServer\r\n\r\necho -e \"\\nDeleting installer jar file and cleaning up.\\n\"\r\nrm -rf installer.jar", - "container": "frolvlad\/alpine-oraclejdk8:cleaned", + "script": "#!\/bin\/ash\r\n# Forge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napk update\r\napk add curl\r\n\r\nif [ -z \"$MC_VERSION\" ] || [ \"$MC_VERSION\" == \"latest\" ]; then\r\n FORGE_VERSION=$(echo $(curl -sSl http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/ | grep -A2 Latest | grep small) | grep -o -e '[1].[0-9]*.[0-9]* - [0-9]*.[0-9]*.[0-9]*.[0-9]*' | sed 's\/ \/\/g')\r\nelse\r\n FORGE_VERSION=$(echo $(curl -sl http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/index_$MC_VERSION.html | grep -A2 Latest | grep small) | grep -o -e '[1].[0-9]*.[0-9]* - [0-9]*.[0-9]*.[0-9]*.[0-9]*' | sed 's\/ \/\/g')\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\necho -e \"\\nDownloading Forge Version $FORGE_VERSION\\n\"\r\ncurl -sS http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/$FORGE_VERSION\/forge-$FORGE_VERSION-installer.jar -o installer.jar\r\ncurl -sS http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/$FORGE_VERSION\/forge-$FORGE_VERSION-universal.jar -o $SERVER_JARFILE\r\n\r\necho -e \"\\nInstalling forge server usint the installer jar file.\\n\"\r\njava -jar installer.jar --installServer\r\n\r\necho -e \"\\nDeleting installer jar file and cleaning up.\\n\"\r\nrm -rf installer.jar", + "container": "openjdk:8-alpine", "entrypoint": "ash" } }, @@ -42,4 +42,4 @@ "rules": "required|string|max:20" } ] -} +} \ No newline at end of file diff --git a/database/seeds/eggs/minecraft/egg-paper.json b/database/seeds/eggs/minecraft/egg-paper.json new file mode 100644 index 000000000..86208f68b --- /dev/null +++ b/database/seeds/eggs/minecraft/egg-paper.json @@ -0,0 +1,63 @@ +{ + "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO", + "meta": { + "version": "PTDL_v1" + }, + "exported_at": "2019-02-27T22:23:55-05:00", + "name": "Paper", + "author": "parker@pterodactyl.io", + "description": "High performance Spigot fork that aims to fix gameplay and mechanics inconsistencies.", + "image": "quay.io\/pterodactyl\/core:java", + "startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}", + "config": { + "files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}", + "startup": "{\r\n \"done\": \")! For help, type \",\r\n \"userInteraction\": [\r\n \"Go to eula.txt for more info.\"\r\n ]\r\n}", + "logs": "{}", + "stop": "stop" + }, + "scripts": { + "installation": { + "script": "#!\/bin\/ash\r\n# Paper Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napk add --no-cache --update curl jq\r\n\r\nif [ -n \"${DL_PATH}\" ]; then\r\n echo -e \"using supplied download url\"\r\n DOWNLOAD_URL=`eval echo $(echo ${DL_PATH} | sed -e 's\/{{\/${\/g' -e 's\/}}\/}\/g')`\r\nelse\r\n VER_EXISTS=`curl -s https:\/\/papermc.io\/api\/v1\/paper | jq -r --arg VERSION $MINECRAFT_VERSION '.versions[] | IN($VERSION)' | grep true`\r\n LATEST_PAPER_VERSION=`curl -s https:\/\/papermc.io\/api\/v1\/paper | jq -r '.versions' | jq -r '.[0]'`\r\n \r\n if [ \"${VER_EXISTS}\" == \"true\" ]; then\r\n echo -e \"Version is valid. Using version ${MINECRAFT_VERSION}\"\r\n else\r\n echo -e \"Using the latest paper version\"\r\n MINECRAFT_VERSION=${LATEST_PAPER_VERSION}\r\n fi\r\n \r\n BUILD_EXISTS=`curl -s https:\/\/papermc.io\/api\/v1\/paper\/${MINECRAFT_VERSION} | jq -r --arg BUILD ${BUILD_NUMBER} '.builds.all[] | IN($BUILD)' | grep true`\r\n LATEST_PAPER_BUILD=`curl -s https:\/\/papermc.io\/api\/v1\/paper\/${MINECRAFT_VERSION} | jq -r '.builds.latest'`\r\n \r\n if [ \"${BUILD_EXISTS}\" == \"true\" ] || [ ${BUILD_NUMBER} == \"latest\" ]; then\r\n echo -e \"Build is valid. Using version ${BUILD_NUMBER}\"\r\n else\r\n echo -e \"Using the latest paper build\"\r\n BUILD_NUMBER=${LATEST_PAPER_BUILD}\r\n fi\r\n \r\n echo \"Version being downloaded\"\r\n echo -e \"MC Version: ${MINECRAFT_VERSION}\"\r\n echo -e \"Build: ${BUILD_NUMBER}\"\r\n DOWNLOAD_URL=https:\/\/papermc.io\/api\/v1\/paper\/${MINECRAFT_VERSION}\/${BUILD_NUMBER}\/download \r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\necho -e \"running curl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\"\r\n\r\ncurl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\r\n\r\necho -e \"Downloading MC server.properties\"\r\ncurl -o server.properties https:\/\/raw.githubusercontent.com\/parkervcp\/eggs\/master\/minecraft_java\/server.properties", + "container": "alpine:3.9", + "entrypoint": "ash" + } + }, + "variables": [ + { + "name": "Minecraft Version", + "description": "The version of minecraft to download. \r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest.", + "env_variable": "MINECRAFT_VERSION", + "default_value": "latest", + "user_viewable": 1, + "user_editable": 0, + "rules": "nullable|string|max:20" + }, + { + "name": "Server Jar File", + "description": "The name of the server jarfile to run the server with.", + "env_variable": "SERVER_JARFILE", + "default_value": "server.jar", + "user_viewable": 1, + "user_editable": 1, + "rules": "required|string|max:20" + }, + { + "name": "Download Path", + "description": "A URL to use to download a server.jar rather than the ones in the install script. This is not user viewable.", + "env_variable": "DL_PATH", + "default_value": "", + "user_viewable": 0, + "user_editable": 0, + "rules": "nullable|string" + }, + { + "name": "Build Number", + "description": "The build number for the paper release.\r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest.", + "env_variable": "BUILD_NUMBER", + "default_value": "latest", + "user_viewable": 1, + "user_editable": 0, + "rules": "required|string|max:20" + } + ] +} \ No newline at end of file diff --git a/database/seeds/eggs/minecraft/egg-spigot.json b/database/seeds/eggs/minecraft/egg-spigot.json deleted file mode 100644 index fa34e22c5..000000000 --- a/database/seeds/eggs/minecraft/egg-spigot.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO", - "meta": { - "version": "PTDL_v1" - }, - "exported_at": "2018-11-16T02:14:51-05:00", - "name": "Spigot", - "author": "support@pterodactyl.io", - "description": "Spigot is the most widely-used modded Minecraft server software in the world. It powers many of the top Minecraft server networks around to ensure they can cope with their huge player base and ensure the satisfaction of their players. Spigot works by reducing and eliminating many causes of lag, as well as adding in handy features and settings that help make your job of server administration easier.", - "image": "quay.io\/pterodactyl\/core:java-glibc", - "startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}", - "config": { - "files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"enable-query\": \"true\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}", - "startup": "{\r\n \"done\": \")! For help, type \",\r\n \"userInteraction\": [\r\n \"Go to eula.txt for more info.\"\r\n ]\r\n}", - "logs": "{\r\n \"custom\": false,\r\n \"location\": \"logs\/latest.log\"\r\n}", - "stop": "stop" - }, - "scripts": { - "installation": { - "script": "#!\/bin\/ash\r\n# Spigot Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\n## Only download if a path is provided, otherwise continue.\r\nif [ ! -z \"${DL_PATH}\" ]; then\r\n apk update\r\n apk add curl\r\n\r\n cd \/mnt\/server\r\n\r\n MODIFIED_DOWNLOAD=`eval echo $(echo ${DL_PATH} | sed -e 's\/{{\/${\/g' -e 's\/}}\/}\/g')`\r\n curl -sSL -o ${SERVER_JARFILE} ${MODIFIED_DOWNLOAD}\r\nelse\r\n apk add --no-cache curl git openjdk8 openssl bash\r\n \r\n cd \/srv\/\r\n \r\n wget https:\/\/hub.spigotmc.org\/jenkins\/job\/BuildTools\/lastSuccessfulBuild\/artifact\/target\/BuildTools.jar\r\n \r\n mv BuildTools.jar \/srv\/\r\n\r\n java -jar BuildTools.jar --rev ${DL_VERSION}\r\n\r\n mv spigot-*.jar \/mnt\/server\/${SERVER_JARFILE}\r\nfi", - "container": "alpine:3.7", - "entrypoint": "ash" - } - }, - "variables": [ - { - "name": "Server Jar File", - "description": "The name of the server jarfile to run the server with.", - "env_variable": "SERVER_JARFILE", - "default_value": "server.jar", - "user_viewable": 1, - "user_editable": 1, - "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" - }, - { - "name": "Spigot Version", - "description": "The version of Spigot to download (using the --rev tag). Use \"latest\" for latest.", - "env_variable": "DL_VERSION", - "default_value": "latest", - "user_viewable": 1, - "user_editable": 1, - "rules": "required|string|between:3,7" - }, - { - "name": "Download Path", - "description": "A URL to use to download Spigot rather than building it on the server. This is not user viewable. Use {{DL_VERSION}}<\/code> in the URL to automatically insert the assigned version into the URL. If you do not enter a URL Spigot will build directly in the container (this will fail on low memory containers).", - "env_variable": "DL_PATH", - "default_value": "", - "user_viewable": 0, - "user_editable": 0, - "rules": "nullable|string" - } - ] -} diff --git a/database/seeds/eggs/minecraft/egg-sponge--sponge-vanilla.json b/database/seeds/eggs/minecraft/egg-sponge--sponge-vanilla.json index 489100284..2bbfba23a 100644 --- a/database/seeds/eggs/minecraft/egg-sponge--sponge-vanilla.json +++ b/database/seeds/eggs/minecraft/egg-sponge--sponge-vanilla.json @@ -18,7 +18,7 @@ "scripts": { "installation": { "script": "#!\/bin\/ash\n# Sponge Installation Script\n#\n# Server Files: \/mnt\/server\n\napk update\napk add curl\n\ncd \/mnt\/server\n\ncurl -sSL \"https:\/\/repo.spongepowered.org\/maven\/org\/spongepowered\/spongevanilla\/${SPONGE_VERSION}\/spongevanilla-${SPONGE_VERSION}.jar\" -o ${SERVER_JARFILE}", - "container": "alpine:3.4", + "container": "alpine:3.9", "entrypoint": "ash" } }, @@ -42,4 +42,4 @@ "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" } ] -} \ No newline at end of file +} diff --git a/database/seeds/eggs/minecraft/egg-vanilla-minecraft.json b/database/seeds/eggs/minecraft/egg-vanilla-minecraft.json index 3c71a6b2b..5937fc176 100644 --- a/database/seeds/eggs/minecraft/egg-vanilla-minecraft.json +++ b/database/seeds/eggs/minecraft/egg-vanilla-minecraft.json @@ -18,7 +18,7 @@ "scripts": { "installation": { "script": "#!\/bin\/ash\r\n# Vanilla MC Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napk update\r\napk add curl jq\r\n\r\ncd \/mnt\/server\r\n\r\nLATEST_VERSION=`curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq -r '.latest.release'`\r\n\r\nif [ -z \"$VANILLA_VERSION\" ] || [ \"$VANILLA_VERSION\" == \"latest\" ]; then\r\n MANIFEST_URL=$(curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq .versions | jq -r '.[] | select(.id == \"'$LATEST_VERSION'\") | .url')\r\nelse\r\n MANIFEST_URL=$(curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq .versions | jq -r '.[] | select(.id == \"'$VANILLA_VERSION'\") | .url')\r\nfi\r\n\r\nDOWNLOAD_URL=`curl $MANIFEST_URL | jq .downloads.server | jq -r '. | .url'`\r\n\r\ncurl -o ${SERVER_JARFILE} $DOWNLOAD_URL", - "container": "alpine:3.7", + "container": "alpine:3.9", "entrypoint": "ash" } }, diff --git a/database/seeds/eggs/rust/egg-rust.json b/database/seeds/eggs/rust/egg-rust.json index af9d0c18c..7d794cc89 100644 --- a/database/seeds/eggs/rust/egg-rust.json +++ b/database/seeds/eggs/rust/egg-rust.json @@ -8,7 +8,7 @@ "author": "support@pterodactyl.io", "description": "The only aim in Rust is to survive. To do this you will need to overcome struggles such as hunger, thirst and cold. Build a fire. Build a shelter. Kill animals for meat. Protect yourself from other players, and kill them for meat. Create alliances with other players and form a town. Do whatever it takes to survive.", "image": "quay.io\/pterodactyl\/core:rust", - "startup": ".\/RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.identity \"rust\" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \\\"{{HOSTNAME}}\\\" +server.level \\\"{{LEVEL}}\\\" +server.description \\\"{{DESCRIPTION}}\\\" +server.url \\\"{{SERVER_URL}}\\\" +server.headerimage \\\"{{SERVER_IMG}}\\\" +server.worldsize \\\"{{WORLD_SIZE}}\\\" +server.seed \\\"{{WORLD_SEED}}\\\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \\\"{{RCON_PASS}}\\\" {{ADDITIONAL_ARGS}}", + "startup": ".\/RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.identity \"rust\" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \\\"{{HOSTNAME}}\\\" +server.level \\\"{{LEVEL}}\\\" +server.description \\\"{{DESCRIPTION}}\\\" +server.url \\\"{{SERVER_URL}}\\\" +server.headerimage \\\"{{SERVER_IMG}}\\\" +server.worldsize \\\"{{WORLD_SIZE}}\\\" +server.seed \\\"{{WORLD_SEED}}\\\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \\\"{{RCON_PASS}}\\\" +server.saveinterval {{SAVEINTERVAL}} {{ADDITIONAL_ARGS}}", "config": { "files": "{}", "startup": "{\r\n \"done\": \"Server startup complete\",\r\n \"userInteraction\": []\r\n}", @@ -108,7 +108,7 @@ "name": "RCON Port", "description": "Port for RCON connections.", "env_variable": "RCON_PORT", - "default_value": "8401", + "default_value": "28016", "user_viewable": 1, "user_editable": 0, "rules": "required|integer" @@ -122,6 +122,15 @@ "user_editable": 1, "rules": "required|string|max:64" }, + { + "name": "Save Interval", + "description": "Sets the server’s auto-save interval in seconds.", + "env_variable": "SAVEINTERVAL", + "default_value": "60", + "user_viewable": 1, + "user_editable": 1, + "rules": "required|integer" + }, { "name": "Additional Arguments", "description": "Add additional startup parameters to the server.", @@ -132,4 +141,4 @@ "rules": "nullable|string" } ] -} \ No newline at end of file +} diff --git a/database/seeds/eggs/source-engine/egg-ark--survival-evolved.json b/database/seeds/eggs/source-engine/egg-ark--survival-evolved.json index 4b2ec6356..35028b20c 100644 --- a/database/seeds/eggs/source-engine/egg-ark--survival-evolved.json +++ b/database/seeds/eggs/source-engine/egg-ark--survival-evolved.json @@ -3,7 +3,7 @@ "meta": { "version": "PTDL_v1" }, - "exported_at": "2018-07-06T11:27:32+02:00", + "exported_at": "2019-02-13T12:30:54-05:00", "name": "Ark: Survival Evolved", "author": "support@pterodactyl.io", "description": "As a man or woman stranded, naked, freezing, and starving on the unforgiving shores of a mysterious island called ARK, use your skill and cunning to kill or tame and ride the plethora of leviathan dinosaurs and other primeval creatures roaming the land. Hunt, harvest resources, craft items, grow crops, research technologies, and build shelters to withstand the elements and store valuables, all while teaming up with (or preying upon) hundreds of other players to survive, dominate... and escape! \u2014 Gamepedia: ARK", @@ -11,7 +11,7 @@ "startup": ".\/ShooterGame\/Binaries\/Linux\/ShooterGameServer {{SERVER_MAP}}?listen?ServerPassword={{ARK_PASSWORD}}?ServerAdminPassword={{ARK_ADMIN_PASSWORD}}?Port={{SERVER_PORT}}?MaxPlayers={{SERVER_MAX_PLAYERS}}", "config": { "files": "{}", - "startup": "{\r\n \"done\": \"gameserver Steam ID\",\r\n \"userInteraction\": []\r\n}", + "startup": "{\r\n \"done\": \"Setting breakpad minidump AppID = 346110\",\r\n \"userInteraction\": []\r\n}", "logs": "{\r\n \"custom\": true,\r\n \"location\": \"logs\/latest.log\"\r\n}", "stop": "quit" }, @@ -60,4 +60,4 @@ "rules": "required|string|max:20" } ] -} +} \ No newline at end of file diff --git a/database/seeds/eggs/source-engine/egg-garrys-mod.json b/database/seeds/eggs/source-engine/egg-garrys-mod.json index 0bfdc0a07..c0e4f31b4 100644 --- a/database/seeds/eggs/source-engine/egg-garrys-mod.json +++ b/database/seeds/eggs/source-engine/egg-garrys-mod.json @@ -3,12 +3,12 @@ "meta": { "version": "PTDL_v1" }, - "exported_at": "2018-06-19T07:46:27-04:00", + "exported_at": "2019-02-16T14:20:52-05:00", "name": "Garrys Mod", "author": "support@pterodactyl.io", "description": "Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company, Facepunch Studios.", "image": "quay.io\/pterodactyl\/core:source", - "startup": ".\/srcds_run -game garrysmod -console -port {{SERVER_PORT}} +ip 0.0.0.0 +map {{SRCDS_MAP}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}}", + "startup": ".\/srcds_run -game garrysmod -console -port {{SERVER_PORT}} +ip 0.0.0.0 +map {{SRCDS_MAP}} +gamemode {{GAMEMODE}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}} +host_workshop_collection {{WORKSHOP_ID}} +maxplayers {{MAX_PLAYERS}} -tickrate {{TICKRATE}}", "config": { "files": "{}", "startup": "{\r\n \"done\": \"gameserver Steam ID\",\r\n \"userInteraction\": []\r\n}", @@ -17,7 +17,7 @@ }, "scripts": { "installation": { - "script": "#!\/bin\/bash\n# Garry's Mod Installation Script\n#\n# Server Files: \/mnt\/server\napt -y update\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\n\ncd \/tmp\ncurl -sSL -o steamcmd.tar.gz http:\/\/media.steampowered.com\/installer\/steamcmd_linux.tar.gz\n\nmkdir -p \/mnt\/server\/steamcmd\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\ncd \/mnt\/server\/steamcmd\n\n# SteamCMD fails otherwise for some reason, even running as root.\n# This is changed at the end of the install process anyways.\nchown -R root:root \/mnt\n\nexport HOME=\/mnt\/server\n.\/steamcmd.sh +login anonymous +force_install_dir \/mnt\/server +app_update 4020 +quit\n\nmkdir -p \/mnt\/server\/.steam\/sdk32\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so", + "script": "#!\/bin\/bash\r\n# Garry's Mod Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napt -y update\r\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\r\n\r\ncd \/tmp\r\ncurl -sSL -o steamcmd.tar.gz http:\/\/media.steampowered.com\/installer\/steamcmd_linux.tar.gz\r\n\r\nmkdir -p \/mnt\/server\/steamcmd\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\n\r\nexport HOME=\/mnt\/server\r\n.\/steamcmd.sh +login anonymous +force_install_dir \/mnt\/server +app_update 4020 +quit\r\n\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n# Creating needed default files for the game\r\ncd \/mnt\/server\/garrysmod\/lua\/autorun\/server\r\necho '\r\n-- Docs: https:\/\/wiki.garrysmod.com\/page\/resource\/AddWorkshop\r\n-- Place the ID of the workshop addon you want to be downloaded to people who join your server, not the collection ID\r\n-- Use https:\/\/beta.configcreator.com\/create\/gmod\/resources.lua to easily create a list based on your collection ID\r\n\r\nresource.AddWorkshop( \"\" )\r\n' > workshop.lua\r\n\r\ncd \/mnt\/server\/garrysmod\/cfg\r\necho '\r\n\/\/ Please do not set RCon in here, use the startup parameters.\r\n\r\nhostname\t\t\"New Gmod Server\"\r\nsv_password\t\t\"\"\r\nsv_loadingurl \"\"\r\n\r\n\/\/ Steam Server List Settings\r\nsv_region \"255\"\r\nsv_lan \"0\"\r\nsv_max_queries_sec_global \"30000\"\r\nsv_max_queries_window \"45\"\r\nsv_max_queries_sec \"5\"\r\n\r\n\/\/ Server Limits\r\nsbox_maxprops\t\t100\r\nsbox_maxragdolls\t5\r\nsbox_maxnpcs\t\t10\r\nsbox_maxballoons\t10\r\nsbox_maxeffects\t\t10\r\nsbox_maxdynamite\t10\r\nsbox_maxlamps\t\t10\r\nsbox_maxthrusters\t10\r\nsbox_maxwheels\t\t10\r\nsbox_maxhoverballs\t10\r\nsbox_maxvehicles\t20\r\nsbox_maxbuttons\t\t10\r\nsbox_maxsents\t\t20\r\nsbox_maxemitters\t5\r\nsbox_godmode\t\t0\r\nsbox_noclip\t\t 0\r\n\r\n\/\/ Network Settings - Please keep these set to default.\r\n\r\nsv_minrate\t\t75000\r\nsv_maxrate\t\t0\r\ngmod_physiterations\t2\r\nnet_splitpacket_maxrate\t45000\r\ndecalfrequency\t\t12 \r\n\r\n\/\/ Execute Ban Files - Please do not edit\r\nexec banned_ip.cfg \r\nexec banned_user.cfg \r\n\r\n\/\/ Add custom lines under here\r\n' > server.cfg", "container": "ubuntu:16.04", "entrypoint": "bash" } @@ -39,7 +39,7 @@ "default_value": "", "user_viewable": 1, "user_editable": 1, - "rules": "required|string|alpha_num|size:32" + "rules": "nullable|string|alpha_num|size:32" }, { "name": "Source AppID", @@ -49,6 +49,42 @@ "user_viewable": 0, "user_editable": 0, "rules": "required|string|max:20" + }, + { + "name": "Workshop ID", + "description": "The ID of your workshop collection (the numbers at the end of the URL)", + "env_variable": "WORKSHOP_ID", + "default_value": "", + "user_viewable": 1, + "user_editable": 1, + "rules": "nullable|integer" + }, + { + "name": "Gamemode", + "description": "The gamemode of your server.", + "env_variable": "GAMEMODE", + "default_value": "sandbox", + "user_viewable": 1, + "user_editable": 1, + "rules": "required|string" + }, + { + "name": "Max Players", + "description": "The maximum amount of players allowed on your game server.", + "env_variable": "MAX_PLAYERS", + "default_value": "32", + "user_viewable": 1, + "user_editable": 1, + "rules": "required|integer|max:128" + }, + { + "name": "Tickrate", + "description": "The tickrate defines how fast the server will update each entities location.", + "env_variable": "TICKRATE", + "default_value": "22", + "user_viewable": 1, + "user_editable": 1, + "rules": "required|integer|max:100" } ] } diff --git a/database/seeds/eggs/terraria/egg-terraria-server--t-shock.json b/database/seeds/eggs/terraria/egg-terraria-server--t-shock.json index 9be0f1142..9381890d1 100644 --- a/database/seeds/eggs/terraria/egg-terraria-server--t-shock.json +++ b/database/seeds/eggs/terraria/egg-terraria-server--t-shock.json @@ -18,7 +18,7 @@ "scripts": { "installation": { "script": "#!\/bin\/ash\n# TShock Installation Script\n#\n# Server Files: \/mnt\/server\napk update\napk add curl unzip\n\ncd \/tmp\n\ncurl -sSLO https:\/\/github.com\/NyxStudios\/TShock\/releases\/download\/v${T_VERSION}\/tshock_${T_VERSION}.zip\n\nunzip -o tshock_${T_VERSION}.zip -d \/mnt\/server", - "container": "alpine:3.4", + "container": "alpine:3.9", "entrypoint": "ash" } }, @@ -42,4 +42,4 @@ "rules": "required|numeric|digits_between:1,3" } ] -} \ No newline at end of file +} diff --git a/database/seeds/eggs/voice-servers/egg-mumble-server.json b/database/seeds/eggs/voice-servers/egg-mumble-server.json index 5e6a98e17..e62562597 100644 --- a/database/seeds/eggs/voice-servers/egg-mumble-server.json +++ b/database/seeds/eggs/voice-servers/egg-mumble-server.json @@ -18,7 +18,7 @@ "scripts": { "installation": { "script": "#!\/bin\/ash\n# Mumble Installation Script\n#\n# Server Files: \/mnt\/server\napk update\napk add tar curl\n\ncd \/tmp\n\ncurl -sSLO https:\/\/github.com\/mumble-voip\/mumble\/releases\/download\/${MUMBLE_VERSION}\/murmur-static_x86-${MUMBLE_VERSION}.tar.bz2\n\ntar -xjvf murmur-static_x86-${MUMBLE_VERSION}.tar.bz2\ncp -r murmur-static_x86-${MUMBLE_VERSION}\/* \/mnt\/server", - "container": "alpine:3.4", + "container": "alpine:3.9", "entrypoint": "ash" } }, @@ -42,4 +42,4 @@ "rules": "required|regex:\/^([0-9_\\.-]{5,8})$\/" } ] -} \ No newline at end of file +} diff --git a/database/seeds/eggs/voice-servers/egg-teamspeak3-server.json b/database/seeds/eggs/voice-servers/egg-teamspeak3-server.json index 7f89de4f2..e92562216 100644 --- a/database/seeds/eggs/voice-servers/egg-teamspeak3-server.json +++ b/database/seeds/eggs/voice-servers/egg-teamspeak3-server.json @@ -3,11 +3,11 @@ "meta": { "version": "PTDL_v1" }, - "exported_at": "2018-10-28T20:50:23+01:00", + "exported_at": "2019-05-01T16:35:15+00:00", "name": "Teamspeak3 Server", "author": "support@pterodactyl.io", "description": "VoIP software designed with security in mind, featuring crystal clear voice quality, endless customization options, and scalabilty up to thousands of simultaneous users.", - "image": "quay.io\/pterodactyl\/core:glibc", + "image": "quay.io/parkervcp/pterodactyl-images:base_debian", "startup": ".\/ts3server_minimal_runscript.sh default_voice_port={{SERVER_PORT}} query_port={{SERVER_PORT}} license_accepted=1", "config": { "files": "{}", @@ -18,7 +18,7 @@ "scripts": { "installation": { "script": "#!\/bin\/ash\n# TS3 Installation Script\n#\n# Server Files: \/mnt\/server\napk update\napk add tar curl\n\ncd \/mnt\/server\n\ncurl http:\/\/dl.4players.de\/ts\/releases\/${TS_VERSION}\/teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2 | tar xj --strip-components=1", - "container": "alpine:3.4", + "container": "alpine:3.9", "entrypoint": "ash" } }, @@ -27,10 +27,10 @@ "name": "Server Version", "description": "The version of Teamspeak 3 to use when running the server.", "env_variable": "TS_VERSION", - "default_value": "3.5.0", + "default_value": "3.7.1", "user_viewable": 1, "user_editable": 1, "rules": "required|regex:\/^([0-9_\\.-]{5,10})$\/" } ] -} +} \ No newline at end of file diff --git a/package.json b/package.json index 2a9697409..591ae3f8c 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,93 @@ { - "name": "pterodactyl-panel", - "devDependencies": { - "babel-cli": "6.18.0", - "babel-plugin-transform-strict-mode": "^6.18.0", - "babel-preset-es2015": "6.18.0" - }, - "scripts": { - "build": "./node_modules/babel-cli/bin/babel.js public/themes/pterodactyl/js/frontend/files/src --source-maps --out-file public/themes/pterodactyl/js/frontend/files/filemanager.min.js" - } + "name": "pterodactyl-panel", + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^1.2.19", + "@fortawesome/free-solid-svg-icons": "^5.9.0", + "@fortawesome/react-fontawesome": "^0.1.4", + "@hot-loader/react-dom": "^16.8.6", + "axios": "^0.18.0", + "brace": "^0.11.1", + "classnames": "^2.2.6", + "date-fns": "^1.29.0", + "easy-peasy": "^2.5.0", + "events": "^3.0.0", + "feather-icons": "^4.10.0", + "formik": "^1.5.7", + "jquery": "^3.3.1", + "lodash": "^4.17.11", + "query-string": "^6.7.0", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "react-hot-loader": "^4.9.0", + "react-redux": "^7.1.0", + "react-router-dom": "^5.0.1", + "react-transition-group": "^4.1.0", + "socket.io-client": "^2.2.0", + "sockette": "^2.0.6", + "use-react-router": "^1.0.7", + "ws-wrapper": "^2.0.0", + "xterm": "^3.14.4", + "xterm-addon-attach": "^0.1.0", + "xterm-addon-fit": "^0.1.0", + "yup": "^0.27.0" + }, + "devDependencies": { + "@babel/core": "^7.2.2", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/preset-env": "^7.3.1", + "@babel/preset-react": "^7.0.0", + "@types/classnames": "^2.2.8", + "@types/events": "^3.0.0", + "@types/feather-icons": "^4.7.0", + "@types/lodash": "^4.14.119", + "@types/query-string": "^6.3.0", + "@types/react": "^16.8.19", + "@types/react-dom": "^16.8.4", + "@types/react-redux": "^7.1.1", + "@types/react-router-dom": "^4.3.3", + "@types/react-transition-group": "^2.9.2", + "@types/webpack-env": "^1.13.6", + "@types/yup": "^0.26.17", + "@typescript-eslint/eslint-plugin": "^1.10.1", + "@typescript-eslint/parser": "^1.10.1", + "babel-loader": "^8.0.5", + "css-loader": "^2.1.0", + "cssnano": "^4.0.3", + "eslint": "^5.16.0", + "eslint-config-standard": "^12.0.0", + "eslint-plugin-import": "^2.17.3", + "eslint-plugin-node": "^9.1.0", + "eslint-plugin-promise": "^4.1.1", + "eslint-plugin-standard": "^4.0.0", + "fork-ts-checker-webpack-plugin": "^0.5.2", + "glob-all": "^3.1.0", + "html-webpack-plugin": "^3.2.0", + "mini-css-extract-plugin": "^0.5.0", + "postcss": "^6.0.21", + "postcss-import": "^11.1.0", + "postcss-loader": "^3.0.0", + "postcss-preset-env": "^3.4.0", + "precss": "^3.1.2", + "purgecss-webpack-plugin": "^1.1.0", + "resolve-url-loader": "^3.0.0", + "source-map-loader": "^0.2.4", + "style-loader": "^0.23.1", + "tailwindcss": "^0.7.4", + "terser-webpack-plugin": "^1.3.0", + "ts-loader": "^5.3.3", + "typescript": "^3.3.1", + "webpack": "^4.29.0", + "webpack-assets-manifest": "^3.1.1", + "webpack-cli": "^3.0.2", + "webpack-dev-server": "^3.1.14", + "webpack-manifest-plugin": "^2.0.3" + }, + "scripts": { + "clean": "rm -rf public/assets/*.js && rm -rf public/assets/*.css", + "watch": "NODE_ENV=development ./node_modules/.bin/webpack --watch --progress", + "build": "NODE_ENV=development ./node_modules/.bin/webpack --progress", + "build:production": "NODE_ENV=production ./node_modules/.bin/webpack", + "serve": "yarn run clean && NODE_ENV=development webpack-dev-server --host 0.0.0.0 --hot", + "v:serve": "PUBLIC_PATH=https://pterodactyl.test:8080 yarn run serve --https --key /etc/ssl/private/pterodactyl.test-key.pem --cert /etc/ssl/private/pterodactyl.test.pem" + } } diff --git a/phpunit.dusk.xml b/phpunit.dusk.xml new file mode 100644 index 000000000..60392c932 --- /dev/null +++ b/phpunit.dusk.xml @@ -0,0 +1,21 @@ + + + + + ./tests/Browser + + + + + ./app + + + diff --git a/phpunit.xml b/phpunit.xml index c0a7cf837..e4e18e417 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,6 +10,9 @@ processIsolation="false" stopOnFailure="false"> + + ./tests/Browser/Processes + ./tests/Integration diff --git a/public/assets/pterodactyl.svg b/public/assets/pterodactyl.svg new file mode 100755 index 000000000..f3582adf2 --- /dev/null +++ b/public/assets/pterodactyl.svg @@ -0,0 +1 @@ +Artboard 1 \ No newline at end of file diff --git a/public/favicons/android-icon-144x144.png b/public/favicons/android-icon-144x144.png new file mode 100644 index 000000000..7e98c1c30 Binary files /dev/null and b/public/favicons/android-icon-144x144.png differ diff --git a/public/favicons/android-icon-192x192.png b/public/favicons/android-icon-192x192.png new file mode 100644 index 000000000..80ce4d33f Binary files /dev/null and b/public/favicons/android-icon-192x192.png differ diff --git a/public/favicons/android-icon-36x36.png b/public/favicons/android-icon-36x36.png new file mode 100644 index 000000000..729e21269 Binary files /dev/null and b/public/favicons/android-icon-36x36.png differ diff --git a/public/favicons/android-icon-48x48.png b/public/favicons/android-icon-48x48.png new file mode 100644 index 000000000..9e5fe2657 Binary files /dev/null and b/public/favicons/android-icon-48x48.png differ diff --git a/public/favicons/android-icon-72x72.png b/public/favicons/android-icon-72x72.png new file mode 100644 index 000000000..e6bb769c8 Binary files /dev/null and b/public/favicons/android-icon-72x72.png differ diff --git a/public/favicons/android-icon-96x96.png b/public/favicons/android-icon-96x96.png new file mode 100644 index 000000000..f9b8fbefb Binary files /dev/null and b/public/favicons/android-icon-96x96.png differ diff --git a/public/favicons/apple-icon-114x114.png b/public/favicons/apple-icon-114x114.png new file mode 100644 index 000000000..3920011b1 Binary files /dev/null and b/public/favicons/apple-icon-114x114.png differ diff --git a/public/favicons/apple-icon-120x120.png b/public/favicons/apple-icon-120x120.png new file mode 100644 index 000000000..81b72ba81 Binary files /dev/null and b/public/favicons/apple-icon-120x120.png differ diff --git a/public/favicons/apple-icon-144x144.png b/public/favicons/apple-icon-144x144.png new file mode 100644 index 000000000..7e98c1c30 Binary files /dev/null and b/public/favicons/apple-icon-144x144.png differ diff --git a/public/favicons/apple-icon-152x152.png b/public/favicons/apple-icon-152x152.png new file mode 100644 index 000000000..6623bd585 Binary files /dev/null and b/public/favicons/apple-icon-152x152.png differ diff --git a/public/favicons/apple-icon-180x180.png b/public/favicons/apple-icon-180x180.png new file mode 100644 index 000000000..0503d522b Binary files /dev/null and b/public/favicons/apple-icon-180x180.png differ diff --git a/public/favicons/apple-icon-57x57.png b/public/favicons/apple-icon-57x57.png new file mode 100644 index 000000000..466878ee1 Binary files /dev/null and b/public/favicons/apple-icon-57x57.png differ diff --git a/public/favicons/apple-icon-60x60.png b/public/favicons/apple-icon-60x60.png new file mode 100644 index 000000000..09a79d997 Binary files /dev/null and b/public/favicons/apple-icon-60x60.png differ diff --git a/public/favicons/apple-icon-72x72.png b/public/favicons/apple-icon-72x72.png new file mode 100644 index 000000000..e6bb769c8 Binary files /dev/null and b/public/favicons/apple-icon-72x72.png differ diff --git a/public/favicons/apple-icon-76x76.png b/public/favicons/apple-icon-76x76.png new file mode 100644 index 000000000..f656479b8 Binary files /dev/null and b/public/favicons/apple-icon-76x76.png differ diff --git a/public/favicons/apple-icon-precomposed.png b/public/favicons/apple-icon-precomposed.png new file mode 100644 index 000000000..277e51bf0 Binary files /dev/null and b/public/favicons/apple-icon-precomposed.png differ diff --git a/public/favicons/apple-icon.png b/public/favicons/apple-icon.png new file mode 100644 index 000000000..277e51bf0 Binary files /dev/null and b/public/favicons/apple-icon.png differ diff --git a/public/favicons/browserconfig.xml b/public/favicons/browserconfig.xml index e3cb776ee..c55414822 100644 --- a/public/favicons/browserconfig.xml +++ b/public/favicons/browserconfig.xml @@ -1,9 +1,2 @@ - - - - - #165ed4 - - - +#ffffff \ No newline at end of file diff --git a/public/favicons/df4b367461890fa5fd0d9339d3c3f9c6.ico.zip b/public/favicons/df4b367461890fa5fd0d9339d3c3f9c6.ico.zip new file mode 100644 index 000000000..8d2efae04 Binary files /dev/null and b/public/favicons/df4b367461890fa5fd0d9339d3c3f9c6.ico.zip differ diff --git a/public/favicons/favicon-16x16.png b/public/favicons/favicon-16x16.png index d568bd20a..9597c6f36 100644 Binary files a/public/favicons/favicon-16x16.png and b/public/favicons/favicon-16x16.png differ diff --git a/public/favicons/favicon-32x32.png b/public/favicons/favicon-32x32.png index edfd13405..737917e1a 100644 Binary files a/public/favicons/favicon-32x32.png and b/public/favicons/favicon-32x32.png differ diff --git a/public/favicons/favicon-96x96.png b/public/favicons/favicon-96x96.png new file mode 100644 index 000000000..f9b8fbefb Binary files /dev/null and b/public/favicons/favicon-96x96.png differ diff --git a/public/favicons/favicon.ico b/public/favicons/favicon.ico index 99e5bffe2..bc2228831 100644 Binary files a/public/favicons/favicon.ico and b/public/favicons/favicon.ico differ diff --git a/public/favicons/manifest.json b/public/favicons/manifest.json index 796d1ff0c..013d4a6a5 100644 --- a/public/favicons/manifest.json +++ b/public/favicons/manifest.json @@ -1,18 +1,41 @@ { - "name": "", - "icons": [ - { - "src": "favicons/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "favicons/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" + "name": "App", + "icons": [ + { + "src": "\/android-icon-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": "0.75" + }, + { + "src": "\/android-icon-48x48.png", + "sizes": "48x48", + "type": "image\/png", + "density": "1.0" + }, + { + "src": "\/android-icon-72x72.png", + "sizes": "72x72", + "type": "image\/png", + "density": "1.5" + }, + { + "src": "\/android-icon-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": "2.0" + }, + { + "src": "\/android-icon-144x144.png", + "sizes": "144x144", + "type": "image\/png", + "density": "3.0" + }, + { + "src": "\/android-icon-192x192.png", + "sizes": "192x192", + "type": "image\/png", + "density": "4.0" + } + ] } \ No newline at end of file diff --git a/public/favicons/ms-icon-144x144.png b/public/favicons/ms-icon-144x144.png new file mode 100644 index 000000000..7e98c1c30 Binary files /dev/null and b/public/favicons/ms-icon-144x144.png differ diff --git a/public/favicons/ms-icon-150x150.png b/public/favicons/ms-icon-150x150.png new file mode 100644 index 000000000..cdd392c86 Binary files /dev/null and b/public/favicons/ms-icon-150x150.png differ diff --git a/public/favicons/ms-icon-310x310.png b/public/favicons/ms-icon-310x310.png new file mode 100644 index 000000000..7b84fc129 Binary files /dev/null and b/public/favicons/ms-icon-310x310.png differ diff --git a/public/favicons/ms-icon-70x70.png b/public/favicons/ms-icon-70x70.png new file mode 100644 index 000000000..383bb7675 Binary files /dev/null and b/public/favicons/ms-icon-70x70.png differ diff --git a/public/themes/pterodactyl/js/frontend/console.js b/public/themes/pterodactyl/js/frontend/console.js index 8943da42a..fc62ac024 100644 --- a/public/themes/pterodactyl/js/frontend/console.js +++ b/public/themes/pterodactyl/js/frontend/console.js @@ -65,7 +65,7 @@ $(document).ready(function () { if (e.which === 13) { saveToHistory($(this).val()); - Socket.emit((ConsoleServerStatus !== 0) ? 'send command' : 'set status', $(this).val()); + Socket.emit((ConsoleServerStatus !== 0) ? 'send command' : 'set state', $(this).val()); $(this).val(''); } @@ -233,7 +233,7 @@ function updateServerPowerControls (data) { $(document).ready(function () { $('[data-attr="power"]').click(function (event) { if (! $(this).hasClass('disabled')) { - Socket.emit('set status', $(this).data('action')); + Socket.emit('set state', $(this).data('action')); } }); @@ -255,6 +255,31 @@ $(document).ready(function () { TimeLabels.push($.format.date(new Date(), 'HH:mm:ss')); + + // memory.cmax is the maximum given by the container + // memory.amax is given by the json config + // use the maximum of both + // with no limit memory.cmax will always be higher + // but with limit memory.amax is sometimes still smaller than memory.total + MemoryChart.config.options.scales.yAxes[0].ticks.max = Math.max(proc.data.memory.cmax, proc.data.memory.amax) / (1000 * 1000); + + if (Pterodactyl.server.cpu > 0) { + // if there is a cpu limit defined use 100% as maximum + CPUChart.config.options.scales.yAxes[0].ticks.max = 100; + } else { + // if there is no cpu limit defined use linux percentage + // and find maximum in all values + var maxCpu = 1; + for(var i = 0; i < CPUData.length; i++) { + maxCpu = Math.max(maxCpu, parseFloat(CPUData[i])) + } + + maxCpu = Math.ceil(maxCpu / 100) * 100; + CPUChart.config.options.scales.yAxes[0].ticks.max = maxCpu; + } + + + CPUChart.update(); MemoryChart.update(); }); @@ -301,6 +326,13 @@ $(document).ready(function () { }, animation: { duration: 1, + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] } } }); @@ -346,6 +378,13 @@ $(document).ready(function () { }, animation: { duration: 1, + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] } } }); diff --git a/public/themes/pterodactyl/js/frontend/files/filemanager.min.js b/public/themes/pterodactyl/js/frontend/files/filemanager.min.js index eeb7370b3..67f9a8900 100644 --- a/public/themes/pterodactyl/js/frontend/files/filemanager.min.js +++ b/public/themes/pterodactyl/js/frontend/files/filemanager.min.js @@ -1,5 +1,5 @@ -'use strict';var _createClass=function(){function defineProperties(target,props){for(var i=0;i\n \n ';nameBlock.html(attachEditor);var inputField=nameBlock.find('input');var inputLoader=nameBlock.find('.input-loader');inputField.focus();inputField.on('blur keydown',function(e){if(e.type==='keydown'&&e.which===27||e.type==='blur'||e.type==='keydown'&&e.which===13&¤tName===inputField.val()){if(!_.isEmpty(currentLink)){nameBlock.html(currentLink)}else{nameBlock.html(currentName)}inputField.remove();ContextMenu.unbind().run();return}if(e.type==='keydown'&&e.which!==13)return;inputLoader.show();var currentPath=decodeURIComponent(nameBlock.data('path'));$.ajax({type:'POST',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/rename',timeout:10000,data:JSON.stringify({from:''+currentPath+currentName,to:''+currentPath+inputField.val()})}).done(function(data){nameBlock.attr('data-name',inputField.val());if(!_.isEmpty(currentLink)){var newLink=currentLink.attr('href');if(nameBlock.parent().data('type')!=='folder'){newLink=newLink.substr(0,newLink.lastIndexOf('/'))+'/'+inputField.val()}currentLink.attr('href',newLink);nameBlock.html(currentLink.html(inputField.val()))}else{nameBlock.html(inputField.val())}inputField.remove()}).fail(function(jqXHR){console.error(jqXHR);var error='An error occurred while trying to process this request.';if(typeof jqXHR.responseJSON!=='undefined'&&typeof jqXHR.responseJSON.error!=='undefined'){error=jqXHR.responseJSON.error}nameBlock.addClass('has-error').delay(2000).queue(function(){nameBlock.removeClass('has-error').dequeue()});inputField.popover({animation:true,placement:'top',content:error,title:'Save Error'}).popover('show')}).always(function(){inputLoader.remove();ContextMenu.unbind().run()})})}},{key:'copy',value:function copy(){var nameBlock=$(this.element).find('td[data-identifier="name"]');var currentName=decodeURIComponent(nameBlock.attr('data-name'));var currentPath=decodeURIComponent(nameBlock.data('path'));swal({type:'input',title:'Copy File',text:'Please enter the new path for the copied file below.',showCancelButton:true,showConfirmButton:true,closeOnConfirm:false,showLoaderOnConfirm:true,inputValue:''+currentPath+currentName},function(val){if(val===false){return false}$.ajax({type:'POST',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/copy',timeout:10000,data:JSON.stringify({from:''+currentPath+currentName,to:''+val})}).done(function(data){swal({type:'success',title:'',text:'File successfully copied.'});Files.list()}).fail(function(jqXHR){console.error(jqXHR);var error='An error occurred while trying to process this request.';if(typeof jqXHR.responseJSON!=='undefined'&&typeof jqXHR.responseJSON.error!=='undefined'){error=jqXHR.responseJSON.error}swal({type:'error',title:'',text:error})})})}},{key:'download',value:function download(){var nameBlock=$(this.element).find('td[data-identifier="name"]');var fileName=decodeURIComponent(nameBlock.attr('data-name'));var filePath=decodeURIComponent(nameBlock.data('path'));window.location='/server/'+Pterodactyl.server.uuidShort+'/files/download/'+filePath+fileName}},{key:'delete',value:function _delete(){var nameBlock=$(this.element).find('td[data-identifier="name"]');var delPath=decodeURIComponent(nameBlock.data('path'));var delName=decodeURIComponent(nameBlock.data('name'));swal({type:'warning',title:'',text:'Are you sure you want to delete '+delName+'?',html:true,showCancelButton:true,showConfirmButton:true,closeOnConfirm:false,showLoaderOnConfirm:true},function(){$.ajax({type:'POST',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/delete',timeout:10000,data:JSON.stringify({items:[''+delPath+delName]})}).done(function(data){nameBlock.parent().addClass('warning').delay(200).fadeOut();swal({type:'success',title:'File Deleted'})}).fail(function(jqXHR){console.error(jqXHR);swal({type:'error',title:'Whoops!',html:true,text:'An error occurred while attempting to delete this file. Please try again.'})})})}},{key:'toggleMassActions',value:function toggleMassActions(){if($('#file_listing input[type="checkbox"]:checked').length){$('#mass_actions').removeClass('disabled')}else{$('#mass_actions').addClass('disabled')}}},{key:'toggleHighlight',value:function toggleHighlight(event){var parent=$(event.currentTarget);var item=$(event.currentTarget).find('input');if($(item).is(':checked')){$(item).prop('checked',false);parent.removeClass('warning').delay(200)}else{$(item).prop('checked',true);parent.addClass('warning').delay(200)}}},{key:'highlightAll',value:function highlightAll(event){var parent=void 0;var item=$(event.currentTarget).find('input');if($(item).is(':checked')){$('#file_listing input[type=checkbox]').prop('checked',false);$('#file_listing input[data-action="addSelection"]').each(function(){parent=$(this).closest('tr');parent.removeClass('warning').delay(200)})}else{$('#file_listing input[type=checkbox]').prop('checked',true);$('#file_listing input[data-action="addSelection"]').each(function(){parent=$(this).closest('tr');parent.addClass('warning').delay(200)})}}},{key:'deleteSelected',value:function deleteSelected(){var selectedItems=[];var selectedItemsElements=[];var parent=void 0;var nameBlock=void 0;var delLocation=void 0;$('#file_listing input[data-action="addSelection"]:checked').each(function(){parent=$(this).closest('tr');nameBlock=$(parent).find('td[data-identifier="name"]');delLocation=decodeURIComponent(nameBlock.data('path'))+decodeURIComponent(nameBlock.data('name'));selectedItems.push(delLocation);selectedItemsElements.push(parent)});if(selectedItems.length!=0){var formattedItems='';var i=0;$.each(selectedItems,function(key,value){formattedItems+=''+value+', ';i++;return i<5});formattedItems=formattedItems.slice(0,-2);if(selectedItems.length>5){formattedItems+=', and '+(selectedItems.length-5)+' other(s)'}swal({type:'warning',title:'',text:'Are you sure you want to delete the following files: '+formattedItems+'?',html:true,showCancelButton:true,showConfirmButton:true,closeOnConfirm:false,showLoaderOnConfirm:true},function(){$.ajax({type:'POST',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/delete',timeout:10000,data:JSON.stringify({items:selectedItems})}).done(function(data){$('#file_listing input:checked').each(function(){$(this).prop('checked',false)});$.each(selectedItemsElements,function(){$(this).addClass('warning').delay(200).fadeOut()});swal({type:'success',title:'Files Deleted'})}).fail(function(jqXHR){console.error(jqXHR);swal({type:'error',title:'Whoops!',html:true,text:'An error occurred while attempting to delete these files. Please try again.'})})})}else{swal({type:'warning',title:'',text:'Please select files/folders to delete.'})}}},{key:'decompress',value:function decompress(){var nameBlock=$(this.element).find('td[data-identifier="name"]');var compPath=decodeURIComponent(nameBlock.data('path'));var compName=decodeURIComponent(nameBlock.data('name'));swal({title:' Decompressing...',text:'This might take a few seconds to complete.',html:true,allowOutsideClick:false,allowEscapeKey:false,showConfirmButton:false});$.ajax({type:'POST',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/decompress',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',data:JSON.stringify({files:''+compPath+compName})}).done(function(data){swal.close();Files.list(compPath)}).fail(function(jqXHR){console.error(jqXHR);var error='An error occurred while trying to process this request.';if(typeof jqXHR.responseJSON!=='undefined'&&typeof jqXHR.responseJSON.error!=='undefined'){error=jqXHR.responseJSON.error}swal({type:'error',title:'Whoops!',html:true,text:error})})}},{key:'compress',value:function compress(){var nameBlock=$(this.element).find('td[data-identifier="name"]');var compPath=decodeURIComponent(nameBlock.data('path'));var compName=decodeURIComponent(nameBlock.data('name'));$.ajax({type:'POST',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/compress',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',data:JSON.stringify({files:''+compPath+compName,to:compPath.toString()})}).done(function(data){Files.list(compPath,function(err){if(err)return;var fileListing=$('#file_listing').find('[data-name="'+data.saved_as+'"]').parent();fileListing.addClass('success pulsate').delay(3000).queue(function(){fileListing.removeClass('success pulsate').dequeue()})})}).fail(function(jqXHR){console.error(jqXHR);var error='An error occurred while trying to process this request.';if(typeof jqXHR.responseJSON!=='undefined'&&typeof jqXHR.responseJSON.error!=='undefined'){error=jqXHR.responseJSON.error}swal({type:'error',title:'Whoops!',html:true,text:error})})}}]);return ActionsClass}(); -'use strict';var _createClass=function(){function defineProperties(target,props){for(var i=0;i New File
  • New Folder
  • '}if(Pterodactyl.permissions.downloadFiles||Pterodactyl.permissions.deleteFiles){buildMenu+='
  • '}if(Pterodactyl.permissions.downloadFiles){buildMenu+=''}if(Pterodactyl.permissions.deleteFiles){buildMenu+='
  • Delete
  • '}buildMenu+='';return buildMenu}},{key:'rightClick',value:function rightClick(){var _this=this;$('[data-action="toggleMenu"]').on('mousedown',function(event){event.preventDefault();if($(document).find('#fileOptionMenu').is(':visible')){$('body').trigger('click');return}_this.showMenu(event)});$('#file_listing > tbody td').on('contextmenu',function(event){_this.showMenu(event)})}},{key:'showMenu',value:function showMenu(event){var _this2=this;var parent=$(event.target).closest('tr');var menu=$(this.makeMenu(parent));if(parent.data('type')==='disabled')return;event.preventDefault();$(menu).appendTo('body');$(menu).data('invokedOn',$(event.target)).show().css({position:'absolute',left:event.pageX-150,top:event.pageY});this.activeLine=parent;this.activeLine.addClass('active');var Actions=new ActionsClass(parent,menu);if(Pterodactyl.permissions.moveFiles){$(menu).find('li[data-action="move"]').unbind().on('click',function(e){e.preventDefault();Actions.move()});$(menu).find('li[data-action="rename"]').unbind().on('click',function(e){e.preventDefault();Actions.rename()})}if(Pterodactyl.permissions.copyFiles){$(menu).find('li[data-action="copy"]').unbind().on('click',function(e){e.preventDefault();Actions.copy()})}if(Pterodactyl.permissions.compressFiles){if(parent.data('type')==='folder'){$(menu).find('li[data-action="compress"]').removeClass('hidden')}$(menu).find('li[data-action="compress"]').unbind().on('click',function(e){e.preventDefault();Actions.compress()})}if(Pterodactyl.permissions.decompressFiles){if(_.without(['application/zip','application/gzip','application/x-gzip'],parent.data('mime')).length<3){$(menu).find('li[data-action="decompress"]').removeClass('hidden')}$(menu).find('li[data-action="decompress"]').unbind().on('click',function(e){e.preventDefault();Actions.decompress()})}if(Pterodactyl.permissions.createFiles){$(menu).find('li[data-action="folder"]').unbind().on('click',function(e){e.preventDefault();Actions.folder()})}if(Pterodactyl.permissions.downloadFiles){if(parent.data('type')==='file'){$(menu).find('li[data-action="download"]').removeClass('hidden')}$(menu).find('li[data-action="download"]').unbind().on('click',function(e){e.preventDefault();Actions.download()})}if(Pterodactyl.permissions.deleteFiles){$(menu).find('li[data-action="delete"]').unbind().on('click',function(e){e.preventDefault();Actions.delete()})}$(window).unbind().on('click',function(event){if($(event.target).is('.disable-menu-hide')){event.preventDefault();return}$(menu).unbind().remove();if(!_.isNull(_this2.activeLine))_this2.activeLine.removeClass('active')})}},{key:'directoryClick',value:function directoryClick(){$('a[data-action="directory-view"]').on('click',function(event){event.preventDefault();var path=$(this).parent().data('path')||'';var name=$(this).parent().data('name')||'';window.location.hash=encodeURIComponent(path+name);Files.list()})}}]);return ContextMenuClass}();window.ContextMenu=new ContextMenuClass; +'use strict';var _createClass=function(){function defineProperties(target,props){for(var i=0;i').text(value).html()}},{key:'folder',value:function folder(path){var inputValue=void 0;if(path){inputValue=path}else{var nameBlock=$(this.element).find('td[data-identifier="name"]');var currentName=decodeURIComponent(nameBlock.data('name'));var currentPath=decodeURIComponent(nameBlock.data('path'));if($(this.element).data('type')==='file'){inputValue=currentPath}else{inputValue=''+currentPath+currentName+'/'}}swal({type:'input',title:'Create Folder',text:'Please enter the path and folder name below.',showCancelButton:true,showConfirmButton:true,closeOnConfirm:false,showLoaderOnConfirm:true,inputValue:inputValue},function(val){if(val===false){return false}$.ajax({type:'POST',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/folder',timeout:10000,data:JSON.stringify({path:val})}).done(function(data){swal.close();Files.list()}).fail(function(jqXHR){console.error(jqXHR);var error='An error occurred while trying to process this request.';if(typeof jqXHR.responseJSON!=='undefined'&&typeof jqXHR.responseJSON.error!=='undefined'){error=jqXHR.responseJSON.error}swal({type:'error',title:'',text:error})})})}},{key:'move',value:function move(){var nameBlock=$(this.element).find('td[data-identifier="name"]');var currentName=decodeURIComponent(nameBlock.attr('data-name'));var currentPath=decodeURIComponent(nameBlock.data('path'));swal({type:'input',title:'Move File',text:'Please enter the new path for the file below.',showCancelButton:true,showConfirmButton:true,closeOnConfirm:false,showLoaderOnConfirm:true,inputValue:''+currentPath+currentName},function(val){if(val===false){return false}$.ajax({type:'POST',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/move',timeout:10000,data:JSON.stringify({from:''+currentPath+currentName,to:''+val})}).done(function(data){nameBlock.parent().addClass('warning').delay(200).fadeOut();swal.close()}).fail(function(jqXHR){console.error(jqXHR);var error='An error occurred while trying to process this request.';if(typeof jqXHR.responseJSON!=='undefined'&&typeof jqXHR.responseJSON.error!=='undefined'){error=jqXHR.responseJSON.error}swal({type:'error',title:'',text:error})})})}},{key:'rename',value:function rename(){var nameBlock=$(this.element).find('td[data-identifier="name"]');var currentLink=nameBlock.find('a');var currentName=decodeURIComponent(nameBlock.attr('data-name'));var attachEditor='\n \n \n ';nameBlock.html(attachEditor);var inputField=nameBlock.find('input');var inputLoader=nameBlock.find('.input-loader');inputField.focus();inputField.on('blur keydown',function(e){if(e.type==='keydown'&&e.which===27||e.type==='blur'||e.type==='keydown'&&e.which===13&¤tName===inputField.val()){if(!_.isEmpty(currentLink)){nameBlock.html(currentLink)}else{nameBlock.html(currentName)}inputField.remove();ContextMenu.unbind().run();return}if(e.type==='keydown'&&e.which!==13)return;inputLoader.show();var currentPath=decodeURIComponent(nameBlock.data('path'));$.ajax({type:'POST',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/rename',timeout:10000,data:JSON.stringify({from:''+currentPath+currentName,to:''+currentPath+inputField.val()})}).done(function(data){nameBlock.attr('data-name',inputField.val());if(!_.isEmpty(currentLink)){var newLink=currentLink.attr('href');if(nameBlock.parent().data('type')!=='folder'){newLink=newLink.substr(0,newLink.lastIndexOf('/'))+'/'+inputField.val()}currentLink.attr('href',newLink);nameBlock.html(currentLink.html(inputField.val()))}else{nameBlock.html(inputField.val())}inputField.remove()}).fail(function(jqXHR){console.error(jqXHR);var error='An error occurred while trying to process this request.';if(typeof jqXHR.responseJSON!=='undefined'&&typeof jqXHR.responseJSON.error!=='undefined'){error=jqXHR.responseJSON.error}nameBlock.addClass('has-error').delay(2000).queue(function(){nameBlock.removeClass('has-error').dequeue()});inputField.popover({animation:true,placement:'top',content:error,title:'Save Error'}).popover('show')}).always(function(){inputLoader.remove();ContextMenu.unbind().run()})})}},{key:'copy',value:function copy(){var nameBlock=$(this.element).find('td[data-identifier="name"]');var currentName=decodeURIComponent(nameBlock.attr('data-name'));var currentPath=decodeURIComponent(nameBlock.data('path'));swal({type:'input',title:'Copy File',text:'Please enter the new path for the copied file below.',showCancelButton:true,showConfirmButton:true,closeOnConfirm:false,showLoaderOnConfirm:true,inputValue:''+currentPath+currentName},function(val){if(val===false){return false}$.ajax({type:'POST',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/copy',timeout:10000,data:JSON.stringify({from:''+currentPath+currentName,to:''+val})}).done(function(data){swal({type:'success',title:'',text:'File successfully copied.'});Files.list()}).fail(function(jqXHR){console.error(jqXHR);var error='An error occurred while trying to process this request.';if(typeof jqXHR.responseJSON!=='undefined'&&typeof jqXHR.responseJSON.error!=='undefined'){error=jqXHR.responseJSON.error}swal({type:'error',title:'',text:error})})})}},{key:'download',value:function download(){var nameBlock=$(this.element).find('td[data-identifier="name"]');var fileName=decodeURIComponent(nameBlock.attr('data-name'));var filePath=decodeURIComponent(nameBlock.data('path'));window.location='/server/'+Pterodactyl.server.uuidShort+'/files/download/'+filePath+fileName}},{key:'delete',value:function _delete(){var nameBlock=$(this.element).find('td[data-identifier="name"]');var delPath=decodeURIComponent(nameBlock.data('path'));var delName=decodeURIComponent(nameBlock.data('name'));swal({type:'warning',title:'',text:'Are you sure you want to delete '+this.sanitizedString(delName)+'?',html:true,showCancelButton:true,showConfirmButton:true,closeOnConfirm:false,showLoaderOnConfirm:true},function(){$.ajax({type:'POST',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/delete',timeout:10000,data:JSON.stringify({items:[''+delPath+delName]})}).done(function(data){nameBlock.parent().addClass('warning').delay(200).fadeOut();swal({type:'success',title:'File Deleted'})}).fail(function(jqXHR){console.error(jqXHR);swal({type:'error',title:'Whoops!',html:true,text:'An error occurred while attempting to delete this file. Please try again.'})})})}},{key:'toggleMassActions',value:function toggleMassActions(){if($('#file_listing input[type="checkbox"]:checked').length){$('#mass_actions').removeClass('disabled')}else{$('#mass_actions').addClass('disabled')}}},{key:'toggleHighlight',value:function toggleHighlight(event){var parent=$(event.currentTarget);var item=$(event.currentTarget).find('input');if($(item).is(':checked')){$(item).prop('checked',false);parent.removeClass('warning').delay(200)}else{$(item).prop('checked',true);parent.addClass('warning').delay(200)}}},{key:'highlightAll',value:function highlightAll(event){var parent=void 0;var item=$(event.currentTarget).find('input');if($(item).is(':checked')){$('#file_listing input[type=checkbox]').prop('checked',false);$('#file_listing input[data-action="addSelection"]').each(function(){parent=$(this).closest('tr');parent.removeClass('warning').delay(200)})}else{$('#file_listing input[type=checkbox]').prop('checked',true);$('#file_listing input[data-action="addSelection"]').each(function(){parent=$(this).closest('tr');parent.addClass('warning').delay(200)})}}},{key:'deleteSelected',value:function deleteSelected(){var selectedItems=[];var selectedItemsElements=[];var parent=void 0;var nameBlock=void 0;var delLocation=void 0;$('#file_listing input[data-action="addSelection"]:checked').each(function(){parent=$(this).closest('tr');nameBlock=$(parent).find('td[data-identifier="name"]');delLocation=decodeURIComponent(nameBlock.data('path'))+decodeURIComponent(nameBlock.data('name'));selectedItems.push(delLocation);selectedItemsElements.push(parent)});if(selectedItems.length!=0){var formattedItems='';var i=0;$.each(selectedItems,function(key,value){formattedItems+=''+this.sanitizedString(value)+', ';i++;return i<5});formattedItems=formattedItems.slice(0,-2);if(selectedItems.length>5){formattedItems+=', and '+(selectedItems.length-5)+' other(s)'}swal({type:'warning',title:'',text:'Are you sure you want to delete the following files: '+this.sanitizedString(formattedItems)+'?',html:true,showCancelButton:true,showConfirmButton:true,closeOnConfirm:false,showLoaderOnConfirm:true},function(){$.ajax({type:'POST',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/delete',timeout:10000,data:JSON.stringify({items:selectedItems})}).done(function(data){$('#file_listing input:checked').each(function(){$(this).prop('checked',false)});$.each(selectedItemsElements,function(){$(this).addClass('warning').delay(200).fadeOut()});swal({type:'success',title:'Files Deleted'})}).fail(function(jqXHR){console.error(jqXHR);swal({type:'error',title:'Whoops!',html:true,text:'An error occurred while attempting to delete these files. Please try again.'})})})}else{swal({type:'warning',title:'',text:'Please select files/folders to delete.'})}}},{key:'decompress',value:function decompress(){var nameBlock=$(this.element).find('td[data-identifier="name"]');var compPath=decodeURIComponent(nameBlock.data('path'));var compName=decodeURIComponent(nameBlock.data('name'));swal({title:' Decompressing...',text:'This might take a few seconds to complete.',html:true,allowOutsideClick:false,allowEscapeKey:false,showConfirmButton:false});$.ajax({type:'POST',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/decompress',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',data:JSON.stringify({files:''+compPath+compName})}).done(function(data){swal.close();Files.list(compPath)}).fail(function(jqXHR){console.error(jqXHR);var error='An error occurred while trying to process this request.';if(typeof jqXHR.responseJSON!=='undefined'&&typeof jqXHR.responseJSON.error!=='undefined'){error=jqXHR.responseJSON.error}swal({type:'error',title:'Whoops!',html:true,text:error})})}},{key:'compress',value:function compress(){var _this=this;var nameBlock=$(this.element).find('td[data-identifier="name"]');var compPath=decodeURIComponent(nameBlock.data('path'));var compName=decodeURIComponent(nameBlock.data('name'));$.ajax({type:'POST',url:Pterodactyl.node.scheme+'://'+Pterodactyl.node.fqdn+':'+Pterodactyl.node.daemonListen+'/v1/server/file/compress',headers:{'X-Access-Token':Pterodactyl.server.daemonSecret,'X-Access-Server':Pterodactyl.server.uuid},contentType:'application/json; charset=utf-8',data:JSON.stringify({files:''+compPath+compName,to:compPath.toString()})}).done(function(data){Files.list(compPath,function(err){if(err)return;var fileListing=$('#file_listing').find('[data-name="'+data.saved_as+'"]').parent();fileListing.addClass('success pulsate').delay(3000).queue(function(){fileListing.removeClass('success pulsate').dequeue()})})}).fail(function(jqXHR){console.error(jqXHR);var error='An error occurred while trying to process this request.';if(typeof jqXHR.responseJSON!=='undefined'&&typeof jqXHR.responseJSON.error!=='undefined'){error=jqXHR.responseJSON.error}swal({type:'error',title:'Whoops!',html:true,text:_this.sanitizedString(error)})})}}]);return ActionsClass}(); +'use strict';var _createClass=function(){function defineProperties(target,props){for(var i=0;i').text(newFilePath).html()+'" class="text-muted"> New File
  • New Folder
  • '}if(Pterodactyl.permissions.downloadFiles||Pterodactyl.permissions.deleteFiles){buildMenu+='
  • '}if(Pterodactyl.permissions.downloadFiles){buildMenu+=''}if(Pterodactyl.permissions.deleteFiles){buildMenu+='
  • Delete
  • '}buildMenu+='';return buildMenu}},{key:'rightClick',value:function rightClick(){var _this=this;$('[data-action="toggleMenu"]').on('mousedown',function(event){event.preventDefault();if($(document).find('#fileOptionMenu').is(':visible')){$('body').trigger('click');return}_this.showMenu(event)});$('#file_listing > tbody td').on('contextmenu',function(event){_this.showMenu(event)})}},{key:'showMenu',value:function showMenu(event){var _this2=this;var parent=$(event.target).closest('tr');var menu=$(this.makeMenu(parent));if(parent.data('type')==='disabled')return;event.preventDefault();$(menu).appendTo('body');$(menu).data('invokedOn',$(event.target)).show().css({position:'absolute',left:event.pageX-150,top:event.pageY});this.activeLine=parent;this.activeLine.addClass('active');var Actions=new ActionsClass(parent,menu);if(Pterodactyl.permissions.moveFiles){$(menu).find('li[data-action="move"]').unbind().on('click',function(e){e.preventDefault();Actions.move()});$(menu).find('li[data-action="rename"]').unbind().on('click',function(e){e.preventDefault();Actions.rename()})}if(Pterodactyl.permissions.copyFiles){$(menu).find('li[data-action="copy"]').unbind().on('click',function(e){e.preventDefault();Actions.copy()})}if(Pterodactyl.permissions.compressFiles){if(parent.data('type')==='folder'){$(menu).find('li[data-action="compress"]').removeClass('hidden')}$(menu).find('li[data-action="compress"]').unbind().on('click',function(e){e.preventDefault();Actions.compress()})}if(Pterodactyl.permissions.decompressFiles){if(_.without(['application/zip','application/gzip','application/x-gzip'],parent.data('mime')).length<3){$(menu).find('li[data-action="decompress"]').removeClass('hidden')}$(menu).find('li[data-action="decompress"]').unbind().on('click',function(e){e.preventDefault();Actions.decompress()})}if(Pterodactyl.permissions.createFiles){$(menu).find('li[data-action="folder"]').unbind().on('click',function(e){e.preventDefault();Actions.folder()})}if(Pterodactyl.permissions.downloadFiles){if(parent.data('type')==='file'){$(menu).find('li[data-action="download"]').removeClass('hidden')}$(menu).find('li[data-action="download"]').unbind().on('click',function(e){e.preventDefault();Actions.download()})}if(Pterodactyl.permissions.deleteFiles){$(menu).find('li[data-action="delete"]').unbind().on('click',function(e){e.preventDefault();Actions.delete()})}$(window).unbind().on('click',function(event){if($(event.target).is('.disable-menu-hide')){event.preventDefault();return}$(menu).unbind().remove();if(!_.isNull(_this2.activeLine))_this2.activeLine.removeClass('active')})}},{key:'directoryClick',value:function directoryClick(){$('a[data-action="directory-view"]').on('click',function(event){event.preventDefault();var path=$(this).parent().data('path')||'';var name=$(this).parent().data('name')||'';window.location.hash=encodeURIComponent(path+name);Files.list()})}}]);return ContextMenuClass}();window.ContextMenu=new ContextMenuClass; 'use strict';var _typeof=typeof Symbol==='function'&&typeof Symbol.iterator==='symbol'?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==='function'&&obj.constructor===Symbol&&obj!==Symbol.prototype?'symbol':typeof obj};var _createClass=function(){function defineProperties(target,props){for(var i=0;i\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nclass ActionsClass {\n constructor(element, menu) {\n this.element = element;\n this.menu = menu;\n }\n\n destroy() {\n this.element = undefined;\n }\n\n folder(path) {\n let inputValue\n if (path) {\n inputValue = path\n } else {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const currentName = decodeURIComponent(nameBlock.data('name'));\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n\n if ($(this.element).data('type') === 'file') {\n inputValue = currentPath;\n } else {\n inputValue = `${currentPath}${currentName}/`;\n }\n }\n\n swal({\n type: 'input',\n title: 'Create Folder',\n text: 'Please enter the path and folder name below.',\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true,\n inputValue: inputValue\n }, (val) => {\n if (val === false) {\n return false;\n }\n\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/folder`,\n timeout: 10000,\n data: JSON.stringify({\n path: val,\n }),\n }).done(data => {\n swal.close();\n Files.list();\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occurred while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: '',\n text: error,\n });\n });\n });\n }\n\n move() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const currentName = decodeURIComponent(nameBlock.attr('data-name'));\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n\n swal({\n type: 'input',\n title: 'Move File',\n text: 'Please enter the new path for the file below.',\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true,\n inputValue: `${currentPath}${currentName}`,\n }, (val) => {\n if (val === false) {\n return false;\n }\n\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/move`,\n timeout: 10000,\n data: JSON.stringify({\n from: `${currentPath}${currentName}`,\n to: `${val}`,\n }),\n }).done(data => {\n nameBlock.parent().addClass('warning').delay(200).fadeOut();\n swal.close();\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occurred while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: '',\n text: error,\n });\n });\n });\n\n }\n\n rename() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const currentLink = nameBlock.find('a');\n const currentName = decodeURIComponent(nameBlock.attr('data-name'));\n const attachEditor = `\n \n \n `;\n\n nameBlock.html(attachEditor);\n const inputField = nameBlock.find('input');\n const inputLoader = nameBlock.find('.input-loader');\n\n inputField.focus();\n inputField.on('blur keydown', e => {\n // Save Field\n if (\n (e.type === 'keydown' && e.which === 27)\n || e.type === 'blur'\n || (e.type === 'keydown' && e.which === 13 && currentName === inputField.val())\n ) {\n if (!_.isEmpty(currentLink)) {\n nameBlock.html(currentLink);\n } else {\n nameBlock.html(currentName);\n }\n inputField.remove();\n ContextMenu.unbind().run();\n return;\n }\n\n if (e.type === 'keydown' && e.which !== 13) return;\n\n inputLoader.show();\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/rename`,\n timeout: 10000,\n data: JSON.stringify({\n from: `${currentPath}${currentName}`,\n to: `${currentPath}${inputField.val()}`,\n }),\n }).done(data => {\n nameBlock.attr('data-name', inputField.val());\n if (!_.isEmpty(currentLink)) {\n let newLink = currentLink.attr('href');\n if (nameBlock.parent().data('type') !== 'folder') {\n newLink = newLink.substr(0, newLink.lastIndexOf('/')) + '/' + inputField.val();\n }\n currentLink.attr('href', newLink);\n nameBlock.html(\n currentLink.html(inputField.val())\n );\n } else {\n nameBlock.html(inputField.val());\n }\n inputField.remove();\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occurred while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n nameBlock.addClass('has-error').delay(2000).queue(() => {\n nameBlock.removeClass('has-error').dequeue();\n });\n inputField.popover({\n animation: true,\n placement: 'top',\n content: error,\n title: 'Save Error'\n }).popover('show');\n }).always(() => {\n inputLoader.remove();\n ContextMenu.unbind().run();\n });\n });\n }\n\n copy() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const currentName = decodeURIComponent(nameBlock.attr('data-name'));\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n\n swal({\n type: 'input',\n title: 'Copy File',\n text: 'Please enter the new path for the copied file below.',\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true,\n inputValue: `${currentPath}${currentName}`,\n }, (val) => {\n if (val === false) {\n return false;\n }\n\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/copy`,\n timeout: 10000,\n data: JSON.stringify({\n from: `${currentPath}${currentName}`,\n to: `${val}`,\n }),\n }).done(data => {\n swal({\n type: 'success',\n title: '',\n text: 'File successfully copied.'\n });\n Files.list();\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occurred while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: '',\n text: error,\n });\n });\n });\n }\n\n download() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const fileName = decodeURIComponent(nameBlock.attr('data-name'));\n const filePath = decodeURIComponent(nameBlock.data('path'));\n\n window.location = `/server/${Pterodactyl.server.uuidShort}/files/download/${filePath}${fileName}`;\n }\n\n delete() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const delPath = decodeURIComponent(nameBlock.data('path'));\n const delName = decodeURIComponent(nameBlock.data('name'));\n\n swal({\n type: 'warning',\n title: '',\n text: 'Are you sure you want to delete ' + delName + '?',\n html: true,\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true\n }, () => {\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/delete`,\n timeout: 10000,\n data: JSON.stringify({\n items: [`${delPath}${delName}`]\n }),\n }).done(data => {\n nameBlock.parent().addClass('warning').delay(200).fadeOut();\n swal({\n type: 'success',\n title: 'File Deleted'\n });\n }).fail(jqXHR => {\n console.error(jqXHR);\n swal({\n type: 'error',\n title: 'Whoops!',\n html: true,\n text: 'An error occurred while attempting to delete this file. Please try again.',\n });\n });\n });\n }\n\n toggleMassActions() {\n if ($('#file_listing input[type=\"checkbox\"]:checked').length) {\n $('#mass_actions').removeClass('disabled');\n } else {\n $('#mass_actions').addClass('disabled');\n }\n }\n\n toggleHighlight(event) {\n const parent = $(event.currentTarget);\n const item = $(event.currentTarget).find('input');\n\n if($(item).is(':checked')) {\n $(item).prop('checked', false);\n parent.removeClass('warning').delay(200);\n } else {\n $(item).prop('checked', true);\n parent.addClass('warning').delay(200);\n }\n }\n\n highlightAll(event) {\n let parent;\n const item = $(event.currentTarget).find('input');\n\n if($(item).is(':checked')) {\n $('#file_listing input[type=checkbox]').prop('checked', false);\n $('#file_listing input[data-action=\"addSelection\"]').each(function() {\n parent = $(this).closest('tr');\n parent.removeClass('warning').delay(200);\n });\n } else {\n $('#file_listing input[type=checkbox]').prop('checked', true);\n $('#file_listing input[data-action=\"addSelection\"]').each(function() {\n parent = $(this).closest('tr');\n parent.addClass('warning').delay(200);\n });\n }\n }\n\n deleteSelected() {\n let selectedItems = [];\n let selectedItemsElements = [];\n let parent;\n let nameBlock;\n let delLocation;\n\n $('#file_listing input[data-action=\"addSelection\"]:checked').each(function() {\n parent = $(this).closest('tr');\n nameBlock = $(parent).find('td[data-identifier=\"name\"]');\n delLocation = decodeURIComponent(nameBlock.data('path')) + decodeURIComponent(nameBlock.data('name'));\n\n selectedItems.push(delLocation);\n selectedItemsElements.push(parent);\n });\n\n if (selectedItems.length != 0)\n {\n let formattedItems = \"\";\n let i = 0;\n $.each(selectedItems, function(key, value) {\n formattedItems += (\"\" + value + \", \");\n i++;\n return i < 5;\n });\n\n formattedItems = formattedItems.slice(0, -2);\n if (selectedItems.length > 5) {\n formattedItems += ', and ' + (selectedItems.length - 5) + ' other(s)';\n }\n\n swal({\n type: 'warning',\n title: '',\n text: 'Are you sure you want to delete the following files: ' + formattedItems + '?',\n html: true,\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true\n }, () => {\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/delete`,\n timeout: 10000,\n data: JSON.stringify({\n items: selectedItems\n }),\n }).done(data => {\n $('#file_listing input:checked').each(function() {\n $(this).prop('checked', false);\n });\n\n $.each(selectedItemsElements, function() {\n $(this).addClass('warning').delay(200).fadeOut();\n })\n\n swal({\n type: 'success',\n title: 'Files Deleted'\n });\n }).fail(jqXHR => {\n console.error(jqXHR);\n swal({\n type: 'error',\n title: 'Whoops!',\n html: true,\n text: 'An error occurred while attempting to delete these files. Please try again.',\n });\n });\n });\n } else {\n swal({\n type: 'warning',\n title: '',\n text: 'Please select files/folders to delete.',\n });\n }\n }\n\n decompress() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const compPath = decodeURIComponent(nameBlock.data('path'));\n const compName = decodeURIComponent(nameBlock.data('name'));\n\n swal({\n title: ' Decompressing...',\n text: 'This might take a few seconds to complete.',\n html: true,\n allowOutsideClick: false,\n allowEscapeKey: false,\n showConfirmButton: false,\n });\n\n $.ajax({\n type: 'POST',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/decompress`,\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n data: JSON.stringify({\n files: `${compPath}${compName}`\n })\n }).done(data => {\n swal.close();\n Files.list(compPath);\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occurred while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: 'Whoops!',\n html: true,\n text: error\n });\n });\n }\n\n compress() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const compPath = decodeURIComponent(nameBlock.data('path'));\n const compName = decodeURIComponent(nameBlock.data('name'));\n\n $.ajax({\n type: 'POST',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/compress`,\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n data: JSON.stringify({\n files: `${compPath}${compName}`,\n to: compPath.toString()\n })\n }).done(data => {\n Files.list(compPath, err => {\n if (err) return;\n const fileListing = $('#file_listing').find(`[data-name=\"${data.saved_as}\"]`).parent();\n fileListing.addClass('success pulsate').delay(3000).queue(() => {\n fileListing.removeClass('success pulsate').dequeue();\n });\n });\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occurred while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: 'Whoops!',\n html: true,\n text: error\n });\n });\n }\n}\n","\"use strict\";\n\n// Copyright (c) 2015 - 2017 Dane Everitt \n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nclass ContextMenuClass {\n constructor() {\n this.activeLine = null;\n }\n\n run() {\n this.directoryClick();\n this.rightClick();\n }\n\n makeMenu(parent) {\n $(document).find('#fileOptionMenu').remove();\n if (!_.isNull(this.activeLine)) this.activeLine.removeClass('active');\n\n let newFilePath = $('#file_listing').data('current-dir');\n if (parent.data('type') === 'folder') {\n const nameBlock = parent.find('td[data-identifier=\"name\"]');\n const currentName = decodeURIComponent(nameBlock.attr('data-name'));\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n newFilePath = `${currentPath}${currentName}`;\n }\n\n let buildMenu = '
      ';\n\n if (Pterodactyl.permissions.moveFiles) {\n buildMenu += '
    • Rename
    • \\\n
    • Move
    • ';\n }\n\n if (Pterodactyl.permissions.copyFiles) {\n buildMenu += '
    • Copy
    • ';\n }\n\n if (Pterodactyl.permissions.compressFiles) {\n buildMenu += '
    • Compress
    • ';\n }\n\n if (Pterodactyl.permissions.decompressFiles) {\n buildMenu += '
    • Decompress
    • ';\n }\n\n if (Pterodactyl.permissions.createFiles) {\n buildMenu += '
    • \\\n
    • New File
    • \\\n
    • New Folder
    • ';\n }\n\n if (Pterodactyl.permissions.downloadFiles || Pterodactyl.permissions.deleteFiles) {\n buildMenu += '
    • ';\n }\n\n if (Pterodactyl.permissions.downloadFiles) {\n buildMenu += '
    • Download
    • ';\n }\n\n if (Pterodactyl.permissions.deleteFiles) {\n buildMenu += '
    • Delete
    • ';\n }\n\n buildMenu += '
    ';\n return buildMenu;\n }\n\n rightClick() {\n $('[data-action=\"toggleMenu\"]').on('mousedown', event => {\n event.preventDefault();\n if ($(document).find('#fileOptionMenu').is(':visible')) {\n $('body').trigger('click');\n return;\n }\n this.showMenu(event);\n });\n $('#file_listing > tbody td').on('contextmenu', event => {\n this.showMenu(event);\n });\n }\n\n showMenu(event) {\n const parent = $(event.target).closest('tr');\n const menu = $(this.makeMenu(parent));\n\n if (parent.data('type') === 'disabled') return;\n event.preventDefault();\n\n $(menu).appendTo('body');\n $(menu).data('invokedOn', $(event.target)).show().css({\n position: 'absolute',\n left: event.pageX - 150,\n top: event.pageY,\n });\n\n this.activeLine = parent;\n this.activeLine.addClass('active');\n\n // Handle Events\n const Actions = new ActionsClass(parent, menu);\n if (Pterodactyl.permissions.moveFiles) {\n $(menu).find('li[data-action=\"move\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.move();\n });\n $(menu).find('li[data-action=\"rename\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.rename();\n });\n }\n\n if (Pterodactyl.permissions.copyFiles) {\n $(menu).find('li[data-action=\"copy\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.copy();\n });\n }\n\n if (Pterodactyl.permissions.compressFiles) {\n if (parent.data('type') === 'folder') {\n $(menu).find('li[data-action=\"compress\"]').removeClass('hidden');\n }\n $(menu).find('li[data-action=\"compress\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.compress();\n });\n }\n\n if (Pterodactyl.permissions.decompressFiles) {\n if (_.without(['application/zip', 'application/gzip', 'application/x-gzip'], parent.data('mime')).length < 3) {\n $(menu).find('li[data-action=\"decompress\"]').removeClass('hidden');\n }\n $(menu).find('li[data-action=\"decompress\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.decompress();\n });\n }\n\n if (Pterodactyl.permissions.createFiles) {\n $(menu).find('li[data-action=\"folder\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.folder();\n });\n }\n\n if (Pterodactyl.permissions.downloadFiles) {\n if (parent.data('type') === 'file') {\n $(menu).find('li[data-action=\"download\"]').removeClass('hidden');\n }\n $(menu).find('li[data-action=\"download\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.download();\n });\n }\n\n if (Pterodactyl.permissions.deleteFiles) {\n $(menu).find('li[data-action=\"delete\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.delete();\n });\n }\n\n $(window).unbind().on('click', event => {\n if($(event.target).is('.disable-menu-hide')) {\n event.preventDefault();\n return;\n }\n $(menu).unbind().remove();\n if(!_.isNull(this.activeLine)) this.activeLine.removeClass('active');\n });\n }\n\n directoryClick() {\n $('a[data-action=\"directory-view\"]').on('click', function (event) {\n event.preventDefault();\n\n const path = $(this).parent().data('path') || '';\n const name = $(this).parent().data('name') || '';\n\n window.location.hash = encodeURIComponent(path + name);\n Files.list();\n });\n }\n}\n\nwindow.ContextMenu = new ContextMenuClass;\n","\"use strict\";\n\n// Copyright (c) 2015 - 2017 Dane Everitt \n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nclass FileManager {\n constructor() {\n this.list(this.decodeHash());\n }\n\n list(path, next) {\n if (_.isUndefined(path)) {\n path = this.decodeHash();\n }\n\n this.loader(true);\n $.ajax({\n type: 'POST',\n url: Pterodactyl.meta.directoryList,\n headers: {\n 'X-CSRF-Token': Pterodactyl.meta.csrftoken,\n },\n data: {\n directory: path,\n },\n }).done(data => {\n this.loader(false);\n $('#load_files').slideUp(10).html(data).slideDown(10, () => {\n ContextMenu.run();\n this.reloadFilesButton();\n this.addFolderButton();\n this.selectItem();\n this.selectAll();\n this.selectiveDeletion();\n this.selectRow();\n if (_.isFunction(next)) {\n return next();\n }\n });\n $('#internal_alert').slideUp();\n\n if (typeof Siofu === 'object') {\n Siofu.listenOnInput(document.getElementById(\"files_touch_target\"));\n }\n }).fail(jqXHR => {\n this.loader(false);\n if (_.isFunction(next)) {\n return next(new Error('Failed to load file listing.'));\n }\n\n if ((path !== '' && path !== '/') && jqXHR.status === 404) {\n return this.list('', next);\n }\n\n swal({\n type: 'error',\n title: 'File Error',\n text: jqXHR.responseJSON.errors[0].detail || 'An error occurred while attempting to process this request. Please try again.',\n });\n console.error(jqXHR);\n });\n }\n\n loader(show) {\n if (show){\n $('.file-overlay').fadeIn(100);\n } else {\n $('.file-overlay').fadeOut(100);\n }\n }\n\n reloadFilesButton() {\n $('i[data-action=\"reload-files\"]').unbind().on('click', () => {\n $('i[data-action=\"reload-files\"]').addClass('fa-spin');\n this.list();\n });\n }\n\n selectItem() {\n $('[data-action=\"addSelection\"]').on('click', event => {\n event.preventDefault();\n });\n }\n\n selectAll() {\n $('[data-action=\"selectAll\"]').on('click', event => {\n event.preventDefault();\n });\n }\n\n selectiveDeletion() {\n $('[data-action=\"selective-deletion\"]').on('mousedown', event => {\n new ActionsClass().deleteSelected();\n });\n }\n\n addFolderButton() {\n $('[data-action=\"add-folder\"]').unbind().on('click', () => {\n new ActionsClass().folder($('#file_listing').data('current-dir') || '/');\n })\n }\n\n selectRow() {\n $('#file_listing tr').on('mousedown', event => {\n if (event.which === 1) {\n if ($(event.target).is('th') || $(event.target).is('input[data-action=\"selectAll\"]')) {\n new ActionsClass().highlightAll(event);\n } else if ($(event.target).is('td') || $(event.target).is('input[data-action=\"addSelection\"]')) {\n new ActionsClass().toggleHighlight(event);\n }\n\n new ActionsClass().toggleMassActions();\n }\n });\n }\n\n decodeHash() {\n return decodeURIComponent(window.location.hash.substring(1));\n }\n\n}\n\nwindow.Files = new FileManager;\n"]} \ No newline at end of file +{"version":3,"sources":["src/actions.js","src/contextmenu.js","src/index.js"],"names":[],"mappings":"AAAA,a,8oBAqBM,a,YACF,sBAAY,OAAZ,CAAqB,IAArB,CAA2B,oCACvB,KAAK,OAAL,CAAe,OAAf,CACA,KAAK,IAAL,CAAY,IACf,C,kEAES,CACN,KAAK,OAAL,CAAe,SAClB,C,wDAEe,K,CAAO,CACnB,MAAO,GAAE,OAAF,EAAW,IAAX,CAAgB,KAAhB,EAAuB,IAAvB,EACV,C,sCAEM,I,CAAM,CACT,GAAI,kBAAJ,CACA,GAAI,IAAJ,CAAU,CACN,WAAa,IAChB,CAFD,IAEO,CACH,GAAM,WAAY,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,4BAArB,CAAlB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAApB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAApB,CAEA,GAAI,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,MAArB,IAAiC,MAArC,CAA6C,CACzC,WAAa,WAChB,CAFD,IAEO,CACH,cAAgB,WAAhB,CAA8B,WAA9B,IACH,CACJ,CAED,KAAK,CACD,KAAM,OADL,CAED,MAAO,eAFN,CAGD,KAAM,8CAHL,CAID,iBAAkB,IAJjB,CAKD,kBAAmB,IALlB,CAMD,eAAgB,KANf,CAOD,oBAAqB,IAPpB,CAQD,WAAY,UARX,CAAL,CASG,SAAC,GAAD,CAAS,CACR,GAAI,MAAQ,KAAZ,CAAmB,CACf,MAAO,MACV,CAED,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,QAAS,CACL,iBAAkB,YAAY,MAAZ,CAAmB,YADhC,CAEL,kBAAmB,YAAY,MAAZ,CAAmB,IAFjC,CAFN,CAMH,YAAa,iCANV,CAOH,IAAQ,YAAY,IAAZ,CAAiB,MAAzB,OAAqC,YAAY,IAAZ,CAAiB,IAAtD,KAA8D,YAAY,IAAZ,CAAiB,YAA/E,yBAPG,CAQH,QAAS,KARN,CASH,KAAM,KAAK,SAAL,CAAe,CACjB,KAAM,GADW,CAAf,CATH,CAAP,EAYG,IAZH,CAYQ,cAAQ,CACZ,KAAK,KAAL,GACA,MAAM,IAAN,EACH,CAfD,EAeG,IAfH,CAeQ,eAAS,CACb,QAAQ,KAAR,CAAc,KAAd,EACA,GAAI,OAAQ,yDAAZ,CACA,GAAI,MAAO,OAAM,YAAb,GAA8B,WAA9B,EAA6C,MAAO,OAAM,YAAN,CAAmB,KAA1B,GAAoC,WAArF,CAAkG,CAC9F,MAAQ,MAAM,YAAN,CAAmB,KAC9B,CACD,KAAK,CACD,KAAM,OADL,CAED,MAAO,EAFN,CAGD,KAAM,KAHL,CAAL,CAKH,CA1BD,CA2BH,CAzCD,CA0CH,C,mCAEM,CACH,GAAM,WAAY,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,4BAArB,CAAlB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,WAAf,CAAnB,CAApB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAApB,CAEA,KAAK,CACD,KAAM,OADL,CAED,MAAO,WAFN,CAGD,KAAM,+CAHL,CAID,iBAAkB,IAJjB,CAKD,kBAAmB,IALlB,CAMD,eAAgB,KANf,CAOD,oBAAqB,IAPpB,CAQD,cAAe,WAAf,CAA6B,WAR5B,CAAL,CASG,SAAC,GAAD,CAAS,CACR,GAAI,MAAQ,KAAZ,CAAmB,CACf,MAAO,MACV,CAED,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,QAAS,CACL,iBAAkB,YAAY,MAAZ,CAAmB,YADhC,CAEL,kBAAmB,YAAY,MAAZ,CAAmB,IAFjC,CAFN,CAMH,YAAa,iCANV,CAOH,IAAQ,YAAY,IAAZ,CAAiB,MAAzB,OAAqC,YAAY,IAAZ,CAAiB,IAAtD,KAA8D,YAAY,IAAZ,CAAiB,YAA/E,uBAPG,CAQH,QAAS,KARN,CASH,KAAM,KAAK,SAAL,CAAe,CACjB,QAAS,WAAT,CAAuB,WADN,CAEjB,MAAO,GAFU,CAAf,CATH,CAAP,EAaG,IAbH,CAaQ,cAAQ,CACZ,UAAU,MAAV,GAAmB,QAAnB,CAA4B,SAA5B,EAAuC,KAAvC,CAA6C,GAA7C,EAAkD,OAAlD,GACA,KAAK,KAAL,EACH,CAhBD,EAgBG,IAhBH,CAgBQ,eAAS,CACb,QAAQ,KAAR,CAAc,KAAd,EACA,GAAI,OAAQ,yDAAZ,CACA,GAAI,MAAO,OAAM,YAAb,GAA8B,WAA9B,EAA6C,MAAO,OAAM,YAAN,CAAmB,KAA1B,GAAoC,WAArF,CAAkG,CAC9F,MAAQ,MAAM,YAAN,CAAmB,KAC9B,CACD,KAAK,CACD,KAAM,OADL,CAED,MAAO,EAFN,CAGD,KAAM,KAHL,CAAL,CAKH,CA3BD,CA4BH,CA1CD,CA4CH,C,uCAEQ,CACL,GAAM,WAAY,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,4BAArB,CAAlB,CACA,GAAM,aAAc,UAAU,IAAV,CAAe,GAAf,CAApB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,WAAf,CAAnB,CAApB,CACA,GAAM,uFACwD,WADxD,4GAAN,CAKA,UAAU,IAAV,CAAe,YAAf,EACA,GAAM,YAAa,UAAU,IAAV,CAAe,OAAf,CAAnB,CACA,GAAM,aAAc,UAAU,IAAV,CAAe,eAAf,CAApB,CAEA,WAAW,KAAX,GACA,WAAW,EAAX,CAAc,cAAd,CAA8B,WAAK,CAE/B,GACK,EAAE,IAAF,GAAW,SAAX,EAAwB,EAAE,KAAF,GAAY,EAArC,EACG,EAAE,IAAF,GAAW,MADd,EAEI,EAAE,IAAF,GAAW,SAAX,EAAwB,EAAE,KAAF,GAAY,EAApC,EAA0C,cAAgB,WAAW,GAAX,EAHlE,CAIE,CACE,GAAI,CAAC,EAAE,OAAF,CAAU,WAAV,CAAL,CAA6B,CACzB,UAAU,IAAV,CAAe,WAAf,CACH,CAFD,IAEO,CACH,UAAU,IAAV,CAAe,WAAf,CACH,CACD,WAAW,MAAX,GACA,YAAY,MAAZ,GAAqB,GAArB,GACA,MACH,CAED,GAAI,EAAE,IAAF,GAAW,SAAX,EAAwB,EAAE,KAAF,GAAY,EAAxC,CAA4C,OAE5C,YAAY,IAAZ,GACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAApB,CAEA,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,QAAS,CACL,iBAAkB,YAAY,MAAZ,CAAmB,YADhC,CAEL,kBAAmB,YAAY,MAAZ,CAAmB,IAFjC,CAFN,CAMH,YAAa,iCANV,CAOH,IAAQ,YAAY,IAAZ,CAAiB,MAAzB,OAAqC,YAAY,IAAZ,CAAiB,IAAtD,KAA8D,YAAY,IAAZ,CAAiB,YAA/E,yBAPG,CAQH,QAAS,KARN,CASH,KAAM,KAAK,SAAL,CAAe,CACjB,QAAS,WAAT,CAAuB,WADN,CAEjB,MAAO,WAAP,CAAqB,WAAW,GAAX,EAFJ,CAAf,CATH,CAAP,EAaG,IAbH,CAaQ,cAAQ,CACZ,UAAU,IAAV,CAAe,WAAf,CAA4B,WAAW,GAAX,EAA5B,EACA,GAAI,CAAC,EAAE,OAAF,CAAU,WAAV,CAAL,CAA6B,CACzB,GAAI,SAAU,YAAY,IAAZ,CAAiB,MAAjB,CAAd,CACA,GAAI,UAAU,MAAV,GAAmB,IAAnB,CAAwB,MAAxB,IAAoC,QAAxC,CAAkD,CAC9C,QAAU,QAAQ,MAAR,CAAe,CAAf,CAAkB,QAAQ,WAAR,CAAoB,GAApB,CAAlB,EAA8C,GAA9C,CAAoD,WAAW,GAAX,EACjE,CACD,YAAY,IAAZ,CAAiB,MAAjB,CAAyB,OAAzB,EACA,UAAU,IAAV,CACI,YAAY,IAAZ,CAAiB,WAAW,GAAX,EAAjB,CADJ,CAGH,CATD,IASO,CACH,UAAU,IAAV,CAAe,WAAW,GAAX,EAAf,CACH,CACD,WAAW,MAAX,EACH,CA5BD,EA4BG,IA5BH,CA4BQ,eAAS,CACb,QAAQ,KAAR,CAAc,KAAd,EACA,GAAI,OAAQ,yDAAZ,CACA,GAAI,MAAO,OAAM,YAAb,GAA8B,WAA9B,EAA6C,MAAO,OAAM,YAAN,CAAmB,KAA1B,GAAoC,WAArF,CAAkG,CAC9F,MAAQ,MAAM,YAAN,CAAmB,KAC9B,CACD,UAAU,QAAV,CAAmB,WAAnB,EAAgC,KAAhC,CAAsC,IAAtC,EAA4C,KAA5C,CAAkD,UAAM,CACpD,UAAU,WAAV,CAAsB,WAAtB,EAAmC,OAAnC,EACH,CAFD,EAGA,WAAW,OAAX,CAAmB,CACf,UAAW,IADI,CAEf,UAAW,KAFI,CAGf,QAAS,KAHM,CAIf,MAAO,YAJQ,CAAnB,EAKG,OALH,CAKW,MALX,CAMH,CA3CD,EA2CG,MA3CH,CA2CU,UAAM,CACZ,YAAY,MAAZ,GACA,YAAY,MAAZ,GAAqB,GAArB,EACH,CA9CD,CA+CH,CArED,CAsEH,C,mCAEM,CACH,GAAM,WAAY,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,4BAArB,CAAlB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,WAAf,CAAnB,CAApB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAApB,CAEA,KAAK,CACD,KAAM,OADL,CAED,MAAO,WAFN,CAGD,KAAM,sDAHL,CAID,iBAAkB,IAJjB,CAKD,kBAAmB,IALlB,CAMD,eAAgB,KANf,CAOD,oBAAqB,IAPpB,CAQD,cAAe,WAAf,CAA6B,WAR5B,CAAL,CASG,SAAC,GAAD,CAAS,CACR,GAAI,MAAQ,KAAZ,CAAmB,CACf,MAAO,MACV,CAED,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,QAAS,CACL,iBAAkB,YAAY,MAAZ,CAAmB,YADhC,CAEL,kBAAmB,YAAY,MAAZ,CAAmB,IAFjC,CAFN,CAMH,YAAa,iCANV,CAOH,IAAQ,YAAY,IAAZ,CAAiB,MAAzB,OAAqC,YAAY,IAAZ,CAAiB,IAAtD,KAA8D,YAAY,IAAZ,CAAiB,YAA/E,uBAPG,CAQH,QAAS,KARN,CASH,KAAM,KAAK,SAAL,CAAe,CACjB,QAAS,WAAT,CAAuB,WADN,CAEjB,MAAO,GAFU,CAAf,CATH,CAAP,EAaG,IAbH,CAaQ,cAAQ,CACZ,KAAK,CACD,KAAM,SADL,CAED,MAAO,EAFN,CAGD,KAAM,2BAHL,CAAL,EAKA,MAAM,IAAN,EACH,CApBD,EAoBG,IApBH,CAoBQ,eAAS,CACb,QAAQ,KAAR,CAAc,KAAd,EACA,GAAI,OAAQ,yDAAZ,CACA,GAAI,MAAO,OAAM,YAAb,GAA8B,WAA9B,EAA6C,MAAO,OAAM,YAAN,CAAmB,KAA1B,GAAoC,WAArF,CAAkG,CAC9F,MAAQ,MAAM,YAAN,CAAmB,KAC9B,CACD,KAAK,CACD,KAAM,OADL,CAED,MAAO,EAFN,CAGD,KAAM,KAHL,CAAL,CAKH,CA/BD,CAgCH,CA9CD,CA+CH,C,2CAEU,CACP,GAAM,WAAY,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,4BAArB,CAAlB,CACA,GAAM,UAAW,mBAAmB,UAAU,IAAV,CAAe,WAAf,CAAnB,CAAjB,CACA,GAAM,UAAW,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAAjB,CAEA,OAAO,QAAP,YAA6B,YAAY,MAAZ,CAAmB,SAAhD,oBAA4E,QAA5E,CAAuF,QAC1F,C,wCAEQ,CACL,GAAM,WAAY,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,4BAArB,CAAlB,CACA,GAAM,SAAU,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAAhB,CACA,GAAM,SAAU,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAAhB,CAEA,KAAK,CACD,KAAM,SADL,CAED,MAAO,EAFN,CAGD,KAAM,yCAA2C,KAAK,eAAL,CAAqB,OAArB,CAA3C,CAA2E,UAHhF,CAID,KAAM,IAJL,CAKD,iBAAkB,IALjB,CAMD,kBAAmB,IANlB,CAOD,eAAgB,KAPf,CAQD,oBAAqB,IARpB,CAAL,CASG,UAAM,CACL,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,QAAS,CACL,iBAAkB,YAAY,MAAZ,CAAmB,YADhC,CAEL,kBAAmB,YAAY,MAAZ,CAAmB,IAFjC,CAFN,CAMH,YAAa,iCANV,CAOH,IAAQ,YAAY,IAAZ,CAAiB,MAAzB,OAAqC,YAAY,IAAZ,CAAiB,IAAtD,KAA8D,YAAY,IAAZ,CAAiB,YAA/E,yBAPG,CAQH,QAAS,KARN,CASH,KAAM,KAAK,SAAL,CAAe,CACjB,MAAO,IAAI,OAAJ,CAAc,OAAd,CADU,CAAf,CATH,CAAP,EAYG,IAZH,CAYQ,cAAQ,CACZ,UAAU,MAAV,GAAmB,QAAnB,CAA4B,SAA5B,EAAuC,KAAvC,CAA6C,GAA7C,EAAkD,OAAlD,GACA,KAAK,CACD,KAAM,SADL,CAED,MAAO,cAFN,CAAL,CAIH,CAlBD,EAkBG,IAlBH,CAkBQ,eAAS,CACb,QAAQ,KAAR,CAAc,KAAd,EACA,KAAK,CACD,KAAM,OADL,CAED,MAAO,SAFN,CAGD,KAAM,IAHL,CAID,KAAM,2EAJL,CAAL,CAMH,CA1BD,CA2BH,CArCD,CAsCH,C,6DAEmB,CAChB,GAAI,EAAE,8CAAF,EAAkD,MAAtD,CAA8D,CAC1D,EAAE,eAAF,EAAmB,WAAnB,CAA+B,UAA/B,CACH,CAFD,IAEO,CACH,EAAE,eAAF,EAAmB,QAAnB,CAA4B,UAA5B,CACH,CACJ,C,wDAEe,K,CAAO,CACnB,GAAM,QAAS,EAAE,MAAM,aAAR,CAAf,CACA,GAAM,MAAO,EAAE,MAAM,aAAR,EAAuB,IAAvB,CAA4B,OAA5B,CAAb,CAEA,GAAG,EAAE,IAAF,EAAQ,EAAR,CAAW,UAAX,CAAH,CAA2B,CACvB,EAAE,IAAF,EAAQ,IAAR,CAAa,SAAb,CAAwB,KAAxB,EACA,OAAO,WAAP,CAAmB,SAAnB,EAA8B,KAA9B,CAAoC,GAApC,CACH,CAHD,IAGO,CACH,EAAE,IAAF,EAAQ,IAAR,CAAa,SAAb,CAAwB,IAAxB,EACA,OAAO,QAAP,CAAgB,SAAhB,EAA2B,KAA3B,CAAiC,GAAjC,CACH,CACJ,C,kDAEY,K,CAAO,CAChB,GAAI,cAAJ,CACA,GAAM,MAAO,EAAE,MAAM,aAAR,EAAuB,IAAvB,CAA4B,OAA5B,CAAb,CAEA,GAAG,EAAE,IAAF,EAAQ,EAAR,CAAW,UAAX,CAAH,CAA2B,CACzB,EAAE,oCAAF,EAAwC,IAAxC,CAA6C,SAA7C,CAAwD,KAAxD,EACA,EAAE,iDAAF,EAAqD,IAArD,CAA0D,UAAW,CACjE,OAAS,EAAE,IAAF,EAAQ,OAAR,CAAgB,IAAhB,CAAT,CACA,OAAO,WAAP,CAAmB,SAAnB,EAA8B,KAA9B,CAAoC,GAApC,CACH,CAHD,CAID,CAND,IAMO,CACL,EAAE,oCAAF,EAAwC,IAAxC,CAA6C,SAA7C,CAAwD,IAAxD,EACA,EAAE,iDAAF,EAAqD,IAArD,CAA0D,UAAW,CACjE,OAAS,EAAE,IAAF,EAAQ,OAAR,CAAgB,IAAhB,CAAT,CACA,OAAO,QAAP,CAAgB,SAAhB,EAA2B,KAA3B,CAAiC,GAAjC,CACH,CAHD,CAID,CACJ,C,uDAEgB,CACb,GAAI,eAAgB,EAApB,CACA,GAAI,uBAAwB,EAA5B,CACA,GAAI,cAAJ,CACA,GAAI,iBAAJ,CACA,GAAI,mBAAJ,CAEA,EAAE,yDAAF,EAA6D,IAA7D,CAAkE,UAAW,CACzE,OAAS,EAAE,IAAF,EAAQ,OAAR,CAAgB,IAAhB,CAAT,CACA,UAAY,EAAE,MAAF,EAAU,IAAV,CAAe,4BAAf,CAAZ,CACA,YAAc,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,EAA6C,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAA3D,CAEA,cAAc,IAAd,CAAmB,WAAnB,EACA,sBAAsB,IAAtB,CAA2B,MAA3B,CACH,CAPD,EASA,GAAI,cAAc,MAAd,EAAwB,CAA5B,CACA,CACI,GAAI,gBAAiB,EAArB,CACA,GAAI,GAAI,CAAR,CACA,EAAE,IAAF,CAAO,aAAP,CAAsB,SAAS,GAAT,CAAc,KAAd,CAAqB,CACvC,gBAAmB,SAAW,KAAK,eAAL,CAAqB,KAArB,CAAX,CAAyC,WAA5D,CACA,IACA,MAAO,GAAI,CACd,CAJD,EAMA,eAAiB,eAAe,KAAf,CAAqB,CAArB,CAAwB,CAAC,CAAzB,CAAjB,CACA,GAAI,cAAc,MAAd,CAAuB,CAA3B,CAA8B,CAC1B,gBAAkB,UAAY,cAAc,MAAd,CAAuB,CAAnC,EAAwC,WAC7D,CAED,KAAK,CACD,KAAM,SADL,CAED,MAAO,EAFN,CAGD,KAAM,wDAA0D,KAAK,eAAL,CAAqB,cAArB,CAA1D,CAAiG,GAHtG,CAID,KAAM,IAJL,CAKD,iBAAkB,IALjB,CAMD,kBAAmB,IANlB,CAOD,eAAgB,KAPf,CAQD,oBAAqB,IARpB,CAAL,CASG,UAAM,CACL,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,QAAS,CACL,iBAAkB,YAAY,MAAZ,CAAmB,YADhC,CAEL,kBAAmB,YAAY,MAAZ,CAAmB,IAFjC,CAFN,CAMH,YAAa,iCANV,CAOH,IAAQ,YAAY,IAAZ,CAAiB,MAAzB,OAAqC,YAAY,IAAZ,CAAiB,IAAtD,KAA8D,YAAY,IAAZ,CAAiB,YAA/E,yBAPG,CAQH,QAAS,KARN,CASH,KAAM,KAAK,SAAL,CAAe,CACjB,MAAO,aADU,CAAf,CATH,CAAP,EAYG,IAZH,CAYQ,cAAQ,CACZ,EAAE,6BAAF,EAAiC,IAAjC,CAAsC,UAAW,CAC7C,EAAE,IAAF,EAAQ,IAAR,CAAa,SAAb,CAAwB,KAAxB,CACH,CAFD,EAIA,EAAE,IAAF,CAAO,qBAAP,CAA8B,UAAW,CACrC,EAAE,IAAF,EAAQ,QAAR,CAAiB,SAAjB,EAA4B,KAA5B,CAAkC,GAAlC,EAAuC,OAAvC,EACH,CAFD,EAIA,KAAK,CACD,KAAM,SADL,CAED,MAAO,eAFN,CAAL,CAIH,CAzBD,EAyBG,IAzBH,CAyBQ,eAAS,CACb,QAAQ,KAAR,CAAc,KAAd,EACA,KAAK,CACD,KAAM,OADL,CAED,MAAO,SAFN,CAGD,KAAM,IAHL,CAID,KAAM,6EAJL,CAAL,CAMH,CAjCD,CAkCH,CA5CD,CA6CH,CA5DD,IA4DO,CACH,KAAK,CACH,KAAM,SADH,CAEH,MAAO,EAFJ,CAGH,KAAM,wCAHH,CAAL,CAKH,CACJ,C,+CAEY,CACT,GAAM,WAAY,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,4BAArB,CAAlB,CACA,GAAM,UAAW,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAAjB,CACA,GAAM,UAAW,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAAjB,CAEA,KAAK,CACD,MAAO,wDADN,CAED,KAAM,4CAFL,CAGD,KAAM,IAHL,CAID,kBAAmB,KAJlB,CAKD,eAAgB,KALf,CAMD,kBAAmB,KANlB,CAAL,EASA,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,IAAQ,YAAY,IAAZ,CAAiB,MAAzB,OAAqC,YAAY,IAAZ,CAAiB,IAAtD,KAA8D,YAAY,IAAZ,CAAiB,YAA/E,6BAFG,CAGH,QAAS,CACL,iBAAkB,YAAY,MAAZ,CAAmB,YADhC,CAEL,kBAAmB,YAAY,MAAZ,CAAmB,IAFjC,CAHN,CAOH,YAAa,iCAPV,CAQH,KAAM,KAAK,SAAL,CAAe,CACjB,SAAU,QAAV,CAAqB,QADJ,CAAf,CARH,CAAP,EAWG,IAXH,CAWQ,cAAQ,CACZ,KAAK,KAAL,GACA,MAAM,IAAN,CAAW,QAAX,CACH,CAdD,EAcG,IAdH,CAcQ,eAAS,CACb,QAAQ,KAAR,CAAc,KAAd,EACA,GAAI,OAAQ,yDAAZ,CACA,GAAI,MAAO,OAAM,YAAb,GAA8B,WAA9B,EAA6C,MAAO,OAAM,YAAN,CAAmB,KAA1B,GAAoC,WAArF,CAAkG,CAC9F,MAAQ,MAAM,YAAN,CAAmB,KAC9B,CACD,KAAK,CACD,KAAM,OADL,CAED,MAAO,SAFN,CAGD,KAAM,IAHL,CAID,KAAM,KAJL,CAAL,CAMH,CA1BD,CA2BH,C,2CAEU,gBACP,GAAM,WAAY,EAAE,KAAK,OAAP,EAAgB,IAAhB,CAAqB,4BAArB,CAAlB,CACA,GAAM,UAAW,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAAjB,CACA,GAAM,UAAW,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAAjB,CAEA,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,IAAQ,YAAY,IAAZ,CAAiB,MAAzB,OAAqC,YAAY,IAAZ,CAAiB,IAAtD,KAA8D,YAAY,IAAZ,CAAiB,YAA/E,2BAFG,CAGH,QAAS,CACL,iBAAkB,YAAY,MAAZ,CAAmB,YADhC,CAEL,kBAAmB,YAAY,MAAZ,CAAmB,IAFjC,CAHN,CAOH,YAAa,iCAPV,CAQH,KAAM,KAAK,SAAL,CAAe,CACjB,SAAU,QAAV,CAAqB,QADJ,CAEjB,GAAI,SAAS,QAAT,EAFa,CAAf,CARH,CAAP,EAYG,IAZH,CAYQ,cAAQ,CACZ,MAAM,IAAN,CAAW,QAAX,CAAqB,aAAO,CACxB,GAAI,GAAJ,CAAS,OACT,GAAM,aAAc,EAAE,eAAF,EAAmB,IAAnB,gBAAuC,KAAK,QAA5C,OAA0D,MAA1D,EAApB,CACA,YAAY,QAAZ,CAAqB,iBAArB,EAAwC,KAAxC,CAA8C,IAA9C,EAAoD,KAApD,CAA0D,UAAM,CAC5D,YAAY,WAAZ,CAAwB,iBAAxB,EAA2C,OAA3C,EACH,CAFD,CAGH,CAND,CAOH,CApBD,EAoBG,IApBH,CAoBQ,eAAS,CACb,QAAQ,KAAR,CAAc,KAAd,EACA,GAAI,OAAQ,yDAAZ,CACA,GAAI,MAAO,OAAM,YAAb,GAA8B,WAA9B,EAA6C,MAAO,OAAM,YAAN,CAAmB,KAA1B,GAAoC,WAArF,CAAkG,CAC9F,MAAQ,MAAM,YAAN,CAAmB,KAC9B,CACD,KAAK,CACD,KAAM,OADL,CAED,MAAO,SAFN,CAGD,KAAM,IAHL,CAID,KAAM,MAAK,eAAL,CAAqB,KAArB,CAJL,CAAL,CAMH,CAhCD,CAiCH,C;;ACjiBL,a,8oBAqBM,iB,YACF,2BAAc,wCACV,KAAK,UAAL,CAAkB,IACrB,C,8DAEK,CACF,KAAK,cAAL,GACA,KAAK,UAAL,EACH,C,0CAEQ,M,CAAQ,CACb,EAAE,QAAF,EAAY,IAAZ,CAAiB,iBAAjB,EAAoC,MAApC,GACA,GAAI,CAAC,EAAE,MAAF,CAAS,KAAK,UAAd,CAAL,CAAgC,KAAK,UAAL,CAAgB,WAAhB,CAA4B,QAA5B,EAEhC,GAAI,aAAc,EAAE,eAAF,EAAmB,IAAnB,CAAwB,aAAxB,CAAlB,CACA,GAAI,OAAO,IAAP,CAAY,MAAZ,IAAwB,QAA5B,CAAsC,CAClC,GAAM,WAAY,OAAO,IAAP,CAAY,4BAAZ,CAAlB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,WAAf,CAAnB,CAApB,CACA,GAAM,aAAc,mBAAmB,UAAU,IAAV,CAAe,MAAf,CAAnB,CAApB,CACA,eAAiB,WAAjB,CAA+B,WAClC,CAED,GAAI,WAAY,kFAAhB,CAEA,GAAI,YAAY,WAAZ,CAAwB,SAA5B,CAAuC,CACnC,WAAa,iPAEhB,CAED,GAAI,YAAY,WAAZ,CAAwB,SAA5B,CAAuC,CACnC,WAAa,kGAChB,CAED,GAAI,YAAY,WAAZ,CAAwB,aAA5B,CAA2C,CACvC,WAAa,kIAChB,CAED,GAAI,YAAY,WAAZ,CAAwB,eAA5B,CAA6C,CACzC,WAAa,8HAChB,CAED,GAAI,YAAY,WAAZ,CAAwB,WAA5B,CAAyC,CACrC,WAAa,+FAC4C,YAAY,MAAZ,CAAmB,SAD/D,CAC0E,kBAD1E,CAC+F,EAAE,OAAF,EAAW,IAAX,CAAgB,WAAhB,EAA6B,IAA7B,EAD/F,CACqI,6MAErJ,CAED,GAAI,YAAY,WAAZ,CAAwB,aAAxB,EAAyC,YAAY,WAAZ,CAAwB,WAArE,CAAkF,CAC9E,WAAa,2BAChB,CAED,GAAI,YAAY,WAAZ,CAAwB,aAA5B,CAA2C,CACvC,WAAa,4HAChB,CAED,GAAI,YAAY,WAAZ,CAAwB,WAA5B,CAAyC,CACrC,WAAa,0HAChB,CAED,WAAa,OAAb,CACA,MAAO,UACV,C,+CAEY,gBACT,EAAE,4BAAF,EAAgC,EAAhC,CAAmC,WAAnC,CAAgD,eAAS,CACrD,MAAM,cAAN,GACA,GAAI,EAAE,QAAF,EAAY,IAAZ,CAAiB,iBAAjB,EAAoC,EAApC,CAAuC,UAAvC,CAAJ,CAAwD,CACpD,EAAE,MAAF,EAAU,OAAV,CAAkB,OAAlB,EACA,MACH,CACD,MAAK,QAAL,CAAc,KAAd,CACH,CAPD,EAQA,EAAE,0BAAF,EAA8B,EAA9B,CAAiC,aAAjC,CAAgD,eAAS,CACrD,MAAK,QAAL,CAAc,KAAd,CACH,CAFD,CAGH,C,0CAEQ,K,CAAO,iBACZ,GAAM,QAAS,EAAE,MAAM,MAAR,EAAgB,OAAhB,CAAwB,IAAxB,CAAf,CACA,GAAM,MAAO,EAAE,KAAK,QAAL,CAAc,MAAd,CAAF,CAAb,CAEA,GAAI,OAAO,IAAP,CAAY,MAAZ,IAAwB,UAA5B,CAAwC,OACxC,MAAM,cAAN,GAEA,EAAE,IAAF,EAAQ,QAAR,CAAiB,MAAjB,EACA,EAAE,IAAF,EAAQ,IAAR,CAAa,WAAb,CAA0B,EAAE,MAAM,MAAR,CAA1B,EAA2C,IAA3C,GAAkD,GAAlD,CAAsD,CAClD,SAAU,UADwC,CAElD,KAAM,MAAM,KAAN,CAAc,GAF8B,CAGlD,IAAK,MAAM,KAHuC,CAAtD,EAMA,KAAK,UAAL,CAAkB,MAAlB,CACA,KAAK,UAAL,CAAgB,QAAhB,CAAyB,QAAzB,EAGA,GAAM,SAAU,GAAI,aAAJ,CAAiB,MAAjB,CAAyB,IAAzB,CAAhB,CACA,GAAI,YAAY,WAAZ,CAAwB,SAA5B,CAAuC,CACnC,EAAE,IAAF,EAAQ,IAAR,CAAa,wBAAb,EAAuC,MAAvC,GAAgD,EAAhD,CAAmD,OAAnD,CAA4D,WAAK,CAC7D,EAAE,cAAF,GACA,QAAQ,IAAR,EACH,CAHD,EAIA,EAAE,IAAF,EAAQ,IAAR,CAAa,0BAAb,EAAyC,MAAzC,GAAkD,EAAlD,CAAqD,OAArD,CAA8D,WAAK,CAC/D,EAAE,cAAF,GACA,QAAQ,MAAR,EACH,CAHD,CAIH,CAED,GAAI,YAAY,WAAZ,CAAwB,SAA5B,CAAuC,CACnC,EAAE,IAAF,EAAQ,IAAR,CAAa,wBAAb,EAAuC,MAAvC,GAAgD,EAAhD,CAAmD,OAAnD,CAA4D,WAAK,CAC7D,EAAE,cAAF,GACA,QAAQ,IAAR,EACH,CAHD,CAIH,CAED,GAAI,YAAY,WAAZ,CAAwB,aAA5B,CAA2C,CACvC,GAAI,OAAO,IAAP,CAAY,MAAZ,IAAwB,QAA5B,CAAsC,CAClC,EAAE,IAAF,EAAQ,IAAR,CAAa,4BAAb,EAA2C,WAA3C,CAAuD,QAAvD,CACH,CACD,EAAE,IAAF,EAAQ,IAAR,CAAa,4BAAb,EAA2C,MAA3C,GAAoD,EAApD,CAAuD,OAAvD,CAAgE,WAAK,CACjE,EAAE,cAAF,GACA,QAAQ,QAAR,EACH,CAHD,CAIH,CAED,GAAI,YAAY,WAAZ,CAAwB,eAA5B,CAA6C,CACzC,GAAI,EAAE,OAAF,CAAU,CAAC,iBAAD,CAAoB,kBAApB,CAAwC,oBAAxC,CAAV,CAAyE,OAAO,IAAP,CAAY,MAAZ,CAAzE,EAA8F,MAA9F,CAAuG,CAA3G,CAA8G,CAC1G,EAAE,IAAF,EAAQ,IAAR,CAAa,8BAAb,EAA6C,WAA7C,CAAyD,QAAzD,CACH,CACD,EAAE,IAAF,EAAQ,IAAR,CAAa,8BAAb,EAA6C,MAA7C,GAAsD,EAAtD,CAAyD,OAAzD,CAAkE,WAAK,CACnE,EAAE,cAAF,GACA,QAAQ,UAAR,EACH,CAHD,CAIH,CAED,GAAI,YAAY,WAAZ,CAAwB,WAA5B,CAAyC,CACrC,EAAE,IAAF,EAAQ,IAAR,CAAa,0BAAb,EAAyC,MAAzC,GAAkD,EAAlD,CAAqD,OAArD,CAA8D,WAAK,CAC/D,EAAE,cAAF,GACA,QAAQ,MAAR,EACH,CAHD,CAIH,CAED,GAAI,YAAY,WAAZ,CAAwB,aAA5B,CAA2C,CACvC,GAAI,OAAO,IAAP,CAAY,MAAZ,IAAwB,MAA5B,CAAoC,CAChC,EAAE,IAAF,EAAQ,IAAR,CAAa,4BAAb,EAA2C,WAA3C,CAAuD,QAAvD,CACH,CACD,EAAE,IAAF,EAAQ,IAAR,CAAa,4BAAb,EAA2C,MAA3C,GAAoD,EAApD,CAAuD,OAAvD,CAAgE,WAAK,CACjE,EAAE,cAAF,GACA,QAAQ,QAAR,EACH,CAHD,CAIH,CAED,GAAI,YAAY,WAAZ,CAAwB,WAA5B,CAAyC,CACrC,EAAE,IAAF,EAAQ,IAAR,CAAa,0BAAb,EAAyC,MAAzC,GAAkD,EAAlD,CAAqD,OAArD,CAA8D,WAAK,CAC/D,EAAE,cAAF,GACA,QAAQ,MAAR,EACH,CAHD,CAIH,CAED,EAAE,MAAF,EAAU,MAAV,GAAmB,EAAnB,CAAsB,OAAtB,CAA+B,eAAS,CACpC,GAAG,EAAE,MAAM,MAAR,EAAgB,EAAhB,CAAmB,oBAAnB,CAAH,CAA6C,CACzC,MAAM,cAAN,GACA,MACH,CACD,EAAE,IAAF,EAAQ,MAAR,GAAiB,MAAjB,GACA,GAAG,CAAC,EAAE,MAAF,CAAS,OAAK,UAAd,CAAJ,CAA+B,OAAK,UAAL,CAAgB,WAAhB,CAA4B,QAA5B,CAClC,CAPD,CAQH,C,uDAEgB,CACb,EAAE,iCAAF,EAAqC,EAArC,CAAwC,OAAxC,CAAiD,SAAU,KAAV,CAAiB,CAC9D,MAAM,cAAN,GAEA,GAAM,MAAO,EAAE,IAAF,EAAQ,MAAR,GAAiB,IAAjB,CAAsB,MAAtB,GAAiC,EAA9C,CACA,GAAM,MAAO,EAAE,IAAF,EAAQ,MAAR,GAAiB,IAAjB,CAAsB,MAAtB,GAAiC,EAA9C,CAEA,OAAO,QAAP,CAAgB,IAAhB,CAAuB,mBAAmB,KAAO,IAA1B,CAAvB,CACA,MAAM,IAAN,EACH,CARD,CASH,C,+BAGL,OAAO,WAAP,CAAqB,GAAI,iBAAzB;AC1MA,a,q3BAqBM,Y,YACF,sBAAc,mCACV,KAAK,IAAL,CAAU,KAAK,UAAL,EAAV,CACH,C,0DAEI,I,CAAM,I,CAAM,gBACb,GAAI,EAAE,WAAF,CAAc,IAAd,CAAJ,CAAyB,CACrB,KAAO,KAAK,UAAL,EACV,CAED,KAAK,MAAL,CAAY,IAAZ,EACA,EAAE,IAAF,CAAO,CACH,KAAM,MADH,CAEH,IAAK,YAAY,IAAZ,CAAiB,aAFnB,CAGH,QAAS,CACL,eAAgB,YAAY,IAAZ,CAAiB,SAD5B,CAHN,CAMH,KAAM,CACF,UAAW,IADT,CANH,CAAP,EASG,IATH,CASQ,cAAQ,CACZ,MAAK,MAAL,CAAY,KAAZ,EACA,EAAE,aAAF,EAAiB,OAAjB,CAAyB,EAAzB,EAA6B,IAA7B,CAAkC,IAAlC,EAAwC,SAAxC,CAAkD,EAAlD,CAAsD,UAAM,CACxD,YAAY,GAAZ,GACA,MAAK,iBAAL,GACA,MAAK,eAAL,GACA,MAAK,UAAL,GACA,MAAK,SAAL,GACA,MAAK,iBAAL,GACA,MAAK,SAAL,GACA,GAAI,EAAE,UAAF,CAAa,IAAb,CAAJ,CAAwB,CACpB,MAAO,OACV,CACJ,CAXD,EAYA,EAAE,iBAAF,EAAqB,OAArB,GAEA,GAAI,OAAO,MAAP,mCAAO,KAAP,KAAiB,QAArB,CAA+B,CAC3B,MAAM,aAAN,CAAoB,SAAS,cAAT,CAAwB,oBAAxB,CAApB,CACH,CACJ,CA5BD,EA4BG,IA5BH,CA4BQ,eAAS,CACb,MAAK,MAAL,CAAY,KAAZ,EACA,GAAI,EAAE,UAAF,CAAa,IAAb,CAAJ,CAAwB,CACpB,MAAO,MAAK,GAAI,MAAJ,CAAU,8BAAV,CAAL,CACV,CAED,GAAK,OAAS,EAAT,EAAe,OAAS,GAAzB,EAAiC,MAAM,MAAN,GAAiB,GAAtD,CAA2D,CACvD,MAAO,OAAK,IAAL,CAAU,EAAV,CAAc,IAAd,CACV,CAED,KAAK,CACD,KAAM,OADL,CAED,MAAO,YAFN,CAGD,KAAM,MAAM,YAAN,CAAmB,MAAnB,CAA0B,CAA1B,EAA6B,MAA7B,EAAuC,+EAH5C,CAAL,EAKA,QAAQ,KAAR,CAAc,KAAd,CACH,CA5CD,CA6CH,C,sCAEM,I,CAAM,CACT,GAAI,IAAJ,CAAS,CACL,EAAE,eAAF,EAAmB,MAAnB,CAA0B,GAA1B,CACH,CAFD,IAEO,CACH,EAAE,eAAF,EAAmB,OAAnB,CAA2B,GAA3B,CACH,CACJ,C,6DAEmB,iBAChB,EAAE,+BAAF,EAAmC,MAAnC,GAA4C,EAA5C,CAA+C,OAA/C,CAAwD,UAAM,CAC1D,EAAE,+BAAF,EAAmC,QAAnC,CAA4C,SAA5C,EACA,OAAK,IAAL,EACH,CAHD,CAIH,C,+CAEY,CACT,EAAE,8BAAF,EAAkC,EAAlC,CAAqC,OAArC,CAA8C,eAAS,CACnD,MAAM,cAAN,EACH,CAFD,CAGH,C,6CAEW,CACR,EAAE,2BAAF,EAA+B,EAA/B,CAAkC,OAAlC,CAA2C,eAAS,CAChD,MAAM,cAAN,EACH,CAFD,CAGH,C,6DAEmB,CAChB,EAAE,oCAAF,EAAwC,EAAxC,CAA2C,WAA3C,CAAwD,eAAS,CAC7D,GAAI,aAAJ,GAAmB,cAAnB,EACH,CAFD,CAGH,C,yDAEiB,CACd,EAAE,4BAAF,EAAgC,MAAhC,GAAyC,EAAzC,CAA4C,OAA5C,CAAqD,UAAM,CACvD,GAAI,aAAJ,GAAmB,MAAnB,CAA0B,EAAE,eAAF,EAAmB,IAAnB,CAAwB,aAAxB,GAA0C,GAApE,CACH,CAFD,CAGH,C,6CAEW,CACV,EAAE,kBAAF,EAAsB,EAAtB,CAAyB,WAAzB,CAAsC,eAAS,CAC3C,GAAI,MAAM,KAAN,GAAgB,CAApB,CAAuB,CACnB,GAAI,EAAE,MAAM,MAAR,EAAgB,EAAhB,CAAmB,IAAnB,GAA4B,EAAE,MAAM,MAAR,EAAgB,EAAhB,CAAmB,gCAAnB,CAAhC,CAAsF,CAClF,GAAI,aAAJ,GAAmB,YAAnB,CAAgC,KAAhC,CACH,CAFD,IAEO,IAAI,EAAE,MAAM,MAAR,EAAgB,EAAhB,CAAmB,IAAnB,GAA4B,EAAE,MAAM,MAAR,EAAgB,EAAhB,CAAmB,mCAAnB,CAAhC,CAAyF,CAC5F,GAAI,aAAJ,GAAmB,eAAnB,CAAmC,KAAnC,CACH,CAED,GAAI,aAAJ,GAAmB,iBAAnB,EACH,CACJ,CAVD,CAWD,C,+CAEY,CACT,MAAO,oBAAmB,OAAO,QAAP,CAAgB,IAAhB,CAAqB,SAArB,CAA+B,CAA/B,CAAnB,CACV,C,0BAIL,OAAO,KAAP,CAAe,GAAI,YAAnB","file":"filemanager.min.js","sourcesContent":["\"use strict\";\n\n// Copyright (c) 2015 - 2017 Dane Everitt \n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nclass ActionsClass {\n constructor(element, menu) {\n this.element = element;\n this.menu = menu;\n }\n\n destroy() {\n this.element = undefined;\n }\n\n sanitizedString(value) {\n return $('
    ').text(value).html();\n }\n\n folder(path) {\n let inputValue\n if (path) {\n inputValue = path\n } else {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const currentName = decodeURIComponent(nameBlock.data('name'));\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n\n if ($(this.element).data('type') === 'file') {\n inputValue = currentPath;\n } else {\n inputValue = `${currentPath}${currentName}/`;\n }\n }\n\n swal({\n type: 'input',\n title: 'Create Folder',\n text: 'Please enter the path and folder name below.',\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true,\n inputValue: inputValue\n }, (val) => {\n if (val === false) {\n return false;\n }\n\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/folder`,\n timeout: 10000,\n data: JSON.stringify({\n path: val,\n }),\n }).done(data => {\n swal.close();\n Files.list();\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occurred while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: '',\n text: error,\n });\n });\n });\n }\n\n move() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const currentName = decodeURIComponent(nameBlock.attr('data-name'));\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n\n swal({\n type: 'input',\n title: 'Move File',\n text: 'Please enter the new path for the file below.',\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true,\n inputValue: `${currentPath}${currentName}`,\n }, (val) => {\n if (val === false) {\n return false;\n }\n\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/move`,\n timeout: 10000,\n data: JSON.stringify({\n from: `${currentPath}${currentName}`,\n to: `${val}`,\n }),\n }).done(data => {\n nameBlock.parent().addClass('warning').delay(200).fadeOut();\n swal.close();\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occurred while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: '',\n text: error,\n });\n });\n });\n\n }\n\n rename() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const currentLink = nameBlock.find('a');\n const currentName = decodeURIComponent(nameBlock.attr('data-name'));\n const attachEditor = `\n \n \n `;\n\n nameBlock.html(attachEditor);\n const inputField = nameBlock.find('input');\n const inputLoader = nameBlock.find('.input-loader');\n\n inputField.focus();\n inputField.on('blur keydown', e => {\n // Save Field\n if (\n (e.type === 'keydown' && e.which === 27)\n || e.type === 'blur'\n || (e.type === 'keydown' && e.which === 13 && currentName === inputField.val())\n ) {\n if (!_.isEmpty(currentLink)) {\n nameBlock.html(currentLink);\n } else {\n nameBlock.html(currentName);\n }\n inputField.remove();\n ContextMenu.unbind().run();\n return;\n }\n\n if (e.type === 'keydown' && e.which !== 13) return;\n\n inputLoader.show();\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/rename`,\n timeout: 10000,\n data: JSON.stringify({\n from: `${currentPath}${currentName}`,\n to: `${currentPath}${inputField.val()}`,\n }),\n }).done(data => {\n nameBlock.attr('data-name', inputField.val());\n if (!_.isEmpty(currentLink)) {\n let newLink = currentLink.attr('href');\n if (nameBlock.parent().data('type') !== 'folder') {\n newLink = newLink.substr(0, newLink.lastIndexOf('/')) + '/' + inputField.val();\n }\n currentLink.attr('href', newLink);\n nameBlock.html(\n currentLink.html(inputField.val())\n );\n } else {\n nameBlock.html(inputField.val());\n }\n inputField.remove();\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occurred while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n nameBlock.addClass('has-error').delay(2000).queue(() => {\n nameBlock.removeClass('has-error').dequeue();\n });\n inputField.popover({\n animation: true,\n placement: 'top',\n content: error,\n title: 'Save Error'\n }).popover('show');\n }).always(() => {\n inputLoader.remove();\n ContextMenu.unbind().run();\n });\n });\n }\n\n copy() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const currentName = decodeURIComponent(nameBlock.attr('data-name'));\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n\n swal({\n type: 'input',\n title: 'Copy File',\n text: 'Please enter the new path for the copied file below.',\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true,\n inputValue: `${currentPath}${currentName}`,\n }, (val) => {\n if (val === false) {\n return false;\n }\n\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/copy`,\n timeout: 10000,\n data: JSON.stringify({\n from: `${currentPath}${currentName}`,\n to: `${val}`,\n }),\n }).done(data => {\n swal({\n type: 'success',\n title: '',\n text: 'File successfully copied.'\n });\n Files.list();\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occurred while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: '',\n text: error,\n });\n });\n });\n }\n\n download() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const fileName = decodeURIComponent(nameBlock.attr('data-name'));\n const filePath = decodeURIComponent(nameBlock.data('path'));\n\n window.location = `/server/${Pterodactyl.server.uuidShort}/files/download/${filePath}${fileName}`;\n }\n\n delete() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const delPath = decodeURIComponent(nameBlock.data('path'));\n const delName = decodeURIComponent(nameBlock.data('name'));\n\n swal({\n type: 'warning',\n title: '',\n text: 'Are you sure you want to delete ' + this.sanitizedString(delName) + '?',\n html: true,\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true\n }, () => {\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/delete`,\n timeout: 10000,\n data: JSON.stringify({\n items: [`${delPath}${delName}`]\n }),\n }).done(data => {\n nameBlock.parent().addClass('warning').delay(200).fadeOut();\n swal({\n type: 'success',\n title: 'File Deleted'\n });\n }).fail(jqXHR => {\n console.error(jqXHR);\n swal({\n type: 'error',\n title: 'Whoops!',\n html: true,\n text: 'An error occurred while attempting to delete this file. Please try again.',\n });\n });\n });\n }\n\n toggleMassActions() {\n if ($('#file_listing input[type=\"checkbox\"]:checked').length) {\n $('#mass_actions').removeClass('disabled');\n } else {\n $('#mass_actions').addClass('disabled');\n }\n }\n\n toggleHighlight(event) {\n const parent = $(event.currentTarget);\n const item = $(event.currentTarget).find('input');\n\n if($(item).is(':checked')) {\n $(item).prop('checked', false);\n parent.removeClass('warning').delay(200);\n } else {\n $(item).prop('checked', true);\n parent.addClass('warning').delay(200);\n }\n }\n\n highlightAll(event) {\n let parent;\n const item = $(event.currentTarget).find('input');\n\n if($(item).is(':checked')) {\n $('#file_listing input[type=checkbox]').prop('checked', false);\n $('#file_listing input[data-action=\"addSelection\"]').each(function() {\n parent = $(this).closest('tr');\n parent.removeClass('warning').delay(200);\n });\n } else {\n $('#file_listing input[type=checkbox]').prop('checked', true);\n $('#file_listing input[data-action=\"addSelection\"]').each(function() {\n parent = $(this).closest('tr');\n parent.addClass('warning').delay(200);\n });\n }\n }\n\n deleteSelected() {\n let selectedItems = [];\n let selectedItemsElements = [];\n let parent;\n let nameBlock;\n let delLocation;\n\n $('#file_listing input[data-action=\"addSelection\"]:checked').each(function() {\n parent = $(this).closest('tr');\n nameBlock = $(parent).find('td[data-identifier=\"name\"]');\n delLocation = decodeURIComponent(nameBlock.data('path')) + decodeURIComponent(nameBlock.data('name'));\n\n selectedItems.push(delLocation);\n selectedItemsElements.push(parent);\n });\n\n if (selectedItems.length != 0)\n {\n let formattedItems = \"\";\n let i = 0;\n $.each(selectedItems, function(key, value) {\n formattedItems += (\"\" + this.sanitizedString(value) + \", \");\n i++;\n return i < 5;\n });\n\n formattedItems = formattedItems.slice(0, -2);\n if (selectedItems.length > 5) {\n formattedItems += ', and ' + (selectedItems.length - 5) + ' other(s)';\n }\n\n swal({\n type: 'warning',\n title: '',\n text: 'Are you sure you want to delete the following files: ' + this.sanitizedString(formattedItems) + '?',\n html: true,\n showCancelButton: true,\n showConfirmButton: true,\n closeOnConfirm: false,\n showLoaderOnConfirm: true\n }, () => {\n $.ajax({\n type: 'POST',\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/delete`,\n timeout: 10000,\n data: JSON.stringify({\n items: selectedItems\n }),\n }).done(data => {\n $('#file_listing input:checked').each(function() {\n $(this).prop('checked', false);\n });\n\n $.each(selectedItemsElements, function() {\n $(this).addClass('warning').delay(200).fadeOut();\n })\n\n swal({\n type: 'success',\n title: 'Files Deleted'\n });\n }).fail(jqXHR => {\n console.error(jqXHR);\n swal({\n type: 'error',\n title: 'Whoops!',\n html: true,\n text: 'An error occurred while attempting to delete these files. Please try again.',\n });\n });\n });\n } else {\n swal({\n type: 'warning',\n title: '',\n text: 'Please select files/folders to delete.',\n });\n }\n }\n\n decompress() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const compPath = decodeURIComponent(nameBlock.data('path'));\n const compName = decodeURIComponent(nameBlock.data('name'));\n\n swal({\n title: ' Decompressing...',\n text: 'This might take a few seconds to complete.',\n html: true,\n allowOutsideClick: false,\n allowEscapeKey: false,\n showConfirmButton: false,\n });\n\n $.ajax({\n type: 'POST',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/decompress`,\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n data: JSON.stringify({\n files: `${compPath}${compName}`\n })\n }).done(data => {\n swal.close();\n Files.list(compPath);\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occurred while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: 'Whoops!',\n html: true,\n text: error\n });\n });\n }\n\n compress() {\n const nameBlock = $(this.element).find('td[data-identifier=\"name\"]');\n const compPath = decodeURIComponent(nameBlock.data('path'));\n const compName = decodeURIComponent(nameBlock.data('name'));\n\n $.ajax({\n type: 'POST',\n url: `${Pterodactyl.node.scheme}://${Pterodactyl.node.fqdn}:${Pterodactyl.node.daemonListen}/v1/server/file/compress`,\n headers: {\n 'X-Access-Token': Pterodactyl.server.daemonSecret,\n 'X-Access-Server': Pterodactyl.server.uuid,\n },\n contentType: 'application/json; charset=utf-8',\n data: JSON.stringify({\n files: `${compPath}${compName}`,\n to: compPath.toString()\n })\n }).done(data => {\n Files.list(compPath, err => {\n if (err) return;\n const fileListing = $('#file_listing').find(`[data-name=\"${data.saved_as}\"]`).parent();\n fileListing.addClass('success pulsate').delay(3000).queue(() => {\n fileListing.removeClass('success pulsate').dequeue();\n });\n });\n }).fail(jqXHR => {\n console.error(jqXHR);\n var error = 'An error occurred while trying to process this request.';\n if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {\n error = jqXHR.responseJSON.error;\n }\n swal({\n type: 'error',\n title: 'Whoops!',\n html: true,\n text: this.sanitizedString(error)\n });\n });\n }\n}\n","\"use strict\";\n\n// Copyright (c) 2015 - 2017 Dane Everitt \n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nclass ContextMenuClass {\n constructor() {\n this.activeLine = null;\n }\n\n run() {\n this.directoryClick();\n this.rightClick();\n }\n\n makeMenu(parent) {\n $(document).find('#fileOptionMenu').remove();\n if (!_.isNull(this.activeLine)) this.activeLine.removeClass('active');\n\n let newFilePath = $('#file_listing').data('current-dir');\n if (parent.data('type') === 'folder') {\n const nameBlock = parent.find('td[data-identifier=\"name\"]');\n const currentName = decodeURIComponent(nameBlock.attr('data-name'));\n const currentPath = decodeURIComponent(nameBlock.data('path'));\n newFilePath = `${currentPath}${currentName}`;\n }\n\n let buildMenu = '
      ';\n\n if (Pterodactyl.permissions.moveFiles) {\n buildMenu += '
    • Rename
    • \\\n
    • Move
    • ';\n }\n\n if (Pterodactyl.permissions.copyFiles) {\n buildMenu += '
    • Copy
    • ';\n }\n\n if (Pterodactyl.permissions.compressFiles) {\n buildMenu += '
    • Compress
    • ';\n }\n\n if (Pterodactyl.permissions.decompressFiles) {\n buildMenu += '
    • Decompress
    • ';\n }\n\n if (Pterodactyl.permissions.createFiles) {\n buildMenu += '
    • \\\n
    • ').text(newFilePath).html() + '\" class=\"text-muted\"> New File
    • \\\n
    • New Folder
    • ';\n }\n\n if (Pterodactyl.permissions.downloadFiles || Pterodactyl.permissions.deleteFiles) {\n buildMenu += '
    • ';\n }\n\n if (Pterodactyl.permissions.downloadFiles) {\n buildMenu += '
    • Download
    • ';\n }\n\n if (Pterodactyl.permissions.deleteFiles) {\n buildMenu += '
    • Delete
    • ';\n }\n\n buildMenu += '
    ';\n return buildMenu;\n }\n\n rightClick() {\n $('[data-action=\"toggleMenu\"]').on('mousedown', event => {\n event.preventDefault();\n if ($(document).find('#fileOptionMenu').is(':visible')) {\n $('body').trigger('click');\n return;\n }\n this.showMenu(event);\n });\n $('#file_listing > tbody td').on('contextmenu', event => {\n this.showMenu(event);\n });\n }\n\n showMenu(event) {\n const parent = $(event.target).closest('tr');\n const menu = $(this.makeMenu(parent));\n\n if (parent.data('type') === 'disabled') return;\n event.preventDefault();\n\n $(menu).appendTo('body');\n $(menu).data('invokedOn', $(event.target)).show().css({\n position: 'absolute',\n left: event.pageX - 150,\n top: event.pageY,\n });\n\n this.activeLine = parent;\n this.activeLine.addClass('active');\n\n // Handle Events\n const Actions = new ActionsClass(parent, menu);\n if (Pterodactyl.permissions.moveFiles) {\n $(menu).find('li[data-action=\"move\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.move();\n });\n $(menu).find('li[data-action=\"rename\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.rename();\n });\n }\n\n if (Pterodactyl.permissions.copyFiles) {\n $(menu).find('li[data-action=\"copy\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.copy();\n });\n }\n\n if (Pterodactyl.permissions.compressFiles) {\n if (parent.data('type') === 'folder') {\n $(menu).find('li[data-action=\"compress\"]').removeClass('hidden');\n }\n $(menu).find('li[data-action=\"compress\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.compress();\n });\n }\n\n if (Pterodactyl.permissions.decompressFiles) {\n if (_.without(['application/zip', 'application/gzip', 'application/x-gzip'], parent.data('mime')).length < 3) {\n $(menu).find('li[data-action=\"decompress\"]').removeClass('hidden');\n }\n $(menu).find('li[data-action=\"decompress\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.decompress();\n });\n }\n\n if (Pterodactyl.permissions.createFiles) {\n $(menu).find('li[data-action=\"folder\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.folder();\n });\n }\n\n if (Pterodactyl.permissions.downloadFiles) {\n if (parent.data('type') === 'file') {\n $(menu).find('li[data-action=\"download\"]').removeClass('hidden');\n }\n $(menu).find('li[data-action=\"download\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.download();\n });\n }\n\n if (Pterodactyl.permissions.deleteFiles) {\n $(menu).find('li[data-action=\"delete\"]').unbind().on('click', e => {\n e.preventDefault();\n Actions.delete();\n });\n }\n\n $(window).unbind().on('click', event => {\n if($(event.target).is('.disable-menu-hide')) {\n event.preventDefault();\n return;\n }\n $(menu).unbind().remove();\n if(!_.isNull(this.activeLine)) this.activeLine.removeClass('active');\n });\n }\n\n directoryClick() {\n $('a[data-action=\"directory-view\"]').on('click', function (event) {\n event.preventDefault();\n\n const path = $(this).parent().data('path') || '';\n const name = $(this).parent().data('name') || '';\n\n window.location.hash = encodeURIComponent(path + name);\n Files.list();\n });\n }\n}\n\nwindow.ContextMenu = new ContextMenuClass;\n","\"use strict\";\n\n// Copyright (c) 2015 - 2017 Dane Everitt \n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\nclass FileManager {\n constructor() {\n this.list(this.decodeHash());\n }\n\n list(path, next) {\n if (_.isUndefined(path)) {\n path = this.decodeHash();\n }\n\n this.loader(true);\n $.ajax({\n type: 'POST',\n url: Pterodactyl.meta.directoryList,\n headers: {\n 'X-CSRF-Token': Pterodactyl.meta.csrftoken,\n },\n data: {\n directory: path,\n },\n }).done(data => {\n this.loader(false);\n $('#load_files').slideUp(10).html(data).slideDown(10, () => {\n ContextMenu.run();\n this.reloadFilesButton();\n this.addFolderButton();\n this.selectItem();\n this.selectAll();\n this.selectiveDeletion();\n this.selectRow();\n if (_.isFunction(next)) {\n return next();\n }\n });\n $('#internal_alert').slideUp();\n\n if (typeof Siofu === 'object') {\n Siofu.listenOnInput(document.getElementById(\"files_touch_target\"));\n }\n }).fail(jqXHR => {\n this.loader(false);\n if (_.isFunction(next)) {\n return next(new Error('Failed to load file listing.'));\n }\n\n if ((path !== '' && path !== '/') && jqXHR.status === 404) {\n return this.list('', next);\n }\n\n swal({\n type: 'error',\n title: 'File Error',\n text: jqXHR.responseJSON.errors[0].detail || 'An error occurred while attempting to process this request. Please try again.',\n });\n console.error(jqXHR);\n });\n }\n\n loader(show) {\n if (show){\n $('.file-overlay').fadeIn(100);\n } else {\n $('.file-overlay').fadeOut(100);\n }\n }\n\n reloadFilesButton() {\n $('i[data-action=\"reload-files\"]').unbind().on('click', () => {\n $('i[data-action=\"reload-files\"]').addClass('fa-spin');\n this.list();\n });\n }\n\n selectItem() {\n $('[data-action=\"addSelection\"]').on('click', event => {\n event.preventDefault();\n });\n }\n\n selectAll() {\n $('[data-action=\"selectAll\"]').on('click', event => {\n event.preventDefault();\n });\n }\n\n selectiveDeletion() {\n $('[data-action=\"selective-deletion\"]').on('mousedown', event => {\n new ActionsClass().deleteSelected();\n });\n }\n\n addFolderButton() {\n $('[data-action=\"add-folder\"]').unbind().on('click', () => {\n new ActionsClass().folder($('#file_listing').data('current-dir') || '/');\n });\n }\n\n selectRow() {\n $('#file_listing tr').on('mousedown', event => {\n if (event.which === 1) {\n if ($(event.target).is('th') || $(event.target).is('input[data-action=\"selectAll\"]')) {\n new ActionsClass().highlightAll(event);\n } else if ($(event.target).is('td') || $(event.target).is('input[data-action=\"addSelection\"]')) {\n new ActionsClass().toggleHighlight(event);\n }\n\n new ActionsClass().toggleMassActions();\n }\n });\n }\n\n decodeHash() {\n return decodeURIComponent(window.location.hash.substring(1));\n }\n\n}\n\nwindow.Files = new FileManager;\n"]} \ No newline at end of file diff --git a/public/themes/pterodactyl/js/frontend/files/src/actions.js b/public/themes/pterodactyl/js/frontend/files/src/actions.js index 0c3b839d6..5b9a95c54 100644 --- a/public/themes/pterodactyl/js/frontend/files/src/actions.js +++ b/public/themes/pterodactyl/js/frontend/files/src/actions.js @@ -29,6 +29,10 @@ class ActionsClass { this.element = undefined; } + sanitizedString(value) { + return $('
    ').text(value).html(); + } + folder(path) { let inputValue if (path) { @@ -296,7 +300,7 @@ class ActionsClass { swal({ type: 'warning', title: '', - text: 'Are you sure you want to delete ' + delName + '?', + text: 'Are you sure you want to delete ' + this.sanitizedString(delName) + '?', html: true, showCancelButton: true, showConfirmButton: true, @@ -394,7 +398,7 @@ class ActionsClass { let formattedItems = ""; let i = 0; $.each(selectedItems, function(key, value) { - formattedItems += ("" + value + ", "); + formattedItems += ("" + this.sanitizedString(value) + ", "); i++; return i < 5; }); @@ -407,7 +411,7 @@ class ActionsClass { swal({ type: 'warning', title: '', - text: 'Are you sure you want to delete the following files: ' + formattedItems + '?', + text: 'Are you sure you want to delete the following files: ' + this.sanitizedString(formattedItems) + '?', html: true, showCancelButton: true, showConfirmButton: true, @@ -536,7 +540,7 @@ class ActionsClass { type: 'error', title: 'Whoops!', html: true, - text: error + text: this.sanitizedString(error) }); }); } diff --git a/public/themes/pterodactyl/js/frontend/files/src/contextmenu.js b/public/themes/pterodactyl/js/frontend/files/src/contextmenu.js index 0e6904385..6796f8b09 100644 --- a/public/themes/pterodactyl/js/frontend/files/src/contextmenu.js +++ b/public/themes/pterodactyl/js/frontend/files/src/contextmenu.js @@ -62,7 +62,7 @@ class ContextMenuClass { if (Pterodactyl.permissions.createFiles) { buildMenu += '
  • \ -
  • New File
  • \ +
  • New File
  • \
  • New Folder
  • '; } diff --git a/resources/lang/en/auth.php b/resources/lang/en/auth.php index 176de8f64..2a3a45268 100644 --- a/resources/lang/en/auth.php +++ b/resources/lang/en/auth.php @@ -1,22 +1,27 @@ 'You are not authorized to perform this action.', - 'auth_error' => 'There was an error while attempting to login.', - 'authentication_required' => 'Authentication is required to continue.', - 'remember_me' => 'Remember Me', 'sign_in' => 'Sign In', - 'forgot_password' => 'I\'ve forgotten my password!', - 'request_reset_text' => 'Forgotten your account password? It is not the end of the world, just provide your email below.', - 'reset_password_text' => 'Reset your account password.', - 'reset_password' => 'Reset Account Password', - 'email_sent' => 'An email has been sent to you with further instructions for resetting your password.', - 'failed' => 'The credentials provided do not match those we have on record, or the 2FA token provided was invalid.', + 'go_to_login' => 'Go to Login', + 'failed' => 'No account matching those credentials could be found.', + + 'forgot_password' => [ + 'label' => 'Forgot Password?', + 'label_help' => 'Enter your account email address to receive instructions on resetting your password.', + 'button' => 'Recover Account', + ], + + 'reset_password' => [ + 'button' => 'Reset and Sign In', + ], + + 'two_factor' => [ + 'label' => '2-Factor Token', + 'label_help' => 'This account requires a second layer of authentication in order to continue. Please enter the code generated by your device to complete this login.', + 'checkpoint_failed' => 'The two-factor authentication token was invalid.', + ], + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', - 'password_requirements' => 'Passwords must contain at least one uppercase, lowercase, and numeric character and must be at least 8 characters in length.', - 'request_reset' => 'Locate Account', - '2fa_required' => '2-Factor Authentication', - '2fa_failed' => 'The 2FA token provided was invalid.', - 'totp_failed' => 'There was an error while attempting to validate TOTP.', + 'password_requirements' => 'Password must be at least 8 characters in length and should be unique to this site.', '2fa_must_be_enabled' => 'The administrator has required that 2-Factor Authentication be enabled for your account in order to use the Panel.', ]; diff --git a/resources/lang/en/base.php b/resources/lang/en/base.php index e2c661067..2646dc4f1 100644 --- a/resources/lang/en/base.php +++ b/resources/lang/en/base.php @@ -54,36 +54,4 @@ return [ ], ], ], - 'account' => [ - 'details_updated' => 'Your account details have been successfully updated.', - 'invalid_password' => 'The password provided for your account was not valid.', - 'header' => 'Your Account', - 'header_sub' => 'Manage your account details.', - 'update_pass' => 'Update Password', - 'update_email' => 'Update Email Address', - 'current_password' => 'Current Password', - 'new_password' => 'New Password', - 'new_password_again' => 'Repeat New Password', - 'new_email' => 'New Email Address', - 'first_name' => 'First Name', - 'last_name' => 'Last Name', - 'update_identity' => 'Update Identity', - 'username_help' => 'Your username must be unique to your account, and may only contain the following characters: :requirements.', - 'language' => 'Language', - ], - 'security' => [ - 'session_mgmt_disabled' => 'Your host has not enabled the ability to manage account sessions via this interface.', - 'header' => 'Account Security', - 'header_sub' => 'Control active sessions and 2-Factor Authentication.', - 'sessions' => 'Active Sessions', - '2fa_header' => '2-Factor Authentication', - '2fa_token_help' => 'Enter the 2FA Token generated by your app (Google Authenticator, Authy, etc.).', - 'disable_2fa' => 'Disable 2-Factor Authentication', - '2fa_enabled' => '2-Factor Authentication is enabled on this account and will be required in order to login to the panel. If you would like to disable 2FA, simply enter a valid token below and submit the form.', - '2fa_disabled' => '2-Factor Authentication is disabled on your account! You should enable 2FA in order to add an extra level of protection on your account.', - 'enable_2fa' => 'Enable 2-Factor Authentication', - '2fa_qr' => 'Configure 2FA on Your Device', - '2fa_checkpoint_help' => 'Use the 2FA application on your phone to take a picture of the QR code to the left, or manually enter the code under it. Once you have done so, generate a token and enter it below.', - '2fa_disable_error' => 'The 2FA token provided was not valid. Protection has not been disabled for this account.', - ], ]; diff --git a/resources/lang/en/dashboard/account.php b/resources/lang/en/dashboard/account.php new file mode 100644 index 000000000..85411ef65 --- /dev/null +++ b/resources/lang/en/dashboard/account.php @@ -0,0 +1,28 @@ + [ + 'title' => 'Update your email', + 'updated' => 'Your email address has been updated.', + ], + 'password' => [ + 'title' => 'Change your password', + 'requirements' => 'Your new password should be at least 8 characters in length.', + 'updated' => 'Your password has been updated.', + ], + 'two_factor' => [ + 'button' => 'Configure 2-Factor Authentication', + 'disabled' => 'Two-factor authentication has been disabled on your account. You will no longer be prompted to provide a token when logging in.', + 'enabled' => 'Two-factor authentication has been enabled on your account! From now on, when logging in, you will be required to provide the code generated by your device.', + 'invalid' => 'The token provided was invalid.', + 'setup' => [ + 'title' => 'Setup two-factor authentication', + 'help' => 'Can\'t scan the code? Enter the code below into your application:', + 'field' => 'Enter token', + ], + 'disable' => [ + 'title' => 'Disable two-factor authentication', + 'field' => 'Enter token', + ], + ], +]; diff --git a/resources/lang/en/dashboard/index.php b/resources/lang/en/dashboard/index.php new file mode 100644 index 000000000..8ab11e994 --- /dev/null +++ b/resources/lang/en/dashboard/index.php @@ -0,0 +1,8 @@ + 'Search for servers...', + 'no_matches' => 'There were no servers found matching the search criteria provided.', + 'cpu_title' => 'CPU', + 'memory_title' => 'Memory', +]; diff --git a/resources/lang/en/strings.php b/resources/lang/en/strings.php index c0bf3f417..c9fbb6349 100644 --- a/resources/lang/en/strings.php +++ b/resources/lang/en/strings.php @@ -2,9 +2,11 @@ return [ 'email' => 'Email', + 'email_address' => 'Email address', 'user_identifier' => 'Username or Email', 'password' => 'Password', - 'confirm_password' => 'Confirm Password', + 'new_password' => 'New password', + 'confirm_password' => 'Confirm new password', 'login' => 'Login', 'home' => 'Home', 'servers' => 'Servers', @@ -85,4 +87,8 @@ return [ 'sat' => 'Saturday', ], 'last_used' => 'Last Used', + 'enable' => 'Enable', + 'disable' => 'Disable', + 'save' => 'Save', + 'copyright' => '© 2015 - :year Pterodactyl Software', ]; diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index 201880ec9..a82aaa1be 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -101,5 +101,6 @@ return [ // Internal validation logic for Pterodactyl 'internal' => [ 'variable_value' => ':env variable', + 'invalid_password' => 'The password provided was invalid for this account.', ], ]; diff --git a/resources/lang/ru/admin/nests.php b/resources/lang/ru/admin/nests.php new file mode 100644 index 000000000..b07441b11 --- /dev/null +++ b/resources/lang/ru/admin/nests.php @@ -0,0 +1,33 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +return [ + 'notices' => [ + 'created' => 'Новое гнездо :name успешно создано.', + 'deleted' => 'Успешно удалили запрошенное гнездо с панели.', + 'updated' => 'Успешно обновлены параметры конфигурации гнезда.', + ], + 'eggs' => [ + 'notices' => [ + 'imported' => 'Успешно импортировано это яйцо и связанные с ним переменные.', + 'updated_via_import' => 'Это яйцо было обновлено с использованием предоставленного файла.', + 'deleted' => 'Успешно удалили запрошенное яйцо из панели.', + 'updated' => 'Конфигурация яйца успешно обновлена.', + 'script_updated' => 'Сценарий установки Egg обновлен и будет запускаться всякий раз, когда будут установлены серверы.', + 'egg_created' => 'Новое яйцо было добавлено успешно. Вам нужно будет перезапустить все работающие демоны, чтобы применить это новое яйцо.', + ], + ], + 'variables' => [ + 'notices' => [ + 'variable_deleted' => 'Переменная ":variable" была удалена и больше не будет доступна серверам после восстановления.', + 'variable_updated' => 'Переменная ":variable" была обновлена. Вам нужно будет перестроить все серверы, использующие эту переменную, чтобы применить изменения.', + 'variable_created' => 'Новая переменная была успешно создана и назначена этому яйцу.', + ], + ], +]; diff --git a/resources/lang/ru/admin/node.php b/resources/lang/ru/admin/node.php new file mode 100644 index 000000000..b4abb5731 --- /dev/null +++ b/resources/lang/ru/admin/node.php @@ -0,0 +1,23 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +return [ + 'validation' => [ + 'fqdn_not_resolvable' => 'Указанное полное доменное имя или IP-адрес не преобразуется в действительный IP-адрес.', + 'fqdn_required_for_ssl' => 'Для использования SSL для этого узла требуется полное доменное имя, которое разрешается в публичный IP-адрес.', + ], + 'notices' => [ + 'allocations_added' => 'Локации успешно добавлены в этот узел.', + 'node_deleted' => 'Узел успешно удален с панели.', + 'location_required' => 'У вас должно быть настроено хотя бы одно местоположение, прежде чем вы сможете добавить узел на эту панель.', + 'node_created' => 'Успешно создан новый узел. Вы можете автоматически настроить демон на этом компьютере, посетив вкладку \'Configuration\'. Прежде чем вы сможете добавить какие-либо серверы, вы должны сначала выделить как минимум один IP-адрес и порт.', + 'node_updated' => 'Информация об узле обновлена. Если какие-либо настройки демона были изменены, вам нужно будет перезагрузить их, чтобы эти изменения вступили в силу.', + 'unallocated_deleted' => 'Удалил все нераспределенные порты для :ip.', + ], +]; diff --git a/resources/lang/ru/admin/pack.php b/resources/lang/ru/admin/pack.php new file mode 100644 index 000000000..0ff9911db --- /dev/null +++ b/resources/lang/ru/admin/pack.php @@ -0,0 +1,16 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +return [ + 'notices' => [ + 'pack_updated' => 'Пак успешно обновлен.', + 'pack_deleted' => 'Успешно удален пакет ":name" из системы.', + 'pack_created' => 'Новый пакет был успешно создан в системе и теперь доступен для развертывания на серверах.', + ], +]; diff --git a/resources/lang/ru/admin/server.php b/resources/lang/ru/admin/server.php new file mode 100644 index 000000000..59fb1882a --- /dev/null +++ b/resources/lang/ru/admin/server.php @@ -0,0 +1,31 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +return [ + 'exceptions' => [ + 'no_new_default_allocation' => 'Вы пытаетесь удалить выделение по умолчанию для этого сервера, но резервное выделение для использования отсутствует.', + 'marked_as_failed' => 'Этот сервер был помечен как Проблемный предыдущей установки. Текущий статус не может быть переключен в этом состоянии.', + 'bad_variable' => 'Произошла ошибка проверки с переменной :name.', + 'daemon_exception' => 'При попытке установить связь с демоном возникла исключительная ситуация, в результате которой был получен код ответа HTTP / :code. Это исключение было зарегистрировано.', + 'default_allocation_not_found' => 'Запрошенное распределение по умолчанию не было найдено в выделениях этого сервера.', + ], + 'alerts' => [ + 'startup_changed' => 'Начальная конфигурация для этого сервера была обновлена. Если гнездо или яйцо этого сервера было изменено, то теперь будет происходить переустановка.', + 'server_deleted' => 'Сервер успешно удален из системы.', + 'server_created' => 'Сервер буспешно создан из панели. Пожалуйста, дайте демону несколько минут, чтобы полностью установить этот сервер.', + 'build_updated' => 'Детали сборки для этого сервера были обновлены. Некоторые изменения могут потребовать перезагрузки для вступления в силу.', + 'suspension_toggled' => 'Состояние приостановки сервера изменено на :status.', + 'rebuild_on_boot' => 'Этот сервер был помечен как требующий перестройки Docker Container. Это произойдет при следующем запуске сервера.', + 'install_toggled' => 'Состояние установки для этого сервера переключено.', + 'server_reinstalled' => 'Этот сервер поставлен в очередь для начала переустановки.', + 'details_updated' => 'Данные сервера успешно обновлены.', + 'docker_image_updated' => 'Успешно изменен образ Docker по умолчанию для использования на этом сервере. Чтобы применить это изменение, требуется перезагрузка.', + 'node_required' => 'У вас должен быть настроен хотя бы один узел, прежде чем вы сможете добавить сервер на эту панель.', + ], +]; diff --git a/resources/lang/ru/admin/user.php b/resources/lang/ru/admin/user.php new file mode 100644 index 000000000..6273da77e --- /dev/null +++ b/resources/lang/ru/admin/user.php @@ -0,0 +1,18 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +return [ + 'exceptions' => [ + 'user_has_servers' => 'Невозможно удалить пользователя с активными серверами, подключенными к его учетной записи. Пожалуйста, удалите их серверы, прежде чем продолжить.', + ], + 'notices' => [ + 'account_created' => 'Аккаунт успешно создан.', + 'account_updated' => 'Аккаунт успешно обновлен.', + ], +]; diff --git a/resources/lang/ru/auth.php b/resources/lang/ru/auth.php new file mode 100644 index 000000000..d14bd8cc3 --- /dev/null +++ b/resources/lang/ru/auth.php @@ -0,0 +1,22 @@ + 'Вы не авторизованы для выполнения этого действия.', + 'auth_error' => 'Произошла ошибка при попытке войти.', + 'authentication_required' => 'Для продолжения требуется Аутентификация.', + 'remember_me' => 'Запомнить меня', + 'sign_in' => 'Войти в систему', + 'forgot_password' => 'Я забыл свой пароль!', + 'request_reset_text' => 'Забыли пароль учетной записи? Это не конец света, просто укажите свой адрес электронной почты.', + 'reset_password_text' => 'Сбросить пароль вашей учетной записи.', + 'reset_password' => 'Сбросить пароль учетной записи', + 'email_sent' => 'Вам отправлено электронное письмо с дальнейшими инструкциями по восстановлению пароля.', + 'failed' => 'Предоставленные учетные данные не совпадают с теми, которые есть у нас в записи, или предоставленный токен 2FA недействителен.', + 'throttle' => 'Слишком много попыток входа в систему. Пожалуйста, повторите попытку через :seconds секунд.', + 'password_requirements' => 'Пароли должны содержать как минимум одну заглавную, строчную букву, цифры и содержать не менее 8 символов.', + 'request_reset' => 'Найти аккаунт', + '2fa_required' => 'Двухфакторная аутентификация', + '2fa_failed' => 'Указанный токен 2FA недействителен.', + 'totp_failed' => 'Произошла ошибка при попытке проверить TOTP.', + '2fa_must_be_enabled' => 'Администратор потребовал, чтобы для вашей учетной записи была включена двухфакторная аутентификация, чтобы использовать панель.', +]; diff --git a/resources/lang/ru/base.php b/resources/lang/ru/base.php new file mode 100644 index 000000000..f73f8f422 --- /dev/null +++ b/resources/lang/ru/base.php @@ -0,0 +1,89 @@ + 'Произошла ошибка с одним или несколькими полями в запросе.', + 'errors' => [ + 'return' => 'Вернуться на предыдущую страницу', + 'home' => 'Домой', + '403' => [ + 'header' => 'Запрещено', + 'desc' => 'У вас нет прав доступа к ресурсам на этом сервере.', + ], + '404' => [ + 'header' => 'Файл не найден', + 'desc' => 'Нам не удалось найти запрошенный ресурс на сервере.', + ], + 'installing' => [ + 'header' => 'Установка сервера', + 'desc' => 'Запрошенный сервер все еще завершает процесс установки. Пожалуйста, зайдите через несколько минут, вы должны получить электронное письмо, как только этот процесс будет завершен.', + ], + 'suspended' => [ + 'header' => 'Сервер приостановлен', + 'desc' => 'Этот сервер был приостановлен и недоступен.', + ], + 'maintenance' => [ + 'header' => 'Узел в обслуживании', + 'title' => 'Временно недоступен', + 'desc' => 'Этот узел находится на обслуживании, поэтому ваш сервер может быть временно недоступен.', + ], + ], + 'index' => [ + 'header' => 'Ваши серверы', + 'header_sub' => 'Серверы, к которым у вас есть доступ.', + 'list' => 'Список серверов', + ], + 'api' => [ + 'index' => [ + 'list' => 'Ваши ключи', + 'header' => 'API аккаунта', + 'header_sub' => 'Управляйте клавишами доступа, которые позволяют вам выполнять действия с панелью.', + 'create_new' => 'Создать новый ключ API', + 'keypair_created' => 'Ключ API был успешно сгенерирован и показан ниже.', + ], + 'new' => [ + 'header' => 'овый ключ API', + 'header_sub' => 'Создайте новый ключ доступа к учетной записи.', + 'form_title' => 'Подробности', + 'descriptive_memo' => [ + 'title' => 'Описание', + 'description' => 'Введите краткое описание этого ключа, которое будет полезно для справки.', + ], + 'allowed_ips' => [ + 'title' => 'Разрешенные IP-адреса', + 'description' => 'Введите разделенный строкой список IP-адресов, которым разрешен доступ к API с помощью этого ключа. Нотация CIDR разрешена. Оставьте пустым, чтобы разрешить любой IP.', + ], + ], + ], + 'account' => [ + 'details_updated' => 'Данные вашей учетной записи успешно обновлены.', + 'invalid_password' => 'Пароль для вашей учетной записи недействителен.', + 'header' => 'Ваш аккаунт', + 'header_sub' => 'Управляйте данными своей учетной записи.', + 'update_pass' => 'Обновить пароль', + 'update_email' => 'Обновить адрес электронной почты', + 'current_password' => 'Текущий пароль', + 'new_password' => 'Новый пароль', + 'new_password_again' => 'Повторите новый пароль', + 'new_email' => 'Новый E-mail адрес', + 'first_name' => 'Имя', + 'last_name' => 'Фамилия', + 'update_identity' => 'Обновить данные', + 'username_help' => 'Ваше имя пользователя должно быть уникальным для вашей учетной записи и может содержать только следующие символы: :requirements.', + 'language' => 'Язык', + ], + 'security' => [ + 'session_mgmt_disabled' => 'Ваш хост не включил возможность управлять сеансами аккаунта через этот интерфейс.', + 'header' => 'Безопасность аккаунта', + 'header_sub' => 'Контроль активных сессий и 2-х факторной аутентификации.', + 'sessions' => 'Активная сессия', + '2fa_header' => '2-х факторная аутентификация', + '2fa_token_help' => 'Введите токен 2FA, созданный вашим приложением (Google Authenticator, Authy, и так далее.).', + 'disable_2fa' => 'Отключить двухфакторную аутентификацию', + '2fa_enabled' => 'В этой учетной записи включена двухфакторная аутентификация, которая потребуется для входа в панель. Если вы хотите отключить 2FA, просто введите действительный токен ниже и отправьте форму.', + '2fa_disabled' => 'Двухфакторная аутентификация отключена на вашем аккаунте! Вы должны включить 2FA, чтобы добавить дополнительный уровень защиты для вашей учетной записи.', + 'enable_2fa' => 'Включить двухфакторную аутентификацию', + '2fa_qr' => 'Настройте 2FA на вашем устройстве', + '2fa_checkpoint_help' => 'Используйте приложение 2FA на своем телефоне, чтобы сделать снимок QR-кода, или вручную введите код под ним. После этого сгенерируйте токен и введите его ниже.', + '2fa_disable_error' => 'Указанный токен 2FA недействителен. Защита не была отключена для этой учетной записи.', + ], +]; diff --git a/resources/lang/ru/command/messages.php b/resources/lang/ru/command/messages.php new file mode 100644 index 000000000..b4aab6475 --- /dev/null +++ b/resources/lang/ru/command/messages.php @@ -0,0 +1,97 @@ + [ + 'warning' => 'Похоже, вы уже настроили ключ шифрования приложения. Продолжая этот процесс с перезаписать этот ключ и вызвать повреждение данных для любых существующих зашифрованных данных. НЕ ПРОДОЛЖАЙТЕ, ЕСЛИ ВЫ НЕ ЗНАЕТЕ, ЧТО ВЫ ДЕЛАЕТЕ.', + 'confirm' => 'Я понимаю последствия выполнения этой команды и принимаю на себя всю ответственность за потерю зашифрованных данных.', + 'final_confirm' => 'Вы уверены, что хотите продолжить? Изменение ключа шифрования приложения приведет к потере данных.', + ], + 'location' => [ + 'no_location_found' => 'Не удалось найти запись, соответствующую предоставленному короткому коду.', + 'ask_short' => 'Короткий код местоположения', + 'ask_long' => 'Описание местоположения', + 'created' => 'Успешно создано новое местоположение (:name) с идентификатором :id.', + 'deleted' => 'Успешно удалено запрошенное местоположение.', + ], + 'user' => [ + 'search_users' => 'Введите имя пользователя, UUID или адрес электронной почты', + 'select_search_user' => 'ID пользователя для удаления (Enter \'0\' to re-search)', + 'deleted' => 'Пользователь успешно удален из Панели.', + 'confirm_delete' => 'Вы уверены, что хотите удалить этого пользователя с панели?', + 'no_users_found' => 'По данному запросу не найдено ни одного пользователя.', + 'multiple_found' => 'Для предоставленного пользователя было найдено несколько учетных записей, не удалось удалить пользователя из-за --no-interaction flag.', + 'ask_admin' => 'Является ли этот пользователь администратором?', + 'ask_email' => 'Адрес электронной почты', + 'ask_username' => 'Ник пользователя', + 'ask_name_first' => 'Имя', + 'ask_name_last' => 'Фамилия', + 'ask_password' => 'Пароль', + 'ask_password_tip' => 'Если вы хотите создать учетную запись со случайным паролем, отправленным пользователю по электронной почте, повторите команду (CTRL + C) и передайте `--no-password` flag.', + 'ask_password_help' => 'Пароли должны быть длиной не менее 8 символов и содержать как минимум одну заглавную букву и цифру.', + '2fa_help_text' => [ + 'Эта команда отключит двухфакторную аутентификацию для учетной записи пользователя, если она включена. Это следует использовать только в качестве команды восстановления учетной записи, если пользователь заблокирован в своей учетной записи.', + 'Если это не то, что вы хотели сделать, нажмите CTRL + C, чтобы выйти из этого процесса.', + ], + '2fa_disabled' => 'Двухфакторная аутентификация была отключена для :email.', + ], + 'schedule' => [ + 'output_line' => 'Диспетчерская работа для первой задачи в `:schedule` (:hash).', + ], + 'maintenance' => [ + 'deleting_service_backup' => 'Удаление файла резервной копии службы :file.', + ], + 'server' => [ + 'rebuild_failed' => 'Запрос на перестроение для ":name" (#:id) на узле ":node" завершился ошибкой: :message', + 'power' => [ + 'confirm' => 'Вы собираетесь выполнить :action против :count серверов. Вы хотите продолжить?', + 'action_failed' => 'Запрос на действие питания для ":name" (#:id) на узле ":node" завершился ошибкой: :message', + ], + ], + 'environment' => [ + 'mail' => [ + 'ask_smtp_host' => 'SMTP Хост (e.g. smtp.gmail.com)', + 'ask_smtp_port' => 'SMTP Порт', + 'ask_smtp_username' => 'SMTP Пользователь', + 'ask_smtp_password' => 'SMTP Password', + 'ask_mailgun_domain' => 'Почтовый Домен', + 'ask_mailgun_secret' => 'Mailgun Secret', + 'ask_mandrill_secret' => 'Mandrill Secret', + 'ask_postmark_username' => 'Postmark API Key', + 'ask_driver' => 'Какой драйвер следует использовать для отправки писем?', + 'ask_mail_from' => 'Адреса электронной почты должны исходить от', + 'ask_mail_name' => 'Имя, с которого должны начинаться электронные письма', + 'ask_encryption' => 'Метод шифрования для использования', + ], + 'database' => [ + 'host_warning' => 'Настоятельно рекомендуется не использовать "localhost" в качестве хоста базы данных, поскольку мы часто сталкиваемся с проблемами подключения к сокету. Если вы хотите использовать локальное соединение, вы должны использовать "127.0.0.1".', + 'host' => 'Хост базы данных', + 'port' => 'Порт базы данных', + 'database' => 'Название базы данных', + 'username_warning' => 'Использование учетной записи "root" для подключений MySQL не только не одобряется, но и не допускается этим приложением. Вам нужно создать пользователя MySQL для этого программного обеспечения.', + 'username' => 'Имя пользователя базы данных', + 'password_defined' => 'Похоже, вы уже определили пароль для подключения к MySQL, хотите изменить его?', + 'password' => 'Пароль базы данных', + 'connection_error' => 'Невозможно подключиться к серверу MySQL с использованием предоставленных учетных данных. Возвращенная ошибка ":error".', + 'creds_not_saved' => 'Ваши учетные данные НЕ были сохранены. Вам нужно будет предоставить действительную информацию о соединении, прежде чем продолжить.', + 'try_again' => 'Вернуться и попробуйте снова?', + ], + 'app' => [ + 'settings' => 'Включить редактор настроек на основе интерфейса?', + 'author' => 'E-mail автора яйца', + 'author_help' => 'Укажите адрес электронной почты, с которого должны быть отправлены яйца, экспортируемые этой панелью. Это должен быть действительный адрес электронной почты.', + 'app_url_help' => 'URL приложения ДОЛЖЕН начинаться с https:// или http:// в зависимости от того, используете ли вы SSL или нет. Если вы не включите схему, ваши электронные письма и другой контент будут ссылаться на неправильное местоположение.', + 'app_url' => 'URL приложения', + 'timezone_help' => 'Часовой пояс должен соответствовать одному из поддерживаемых часовых поясов PHP. Если вы не уверены, пожалуйста перейдите по ссылке http://php.net/manual/en/timezones.php.', + 'timezone' => 'Часовой пояс приложения', + 'cache_driver' => 'Драйвер кеша', + 'session_driver' => 'Драйвер сеанса', + 'queue_driver' => 'Драйвер очереди ', + 'using_redis' => 'Вы выбрали драйвер Redis для одного или нескольких параметров. Пожалуйста, предоставьте действительную информацию о подключении ниже. В большинстве случаев вы можете использовать предоставленные значения по умолчанию, если только вы не изменили настройки.', + 'redis_host' => 'Ност Redis', + 'redis_password' => 'Пароль Redis', + 'redis_pass_help' => 'По умолчанию экземпляр сервера Redis не имеет пароля, поскольку он работает локально и недоступен для внешнего мира. Если это так, просто нажмите Enter, не вводя значение.', + 'redis_port' => 'Порт Redis', + 'redis_pass_defined' => 'Кажется, пароль для Redis уже определен, вы хотите его изменить?', + ], + ], +]; diff --git a/resources/lang/ru/exceptions.php b/resources/lang/ru/exceptions.php new file mode 100644 index 000000000..6c3afbb49 --- /dev/null +++ b/resources/lang/ru/exceptions.php @@ -0,0 +1,68 @@ + 'При попытке установить связь с демоном возникла исключительная ситуация, в результате которой был получен код ответа HTTP /:code. Это исключение было зарегистрировано.', + 'node' => [ + 'servers_attached' => 'Узел не должен иметь серверов, связанных с ним, для удаления.', + 'daemon_off_config_updated' => 'Конфигурация демона была обновлена, однако при попытке автоматического обновления файла конфигурации в демоне произошла ошибка. Вам нужно будет вручную обновить файл конфигурации (core.json), чтобы демон применил эти изменения.', + ], + 'allocations' => [ + 'server_using' => 'Сервер в настоящее время назначен на это распределение. Распределение может быть удалено, только если в данный момент не назначен сервер.', + 'too_many_ports' => 'Добавление более 1000 портов в одном диапазоне одновременно не поддерживается.', + 'invalid_mapping' => 'Отображение, предусмотренное для :port, было недействительным и не могло быть обработано.', + 'cidr_out_of_range' => 'Нотация CIDR допускает маски только от / 25 до / 32.', + 'port_out_of_range' => 'Порты в распределении должны быть больше 1024 и меньше или равны 65535.', + ], + 'nest' => [ + 'delete_has_servers' => 'Гнездо с подключенными к нему активными серверами нельзя удалить с панели.', + 'egg' => [ + 'delete_has_servers' => 'Яйцо с подключенными к нему активными серверами нельзя удалить с панели.', + 'invalid_copy_id' => 'Яйцо, выбранное для копирования скрипта, либо не существует, либо копирует сам скрипт.', + 'must_be_child' => 'Директива "Copy Settings From" для этого яйца должна быть дочерней для выбранного гнезда.', + 'has_children' => 'Это яйцо является родителем одного или нескольких других яиц. Пожалуйста, удалите эти яйца перед удалением этого яйца.', + ], + 'variables' => [ + 'env_not_unique' => 'Переменная окружения :name должно быть уникальным для этого яйца.', + 'reserved_name' => 'Переменная среды :name защищено и не может быть присвоено переменной.', + 'bad_validation_rule' => 'Правило проверки ":rule" не является допустимым правилом для этого приложения.', + ], + 'importer' => [ + 'json_error' => 'Произошла ошибка при попытке проанализировать файл JSON: :error.', + 'file_error' => 'Предоставленный файл JSON недействителен.', + 'invalid_json_provided' => 'Предоставленный файл JSON не в формате, который может быть распознан.', + ], + ], + 'packs' => [ + 'delete_has_servers' => 'Невозможно удалить пакет, прикрепленный к активным серверам.', + 'update_has_servers' => 'Невозможно изменить идентификатор связанной опции, когда серверы в данный момент подключены к пакету.', + 'invalid_upload' => 'Предоставленный файл не является действительным.', + 'invalid_mime' => 'Предоставленный файл не соответствует требуемому типу :type', + 'unreadable' => 'Предоставленный архив не может быть открыт сервером.', + 'zip_extraction' => 'Возникла исключительная ситуация при попытке извлечь архив, предоставленный на сервер.', + 'invalid_archive_exception' => 'В архиве пакета отсутствует требуемый файл archive.tar.gz или import.json в базовом каталоге.', + ], + 'subusers' => [ + 'editing_self' => 'Редактирование вашей учетной записи подпользователя запрещено.', + 'user_is_owner' => 'Вы не можете добавить владельца сервера в качестве подпользователя для этого сервера.', + 'subuser_exists' => 'Пользователь с таким адресом электронной почты уже назначен в качестве подпользователя для этого сервера.', + ], + 'databases' => [ + 'delete_has_databases' => 'Невозможно удалить хост-сервер базы данных, с которым связаны активные базы данных.', + ], + 'tasks' => [ + 'chain_interval_too_long' => 'Максимальный интервал времени для связанной задачи составляет 15 минут.', + ], + 'locations' => [ + 'has_nodes' => 'Невозможно удалить местоположение, к которому прикреплены активные узлы.', + ], + 'users' => [ + 'node_revocation_failed' => 'Не удалось отозвать ключи на Node #:node. :error', + ], + 'deployment' => [ + 'no_viable_nodes' => 'Узлы, удовлетворяющие требованиям, указанным для автоматического развертывания, не найдены.', + 'no_viable_allocations' => 'Местоположений, удовлетворяющих требованиям для автоматического развертывания, обнаружено не было.', + ], + 'api' => [ + 'resource_not_found' => 'Запрашиваемый ресурс не существует на этом сервере.', + ], +]; diff --git a/resources/lang/ru/navigation.php b/resources/lang/ru/navigation.php new file mode 100644 index 000000000..a924c2188 --- /dev/null +++ b/resources/lang/ru/navigation.php @@ -0,0 +1,32 @@ + 'Главная', + 'account' => [ + 'header' => 'УПРАВЛЕНИЕ АККАУНТОМ', + 'my_account' => 'Мой аккаунт', + 'security_controls' => 'Контроль безопасности', + 'api_access' => 'API аккаунта', + 'my_servers' => 'Мои серверы', + ], + 'server' => [ + 'header' => 'УПРАВЛЕНИЕ СЕРВЕРАМИ', + 'console' => 'Консоль', + 'console-pop' => 'Полноэкранная консоль', + 'file_management' => 'Управление файлами', + 'file_browser' => 'Браузер файлов', + 'create_file' => 'Создать файл', + 'upload_files' => 'Загрузить файлы', + 'subusers' => 'Подпользователь', + 'schedules' => 'Расписание', + 'configuration' => 'Конфигурация', + 'port_allocations' => 'Настройки портов', + 'sftp_settings' => 'Настройки SFTP', + 'startup_parameters' => 'Параметры запуска', + 'databases' => 'Базы данных', + 'edit_file' => 'Редактировать файл', + 'admin_header' => 'АДМИНИСТРАТИВНЫЕ', + 'admin' => 'Конфигурация сервера', + 'server_name' => 'Название сервера', + ], +]; diff --git a/resources/lang/ru/pagination.php b/resources/lang/ru/pagination.php new file mode 100644 index 000000000..bae6004c3 --- /dev/null +++ b/resources/lang/ru/pagination.php @@ -0,0 +1,17 @@ + '« Предыдущий', + 'next' => 'Следующий »', +]; diff --git a/resources/lang/ru/passwords.php b/resources/lang/ru/passwords.php new file mode 100644 index 000000000..129e46a6c --- /dev/null +++ b/resources/lang/ru/passwords.php @@ -0,0 +1,19 @@ + 'Пароли должны содержать не менее шести символов и соответствовать подтверждению.', + 'reset' => 'Ваш пароль сброшен!', + 'sent' => 'Мы отправили вам ссылку для сброса пароля по электронной почте!', + 'token' => 'Этот токен сброса пароля недействителен.', + 'user' => 'Мы не можем найти пользователя с таким адресом электронной почты.', +]; diff --git a/resources/lang/ru/server.php b/resources/lang/ru/server.php new file mode 100644 index 000000000..d37bc5890 --- /dev/null +++ b/resources/lang/ru/server.php @@ -0,0 +1,334 @@ + [ + 'title' => 'Просмотр сервера :name', + 'header' => 'Консоль сервера', + 'header_sub' => 'Контролируйте свой сервер в режиме реального времени.', + ], + 'schedule' => [ + 'header' => 'Диспетчер расписаний', + 'header_sub' => 'Управляйте всеми расписаниями этого сервера в одном месте.', + 'current' => 'Текущие расписания', + 'new' => [ + 'header' => 'Создать новое расписание', + 'header_sub' => 'Создайте новый набор запланированных задач для этого сервера.', + 'submit' => 'Создать расписание', + ], + 'manage' => [ + 'header' => 'Управление расписанием', + 'submit' => 'Обновить расписание', + 'delete' => 'Удалить расписание', + ], + 'task' => [ + 'time' => 'После', + 'action' => 'Выполнить действие', + 'payload' => 'С полезной нагрузкой', + 'add_more' => 'Добавить еще одну задачу', + ], + 'actions' => [ + 'command' => 'Отправить команду', + 'power' => 'Управление питанием', + ], + 'toggle' => 'Переключить статус', + 'run_now' => 'Расписание запуска', + 'schedule_created' => 'Успешно создано новое расписание для этого сервера.', + 'schedule_updated' => 'Расписание обновлено.', + 'unnamed' => 'Расписание без имени', + 'setup' => 'Настройка расписания', + 'day_of_week' => 'День недели', + 'day_of_month' => 'День месяца', + 'hour' => 'Час дня', + 'minute' => 'Минута часа', + 'time_help' => 'Система расписаний поддерживает использование синтаксиса Cronjob при определении момента начала выполнения задач. Используйте поля выше, чтобы указать, когда эти задачи должны начать выполняться, или выберите параметры в меню множественного выбора.', + 'task_help' => 'Время выполнения заданий относительно ранее определенного задания. Каждому расписанию может быть назначено не более 5 задач, а задачи не могут быть запланированы с интервалом более 15 минут.', + ], + 'tasks' => [ + 'task_created' => 'Успешно создано новое задание на панели.', + 'task_updated' => 'Задача успешно обновлена. Любые действия задачи, поставленные в очередь, будут отменены и запущены снова в следующее определенное время.', + 'header' => 'Запланированные задачи', + 'header_sub' => 'Автоматизируйте свой сервер.', + 'current' => 'Текущие запланированные задачи', + 'actions' => [ + 'command' => 'Отправить команду', + 'power' => 'Опция управленя питанием ', + ], + 'new_task' => 'Добавить новую задачу', + 'toggle' => 'Переключить статус', + 'new' => [ + 'header' => 'Новое задание', + 'header_sub' => 'Создайте новое запланированное задание для этого сервера.', + 'task_name' => 'Название задачи', + 'day_of_week' => 'День недели', + 'custom' => 'Пользовательское значение', + 'day_of_month' => 'День месяца', + 'hour' => 'Час', + 'minute' => 'Минута', + 'sun' => 'Воскресенье', + 'mon' => 'Понедельник', + 'tues' => 'Вторник', + 'wed' => 'Среда', + 'thurs' => 'Четверг', + 'fri' => 'Пятница', + 'sat' => 'Суббота', + 'submit' => 'Создать задачу', + 'type' => 'Тип задачи', + 'chain_then' => 'Затем после', + 'chain_do' => 'Выполнять', + 'chain_arguments' => 'С аргументами', + 'payload' => 'Задача Полезная нагрузка', + 'payload_help' => 'Например, если вы выбрали Send Command, введите команду здесь. Если вы выбрали Send Power Option, укажите здесь действие power (например, restart).', + ], + 'edit' => [ + 'header' => 'Управление задачей', + 'submit' => 'Обновить задачу', + ], + ], + 'users' => [ + 'header' => 'Управление пользователями', + 'header_sub' => 'Контроль, кто может получить доступ к вашему серверу.', + 'configure' => 'Настроить разрешения', + 'list' => 'Аккаунты с доступом', + 'add' => 'Добавить нового пользователя', + 'update' => 'Обновить пользователя', + 'user_assigned' => 'Успешно назначен новый пользователь на этот сервер.', + 'user_updated' => 'Успешно обновлены разрешения.', + 'edit' => [ + 'header' => 'Редактировать пользователя', + 'header_sub' => 'Изменить доступ пользователя к серверу.', + ], + 'new' => [ + 'header' => 'Добавить нового пользователя', + 'header_sub' => 'Добавьте нового пользователя с разрешениями на этот сервер.', + 'email' => 'Адрес электронной почты', + 'email_help' => 'Введите адрес электронной почты для пользователя, которого вы хотите пригласить для управления этим сервером.', + 'power_header' => 'Управление питанием', + 'file_header' => 'Управление файлами', + 'subuser_header' => 'Управление пользователями', + 'server_header' => 'Управление сервером', + 'task_header' => 'Управление расписанием', + 'database_header' => 'Управление базой данных', + 'power_start' => [ + 'title' => 'Запустить сервер', + 'description' => 'Позволяет пользователю запустить сервер.', + ], + 'power_stop' => [ + 'title' => 'Остановить сервер.', + 'description' => 'Позволяет пользователю остановить сервер.', + ], + 'power_restart' => [ + 'title' => 'Перезагрузить сервер', + 'description' => 'Позволяет пользователю перезапустить сервер.', + ], + 'power_kill' => [ + 'title' => 'Убить Сервер', + 'description' => 'Позволяет пользователю убить процесс сервера.', + ], + 'send_command' => [ + 'title' => 'Отправить консольную команду', + 'description' => 'Позволяет отправить команду из консоли. Если у пользователя нет разрешений на остановку или перезапуск, он не может отправить команду stop', + ], + 'access_sftp' => [ + 'title' => 'SFTP разрешения', + 'description' => 'Позволяет пользователю подключаться к SFTP-серверу, предоставленному демоном.', + ], + 'list_files' => [ + 'title' => 'Список файлов', + 'description' => 'Позволяет пользователю перечислять все файлы и папки на сервере, но не просматривать содержимое файла.', + ], + 'edit_files' => [ + 'title' => 'Редактировать файлы', + 'description' => 'Позволяет пользователю открыть файл только для просмотра. SFTP не влияет на это разрешение.', + ], + 'save_files' => [ + 'title' => 'Сохранить файлы', + 'description' => 'По', + ], + 'move_files' => [ + 'title' => 'Переименовать и переместить файлы', + 'description' => 'Позволяет пользователю перемещать и переименовывать файлы и папки в файловой системе.', + ], + 'copy_files' => [ + 'title' => 'Копировать файлы', + 'description' => 'Позволяет пользователю копировать файлы и папки в файловой системе.', + ], + 'compress_files' => [ + 'title' => 'Сжатие файлов', + 'description' => 'Позволяет пользователю создавать архивы файлов и папок в системе.', + ], + 'decompress_files' => [ + 'title' => 'Распаковать файлы', + 'description' => 'Позволяет пользователю распаковать архивы .zip и .tar (.gz).', + ], + 'create_files' => [ + 'title' => 'Создать файлы', + 'description' => 'Позволяет пользователю создавать новый файл в панели.', + ], + 'upload_files' => [ + 'title' => 'Загрузить файлы', + 'description' => 'Позволяет пользователю загружать файлы через файловый менеджер.', + ], + 'delete_files' => [ + 'title' => 'Удалить файлы', + 'description' => 'Позволяет пользователю удалять файлы из системы.', + ], + 'download_files' => [ + 'title' => 'Скачать файлы', + 'description' => 'Позволяет пользователю загружать файлы. Если пользователю дано это разрешение, он может загружать и просматривать содержимое файла, даже если это разрешение не назначено на панели.', + ], + 'list_subusers' => [ + 'title' => 'Список пользователей', + 'description' => 'Позволяет пользователю просматривать список всех суб-пользователей, назначенных серверу.', + ], + 'view_subuser' => [ + 'title' => 'Просмотр пользователей', + 'description' => 'Позволяет пользователю просматривать разрешения, назначенные для пользователей.', + ], + 'edit_subuser' => [ + 'title' => 'Редактировать пользователей', + 'description' => 'Позволяет пользователю редактировать разрешения, назначенные другим пользователям.', + ], + 'create_subuser' => [ + 'title' => 'Создать пользователя', + 'description' => 'Позволяет пользователю создавать дополнительных пользователей на сервере.', + ], + 'delete_subuser' => [ + 'title' => 'Удалить пользователя', + 'description' => 'Позволяет пользователю удалять других пользователей на сервере.', + ], + 'view_allocations' => [ + 'title' => 'Посмотреть распределение', + 'description' => 'Позволяет пользователю просматривать все IP-адреса и порты, назначенные серверу.', + ], + 'edit_allocation' => [ + 'title' => 'Изменить подключение по умолчанию', + 'description' => 'Позволяет пользователю изменять распределение соединения по умолчанию для использования на сервере.', + ], + 'view_startup' => [ + 'title' => 'Просмотреть команду запуска', + 'description' => 'Позволяет пользователю просматривать команду запуска и связанные переменные для сервера.', + ], + 'edit_startup' => [ + 'title' => 'Редактировать команду запуска', + 'description' => 'Позволяет пользователю изменять переменные запуска для сервера.', + ], + 'list_schedules' => [ + 'title' => 'Список расписаний', + 'description' => 'Позволяет пользователю посмотреть все расписания (включенные и отключенные) для этого сервера.', + ], + 'view_schedule' => [ + 'title' => 'Посмотреть расписание', + 'description' => 'Позволяет пользователю просматривать детали конкретного расписания, включая все назначенные задачи.', + ], + 'toggle_schedule' => [ + 'title' => 'Переключить Расписание', + 'description' => 'Позволяет пользователю переключать расписание на активные или неактивные.', + ], + 'queue_schedule' => [ + 'title' => 'Очередь Расписаний', + 'description' => 'Позволяет пользователю ставить в очередь расписание для выполнения его задач в следующем цикле процесса.', + ], + 'edit_schedule' => [ + 'title' => 'Изменить расписание', + 'description' => 'Позволяет пользователю редактировать расписание, включая все задачи расписания. Это позволит пользователю удалять отдельные задачи, но не удалять само расписание.', + ], + 'create_schedule' => [ + 'title' => 'Создать расписание', + 'description' => 'Позволяет пользователю создавать новое расписание.', + ], + 'delete_schedule' => [ + 'title' => 'Удалить расписание', + 'description' => 'Позволяет пользователю удалить расписание с сервера.', + ], + 'view_databases' => [ + 'title' => 'Просмотр базы данных', + 'description' => 'Позволяет пользователю просматривать все базы данных, связанные с этим сервером, включая имена пользователей и пароли для баз данных.', + ], + 'reset_db_password' => [ + 'title' => 'Сбросить пароль базы данных', + 'description' => 'Позволяет пользователю сбросить пароли для баз данных.', + ], + 'delete_database' => [ + 'title' => 'Удалить базы данных', + 'description' => 'Позволяет пользователю удалять базы данных для этого сервера.', + ], + 'create_database' => [ + 'title' => 'Создать базу данных', + 'description' => 'Позволяет пользователю создавать дополнительные базы данных для этого сервера.', + ], + ], + ], + 'allocations' => [ + 'mass_actions' => 'Массовые акции', + 'delete' => 'Удалить распределение', + ], + 'files' => [ + 'exceptions' => [ + 'invalid_mime' => 'Этот тип файла не может быть отредактирован через встроенный редактор.', + 'max_size' => 'Этот файл слишком велик для редактирования через встроенный редактор.', + ], + 'header' => 'Файловый менеджер', + 'header_sub' => 'Управляйте всеми своими файлами прямо из Интернета.', + 'loading' => 'Загрузка исходной файловой структуры, это может занять несколько секунд.', + 'path' => 'При настройке любых путей к файлам в плагинах или настройках вашего сервера вы должны использовать :path в качестве базового пути. Максимальный размер загрузки файлов через Интернет на этот узел: :size.', + 'seconds_ago' => 'секунд назад', + 'file_name' => 'Имя файла', + 'size' => 'Размер', + 'last_modified' => 'Последнее изменение', + 'add_new' => 'Добавить новый файл', + 'add_folder' => 'Добавить новую папку', + 'mass_actions' => 'Массовые акции', + 'delete' => 'Удалить файлы', + 'edit' => [ + 'header' => 'Редактировать файл', + 'header_sub' => 'Внесите изменения в файл из Интернета.', + 'save' => 'Сохранить файл', + 'return' => 'Вернуться в файловый менеджер', + ], + 'add' => [ + 'header' => 'Новый файл', + 'header_sub' => 'Создайте новый файл на вашем сервере.', + 'name' => 'Имя файла', + 'create' => 'Создать файл', + ], + ], + 'config' => [ + 'name' => [ + 'header' => 'Название сервера', + 'header_sub' => 'Измените имя этого сервера.', + 'details' => 'Имя сервера является только ссылкой на этот сервер на панели и не влияет на какие-либо конкретные конфигурации серверов, которые могут отображаться для пользователей в играх.', + ], + 'startup' => [ + 'header' => 'Начать настройку', + 'header_sub' => 'Управляйте аргументами запуска сервера.', + 'command' => 'Команда запуска', + 'edit_params' => 'Изменить параметры', + 'update' => 'Обновить параметры запуска', + 'startup_regex' => 'Правила ввода', + 'edited' => 'Переменные запуска были успешно отредактированы. Они вступят в силу при следующем запуске этого сервера.', + ], + 'sftp' => [ + 'header' => 'Конфигурация SFTP', + 'header_sub' => 'Данные учетной записи для SFTP-соединений.', + 'details' => 'Подробности SFTP', + 'conn_addr' => 'Адрес подключения', + 'warning' => 'Пароль SFTP - это пароль вашей учетной записи. Убедитесь, что ваш клиент настроен на использование SFTP, а не FTP или FTPS для соединений, между протоколами есть разница.', + ], + 'database' => [ + 'header' => 'Базы данных', + 'header_sub' => 'Все базы данных доступные для этого сервера.', + 'your_dbs' => 'Настройки базы данных', + 'host' => 'MySQL Хост', + 'reset_password' => 'Сброс пароля', + 'no_dbs' => 'Для этого сервера нет баз данных.', + 'add_db' => 'Добавить новую базу данных.', + ], + 'allocation' => [ + 'header' => 'Порты сервера', + 'header_sub' => 'Управляйте IP-адресами и портами, доступными на этом сервере.', + 'available' => 'Доступные Распределения', + 'help' => 'Справка по Распределению портов.', + 'help_text' => 'Список включает в себя все доступные IP-адреса и порты, которые открыты для вашего сервера, чтобы использовать для входящих подключений.', + ], + ], +]; diff --git a/resources/lang/ru/strings.php b/resources/lang/ru/strings.php new file mode 100644 index 000000000..0dc9ff55e --- /dev/null +++ b/resources/lang/ru/strings.php @@ -0,0 +1,88 @@ + 'Эл. адрес', + 'user_identifier' => 'Имя пользователя или адрес электронной почты', + 'password' => 'Пароль', + 'confirm_password' => 'Подтвердите Пароль', + 'login' => 'Войти', + 'home' => 'Домой', + 'servers' => 'Серверы', + 'id' => 'ID', + 'name' => 'Название', + 'node' => 'Узел', + 'connection' => 'Подключение', + 'memory' => 'Память', + 'cpu' => 'Процессор', + 'status' => 'Статус', + 'search' => 'Поиск', + 'suspended' => 'Приостановленный', + 'account' => 'Аккаунт', + 'security' => 'Безопасность', + 'ip' => 'IP адрес', + 'last_activity' => 'Последняя активность', + 'revoke' => 'Отменить', + '2fa_token' => 'Токен аутентификации', + 'submit' => 'Отправить', + 'close' => 'Закрыть', + 'settings' => 'Настройки', + 'configuration' => 'Конфигурация', + 'sftp' => 'SFTP', + 'databases' => 'База данных', + 'memo' => 'Напоминание', + 'created' => 'Созданный', + 'expires' => 'Истекает', + 'public_key' => 'Токен', + 'api_access' => 'Доступ к API', + 'never' => 'никогда', + 'sign_out' => 'Выход', + 'admin_control' => 'Админ панель', + 'required' => 'Необходимо', + 'port' => 'Порт', + 'username' => 'Имя пользователя', + 'database' => 'База данных', + 'new' => 'Новый', + 'danger' => 'Опасность', + 'create' => 'Создать', + 'select_all' => 'Выбрать все', + 'select_none' => 'Выберат ни одного', + 'alias' => 'Псевдоним', + 'primary' => 'Основной', + 'make_primary' => 'Сделать основным', + 'none' => 'Никто', + 'cancel' => 'Отмена', + 'created_at' => 'Создан в', + 'action' => 'Действие', + 'data' => 'Дата', + 'queued' => 'Очередь', + 'last_run' => 'Последний запуск', + 'next_run' => 'Следующий запуск', + 'not_run_yet' => 'Еще не работает', + 'yes' => 'Да', + 'no' => 'Нет', + 'delete' => 'Удалить', + '2fa' => '2FA', + 'logout' => 'Выйти', + 'admin_cp' => 'Панель управления администратора', + 'optional' => 'Опциональный', + 'read_only' => 'Только чтение', + 'relation' => 'Зависимость', + 'owner' => 'Владелец', + 'admin' => 'Администратор', + 'subuser' => 'Доп-пользователь', + 'captcha_invalid' => 'Капча недействительна.', + 'tasks' => 'Задачи', + 'seconds' => 'Секунд', + 'minutes' => 'Минут', + 'under_maintenance' => 'На техобслуживании', + 'days' => [ + 'sun' => 'Воскресенье', + 'mon' => 'Понедельник', + 'tues' => 'Вторник', + 'wed' => 'Среда', + 'thurs' => 'Черверг', + 'fri' => 'Пятница', + 'sat' => 'Суббота', + ], + 'last_used' => 'Последний раз был использован', +]; diff --git a/resources/lang/ru/validation.php b/resources/lang/ru/validation.php new file mode 100644 index 000000000..9baf6c96b --- /dev/null +++ b/resources/lang/ru/validation.php @@ -0,0 +1,105 @@ + ':attribute должен быть принят.', + 'active_url' => ':attribute не является допустимым URL.', + 'after' => ':attribute должен быть датой после :date.', + 'after_or_equal' => ':attribute должен быть датой после или равен :date.', + 'alpha' => ':attribute может содержать только буквы.', + 'alpha_dash' => ':attribute может содержать только буквы, цифры и тире.', + 'alpha_num' => ':attribute может содержать только буквы и цифры.', + 'array' => ':attribute должен быть массивом.', + 'before' => ':attribute должен быть датой до :date.', + 'before_or_equal' => ':attribute должен быть датой до или равен :date.', + 'between' => [ + 'numeric' => ':attribute должен быть между :min и :max.', + 'file' => ':attribute должен быть между :min и :max килобайт.', + 'string' => ':attribute должен быть между :min и :max символами.', + 'array' => ':attribute должен содержать от :min до :max элементов.', + ], + 'boolean' => ':attribute должно быть true или false.', + 'confirmed' => ':attribute Подтверждение не совпадает.', + 'date' => ':attribute не является допустимой датой.', + 'date_format' => ':attribute не соответствует формату :format.', + 'different' => ':attribute и :other должен быть другим.', + 'digits' => ':attribute должен быть :digits цифровым.', + 'digits_between' => ':attribute должен быть между :min и :max цифрами.', + 'dimensions' => ':attribute имеет недопустимые размеры изображения.', + 'distinct' => ':attribute имеет повторяющееся значение.', + 'email' => ':attribute должен быть действительным адресом электронной почты.', + 'exists' => 'выбранный :attribute недействителен.', + 'file' => ':attribute должен быть файлом.', + 'filled' => 'поле :attribute обязательно для заполнения.', + 'image' => ':attribute должен быть изображением.', + 'in' => 'выбранный :attribute недействителен.', + 'in_array' => 'поле :attribute не существует в :other.', + 'integer' => ':attribute должен быть целым числом.', + 'ip' => ':attribute должен быть действительным IP-адресом.', + 'json' => ':attribute должен быть допустимой строкой JSON.', + 'max' => [ + 'numeric' => ':attribute не может быть больше чем :max.', + 'file' => ':attribute не может быть больше, чем :max килобайт.', + 'string' => ':attribute не может быть больше, чем :max символов.', + 'array' => ':attribute может содержать не более :max предметов.', + ], + 'mimes' => ':attribute должен быть файл типа: :values.', + 'mimetypes' => ':attribute должен быть файл типа: :values.', + 'min' => [ + 'numeric' => ':attribute должен быть не менее :min.', + 'file' => ':attribute должно быть не менее :min килобайт.', + 'string' => ':attribute должно быть не менее :min символов.', + 'array' => ':attribute должно иметь как минимум :min предметов.', + ], + 'not_in' => 'выбранный :attribute недействителен.', + 'numeric' => ':attribute должен быть числом.', + 'present' => ':attribute поле должно присутствовать.', + 'regex' => ':attribute Формат неверен.', + 'required' => ':attribute Поле, обязательное для заполнения.', + 'required_if' => ':attribute Поле обязательно для заполнения, когда :other является :value.', + 'required_unless' => ':attribute Поле обязательно для заполнения, если :other не находится в :values.', + 'required_with' => ':attribute Поле обязательно для заполнения, когда :values присутствуют.', + 'required_with_all' => ':attribute Поле обязательно для заполнения, когда :values присутствуют.', + 'required_without' => ':attribute Поле обязательно для заполнения, когда :values отсутствуют.', + 'required_without_all' => ':attribute поле обязательно для заполнения, когда нет ни одного из :values.', + 'same' => ':attribute и :other должены совпадать.', + 'size' => [ + 'numeric' => ':attribute должно быть :size.', + 'file' => ':attribute должно быть :size килобайт.', + 'string' => ':attribute должно быть :size символов.', + 'array' => ':attribute должен содержать :size предметов.', + ], + 'string' => ':attribute должен быть строкой.', + 'timezone' => ':attribute должна быть действительной зоной.', + 'unique' => ':attribute уже занят.', + 'uploaded' => ':attribute не удалось загрузить.', + 'url' => ':attribute Формат неверен.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | following language lines are used to swap attribute place-holders + | with something more reader friendly such as E-Mail Address instead + | of "email". This simply helps us make messages a little cleaner. + | + */ + + 'attributes' => [], + + // Internal validation logic for Pterodactyl + 'internal' => [ + 'variable_value' => ':env переменная', + ], +]; diff --git a/resources/lang/zh/admin/nests.php b/resources/lang/zh/admin/nests.php index d5e2a511b..388c32698 100644 --- a/resources/lang/zh/admin/nests.php +++ b/resources/lang/zh/admin/nests.php @@ -9,25 +9,25 @@ return [ 'notices' => [ - 'created' => '一个新的管理模块, :name, 已成功创建。', - 'deleted' => '成功从面板删除指定的管理模块。', - 'updated' => '成功更新管理模块的选项。', + 'created' => '已成功创建 :name 。', + 'deleted' => '已成功从面板删除指定的管理模块。', + 'updated' => '已成功更新管理模块选项。', ], 'eggs' => [ 'notices' => [ - 'imported' => '成功导入一个管理模板。', - 'updated_via_import' => '该管理模板已按照上传的文件完成更新。', - 'deleted' => '成功删除指定的管路模板。', - 'updated' => '成功更新管理模板的配置。', - 'script_updated' => '管理模板的安装脚本已经成功更新并且会在安装新服务器时被执行。', + 'imported' => '已成功导入管理模板。', + 'updated_via_import' => '此管理模板已按照上传的文件完成更新。', + 'deleted' => '已成功删除指定的管路模板。', + 'updated' => '已成功更新管理模板的配置。', + 'script_updated' => '已成功更新孵化蛋安装脚本且将于服务器安装时自动执行。', 'egg_created' => '一个管理模板已经成功创建. 你需要重启所有正在运行的节点受控端来使该模板生效。', ], ], 'variables' => [ 'notices' => [ - 'variable_deleted' => '参数 ":variable" 已被移除,在服务器重装之后将不在有效。', - 'variable_updated' => '参数 ":variable" 已更新。 你需要重装所有服务器来使该参数生效.', - 'variable_created' => '新的参数已经创建并被赋值,该操作会影响此管理模板下的所有服务器', + 'variable_deleted' => '已移除变量 ":variable" 且其在重构服务器镜像后将会失效。 ', + 'variable_updated' => '已更新变量 ":variable" 。您需要重构使用此变量的服务器以应用更改。', + 'variable_created' => '已成功创建新变量并分配给此孵化蛋。', ], ], ]; diff --git a/resources/lang/zh/admin/node.php b/resources/lang/zh/admin/node.php index e2b9984aa..ad5a40a00 100644 --- a/resources/lang/zh/admin/node.php +++ b/resources/lang/zh/admin/node.php @@ -9,15 +9,15 @@ return [ 'validation' => [ - 'fqdn_not_resolvable' => '提供的域名或地址没有解析到一个合法的IP地址.', - 'fqdn_required_for_ssl' => '这个节点要求解析到一个公共IP的域名必须使用SSL', + 'fqdn_not_resolvable' => '提供的正式域名(FQDN)或 IP 地址未解析到有效的 IP 地址。', + 'fqdn_required_for_ssl' => '此节点需要解析到公网 IP 地址的正式域名才能使用 SSL。', ], 'notices' => [ - 'allocations_added' => '配额已经成功的被添加到这个节点.', - 'node_deleted' => '节点成功从面板中移除.', - 'location_required' => '在你可以添加一个节点之前必须至少有一个可用区配置。', - 'node_created' => '节点新建成功! 使用 \'Configuration\' 标签,你可以在此节点上自动配置受控端. 在你可以创建服务器之前,你必须至少分配一个IP和端口', - 'node_updated' => '节点信息更新成功!如果任何节点受控端的设置更改了,您需要重启受控端来使设置生效.', - 'unallocated_deleted' => '已删除 :ip 上的所有未分配的端口', + 'allocations_added' => '已成功为此节点分配地址。', + 'node_deleted' => '已成功从面板中移除节点。', + 'location_required' => '您必须至少配置一个区域才能添加节点至面板。', + 'node_created' => '已成功新建节点!您可通过\'配置\'选项卡已自动配置此机器上的守护程序。在您添加服务器前,您必须先分配一个 IP 地址及端口。', + 'node_updated' => '已更新节点信息。若守护程序设置更改,您需要重启守护程序才能生效。', + 'unallocated_deleted' => '已为 :ip 删除所有未分配的端口。', ], ]; diff --git a/resources/lang/zh/admin/pack.php b/resources/lang/zh/admin/pack.php index 1d95dc074..e8494ef27 100644 --- a/resources/lang/zh/admin/pack.php +++ b/resources/lang/zh/admin/pack.php @@ -9,8 +9,8 @@ return [ 'notices' => [ - 'pack_updated' => '整合包已经被更新。', - 'pack_deleted' => '成功删除整合包: ":name" 。', - 'pack_created' => '一个整合包已被成功创建,现在可以用它来部署服务器了。', + 'pack_updated' => '已成功更新整合包。', + 'pack_deleted' => '已成功从系统中删除整合包 “:name”。', + 'pack_created' => '已成功在系统上创建整合包,您现在可以使用它来部署服务器了。', ], ]; diff --git a/resources/lang/zh/admin/server.php b/resources/lang/zh/admin/server.php index c850b2735..5973034ed 100644 --- a/resources/lang/zh/admin/server.php +++ b/resources/lang/zh/admin/server.php @@ -9,23 +9,23 @@ return [ 'exceptions' => [ - 'no_new_default_allocation' => '你正在尝试删除此服务器的默认配额,但是该服务器没有足够的后备配额。', - 'marked_as_failed' => '这个服务器目前被标记为安装失败。 当前状态不能改变为此状态。', - 'bad_variable' => '变量 :name 有一个已确认的错误 。', - 'daemon_exception' => '连接受控端时发生意外 返回错误码 HTTP/:code response code. 此错误已被记录。', - 'default_allocation_not_found' => '请求的默认配额没有在这台服务器上找到。', + 'no_new_default_allocation' => '您正在尝试删除此服务器的默认分配地址,但此服务器可用的备选分配地址。', + 'marked_as_failed' => '此服务器被标记为安装失败。当前状态无法在面板中被改变。', + 'bad_variable' => '变量 :name 有验证错误。', + 'daemon_exception' => '连接守护程序时返回 HTTP/:code 反馈码。此错误已被记录。', + 'default_allocation_not_found' => '未在此服务器上找到请求的默认分配地址。', ], 'alerts' => [ - 'startup_changed' => '该服务器的启动配置已被更新. 如果此服务器所属的管理模块或管理模板更改,此时将发生一次配置重设', - 'server_deleted' => '成功从系统中删除服务器', - 'server_created' => '创建服务器成功。 请稍后几分钟,受控端将尽快完成服务器安装', - 'build_updated' => '启动参数已更改。 一些修改需要重启该服务器后生效。', - 'suspension_toggled' => '服务器状态已更改为 :status.', - 'rebuild_on_boot' => '此服务器已被标记为需要在Docker容器中启动。 此操作会在下次重启后生效。', - 'install_toggled' => '此服务器的安装状态已被更改', - 'server_reinstalled' => '此服务器目前已置于重装队列中,即将开始重装', - 'details_updated' => '服务器信息成功被更新', - 'docker_image_updated' => '成功更改用于该服务器的默认的Docker镜像。 此操作需要重启后生效', - 'node_required' => '你需要至少一个节点才能开始添加服务器', + 'startup_changed' => '已更新此服务器的启动配置。若此服务器的启动模板被更改,其将被重新安装。', + 'server_deleted' => '已成功从系统中删除服务器。', + 'server_created' => '已成功在面板中创建服务器。请稍等面板完全安装服务器完毕。', + 'build_updated' => '已更新此服务器的构建参数。部分更改可能需要重启才能生效。启动参数已更改。', + 'suspension_toggled' => '服务器停用状态已更改为 :status.', + 'rebuild_on_boot' => '此服务器已被标记为需要重新构建 Docker 容器。此操作会在下次启动服务器后生效。', + 'install_toggled' => '此服务器的安装状态已被更改。', + 'server_reinstalled' => '此服务器已置于即将开始的重装队列中。', + 'details_updated' => '已成功更新服务器信息。', + 'docker_image_updated' => '已成功更改此服务器使用的默认 Docker 镜像。此操作需要重启以应用更改。', + 'node_required' => '您需要配置至少一个节点以添加服务器至面板。', ], ]; diff --git a/resources/lang/zh/admin/user.php b/resources/lang/zh/admin/user.php index 1250cdae7..27c2cc616 100644 --- a/resources/lang/zh/admin/user.php +++ b/resources/lang/zh/admin/user.php @@ -9,10 +9,10 @@ return [ 'exceptions' => [ - 'user_has_servers' => '无法删除一个拥有活动状态服务器的用户. 请在继续此操作前删除他的服务器', + 'user_has_servers' => '无法删除已绑定活跃服务器的账户。请删除服务器后继续。', ], 'notices' => [ - 'account_created' => '成功创建用户', - 'account_updated' => '成功更新用户', + 'account_created' => '已成功创建用户。', + 'account_updated' => '已成功更新用户。', ], ]; diff --git a/resources/lang/zh/auth.php b/resources/lang/zh/auth.php index 6758f8238..df59bb489 100644 --- a/resources/lang/zh/auth.php +++ b/resources/lang/zh/auth.php @@ -2,21 +2,21 @@ return [ 'not_authorized' => '您无权执行此操作。', - 'auth_error' => '尝试登录时发生错误.', - 'authentication_required' => '需要认证才能继续操作', + 'auth_error' => '登录时发生错误。', + 'authentication_required' => '需要认证以继续', 'remember_me' => '记住我', 'sign_in' => '登陆', 'forgot_password' => '忘记密码', - 'request_reset_text' => '忘记密码? 请在下方填入您的Email.', - 'reset_password_text' => '重设您账户的密码.', + 'request_reset_text' => '忘记密码了吗?请在下方填入您的电子邮件地址。', + 'reset_password_text' => '重设账户密码', 'reset_password' => '重设密码', - 'email_sent' => '一封帮助您重置密码的电子邮件已发出,请查收并按提示操作(如未收到请检查垃圾箱)', - 'failed' => '用户名或密码错误, 或者两步验证失败.', - 'throttle' => '太多次登陆失败. 请在 :seconds 秒后尝试', - 'password_requirements' => '密码至少包含大写字母,小写字母,数字,并且在8位以上.', + 'email_sent' => '一封含有重置密码指引的邮件已发送至您的电子邮箱地址。', + 'failed' => '所提供的凭证与我们所记录的不符,或可能两步验证失败。', + 'throttle' => '登录尝试次数过多。请 :seconds 秒后重试。', + 'password_requirements' => '密码必须含有一位大写字母、小写字母及数字且长度至少为八位。', 'request_reset' => '查找账户', '2fa_required' => '两步验证', '2fa_failed' => '两步验证密码错误', - 'totp_failed' => '错误的TOTP验证.', - '2fa_must_be_enabled' => '管理员要求您的账户必须开启两步验证才能使用此面板.', + 'totp_failed' => '尝试进行两步验证时发生错误。', + '2fa_must_be_enabled' => '管理员要求您启用两步验证才能使用面板。', ]; diff --git a/resources/lang/zh/base.php b/resources/lang/zh/base.php index 528e07f1c..5613750bb 100644 --- a/resources/lang/zh/base.php +++ b/resources/lang/zh/base.php @@ -3,86 +3,87 @@ return [ 'validation_error' => '请求中有一个或多个字段出错', 'errors' => [ - 'return' => '返回上一个页面', + 'return' => '返回上页', 'home' => '返回主页', '403' => [ 'header' => '禁止访问', - 'desc' => '您没有权限访问此服务器上的资源.', + 'desc' => '您没有访问此服务器上的资源的权限。', ], '404' => [ - 'header' => 'Not Found', - 'desc' => '未找到资源.', + 'header' => '文件未找到', + 'desc' => '我们无法在此服务器上找到所请求的资源。', ], 'installing' => [ - 'header' => '服务器正在安装', - 'desc' => '请求的服务器仍然在部署中,请稍等几分钟,完成后您将收到一封电子邮件', + 'header' => '服务器安装中', + 'desc' => '请求的服务器正在完成安装进程。请几分钟后再来查看,您将在此过程完成后收到电子邮件提醒。', ], 'suspended' => [ - 'header' => '服务器已暂停', - 'desc' => '此服务器已被暂停,无法访问,请联系管理员', + 'header' => '服务器已停用', + 'desc' => '此服务器已停用且无法访问。', ], 'maintenance' => [ 'header' => '节点维护中', 'title' => '暂时不可用', - 'desc' => '此节点正在维护,当前无法访问.', + 'desc' => '此节点正在维护,当前无法访问。', ], ], 'index' => [ 'header' => '您的服务器', - 'header_sub' => '您当前可访问的服务器.', + 'header_sub' => '您有权限访问的服务器。', 'list' => '服务器列表', ], 'api' => [ 'index' => [ 'list' => '您的密钥', 'header' => '账户 API', - 'header_sub' => '管理访问密钥允许您使用API操作面板.', + 'header_sub' => '管理允许您对面板执行操作的 API 密钥。', 'create_new' => '新建 API 密钥', - 'keypair_created' => '新建API密钥成功.', + 'keypair_created' => '已成功生成 API 密钥并列于下方。', ], 'new' => [ 'header' => '新建 API 密钥', - 'header_sub' => '创建一个新的账户API密钥.', - 'form_title' => '选项', + 'header_sub' => '新建账户访问密钥。', + 'form_title' => '详细信息', 'descriptive_memo' => [ 'title' => '描述', - 'description' => '添加一个关于此密钥的描述.', + 'description' => '请输入便于分辨此密钥的描述信息。', ], 'allowed_ips' => [ - 'title' => '允许的IP', - 'description' => '添加IP地址限制来保护API安全. CIDR 标记是被允许的. 留空将允许所有IP.', + 'title' => '许可 IP', + 'description' => '输入允许使用此密钥的 IP 地址列表。此功能支持无类别域间路由。留空将允许所有 IP 使用。', ], ], ], 'account' => [ - 'details_updated' => '您账户的信息成功更新.', - 'invalid_password' => '您提供的密码不正确.', + 'details_updated' => '已成功更新您的账户信息。', + 'invalid_password' => '您提供的密码无效。', 'header' => '您的账户', 'header_sub' => '管理您的账户信息.', 'update_pass' => '修改密码', - 'update_email' => '修改 Email 地址', + 'update_email' => '修改电子邮件地址', 'current_password' => '当前密码', 'new_password' => '新密码', 'new_password_again' => '重复密码', - 'new_email' => '新 Email 地址', - 'first_name' => '姓', - 'last_name' => '名', + 'new_email' => '新电子邮件地址', + 'first_name' => '姓氏', + 'last_name' => '名称', 'update_identity' => '更新个人信息', - 'username_help' => '您的用户名必须唯一(未被使用),并满足以下要求: :requirements.', + 'username_help' => '您的用户名必须未被他人使用,且仅包含下列字符::requirements。', + 'language' => '语言', ], 'security' => [ - 'session_mgmt_disabled' => '为了安全原因,您的此次会话无法访问用户管理.', + 'session_mgmt_disabled' => '您的托管商未启用此界面来管理账户会话。', 'header' => '账户安全', - 'header_sub' => '管理活动会话和两步认证.', - 'sessions' => '活动中的会话', + 'header_sub' => '管理活跃中的会话与两步验证。', + 'sessions' => '活跃会话', '2fa_header' => '两步验证', - '2fa_token_help' => '填入您两步验证生成器生成的密码 (Google Authenticator, Authy, etc.).', + '2fa_token_help' => '请填入由应用程序所生成的两步验证密钥(Google 身份验证器、Authy 等)。', 'disable_2fa' => '关闭两步验证', - '2fa_enabled' => '两步验证已开启,在您登陆面板时会要求两步验证.如果您想关闭两步验证,只需输入两步验证的密码即可', - '2fa_disabled' => '两步验证已关闭! 您应该开启两步验证将其作为您账户的额外防护', - 'enable_2fa' => '开启两步验证', - '2fa_qr' => '在您的设备上上配置两步验证', - '2fa_checkpoint_help' => '使用两步验证需要用您的应用扫左侧二维码, 或手动输入下方的代码.完成后请将生成的密码输入下方方框.', - '2fa_disable_error' => '两步验证密码错误. 关闭两步验证失败.', + '2fa_enabled' => '已为此账户启用两步验证,您将需要验证以登录至此账户。若您想关闭两步验证,您只需在下方输入密钥并提交即可。', + '2fa_disabled' => '已关闭两步验证!您应启用此功能来作为此账户的附加防护手段。', + 'enable_2fa' => '启用两步验证', + '2fa_qr' => '在您的设备上配置两步验证', + '2fa_checkpoint_help' => '在您的手机上使用两步验证应用程序扫描左侧的二维码或直接输入下方的代码。录入后,请在下方输入应用程序生成的密码。', + '2fa_disable_error' => '提供的两步验证密钥无效。未关闭此账户的两步验证。两步验证密码错误. 关闭两步验证失败.', ], ]; diff --git a/resources/lang/zh/command/messages.php b/resources/lang/zh/command/messages.php index 11081a7e9..e3c84860b 100644 --- a/resources/lang/zh/command/messages.php +++ b/resources/lang/zh/command/messages.php @@ -2,54 +2,54 @@ return [ 'key' => [ - 'warning' => '貌似您已经拥有一个应用加密密钥了. 继续操作会导致之前的密钥被覆盖,所有的加密文件都将损坏。 !!!危险操作,请注意文件安全!!!', - 'confirm' => '我已了解此操作的后果,可以承受丢失文件的风险,请继续。', - 'final_confirm' => '确定继续操作? 更改应用加密密钥 !!将会导致数据丢失!!.', + 'warning' => '似乎您已配置应用程序加密密钥。继续此操作将覆盖密钥并损坏已加密数据。请了解您所执行的操作后再决定是否继续!!!', + 'confirm' => '我已了解执行此操作的后果并承受丢失加密数据的风险,请继续。', + 'final_confirm' => '确您是否决定继续?更改应用程序加密密钥将导致数据丢失!!!', ], 'location' => [ - 'no_location_found' => '可用区ID错误:无发找到该可用区', - 'ask_short' => '可用区ID', - 'ask_long' => '可用区描述', - 'created' => '成功创建可用区 (:name) ,可用区ID: :id.', - 'deleted' => '成功删除指定的可用区。', + 'no_location_found' => '无法找到与提供的代码匹配的记录。', + 'ask_short' => '地区代码', + 'ask_long' => '地区描述', + 'created' => '已成功创建地区(:name),编号为 :id。', + 'deleted' => '已成功删除请求的地区。', ], 'user' => [ - 'search_users' => '输入用户名, UUID, 或 Email 地址', - 'select_search_user' => '要删除的用户ID (键入 \'0\' 来重新搜索)', - 'deleted' => '成功删除用户。', - 'confirm_delete' => '确定要删除此用户吗', - 'no_users_found' => '未找到指定的用户', - 'multiple_found' => '找到多个用户, 无法删除用户,原因: --no-interaction 参数。', - 'ask_admin' => '此用户是管理员吗?', + 'search_users' => '请输入用户名、UUID 或电子邮件地址', + 'select_search_user' => '待删除的用户编号(请键入 \'0\' 来重新搜索)', + 'deleted' => '已成功从面板删除用户。', + 'confirm_delete' => '您是否想从面板中删除此用户?', + 'no_users_found' => '未找到匹配搜索项的用户。', + 'multiple_found' => '已找到多个匹配搜索项的用户,由于 --no-interaction 参数而无法删除。', + 'ask_admin' => '此用户是否为管理员?', 'ask_email' => '电子邮件地址', 'ask_username' => '用户名', - 'ask_name_first' => '姓', - 'ask_name_last' => '名', + 'ask_name_first' => '姓氏', + 'ask_name_last' => '名称', 'ask_password' => '密码', - 'ask_password_tip' => '如果您想创建一个随机密码的用户,请重新执行指令(CTRL+C) 并输入 `--no-password` 参数.', - 'ask_password_help' => '密码至少8位,并包含一个字母和数字', + 'ask_password_tip' => '若您想创建用户并稍后发送生成的随机密码给用户,请重新运行此命令(CTRL+C)并添加 `--no-password` 参数。', + 'ask_password_help' => '密码长度必须至少为八位且包含至少一位大写字母和数字。', '2fa_help_text' => [ - '此命令会关闭一个用户的两步验证(如果他打开了). 此命令应仅用于用户恢复或解锁(两步验证无法成功情况下)。', - '如果您不想这么做, 情书用 CTRL+C 退出此操作。', + '此命令将关闭账户的两步验证(若启用)。此命令应作为账户被锁定时的恢复措施。', + '若您不想这么做,请使用 CTRL+C 退出进程。', ], - '2fa_disabled' => '已成功禁用以下账户的两步验证: :email.', + '2fa_disabled' => '已成功为 :email 禁用两步验证。', ], 'schedule' => [ - 'output_line' => '第一次任务已计划于 `:schedule` (:hash).', + 'output_line' => '首次任务将于 `:schedule`(:hash)后执行。', ], 'maintenance' => [ - 'deleting_service_backup' => '正在删除服务备份文件 :file.', + 'deleting_service_backup' => '正在删除服务备份文件 :file。', ], 'server' => [ - 'rebuild_failed' => '重构操作 ":name" (#:id) ,位于节点 ":node" 失败,错误信息: :message', + 'rebuild_failed' => '节点 ":node" 上的重构操作 ":name"(#:id) 发生了 :message 错误。', 'power' => [ - 'confirm' => '您即将执行 :action 在 :count 个服务器. 是否继续?', - 'action_failed' => '电源命令 ":name" (#:id) 位于节点 ":node" 失败,错误信息: :message', + 'confirm' => '您将在 :count 台服务器上执行 :action 操作。是否继续?', + 'action_failed' => '节点 ":node" 上的电源命令 ":name"(#:id) 发生了 :message 错误。', ], ], 'environment' => [ 'mail' => [ - 'ask_smtp_host' => 'SMTP 主机 (e.g. smtp.gmail.com)', + 'ask_smtp_host' => 'SMTP 主机(如 smtp.gmail.com)', 'ask_smtp_port' => 'SMTP 端口', 'ask_smtp_username' => 'SMTP 用户名', 'ask_smtp_password' => 'SMTP 密码', @@ -57,41 +57,41 @@ return [ 'ask_mailgun_secret' => 'Mailgun 密钥', 'ask_mandrill_secret' => 'Mandrill 密钥', 'ask_postmark_username' => 'Postmark API 密钥', - 'ask_driver' => '哪个引擎应该用于发送邮件?', - 'ask_mail_from' => 'Email来自哪个邮箱', - 'ask_mail_name' => 'Email应该由谁发送(发送者姓名)?', - 'ask_encryption' => '应该使用的加密方法', + 'ask_driver' => '应使用哪款引擎发送邮件?', + 'ask_mail_from' => '电子邮件地址的邮件发信人为', + 'ask_mail_name' => '电子邮件的显示发信人为', + 'ask_encryption' => '加密方法', ], 'database' => [ - 'host_warning' => '极度推荐不使用localhost作为主机地址(可能有bug). 如果确实要使用本机作为MySQL地址,请使用 "127.0.0.1".', + 'host_warning' => '由于经常发生套接字连接错误,我们极度不推荐您使用 “localhost” 作为数据库主机地址。若您仍想使用本地连接则应使用 “127.0.0.1”。', 'host' => '数据库主机', 'port' => '数据库端口', 'database' => '数据库名', - 'username_warning' => '使用 "root" 账户会导致安全漏洞, 翼龙面板不允许此账户作为面板数据库账户. 你应该为此程序创建MySQL庄户', + 'username_warning' => '不仅翼龙面板不允许使用 "root" 账户连接 MySQL 数据库,且这将产生严重安全漏洞。您应为此软件单独创建 MySQL 账户。', 'username' => '数据库用户名', - 'password_defined' => '您似乎已经指定了MySQL连接密码,你想更改它吗', + 'password_defined' => '您似乎已创建了带有密码的 MySQL 连接,您是否想更改?', 'password' => '数据库密码', - 'connection_error' => '无法连接数据库. 返回错误: ":error".', - 'creds_not_saved' => '您的数据库访问信息未保存. 在继续之前您将需要提供有效的信息', - 'try_again' => '返回再试一次?', + 'connection_error' => '无法使用提供的凭证连接 MySQL 服务器。 返回的错误为 ":error"。', + 'creds_not_saved' => '您的数据库访问凭证尚未保存。您需要在继续前提供有效的连接信息。', + 'try_again' => '是否返回重试?', ], 'app' => [ - 'settings' => '启用基于UI的设置编辑器?', - 'author' => '管理模板作者Email', - 'author_help' => '提供此面板到处的管理模板作者的电子邮件地址. 这应该是一个合法的电子邮件地址', - 'app_url_help' => '这个应用的URL必须以 https:// or http:// 开头(取决于是否启用SSL). 如果不包含这些您的电子邮件地址和其他内容可能会指向错误的地址.', - 'app_url' => '应用 URL', - 'timezone_help' => '设置的时区应该满足PHP支持的时区. 如果您不确定,请参阅 http://php.net/manual/en/timezones.php.', - 'timezone' => '应用时区', - 'cache_driver' => 'Cache Driver', - 'session_driver' => 'Session Driver', - 'queue_driver' => 'Queue Driver', - 'using_redis' => '如果您选择使用Redis, 请在下方提供有效的连接信息. 一般使用默认信息即可,除非您更改过设置.', + 'settings' => '是否启用可视化设置编辑器?', + 'author' => '管理模板作者电子邮件地址', + 'author_help' => '提供从此面板导出管理模板人员的电子邮件地址。此电子邮件地址必须合法。', + 'app_url_help' => '根据您是否启用 SSL 来决定此应用程序的 URL 应为 https:// 或 http://。若您选择错误,您的电子邮件及其他内容将指向到错误地址。', + 'app_url' => '应用程序 URL', + 'timezone_help' => '时区应匹配 PHP 所支持的时区。 如果您不确定,请参阅 http://php.net/manual/en/timezones.php。', + 'timezone' => '应用程序时区', + 'cache_driver' => '缓存驱动程序', + 'session_driver' => '会话驱动程序', + 'queue_driver' => '队列驱动程序', + 'using_redis' => '若您选择使用 Redis,请在下方提供有效的连接信息。在您未更改设置的大多数情况下,您均可使用默认值。', 'redis_host' => 'Redis 主机', 'redis_password' => 'Redis 密码', - 'redis_pass_help' => '默认情况下,Redis数据库不需要密码,且仅运行于本地. 这种情况下,您什么都不用填.', + 'redis_pass_help' => '默认情况下,Redis 服务器实例无需密码且在本地运行禁止外界访问。这种情况下,您只需回车即可。', 'redis_port' => 'Redis 端口', - 'redis_pass_defined' => '似乎您已经设置过Redis密码了,您需要更改吗?', + 'redis_pass_defined' => '您似乎已为 Redis 配置了密码,您是否想更改?', ], ], ]; diff --git a/resources/lang/zh/exceptions.php b/resources/lang/zh/exceptions.php index 4fd745aaf..38cb954ee 100644 --- a/resources/lang/zh/exceptions.php +++ b/resources/lang/zh/exceptions.php @@ -1,68 +1,68 @@ '连接受控端时发生意外 返回错误码 HTTP/:code response code. 此错误已被记录', + 'daemon_connection_failed' => '尝试连接守护程序是发生错误,状态码 HTTP/:code。此错误已被记录。', 'node' => [ - 'servers_attached' => '节点删除必须按先移除其所有的服务器.', - 'daemon_off_config_updated' => '受控端配置 已被更新, 但是自动更新受控端上的配置文件时发生错误. 你需要手动将配置文件 (core.json) 更新至受控端来完成更新.', + 'servers_attached' => '要删除节点,您必须先取消其与其他服务器的关联。', + 'daemon_off_config_updated' => '已更新守护程序配置,但在尝试自动更新守护程序配置文件时发生错误。您需要手动更新守护程序的配置文件(core.json)以应用更改。', ], 'allocations' => [ - 'server_using' => '一个服务器已分配该地址. 一个地址只有在无服务器使用时才能删除.', - 'too_many_ports' => '一次添加1000个以上的端口是不被支持的.', - 'invalid_mapping' => '提供的端口: :port 无效,无法继续操作.', - 'cidr_out_of_range' => 'CIDR 标记 只允许掩码在 /25 到 /32之间。', - 'port_out_of_range' => '端口超过范围,范围必须在 1024 到 65535 之间.', + 'server_using' => '已有服务器被分配到该地址。您必须先解除关联才能删除此地址。', + 'too_many_ports' => '不支持单次添加多于 1000 个端口。', + 'invalid_mapping' => '所提供的端口 :port 无效,无法继续操作。', + 'cidr_out_of_range' => '类别域间路由仅允许介于 /25 和 /32 之间的掩码。', + 'port_out_of_range' => '分配端口的范围必须介于 1024 至 65535 之间。', ], 'nest' => [ - 'delete_has_servers' => '活动服务器使用的管理模块不能被删除.', + 'delete_has_servers' => '无法删除附着到活跃服务器上的管理模块。', 'egg' => [ - 'delete_has_servers' => '活动服务器使用的管理模板不能被删除.', - 'invalid_copy_id' => '管理模板复制的脚本ID无效.', - 'must_be_child' => ' "复制设置自"选项指定的目标必须是管理模块的附属.', - 'has_children' => '此管理模版附属有一个或多个管理模板. 在删除之前请先删除所有附属.', + 'delete_has_servers' => '无法删除附着到活跃服务器上的管理模块。', + 'invalid_copy_id' => '用于复制脚本的管理模板不存在,或脚本本身不存在。', + 'must_be_child' => '“复制设置自”选项指定的目标必须为所选管理模块的子选项。', + 'has_children' => '此管理模版为一个或多个管理模板的母模板。请先删除其他管理模板再删除此模板。', ], 'variables' => [ - 'env_not_unique' => '环境变量 :name 必须唯一.', - 'reserved_name' => '环境变量 :name 是被保护的,无法指定为变量.', - 'bad_validation_rule' => '环境变量规则 ":rule" 对于这个应用不是一个有效的规则.', + 'env_not_unique' => '此管理面板的环境变量 :name 必须唯一。', + 'reserved_name' => '环境变量 :name 被保护的且无法被分配至其他变量。', + 'bad_validation_rule' => '验证规则 “:rule” 不是此应用程序的有效规则。', ], 'importer' => [ - 'json_error' => '尝试导入JSON 文件时发生错误: :error.', - 'file_error' => '提供的JSON文件不合法.', - 'invalid_json_provided' => '提供的JSON文件格式不正确,无法被解析。', + 'json_error' => '导入 JSON 文件时发生错误::error.', + 'file_error' => '所提供的 JSON 文件无效。', + 'invalid_json_provided' => '所提供的 JSON 文件格式无法被解析。', ], ], 'packs' => [ - 'delete_has_servers' => '活动服务器使用的整合包不能被删除', - 'update_has_servers' => '当前有服务器附属于包时无法修改关联选项的ID.', - 'invalid_upload' => '上传的文件不合法.', - 'invalid_mime' => '上传的文件不符合要求的文件类型 :type', - 'unreadable' => '服务器无法打开该压缩包.', - 'zip_extraction' => '解压时发生错误.', - 'invalid_archive_exception' => '压缩包缺失archive.tar.gz 或 import.json 文件在根目录.', + 'delete_has_servers' => '无法删除依附到活跃服务器的整合包。', + 'update_has_servers' => '无法在有服务器依附至整合包时修改关联选项编号。', + 'invalid_upload' => '所提供的文件格式无效。', + 'invalid_mime' => '提供的文件不符合所需文件类型 :type', + 'unreadable' => '服务器无法打开所提供的归档文件。', + 'zip_extraction' => '提取归档文件至服务器时发生错误。', + 'invalid_archive_exception' => '整合包归档文件的根目录似乎缺少 archive.tar.gz 或 import.json。', ], 'subusers' => [ - 'editing_self' => '编辑您自己的子用户时不被允许的.', - 'user_is_owner' => '子用户无法添加服主.', - 'subuser_exists' => '那个电子邮件的用户已经是此服务器的子用户了.', + 'editing_self' => '您无法作为子用户编辑您自己的子用户账号。', + 'user_is_owner' => '您无法作为子用户来添加为此服务器的服主。', + 'subuser_exists' => '使用该电子邮件地址的用户已被分配为此服务器的子用户。', ], 'databases' => [ - 'delete_has_databases' => '无法删除一个拥有活跃数据库的数据库服务器.', + 'delete_has_databases' => '无法删除关联至活跃数据库的数据库服务器。', ], 'tasks' => [ - 'chain_interval_too_long' => '链接任务的最大间隔时间为15分钟。', + 'chain_interval_too_long' => '连环任务的最大时间间隔为 15 分钟。', ], 'locations' => [ - 'has_nodes' => '活动节点附属的可用区无法被删除.', + 'has_nodes' => '无法删除被依附活动节点的区域。', ], 'users' => [ - 'node_revocation_failed' => '吊销密钥失败 节点 #:node. :error', + 'node_revocation_failed' => '注销节点 #:node 的密钥失败::error', ], 'deployment' => [ - 'no_viable_nodes' => '没有合适的节点来自动部署服务器', - 'no_viable_allocations' => '没有合适的地址来自动部署服务器', + 'no_viable_nodes' => '无法找到满足自动化部署需求的节点。', + 'no_viable_allocations' => '无法找到满足自动化部署需求的分配地址。', ], 'api' => [ - 'resource_not_found' => '需求的资源未找到.', + 'resource_not_found' => '服务器上不存在所请求的资源。', ], ]; diff --git a/resources/lang/zh/navigation.php b/resources/lang/zh/navigation.php index aebcd0fd0..d0c43b021 100644 --- a/resources/lang/zh/navigation.php +++ b/resources/lang/zh/navigation.php @@ -1,7 +1,7 @@ '主页', + 'home' => '首页', 'account' => [ 'header' => '账户管理', 'my_account' => '我的账户', diff --git a/resources/lang/zh/pagination.php b/resources/lang/zh/pagination.php index 67e361e02..259109ed2 100644 --- a/resources/lang/zh/pagination.php +++ b/resources/lang/zh/pagination.php @@ -12,6 +12,6 @@ return [ | */ - 'previous' => '« 上一步', - 'next' => '下一步 »', + 'previous' => '« 上页', + 'next' => '下页 »', ]; diff --git a/resources/lang/zh/passwords.php b/resources/lang/zh/passwords.php index 3c471ae73..4772044b6 100644 --- a/resources/lang/zh/passwords.php +++ b/resources/lang/zh/passwords.php @@ -11,9 +11,9 @@ return [ | has failed, such as for an invalid token or invalid new password. | */ - 'password' => '密码至少六位数,并且两次输入的密码一致.', - 'reset' => '您的密码已重设!', - 'sent' => '我们已发送密码重设电子邮件!', - 'token' => '此密码重设连接的令牌已过期.', - 'user' => '无法找到此Email的用户.', + 'password' => '密码必须至少包含六位字符且与确认密码匹配。', + 'reset' => '已重设您的密码!', + 'sent' => '我们已发送密码重设链接至您的电子邮件地址!', + 'token' => '此密码重设令牌无效。', + 'user' => '我们无法找到使用此电子邮件地址的用户。', ]; diff --git a/resources/lang/zh/server.php b/resources/lang/zh/server.php index 296eb05c8..56d76a97c 100644 --- a/resources/lang/zh/server.php +++ b/resources/lang/zh/server.php @@ -2,17 +2,17 @@ return [ 'index' => [ - 'title' => '服务器状态 :name', + 'title' => '查看服务器 :name ', 'header' => '服务器控制台', - 'header_sub' => '实时控制您的服务器.', + 'header_sub' => '实时掌控您的服务器。', ], 'schedule' => [ 'header' => '计划任务', - 'header_sub' => '在一处,轻松管理服务器任务.', + 'header_sub' => '一处轻松掌管服务器任务。', 'current' => '当前计划', 'new' => [ - 'header' => '创建新任务', - 'header_sub' => '创建一个新的定时任务.', + 'header' => '新建计划', + 'header_sub' => '为此服务器新建一组计划任务。', 'submit' => '创建任务', ], 'manage' => [ @@ -21,63 +21,63 @@ return [ 'delete' => '删除任务', ], 'task' => [ - 'time' => '在。。。之后', + 'time' => '在···之后', 'action' => '执行操作', 'payload' => '任务内容', - 'add_more' => '添加另一个任务', + 'add_more' => '添加其他任务', ], 'actions' => [ 'command' => '发送命令', 'power' => '电源命令', ], 'toggle' => '更改状态', - 'run_now' => '触发任务(现在)', - 'schedule_created' => '成功在服务器上创建一个计划任务.', - 'schedule_updated' => '任务已被更新.', + 'run_now' => '触发任务', + 'schedule_created' => '已成功在此服务器上新建计划任务。', + 'schedule_updated' => '已更新计划任务。', 'unnamed' => '未命名任务', - 'setup' => '任务创建', + 'setup' => '任务配置', 'day_of_week' => '星期', 'day_of_month' => '日', - 'hour' => '小时', - 'minute' => '分钟', - 'time_help' => '任务系统在定义任务应该何时开始运行时支持使用Cronjob语法。 使用上面的字段指定何时应开始运行这些任务,或从多个选择菜单中选择选项。', - 'task_help' => '任务的时间与先前定义的任务相关。 每个计划任务可能分配的任务不超过5个,任务可能不会超过15分钟的时间安排。', + 'hour' => '时', + 'minute' => '分', + 'time_help' => '计划任务系统支持使用 Cronjob 语法来定义任务启动时间。使用上方的字段来指定计划任务的开始时间或选择多选菜单中的多个选项。', + 'task_help' => '任务时间与先前定义的任务紧密相关。每个计划最多可分配 5 项任务且任务时间间隔不得超过 15 分钟。', ], 'tasks' => [ - 'task_created' => '成功在面板上创建一个新任务', - 'task_updated' => '任务成功被更新. 列表中的所有任务会被取消,并在下一次设定的时间运行.', - 'header' => '计划的任务', - 'header_sub' => '自动化你的服务器.', - 'current' => '当前计划的任务', + 'task_created' => '已成功在面板上新建任务。', + 'task_updated' => '已成功更新任务。现有的所有队列中的任务操作将被取消并于下个定义时间执行。', + 'header' => '计划任务', + 'header_sub' => '自动化您的服务器。', + 'current' => '当前计划任务', 'actions' => [ 'command' => '发送命令', - 'power' => '发送电源命令', + 'power' => '发送电源指令', ], - 'new_task' => '添加新任务', + 'new_task' => '新增任务', 'toggle' => '更改状态', 'new' => [ - 'header' => '新任务', - 'header_sub' => '为这个服务器创建一个新任务。', - 'task_name' => '任务名', + 'header' => '新建任务', + 'header_sub' => '为此服务器新建计划任务。', + 'task_name' => '任务名称', 'day_of_week' => '星期', 'custom' => '自定义', 'day_of_month' => '日', - 'hour' => '小时', + 'hour' => '时', 'minute' => '分', - 'sun' => '周日', - 'mon' => '周一', - 'tues' => '周二', - 'wed' => '周三', - 'thurs' => '周四', - 'fri' => '周五', - 'sat' => '周六', + 'sun' => '星期日', + 'mon' => '星期一', + 'tues' => '星期二', + 'wed' => '星期三', + 'thurs' => '星期四', + 'fri' => '星期五', + 'sat' => '星期六', 'submit' => '创建任务', 'type' => '任务类型', - 'chain_then' => '然后, 之后', + 'chain_then' => '先···再···', 'chain_do' => '执行', - 'chain_arguments' => '参数', + 'chain_arguments' => '使用参数', 'payload' => '任务内容', - 'payload_help' => '例如, 如果你选择 发送命令 ,就在此处填写要发送的命令. 如果您选择 发送电源命令 在这里填入电源命令 (e.g. restart).', + 'payload_help' => '例如,若您选择发送命令,请在此处填写要发送的命令。若您选择发送电源指令,请在此处填写电源命令(如重启).', ], 'edit' => [ 'header' => '任务管理', @@ -86,22 +86,22 @@ return [ ], 'users' => [ 'header' => '用户管理', - 'header_sub' => '控制访问你服务器的用户.', + 'header_sub' => '掌控谁能访问您的服务器。', 'configure' => '配置权限', - 'list' => '有权限的用户列表', - 'add' => '添加一个新的子用户', - 'update' => '修改子用户', - 'user_assigned' => '成功连接到一个子用户连接到服务器.', - 'user_updated' => '成功更新权限.', + 'list' => '权限用户', + 'add' => '新增子用户', + 'update' => '更新子用户', + 'user_assigned' => '已成功分配新子用户至此服务器。', + 'user_updated' => '已成功更新权限。', 'edit' => [ 'header' => '编辑子用户', - 'header_sub' => '管理用户在此服务器的访问权限.', + 'header_sub' => '编辑此用户的服务器访问权限。', ], 'new' => [ - 'header' => '添加新用户', - 'header_sub' => '添加一个允许访问此服务器的用户.', - 'email' => 'Email 地址', - 'email_help' => '填入你希望邀请的协助您管理服务器的人的Email地址.', + 'header' => '新增新用户', + 'header_sub' => '新增允许访问此服务器的用户。', + 'email' => '电子邮件地址', + 'email_help' => '输入您邀请管理此服务器用户的电子邮件地址。', 'power_header' => '电源管理', 'file_header' => '文件管理', 'subuser_header' => '子用户管理', @@ -110,180 +110,184 @@ return [ 'database_header' => '数据库管理', 'power_start' => [ 'title' => '启动服务器', - 'description' => '允许该用户启动服务器.', + 'description' => '允许此用户启动服务器。', ], 'power_stop' => [ 'title' => '停止服务器', - 'description' => '允许该用户停止服务器.', + 'description' => '允许此用户停止服务器。', ], 'power_restart' => [ 'title' => '重新启动服务器', - 'description' => '允许该用户重新启动服务器', + 'description' => '允许此用户重新启动服务器。', ], 'power_kill' => [ - 'title' => '强制结束服务器', - 'description' => '允许该用户强行关闭服务器', + 'title' => '强制关闭服务器', + 'description' => '允许此用户强行关闭服务器。', ], 'send_command' => [ 'title' => '发送控制台命令', - 'description' => '允许用户发送控制台. 如果用户没有"停止服务器"权限,那么他无法使用stop命令', + 'description' => '允许用户发送控制台命令。若用户没有“停止服务器”权限,则其 stop 命令。', ], 'access_sftp' => [ 'title' => 'SFTP 权限', - 'description' => '允许用户连接到受控端提供的SFTP服务器.', + 'description' => '允许用户连接到守护程序所提供的 SFTP 服务器。', ], 'list_files' => [ 'title' => '列出文件', - 'description' => '允许用户列出所有文件及文件夹列表,但是无权访问文件.', + 'description' => '允许用户列出服务器上所有文件及文件夹,但是无法查看文件内容。', ], 'edit_files' => [ 'title' => '编辑文件', - 'description' => '允许用户访问文件内容(但更改后无法保存). SFTP 不受此权限影响.', + 'description' => '允许用户打开文件查看内容。SFTP 不受此权限影响。', ], 'save_files' => [ - 'title' => 'Save Files', - 'description' => '允许用户保存文件(和编辑文件权限联动). SFTP 不受此权限影响.', + 'title' => '保存文件', + 'description' => '允许用户保存编辑过的文件内容。SFTP 不受此权限影响。', ], 'move_files' => [ - 'title' => '重命名和移动文件', - 'description' => '允许用户在文件系统中重命名和移动文件及文件夹.', + 'title' => '重命名与移动文件', + 'description' => '允许用户在文件系统上重命名与移动文件及文件夹。', ], 'copy_files' => [ 'title' => '复制文件', - 'description' => '允许用户在文件系统中复制文件及文件夹.', + 'description' => '允许用户在文件系统上复制文件及文件夹。', ], 'compress_files' => [ 'title' => '压缩文件', - 'description' => '允许用户在文件系统中压缩文件及文件夹', + 'description' => '允许用户在文件系统上压缩文件及文件夹。', ], 'decompress_files' => [ 'title' => '解压文件', - 'description' => '允许用户解压 .zip 和 .tar(.gz) 压缩文件.', + 'description' => '允许用户解压 .zip 和 .tar(.gz)归档文件。', ], 'create_files' => [ 'title' => '创建文件', - 'description' => '允许用户通过面板创建文件.', + 'description' => '允许用户通过面板创建文件。', ], 'upload_files' => [ 'title' => '上传文件', - 'description' => '允许用户通过文件管理上传文件.', + 'description' => '允许用户通过文件管理上传文件。', ], 'delete_files' => [ 'title' => '删除文件', - 'description' => '允许用户在文件系统中删除文件.', + 'description' => '允许用户删除文件系统上的文件。', ], 'download_files' => [ - 'title' => '下载文件s', - 'description' => '允许用户下载文件. 如果为用户分配该权限,那么他将自动拥有下载和查看文件内容的权限.', + 'title' => '下载文件', + 'description' => '允许用户下载文件。若用户被给予此权限,其可以在下载后查看文件而无需所需面板权限。', ], 'list_subusers' => [ 'title' => '列出子用户', - 'description' => '允许用户访问此服务器的子用户列表.', + 'description' => '允许用户访问此服务器的子用户列表。', ], 'view_subuser' => [ - 'title' => '访问子用户', - 'description' => '允许用户查看子用户的权限.', + 'title' => '查看子用户', + 'description' => '允许用户查看子用户的权限。', ], 'edit_subuser' => [ 'title' => '编辑子用户', - 'description' => '允许用户编辑此服务器上的子用户权限.', + 'description' => '允许用户编辑此服务器上的子用户权限。', ], 'create_subuser' => [ 'title' => '创建子用户', - 'description' => '允许用户在此服务器上添加子用户.', + 'description' => '允许用户在此服务器上添加子用户。', ], 'delete_subuser' => [ 'title' => '删除子用户', - 'description' => '允许用户删除此服务器上的子用户.', + 'description' => '允许用户删除此服务器上的子用户。', ], 'view_allocations' => [ - 'title' => '访问分配表', - 'description' => '允许用户访问所有分配到此服务器上的IP和端口列表.', + 'title' => '查看分配', + 'description' => '允许用户查看所有分配到此服务器上的 IP 及端口。', ], 'edit_allocation' => [ 'title' => '编辑默认连接', - 'description' => '允许用户更改连接到此服务器的默认连接地址.', + 'description' => '允许用户更改此服务器的默认连接地址。', ], 'view_startup' => [ - 'title' => '访问启动参数', - 'description' => '允许用户访问服务器的启动参数和变量.', + 'title' => '查看启动参数', + 'description' => '允许用户访问服务器的启动参数和相关变量。', ], 'edit_startup' => [ 'title' => '编辑启动参数', - 'description' => '允许用户更改服务器的启动参数和变量.', + 'description' => '允许用户更改服务器的启动参数。', ], 'list_schedules' => [ 'title' => '列出计划任务', - 'description' => '允许用户列出服务器上的所有计划任务 (无论是否启用) .', + 'description' => '允许用户列出服务器上的所有计划任务(无论是否启用)。', ], 'view_schedule' => [ - 'title' => '访问计划任务', - 'description' => '允许用户查看一个计划任务的具体信息,包括其执行的时间和命令.', + 'title' => '查看计划', + 'description' => '允许用户查看计划任务的详细信息,包含执行时间及分配任务。', ], 'toggle_schedule' => [ - 'title' => '开关计划任务', - 'description' => '允许用户更改计划任务的启用或禁用状态.', + 'title' => '开关计划', + 'description' => '允许用户启用或禁用计划的。', ], 'queue_schedule' => [ - 'title' => '队列化计划任务', - 'description' => '允许用户将一个计划任务队列,以便在下一个周期执行.', + 'title' => '队列计划', + 'description' => '允许用户将计划纳入队列在下个周期执行。', ], 'edit_schedule' => [ - 'title' => '编辑计划任务', - 'description' => '允许用户编辑计划任务. 此权限允许用户删除所有的执行任务,但无法删除计划任务本身.', + 'title' => '编辑计划', + 'description' => '允许用户编辑计划,包括所有的执行任务。这将允许用户移除单个任务,但无法移除计划本身。', ], 'create_schedule' => [ - 'title' => '创建计划任务', - 'description' => '允许用户创建一个计划任务.', + 'title' => '创建计划', + 'description' => '允许用户新建计划任务。', ], 'delete_schedule' => [ - 'title' => '删除计划任务', - 'description' => '允许用户从服务器删除一个计划任务.', + 'title' => '删除计划', + 'description' => '允许用户从服务器删除计划。', ], 'view_databases' => [ - 'title' => '访问数据库信息', - 'description' => '允许用户访问附属于此服务器的数据库信息,包含数据库的地址,用户名和密码', + 'title' => '查看数据库信息', + 'description' => '允许用户查看所有与此服务器相关联的数据库及其用户名与密码信息。', ], 'reset_db_password' => [ - 'title' => '重设数据库', - 'description' => '允许用户重新设置服务器数据库的密码.', + 'title' => '重置数据库', + 'description' => '允许用户重置服务器数据库密码。', ], 'delete_database' => [ 'title' => '删除数据库', - 'description' => '允许用户从面板删除此服务器的数据库.', + 'description' => '允许用户从面板删除此服务器数据库。', ], 'create_database' => [ 'title' => '新建数据库', - 'description' => '允许用户为这个服务器创建一个数据库.', + 'description' => '允许用户为此服务器新建数据库。', ], ], ], + 'allocations' => [ + 'mass_actions' => '批量操作', + 'delete' => '删除分配地址', + ], 'files' => [ 'exceptions' => [ - 'invalid_mime' => '这种类型的文件无法使用面板内建编辑器编辑.', - 'max_size' => '此文件太大,无法使用面板内建编辑器编辑.', + 'invalid_mime' => '此类型文件无法通过面板内置编辑器编辑。', + 'max_size' => '此文件过大,无法使用面板内置编辑器编辑。', ], 'header' => '文件管理', - 'header_sub' => '从网页直接管理您所有的文件.', - 'loading' => '正在加载初始文件结构,这可能需要几秒钟.', - 'path' => '当你在配置任何插件或服务器设置的文件路径时 :path 应该为您的根目录. 此节点的网页上传最大文件限制为 :size.', - 'seconds_ago' => '几秒之前', + 'header_sub' => '从网页直接管理您的所有文件。', + 'loading' => '正在加载初始文件结构,这可能需要几秒钟。', + 'path' => '当您配置插件或服务器设置的文件路径时,您应使用 :path 作为您的根目录。此节点通过网页上传的最大文件限制为 :size。', + 'seconds_ago' => '数秒前', 'file_name' => '文件名', 'size' => '大小', 'last_modified' => '最后修改', 'add_new' => '新建文件', 'add_folder' => '新建文件夹', - 'mass_actions' => '更多操作', + 'mass_actions' => '批量操作', 'delete' => '删除文件', 'edit' => [ 'header' => '编辑文件', - 'header_sub' => '从网页更改一个文件.', + 'header_sub' => '从网页编辑文件。', 'save' => '保存文件', 'return' => '返回文件管理', ], 'add' => [ 'header' => '新建文件', - 'header_sub' => '在您服务器上创建一个新文件.', + 'header_sub' => '在您服务器上新建新文件。', 'name' => '文件名', 'create' => '创建文件', ], @@ -291,40 +295,40 @@ return [ 'config' => [ 'name' => [ 'header' => '服务器名', - 'header_sub' => '更改您服务器的名称。', - 'details' => '此服务器名只是为了让你更好的管理服务器,并不会对服务器内的玩家有所影响.', + 'header_sub' => '更改服务器名称。', + 'details' => '此服务器名只是为了让您更好的管理服务器,并不会对向游戏内玩家显示的服务器配置造成影响。', ], 'startup' => [ 'header' => '启动配置', - 'header_sub' => '控制服务器的启动参数.', + 'header_sub' => '控制服务器的启动参数。', 'command' => '启动命令', 'edit_params' => '编辑参数', 'update' => '更新启动参数', 'startup_regex' => '输入规则', - 'edited' => '启动参数已成功更新. 更新的内容会在下一次启动时生效.', + 'edited' => '已成功编辑启动变量。这将在下次服务器启动时发挥功用。', ], 'sftp' => [ 'header' => 'SFTP 配置', - 'header_sub' => 'SFTP 连接所需要的信息.', + 'header_sub' => 'SFTP 连接所需的账户信息。', 'details' => 'SFTP 信息', 'conn_addr' => '连接地址', - 'warning' => 'SFTP密码就是您的用户密码. 请确认你使用的时SFTP,不是FTP,也不是FTPS, 这些都是不同的协议.', + 'warning' => 'SFTP 密码为您的账户密码。请确保您的客户端被设置为使用 SFTP 而非 FTP 或 FTPS,这些协议间存在差异。', ], 'database' => [ 'header' => '数据库', - 'header_sub' => '此服务器可用的数据库.', + 'header_sub' => '此服务器的所有可用数据库。', 'your_dbs' => '已配置的数据库', 'host' => 'MySQL 主机', - 'reset_password' => '重设密码', - 'no_dbs' => '没有此服务器可用的数据库.', - 'add_db' => '创建一个新数据库.', + 'reset_password' => '重置密码', + 'no_dbs' => '此服务器没有可用的数据库。', + 'add_db' => '新建新数据库。', ], 'allocation' => [ - 'header' => '服务器连接信息', - 'header_sub' => '控制此服务器可用的IP和端口.', - 'available' => '可用的连接信息', - 'help' => '连接信息版主', - 'help_text' => '左边列出的所有IP和端口都是开放的,是您连接到您服务器的地址', + 'header' => '服务器地址分配', + 'header_sub' => '控制此服务器可使用的 IP 地址和端口。', + 'available' => '可用分配地址', + 'help' => '分配地址帮助', + 'help_text' => '左方列表列出了您可用于传入连接的所有可用 IP 地址及端口。', ], ], ]; diff --git a/resources/lang/zh/strings.php b/resources/lang/zh/strings.php index 50bc70697..577cc6f4e 100644 --- a/resources/lang/zh/strings.php +++ b/resources/lang/zh/strings.php @@ -1,14 +1,14 @@ 'Email', - 'user_identifier' => '用户名 或 Email', + 'email' => '电子邮件地址', + 'user_identifier' => '用户名或电子邮件地址', 'password' => '密码', 'confirm_password' => '确认密码', - 'login' => '登陆', - 'home' => '主页', + 'login' => '登录', + 'home' => '首页', 'servers' => '服务器', - 'id' => 'ID', + 'id' => '编号', 'name' => '名称', 'node' => '节点', 'connection' => '连接', @@ -16,12 +16,12 @@ return [ 'cpu' => 'CPU', 'status' => '状态', 'search' => '搜索', - 'suspended' => '已暂停', + 'suspended' => '已停用', 'account' => '用户', 'security' => '安全', 'ip' => 'IP 地址', 'last_activity' => '上次活动', - 'revoke' => '吊销', + 'revoke' => '注销', '2fa_token' => '认证密钥', 'submit' => '确认', 'close' => '关闭', @@ -29,11 +29,11 @@ return [ 'configuration' => '配置', 'sftp' => 'SFTP', 'databases' => '数据库', - 'memo' => 'Memo', + 'memo' => '描述', 'created' => '已创建', 'expires' => '过期', 'public_key' => '令牌', - 'api_access' => 'Api 访问', + 'api_access' => 'API 访问', 'never' => '从未', 'sign_out' => '登出', 'admin_control' => '管理员面板', @@ -64,25 +64,25 @@ return [ '2fa' => '两步验证', 'logout' => '登出', 'admin_cp' => '管理员控制面板', - 'optional' => '可选的', + 'optional' => '可选项', 'read_only' => '只读', 'relation' => '关系', 'owner' => '所有者', 'admin' => '管理员', 'subuser' => '子用户', - 'captcha_invalid' => '输入的验证码错误.', + 'captcha_invalid' => '验证码无效', 'tasks' => '任务', 'seconds' => '秒', 'minutes' => '分', 'under_maintenance' => '维护中', 'days' => [ - 'sun' => '周日', - 'mon' => '周一', - 'tues' => '周二', - 'wed' => '周三', - 'thurs' => '周四', - 'fri' => '周五', - 'sat' => '周六', + 'sun' => '星期天', + 'mon' => '星期一', + 'tues' => '星期二', + 'wed' => '星期三', + 'thurs' => '星期四', + 'fri' => '星期五', + 'sat' => '星期六', ], 'last_used' => '上次使用', ]; diff --git a/resources/lang/zh/validation.php b/resources/lang/zh/validation.php index 932a39479..201880ec9 100644 --- a/resources/lang/zh/validation.php +++ b/resources/lang/zh/validation.php @@ -12,78 +12,78 @@ return [ | */ - 'accepted' => ' :attribute 被接受.', - 'active_url' => ' :attribute 不是一个有效的URL.', - 'after' => ' :attribute 必须是一个位于 :date 之后的日期.', - 'after_or_equal' => ' :attribute 必须是 :date 之后或同样的日期.', - 'alpha' => ' :attribute 只能含有字母.', - 'alpha_dash' => ':attribute 只能含有数字字母和分隔线.', - 'alpha_num' => ' :attribute 只能含有数字和字母.', - 'array' => ' :attribute 必须是个数组.', - 'before' => ' :attribute 必须是一个位于 :date 之前的日前.', - 'before_or_equal' => ' :attribute 必须是 :date 之前或同样的日期.', + 'accepted' => 'The :attribute must be accepted.', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', + 'alpha' => 'The :attribute may only contain letters.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', + 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'before' => 'The :attribute must be a date before :date.', + 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', 'between' => [ - 'numeric' => ' :attribute 必须在 :min 到 :max 之间.', - 'file' => ' :attribute 必须在 :min 到 :max KB 之间.', - 'string' => ' :attribute m必须在 :min 到 :max 个字符之间.', - 'array' => ' :attribute 必须在 :min 到 :max 个项目之间.', + 'numeric' => 'The :attribute must be between :min and :max.', + 'file' => 'The :attribute must be between :min and :max kilobytes.', + 'string' => 'The :attribute must be between :min and :max characters.', + 'array' => 'The :attribute must have between :min and :max items.', ], - 'boolean' => ' :attribute 填入的必须为 true 或 false.', - 'confirmed' => ' :attribute 确认不匹配.', - 'date' => ' :attribute 不是一个合法的日期.', - 'date_format' => ' :attribute 不是正确的格式: :format.', - 'different' => ' :attribute 和 :other 必须不同.', - 'digits' => ' :attribute 必须为 :digits 个数字.', - 'digits_between' => ' :attribute 必须在 :min 到 :max 个数字间.', - 'dimensions' => ' :attribute 有一个非法的镜像大小.', - 'distinct' => ' :attribute 填入了一个重复的值.', - 'email' => ' :attribute 必须是一个合法的Email地址.', - 'exists' => '所选择的 :attribute 无效.', - 'file' => ' :attribute 必须为一个文件.', - 'filled' => ' :attribute 为必填项目.', - 'image' => ' :attribute 必须是一个镜像.', - 'in' => '所选择的 :attribute 无效.', - 'in_array' => ' :attribute 填入的信息在 :other 不存在.', - 'integer' => ' :attribute 必须是一个整数.', - 'ip' => ' :attribute 必须是一个合法的IP地址.', - 'json' => ' :attribute 必须是一个合法的JSON字符串.', + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'date' => 'The :attribute is not a valid date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'email' => 'The :attribute must be a valid email address.', + 'exists' => 'The selected :attribute is invalid.', + 'file' => 'The :attribute must be a file.', + 'filled' => 'The :attribute field is required.', + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'integer' => 'The :attribute must be an integer.', + 'ip' => 'The :attribute must be a valid IP address.', + 'json' => 'The :attribute must be a valid JSON string.', 'max' => [ - 'numeric' => ' :attribute 不能大于 :max.', - 'file' => ' :attribute 不能大于 :max KB.', - 'string' => ' :attribute 不能多于 :max 个字符.', - 'array' => ' :attribute 不能多于 :max 个项目.', + 'numeric' => 'The :attribute may not be greater than :max.', + 'file' => 'The :attribute may not be greater than :max kilobytes.', + 'string' => 'The :attribute may not be greater than :max characters.', + 'array' => 'The :attribute may not have more than :max items.', ], - 'mimes' => ' :attribute 文件类型必须为: :values.', - 'mimetypes' => ' :attribute 文件类型必须为: :values.', + 'mimes' => 'The :attribute must be a file of type: :values.', + 'mimetypes' => 'The :attribute must be a file of type: :values.', 'min' => [ - 'numeric' => ' :attribute 至少应在 :min.', - 'file' => ' :attribute 至少应在 :min KB.', - 'string' => ' :attribute 至少应在 :min 个字符.', - 'array' => ' :attribute 至少应有 :min 个项目.', + 'numeric' => 'The :attribute must be at least :min.', + 'file' => 'The :attribute must be at least :min kilobytes.', + 'string' => 'The :attribute must be at least :min characters.', + 'array' => 'The :attribute must have at least :min items.', ], - 'not_in' => '所选择的 :attribute 不正确.', - 'numeric' => ' :attribute 必须是个数字.', - 'present' => ' :attribute 填入的必须存在.', - 'regex' => ' :attribute 格式不正确.', - 'required' => ' :attribute 为必填.', - 'required_if' => ' :attribute 被要求填入, 当 :other 为 :value 的时候.', - 'required_unless' => ' :attribute 被要求填入,除非 :other 为 :values.', - 'required_with' => ' :attribute 被要求填入,当 :values 存在的时候.', - 'required_with_all' => ' :attribute 被要求填入,当 :values 存在.', - 'required_without' => ' :attribute 被要求填入,当 :values 不存在.', - 'required_without_all' => ' :attribute 被要求填入,当 :values 都不存在.', - 'same' => ' :attribute 和 :other 必须相同.', + 'not_in' => 'The selected :attribute is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'present' => 'The :attribute field must be present.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values is present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'same' => 'The :attribute and :other must match.', 'size' => [ - 'numeric' => ' :attribute 必须为 :size.', - 'file' => ' :attribute 必须为 :size KB.', - 'string' => ' :attribute 必须为 :size 个字符.', - 'array' => ' :attribute 必须包含 :size 个项目.', + 'numeric' => 'The :attribute must be :size.', + 'file' => 'The :attribute must be :size kilobytes.', + 'string' => 'The :attribute must be :size characters.', + 'array' => 'The :attribute must contain :size items.', ], - 'string' => ' :attribute 必须为字符串.', - 'timezone' => ' :attribute 必须是一个有效的时区.', - 'unique' => ' :attribute 已经被使用.', - 'uploaded' => ' :attribute 上传失败.', - 'url' => ' :attribute 格式不合法.', + 'string' => 'The :attribute must be a string.', + 'timezone' => 'The :attribute must be a valid zone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'url' => 'The :attribute format is invalid.', /* |-------------------------------------------------------------------------- diff --git a/resources/scripts/.eslintrc.yml b/resources/scripts/.eslintrc.yml new file mode 100644 index 000000000..a5cfc529e --- /dev/null +++ b/resources/scripts/.eslintrc.yml @@ -0,0 +1,36 @@ +parser: "@typescript-eslint/parser" +parserOptions: + ecmaVersion: 6 + project: "./tsconfig.json" + tsconfigRootDir: "./" +env: + browser: true + es6: true +plugins: + - "@typescript-eslint" +extends: + - "standard" + - "plugin:@typescript-eslint/recommended" +rules: + semi: + - error + - always + comma-dangle: + - error + - always-multiline + "@typescript-eslint/explicit-function-return-type": 0 + "@typescript-eslint/explicit-member-accessibility": 0 + "@typescript-eslint/no-unused-vars": 0 + "@typescript-eslint/no-explicit-any": 0 + "@typescript-eslint/no-non-null-assertion": 0 +overrides: + - files: + - "**/*.tsx" + rules: + operator-linebreak: + - error + - before + - overrides: + "&&": "after" + "?": "ignore" + ":": "ignore" diff --git a/resources/scripts/TransitionRouter.tsx b/resources/scripts/TransitionRouter.tsx new file mode 100644 index 000000000..cdb7a1d80 --- /dev/null +++ b/resources/scripts/TransitionRouter.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Route } from 'react-router'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; + +type Props = Readonly<{ + children: React.ReactNode; +}>; + +export default ({ children }: Props) => ( + ( + + +
    + {children} +
    +

    + © 2015 - 2019  + + Pterodactyl Software + +

    +
    +
    +
    +
    + )} + /> +); diff --git a/resources/scripts/api/account/updateAccountEmail.ts b/resources/scripts/api/account/updateAccountEmail.ts new file mode 100644 index 000000000..5ff230265 --- /dev/null +++ b/resources/scripts/api/account/updateAccountEmail.ts @@ -0,0 +1,9 @@ +import http from '@/api/http'; + +export default (email: string, password: string): Promise => { + return new Promise((resolve, reject) => { + http.put('/api/client/account/email', { email, password }) + .then(() => resolve()) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/account/updateAccountPassword.ts b/resources/scripts/api/account/updateAccountPassword.ts new file mode 100644 index 000000000..c29aefd2d --- /dev/null +++ b/resources/scripts/api/account/updateAccountPassword.ts @@ -0,0 +1,21 @@ +import http from '@/api/http'; + +interface Data { + current: string; + password: string; + confirmPassword: string; +} + +export default ({ current, password, confirmPassword }: Data): Promise => { + return new Promise((resolve, reject) => { + http.put('/api/client/account/password', { + // eslint-disable-next-line @typescript-eslint/camelcase + current_password: current, + password: password, + // eslint-disable-next-line @typescript-eslint/camelcase + password_confirmation: confirmPassword, + }) + .then(() => resolve()) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/auth/login.ts b/resources/scripts/api/auth/login.ts new file mode 100644 index 000000000..2bbdfe5a0 --- /dev/null +++ b/resources/scripts/api/auth/login.ts @@ -0,0 +1,25 @@ +import http from '@/api/http'; + +export interface LoginResponse { + complete: boolean; + intended?: string; + confirmationToken?: string; +} + +export default (user: string, password: string): Promise => { + return new Promise((resolve, reject) => { + http.post('/auth/login', { user, password }) + .then(response => { + if (!(response.data instanceof Object)) { + return reject(new Error('An error occurred while processing the login request.')); + } + + return resolve({ + complete: response.data.data.complete, + intended: response.data.data.intended || undefined, + confirmationToken: response.data.data.confirmation_token || undefined, + }); + }) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/auth/loginCheckpoint.ts b/resources/scripts/api/auth/loginCheckpoint.ts new file mode 100644 index 000000000..244d27c81 --- /dev/null +++ b/resources/scripts/api/auth/loginCheckpoint.ts @@ -0,0 +1,18 @@ +import http from '@/api/http'; +import { LoginResponse } from '@/api/auth/login'; + +export default (token: string, code: string): Promise => { + return new Promise((resolve, reject) => { + http.post('/auth/login/checkpoint', { + // eslint-disable-next-line @typescript-eslint/camelcase + confirmation_token: token, + // eslint-disable-next-line @typescript-eslint/camelcase + authentication_code: code, + }) + .then(response => resolve({ + complete: response.data.data.complete, + intended: response.data.data.intended || undefined, + })) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/auth/performPasswordReset.ts b/resources/scripts/api/auth/performPasswordReset.ts new file mode 100644 index 000000000..f6263c4fe --- /dev/null +++ b/resources/scripts/api/auth/performPasswordReset.ts @@ -0,0 +1,29 @@ +import http from '@/api/http'; + +interface Data { + token: string; + password: string; + passwordConfirmation: string; +} + +interface PasswordResetResponse { + redirectTo?: string | null; + sendToLogin: boolean; +} + +export default (email: string, data: Data): Promise => { + return new Promise((resolve, reject) => { + http.post('/auth/password/reset', { + email, + token: data.token, + password: data.password, + // eslint-disable-next-line @typescript-eslint/camelcase + password_confirmation: data.passwordConfirmation, + }) + .then(response => resolve({ + redirectTo: response.data.redirect_to, + sendToLogin: response.data.send_to_login, + })) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/auth/requestPasswordResetEmail.ts b/resources/scripts/api/auth/requestPasswordResetEmail.ts new file mode 100644 index 000000000..d70139899 --- /dev/null +++ b/resources/scripts/api/auth/requestPasswordResetEmail.ts @@ -0,0 +1,9 @@ +import http from '@/api/http'; + +export default (email: string): Promise => { + return new Promise((resolve, reject) => { + http.post('/auth/password', { email }) + .then(response => resolve(response.data.status || '')) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/http.ts b/resources/scripts/api/http.ts new file mode 100644 index 000000000..676a735c7 --- /dev/null +++ b/resources/scripts/api/http.ts @@ -0,0 +1,43 @@ +import axios, { AxiosInstance } from 'axios'; + +// This token is set in the bootstrap.js file at the beginning of the request +// and is carried through from there. +// const token: string = ''; + +const http: AxiosInstance = axios.create({ + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-CSRF-Token': (window as any).X_CSRF_TOKEN as string || '', + }, +}); + +// If we have a phpdebugbar instance registered at this point in time go +// ahead and route the response data through to it so things show up. +// @ts-ignore +if (typeof window.phpdebugbar !== 'undefined') { + http.interceptors.response.use(response => { + // @ts-ignore + window.phpdebugbar.ajaxHandler.handle(response.request); + + return response; + }); +} + +export default http; + +/** + * Converts an error into a human readable response. Mostly just a generic helper to + * make sure we display the message from the server back to the user if we can. + */ +export function httpErrorToHuman (error: any): string { + if (error.response && error.response.data) { + const { data } = error.response; + if (data.errors && data.errors[0] && data.errors[0].detail) { + return data.errors[0].detail; + } + } + + return error.message; +} diff --git a/resources/scripts/api/server/createServerDatabase.ts b/resources/scripts/api/server/createServerDatabase.ts new file mode 100644 index 000000000..90103337c --- /dev/null +++ b/resources/scripts/api/server/createServerDatabase.ts @@ -0,0 +1,15 @@ +import { rawDataToServerDatabase, ServerDatabase } from '@/api/server/getServerDatabases'; +import http from '@/api/http'; + +export default (uuid: string, data: { connectionsFrom: string; databaseName: string }): Promise => { + return new Promise((resolve, reject) => { + http.post(`/api/client/servers/${uuid}/databases`, { + database: data.databaseName, + remote: data.connectionsFrom, + }, { + params: { include: 'password' }, + }) + .then(response => resolve(rawDataToServerDatabase(response.data.attributes))) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/server/deleteServerDatabase.ts b/resources/scripts/api/server/deleteServerDatabase.ts new file mode 100644 index 000000000..23275bd36 --- /dev/null +++ b/resources/scripts/api/server/deleteServerDatabase.ts @@ -0,0 +1,9 @@ +import http from '@/api/http'; + +export default (uuid: string, database: string): Promise => { + return new Promise((resolve, reject) => { + http.delete(`/api/client/servers/${uuid}/databases/${database}`) + .then(() => resolve()) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/server/getServer.ts b/resources/scripts/api/server/getServer.ts new file mode 100644 index 000000000..e56a0f780 --- /dev/null +++ b/resources/scripts/api/server/getServer.ts @@ -0,0 +1,50 @@ +import http from '@/api/http'; + +export interface Allocation { + ip: string; + alias: string | null; + port: number; +} + +export interface Server { + id: string; + uuid: string; + name: string; + node: string; + description: string; + allocations: Allocation[]; + limits: { + memory: number; + swap: number; + disk: number; + io: number; + cpu: number; + }; + featureLimits: { + databases: number; + allocations: number; + }; +} + +export const rawDataToServerObject = (data: any): Server => ({ + id: data.identifier, + uuid: data.uuid, + name: data.name, + node: data.node, + description: data.description ? ((data.description.length > 0) ? data.description : null) : null, + allocations: [{ + ip: data.allocation.ip, + alias: null, + port: data.allocation.port, + }], + limits: { ...data.limits }, + featureLimits: { ...data.feature_limits }, +}); + +export default (uuid: string): Promise => { + return new Promise((resolve, reject) => { + http.get(`/api/client/servers/${uuid}`) + .then(response => resolve(rawDataToServerObject(response.data.attributes))) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/server/getServerDatabases.ts b/resources/scripts/api/server/getServerDatabases.ts new file mode 100644 index 000000000..835964c27 --- /dev/null +++ b/resources/scripts/api/server/getServerDatabases.ts @@ -0,0 +1,31 @@ +import http from '@/api/http'; + +export interface ServerDatabase { + id: string; + name: string; + username: string; + connectionString: string; + allowConnectionsFrom: string; + password?: string; +} + +export const rawDataToServerDatabase = (data: any): ServerDatabase => ({ + id: data.id, + name: data.name, + username: data.username, + connectionString: `${data.host.address}:${data.host.port}`, + allowConnectionsFrom: data.connections_from, + password: data.relationships && data.relationships.password ? data.relationships.password.attributes.password : undefined, +}); + +export default (uuid: string, includePassword: boolean = true): Promise => { + return new Promise((resolve, reject) => { + http.get(`/api/client/servers/${uuid}/databases`, { + params: includePassword ? { include: 'password' } : undefined, + }) + .then(response => resolve( + (response.data.data || []).map((item: any) => rawDataToServerDatabase(item.attributes)) + )) + .catch(reject); + }); +}; diff --git a/resources/scripts/components/App.tsx b/resources/scripts/components/App.tsx new file mode 100644 index 000000000..98650be64 --- /dev/null +++ b/resources/scripts/components/App.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { hot } from 'react-hot-loader/root'; +import { BrowserRouter, BrowserRouter as Router, Route, Switch } from 'react-router-dom'; +import { StoreProvider } from 'easy-peasy'; +import { store } from '@/state'; +import DashboardRouter from '@/routers/DashboardRouter'; +import ServerRouter from '@/routers/ServerRouter'; +import AuthenticationRouter from '@/routers/AuthenticationRouter'; +import { Provider } from 'react-redux'; + +interface WindowWithUser extends Window { + PterodactylUser?: { + uuid: string; + username: string; + email: string; + root_admin: boolean; + use_totp: boolean; + language: string; + updated_at: string; + created_at: string; + }; +} + +const App = () => { + const data = (window as WindowWithUser).PterodactylUser; + if (data && !store.getState().user.data) { + store.getActions().user.setUserData({ + uuid: data.uuid, + username: data.username, + email: data.email, + language: data.language, + rootAdmin: data.root_admin, + useTotp: data.use_totp, + createdAt: new Date(data.created_at), + updatedAt: new Date(data.updated_at), + }); + } + + return ( + + + +
    + + + + + + + +
    +
    +
    +
    + ); +}; + +export default hot(App); diff --git a/resources/scripts/components/FlashMessageRender.tsx b/resources/scripts/components/FlashMessageRender.tsx new file mode 100644 index 000000000..cb0b37026 --- /dev/null +++ b/resources/scripts/components/FlashMessageRender.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import MessageBox from '@/components/MessageBox'; +import { State, useStoreState } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; + +type Props = Readonly<{ + byKey?: string; + spacerClass?: string; + className?: string; +}>; + +export default ({ className, spacerClass, byKey }: Props) => { + const flashes = useStoreState((state: State) => state.flashes.items); + + let filtered = flashes; + if (byKey) { + filtered = flashes.filter(flash => flash.key === byKey); + } + + if (filtered.length === 0) { + return null; + } + + return ( +
    + { + filtered.map((flash, index) => ( + + {index > 0 &&
    } + + {flash.message} + +
    + )) + } +
    + ); +}; diff --git a/resources/scripts/components/MessageBox.tsx b/resources/scripts/components/MessageBox.tsx new file mode 100644 index 000000000..a962afb88 --- /dev/null +++ b/resources/scripts/components/MessageBox.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; + +export type FlashMessageType = 'success' | 'info' | 'warning' | 'error'; + +interface Props { + title?: string; + children: string; + type?: FlashMessageType; +} + +export default ({ title, children, type }: Props) => ( +
    + {title && {title}} + + {children} + +
    +); diff --git a/resources/scripts/components/NavigationBar.tsx b/resources/scripts/components/NavigationBar.tsx new file mode 100644 index 000000000..85b400c00 --- /dev/null +++ b/resources/scripts/components/NavigationBar.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { Link, NavLink } from 'react-router-dom'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faLayerGroup } from '@fortawesome/free-solid-svg-icons/faLayerGroup'; +import { faUserCircle } from '@fortawesome/free-solid-svg-icons/faUserCircle'; +import { faSignOutAlt } from '@fortawesome/free-solid-svg-icons/faSignOutAlt'; +import { faSwatchbook } from '@fortawesome/free-solid-svg-icons/faSwatchbook'; + +export default () => ( +
    +
    +
    + + Pterodactyl + +
    +
    + + + + + + + {process.env.NODE_ENV !== 'production' && + + + + } + + + +
    +
    +
    +); diff --git a/resources/scripts/components/NetworkErrorMessage.tsx b/resources/scripts/components/NetworkErrorMessage.tsx new file mode 100644 index 000000000..8d4150f2c --- /dev/null +++ b/resources/scripts/components/NetworkErrorMessage.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import MessageBox from '@/components/MessageBox'; + +export default ({ message }: { message: string | undefined | null }) => ( + !message ? + null + : +
    + + {message} + +
    +); diff --git a/resources/scripts/components/ServerOverviewContainer.tsx b/resources/scripts/components/ServerOverviewContainer.tsx new file mode 100644 index 000000000..8cc5cf7cb --- /dev/null +++ b/resources/scripts/components/ServerOverviewContainer.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { NavLink } from 'react-router-dom'; + +export default class ServerOverviewContainer extends React.PureComponent { + render () { + return ( +
    + Account + Design +
    + ); + } +} diff --git a/resources/scripts/components/auth/ForgotPasswordContainer.tsx b/resources/scripts/components/auth/ForgotPasswordContainer.tsx new file mode 100644 index 000000000..668875ca0 --- /dev/null +++ b/resources/scripts/components/auth/ForgotPasswordContainer.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail'; +import { httpErrorToHuman } from '@/api/http'; +import LoginFormContainer from '@/components/auth/LoginFormContainer'; +import { Actions, useStoreActions } from 'easy-peasy'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import { ApplicationStore } from '@/state'; + +export default () => { + const [ isSubmitting, setSubmitting ] = React.useState(false); + const [ email, setEmail ] = React.useState(''); + + const { clearFlashes, addFlash } = useStoreActions((actions: Actions) => actions.flashes); + + const handleFieldUpdate = (e: React.ChangeEvent) => setEmail(e.target.value); + + const handleSubmission = (e: React.FormEvent) => { + e.preventDefault(); + + setSubmitting(true); + clearFlashes(); + requestPasswordResetEmail(email) + .then(response => { + setEmail(''); + addFlash({ type: 'success', title: 'Success', message: response }); + }) + .catch(error => { + console.error(error); + addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) }); + }) + .then(() => setSubmitting(false)); + }; + + return ( +
    +

    + Request Password Reset +

    + + + + +

    + Enter your account email address to receive instructions on resetting your password. +

    +
    + +
    +
    + + Return to Login + +
    +
    +
    + ); +}; diff --git a/resources/scripts/components/auth/LoginCheckpointContainer.tsx b/resources/scripts/components/auth/LoginCheckpointContainer.tsx new file mode 100644 index 000000000..54ac3ee48 --- /dev/null +++ b/resources/scripts/components/auth/LoginCheckpointContainer.tsx @@ -0,0 +1,91 @@ +import React, { useState } from 'react'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import loginCheckpoint from '@/api/auth/loginCheckpoint'; +import { httpErrorToHuman } from '@/api/http'; +import LoginFormContainer from '@/components/auth/LoginFormContainer'; +import { Actions, useStoreActions } from 'easy-peasy'; +import { StaticContext } from 'react-router'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import { ApplicationStore } from '@/state'; + +export default ({ history, location: { state } }: RouteComponentProps<{}, StaticContext, { token?: string }>) => { + const [ code, setCode ] = useState(''); + const [ isLoading, setIsLoading ] = useState(false); + + const { clearFlashes, addFlash } = useStoreActions((actions: Actions) => actions.flashes); + + if (!state || !state.token) { + history.replace('/auth/login'); + + return null; + } + + const onChangeHandler = (e: React.ChangeEvent) => { + if (e.target.value.length <= 6) { + setCode(e.target.value); + } + }; + + const submit = (e: React.FormEvent) => { + e.preventDefault(); + + setIsLoading(true); + clearFlashes(); + + loginCheckpoint(state.token!, code) + .then(response => { + if (response.complete) { + // @ts-ignore + window.location = response.intended || '/'; + } + }) + .catch(error => { + console.error(error); + addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) }); + setIsLoading(false); + }); + }; + + return ( + +

    + Device Checkpoint +

    + + +
    + + +
    +
    + +
    +
    + + Return to Login + +
    +
    +
    + ); +}; diff --git a/resources/scripts/components/auth/LoginContainer.tsx b/resources/scripts/components/auth/LoginContainer.tsx new file mode 100644 index 000000000..95538d78c --- /dev/null +++ b/resources/scripts/components/auth/LoginContainer.tsx @@ -0,0 +1,94 @@ +import React, { useState } from 'react'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import login from '@/api/auth/login'; +import { httpErrorToHuman } from '@/api/http'; +import LoginFormContainer from '@/components/auth/LoginFormContainer'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import { Actions, useStoreActions } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; + +export default ({ history }: RouteComponentProps) => { + const [ username, setUsername ] = useState(''); + const [ password, setPassword ] = useState(''); + const [ isLoading, setLoading ] = useState(false); + + const { clearFlashes, addFlash } = useStoreActions((actions: Actions) => actions.flashes); + + const submit = (e: React.FormEvent) => { + e.preventDefault(); + + setLoading(true); + clearFlashes(); + + login(username!, password!) + .then(response => { + if (response.complete) { + // @ts-ignore + window.location = response.intended || '/'; + return; + } + + history.replace('/auth/login/checkpoint', { token: response.confirmationToken }); + }) + .catch(error => { + console.error(error); + + setLoading(false); + addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) }); + }); + }; + + const canSubmit = () => username && password && username.length > 0 && password.length > 0; + + return ( + +

    + Login to Continue +

    + + + + setUsername(e.target.value)} + disabled={isLoading} + /> +
    + + setPassword(e.target.value)} + disabled={isLoading} + /> +
    +
    + +
    +
    + + Forgot password? + +
    +
    +
    + ); +}; diff --git a/resources/scripts/components/auth/LoginFormContainer.tsx b/resources/scripts/components/auth/LoginFormContainer.tsx new file mode 100644 index 000000000..8f364e335 --- /dev/null +++ b/resources/scripts/components/auth/LoginFormContainer.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; + +export default ({ className, ...props }: React.DetailedHTMLProps, HTMLFormElement>) => ( +
    +
    + +
    +
    + {props.children} +
    +
    +); diff --git a/resources/scripts/components/auth/ResetPasswordContainer.tsx b/resources/scripts/components/auth/ResetPasswordContainer.tsx new file mode 100644 index 000000000..6954ebd2a --- /dev/null +++ b/resources/scripts/components/auth/ResetPasswordContainer.tsx @@ -0,0 +1,109 @@ +import React, { useState } from 'react'; +import { RouteComponentProps } from 'react-router'; +import { parse } from 'query-string'; +import { Link } from 'react-router-dom'; +import performPasswordReset from '@/api/auth/performPasswordReset'; +import { httpErrorToHuman } from '@/api/http'; +import LoginFormContainer from '@/components/auth/LoginFormContainer'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import { Actions, useStoreActions } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; + +type Props = Readonly & {}>; + +export default (props: Props) => { + const [ isLoading, setIsLoading ] = useState(false); + const [ email, setEmail ] = useState(''); + const [ password, setPassword ] = useState(''); + const [ passwordConfirm, setPasswordConfirm ] = useState(''); + + const { clearFlashes, addFlash } = useStoreActions((actions: Actions) => actions.flashes); + + const parsed = parse(props.location.search); + if (email.length === 0 && parsed.email) { + setEmail(parsed.email as string); + } + + const canSubmit = () => password && email && password.length >= 8 && password === passwordConfirm; + + const submit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!password || !email || !passwordConfirm) { + return; + } + + setIsLoading(true); + clearFlashes(); + + performPasswordReset(email, { + token: props.match.params.token, password, passwordConfirmation: passwordConfirm, + }) + .then(() => { + addFlash({ type: 'success', message: 'Your password has been reset, please login to continue.' }); + props.history.push('/auth/login'); + }) + .catch(error => { + console.error(error); + addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) }); + }) + .then(() => setIsLoading(false)); + }; + + return ( +
    +

    + Reset Password +

    + + + + +
    + + setPassword(e.target.value)} + /> +

    + Passwords must be at least 8 characters in length. +

    +
    +
    + + setPasswordConfirm(e.target.value)} + /> +
    +
    + +
    +
    + + Return to Login + +
    +
    +
    + ); +}; diff --git a/resources/scripts/components/dashboard/AccountOverviewContainer.tsx b/resources/scripts/components/dashboard/AccountOverviewContainer.tsx new file mode 100644 index 000000000..e35fb7d61 --- /dev/null +++ b/resources/scripts/components/dashboard/AccountOverviewContainer.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import ContentBox from '@/components/elements/ContentBox'; +import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm'; +import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm'; + +export default () => { + return ( +
    + + + + + + +
    + ); +}; diff --git a/resources/scripts/components/dashboard/DashboardContainer.tsx b/resources/scripts/components/dashboard/DashboardContainer.tsx new file mode 100644 index 000000000..a9c59a679 --- /dev/null +++ b/resources/scripts/components/dashboard/DashboardContainer.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faServer } from '@fortawesome/free-solid-svg-icons/faServer'; +import { faMicrochip } from '@fortawesome/free-solid-svg-icons/faMicrochip'; +import { faMemory } from '@fortawesome/free-solid-svg-icons/faMemory'; +import { faHdd } from '@fortawesome/free-solid-svg-icons/faHdd'; +import { faEthernet } from '@fortawesome/free-solid-svg-icons/faEthernet'; +import { Link } from 'react-router-dom'; + +export default () => ( +
    + +
    + +
    +
    +

    Party Parrots

    +
    +
    +
    + +

    + 192.168.100.100:25565 +

    +
    +
    + +

    + 34.6% +

    +
    +
    +
    + +

    + 2094 MB +

    +
    +

    of 4096 MB

    +
    +
    +
    + +

    + 278 MB +

    +
    +

    of 16 GB

    +
    +
    + +
    +
    + +
    +
    +

    My Factions Server

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore + et dolore magna aliqua. +

    +
    +
    +
    + +

    + 192.168.202.10:34556 +

    +
    +
    + +

    + 98.2 % +

    +
    +
    +
    + +

    + 376 MB +

    +
    +

    of 1024 MB

    +
    +
    +
    + +

    + 187 MB +

    +
    +

    of 32 GB

    +
    +
    +
    +
    +); diff --git a/resources/scripts/components/dashboard/DesignElementsContainer.tsx b/resources/scripts/components/dashboard/DesignElementsContainer.tsx new file mode 100644 index 000000000..fc4ad61f8 --- /dev/null +++ b/resources/scripts/components/dashboard/DesignElementsContainer.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import ContentBox from '@/components/elements/ContentBox'; + +export default class DesignElementsContainer extends React.PureComponent { + render () { + return ( + +
    +
    + +

    + Your demands have been received: Dark Mode will be default in Pterodactyl 0.8! +

    +

    Back

    +
    +
    +

    Form Elements

    +
    + + +

    + This is some descriptive helper text to explain how things work. +

    +
    + + +

    + This field has an error. +

    +
    + + +
    + + +
    + + + + +
    + + +
    + + +
    +
    +
    +
    + + ); + } +} diff --git a/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx b/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx new file mode 100644 index 000000000..1ee88a9ca --- /dev/null +++ b/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { Actions, State, useStoreActions, useStoreState } from 'easy-peasy'; +import { Form, Formik, FormikActions } from 'formik'; +import * as Yup from 'yup'; +import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; +import Field from '@/components/elements/Field'; +import { httpErrorToHuman } from '@/api/http'; +import { ApplicationStore } from '@/state'; + +interface Values { + email: string; + password: string; +} + +const schema = Yup.object().shape({ + email: Yup.string().email().required(), + password: Yup.string().required('You must provide your current account password.'), +}); + +export default () => { + const user = useStoreState((state: State) => state.user.data); + const updateEmail = useStoreActions((state: Actions) => state.user.updateUserEmail); + + const { clearFlashes, addFlash } = useStoreActions((actions: Actions) => actions.flashes); + + const submit = (values: Values, { resetForm, setSubmitting }: FormikActions) => { + clearFlashes('account:email'); + + updateEmail({ ...values }) + .then(() => addFlash({ + type: 'success', + key: 'account:email', + message: 'Your primary email has been updated.', + })) + .catch(error => addFlash({ + type: 'error', + key: 'account:email', + title: 'Error', + message: httpErrorToHuman(error), + })) + .then(() => { + resetForm(); + setSubmitting(false); + }); + }; + + return ( + + { + ({ isSubmitting, isValid }) => ( + + +
    + +
    + +
    +
    + +
    + +
    + ) + } +
    + ); +}; diff --git a/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx b/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx new file mode 100644 index 000000000..7c25c1287 --- /dev/null +++ b/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { Actions, State, useStoreActions, useStoreState } from 'easy-peasy'; +import { Form, Formik, FormikActions } from 'formik'; +import Field from '@/components/elements/Field'; +import * as Yup from 'yup'; +import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; +import updateAccountPassword from '@/api/account/updateAccountPassword'; +import { httpErrorToHuman } from '@/api/http'; +import { ApplicationStore } from '@/state'; + +interface Values { + current: string; + password: string; + confirmPassword: string; +} + +const schema = Yup.object().shape({ + current: Yup.string().min(1).required('You must provide your current password.'), + password: Yup.string().min(8).required(), + confirmPassword: Yup.string().test('password', 'Password confirmation does not match the password you entered.', function (value) { + return value === this.parent.password; + }), +}); + +export default () => { + const user = useStoreState((state: State) => state.user.data); + const { clearFlashes, addFlash } = useStoreActions((actions: Actions) => actions.flashes); + + if (!user) { + return null; + } + + const submit = (values: Values, { resetForm, setSubmitting }: FormikActions) => { + clearFlashes('account:password'); + updateAccountPassword({ ...values }) + .then(() => { + resetForm(); + addFlash({ key: 'account:password', type: 'success', message: 'Your password has been updated.' }); + }) + .catch(error => addFlash({ + key: 'account:password', + type: 'error', + title: 'Error', + message: httpErrorToHuman(error), + })) + .then(() => setSubmitting(false)); + }; + + return ( + + + { + ({ isSubmitting, isValid }) => ( + + +
    + +
    + +
    +
    + +
    +
    + +
    + +
    + ) + } +
    +
    + ); +}; diff --git a/resources/scripts/components/elements/ContentBox.tsx b/resources/scripts/components/elements/ContentBox.tsx new file mode 100644 index 000000000..8fc74ea61 --- /dev/null +++ b/resources/scripts/components/elements/ContentBox.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import classNames from 'classnames'; +import FlashMessageRender from '@/components/FlashMessageRender'; + +type Props = Readonly, HTMLDivElement> & { + title?: string; + borderColor?: string; + showFlashes?: string | boolean; +}>; + +export default ({ title, borderColor, showFlashes, children, ...props }: Props) => ( +
    + {title &&

    {title}

    } + {showFlashes && } +
    + {children} +
    +
    +); diff --git a/resources/scripts/components/elements/Field.tsx b/resources/scripts/components/elements/Field.tsx new file mode 100644 index 000000000..f1820343e --- /dev/null +++ b/resources/scripts/components/elements/Field.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Field, FieldProps } from 'formik'; +import classNames from 'classnames'; + +interface Props { + id?: string; + type: string; + name: string; + label?: string; + description?: string; + validate?: (value: any) => undefined | string | Promise; +} + +export default ({ id, type, name, label, description, validate }: Props) => ( + + { + ({ field, form: { errors, touched } }: FieldProps) => ( + + {label && + + } + + {touched[field.name] && errors[field.name] ? +

    + {(errors[field.name] as string).charAt(0).toUpperCase() + (errors[field.name] as string).slice(1)} +

    + : + description ?

    {description}

    : null + } +
    + ) + } +
    +); diff --git a/resources/scripts/components/elements/Modal.tsx b/resources/scripts/components/elements/Modal.tsx new file mode 100644 index 000000000..be1e62e20 --- /dev/null +++ b/resources/scripts/components/elements/Modal.tsx @@ -0,0 +1,71 @@ +import React, { useEffect, useState } from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTimes } from '@fortawesome/free-solid-svg-icons/faTimes'; +import { CSSTransition } from 'react-transition-group'; +import Spinner from '@/components/elements/Spinner'; + +interface Props { + visible: boolean; + onDismissed: () => void; + dismissable?: boolean; + closeOnEscape?: boolean; + closeOnBackground?: boolean; + showSpinnerOverlay?: boolean; + children: React.ReactNode; +} + +export default (props: Props) => { + const [render, setRender] = useState(props.visible); + + const handleEscapeEvent = (e: KeyboardEvent) => { + if (props.dismissable !== false && props.closeOnEscape !== false && e.key === 'Escape') { + setRender(false); + } + }; + + useEffect(() => setRender(props.visible), [props.visible]); + + useEffect(() => { + window.addEventListener('keydown', handleEscapeEvent); + + return () => window.removeEventListener('keydown', handleEscapeEvent); + }, [render]); + + return ( + props.onDismissed()} + > +
    { + if (props.dismissable !== false && props.closeOnBackground !== false) { + e.stopPropagation(); + if (e.target === e.currentTarget) { + setRender(false); + } + } + }}> +
    + {props.dismissable !== false && +
    setRender(false)}> + +
    + } + {props.showSpinnerOverlay && +
    + +
    + } +
    + {props.children} +
    +
    +
    +
    + ); +}; diff --git a/resources/scripts/components/elements/Spinner.tsx b/resources/scripts/components/elements/Spinner.tsx new file mode 100644 index 000000000..d1704916f --- /dev/null +++ b/resources/scripts/components/elements/Spinner.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import classNames from 'classnames'; + +export default ({ large, centered }: { large?: boolean; centered?: boolean }) => ( + centered ? +
    +
    +
    + : +
    +); diff --git a/resources/scripts/components/elements/SpinnerOverlay.tsx b/resources/scripts/components/elements/SpinnerOverlay.tsx new file mode 100644 index 000000000..92eb43cf1 --- /dev/null +++ b/resources/scripts/components/elements/SpinnerOverlay.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import classNames from 'classnames'; +import { CSSTransition } from 'react-transition-group'; +import Spinner from '@/components/elements/Spinner'; + +export default ({ large, visible }: { visible: boolean; large?: boolean }) => ( + +
    + +
    +
    +); diff --git a/resources/scripts/components/server/Console.tsx b/resources/scripts/components/server/Console.tsx new file mode 100644 index 000000000..66f0b76bd --- /dev/null +++ b/resources/scripts/components/server/Console.tsx @@ -0,0 +1,120 @@ +import React, { createRef } from 'react'; +import { Terminal } from 'xterm'; +import * as TerminalFit from 'xterm/lib/addons/fit/fit'; +import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; +import { connect } from 'react-redux'; +import { Websocket } from '@/plugins/Websocket'; +import { ServerStore } from '@/state/server'; + +const theme = { + background: 'transparent', + cursor: 'transparent', + black: '#000000', + red: '#E54B4B', + green: '#9ECE58', + yellow: '#FAED70', + blue: '#396FE2', + magenta: '#BB80B3', + cyan: '#2DDAFD', + white: '#d0d0d0', + brightBlack: 'rgba(255, 255, 255, 0.2)', + brightRed: '#FF5370', + brightGreen: '#C3E88D', + brightYellow: '#FFCB6B', + brightBlue: '#82AAFF', + brightMagenta: '#C792EA', + brightCyan: '#89DDFF', + brightWhite: '#ffffff', +}; + +interface Props { + connected: boolean; + instance: Websocket | null; +} + +class Console extends React.PureComponent> { + ref = createRef(); + terminal = new Terminal({ + disableStdin: true, + cursorStyle: 'underline', + allowTransparency: true, + fontSize: 12, + fontFamily: 'Menlo, Monaco, Consolas, monospace', + rows: 30, + theme: theme, + }); + + componentDidMount () { + if (this.ref.current) { + this.terminal.open(this.ref.current); + this.terminal.clear(); + + // @see https://github.com/xtermjs/xterm.js/issues/2265 + // @see https://github.com/xtermjs/xterm.js/issues/2230 + TerminalFit.fit(this.terminal); + } + + if (this.props.connected && this.props.instance) { + this.listenForEvents(); + } + } + + componentDidUpdate (prevProps: Readonly>) { + if (!prevProps.connected && this.props.connected) { + this.listenForEvents(); + } + } + + componentWillUnmount () { + if (this.props.instance) { + this.props.instance.removeListener('server log', this.handleServerLog); + this.props.instance.removeListener('server log', this.handleConsoleOutput); + } + } + + listenForEvents () { + const instance = this.props.instance!; + + instance.addListener('server log', this.handleServerLog); + instance.addListener('console output', this.handleConsoleOutput); + instance.send('send logs'); + } + + handleServerLog = (lines: string[]) => lines.forEach(data => { + return data.split(/\n/g).forEach(line => this.terminal.writeln(line + '\u001b[0m')); + }); + + handleConsoleOutput = (line: string) => this.terminal.writeln( + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m' + ); + + render () { + return ( +
    + +
    +
    +
    +
    +
    $
    +
    + +
    +
    +
    + ); + } +} + +export default connect( + (state: ServerStore) => ({ + connected: state.socket.connected, + instance: state.socket.instance, + }), +)(Console); diff --git a/resources/scripts/components/server/ServerConsole.tsx b/resources/scripts/components/server/ServerConsole.tsx new file mode 100644 index 000000000..19a8d9d1b --- /dev/null +++ b/resources/scripts/components/server/ServerConsole.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import Console from '@/components/server/Console'; +import { ServerContext } from '@/state/server'; + +export default () => { + const status = ServerContext.useStoreState(state => state.status.value); + + return ( +
    +
    + +
    +
    +

    Current status: {status}

    +
    +
    + ); +}; diff --git a/resources/scripts/components/server/WebsocketHandler.tsx b/resources/scripts/components/server/WebsocketHandler.tsx new file mode 100644 index 000000000..a8d2f3003 --- /dev/null +++ b/resources/scripts/components/server/WebsocketHandler.tsx @@ -0,0 +1,34 @@ +import React, { useEffect } from 'react'; +import { Websocket } from '@/plugins/Websocket'; +import { ServerContext } from '@/state/server'; + +export default () => { + const server = ServerContext.useStoreState(state => state.server.data); + const instance = ServerContext.useStoreState(state => state.socket.instance); + const setServerStatus = ServerContext.useStoreActions(actions => actions.status.setServerStatus); + const { setInstance, setConnectionState } = ServerContext.useStoreActions(actions => actions.socket); + + useEffect(() => { + // If there is already an instance or there is no server, just exit out of this process + // since we don't need to make a new connection. + if (instance || !server) { + return; + } + + console.log('Connecting!'); + + const socket = new Websocket( + `wss://wings.pterodactyl.test:8080/api/servers/${server.uuid}/ws`, + 'CC8kHCuMkXPosgzGO6d37wvhNcksWxG6kTrA', + ); + + socket.on('SOCKET_OPEN', () => setConnectionState(true)); + socket.on('SOCKET_CLOSE', () => setConnectionState(false)); + socket.on('SOCKET_ERROR', () => setConnectionState(false)); + socket.on('status', (status) => setServerStatus(status)); + + setInstance(socket); + }, [ server ]); + + return null; +}; diff --git a/resources/scripts/components/server/databases/CreateDatabaseButton.tsx b/resources/scripts/components/server/databases/CreateDatabaseButton.tsx new file mode 100644 index 000000000..54261605d --- /dev/null +++ b/resources/scripts/components/server/databases/CreateDatabaseButton.tsx @@ -0,0 +1,113 @@ +import React, { useState } from 'react'; +import { ServerDatabase } from '@/api/server/getServerDatabases'; +import Modal from '@/components/elements/Modal'; +import { Form, Formik, FormikActions } from 'formik'; +import Field from '@/components/elements/Field'; +import { object, string } from 'yup'; +import createServerDatabase from '@/api/server/createServerDatabase'; +import { ServerContext } from '@/state/server'; +import { Actions, useStoreActions } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; +import { httpErrorToHuman } from '@/api/http'; +import FlashMessageRender from '@/components/FlashMessageRender'; + +interface Values { + databaseName: string; + connectionsFrom: string; +} + +const schema = object().shape({ + databaseName: string() + .required('A database name must be provided.') + .min(5, 'Database name must be at least 5 characters.') + .max(64, 'Database name must not exceed 64 characters.') + .matches(/^[A-Za-z0-9_\-.]{5,64}$/, 'Database name should only contain alphanumeric characters, underscores, dashes, and/or periods.'), + connectionsFrom: string() + .required('A connection value must be provided.') + .matches(/^([1-9]{1,3}|%)(\.([0-9]{1,3}|%))?(\.([0-9]{1,3}|%))?(\.([0-9]{1,3}|%))?$/, 'A valid connection address must be provided.'), +}); + +export default ({ onCreated }: { onCreated: (database: ServerDatabase) => void }) => { + const [ visible, setVisible ] = useState(false); + const { addFlash, clearFlashes } = useStoreActions((actions: Actions) => actions.flashes); + const server = ServerContext.useStoreState(state => state.server.data!); + + const submit = (values: Values, { setSubmitting }: FormikActions) => { + clearFlashes(); + createServerDatabase(server.uuid, { ...values }) + .then(database => { + onCreated(database); + setVisible(false); + }) + .catch(error => { + console.log(error); + addFlash({ + key: 'create-database-modal', + type: 'error', + title: 'Error', + message: httpErrorToHuman(error), + }); + }) + .then(() => setSubmitting(false)); + }; + + return ( + + + { + ({ isSubmitting, resetForm }) => ( + { + resetForm(); + setVisible(false); + }} + > + +

    Create new database

    +
    + +
    + +
    +
    + + +
    + +
    + ) + } +
    + +
    + ); +}; diff --git a/resources/scripts/components/server/databases/DatabaseRow.tsx b/resources/scripts/components/server/databases/DatabaseRow.tsx new file mode 100644 index 000000000..3cad11755 --- /dev/null +++ b/resources/scripts/components/server/databases/DatabaseRow.tsx @@ -0,0 +1,159 @@ +import React, { useState } from 'react'; +import { ServerDatabase } from '@/api/server/getServerDatabases'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faDatabase } from '@fortawesome/free-solid-svg-icons/faDatabase'; +import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt'; +import { faEye } from '@fortawesome/free-solid-svg-icons/faEye'; +import classNames from 'classnames'; +import Modal from '@/components/elements/Modal'; +import { Form, Formik, FormikActions } from 'formik'; +import Field from '@/components/elements/Field'; +import { object, string } from 'yup'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import { Actions, useStoreActions } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; +import { ServerContext } from '@/state/server'; +import deleteServerDatabase from '@/api/server/deleteServerDatabase'; +import { httpErrorToHuman } from '@/api/http'; + +interface Props { + database: ServerDatabase; + className?: string; + onDelete: () => void; +} + +export default ({ database, className, onDelete }: Props) => { + const [visible, setVisible] = useState(false); + const [connectionVisible, setConnectionVisible] = useState(false); + const { addFlash, clearFlashes } = useStoreActions((actions: Actions) => actions.flashes); + const server = ServerContext.useStoreState(state => state.server.data!); + + const schema = object().shape({ + confirm: string() + .required('The database name must be provided.') + .oneOf([database.name.split('_', 2)[1], database.name], 'The database name must be provided.'), + }); + + const submit = (values: { confirm: string }, { setSubmitting }: FormikActions<{ confirm: string }>) => { + clearFlashes(); + deleteServerDatabase(server.uuid, database.id) + .then(() => { + setVisible(false); + setTimeout(() => onDelete(), 150); + }) + .catch(error => { + console.error(error); + setSubmitting(false); + addFlash({ + key: 'delete-database-modal', + type: 'error', + title: 'Error', + message: httpErrorToHuman(error), + }); + }); + }; + + return ( + + + { + ({ isSubmitting, isValid, resetForm }) => ( + { setVisible(false); resetForm(); }} + > + +

    Confirm database deletion

    +

    + Deleting a database is a permanent action, it cannot be undone. This will permanetly + delete the {database.name} database and remove all associated data. +

    +
    + +
    + + +
    + +
    + ) + } +
    + setConnectionVisible(false)}> +

    Database connection details

    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    + +
    +
    +

    {database.name}

    +
    +
    +

    Endpoint:

    +

    {database.connectionString}

    +
    +
    +

    + Connections From: +

    +

    {database.allowConnectionsFrom}

    +
    +
    +

    Username:

    +

    {database.username}

    +
    +
    + + +
    +
    +
    + ); +}; diff --git a/resources/scripts/components/server/databases/DatabasesContainer.tsx b/resources/scripts/components/server/databases/DatabasesContainer.tsx new file mode 100644 index 000000000..a7c2c1d72 --- /dev/null +++ b/resources/scripts/components/server/databases/DatabasesContainer.tsx @@ -0,0 +1,64 @@ +import React, { useEffect, useState } from 'react'; +import getServerDatabases, { ServerDatabase } from '@/api/server/getServerDatabases'; +import { ServerContext } from '@/state/server'; +import { Actions, useStoreActions } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; +import { httpErrorToHuman } from '@/api/http'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import DatabaseRow from '@/components/server/databases/DatabaseRow'; +import Spinner from '@/components/elements/Spinner'; +import { CSSTransition } from 'react-transition-group'; +import CreateDatabaseButton from '@/components/server/databases/CreateDatabaseButton'; + +export default () => { + const [ loading, setLoading ] = useState(true); + const [ databases, setDatabases ] = useState([]); + const server = ServerContext.useStoreState(state => state.server.data!); + const { addFlash, clearFlashes } = useStoreActions((actions: Actions) => actions.flashes); + + useEffect(() => { + clearFlashes('databases'); + getServerDatabases(server.uuid) + .then(databases => { + setDatabases(databases); + setLoading(false); + }) + .catch(error => addFlash({ + key: 'databases', + title: 'Error', + message: httpErrorToHuman(error), + type: 'error', + })); + }, []); + + return ( +
    + + {loading ? + + : + + + {databases.length > 0 ? + databases.map((database, index) => ( + setDatabases(s => [ ...s.filter(d => d.id !== database.id) ])} + className={index > 0 ? 'mt-1' : undefined} + /> + )) + : +

    + It looks like you have no databases. Click the button below to create one now. +

    + } +
    + setDatabases(s => [ ...s, database ])}/> +
    +
    +
    + } +
    + ); +}; diff --git a/resources/scripts/index.tsx b/resources/scripts/index.tsx new file mode 100644 index 000000000..ebbb9d880 --- /dev/null +++ b/resources/scripts/index.tsx @@ -0,0 +1,5 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import App from '@/components/App'; + +ReactDOM.render(, document.getElementById('app')); diff --git a/resources/scripts/plugins/Websocket.ts b/resources/scripts/plugins/Websocket.ts new file mode 100644 index 000000000..f48bddec6 --- /dev/null +++ b/resources/scripts/plugins/Websocket.ts @@ -0,0 +1,51 @@ +import Sockette from 'sockette'; +import { EventEmitter } from 'events'; + +export const SOCKET_EVENTS = [ + 'SOCKET_OPEN', + 'SOCKET_RECONNECT', + 'SOCKET_CLOSE', + 'SOCKET_ERROR', +]; + +export class Websocket extends EventEmitter { + socket: Sockette; + + constructor (url: string, protocol: string) { + super(); + + this.socket = new Sockette(url, { + protocols: protocol, + onmessage: e => { + try { + let { event, args } = JSON.parse(e.data); + this.emit(event, ...args); + } catch (ex) { + console.warn('Failed to parse incoming websocket message.', ex); + } + }, + onopen: () => this.emit('SOCKET_OPEN'), + onreconnect: () => this.emit('SOCKET_RECONNECT'), + onclose: () => this.emit('SOCKET_CLOSE'), + onerror: () => this.emit('SOCKET_ERROR'), + }); + } + + close (code?: number, reason?: string) { + this.socket.close(code, reason); + } + + open () { + this.socket.open(); + } + + reconnect () { + this.socket.reconnect(); + } + + send (event: string, payload?: string | string[]) { + this.socket.send(JSON.stringify({ + event, args: Array.isArray(payload) ? payload : [ payload ], + })); + } +} diff --git a/resources/scripts/routers/AuthenticationRouter.tsx b/resources/scripts/routers/AuthenticationRouter.tsx new file mode 100644 index 000000000..4054f55d5 --- /dev/null +++ b/resources/scripts/routers/AuthenticationRouter.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { Route, RouteComponentProps } from 'react-router-dom'; +import LoginContainer from '@/components/auth/LoginContainer'; +import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer'; +import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer'; +import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer'; + +export default ({ match }: RouteComponentProps) => ( +
    + + + + + +
    +); diff --git a/resources/scripts/routers/DashboardRouter.tsx b/resources/scripts/routers/DashboardRouter.tsx new file mode 100644 index 000000000..d9312fd8c --- /dev/null +++ b/resources/scripts/routers/DashboardRouter.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { Route, RouteComponentProps, Switch } from 'react-router-dom'; +import DesignElementsContainer from '@/components/dashboard/DesignElementsContainer'; +import AccountOverviewContainer from '@/components/dashboard/AccountOverviewContainer'; +import NavigationBar from '@/components/NavigationBar'; +import DashboardContainer from '@/components/dashboard/DashboardContainer'; +import TransitionRouter from '@/TransitionRouter'; + +export default ({ location }: RouteComponentProps) => ( + + + +
    + + + + + +
    +
    +
    +); diff --git a/resources/scripts/routers/ServerRouter.tsx b/resources/scripts/routers/ServerRouter.tsx new file mode 100644 index 000000000..4b01f0b63 --- /dev/null +++ b/resources/scripts/routers/ServerRouter.tsx @@ -0,0 +1,63 @@ +import React, { useEffect } from 'react'; +import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom'; +import NavigationBar from '@/components/NavigationBar'; +import ServerConsole from '@/components/server/ServerConsole'; +import TransitionRouter from '@/TransitionRouter'; +import Spinner from '@/components/elements/Spinner'; +import WebsocketHandler from '@/components/server/WebsocketHandler'; +import { ServerContext } from '@/state/server'; +import { Provider } from 'react-redux'; +import DatabasesContainer from '@/components/server/databases/DatabasesContainer'; + +const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => { + const server = ServerContext.useStoreState(state => state.server.data); + const getServer = ServerContext.useStoreActions(actions => actions.server.getServer); + const clearServerState = ServerContext.useStoreActions(actions => actions.clearServerState); + + if (!server) { + getServer(match.params.id); + } + + useEffect(() => () => clearServerState(), []); + + return ( + + +
    +
    +
    + Console + File Manager + Databases + User Management +
    +
    +
    + + +
    + {!server ? +
    + +
    + : + + + + + + + + } +
    +
    +
    +
    + ); +}; + +export default (props: RouteComponentProps) => ( + + + +); diff --git a/resources/scripts/state/flashes.ts b/resources/scripts/state/flashes.ts new file mode 100644 index 000000000..666778a11 --- /dev/null +++ b/resources/scripts/state/flashes.ts @@ -0,0 +1,28 @@ +import { Action, action } from 'easy-peasy'; +import { FlashMessageType } from '@/components/MessageBox'; + +export interface FlashStore { + items: FlashMessage[]; + addFlash: Action; + clearFlashes: Action; +} + +export interface FlashMessage { + id?: string; + key?: string; + type: FlashMessageType; + title?: string; + message: string; +} + +const flashes: FlashStore = { + items: [], + addFlash: action((state, payload) => { + state.items.push(payload); + }), + clearFlashes: action((state, payload) => { + state.items = payload ? state.items.filter(flashes => flashes.key !== payload) : []; + }), +}; + +export default flashes; diff --git a/resources/scripts/state/index.ts b/resources/scripts/state/index.ts new file mode 100644 index 000000000..db3181aa5 --- /dev/null +++ b/resources/scripts/state/index.ts @@ -0,0 +1,15 @@ +import { createStore } from 'easy-peasy'; +import flashes, { FlashStore } from '@/state/flashes'; +import user, { UserStore } from '@/state/user'; + +export interface ApplicationStore { + flashes: FlashStore; + user: UserStore; +} + +const state: ApplicationStore = { + flashes, + user, +}; + +export const store = createStore(state); diff --git a/resources/scripts/state/server/index.ts b/resources/scripts/state/server/index.ts new file mode 100644 index 000000000..c29e61bf8 --- /dev/null +++ b/resources/scripts/state/server/index.ts @@ -0,0 +1,57 @@ +import getServer, { Server } from '@/api/server/getServer'; +import { action, Action, createContextStore, thunk, Thunk } from 'easy-peasy'; +import socket, { SocketStore } from './socket'; + +export type ServerStatus = 'offline' | 'starting' | 'stopping' | 'running'; + +interface ServerDataStore { + data?: Server; + getServer: Thunk>; + setServer: Action; +} + +const server: ServerDataStore = { + getServer: thunk(async (actions, payload) => { + const server = await getServer(payload); + actions.setServer(server); + }), + setServer: action((state, payload) => { + state.data = payload; + }), +}; + +interface ServerStatusStore { + value: ServerStatus; + setServerStatus: Action; +} + +const status: ServerStatusStore = { + value: 'offline', + setServerStatus: action((state, payload) => { + state.value = payload; + }), +}; + +export interface ServerStore { + server: ServerDataStore; + socket: SocketStore; + status: ServerStatusStore; + clearServerState: Action; +} + +export const ServerContext = createContextStore({ + server, + socket, + status, + clearServerState: action(state => { + state.server.data = undefined; + + if (state.socket.instance) { + state.socket.instance.removeAllListeners(); + state.socket.instance.close(); + } + + state.socket.instance = null; + state.socket.connected = false; + }), +}, { name: 'ServerStore' }); diff --git a/resources/scripts/state/server/socket.ts b/resources/scripts/state/server/socket.ts new file mode 100644 index 000000000..e67910668 --- /dev/null +++ b/resources/scripts/state/server/socket.ts @@ -0,0 +1,22 @@ +import { Action, action } from 'easy-peasy'; +import { Websocket } from '@/plugins/Websocket'; + +export interface SocketStore { + instance: Websocket | null; + connected: boolean; + setInstance: Action; + setConnectionState: Action; +} + +const socket: SocketStore = { + instance: null, + connected: false, + setInstance: action((state, payload) => { + state.instance = payload; + }), + setConnectionState: action((state, payload) => { + state.connected = payload; + }), +}; + +export default socket; diff --git a/resources/scripts/state/user.ts b/resources/scripts/state/user.ts new file mode 100644 index 000000000..087bfe003 --- /dev/null +++ b/resources/scripts/state/user.ts @@ -0,0 +1,41 @@ +import { Action, action, Thunk, thunk } from 'easy-peasy'; +import updateAccountEmail from '@/api/account/updateAccountEmail'; + +export interface UserData { + uuid: string; + username: string; + email: string; + language: string; + rootAdmin: boolean; + useTotp: boolean; + createdAt: Date; + updatedAt: Date; +} + +export interface UserStore { + data?: UserData; + setUserData: Action; + updateUserData: Action>; + updateUserEmail: Thunk>; +} + +const user: UserStore = { + data: undefined, + setUserData: action((state, payload) => { + state.data = payload; + }), + + updateUserData: action((state, payload) => { + // Limitation of Typescript, can't do much about that currently unfortunately. + // @ts-ignore + state.data = { ...state.data, ...payload }; + }), + + updateUserEmail: thunk(async (actions, payload) => { + await updateAccountEmail(payload.email, payload.password); + + actions.updateUserData({ email: payload.email }); + }), +}; + +export default user; diff --git a/resources/styles/components/animations.css b/resources/styles/components/animations.css new file mode 100644 index 000000000..d472d1267 --- /dev/null +++ b/resources/styles/components/animations.css @@ -0,0 +1,28 @@ +/*! purgecss start ignore */ +.fade-enter { + @apply .opacity-0; +} + +.fade-enter-active { + @apply .opacity-100; + transition: opacity 250ms; +} + +.fade-exit { + @apply .opacity-100; +} + +.fade-exit-active { + @apply .opacity-0; + transition: opacity 250ms; +} + +/** @todo fix this, hides footer stuff */ +div.route-transition-group { + @apply .relative; + + & section { + @apply .absolute .w-full .pin-t .pin-l; + } +} +/*! purgecss end ignore */ diff --git a/resources/styles/components/authentication.css b/resources/styles/components/authentication.css new file mode 100644 index 000000000..2d841fe2e --- /dev/null +++ b/resources/styles/components/authentication.css @@ -0,0 +1,9 @@ +.login-box { + @apply .bg-white .shadow-lg .rounded-lg .p-6; + + @screen xsx { + @apply .rounded-none; + margin-top: 25%; + box-shadow: 0 15px 30px 0 rgba(0, 0, 0, .2), 0 -15px 30px 0 rgba(0, 0, 0, .2); + } +} diff --git a/resources/styles/components/filemanager.css b/resources/styles/components/filemanager.css new file mode 100644 index 000000000..329f90ea1 --- /dev/null +++ b/resources/styles/components/filemanager.css @@ -0,0 +1,81 @@ +.filemanager { + & .header { + @apply .flex .text-xs .text-neutral-600 .pb-4 .font-bold .border-b .border-neutral-200 .mb-3 .uppercase; + + & > div:not(:last-of-type) { + @apply .pr-4; + } + } + + & .row { + @apply .flex .text-sm .py-3 .text-sm .border .border-transparent .text-black .rounded .no-underline; + + & > div:not(:last-of-type) { + @apply .pr-4; + } + + &.active-selection, &:hover { + @apply .bg-neutral-50 .text-neutral-900; + } + + & > .icon { + @apply .w-8 .text-center; + + & > svg { + @apply .h-4; + } + } + } + + .context-menu { + @apply .absolute .bg-white .py-2 .border .border-neutral-300 .shadow-lg .rounded .text-neutral-600 .text-sm .cursor-pointer; + + & > div:not(:last-of-type) { + @apply .border-b .border-neutral-100 .pb-2 .mb-2; + } + + & .context-row { + @apply .flex .flex-row .items-center .py-2 .px-8 .mx-2 .rounded; + transition: background-color 50ms linear; + + & > .icon { + @apply .flex-none; + + & > svg { + @apply .h-4; + } + } + + & > .action { + @apply .flex-auto .pl-2; + } + + &:hover { + @apply .bg-neutral-50 .text-neutral-800; + } + + &.danger { + @apply .border .border-transparent; + transition: border 50ms linear; + + &:hover { + @apply .bg-red-50 .border-red-100; + } + } + + } + } +} + +.filemanager-breadcrumbs { + @apply .px-4 .py-3 .mb-6 .rounded .bg-white .text-neutral-400 .border .border-neutral-100 .shadow; + + & a { + @apply .no-underline .text-neutral-400; + transition: color 100ms linear; + + &:hover { + @apply .text-primary-500; + } + } +} diff --git a/resources/styles/components/forms.css b/resources/styles/components/forms.css new file mode 100644 index 000000000..a56b86a95 --- /dev/null +++ b/resources/styles/components/forms.css @@ -0,0 +1,197 @@ +textarea, select, input, button { + outline: none; +} + +input[type=number]::-webkit-outer-spin-button, +input[type=number]::-webkit-inner-spin-button { + -webkit-appearance: none !important; + margin: 0; +} + +input[type=number] { + -moz-appearance: textfield !important; +} + +/** + * Styling for other forms throughout the Panel. + */ +.input, .input-dark { + @apply .appearance-none .w-full; + min-width: 0; + + &:required, &:invalid { + box-shadow: none; + } +} + +.input { + @apply .p-3 .rounded .border .border-neutral-200 .text-neutral-800; + transition: border 150ms linear; + + &:focus { + @apply .border-primary-400; + } + + &.error { + @apply .text-red-600 .border-red-500; + } +} + +.input:disabled { + @apply .bg-neutral-100 .border-neutral-200; +} + +.input + .input-help { + @apply .text-xs .text-neutral-400 .pt-2; + + &.error { + @apply .text-red-600; + } +} + +.input-dark { + @apply .p-3 .bg-neutral-600 .border .border-neutral-500 .text-sm .rounded .text-neutral-200 .shadow-none; + transition: border 150ms linear, box-shaodw 150ms ease-in; + + &:focus { + @apply .shadow-md .border-neutral-400; + } + + &:hover { + @apply .border-neutral-400; + } + + & + .input-help { + @apply .text-xs .text-neutral-400 .mt-2 + } + + &.error { + @apply .text-red-100 .border-red-400; + } + + &.error + .input-help { + @apply .text-red-400; + } + + &:disabled { + @apply .opacity-75; + } +} + +label { + @apply .block .text-xs .uppercase .text-neutral-700 .mb-2; +} + +select:not(.appearance-none) { + @apply .outline-none .appearance-none .block .bg-white .border .border-neutral-200 .text-neutral-400 .p-3 .pr-8 rounded; + transition: border-color 150ms linear, color 150ms linear; + + &:hover:not(:disabled), &:focus { + @apply .outline-none .border-primary-500 .text-neutral-700; + } + + -webkit-appearance: none; + -moz-appearance: none; + + &::-ms-expand { + display: none; + } + + background: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath d='M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z'/%3e%3c/svg%3e ") no-repeat center center; + background-size: 1rem; + background-position-x: calc(100% - 0.75rem); +} + +.input-dark-label { + @apply .uppercase .text-neutral-200; +} + +.input-label { + @apply .block .uppercase .tracking-wide .text-neutral-800 .text-xs .font-bold; + + &:not(.mb-0) { + @apply .mb-2; + } +} + +a.btn { + @apply .no-underline; +} + +.btn { + @apply .rounded .p-2 .uppercase .tracking-wide .text-sm; + transition: all 150ms linear; + + /** + * Button Colors + */ + &.btn-primary { + @apply .bg-primary-500 .border-primary-600 .border .text-primary-50; + + &:hover:not(:disabled) { + @apply .bg-primary-600 .border-primary-700; + } + } + + &.btn-green { + @apply .bg-green-500 .border-green-600 .border .text-green-50; + + &:hover:not(:disabled) { + @apply .bg-green-600 .border-green-700; + } + } + + &.btn-red { + &:not(.btn-secondary) { + @apply .bg-red-500 .border-red-600 .text-red-50; + } + + &:hover:not(:disabled) { + @apply .bg-red-600 .border-red-700; + } + } + + &.btn-grey { + @apply .border .border-neutral-600 .bg-neutral-500 .text-neutral-50; + + &:hover:not(:disabled) { + @apply .bg-neutral-600 .border-neutral-700; + } + } + + &.btn-secondary { + @apply .border .border-neutral-600 .bg-transparent .text-neutral-200; + + &:hover:not(:disabled) { + @apply .border-neutral-500 .text-neutral-100; + } + + &.btn-red:hover:not(:disabled) { + @apply .bg-red-500 .border-red-600 .text-red-50; + } + } + + /** + * Button Sizes + */ + &.btn-jumbo { + @apply .p-4 .w-full; + } + + &.btn-lg { + @apply .p-4 .text-sm; + } + + &.btn-sm { + @apply .p-3; + } + + &.btn-xs { + @apply .p-2 .text-xs; + } + + &:disabled, &.disabled { + opacity: 0.55; + cursor: default; + } +} diff --git a/resources/styles/components/miscellaneous.css b/resources/styles/components/miscellaneous.css new file mode 100644 index 000000000..f023a834b --- /dev/null +++ b/resources/styles/components/miscellaneous.css @@ -0,0 +1,21 @@ +code.clean { + @apply .font-mono .px-2 .py-1; + background-color: #eef1f6; + color: #596981; + border-radius: 2px; + border: 1px solid rgba(0, 0, 0, .1); + display: inline-block; +} + +.grey-row-box { + @apply .flex .rounded .no-underline .text-neutral-200 .items-center .bg-neutral-700 .p-4 .border .border-transparent; + transition: border-color 150ms linear; + + &:not(.no-hover):hover { + @apply .border-neutral-500; + } + + & > div.icon { + @apply .rounded-full .bg-neutral-500 .p-3; + } +} diff --git a/resources/styles/components/modal.css b/resources/styles/components/modal.css new file mode 100644 index 000000000..7c3a6ae94 --- /dev/null +++ b/resources/styles/components/modal.css @@ -0,0 +1,50 @@ +.modal-mask { + @apply .fixed .pin .z-50 .overflow-auto .flex; + background: rgba(0, 0, 0, 0.70); + transition: opacity 250ms ease; + + & > .modal-container { + @apply .relative .w-full .max-w-md .m-auto .flex-col .flex; + + &.top { + margin-top: 10%; + } + + & > .modal-close-icon { + @apply .absolute .pin-r .p-2 .text-white .cursor-pointer .opacity-50; + transition: opacity 150ms linear, transform 150ms ease-in; + top: -2rem; + + &:hover { + @apply .opacity-100; + transform: rotate(90deg); + } + } + + & > .modal-content { + @apply .bg-neutral-800 .rounded .shadow-md; + transition: all 250ms ease; + } + + /** + * On tiny phone screens make sure there is a margin on the sides and also + * center the modal rather than putting it towards the top of the screen. + */ + @screen smx { + width: 90%; + .top { + margin-top: auto; + } + } + } + + & > .modal-container.full-screen { + @apply .w-3/4 .mt-32; + height: calc(100vh - 16rem); + max-width: none; + } + + & > .modal-container.w-auto { + @apply .w-auto; + } +} diff --git a/resources/styles/components/navigation.css b/resources/styles/components/navigation.css new file mode 100644 index 000000000..5db937ed8 --- /dev/null +++ b/resources/styles/components/navigation.css @@ -0,0 +1,64 @@ +#navigation { + @apply .w-full .bg-neutral-900 .shadow-md; + + & > div { + @apply .mx-auto .w-full .flex .items-center; + } + + & #logo { + @apply .flex-1; + + & > a { + @apply .text-2xl .font-header .px-4 .no-underline .text-neutral-200; + transition: color 150ms linear; + + &:hover { + @apply .text-neutral-100; + } + } + } + + & .right-navigation { + @apply .flex .h-full .items-center .justify-center; + + & > a { + @apply .flex .items-center .h-full .no-underline .text-neutral-300 .px-6; + transition: background-color 150ms linear, color 150ms linear, box-shadow 150ms ease-in; + + &.active, &:hover { + @apply .text-neutral-100 .bg-black; + box-shadow: inset 0 -2px config('colors.cyan-700'); + } + + &.active { + box-shadow: inset 0 -2px config('colors.cyan-500'); + } + } + } +} + +#sub-navigation { + @apply .w-full .bg-neutral-700 .shadow; + + .items { + @apply .flex .items-center .text-sm .mx-2; + + & > a, & > div { + @apply .inline-block .py-3 .px-4 .text-neutral-300 .no-underline; + transition: color 150ms linear, box-shadow 150ms ease-in; + + &:not(:first-of-type) { + @apply .ml-2; + } + + &.active, &:hover { + @apply .text-neutral-100; + box-shadow: inset 0 -2px config('colors.cyan-700'); + } + + &.active { + box-shadow: inset 0 -2px config('colors.cyan-500'); + } + } + } +} diff --git a/resources/styles/components/notifications.css b/resources/styles/components/notifications.css new file mode 100644 index 000000000..85d31ee6c --- /dev/null +++ b/resources/styles/components/notifications.css @@ -0,0 +1,46 @@ +/** + * Styling to control alert boxes. + */ +.alert { + @apply .p-2 .border .items-center .leading-normal .rounded .flex .w-full .text-sm; + + & > .title { + @apply .flex .rounded-full .uppercase .px-2 .py-1 .text-xs .font-bold .mr-3 .leading-none; + } + + & > .message { + @apply .mr-2 .text-left .flex-auto; + } + + &.error { + @apply .bg-red-600 .border-red-800 .text-red-50; + + & > .title { + @apply .bg-red-500; + } + } + + &.info { + @apply .bg-primary-600 .border-primary-800 .text-primary-50; + + & > .title { + @apply .bg-primary-500; + } + } + + &.success { + @apply .bg-green-600 .border-green-800 .text-green-50; + + & > .title { + @apply .bg-green-500; + } + } + + &.warning { + @apply .bg-yellow-600 .border-yellow-800 .text-yellow-50; + + & > .title { + @apply .bg-yellow-500; + } + } +} diff --git a/resources/styles/components/spinners.css b/resources/styles/components/spinners.css new file mode 100644 index 000000000..2141e76a6 --- /dev/null +++ b/resources/styles/components/spinners.css @@ -0,0 +1,91 @@ +.spinner { + @apply .h-4 .relative .bg-transparent; + pointer-events: none; + + &.spinner-xl { + @apply .h-16; + } + + &:after { + @apply .border-2 .border-neutral-400 .absolute .block .h-4 .w-4 .rounded-full; + animation: spinners--spin 500ms infinite linear; + border-top-color: transparent !important; + border-right-color: transparent !important; + content: ''; + left: calc(50% - (1em / 2)); + } + + &.spinner-relative:after { + @apply .relative; + } + + &.spinner-xl:after { + @apply .h-16 .w-16; + left: calc(50% - (4rem / 2)); + } + + /** + * Speeds + */ + &.spin-slow:after { + animation: spinners--spin 1200ms infinite linear; + } + + /** + * Spinner Colors + */ + &.blue:after, &.text-blue:after { + @apply .border-primary-500; + } + + &.white:after, &.text-white:after { + @apply .border-white; + } + + &.spinner-thick:after { + @apply .border-4; + } +} + +@keyframes spinners--spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +.spinner-circle { + @apply .w-8 .h-8; + border: 3px solid hsla(211, 12%, 43%, 0.2); + border-top-color: hsl(211, 12%, 43%); + border-radius: 50%; + animation: spin 1s cubic-bezier(0.55, 0.25, 0.25, 0.70) infinite; + + &.spinner-sm { + @apply .w-4 .h-4 .border-2; + } + + &.spinner-lg { + @apply .w-16 .h-16; + border-width: 6px; + } + + &.spinner-blue { + border: 3px solid hsla(212, 92%, 43%, 0.2); + border-top-color: hsl(212, 92%, 43%); + } + + &.spinner-white { + border: 3px solid rgba(255, 255, 255, 0.2); + border-top-color: rgb(255, 255, 255); + } +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/resources/styles/components/typography.css b/resources/styles/components/typography.css new file mode 100644 index 000000000..c6b3f10ff --- /dev/null +++ b/resources/styles/components/typography.css @@ -0,0 +1,17 @@ +@import url('//fonts.googleapis.com/css?family=Rubik:300,400,500&display=swap'); +@import url('https://fonts.googleapis.com/css?family=IBM+Plex+Sans:500&display=swap'); + +body { + @apply .text-neutral-200; + letter-spacing: 0.015em; +} + +h1, h2, h3, h4, h5, h6 { + @apply .font-medium; + letter-spacing: 0; + font-family: 'IBM Plex Sans', -apple-system, '"Roboto"', 'system-ui', 'sans-serif'; +} + +p { + @apply .text-neutral-200 .leading-snug; +} diff --git a/resources/styles/main.css b/resources/styles/main.css new file mode 100644 index 000000000..d77937e72 --- /dev/null +++ b/resources/styles/main.css @@ -0,0 +1,33 @@ +/** + * Tailwind Preflight Classes + */ +@import "tailwindcss/preflight"; +@import "tailwindcss/components"; + +@import "xterm/src/xterm.css"; + +/** + * Pterodactyl Specific CSS + */ +@import "components/typography.css"; +@import "components/animations.css"; +@import "components/authentication.css"; +@import "components/forms.css"; +@import "components/miscellaneous.css"; +@import "components/modal.css"; +@import "components/navigation.css"; +@import "components/notifications.css"; +@import "components/spinners.css"; +@import "components/filemanager.css"; + +/** + * Tailwind Utilities + */ +@import "tailwindcss/utilities"; + +/** + * Assorted Other CSS + */ +body { + @apply .font-sans; +} diff --git a/resources/themes/pterodactyl/admin/eggs/variables.blade.php b/resources/themes/pterodactyl/admin/eggs/variables.blade.php index f49886ee5..4a3a93417 100644 --- a/resources/themes/pterodactyl/admin/eggs/variables.blade.php +++ b/resources/themes/pterodactyl/admin/eggs/variables.blade.php @@ -81,7 +81,7 @@
    -

    These rules are defined using standard Laravel Framework validation rules.

    +

    These rules are defined using standard Laravel Framework validation rules.