diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1c53c100bc..3847db23ba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,10 +26,6 @@ jobs: git commit -m "bump version for release" git push - - name: Copy settings.json - run: | - mv settings.json.example settings.json - - name: Build assets run: | yarn install diff --git a/app/Console/Commands/Node/MakeNodeCommand.php b/app/Console/Commands/Node/MakeNodeCommand.php new file mode 100644 index 0000000000..c70aef35fb --- /dev/null +++ b/app/Console/Commands/Node/MakeNodeCommand.php @@ -0,0 +1,88 @@ +. + * + * This software is licensed under the terms of the MIT license. + * https://opensource.org/licenses/MIT + */ + +namespace Pterodactyl\Console\Commands\Node; + +use Illuminate\Console\Command; +use Pterodactyl\Services\Nodes\NodeCreationService; + +class MakeNodeCommand extends Command +{ + /** + * @var \Pterodactyl\Services\Nodes\NodeCreationService + */ + protected $creationService; + + /** + * @var string + */ + protected $signature = 'p:node:make + {--name= : A name to identify the node.} + {--description= : A description to identify the node.} + {--locationId= : A valid locationId.} + {--fqdn= : The domain name (e.g node.example.com) to be used for connecting to the daemon. An IP address may only be used if you are not using SSL for this node.} + {--public= : Should the node be public or private? (public=1 / private=0).} + {--scheme= : Which scheme should be used? (Enable SSL=https / Disable SSL=http).} + {--proxy= : Is the daemon behind a proxy? (Yes=1 / No=0).} + {--maintenance= : Should maintenance mode be enabled? (Enable Maintenance mode=1 / Disable Maintenance mode=0).} + {--maxMemory= : Set the max memory amount.} + {--overallocateMemory= : Enter the amount of ram to overallocate (% or -1 to overallocate the maximum).} + {--maxDisk= : Set the max disk amount.} + {--overallocateDisk= : Enter the amount of disk to overallocate (% or -1 to overallocate the maximum).} + {--uploadSize= : Enter the maximum upload filesize.} + {--daemonListeningPort= : Enter the wings listening port.} + {--daemonSFTPPort= : Enter the wings SFTP listening port.} + {--daemonBase= : Enter the base folder.}'; + + /** + * @var string + */ + protected $description = 'Creates a new node on the system via the CLI.'; + + + /** + * Handle the command execution process. + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function handle(NodeCreationService $creationService) + { + $this->creationService = $creationService; + + $data['name'] = $this->option('name') ?? $this->ask('Enter a short identifier used to distinguish this node from others'); + $data['description'] = $this->option('description') ?? $this->ask('Enter a description to identify the node'); + $data['location_id'] = $this->option('locationId') ?? $this->ask('Enter a valid location id'); + $data['fqdn'] = $this->option('fqdn') ?? $this->ask('Enter a domain name (e.g node.example.com) to be used for connecting to the daemon. An IP address may only be used if you are not using SSL for this node'); + if (!filter_var(gethostbyname($data['fqdn']), FILTER_VALIDATE_IP)) { + $this->error('The FQDN or IP address provided does not resolve to a valid IP address.'); + return; + } + $data['public'] = $this->option('public') ?? $this->confirm('Should this node be public? As a note, setting a node to private you will be denying the ability to auto-deploy to this node.', true); + $data['scheme'] = $this->option('scheme') ?? $this->anticipate('Please either enter https for SSL or http for a non-ssl connection', + ["https","http",],"https"); + if (filter_var($data['fqdn'], FILTER_VALIDATE_IP) && $data['scheme'] === 'https') { + $this->error('A fully qualified domain name that resolves to a public IP address is required in order to use SSL for this node.'); + return; + } + $data['behind_proxy'] = $this->option('proxy') ?? $this->confirm('Is your FQDN behind a proxy?'); + $data['maintenance_mode'] = $this->option('maintenance') ?? $this->confirm('Should maintenance mode be enabled?'); + $data['memory'] = $this->option('maxMemory') ?? $this->ask('Enter the maximum amount of memory'); + $data['memory_overallocate'] = $this->option('overallocateMemory') ?? $this->ask('Enter the amount of memory to over allocate by, -1 will disable checking and 0 will prevent creating new servers'); + $data['disk'] = $this->option('maxDisk') ?? $this->ask('Enter the maximum amount of disk space'); + $data['disk_overallocate'] = $this->option('overallocateDisk') ?? $this->ask('Enter the amount of memory to over allocate by, -1 will disable checking and 0 will prevent creating new server'); + $data['upload_size'] = $this->option('uploadSize') ?? $this->ask('Enter the maximum filesize upload', '100'); + $data['daemonListen'] = $this->option('daemonListeningPort') ?? $this->ask('Enter the wings listening port', '8080'); + $data['daemonSFTP'] = $this->option('daemonSFTPPort') ?? $this->ask('Enter the wings SFTP listening port', '2022'); + $data['daemonBase'] = $this->option('daemonBase') ?? $this->ask('Enter the base folder', '/var/lib/pterodactyl/volumes'); + + $node = $this->creationService->handle($data); + $this->line('Successfully created a new node on the location ' . $data['location_id'] . ' with the name ' . $data['name'] . ' and has an id of ' . $node->id . '.'); + } +} diff --git a/app/Contracts/Repository/BillingRepositoryInterface.php b/app/Contracts/Repository/BillingRepositoryInterface.php new file mode 100644 index 0000000000..7e0a1c85c9 --- /dev/null +++ b/app/Contracts/Repository/BillingRepositoryInterface.php @@ -0,0 +1,30 @@ +billing = $billing; + $this->alert = $alert; + } + + /** + * @return View + */ + public function index(): View + { + return view('admin.billing', [ + 'enabled' => $this->billing->get('config:enabled', false) + ]); + } + + /** + * @throws DataValidationException + * @throws RecordNotFoundException + */ + public function update(BillingFormRequest $request): RedirectResponse + { + foreach ($request->normalize() as $key => $value) { + $this->billing->set('config:'.$key, $value); + } + + $this->alert->success('Billing System has been updated.')->flash(); + + return redirect()->route('admin.billing'); + } +} + diff --git a/app/Http/Controllers/Admin/Settings/SecretController.php b/app/Http/Controllers/Admin/Settings/SecretController.php deleted file mode 100644 index 0fb4a9b7b9..0000000000 --- a/app/Http/Controllers/Admin/Settings/SecretController.php +++ /dev/null @@ -1,81 +0,0 @@ -alert = $alert; - $this->kernel = $kernel; - $this->settings = $settings; - $this->versionService = $versionService; - } - - /** - * Render the UI for basic Panel settings. - */ - public function index(): View - { - return view('admin.settings.secret', [ - 'rainbow_bar' => $this->settings->get('settings::app:particles', true), - ]); - } - - /** - * Handle settings update. - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function update(SecretSettingsFormRequest $request): RedirectResponse - { - foreach ($request->normalize() as $key => $value) { - $this->settings->set('settings::' . $key, $value); - } - - $this->kernel->call('queue:restart'); - $this->alert->success('Panel settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash(); - - return redirect()->route('admin.settings'); - } -} diff --git a/app/Http/Controllers/Api/Client/Credits/StoreController.php b/app/Http/Controllers/Api/Client/Credits/StoreController.php index 8fb4829e27..bfb677513b 100644 --- a/app/Http/Controllers/Api/Client/Credits/StoreController.php +++ b/app/Http/Controllers/Api/Client/Credits/StoreController.php @@ -2,22 +2,23 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Credits; +use Throwable; +use Pterodactyl\Models\Node; use Illuminate\Support\Facades\DB; -use Illuminate\Validation\ValidationException; use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\Repository\RecordNotFoundException; -use Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException; -use Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException; -use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; +use Illuminate\Validation\ValidationException; use Pterodactyl\Http\Requests\Api\Client\StoreRequest; -use Pterodactyl\Models\Node; use Pterodactyl\Services\Servers\ServerCreationService; -use Throwable; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; +use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; +use Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException; +use Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException; class StoreController extends ClientApiController { private Node $node; private ServerCreationService $creationService; + /** * StoreController constructor. */ @@ -40,7 +41,6 @@ public function getConfig(StoreRequest $request): array 'user' => $user, ], ]; - } /** @@ -60,9 +60,6 @@ public function newServer(StoreRequest $request): array 'storage' => 'required', ]); - $allocation = $this->getAllocationId(1); - if ($allocation == -1) throw new DisplayException('No allocations could be found on the requested node.'); - $egg = DB::table('eggs')->where('id', '=', 1)->first(); $nest = DB::table('nests')->where('id', '=', 1)->first(); @@ -71,7 +68,7 @@ public function newServer(StoreRequest $request): array 'owner_id' => $request->user()->id, 'egg_id' => $egg->id, 'nest_id' => $nest->id, - 'allocation_id' => $allocation, + 'allocation_id' => $this->getAllocationId(1), 'environment' => [], 'memory' => $request->input('ram') * 1024, 'disk' => $request->input('storage') * 1024, @@ -83,40 +80,24 @@ public function newServer(StoreRequest $request): array 'start_on_completion' => true, ]; - if ($request->user()->cr_slots < 1) { - throw new DisplayException('You don\'t have a server slot available to make this server.'); - return [ - 'success' => false, - 'data' => [] - ]; - } - if ($request->user()->cr_cpu < $request->input('cpu')) { - throw new DisplayException('You don\'t have enough CPU available in your account.'); - return [ - 'success' => false, - 'data' => [] - ]; - } - if ($request->user()->cr_ram < $request->input('ram')) { - throw new DisplayException('You don\'t have enough RAM available in your account.'); - return [ - 'success' => false, - 'data' => [] - ]; + foreach (DB::table('egg_variables')->where('egg_id', '=', $egg->id)->get() as $var) { + $key = "v{$nest->id}-{$egg->id}-{$var->env_variable}"; + $data['environment'][$var->env_variable] = $request->get($key, $var->default_value); } - if ($request->user()->cr_storage < $request->input('storage')) { - throw new DisplayException('You don\'t have that much storage available om your account.'); + + if ( + $request->user()->cr_slots < 1 | + $request->user()->cr_cpu < $request->input('cpu') | + $request->user()->cr_ram < $request->input('ram') | + $request->user()->cr_storage < $request->input('storage') + ) { + throw new DisplayException('You don\'t have the resources available to make this server.'); return [ 'success' => false, 'data' => [] ]; } - foreach (DB::table('egg_variables')->where('egg_id', '=', $egg->id)->get() as $var) { - $key = "v{$nest->id}-{$egg->id}-{$var->env_variable}"; - $data['environment'][$var->env_variable] = $request->get($key, $var->default_value); - } - $server = $this->creationService->handle($data); $server->save(); diff --git a/app/Http/Controllers/Api/Client/Servers/AuditLogsController.php b/app/Http/Controllers/Api/Client/Servers/AuditLogsController.php new file mode 100644 index 0000000000..648feeb61e --- /dev/null +++ b/app/Http/Controllers/Api/Client/Servers/AuditLogsController.php @@ -0,0 +1,40 @@ +query('per_page') ?? 20, 50); + + return $this->fractal->collection($server->audits()->orderByDesc('created_at')->paginate($limit)) + ->transformWith($this->getTransformer(AuditLogTransformer::class)) + ->addMeta([ + 'log_count' => $server->audits()->count(), + ]) + ->toArray(); + } +} diff --git a/app/Http/Requests/Admin/BillingFormRequest.php b/app/Http/Requests/Admin/BillingFormRequest.php new file mode 100644 index 0000000000..da21b5ccc8 --- /dev/null +++ b/app/Http/Requests/Admin/BillingFormRequest.php @@ -0,0 +1,18 @@ + 'int', + ]; + } +} diff --git a/app/Http/Requests/Admin/Settings/SecretSettingsFormRequest.php b/app/Http/Requests/Admin/Settings/SecretSettingsFormRequest.php deleted file mode 100644 index bb8ef61e01..0000000000 --- a/app/Http/Requests/Admin/Settings/SecretSettingsFormRequest.php +++ /dev/null @@ -1,31 +0,0 @@ - 'nullable|integer|in:0,1', - ]; - } - /** - * @return array - */ - public function attributes() - { - return [ - 'app:rainbow_bar' => 'Rainbow Progress Bar', - ]; - } -} diff --git a/app/Http/Requests/Api/Client/Servers/AuditLogs/GetAuditLogsRequest.php b/app/Http/Requests/Api/Client/Servers/AuditLogs/GetAuditLogsRequest.php new file mode 100644 index 0000000000..e47fc323ca --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/AuditLogs/GetAuditLogsRequest.php @@ -0,0 +1,15 @@ + 'required|string|between:1,191', + 'value' => 'string', + ]; +} diff --git a/app/Models/Permission.php b/app/Models/Permission.php index 5a0fc95f82..7ae6d634ba 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -56,6 +56,8 @@ class Permission extends Model public const ACTION_FILE_ARCHIVE = 'file.archive'; public const ACTION_FILE_SFTP = 'file.sftp'; + public const ACTION_AUDITLOGS_READ = 'audit-logs.read'; + public const ACTION_STARTUP_READ = 'startup.read'; public const ACTION_STARTUP_UPDATE = 'startup.update'; public const ACTION_STARTUP_DOCKER_IMAGE = 'startup.docker-image'; @@ -203,6 +205,13 @@ class Permission extends Model ], ], + 'audit-logs' => [ + 'description' => 'Permissions that control a user\'s access to the audit logs for this server.', + 'keys' => [ + 'read' => 'Allows a user to read audit logs.', + ], + ], + 'settings' => [ 'description' => 'Permissions that control a user\'s access to the settings for this server.', 'keys' => [ diff --git a/app/Models/User.php b/app/Models/User.php index c29a8470f8..0a58654573 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -182,7 +182,10 @@ public static function getRules() */ public function toReactObject(): array { - return (new Collection($this->toArray()))->except(['id', 'external_id'])->toArray(); + $object = (new Collection($this->toArray()))->except(['id', 'external_id'])->toArray(); + $object['avatar_url'] = $this->avatarURL(); + + return $object; } /** @@ -203,6 +206,16 @@ public function setUsernameAttribute(string $value) $this->attributes['username'] = mb_strtolower($value); } + /** + * Get's the avatar url for the user. + * + * @return string + */ + public function avatarURL(): string + { + return 'https://www.gravatar.com/avatar/' . md5($this->email) . '.jpg'; + } + /** * Return a concatenated result for the accounts full name. * diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index db6cbf0794..6e6ff9bfdc 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -13,6 +13,7 @@ use Pterodactyl\Repositories\Eloquent\CreditsRepository; use Pterodactyl\Repositories\Eloquent\SessionRepository; use Pterodactyl\Repositories\Eloquent\SubuserRepository; +use Pterodactyl\Repositories\Eloquent\BillingRepository; use Pterodactyl\Repositories\Eloquent\DatabaseRepository; use Pterodactyl\Repositories\Eloquent\LocationRepository; use Pterodactyl\Repositories\Eloquent\ScheduleRepository; @@ -29,6 +30,7 @@ use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Repositories\Eloquent\ServerVariableRepository; use Pterodactyl\Contracts\Repository\SessionRepositoryInterface; +use Pterodactyl\Contracts\Repository\BillingRepositoryInterface; use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; use Pterodactyl\Contracts\Repository\CreditsRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; @@ -66,5 +68,6 @@ public function register() $this->app->bind(TaskRepositoryInterface::class, TaskRepository::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class); $this->app->bind(CreditsRepositoryInterface::class, CreditsRepository::class); + $this->app->bind(BillingRepositoryInterface::class, BillingRepository::class); } } diff --git a/app/Repositories/Eloquent/BillingRepository.php b/app/Repositories/Eloquent/BillingRepository.php new file mode 100644 index 0000000000..405b818dea --- /dev/null +++ b/app/Repositories/Eloquent/BillingRepository.php @@ -0,0 +1,90 @@ +clearCache($key); + $this->withoutFreshModel()->updateOrCreate(['key' => $key], ['value' => $value ?? '']); + + self::$cache[$key] = $value; + } + + /** + * Retrieve a persistent setting from the database. + * + * @param mixed $default + * + * @return mixed + */ + public function get(string $key, $default = null) + { + // If item has already been requested return it from the cache. If + // we already know it is missing, immediately return the default value. + if (array_key_exists($key, self::$cache)) { + return self::$cache[$key]; + } elseif (array_key_exists($key, self::$databaseMiss)) { + return value($default); + } + + $instance = $this->getBuilder()->where('key', $key)->first(); + if (is_null($instance)) { + self::$databaseMiss[$key] = true; + + return value($default); + } + + return self::$cache[$key] = $instance->value; + } + + /** + * Remove a key from the database cache. + */ + public function forget(string $key) + { + $this->clearCache($key); + $this->deleteWhere(['key' => $key]); + } + + /** + * Remove a key from the cache. + */ + private function clearCache(string $key) + { + unset(self::$cache[$key], self::$databaseMiss[$key]); + } +} diff --git a/app/Transformers/Api/Client/AuditLogTransformer.php b/app/Transformers/Api/Client/AuditLogTransformer.php new file mode 100644 index 0000000000..7e60e16a94 --- /dev/null +++ b/app/Transformers/Api/Client/AuditLogTransformer.php @@ -0,0 +1,38 @@ + $model->uuid, + 'user' => $model->user ? $model->user->email : "System", + 'action' => $model->action, + 'device' => $model->device, + 'metadata' => $model->metadata, + 'is_system' => $model->is_system, + 'created_at' => $model->created_at->toIso8601String(), + ]; + } +} diff --git a/app/Transformers/Api/Client/UserTransformer.php b/app/Transformers/Api/Client/UserTransformer.php index 468232f8d6..e4c34c10c1 100644 --- a/app/Transformers/Api/Client/UserTransformer.php +++ b/app/Transformers/Api/Client/UserTransformer.php @@ -27,7 +27,7 @@ public function transform(User $model) 'uuid' => $model->uuid, 'username' => $model->username, 'email' => $model->email, - 'image' => 'https://gravatar.com/avatar/' . md5(Str::lower($model->email)), + 'avatar_url' => $model->avatarURL(), '2fa_enabled' => $model->use_totp, 'created_at' => $model->created_at->toIso8601String(), ]; diff --git a/composer.json b/composer.json index 8be3977bc5..adf8f5d1ad 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "guzzlehttp/guzzle": "^7.3", "hashids/hashids": "^4.1", "laracasts/utilities": "^3.2", + "laravel/cashier": "^13.7", "laravel/framework": "^8.68", "laravel/helpers": "^1.4", "laravel/sanctum": "^2.11", @@ -33,6 +34,7 @@ "spatie/laravel-fractal": "^5.8", "spatie/laravel-query-builder": "^3.5", "staudenmeir/belongs-to-through": "^2.11", + "stripe/stripe-php": "^7.110", "symfony/yaml": "^5.3", "webmozart/assert": "^1.10" }, @@ -82,6 +84,9 @@ "config": { "preferred-install": "dist", "sort-packages": true, - "optimize-autoloader": false + "optimize-autoloader": false, + "allow-plugins": { + "composer/package-versions-deprecated": true + } } } diff --git a/database/Seeders/eggs/jexactyl/multi-egg.json b/database/Seeders/eggs/jexactyl/multi-egg.json index c1658557be..7084b82073 100644 --- a/database/Seeders/eggs/jexactyl/multi-egg.json +++ b/database/Seeders/eggs/jexactyl/multi-egg.json @@ -8,7 +8,9 @@ "name": "Multi-egg", "author": "doge@chiragdev.xyz", "description": "A multi egg by chirag350.", - "features": null, + "features": [ + "pid_limit" + ], "images": [ "quay.io\/chirag350\/multi-egg" ], diff --git a/database/migrations/2022_01_14_165832_billing.php b/database/migrations/2022_01_14_165832_billing.php new file mode 100644 index 0000000000..20bed962db --- /dev/null +++ b/database/migrations/2022_01_14_165832_billing.php @@ -0,0 +1,32 @@ +id(); + $table->string('key')->unique(); + $table->text('value'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('billing'); + } +} diff --git a/package.json b/package.json index 00850ab104..cf63f0558e 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "styled-components": "^5.2.1", "styled-components-breakpoint": "^3.0.0-preview.20", "swr": "^0.2.3", - "tailwindcss": "^2.0.2", + "tailwindcss": "^2.2.7", "uuid": "^3.3.2", "xterm": "^4.15.0", "xterm-addon-attach": "^0.6.0", @@ -116,6 +116,17 @@ "build:production": "yarn run clean && cross-env NODE_ENV=production ./node_modules/.bin/webpack --mode production", "serve": "yarn run clean && cross-env PUBLIC_PATH=https://pterodactyl.test:8080 NODE_ENV=development webpack-dev-server --host 0.0.0.0 --hot --https --key /etc/ssl/private/pterodactyl.test-key.pem --cert /etc/ssl/private/pterodactyl.test.pem" }, + "babelMacros": { + "twin": { + "preset": "styled-components", + "autoCssProp": true + }, + "styledComponents": { + "pure": true, + "displayName": true, + "fileName": true + } + }, "browserslist": [ "> 0.5%", "last 2 versions", diff --git a/public/themes/pterodactyl/css/checkbox.css b/public/themes/pterodactyl/css/checkbox.css index a75e63af64..8de52b7876 100644 --- a/public/themes/pterodactyl/css/checkbox.css +++ b/public/themes/pterodactyl/css/checkbox.css @@ -1,232 +1 @@ -/** - * Bootsnip - "Bootstrap Checkboxes/Radios" - * Bootstrap 3.2.0 Snippet by i-heart-php - * - * Copyright (c) 2013 Bootsnipp.com - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - .checkbox { - padding-left: 20px; -} -.checkbox label { - display: inline-block; - position: relative; - padding-left: 5px; -} -.checkbox label::before { - content: ""; - display: inline-block; - position: absolute; - width: 17px; - height: 17px; - left: 0; - top: 2.5px; - margin-left: -20px; - border: 1px solid #cccccc; - border-radius: 3px; - background-color: #fff; - -webkit-transition: border 0.15s ease-in-out, color 0.15s ease-in-out; - -o-transition: border 0.15s ease-in-out, color 0.15s ease-in-out; - transition: border 0.15s ease-in-out, color 0.15s ease-in-out; -} -.checkbox label::after { - display: inline-block; - position: absolute; - width: 16px; - height: 16px; - left: 0; - top: 2.5px; - margin-left: -20px; - padding-left: 3px; - padding-top: 1px; - font-size: 11px; - color: #555555; -} -.checkbox input[type="checkbox"] { - opacity: 0; -} -.checkbox input[type="checkbox"]:focus + label::before { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} -.checkbox input[type="checkbox"]:checked + label::after { - font-family: 'FontAwesome'; - content: "\f00c"; -} -.checkbox input[type="checkbox"]:disabled + label { - opacity: 0.65; -} -.checkbox input[type="checkbox"]:disabled + label::before { - background-color: #eeeeee; - cursor: not-allowed; -} -.checkbox.checkbox-circle label::before { - border-radius: 50%; -} -.checkbox.checkbox-inline { - margin-top: 0; -} -.checkbox-primary input[type="checkbox"]:checked + label::before { - background-color: #428bca; - border-color: #428bca; -} -.checkbox-primary input[type="checkbox"]:checked + label::after { - color: #fff; -} -.checkbox-danger input[type="checkbox"]:checked + label::before { - background-color: #d9534f; - border-color: #d9534f; -} -.checkbox-danger input[type="checkbox"]:checked + label::after { - color: #fff; -} -.checkbox-info input[type="checkbox"]:checked + label::before { - background-color: #5bc0de; - border-color: #5bc0de; -} -.checkbox-info input[type="checkbox"]:checked + label::after { - color: #fff; -} -.checkbox-warning input[type="checkbox"]:checked + label::before { - background-color: #f0ad4e; - border-color: #f0ad4e; -} -.checkbox-warning input[type="checkbox"]:checked + label::after { - color: #fff; -} -.checkbox-success input[type="checkbox"]:checked + label::before { - background-color: #5cb85c; - border-color: #5cb85c; -} -.checkbox-success input[type="checkbox"]:checked + label::after { - color: #fff; -} -.radio { - padding-left: 20px; -} -.radio label { - display: inline-block; - position: relative; - padding-left: 5px; -} -.radio label::before { - content: ""; - display: inline-block; - position: absolute; - width: 17px; - height: 17px; - left: 0; - margin-left: -20px; - border: 1px solid #cccccc; - border-radius: 50%; - background-color: #fff; - -webkit-transition: border 0.15s ease-in-out; - -o-transition: border 0.15s ease-in-out; - transition: border 0.15s ease-in-out; -} -.radio label::after { - display: inline-block; - position: absolute; - content: " "; - width: 11px; - height: 11px; - left: 3px; - top: 3px; - margin-left: -20px; - border-radius: 50%; - background-color: #555555; - -webkit-transform: scale(0, 0); - -ms-transform: scale(0, 0); - -o-transform: scale(0, 0); - transform: scale(0, 0); - -webkit-transition: -webkit-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); - -moz-transition: -moz-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); - -o-transition: -o-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); - transition: transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); -} -.radio input[type="radio"] { - opacity: 0; -} -.radio input[type="radio"]:focus + label::before { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} -.radio input[type="radio"]:checked + label::after { - -webkit-transform: scale(1, 1); - -ms-transform: scale(1, 1); - -o-transform: scale(1, 1); - transform: scale(1, 1); -} -.radio input[type="radio"]:disabled + label { - opacity: 0.65; -} -.radio input[type="radio"]:disabled + label::before { - cursor: not-allowed; -} -.radio.radio-inline { - margin-top: 0; -} -.radio-primary input[type="radio"] + label::after { - background-color: #428bca; -} -.radio-primary input[type="radio"]:checked + label::before { - border-color: #428bca; -} -.radio-primary input[type="radio"]:checked + label::after { - background-color: #428bca; -} -.radio-danger input[type="radio"] + label::after { - background-color: #d9534f; -} -.radio-danger input[type="radio"]:checked + label::before { - border-color: #d9534f; -} -.radio-danger input[type="radio"]:checked + label::after { - background-color: #d9534f; -} -.radio-info input[type="radio"] + label::after { - background-color: #5bc0de; -} -.radio-info input[type="radio"]:checked + label::before { - border-color: #5bc0de; -} -.radio-info input[type="radio"]:checked + label::after { - background-color: #5bc0de; -} -.radio-warning input[type="radio"] + label::after { - background-color: #f0ad4e; -} -.radio-warning input[type="radio"]:checked + label::before { - border-color: #f0ad4e; -} -.radio-warning input[type="radio"]:checked + label::after { - background-color: #f0ad4e; -} -.radio-success input[type="radio"] + label::after { - background-color: #5cb85c; -} -.radio-success input[type="radio"]:checked + label::before { - border-color: #5cb85c; -} -.radio-success input[type="radio"]:checked + label::after { - background-color: #5cb85c; -} +.checkbox{padding-left:20px}.checkbox label{display:inline-block;position:relative;padding-left:5px}.checkbox label::before{content:"";display:inline-block;position:absolute;width:17px;height:17px;left:0;top:2.5px;margin-left:-20px;border:1px solid #ccc;border-radius:3px;background-color:#fff;-webkit-transition:border .15s ease-in-out,color .15s ease-in-out;-o-transition:border .15s ease-in-out,color .15s ease-in-out;transition:border .15s ease-in-out,color .15s ease-in-out}.checkbox label::after{display:inline-block;position:absolute;width:16px;height:16px;left:0;top:2.5px;margin-left:-20px;padding-left:3px;padding-top:1px;font-size:11px;color:#555}.checkbox input[type=checkbox]{opacity:0}.checkbox input[type=checkbox]:focus+label::before{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.checkbox input[type=checkbox]:checked+label::after{font-family:FontAwesome;content:"\f00c"}.checkbox input[type=checkbox]:disabled+label{opacity:.65}.checkbox input[type=checkbox]:disabled+label::before{background-color:#eee;cursor:not-allowed}.checkbox.checkbox-circle label::before{border-radius:50%}.checkbox.checkbox-inline{margin-top:0}.checkbox-primary input[type=checkbox]:checked+label::before{background-color:#428bca;border-color:#428bca}.checkbox-primary input[type=checkbox]:checked+label::after{color:#fff}.checkbox-danger input[type=checkbox]:checked+label::before{background-color:#d9534f;border-color:#d9534f}.checkbox-danger input[type=checkbox]:checked+label::after{color:#fff}.checkbox-info input[type=checkbox]:checked+label::before{background-color:#5bc0de;border-color:#5bc0de}.checkbox-info input[type=checkbox]:checked+label::after{color:#fff}.checkbox-warning input[type=checkbox]:checked+label::before{background-color:#f0ad4e;border-color:#f0ad4e}.checkbox-warning input[type=checkbox]:checked+label::after{color:#fff}.checkbox-success input[type=checkbox]:checked+label::before{background-color:#5cb85c;border-color:#5cb85c}.checkbox-success input[type=checkbox]:checked+label::after{color:#fff}.radio{padding-left:20px}.radio label{display:inline-block;position:relative;padding-left:5px}.radio label::before{content:"";display:inline-block;position:absolute;width:17px;height:17px;left:0;margin-left:-20px;border:1px solid #ccc;border-radius:50%;background-color:#fff;-webkit-transition:border .15s ease-in-out;-o-transition:border .15s ease-in-out;transition:border .15s ease-in-out}.radio label::after{display:inline-block;position:absolute;content:" ";width:11px;height:11px;left:3px;top:3px;margin-left:-20px;border-radius:50%;background-color:#555;-webkit-transform:scale(0,0);-ms-transform:scale(0,0);-o-transform:scale(0,0);transform:scale(0,0);-webkit-transition:-webkit-transform .1s cubic-bezier(.8, -.33, .2, 1.33);-moz-transition:-moz-transform .1s cubic-bezier(.8, -.33, .2, 1.33);-o-transition:-o-transform .1s cubic-bezier(.8, -.33, .2, 1.33);transition:transform .1s cubic-bezier(.8, -.33, .2, 1.33)}.radio input[type=radio]{opacity:0}.radio input[type=radio]:focus+label::before{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.radio input[type=radio]:checked+label::after{-webkit-transform:scale(1,1);-ms-transform:scale(1,1);-o-transform:scale(1,1);transform:scale(1,1)}.radio input[type=radio]:disabled+label{opacity:.65}.radio input[type=radio]:disabled+label::before{cursor:not-allowed}.radio.radio-inline{margin-top:0}.radio-primary input[type=radio]+label::after{background-color:#428bca}.radio-primary input[type=radio]:checked+label::before{border-color:#428bca}.radio-primary input[type=radio]:checked+label::after{background-color:#428bca}.radio-danger input[type=radio]+label::after{background-color:#d9534f}.radio-danger input[type=radio]:checked+label::before{border-color:#d9534f}.radio-danger input[type=radio]:checked+label::after{background-color:#d9534f}.radio-info input[type=radio]+label::after{background-color:#5bc0de}.radio-info input[type=radio]:checked+label::before{border-color:#5bc0de}.radio-info input[type=radio]:checked+label::after{background-color:#5bc0de}.radio-warning input[type=radio]+label::after{background-color:#f0ad4e}.radio-warning input[type=radio]:checked+label::before{border-color:#f0ad4e}.radio-warning input[type=radio]:checked+label::after{background-color:#f0ad4e}.radio-success input[type=radio]+label::after{background-color:#5cb85c}.radio-success input[type=radio]:checked+label::before{border-color:#5cb85c}.radio-success input[type=radio]:checked+label::after{background-color:#5cb85c} \ No newline at end of file diff --git a/public/themes/pterodactyl/css/pterodactyl.css b/public/themes/pterodactyl/css/pterodactyl.css index 8e3580f2be..58dc5da5b0 100644 --- a/public/themes/pterodactyl/css/pterodactyl.css +++ b/public/themes/pterodactyl/css/pterodactyl.css @@ -1,835 +1 @@ -/** - * Pterodactyl - Panel - * Copyright (c) 2015 - 2017 Dane Everitt - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - @import 'checkbox.css'; - -.login-page { - background: #10529f; -} - -#login-position-elements { - margin: 25% auto; -} - -.login-logo { - color: #fff; - font-weight: 400; -} - -.login-copyright { - color: rgba(255, 255, 255, 0.3); -} - -.login-copyright > a { - color: rgba(255, 255, 255, 0.6); -} - -.particles-js-canvas-el { - position: absolute; - width: 100%; - height: 100%; - top: 0; - z-index: -1; -} - -.pterodactyl-login-box { - background: rgba(0, 0, 0, 0.25); - border-radius: 3px; - padding: 20px; -} - -.pterodactyl-login-input > input { - background: rgba(0, 0, 0, 0.4); - border: 1px solid #000; - border-radius: 2px; - color: #fff; -} - -.pterodactyl-login-input > .form-control-feedback { - color: #fff; -} - -.pterodactyl-login-button--main { - background: rgba(0, 0, 0, 0.4); - border: 1px solid #000; - border-radius: 2px; - color: #fff; -} - -.pterodactyl-login-button--main:hover { - background: rgba(0, 0, 0, 0.7); - border: 1px solid #000; - border-radius: 2px; - color: #fff; -} - -.pterodactyl-login-button--left { - background: rgba(255, 255, 255, 0.4); - border: 1px solid rgba(255, 255, 255, 0.6); - border-radius: 2px; - color: #fff; -} - -.pterodactyl-login-button--left:hover { - background: rgba(255, 255, 255, 0.6); - border: 1px solid rgba(255, 255, 255, 0.8); - border-radius: 2px; - color: #fff; -} - -.weight-100 { - font-weight: 100; -} - -.weight-300 { - font-weight: 300; -} - -.btn-clear { - background: transparent; -} - -.user-panel > .info { - position: relative; - left: 0; -} - -code { - background-color: #eef1f6; - color: #596981; - border-radius: 2px; - padding-left: 4px; - padding-right: 4px; - line-height: 1.4; - font-size: 85%; - border: 1px solid rgba(0, 0, 0, .1); - display: inline-block; -} - -p { - line-height: 1.6 !important; -} - -p.small { - margin-top: 3px !important; -} - -.control-sidebar-dark .control-sidebar-menu > li > a.active { - background: #1e282c; -} - -.callout-nomargin { - margin: 0; -} - -.table { - font-size: 14px !important; -} - -.table .min-size { - width:1px; - white-space: nowrap; -} - -@media (max-width:767px) { - .box-header > .box-tools { - position: relative !important; - padding: 0px 10px 10px; - } -} - -.middle, .align-middle { - vertical-align: middle !important; -} - -#fileOptionMenu.dropdown-menu > li > a { - padding:3px 6px; -} - -.hasFileHover { - border: 2px dashed #0087F7; - border-top: 0 !important; - border-radius: 5px; - margin: 0; - opacity: 0.5; -} - -.hasFileHover * { - pointer-events: none !important; -} - -td.has-progress { - padding: 0px !important; - border-top: 0px !important; -} - -.progress.progress-table-bottom { - margin: 0 !important; - height:5px !important; - padding:0; - border:0; -} - -.muted { - filter: alpha(opacity=20); - opacity: 0.2; -} - -.muted-hover:hover { - filter: alpha(opacity=100); - opacity: 1; -} - -.use-pointer { - cursor: pointer !important; -} - -.input-loader { - display: none; - position:relative; - top: -25px; - float: right; - right: 5px; - color: #cccccc; - height: 0; -} - -.box-header > .form-group { - margin-bottom: 0; -} - -.box-header > .form-group > div > p.small { - margin: 0; -} - -.no-margin { - margin: 0 !important; -} - -li.select2-results__option--highlighted[aria-selected="false"] > .user-block > .username > a { - color: #fff; -} - -li.select2-results__option--highlighted[aria-selected="false"] > .user-block > .description { - color: #eee; -} - -.select2-selection.select2-selection--multiple { - min-height: 36px !important; -} - -.select2-search--inline .select2-search__field:focus { - outline: none; - border: 0 !important; -} - -.img-bordered-xs { - border: 1px solid #d2d6de; - padding: 1px; -} - -span[aria-labelledby="select2-pUserId-container"] { - padding-left: 2px !important; -} - -.box { - box-shadow: 0 0 0 1px rgba(89, 105, 128, .1), 0 1px 3px 0 rgba(89, 105, 128, .1), 0 1px 2px 0 rgba(0, 0, 0, .05) !important; -} - -.alert-danger { - color: #ffffff !important; - background: #d64242 !important; - border: 1px solid #841d1d; -} - -.alert-info { - color: #ffffff !important; - background: #408fec !important; - border: 1px solid #1055a5; -} - -.alert-success { - color: #ffffff !important; - background: #51b060 !important; - border: 1px solid #2b5f33; -} - -.alert-warning { - color: #ffffff !important; - background: #fa9636 !important; - border: 1px solid #b45b05; -} - -.callout-slim a { - color: #555 !important; -} - -.bg-purple { - background-color: #79589f !important; -} - -.label-default { - background-color: #eef1f6 !important; -} - -.callout.callout-info.callout-slim { - border: 1px solid #1055a5 !important; - border-left: 5px solid #1055a5 !important; - border-right: 5px solid #1055a5 !important; - color: #777 !important; - background: transparent !important; -} - -.callout.callout-danger.callout-slim { - border: 1px solid #c23321 !important; - border-left: 5px solid #c23321 !important; - border-right: 5px solid #c23321 !important; - color: #777 !important; - background: transparent !important; -} - -.callout.callout-warning.callout-slim { - border: 1px solid #c87f0a !important; - border-left: 5px solid #c87f0a !important; - border-right: 5px solid #c87f0a !important; - color: #777 !important; - background: transparent !important; -} - -.callout.callout-success.callout-slim { - border: 1px solid #00733e !important; - border-left: 5px solid #00733e !important; - border-right: 5px solid #00733e !important; - color: #777 !important; - background: transparent !important; -} - -.callout.callout-default.callout-slim { - border: 1px solid #eee !important; - border-left: 5px solid #eee !important; - border-right: 5px solid #eee !important; - color: #777 !important; - background: transparent !important; -} - -.callout code { - background-color: #515f6cbb; - color: #c3c3c3; - border: 1px solid rgba(0, 0, 0, .25); -} - -.tab-pane .box-footer { - margin: 0 -10px -10px; -} - -.select2-container{ width: 100% !important; } - -.nav-tabs-custom > .nav-tabs > li:hover { - border-top-color:#3c8dbc; -} - -.nav-tabs-custom > .nav-tabs > li.active.tab-danger, .nav-tabs-custom > .nav-tabs > li.tab-danger:hover { - border-top-color: #c23321; -} - -.nav-tabs-custom > .nav-tabs > li.active.tab-success, .nav-tabs-custom > .nav-tabs > li.tab-success:hover { - border-top-color: #00733e; -} - -.nav-tabs-custom > .nav-tabs > li.active.tab-info, .nav-tabs-custom > .nav-tabs > li.tab-info:hover { - border-top-color: #0097bc; -} - -.nav-tabs-custom > .nav-tabs > li.active.tab-warning, .nav-tabs-custom > .nav-tabs > li.tab-warning:hover { - border-top-color: #c87f0a; -} - -.nav-tabs-custom.nav-tabs-floating > .nav-tabs { - border-bottom: 0px !important; -} - -.nav-tabs-custom.nav-tabs-floating > .nav-tabs > li { - margin-bottom: 0px !important; -} - -.nav-tabs-custom.nav-tabs-floating > .nav-tabs > li:first-child.active, -.nav-tabs-custom.nav-tabs-floating > .nav-tabs > li:first-child:hover { - border-radius: 3px 0 0 0; -} - -.nav-tabs-custom.nav-tabs-floating > .nav-tabs > li:first-child.active > a { - border-radius: 0 0 0 3px; -} - -.position-relative { - position: relative; -} - -.no-pad-bottom { - padding-bottom: 0 !important; -} - -.no-margin-bottom { - margin-bottom: 0 !important; -} - -.btn-icon > i.fa { - line-height: 1.5; -} - -.btn.active, .btn.active.focus { - background-color: #408fec; -} - -.strong { - font-weight: bold !important; -} - -.server-description > td { - padding-top: 0 !important; - border-top: 0 !important; -} - -tr:hover + tr.server-description { - background-color: #f5f5f5 !important; -} - -.login-corner-info { - position: absolute; - bottom: 5px; - right: 10px; - color: white; -} - -input.form-autocomplete-stop[readonly] { - background: inherit; - cursor: text; -} - -/* fix Google Recaptcha badge */ -.grecaptcha-badge { - bottom: 54px !important; - background: white; - box-shadow: none !important; -} - -.dropdown-massactions { - min-width: 80px; -} - -.select-all-files { - position: relative; - bottom: 1px; - margin-right: 7px !important; -} - -.select-file { - position: relative; - bottom: 1px; - margin-right: 2px !important; -} - -.select-folder { - position: relative; - bottom: 1px; - margin-right: 5px !important; -} - -label.control-label > span { - font-size: 80%; - font-weight: 400; - font-style: italic; - color: #dd4b39; -} - -label.control-label > span.field-required:before { - content: "required"; - color: #dd4b39; -} - -label.control-label > span.field-optional:before { - content: "optional"; - color: #bbbbbb; -} - -.pagination > li > a, .pagination > li > span { - padding: 3px 10px !important; -} - -.logo-mini > img { - height: 42px; - width: auto; -} - -.search01 { - width: 30%; -} - -.number-info-box-content { - padding: 15px 10px 0; -} - - -/* ******* - - > Version v1.0 - -******* */ - -body { - color: #cad1d8; -} - -.fixed .main-header { - box-shadow: 0 4px 8px 0 rgba(0,0,0,.12), 0 2px 4px 0 rgba(0,0,0,.08); -} - -.skin-blue .wrapper, .skin-blue .main-sidebar, .skin-blue .left-side { - background-color: #181f27; - box-shadow: 0 4px 8px 0 rgba(0,0,0,.12), 0 2px 4px 0 rgba(0,0,0,.08); -} - -.skin-blue .main-header .logo { - background-color: #1f2933; - color: #9aa5b1; -} - -.skin-blue .main-header .navbar .sidebar-toggle { - color: #9aa5b1; -} - -.skin-blue .main-header .navbar .nav>li>a { - color: #9aa5b1; -} - -.skin-blue .sidebar-menu>li.header { - color: #797979; - background: #0e111582; -} - -.skin-blue .main-header .navbar { - background-color: #1f2933; -} - -.skin-blue .main-header .navbar .sidebar-toggle:hover { - background-color: #1c252e; -} - -.skin-blue .main-header .logo:hover { - background-color: #1c252e; -} - -.main-footer { - background: #1f2933; - color: #9aa5b1; - border-top: 1px solid #1f2933; -} - -.skin-blue .sidebar-menu>li.active>a { - border-left-color: #099aa5; -} - -.text-gray { - color: #9aa5b1 !important; -} - -.text-green { - color: #00a65a !important; -} - -.text-muted { - color: #9aa5b1 !important; -} - -.text-danger { - color: #ff1c00; -} - -.content-wrapper { - background-color: #33404d; -} - -.btn-success { - background-color: #189a1c; - border-color: #0f8513; -} - -.btn.btn-green:hover { - background-color: #0f8513; - border-color: #0e7717; -} - -.btn-primary { - background-color: #0967d3; - border-color: #0550b3; -} - -.btn.btn-primary:hover { - background-color: #0550b3; - border-color: #0345a0; -} - -.box { - box-shadow: 0 4px 8px 0 rgba(0,0,0,.12), 0 2px 4px 0 rgba(0,0,0,.08) !important; - background: #3f4d5a; - border-top: 3px solid #1f2933; -} - -.box-header { - color: #cad1d8; - background: #1f2933; -} - -.box-header.with-border { - border-bottom: 1px solid #1f2933; -} - -.box.box-default { - border-top-color: #1f2933; -} - -.box-footer { - border-top: 1px solid #4d5b69; - background-color: #3f4d5a; -} -.content-header>.breadcrumb>li>a { - color: #cad1d8; -} - -.breadcrumb>.active { - color: #cad1d8; -} - -.h1 .small, .h1 small, .h2 .small, .h2 small, .h3 .small, .h3 small, .h4 .small, .h4 small, .h5 .small, .h5 small, .h6 .small, .h6 small, h1 .small, h1 small, h2 .small, h2 small, h3 .small, h3 small, h4 .small, h4 small, h5 .small, h5 small, h6 .small, h6 small { - color: #cad1d8; -} - -.table>thead>tr>th, .table>tbody>tr>th, .table>tfoot>tr>th, .table>thead>tr>td, .table>tbody>tr>td, .table>tfoot>tr>td { - border-top: 1px solid #4d5b69; -} - -.table>thead>tr>th { - border-bottom: 2px solid #4d5b69; -} - -.table-hover>tbody>tr:hover { - background-color: #33404d; -} - -a { - color: #007eff; -} - -.nav-tabs-custom { - background: #1f2933; -} - -.nav-tabs-custom>.nav-tabs>li.active { - border-top-color: #099aa5; -} - -.nav-tabs-custom>.nav-tabs>li.active>a { - border-top-color: transparent; - border-left-color: #15161c; - border-right-color: #15161c; - background: #13181e; -} - -.nav-tabs-custom>.nav-tabs>li>a { - color: #9aa5b1; -} - -.nav-tabs-custom>.nav-tabs>li.active>a, .nav-tabs-custom>.nav-tabs>li.active:hover>a { - color: #9aa5b1; -} - -input.form-control { - padding: .75rem; - background-color: #515f6cbb; - border-width: 1px; - border-color: #606d7b; - border-radius: .25rem; - color: #cad1d8; - box-shadow: none; - -webkit-transition: border .15s linear,box-shaodw .15s ease-in; - transition: border .15s linear,box-shaodw .15s ease-in; -} - -textarea.form-control { - padding: .75rem; - background-color: #515f6cbb; - border-width: 1px; - border-color: #606d7b; - border-radius: .25rem; - color: #cad1d8; - box-shadow: none; - -webkit-transition: border .15s linear,box-shaodw .15s ease-in; - transition: border .15s linear,box-shaodw .15s ease-in; -} - -.input-group .input-group-addon { - border-color: #606d7b; - background-color: #515f6cbb; - color: #cad1d8; -} - -.select2-container--default .select2-selection--single, .select2-selection .select2-selection--single { - border: 1px solid #606d7b; -} - -.select2-container--default .select2-selection--single { - background-color: #515f6cbb; -} - -.select2-container--default .select2-selection--single .select2-selection__rendered { - color: #cad1d8; -} - -.select2-container--default .select2-selection--multiple { - background-color: #515f6cbb; -} - -.select2-container--default .select2-selection--multiple { - border: 1px solid #606d7b; - border-radius: 0; -} - -code { - background-color: #515f6cbb; - color: #c3c3c3; - border: 1px solid rgba(0, 0, 0, .25); -} - -.btn-default { - background-color: #33404d; - color: #cad1d8; - border-color: #606d7b; -} - -.select2-results__option { - background-color: #b5bcc1; - color: #444; -} - -.select2-container--default .select2-results__option--highlighted[aria-selected] { - background-color: #3c8dbc; -} - -.modal-body { - background: #3f4d5a; -} - -.modal-header { - background: #3f4d5a; - border-bottom-color: #4d5b69; -} - -.modal-footer { - background: #3f4d5a; - border-top-color: #4d5b69; -} - -@media (max-width: 991px) { - .content-header>.breadcrumb { - background: #1f2933 !important; - } -} - -.nav-tabs-custom>.nav-tabs>li.active>a, .nav-tabs-custom>.nav-tabs>li.active:hover>a { - background-color: #101216; -} - -.select2-container--default .select2-results__option[aria-selected=true], .select2-container--default .select2-results__option[aria-selected=true]:hover { - color: #fff; -} - -.select2-dropdown { - background-color: #515f6cbb; - border: 1px solid #606d7b; -} -.select2-container--default.select2-container--focus .select2-selection--multiple, .select2-container--default .select2-search--dropdown .select2-search__field { - border-color: #d2d6de !important; - background-color: #515f6cbb; -} - -.select2-container--default .select2-results__option--highlighted[aria-selected] { - background-color: #099aa5; -} - -a { - color: #288afb; -} - -a:hover { - color: #206ec7; -} - -.form-control { - border-color: #606d7b; - background-color: #515f6cbb; - color: #cad1d8; -} - -.form-control[disabled], .form-control[readonly], -fieldset[disabled] .form-control { - background-color: #1f2933; - color: #cad1d8; - cursor: not-allowed; -} - -.well { - min-height: 20px; - padding: 19px; - margin-bottom: 20px; - background-color: #515f6cbb; - border: 1px solid #606d7b; -} - -.well-lg { - padding: 24px; -} - -.well-sm { - padding: 9px; -} - -.small-box h3, .small-box p { - color: #c3c3c3; -} - -.small-box-footer { - color: #288afb; -} - -.small-box .icon { - color: #cad1d8; -} - -.bg-gray { - background-color: #33404d !important; -} - -pre { - color: #cad1d8; - background-color: #515f6cbb; - border-color: #1f2933; -} +@import url(checkbox.css);body{color:#cad1d8}.fixed .main-header{box-shadow:0 4px 8px 0 rgba(0,0,0,.12),0 2px 4px 0 rgba(0,0,0,.08)}.skin-blue .left-side,.skin-blue .main-sidebar,.skin-blue .wrapper{background-color:#181f27;box-shadow:0 4px 8px 0 rgba(0,0,0,.12),0 2px 4px 0 rgba(0,0,0,.08)}.skin-blue .main-header .logo{background-color:#1f2933;color:#9aa5b1}.skin-blue .main-header .navbar .sidebar-toggle{color:#9aa5b1}.skin-blue .main-header .navbar .nav>li>a{color:#9aa5b1}.skin-blue .sidebar-menu>li.header{color:#797979;background:#0e111582}.skin-blue .main-header .navbar{background-color:#1f2933}.skin-blue .main-header .navbar .sidebar-toggle:hover{background-color:#1c252e}.skin-blue .main-header .logo:hover{background-color:#1c252e}.main-footer{background:#1f2933;color:#9aa5b1;border-top:1px solid #1f2933}.skin-blue .sidebar-menu>li.active>a{border-left-color:#099aa5}.text-gray{color:#9aa5b1!important}.text-green{color:#00a65a!important}.text-muted{color:#9aa5b1!important}.text-danger{color:#ff1c00}.content-wrapper{background-color:#33404d}.btn-success{background-color:#189a1c;border-color:#0f8513}.btn.btn-green:hover{background-color:#0f8513;border-color:#0e7717}.btn-primary{background-color:#0967d3;border-color:#0550b3}.btn.btn-primary:hover{background-color:#0550b3;border-color:#0345a0}.box{box-shadow:0 4px 8px 0 rgba(0,0,0,.12),0 2px 4px 0 rgba(0,0,0,.08)!important;background:#3f4d5a;border-top:3px solid #1f2933}.box-header{color:#cad1d8;background:#1f2933}.box-header.with-border{border-bottom:1px solid #1f2933}.box.box-default{border-top-color:#1f2933}.box-footer{border-top:1px solid #4d5b69;background-color:#3f4d5a}.content-header>.breadcrumb>li>a{color:#cad1d8}.breadcrumb>.active{color:#cad1d8}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{color:#cad1d8}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{border-top:1px solid #4d5b69}.table>thead>tr>th{border-bottom:2px solid #4d5b69}.table-hover>tbody>tr:hover{background-color:#33404d}a{color:#007eff}.nav-tabs-custom{background:#1f2933}.nav-tabs-custom>.nav-tabs>li.active{border-top-color:#099aa5}.nav-tabs-custom>.nav-tabs>li.active>a{border-top-color:transparent;border-left-color:#15161c;border-right-color:#15161c;background:#13181e}.nav-tabs-custom>.nav-tabs>li>a{color:#9aa5b1}.nav-tabs-custom>.nav-tabs>li.active:hover>a,.nav-tabs-custom>.nav-tabs>li.active>a{color:#9aa5b1}input.form-control{padding:.75rem;background-color:#515f6cbb;border-width:1px;border-color:#606d7b;border-radius:.25rem;color:#cad1d8;box-shadow:none;-webkit-transition:border .15s linear,box-shaodw .15s ease-in;transition:border .15s linear,box-shaodw .15s ease-in}textarea.form-control{padding:.75rem;background-color:#515f6cbb;border-width:1px;border-color:#606d7b;border-radius:.25rem;color:#cad1d8;box-shadow:none;-webkit-transition:border .15s linear,box-shaodw .15s ease-in;transition:border .15s linear,box-shaodw .15s ease-in}.input-group .input-group-addon{border-color:#606d7b;background-color:#515f6cbb;color:#cad1d8}.select2-container--default .select2-selection--single,.select2-selection .select2-selection--single{border:1px solid #606d7b}.select2-container--default .select2-selection--single{background-color:#515f6cbb}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#cad1d8}.select2-container--default .select2-selection--multiple{background-color:#515f6cbb}.select2-container--default .select2-selection--multiple{border:1px solid #606d7b;border-radius:0}code{background-color:#515f6cbb;color:#c3c3c3;border:1px solid rgba(0,0,0,.25)}.btn-default{background-color:#33404d;color:#cad1d8;border-color:#606d7b}.select2-results__option{background-color:#b5bcc1;color:#444}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#3c8dbc}.modal-body{background:#3f4d5a}.modal-header{background:#3f4d5a;border-bottom-color:#4d5b69}.modal-footer{background:#3f4d5a;border-top-color:#4d5b69}@media (max-width:991px){.content-header>.breadcrumb{background:#1f2933!important}}.nav-tabs-custom>.nav-tabs>li.active:hover>a,.nav-tabs-custom>.nav-tabs>li.active>a{background-color:#101216}.select2-container--default .select2-results__option[aria-selected=true],.select2-container--default .select2-results__option[aria-selected=true]:hover{color:#fff}.select2-dropdown{background-color:#515f6cbb;border:1px solid #606d7b}.select2-container--default .select2-search--dropdown .select2-search__field,.select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#d2d6de!important;background-color:#515f6cbb}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#099aa5}a{color:#288afb}a:hover{color:#206ec7}.form-control{border-color:#606d7b;background-color:#515f6cbb;color:#cad1d8}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#1f2933;color:#cad1d8;cursor:not-allowed}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#515f6cbb;border:1px solid #606d7b}.well-lg{padding:24px}.well-sm{padding:9px}.small-box h3,.small-box p{color:#c3c3c3}.small-box-footer{color:#288afb}.small-box .icon{color:#cad1d8}.bg-gray{background-color:#33404d!important}pre{color:#cad1d8;background-color:#515f6cbb;border-color:#1f2933} \ No newline at end of file diff --git a/public/themes/pterodactyl/css/terminal.css b/public/themes/pterodactyl/css/terminal.css deleted file mode 100644 index d2dc4d00f4..0000000000 --- a/public/themes/pterodactyl/css/terminal.css +++ /dev/null @@ -1,105 +0,0 @@ -/*Design for Terminal*/ -@import url('https://fonts.googleapis.com/css?family=Source+Code+Pro'); - -#terminal-body { - background: rgb(26, 26, 26); - margin: 0; - width: 100%; - height: 100%; - overflow: hidden; -} - -#terminal { - font-family: 'Source Code Pro', monospace; - color: rgb(223, 223, 223); - background: rgb(26, 26, 26); - font-size: 12px; - line-height: 14px; - padding: 10px 10px 0; - box-sizing: border-box; - height: 500px; - max-height: 500px; - overflow-y: auto; - overflow-x: hidden; - border-radius: 5px 5px 0 0; -} - -#terminal > .cmd { - padding: 1px 0; - word-wrap: break-word; - white-space: pre-wrap; -} - -#terminal_input { - width: 100%; - background: rgb(26, 26, 26); - border-radius: 0 0 5px 5px; - padding: 0 0 0 10px !important; -} - -.terminal_input--input, .terminal_input--prompt { - font-family: 'Source Code Pro', monospace; - margin-bottom: 0; - border: 0 !important; - background: transparent !important; - color: rgb(223, 223, 223); - font-size: 12px; - padding: 1px 0 4px !important; -} -.terminal_input--input { - margin-left: 6px; - line-height: 1; - outline: none !important; -} - -.terminal-notify { - position: absolute; - right: 30px; - bottom: 30px; - padding: 3.5px 7px; - border-radius: 3px; - background: #fff; - color: #000; - opacity: .5; - font-size: 16px; - cursor: pointer; - z-index: 10; -} - -.terminal-notify:hover { - opacity: .9; -} - -.ansi-black-fg { color: rgb(0, 0, 0); } -.ansi-red-fg { color: rgb(166, 0, 44); } -.ansi-green-fg { color: rgb(55, 106, 27); } -.ansi-yellow-fg { color: rgb(241, 133, 24); } -.ansi-blue-fg { color: rgb(17, 56, 163); } -.ansi-magenta-fg { color: rgb(67, 0, 117); } -.ansi-cyan-fg { color: rgb(18, 95, 105); } -.ansi-white-fg { color: rgb(255, 255, 255); } -.ansi-bright-black-fg { color: rgb(51, 51, 51); } -.ansi-bright-red-fg { color: rgb(223, 45, 39); } -.ansi-bright-green-fg { color: rgb(105, 175, 45); } -.ansi-bright-yellow-fg { color: rgb(254, 232, 57); } -.ansi-bright-blue-fg { color: rgb(68, 145, 240); } -.ansi-bright-magenta-fg { color: rgb(151, 50, 174); } -.ansi-bright-cyan-fg{ color: rgb(37, 173, 98); } -.ansi-bright-white-fg { color: rgb(208, 208, 208); } - -.ansi-black-bg { background: rgb(0, 0, 0); } -.ansi-red-bg { background: rgb(166, 0, 44); } -.ansi-green-bg { background: rgb(55, 106, 27); } -.ansi-yellow-bg { background: rgb(241, 133, 24); } -.ansi-blue-bg { background: rgb(17, 56, 163); } -.ansi-magenta-bg { background: rgb(67, 0, 117); } -.ansi-cyan-bg { background: rgb(18, 95, 105); } -.ansi-white-bg { background: rgb(255, 255, 255); } -.ansi-bright-black-bg { background: rgb(51, 51, 51); } -.ansi-bright-red-bg { background: rgb(223, 45, 39); } -.ansi-bright-green-bg { background: rgb(105, 175, 45); } -.ansi-bright-yellow-bg { background: rgb(254, 232, 57); } -.ansi-bright-blue-bg { background: rgb(68, 145, 240); } -.ansi-bright-magenta-bg { background: rgb(151, 50, 174); } -.ansi-bright-cyan-bg { background: rgb(37, 173, 98); } -.ansi-bright-white-bg { background: rgb(208, 208, 208); } diff --git a/resources/scripts/api/server/types.d.ts b/resources/scripts/api/server/types.d.ts index 7e3e540c0a..9cf497edf7 100644 --- a/resources/scripts/api/server/types.d.ts +++ b/resources/scripts/api/server/types.d.ts @@ -21,3 +21,13 @@ export interface ServerEggVariable { isEditable: boolean; rules: string[]; } + +export interface ServerAuditLog { + uuid: string; + user: string; + action: string; + device: Record; + metadata: Record; + isSystem: boolean; + createdAt: Date; +} diff --git a/resources/scripts/api/swr/getServerAuditLogs.ts b/resources/scripts/api/swr/getServerAuditLogs.ts new file mode 100644 index 0000000000..228c4c1a12 --- /dev/null +++ b/resources/scripts/api/swr/getServerAuditLogs.ts @@ -0,0 +1,30 @@ +import useSWR from 'swr'; +import http, { getPaginationSet, PaginatedResult } from '@/api/http'; +import { ServerAuditLog } from '@/api/server/types'; +import { rawDataToServerAuditLog } from '@/api/transformers'; +import { ServerContext } from '@/state/server'; +import { createContext, useContext } from 'react'; + +interface ctx { + page: number; + setPage: (value: number | ((s: number) => number)) => void; +} + +export const Context = createContext({ page: 1, setPage: () => 1 }); + +type AuditLogResponse = PaginatedResult & { logCount: number }; + +export default () => { + const { page } = useContext(Context); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); + + return useSWR([ 'server:logs', uuid, page ], async () => { + const { data } = await http.get(`/api/client/servers/${uuid}/auditlogs`, { params: { page } }); + + return ({ + items: (data.data || []).map(rawDataToServerAuditLog), + pagination: getPaginationSet(data.meta.pagination), + logCount: data.meta.log_count, + }); + }); +}; diff --git a/resources/scripts/api/transformers.ts b/resources/scripts/api/transformers.ts index 069baf1264..433630a0df 100644 --- a/resources/scripts/api/transformers.ts +++ b/resources/scripts/api/transformers.ts @@ -1,7 +1,7 @@ import { Allocation } from '@/api/server/getServer'; import { FractalResponseData } from '@/api/http'; import { FileObject } from '@/api/server/files/loadDirectory'; -import { ServerBackup, ServerEggVariable } from '@/api/server/types'; +import { ServerBackup, ServerEggVariable, ServerAuditLog } from '@/api/server/types'; export const rawDataToServerAllocation = (data: FractalResponseData): Allocation => ({ id: data.attributes.id, @@ -76,3 +76,13 @@ export const rawDataToServerEggVariable = ({ attributes }: FractalResponseData): isEditable: attributes.is_editable, rules: attributes.rules.split('|'), }); + +export const rawDataToServerAuditLog = ({ attributes }: FractalResponseData): ServerAuditLog => ({ + uuid: attributes.uuid, + user: attributes.user, + action: attributes.action, + device: attributes.device, + metadata: attributes.metadata, + isSystem: attributes.is_system, + createdAt: new Date(attributes.created_at), +}); diff --git a/resources/scripts/assets/css/jexactyl.css b/resources/scripts/assets/css/jexactyl.css index 53507d6e0e..d0c2673e4d 100644 --- a/resources/scripts/assets/css/jexactyl.css +++ b/resources/scripts/assets/css/jexactyl.css @@ -20,45 +20,3 @@ } } -.storeResourceBox { - flex: 0 0 25%; - max-width: 25%; - position: relative; - width: 100%; - min-height: 1px; - padding-right: 5px; - padding-left: 5px; -} - -.storeResourceCol { - position: relative; - width: 100%; - min-height: 1px; - padding-right: 5px; - padding-left: 5px; - flex-basis: 0; - flex-grow: 1; - max-width: 100%; -} - -.storePurchaseBox { - flex: 0 0 25%; - max-width: 25%; - position: relative; - width: 100%; - min-height: 1px; - padding-right: 5px; - padding-left: 5px; -} - -.storePurchaseCol { - position: relative; - width: 100%; - min-height: 1px; - padding-right: 5px; - padding-left: 5px; - flex-basis: 0; - flex-grow: 1; - max-width: 100%; -} - diff --git a/resources/scripts/components/App.tsx b/resources/scripts/components/App.tsx index a06a4c7b2e..5574f2cdea 100644 --- a/resources/scripts/components/App.tsx +++ b/resources/scripts/components/App.tsx @@ -25,6 +25,7 @@ interface ExtendedWindow extends Window { /* eslint-disable camelcase */ root_admin: boolean; use_totp: boolean; + avatar_url: string; cr_balance: number; cr_slots: number; cr_cpu: number; @@ -59,6 +60,7 @@ const App = () => { language: PterodactylUser.language, rootAdmin: PterodactylUser.root_admin, useTotp: PterodactylUser.use_totp, + avatarURL: PterodactylUser.avatar_url, crBalance: PterodactylUser.cr_balance, crSlots: PterodactylUser.cr_slots, crCpu: PterodactylUser.cr_cpu, diff --git a/resources/scripts/components/SidePanel.tsx b/resources/scripts/components/SidePanel.tsx deleted file mode 100644 index 18d5be0633..0000000000 --- a/resources/scripts/components/SidePanel.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from 'react'; -import tw from 'twin.macro'; -import { useStoreState } from 'easy-peasy'; -import { ApplicationStore } from '@/state'; -import { NavLink, Link } from 'react-router-dom'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faCogs, faKey, faLayerGroup, faSignOutAlt, faSitemap, faStore, faUserCircle } from '@fortawesome/free-solid-svg-icons'; -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import styled from 'styled-components/macro'; -import http from '@/api/http'; - -export function Category (props: { children: React.ReactNode }) { - return ( -
- {props.children} -
); -} - -export function SidePanelLink (props: { icon: IconProp, react?: boolean, link: string, exact?: boolean, title: string }) { - return props.react ?? false ? ( - - - - ) : ( - - - - ); -} - -export default (props: { children?: React.ReactNode }) => { - const storeEnabled = useStoreState(state => state.settings!.data?.store.enabled); - const rootAdmin = useStoreState((state: ApplicationStore) => state.user.data!.rootAdmin); - - const onTriggerLogout = () => { - http.post('/auth/logout').finally(() => { - // @ts-ignore - window.location = '/'; - }); - }; - - const PanelDiv = styled.div` - ${tw`h-screen bg-neutral-900 shadow-lg flex flex-col w-32 px-4 py-2 fixed top-0 left-0`}; - } - `; - - return ( - -
- - - -
- {props.children} - - -
- {storeEnabled && - <> - -
- - } - -
- -
- - {rootAdmin && - <> -
- - - } -
- -
-
- ); -}; diff --git a/resources/scripts/components/auth/LoginFormContainer.tsx b/resources/scripts/components/auth/LoginFormContainer.tsx index ad1e9af8dc..94b41b0439 100644 --- a/resources/scripts/components/auth/LoginFormContainer.tsx +++ b/resources/scripts/components/auth/LoginFormContainer.tsx @@ -161,7 +161,7 @@ const Container = ({ title, children }: { title?: string, children: React.ReactN {children}

- © {(new Date()).getFullYear()} Jexactyl + © {(new Date()).getFullYear()} Jexactyl, built on Pterodactyl.

diff --git a/resources/scripts/components/elements/Sidebar.tsx b/resources/scripts/components/elements/Sidebar.tsx new file mode 100644 index 0000000000..0c920ea44f --- /dev/null +++ b/resources/scripts/components/elements/Sidebar.tsx @@ -0,0 +1,48 @@ +import tw from 'twin.macro'; +import styled from 'styled-components/macro'; +import { withSubComponents } from '@/components/helpers'; + +const Wrapper = styled.div` + ${tw`w-full flex flex-col px-4`}; + & > a { + ${tw`h-12 w-full flex flex-row items-center text-neutral-300 cursor-pointer select-none px-5`}; + ${tw`hover:text-neutral-50`}; + & > svg { + ${tw`h-6 w-6 flex flex-shrink-0`}; + } + & > span { + ${tw`font-header font-medium text-lg whitespace-nowrap leading-none ml-3`}; + } + &:active, &.active { + ${tw`text-neutral-50 bg-neutral-800 rounded`}; + } + } +`; + +const Section = styled.div` + ${tw`h-6 font-header font-medium text-xs text-neutral-300 whitespace-nowrap uppercase ml-4 mb-1 select-none`}; + &:not(:first-of-type) { + ${tw`mt-4`}; + } +`; + +const User = styled.div` + ${tw`h-16 w-full flex items-center bg-neutral-700 justify-center`}; +`; + +const Sidebar = styled.div` + ${tw`h-screen hidden md:flex flex-col items-center flex-shrink-0 bg-neutral-900 overflow-x-hidden ease-linear`}; + ${tw`w-48`}; + & > a { + ${tw`flex flex-row items-center text-neutral-300 cursor-pointer select-none px-8`}; + ${tw`hover:text-neutral-50`}; + & > svg { + ${tw`transition-none h-6 w-6 flex flex-shrink-0`}; + } + & > span { + ${tw`font-header font-medium text-lg whitespace-nowrap leading-none ml-3`}; + } + } +`; + +export default withSubComponents(Sidebar, { Section, Wrapper, User }); diff --git a/resources/scripts/components/elements/StaticSubNavigation.tsx b/resources/scripts/components/elements/StaticSubNavigation.tsx new file mode 100644 index 0000000000..ed0b33a6da --- /dev/null +++ b/resources/scripts/components/elements/StaticSubNavigation.tsx @@ -0,0 +1,21 @@ +import styled from 'styled-components/macro'; +import tw from 'twin.macro'; + +const StaticSubNavigation = styled.div` + ${tw`w-full overflow-x-auto`}; + + & > div { + ${tw`flex items-center text-sm mx-auto px-2`}; + max-width: 1200px; + + & > a, & > div { + ${tw`inline-block py-4 px-4 text-neutral-300 no-underline whitespace-nowrap transition-all duration-150`}; + + &:not(:first-of-type) { + ${tw`ml-2`}; + } + } + } +`; + +export default StaticSubNavigation; diff --git a/resources/scripts/components/elements/Switch.tsx b/resources/scripts/components/elements/Switch.tsx index 6420bccfa0..5f2aff4a2f 100644 --- a/resources/scripts/components/elements/Switch.tsx +++ b/resources/scripts/components/elements/Switch.tsx @@ -7,23 +7,18 @@ import Input from '@/components/elements/Input'; const ToggleContainer = styled.div` ${tw`relative select-none w-12 leading-normal`}; - & > input[type="checkbox"] { ${tw`hidden`}; - &:checked + label { ${tw`bg-primary-500 border-primary-700 shadow-none`}; } - &:checked + label:before { right: 0.125rem; } } - & > label { ${tw`mb-0 block overflow-hidden cursor-pointer bg-neutral-400 border border-neutral-700 rounded-full h-6 shadow-inner`}; transition: all 75ms linear; - &::before { ${tw`absolute block bg-white border h-5 w-5 rounded-full`}; top: 0.125rem; diff --git a/resources/scripts/components/helpers.ts b/resources/scripts/components/helpers.ts new file mode 100644 index 0000000000..47b9d5f00a --- /dev/null +++ b/resources/scripts/components/helpers.ts @@ -0,0 +1,9 @@ +import { StyledComponent } from 'styled-components/macro'; + +export const withSubComponents = , P extends Record> (component: C, properties: P): C & P => { + Object.keys(properties).forEach((key: keyof P) => { + (component as any)[key] = properties[key]; + }); + + return component as C & P; +}; diff --git a/resources/scripts/components/server/ServerConsole.tsx b/resources/scripts/components/server/ServerConsole.tsx index e6115614fa..f6e7435445 100644 --- a/resources/scripts/components/server/ServerConsole.tsx +++ b/resources/scripts/components/server/ServerConsole.tsx @@ -32,6 +32,8 @@ const ServerConsole = () => { {eggFeatures.includes('eula') && } {eggFeatures.includes('java_version') && } + {eggFeatures.includes('gsl_token') && } + {eggFeatures.includes('pid_limit') && }
diff --git a/resources/scripts/components/server/auditlogs/AuditLogHandler.ts b/resources/scripts/components/server/auditlogs/AuditLogHandler.ts new file mode 100644 index 0000000000..79352117ae --- /dev/null +++ b/resources/scripts/components/server/auditlogs/AuditLogHandler.ts @@ -0,0 +1,95 @@ +import { ServerAuditLog } from '@/api/server/types'; + +export function AuditLogHandler (log: ServerAuditLog) { + switch (log.action) { + case 'server:backup.start': + return `Created backup ${log.metadata.backup_name}`; + case 'server:backup.fail': + return `Backup ${log.metadata.backup_name} failed`; + case 'server:backup.complete': + return `Backup ${log.metadata.backup_name} completed`; + case 'server:backup.delete': + return `Deleted backup ${log.metadata.backup_name}`; + case 'server:backup.download': + return `Downloaded backup ${log.metadata.backup_name}`; + case 'server:backup.lock': + return `Locked backup ${log.metadata.backup_name}`; + case 'server:backup.unlock': + return `Unlocked backup ${log.metadata.backup_name}`; + case 'server:backup.restore.start': + return `Backup ${log.metadata.backup_name} restoration started`; + case 'server:backup.restore.complete': + return `Backup ${log.metadata.backup_name} restoration completed`; + case 'server:backup.restore.fail': + return `Backup ${log.metadata.backup_name} restoration failed`; + + case 'server:database.create': + return `Created database ${log.metadata.database_name}`; + case 'server:database.password.rotate': + return `Rotated password of database ${log.metadata.database_name}`; + case 'server:database.delete': + return `Deleted database ${log.metadata.database_name}`; + + case 'server:filesystem.download': + return `Downloaded file ${log.metadata.file}`; + case 'server:filesystem.write': + return `Edited file ${log.metadata.file}`; + case 'server:filesystem.delete': + if (log.metadata.files.length > 1) { + return `Deleted files ${log.metadata.files.join(', ')}`; + } else { + return `Deleted file ${log.metadata.files[0]}`; + } + case 'server:filesystem.rename': + return `Renamed file ${log.metadata.file}`; + case 'server:filesystem.compress': + if (log.metadata.files.length > 1) { + return `Compressed files ${log.metadata.files.join(', ')} in ${log.metadata.root}`; + } else { + return `Compressed file ${log.metadata.root + log.metadata.files[0]}`; + } + case 'server:filesystem.decompress': + return `Decompressed file ${log.metadata.root + log.metadata.files}`; + case 'server:filesystem.pull': + return `URL ${log.metadata.url} pulled into ${log.metadata.directory}`; + + case 'server:allocation.set.primary': + return `Set allocation port ${log.metadata.allocation_port} as primary`; + case 'server:allocation.delete': + return `Deleted allocation port ${log.metadata.allocation_port}`; + case 'server:allocation.create': + return `Created allocation port ${log.metadata.allocation_port}`; + + case 'server:schedule.create': + return `Created schedule ${log.metadata.schedule_name}`; + case 'server:schedule.update': + return `Updated schedule ${log.metadata.schedule_name}`; + case 'server:schedule.delete': + return `Deleted schedule ${log.metadata.schedule_name}`; + case 'server:schedule.run': + return `Executed schedule ${log.metadata.schedule_name}`; + case 'server:schedule.task.create': + return `Created task inside schedule ${log.metadata.schedule_name}`; + case 'server:schedule.task.update': + return `Updated task inside schedule ${log.metadata.schedule_name}`; + case 'server:schedule.task.delete': + return `Deleted task inside schedule ${log.metadata.schedule_name}`; + + case 'server:settings.name.update': + return `Renamed server from ${log.metadata.old} to ${log.metadata.new}`; + case 'server:settings.reinstall': + return 'Reinstalled server'; + case 'server:settings.image.update': + return `Updated Docker image from ${log.metadata.old} to ${log.metadata.new}`; + + case 'server:subuser.create': + return `Created subuser ${log.metadata.user}`; + case 'server:subuser.update': + return `Updated subuser ${log.metadata.user}`; + case 'server:subuser.delete': + return `Deleted subuser ${log.metadata.user}`; + + default: + return 'Log type not matching any know types'; + } +} diff --git a/resources/scripts/components/server/auditlogs/AuditLogRow.tsx b/resources/scripts/components/server/auditlogs/AuditLogRow.tsx new file mode 100644 index 0000000000..3e1af867e7 --- /dev/null +++ b/resources/scripts/components/server/auditlogs/AuditLogRow.tsx @@ -0,0 +1,76 @@ +import React, { useState } from 'react'; +import { formatDistanceToNow } from 'date-fns'; +import tw from 'twin.macro'; +import GreyRowBox from '@/components/elements/GreyRowBox'; +import { ServerAuditLog } from '@/api/server/types'; +import { AuditLogHandler } from '@/components/server/auditlogs/AuditLogHandler'; +import Modal from '@/components/elements/Modal'; +import Label from '@/components/elements/Label'; +import Button from '@/components/elements/Button'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faEye } from '@fortawesome/free-solid-svg-icons'; + +interface Props { + log: ServerAuditLog; + className?: string; +} + +export default ({ log, className }: Props) => { + const [ visible, setVisible ] = useState(false); + + return ( + <> + setVisible(false)}> +

Audit log details

+
+ +

{AuditLogHandler(log)}

+
+
+ +

{log.user}

+
+
+ +

+ {log.device.ip_address} +

+
+
+ +

{formatDistanceToNow(log.createdAt, { includeSeconds: true, addSuffix: true })}

+
+
+ +

{JSON.stringify(log.metadata)}

+
+
+ +
+
+ +
+

{AuditLogHandler(log)}

+

Action

+
+
+

{log.user}

+

User

+
+
+

{formatDistanceToNow(log.createdAt, { includeSeconds: true, addSuffix: true })}

+

Created

+
+
+ +
+
+ + ); +}; diff --git a/resources/scripts/components/server/auditlogs/AuditLogsContainer.tsx b/resources/scripts/components/server/auditlogs/AuditLogsContainer.tsx new file mode 100644 index 0000000000..16b9c3e4c7 --- /dev/null +++ b/resources/scripts/components/server/auditlogs/AuditLogsContainer.tsx @@ -0,0 +1,62 @@ +import React, { useContext, useEffect, useState } from 'react'; +import Spinner from '@/components/elements/Spinner'; +import useFlash from '@/plugins/useFlash'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import AuditLogRow from '@/components/server/auditlogs/AuditLogRow'; +import tw from 'twin.macro'; +import getServerAuditLogs, { Context as ServerLogsContext } from '@/api/swr/getServerAuditLogs'; +import ServerContentBlock from '@/components/elements/ServerContentBlock'; +import Pagination from '@/components/elements/Pagination'; + +const AuditLogsContainer = () => { + const { page, setPage } = useContext(ServerLogsContext); + const { clearFlashes, clearAndAddHttpError } = useFlash(); + const { data: logs, error, isValidating } = getServerAuditLogs(); + + useEffect(() => { + if (!error) { + clearFlashes('logs'); + + return; + } + + clearAndAddHttpError({ error, key: 'logs' }); + }, [ error ]); + + if (!logs || (error && isValidating)) { + return ; + } + + return ( + + + + {({ items }) => ( + !items.length ? +

+ {page > 1 ? + 'Looks like we\'ve run out of logs to show you, try going back a page.' + : + 'It looks like there are no logs currently stored for this server.' + } +

+ : + items.map((log, index) => 0 ? tw`mt-2` : undefined} + />) + )} +
+
+ ); +}; + +export default () => { + const [ page, setPage ] = useState(1); + return ( + + + + ); +}; diff --git a/resources/scripts/components/server/files/FileManagerContainer.tsx b/resources/scripts/components/server/files/FileManagerContainer.tsx index 47910b8c37..8c4b2bbe2e 100644 --- a/resources/scripts/components/server/files/FileManagerContainer.tsx +++ b/resources/scripts/components/server/files/FileManagerContainer.tsx @@ -21,9 +21,42 @@ import ErrorBoundary from '@/components/elements/ErrorBoundary'; import { FileActionCheckbox } from '@/components/server/files/SelectFileCheckbox'; import { hashToPath } from '@/helpers'; +let sortMethod = 'namedown'; + const sortFiles = (files: FileObject[]): FileObject[] => { - const sortedFiles: FileObject[] = files.sort((a, b) => a.name.localeCompare(b.name)).sort((a, b) => a.isFile === b.isFile ? 0 : (a.isFile ? 1 : -1)); - return sortedFiles.filter((file, index) => index === 0 || file.name !== sortedFiles[index - 1].name); + // Sorts by name + if (sortMethod === 'namedown') { + return files.sort((a, b) => a.name.localeCompare(b.name)) + .sort((a, b) => a.isFile === b.isFile ? 0 : (a.isFile ? 1 : -1)); + } + // Sorts by file size + if (sortMethod === 'sizedown') { + return files.sort((a, b) => b.size - a.size) + .sort((a, b) => a.isFile === b.isFile ? 0 : (a.isFile ? 1 : -1)); + } + // Sorts by date modified + if (sortMethod === 'datedown') { + return files.sort((a, b) => a.modifiedAt.getDate() - b.modifiedAt.getDate()) + .sort((a, b) => a.isFile === b.isFile ? 0 : (a.isFile ? 1 : -1)); + } + // Sorts by name + if (sortMethod === 'nameup') { + return files.sort((a, b) => b.name.localeCompare(a.name)) + .sort((a, b) => a.isFile === b.isFile ? 0 : (a.isFile ? 1 : -1)); + } + // Sorts by file size + if (sortMethod === 'sizeup') { + return files.sort((a, b) => a.size - b.size) + .sort((a, b) => a.isFile === b.isFile ? 0 : (a.isFile ? 1 : -1)); + } + // Sorts by date modified + if (sortMethod === 'dateup') { + return files.sort((a, b) => b.modifiedAt.getDate() - a.modifiedAt.getDate()) + .sort((a, b) => a.isFile === b.isFile ? 0 : (a.isFile ? 1 : -1)); + } + // Fallback to name + return files.sort((a, b) => a.name.localeCompare(b.name)) + .sort((a, b) => a.isFile === b.isFile ? 0 : (a.isFile ? 1 : -1)); }; export default () => { @@ -51,6 +84,40 @@ export default () => { setSelectedFiles(e.currentTarget.checked ? (files?.map(file => file.name) || []) : []); }; + const sortName = () => { + if (sortMethod === 'namedown') { + sortMethod = 'nameup'; + } else { + sortMethod = 'namedown'; + } + + if (!files) return; + sortFiles(files.splice(0, 250)); + mutate(); + }; + + const sortSize = () => { + if (sortMethod === 'sizedown') { + sortMethod = 'sizeup'; + } else { + sortMethod = 'sizedown'; + } + if (!files) return; + sortFiles(files.splice(0, 250)); + mutate(); + }; + + const sortDate = () => { + if (sortMethod === 'datedown') { + sortMethod = 'dateup'; + } else { + sortMethod = 'datedown'; + } + if (!files) return; + sortFiles(files.splice(0, 250)); + mutate(); + }; + if (error) { return ( mutate()}/> @@ -94,12 +161,47 @@ export default () => { : <> +
+ + + +
{!files.length ?

This directory seems to be empty.

: - +
{files.length > 250 &&
diff --git a/resources/scripts/components/server/schedules/EditScheduleModal.tsx b/resources/scripts/components/server/schedules/EditScheduleModal.tsx index 57602f2ed9..84aa1e3c1b 100644 --- a/resources/scripts/components/server/schedules/EditScheduleModal.tsx +++ b/resources/scripts/components/server/schedules/EditScheduleModal.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { Schedule } from '@/api/server/schedules/getServerSchedules'; import Field from '@/components/elements/Field'; import { Form, Formik, FormikHelpers } from 'formik'; @@ -12,6 +12,8 @@ import tw from 'twin.macro'; import Button from '@/components/elements/Button'; import ModalContext from '@/context/ModalContext'; import asModal from '@/hoc/asModal'; +import Switch from '@/components/elements/Switch'; +import ScheduleCheatsheetCards from '@/components/server/schedules/ScheduleCheetsheetCards'; interface Props { schedule?: Schedule; @@ -30,6 +32,7 @@ interface Values { const EditScheduleModal = ({ schedule }: Props) => { const { addError, clearFlashes } = useFlash(); + const [ showCheatsheet, setShowCheetsheet ] = useState(false); const { dismiss } = useContext(ModalContext); const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); @@ -103,6 +106,20 @@ const EditScheduleModal = ({ schedule }: Props) => { The schedule system supports the use of Cronjob syntax when defining when tasks should begin running. Use the fields above to specify when these tasks should begin running.

+
+ setShowCheetsheet(s => !s)} + /> + {showCheatsheet && +
+ +
+ } +
{ + return ( + <> +
+
+

Examples

+
+
*/5 * * * *
+
every 5 minutes
+
+
+
0 */1 * * *
+
every hour
+
+
+
0 8-12 * * *
+
hour range
+
+
+
0 0 * * *
+
once a day
+
+
+
0 0 * * MON
+
every Monday
+
+
+
+
+

Special Characters

+
+
+
*
+
any value
+
+
+
,
+
value list separator
+
+
+
-
+
range values
+
+
+
/
+
step values
+
+
+
+ + ); +}; diff --git a/resources/scripts/components/store/ActionsRow.tsx b/resources/scripts/components/store/ActionsRow.tsx index f6056b1a67..f1dbc3c9b5 100644 --- a/resources/scripts/components/store/ActionsRow.tsx +++ b/resources/scripts/components/store/ActionsRow.tsx @@ -1,12 +1,13 @@ -import { faHdd, faLayerGroup, faMemory, faMicrochip } from '@fortawesome/free-solid-svg-icons'; +import tw from 'twin.macro'; import React, { useState } from 'react'; -import Button from '@/components/elements/Button'; -import TitledGreyBox from '@/components/elements/TitledGreyBox'; -import buyCPU from '@/api/store/buy/buyCPU'; import useFlash from '@/plugins/useFlash'; -import buySlots from '@/api/store/buy/buySlots'; +import buyCPU from '@/api/store/buy/buyCPU'; import buyRAM from '@/api/store/buy/buyRAM'; +import buySlots from '@/api/store/buy/buySlots'; +import Button from '@/components/elements/Button'; import buyStorage from '@/api/store/buy/buyStorage'; +import TitledGreyBox from '@/components/elements/TitledGreyBox'; +import { faHdd, faLayerGroup, faMemory, faMicrochip } from '@fortawesome/free-solid-svg-icons'; const ActionsRow = () => { const { addFlash, clearFlashes, clearAndAddHttpError } = useFlash(); @@ -24,7 +25,8 @@ const ActionsRow = () => { message: '1 server slot has been added to your account.', })) .catch(error => { - clearAndAddHttpError({ error }); + console.error(error); + clearAndAddHttpError({ key: 'resources', error }); setSubmit(false); }); }; @@ -41,7 +43,9 @@ const ActionsRow = () => { message: '50% CPU has been added to your account.', })) .catch(error => { - clearAndAddHttpError({ error }); + console.error(error); + clearAndAddHttpError({ key: 'resources', error }); + setSubmit(false); }); }; @@ -57,7 +61,8 @@ const ActionsRow = () => { message: '1GB RAM has been added to your account.', })) .catch(error => { - clearAndAddHttpError({ error }); + clearAndAddHttpError({ key: 'resources', error }); + setSubmit(false); }); }; @@ -73,49 +78,50 @@ const ActionsRow = () => { message: '1GB Storage has been added to your account.', })) .catch(error => { - clearAndAddHttpError({ error }); + clearAndAddHttpError({ key: 'resources', error }); + setSubmit(false); }); }; return ( - <> -
-
- - - -
-
-
-
- - - -
-
-
-
- - - -
-
-
-
- - - -
-
- +
+ + + + + + + + + + + + +
); }; diff --git a/resources/scripts/components/store/ResourceRow.tsx b/resources/scripts/components/store/ResourceRow.tsx index a928c374e0..874381a609 100644 --- a/resources/scripts/components/store/ResourceRow.tsx +++ b/resources/scripts/components/store/ResourceRow.tsx @@ -1,61 +1,44 @@ -import { - faLayerGroup, - faMicrochip, - faMemory, - faHdd, -} from '@fortawesome/free-solid-svg-icons'; import React from 'react'; import tw from 'twin.macro'; -import TitledGreyBox from '@/components/elements/TitledGreyBox'; -import { useStoreState } from '@/state/hooks'; import { megabytesToHuman } from '@/helpers'; +import { useStoreState } from '@/state/hooks'; +import TitledGreyBox from '@/components/elements/TitledGreyBox'; +import { faLayerGroup, faMicrochip, faMemory, faHdd } from '@fortawesome/free-solid-svg-icons'; const ResourceRow = () => { const user = useStoreState(state => state.user.data); return ( - <> -
-
- -

{user!.crSlots}

-
-
-
-
-
- -

{user!.crCpu}%

-
-
-
-
-
- -

{megabytesToHuman(user!.crRam)}

-
-
-
-
-
- -

{megabytesToHuman(user!.crStorage)}

-
-
-
- +
+ +

{user!.crSlots}

+
+ +

{user!.crCpu}%

+
+ +

{megabytesToHuman(user!.crRam)}

+
+ +

{megabytesToHuman(user!.crStorage)}

+
+
); }; diff --git a/resources/scripts/components/store/StoreContainer.tsx b/resources/scripts/components/store/StoreContainer.tsx index 5a24f4ac07..5a6bb15041 100644 --- a/resources/scripts/components/store/StoreContainer.tsx +++ b/resources/scripts/components/store/StoreContainer.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import PageContentBlock from '@/components/elements/PageContentBlock'; import tw from 'twin.macro'; -import UserInformationRow from '@/components/store/UserInformationRow'; -import ResourceRow from '@/components/store/ResourceRow'; import ActionsRow from '@/components/store/ActionsRow'; +import ResourceRow from '@/components/store/ResourceRow'; import FlashMessageRender from '@/components/FlashMessageRender'; +import PageContentBlock from '@/components/elements/PageContentBlock'; +import UserInformationRow from '@/components/store/UserInformationRow'; const StoreContainer = () => { return ( @@ -16,11 +16,11 @@ const StoreContainer = () => {
-

Resources

+

Resources

-

Actions

+

Actions

diff --git a/resources/scripts/components/store/UserInformationRow.tsx b/resources/scripts/components/store/UserInformationRow.tsx index f8c95c4fb1..bc004ccf42 100644 --- a/resources/scripts/components/store/UserInformationRow.tsx +++ b/resources/scripts/components/store/UserInformationRow.tsx @@ -1,21 +1,20 @@ -import { faArrowCircleRight, faInfoCircle, faLayerGroup } from '@fortawesome/free-solid-svg-icons'; import React from 'react'; import tw from 'twin.macro'; -import TitledGreyBox from '@/components/elements/TitledGreyBox'; -import { useStoreState } from '@/state/hooks'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { NavLink } from 'react-router-dom'; +import { useStoreState } from '@/state/hooks'; import Button from '@/components/elements/Button'; +import TitledGreyBox from '@/components/elements/TitledGreyBox'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faArrowCircleRight, faInfoCircle, faLayerGroup } from '@fortawesome/free-solid-svg-icons'; const UserInformationRow = () => { const user = useStoreState(state => state.user.data); return ( -
+
Nice to see you, {user!.username}!
You have {user!.crBalance} credits available.
@@ -31,7 +30,6 @@ const UserInformationRow = () => { Create a server with a specific amount of RAM, CPU and storage allocated to it. diff --git a/resources/scripts/components/store/servers/CreateServerContainer.tsx b/resources/scripts/components/store/servers/CreateServerContainer.tsx index 54c12f8287..06ee4f3584 100644 --- a/resources/scripts/components/store/servers/CreateServerContainer.tsx +++ b/resources/scripts/components/store/servers/CreateServerContainer.tsx @@ -1,21 +1,21 @@ -import React, { useEffect, useState } from 'react'; -import PageContentBlock from '@/components/elements/PageContentBlock'; -import useFlash from '@/plugins/useFlash'; +import useSWR from 'swr'; import tw from 'twin.macro'; -import TitledGreyBox from '@/components/elements/TitledGreyBox'; -import { Form, Formik, FormikHelpers } from 'formik'; -import Button from '@/components/elements/Button'; +import useFlash from '@/plugins/useFlash'; import { number, object, string } from 'yup'; -import Field from '@/components/elements/Field'; -import createServer from '@/api/store/createServer'; -import getConfig from '@/api/store/getConfig'; -import { faHdd, faLayerGroup, faMicrochip, faMemory } from '@fortawesome/free-solid-svg-icons'; import { megabytesToHuman } from '@/helpers'; import { useStoreState } from '@/state/hooks'; -import useSWR from 'swr'; +import getConfig from '@/api/store/getConfig'; +import Field from '@/components/elements/Field'; +import Button from '@/components/elements/Button'; +import React, { useEffect, useState } from 'react'; +import createServer from '@/api/store/createServer'; import Spinner from '@/components/elements/Spinner'; -import FlashMessageRender from '@/components/FlashMessageRender'; +import { Form, Formik, FormikHelpers } from 'formik'; import InputSpinner from '@/components/elements/InputSpinner'; +import TitledGreyBox from '@/components/elements/TitledGreyBox'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import PageContentBlock from '@/components/elements/PageContentBlock'; +import { faHdd, faLayerGroup, faMicrochip, faMemory } from '@fortawesome/free-solid-svg-icons'; export interface ConfigResponse { user: any[]; @@ -104,51 +104,44 @@ export default () => { })} >
-
-
- -
- -

Character limits: a-z A-Z 0-9 _ - . and [Space].

-

Assign a name to your server for use in the Panel.

-
-
-
-
- -
- -

{user!.crCpu}% available

-

Assign CPU cores and threads to your server. (100% = 1 thread)

-
-
-
-
- -
- -

{megabytesToHuman(user!.crRam)} available

-

The maximum amount of memory allowed for this server in GB.

-
-
-
-
- -
- -

{megabytesToHuman(user!.crStorage)} available

-

The maximum amount of storage allowed for this server in GB.

-
-
-
+
+ + +
+ +

Character limits: a-z A-Z 0-9 _ - . and [Space].

+

Assign a name to your server for use in the Panel.

+
+
+ +
+ +

{user!.crCpu}% available

+

Assign CPU cores and threads to your server. (100% = 1 thread)

+
+
+ +
+ +

{megabytesToHuman(user!.crRam)} available

+

The maximum amount of memory allowed for this server in GB.

+
+
+ +
+ +

{megabytesToHuman(user!.crStorage)} available

+

The maximum amount of storage allowed for this server in GB.

+
+


diff --git a/resources/scripts/plugins/usePersistedState.ts b/resources/scripts/plugins/usePersistedState.ts index 007e3fcdea..b1920477f1 100644 --- a/resources/scripts/plugins/usePersistedState.ts +++ b/resources/scripts/plugins/usePersistedState.ts @@ -1,12 +1,16 @@ import { Dispatch, SetStateAction, useEffect, useState } from 'react'; -export function usePersistedState (key: string, defaultValue: S): [ S | undefined, Dispatch> ] { +export function usePersistedState (key: string, defaultValue: S): [ S, Dispatch> ] { const [ state, setState ] = useState( () => { try { const item = localStorage.getItem(key); - return JSON.parse(item || (String(defaultValue))); + if (item === null) { + return defaultValue; + } + + return JSON.parse(item || String(defaultValue)); } catch (e) { console.warn('Failed to retrieve persisted value from store.', e); diff --git a/resources/scripts/plugins/useUserPersistedState.ts b/resources/scripts/plugins/useUserPersistedState.ts new file mode 100644 index 0000000000..7e0fe2c5a0 --- /dev/null +++ b/resources/scripts/plugins/useUserPersistedState.ts @@ -0,0 +1,9 @@ +import { useStoreState } from 'easy-peasy'; +import { usePersistedState } from '@/plugins/usePersistedState'; +import { Dispatch, SetStateAction } from 'react'; + +export default (key: string, defaultValue: S): [ S, Dispatch> ] => { + const uuid = useStoreState(state => state.user.data!.uuid); + + return usePersistedState(`${uuid}:${key}`, defaultValue); +}; diff --git a/resources/scripts/plugins/useWindowDimensions.ts b/resources/scripts/plugins/useWindowDimensions.ts new file mode 100644 index 0000000000..ff0539ffec --- /dev/null +++ b/resources/scripts/plugins/useWindowDimensions.ts @@ -0,0 +1,22 @@ +import { useState, useEffect } from 'react'; + +function getWindowDimensions () { + const { innerWidth: width, innerHeight: height } = window; + return { width, height }; +} + +export default function useWindowDimensions () { + const [ windowDimensions, setWindowDimensions ] = useState(getWindowDimensions()); + + useEffect(() => { + function handleResize () { + setWindowDimensions(getWindowDimensions()); + } + + window.addEventListener('resize', handleResize); + + return () => window.removeEventListener('resize', handleResize); + }, []); + + return windowDimensions; +} diff --git a/resources/scripts/routers/DashboardRouter.tsx b/resources/scripts/routers/DashboardRouter.tsx index 942c7c7073..b964e21c0f 100644 --- a/resources/scripts/routers/DashboardRouter.tsx +++ b/resources/scripts/routers/DashboardRouter.tsx @@ -1,37 +1,151 @@ import React from 'react'; -import { Route, RouteComponentProps, Switch } from 'react-router-dom'; +import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom'; import AccountOverviewContainer from '@/components/dashboard/AccountOverviewContainer'; import DashboardContainer from '@/components/dashboard/DashboardContainer'; import AccountApiContainer from '@/components/dashboard/AccountApiContainer'; import SecurityKeyContainer from '@/components/dashboard/SecurityKeyContainer'; import { NotFound } from '@/components/elements/ScreenBlock'; import TransitionRouter from '@/TransitionRouter'; -import SidePanel from '@/components/SidePanel'; import tw from 'twin.macro'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faLayerGroup, faLock, faSignOutAlt, faSitemap, faUser, faCog, faStore } from '@fortawesome/free-solid-svg-icons'; +import Sidebar from '@/components/elements/Sidebar'; +import { useStoreState } from '@/state/hooks'; +import { ApplicationStore } from '@/state'; +import { CSSTransition } from 'react-transition-group'; +import { State } from 'easy-peasy'; +import http from '@/api/http'; +import useWindowDimensions from '@/plugins/useWindowDimensions'; +import StaticSubNavigation from '@/components/elements/StaticSubNavigation'; +import Spinner from '@/components/elements/Spinner'; -export default ({ location }: RouteComponentProps) => ( -
- -
- - - - - - - - - - - - - - - - - - - +const DashboardRouter = ({ location }: RouteComponentProps) => { + const { width } = useWindowDimensions(); + + const avatarURL = useStoreState((state: State) => state.user.data!.avatarURL); + const name = useStoreState((state: State) => state.settings.data!.name); + const email = useStoreState((state: State) => state.user.data!.email); + const crBalance = useStoreState((state: State) => state.user.data!.crBalance); + const rootAdmin = useStoreState((state: State) => state.user.data!.rootAdmin); + const storeEnabled = useStoreState((state: State) => state.settings.data!.store.enabled); + + const onTriggerLogout = () => { + http.post('/auth/logout').finally(() => { + // @ts-ignore + window.location = '/'; + }); + }; + + return ( +
+ +
+

{name}

+
+ + {location.pathname.endsWith('/') ? + Dashboard - Servers + : location.pathname.endsWith('/account') ? + Dashboard - Account + : location.pathname.endsWith('/account/api') ? + Dashboard - API + : location.pathname.endsWith('/account/security') ? + Dashboard - Security + : + + } + + Servers + + + Account + + + API + + + Security + + {storeEnabled && + + Store + + } + {rootAdmin && + + Admin + + } + + + Logout + + + {avatarURL && + Profile Picture + } +
+ {email} + {crBalance} credits +
+
+
+
+ {width < 768 && + + +
+ + + + + + + + + + + + + {storeEnabled && + + + + } + {rootAdmin && + + + + } + + + +
+
+
+ } + + + + + + + + + + + + + + + + + + + +
-
-); + ); +}; + +export default DashboardRouter; diff --git a/resources/scripts/routers/ServerRouter.tsx b/resources/scripts/routers/ServerRouter.tsx index 7a74db7f0d..9f18eea47e 100644 --- a/resources/scripts/routers/ServerRouter.tsx +++ b/resources/scripts/routers/ServerRouter.tsx @@ -4,12 +4,12 @@ import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom'; import ServerConsole from '@/components/server/ServerConsole'; import TransitionRouter from '@/TransitionRouter'; import WebsocketHandler from '@/components/server/WebsocketHandler'; -import SidePanel from '@/components/SidePanel'; import { ServerContext } from '@/state/server'; import DatabasesContainer from '@/components/server/databases/DatabasesContainer'; import FileManagerContainer from '@/components/server/files/FileManagerContainer'; import { CSSTransition } from 'react-transition-group'; import FileEditContainer from '@/components/server/files/FileEditContainer'; +import AuditLogsContainer from '@/components/server/auditlogs/AuditLogsContainer'; import SettingsContainer from '@/components/server/settings/SettingsContainer'; import ScheduleContainer from '@/components/server/schedules/ScheduleContainer'; import ScheduleEditContainer from '@/components/server/schedules/ScheduleEditContainer'; @@ -18,10 +18,11 @@ import Can from '@/components/elements/Can'; import BackupContainer from '@/components/server/backups/BackupContainer'; import Spinner from '@/components/elements/Spinner'; import ScreenBlock, { NotFound, ServerError } from '@/components/elements/ScreenBlock'; -import { httpErrorToHuman } from '@/api/http'; -import { useStoreState } from 'easy-peasy'; +import http, { httpErrorToHuman } from '@/api/http'; +import { State, useStoreState } from 'easy-peasy'; import SubNavigation from '@/components/elements/SubNavigation'; import NetworkContainer from '@/components/server/network/NetworkContainer'; +import Sidebar from '@/components/elements/Sidebar'; import InstallListener from '@/components/server/InstallListener'; import StartupContainer from '@/components/server/startup/StartupContainer'; import ErrorBoundary from '@/components/elements/ErrorBoundary'; @@ -33,16 +34,24 @@ import { faDatabase, faExternalLinkAlt, faFolder, + faLayerGroup, + faLock, faPlay, + faScroll, + faSignOutAlt, faSitemap, faTerminal, faUser, + faStore, + faHome, } from '@fortawesome/free-solid-svg-icons'; import RequireServerPermission from '@/hoc/RequireServerPermission'; import ServerInstallSvg from '@/assets/images/server_installing.svg'; import ServerRestoreSvg from '@/assets/images/server_restore.svg'; import ServerErrorSvg from '@/assets/images/server_error.svg'; import tw from 'twin.macro'; +import { ApplicationStore } from '@/state'; +import useWindowDimensions from '@/plugins/useWindowDimensions'; const ConflictStateRenderer = () => { const status = ServerContext.useStoreState(state => state.server.data?.status || null); @@ -72,8 +81,8 @@ const ConflictStateRenderer = () => { }; const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => { - const rootAdmin = useStoreState(state => state.user.data!.rootAdmin); const [ error, setError ] = useState(''); + const { width } = useWindowDimensions(); const id = ServerContext.useStoreState(state => state.server.data?.id); const uuid = ServerContext.useStoreState(state => state.server.data?.uuid); @@ -81,6 +90,19 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) const serverId = ServerContext.useStoreState(state => state.server.data?.internalId); const getServer = ServerContext.useStoreActions(actions => actions.server.getServer); const clearServerState = ServerContext.useStoreActions(actions => actions.clearServerState); + const avatarURL = useStoreState((state: State) => state.user.data!.avatarURL); + const rootAdmin = useStoreState(state => state.user.data!.rootAdmin); + const name = useStoreState((state: State) => state.settings.data!.name); + const email = useStoreState((state: State) => state.user.data!.email); + const crBalance = useStoreState((state: State) => state.user.data!.crBalance); + const storeEnabled = useStoreState((state: State) => state.settings.data!.store.enabled); + + const onTriggerLogout = () => { + http.post('/auth/logout').finally(() => { + // @ts-ignore + window.location = '/'; + }); + }; useEffect(() => () => { clearServerState(); @@ -103,8 +125,72 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) return (
- -
+ +
+

{name}

+
+ + {location.pathname.endsWith(`/server/${id}`) ? + Server - Console + : location.pathname.startsWith(`/server/${id}/files`) ? + Server - Files + : location.pathname.startsWith(`/server/${id}/auditlogs`) ? + Server - Logs + : location.pathname.startsWith(`/server/${id}/databases`) ? + Server - Databases + : location.pathname.startsWith(`/server/${id}/schedules`) ? + Server - Tasks + : location.pathname.startsWith(`/server/${id}/users`) ? + Server - Subusers + : location.pathname.startsWith(`/server/${id}/backups`) ? + Server - Backups + : location.pathname.startsWith(`/server/${id}/network`) ? + Server - Network + : location.pathname.startsWith(`/server/${id}/startup`) ? + Server - Startup + : location.pathname.startsWith(`/server/${id}/settings`) ? + Server - Settings + : + + + } + + Servers + + + Account + + + API + + + Security + + {storeEnabled && + + Store + + } + {rootAdmin && + + Admin + + } + + + Logout + + + {avatarURL && + Profile Picture + } +
+ {email} + {crBalance} credits +
+
+
+
{(!uuid || !id) ? error ? @@ -115,6 +201,11 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
+ {width < 768 && + + + + } Console @@ -123,6 +214,11 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) Files + + + Logs + + Databases @@ -186,6 +282,11 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) + + + + + diff --git a/resources/scripts/routers/StoreRouter.tsx b/resources/scripts/routers/StoreRouter.tsx index dd9b734a17..32903cc188 100644 --- a/resources/scripts/routers/StoreRouter.tsx +++ b/resources/scripts/routers/StoreRouter.tsx @@ -1,21 +1,131 @@ import React from 'react'; -import { Route, RouteComponentProps, Switch } from 'react-router-dom'; +import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom'; import TransitionRouter from '@/TransitionRouter'; -import SidePanel from '@/components/SidePanel'; import tw from 'twin.macro'; import StoreContainer from '@/components/store/StoreContainer'; import CreateServerContainer from '@/components/store/servers/CreateServerContainer'; +import { useStoreState } from '@/state/hooks'; +import { State } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; +import Sidebar from '@/components/elements/Sidebar'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faLayerGroup, faLock, faSignOutAlt, faSitemap, faUser, faCog, faStore } from '@fortawesome/free-solid-svg-icons'; +import http from '@/api/http'; +import useWindowDimensions from '@/plugins/useWindowDimensions'; +import StaticSubNavigation from '@/components/elements/StaticSubNavigation'; +import { CSSTransition } from 'react-transition-group'; +import Spinner from '@/components/elements/Spinner'; -export default ({ location, match }: RouteComponentProps) => ( -
- -
- - - - - - +const StoreRouter = ({ location, match }: RouteComponentProps) => { + const { width } = useWindowDimensions(); + + const avatarURL = useStoreState((state: State) => state.user.data!.avatarURL); + const name = useStoreState((state: State) => state.settings.data!.name); + const email = useStoreState((state: State) => state.user.data!.email); + const crBalance = useStoreState((state: State) => state.user.data!.crBalance); + const rootAdmin = useStoreState((state: State) => state.user.data!.rootAdmin); + const storeEnabled = useStoreState((state: State) => state.settings.data!.store.enabled); + + const onTriggerLogout = () => { + http.post('/auth/logout').finally(() => { + // @ts-ignore + window.location = '/'; + }); + }; + + return ( +
+ +
+

{name}

+
+ + {location.pathname.endsWith('/store') ? + Store - Home + : location.pathname.endsWith('/store/servers/new') ? + Store - New Server + : + + } + + Servers + + + Account + + + API + + + Security + + {storeEnabled && + + Store + + } + {rootAdmin && + + Admin + + } + + + Logout + + + {avatarURL && + Profile Picture + } +
+ {email} + {crBalance} credits +
+
+
+
+ {width < 768 && + + +
+ + + + + + + + + + + + + {storeEnabled && + + + + } + {rootAdmin && + + + + } + + + +
+
+
+ } + + + + + + +
-
-); + ); +}; + +export default StoreRouter; diff --git a/resources/scripts/state/server/subusers.ts b/resources/scripts/state/server/subusers.ts index 7f8a6ef064..6a40c02783 100644 --- a/resources/scripts/state/server/subusers.ts +++ b/resources/scripts/state/server/subusers.ts @@ -8,7 +8,8 @@ export type SubuserPermission = 'allocation.read' | 'allocation.update' | 'startup.read' | 'startup.update' | 'database.create' | 'database.read' | 'database.update' | 'database.delete' | 'database.view_password' | - 'schedule.create' | 'schedule.read' | 'schedule.update' | 'schedule.delete' + 'schedule.create' | 'schedule.read' | 'schedule.update' | 'schedule.delete' | + 'audit-logs.read' ; export interface Subuser { diff --git a/resources/scripts/state/user.ts b/resources/scripts/state/user.ts index b13450e9e5..a71df634e1 100644 --- a/resources/scripts/state/user.ts +++ b/resources/scripts/state/user.ts @@ -9,6 +9,7 @@ export interface UserData { language: string; rootAdmin: boolean; useTotp: boolean; + avatarURL: string; crBalance: number; crSlots: number; crCpu: number; diff --git a/resources/views/admin/billing.blade.php b/resources/views/admin/billing.blade.php new file mode 100644 index 0000000000..00a5a9fb91 --- /dev/null +++ b/resources/views/admin/billing.blade.php @@ -0,0 +1,43 @@ +@extends('layouts.admin') + +@section('title') + Billing Config +@endsection + +@section('content-header') +

Billing ConfigConfigure the billing system.

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

Billing System

+
+ +
+
+
+ + +

When enabled, users will be able to purchase credits.

+
+
+
+ + +
+
+
+@endsection diff --git a/resources/views/admin/settings/secret.blade.php b/resources/views/admin/settings/secret.blade.php deleted file mode 100644 index b7789c2600..0000000000 --- a/resources/views/admin/settings/secret.blade.php +++ /dev/null @@ -1,47 +0,0 @@ -@extends('layouts.admin') -@include('partials/admin.settings.nav', ['activeTab' => 'secret']) - -@section('title') - Super Secret Settings -@endsection - -@section('content-header') -

Secret SettingsConfigure some super secret settings.

- -@endsection - -@section('content') - @yield('settings::nav') -
-
-
-
-

Panel Settings

-
-
-
-
-
- -
- -

If enabled, the loading bar will be rainbow!

-
-
-
-
- -
-
-
-
-@endsection diff --git a/resources/views/admin/users/store.blade.php b/resources/views/admin/users/store.blade.php index 1eac84b8b9..d51684d110 100644 --- a/resources/views/admin/users/store.blade.php +++ b/resources/views/admin/users/store.blade.php @@ -35,6 +35,10 @@
+
+ + +
diff --git a/resources/views/layouts/admin.blade.php b/resources/views/layouts/admin.blade.php index e2811a6a4c..1959b2736b 100644 --- a/resources/views/layouts/admin.blade.php +++ b/resources/views/layouts/admin.blade.php @@ -171,7 +171,7 @@ {{ $appVersion }}
{{ round(microtime(true) - LARAVEL_START, 3) }}s
- Copyright © {{ date('Y') }} Jexactyl. + Copyright © {{ date('Y') }} Jexactyl built with Pterodactyl.
@section('footer-scripts') diff --git a/routes/admin.php b/routes/admin.php index 04d5960cd3..5d1513cf49 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -65,10 +65,8 @@ */ Route::group(['prefix' => 'settings'], function () { Route::get('/', 'Settings\IndexController@index')->name('admin.settings'); - Route::get('/secret', 'Settings\SecretController@index')->name('admin.settings.secret'); Route::get('/mail', 'Settings\MailController@index')->name('admin.settings.mail'); Route::get('/advanced', 'Settings\AdvancedController@index')->name('admin.settings.advanced'); - Route::get('/advanced', 'Settings\AdvancedController@index')->name('admin.settings.advanced'); Route::post('/mail/test', 'Settings\MailController@test')->name('admin.settings.mail.test'); Route::patch('/', 'Settings\IndexController@update'); @@ -256,3 +254,16 @@ Route::patch('/', 'Credits\ConfigController@update')->name('admin.credits.update'); Route::patch('/store', 'Credits\StoreController@update')->name('admin.credits.store.update'); }); + +/* +|-------------------------------------------------------------------------- +| Billing System Controller Routes +|-------------------------------------------------------------------------- +| +| Endpoint: /admin/billing +| +*/ +Route::group(['prefix' => 'billing'], function () { + Route::get('/', 'BillingController@index')->name('admin.billing'); + Route::patch('/', 'BillingController@update')->name('admin.billing.update'); +}); diff --git a/routes/api-client.php b/routes/api-client.php index e91f4ae320..84249802b4 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -75,6 +75,10 @@ Route::get('/upload', 'Servers\FileUploadController'); }); + Route::group(['prefix' => '/auditlogs'], function () { + Route::get('/', 'Servers\AuditLogsController@index'); + }); + Route::group(['prefix' => '/schedules'], function () { Route::get('/', 'Servers\ScheduleController@index'); Route::post('/', 'Servers\ScheduleController@store'); diff --git a/settings.json.example b/settings.json.example deleted file mode 100644 index 5d1d9c0b26..0000000000 --- a/settings.json.example +++ /dev/null @@ -1,5 +0,0 @@ -{ - "userRegistration": { - "enabled": true - } -} \ No newline at end of file diff --git a/tests/Integration/Api/Application/Nests/NestControllerTest.php b/tests/Integration/Api/Application/Nests/NestControllerTest.php index 58434ec4cc..df54618b96 100644 --- a/tests/Integration/Api/Application/Nests/NestControllerTest.php +++ b/tests/Integration/Api/Application/Nests/NestControllerTest.php @@ -46,8 +46,8 @@ public function testNestResponse() 'data' => [], 'meta' => [ 'pagination' => [ - 'total' => 4, - 'count' => 4, + 'total' => 1, + 'count' => 1, 'per_page' => 50, 'current_page' => 1, 'total_pages' => 1, diff --git a/tests/Integration/Api/Client/Server/Startup/GetStartupAndVariablesTest.php b/tests/Integration/Api/Client/Server/Startup/GetStartupAndVariablesTest.php deleted file mode 100644 index 09a6abd029..0000000000 --- a/tests/Integration/Api/Client/Server/Startup/GetStartupAndVariablesTest.php +++ /dev/null @@ -1,69 +0,0 @@ -generateTestAccount($permissions); - - $egg = $this->cloneEggAndVariables($server->egg); - // BUNGEE_VERSION should never be returned back to the user in this API call, either in - // the array of variables, or revealed in the startup command. - $egg->variables()->first()->update([ - 'user_viewable' => false, - ]); - - $server->fill([ - 'egg_id' => $egg->id, - 'startup' => 'java {{SERVER_JARFILE}} --version {{BUNGEE_VERSION}}', - ])->save(); - $server = $server->refresh(); - - $response = $this->actingAs($user)->getJson($this->link($server) . '/startup'); - - $response->assertOk(); - $response->assertJsonPath('meta.startup_command', 'java bungeecord.jar --version [hidden]'); - $response->assertJsonPath('meta.raw_startup_command', $server->startup); - - $response->assertJsonPath('object', 'list'); - $response->assertJsonCount(1, 'data'); - $response->assertJsonPath('data.0.object', EggVariable::RESOURCE_NAME); - $this->assertJsonTransformedWith($response->json('data.0.attributes'), $egg->variables[1]); - } - - /** - * Test that a user without the required permission, or who does not have any permission to - * access the server cannot get the startup information for it. - */ - public function testStartupDataIsNotReturnedWithoutPermission() - { - [$user, $server] = $this->generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]); - $this->actingAs($user)->getJson($this->link($server) . '/startup')->assertForbidden(); - - $user2 = User::factory()->create(); - $this->actingAs($user2)->getJson($this->link($server) . '/startup')->assertNotFound(); - } - - /** - * @return array[] - */ - public function permissionsDataProvider() - { - return [[[]], [[Permission::ACTION_STARTUP_READ]]]; - } -} diff --git a/tests/Integration/Api/Client/Server/Startup/UpdateStartupVariableTest.php b/tests/Integration/Api/Client/Server/Startup/UpdateStartupVariableTest.php deleted file mode 100644 index 0e5e421c1f..0000000000 --- a/tests/Integration/Api/Client/Server/Startup/UpdateStartupVariableTest.php +++ /dev/null @@ -1,160 +0,0 @@ -generateTestAccount($permissions); - $server->fill([ - 'startup' => 'java {{SERVER_JARFILE}} --version {{BUNGEE_VERSION}}', - ])->save(); - - $response = $this->actingAs($user)->putJson($this->link($server) . '/startup/variable', [ - 'key' => 'BUNGEE_VERSION', - 'value' => '1.2.3', - ]); - - $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); - $response->assertJsonPath('errors.0.code', 'ValidationException'); - $response->assertJsonPath('errors.0.detail', 'The value may only contain letters and numbers.'); - - $response = $this->actingAs($user)->putJson($this->link($server) . '/startup/variable', [ - 'key' => 'BUNGEE_VERSION', - 'value' => '123', - ]); - - $response->assertOk(); - $response->assertJsonPath('object', EggVariable::RESOURCE_NAME); - $this->assertJsonTransformedWith($response->json('attributes'), $server->variables[0]); - $response->assertJsonPath('meta.startup_command', 'java bungeecord.jar --version 123'); - $response->assertJsonPath('meta.raw_startup_command', $server->startup); - } - - /** - * Test that variables that are either not user_viewable, or not user_editable, cannot be - * updated via this endpoint. - * - * @dataProvider permissionsDataProvider - */ - public function testStartupVariableCannotBeUpdatedIfNotUserViewableOrEditable(array $permissions) - { - /** @var \Pterodactyl\Models\Server $server */ - [$user, $server] = $this->generateTestAccount($permissions); - - $egg = $this->cloneEggAndVariables($server->egg); - $egg->variables()->where('env_variable', 'BUNGEE_VERSION')->update(['user_viewable' => false]); - $egg->variables()->where('env_variable', 'SERVER_JARFILE')->update(['user_editable' => false]); - - $server->fill(['egg_id' => $egg->id])->save(); - $server->refresh(); - - $response = $this->actingAs($user)->putJson($this->link($server) . '/startup/variable', [ - 'key' => 'BUNGEE_VERSION', - 'value' => '123', - ]); - - $response->assertStatus(Response::HTTP_BAD_REQUEST); - $response->assertJsonPath('errors.0.code', 'BadRequestHttpException'); - $response->assertJsonPath('errors.0.detail', 'The environment variable you are trying to edit does not exist.'); - - $response = $this->actingAs($user)->putJson($this->link($server) . '/startup/variable', [ - 'key' => 'SERVER_JARFILE', - 'value' => 'server2.jar', - ]); - - $response->assertStatus(Response::HTTP_BAD_REQUEST); - $response->assertJsonPath('errors.0.code', 'BadRequestHttpException'); - $response->assertJsonPath('errors.0.detail', 'The environment variable you are trying to edit is read-only.'); - } - - /** - * Test that a hidden variable is not included in the startup_command output for the server if - * a different variable is updated. - */ - public function testHiddenVariablesAreNotReturnedInStartupCommandWhenUpdatingVariable() - { - /** @var \Pterodactyl\Models\Server $server */ - [$user, $server] = $this->generateTestAccount(); - - $egg = $this->cloneEggAndVariables($server->egg); - $egg->variables()->first()->update(['user_viewable' => false]); - - $server->fill([ - 'egg_id' => $egg->id, - 'startup' => 'java {{SERVER_JARFILE}} --version {{BUNGEE_VERSION}}', - ])->save(); - - $server->refresh(); - - $response = $this->actingAs($user)->putJson($this->link($server) . '/startup/variable', [ - 'key' => 'SERVER_JARFILE', - 'value' => 'server2.jar', - ]); - - $response->assertOk(); - $response->assertJsonPath('meta.startup_command', 'java server2.jar --version [hidden]'); - $response->assertJsonPath('meta.raw_startup_command', $server->startup); - } - - /** - * Test that an egg variable with a validation rule of 'nullable|string' works if no value - * is passed through in the request. - * - * @see https://github.com/pterodactyl/panel/issues/2433 - */ - public function testEggVariableWithNullableStringIsNotRequired() - { - /** @var \Pterodactyl\Models\Server $server */ - [$user, $server] = $this->generateTestAccount(); - - $egg = $this->cloneEggAndVariables($server->egg); - $egg->variables()->first()->update(['rules' => 'nullable|string']); - - $server->fill(['egg_id' => $egg->id])->save(); - $server->refresh(); - - $response = $this->actingAs($user)->putJson($this->link($server) . '/startup/variable', [ - 'key' => 'BUNGEE_VERSION', - 'value' => '', - ]); - - $response->assertOk(); - $response->assertJsonPath('attributes.server_value', null); - } - - /** - * Test that a variable cannot be updated if the user does not have permission to perform - * that action, or they aren't assigned at all to the server. - */ - public function testStartupVariableCannotBeUpdatedIfNotUserViewable() - { - [$user, $server] = $this->generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]); - $this->actingAs($user)->putJson($this->link($server) . '/startup/variable')->assertForbidden(); - - $user2 = User::factory()->create(); - $this->actingAs($user2)->putJson($this->link($server) . '/startup/variable')->assertNotFound(); - } - - /** - * @return \array[][] - */ - public function permissionsDataProvider() - { - return [[[]], [[Permission::ACTION_STARTUP_UPDATE]]]; - } -} diff --git a/tests/Integration/Services/Servers/BuildModificationServiceTest.php b/tests/Integration/Services/Servers/BuildModificationServiceTest.php deleted file mode 100644 index cadc9276dd..0000000000 --- a/tests/Integration/Services/Servers/BuildModificationServiceTest.php +++ /dev/null @@ -1,258 +0,0 @@ -daemonServerRepository = $this->mock(DaemonServerRepository::class); - } - - /** - * Test that allocations can be added and removed from a server. Only the allocations on the - * current node and belonging to this server should be modified. - */ - public function testAllocationsCanBeModifiedForTheServer() - { - $server = $this->createServerModel(); - $server2 = $this->createServerModel(); - - /** @var \Pterodactyl\Models\Allocation[] $allocations */ - $allocations = Allocation::factory()->times(4)->create(['node_id' => $server->node_id, 'notes' => 'Random notes']); - - $initialAllocationId = $server->allocation_id; - $allocations[0]->update(['server_id' => $server->id, 'notes' => 'Test notes']); - - // Some additional test allocations for the other server, not the server we are attempting - // to modify. - $allocations[2]->update(['server_id' => $server2->id]); - $allocations[3]->update(['server_id' => $server2->id]); - - $this->daemonServerRepository->expects('setServer->sync')->andReturnUndefined(); - - $response = $this->getService()->handle($server, [ - // Attempt to add one new allocation, and an allocation assigned to another server. The - // other server allocation should be ignored, and only the allocation for this server should - // be used. - 'add_allocations' => [$allocations[2]->id, $allocations[1]->id], - // Remove the default server allocation, ensuring that the new allocation passed through - // in the data becomes the default allocation. - 'remove_allocations' => [$server->allocation_id, $allocations[0]->id, $allocations[3]->id], - ]); - - $this->assertInstanceOf(Server::class, $response); - - // Only one allocation should exist for this server now. - $this->assertCount(1, $response->allocations); - $this->assertSame($allocations[1]->id, $response->allocation_id); - $this->assertNull($response->allocation->notes); - - // These two allocations should not have been touched. - $this->assertDatabaseHas('allocations', ['id' => $allocations[2]->id, 'server_id' => $server2->id]); - $this->assertDatabaseHas('allocations', ['id' => $allocations[3]->id, 'server_id' => $server2->id]); - - // Both of these allocations should have been removed from the server, and have had their - // notes properly reset. - $this->assertDatabaseHas('allocations', ['id' => $initialAllocationId, 'server_id' => null, 'notes' => null]); - $this->assertDatabaseHas('allocations', ['id' => $allocations[0]->id, 'server_id' => null, 'notes' => null]); - } - - /** - * Test that an exception is thrown if removing the default allocation without also assigning - * new allocations to the server. - */ - public function testExceptionIsThrownIfRemovingTheDefaultAllocation() - { - $server = $this->createServerModel(); - /** @var \Pterodactyl\Models\Allocation[] $allocations */ - $allocations = Allocation::factory()->times(4)->create(['node_id' => $server->node_id]); - - $allocations[0]->update(['server_id' => $server->id]); - - $this->expectException(DisplayException::class); - $this->expectExceptionMessage('You are attempting to delete the default allocation for this server but there is no fallback allocation to use.'); - - $this->getService()->handle($server, [ - 'add_allocations' => [], - 'remove_allocations' => [$server->allocation_id, $allocations[0]->id], - ]); - } - - /** - * Test that the build data for the server is properly passed along to the Wings instance so that - * the server data is updated in realtime. This test also ensures that only certain fields get updated - * for the server, and not just any arbitrary field. - */ - public function testServerBuildDataIsProperlyUpdatedOnWings() - { - $server = $this->createServerModel(); - - $this->daemonServerRepository->expects('setServer')->with(Mockery::on(function (Server $s) use ($server) { - return $s->id === $server->id; - }))->andReturnSelf(); - - $this->daemonServerRepository->expects('sync')->withNoArgs()->andReturnUndefined(); - - $response = $this->getService()->handle($server, [ - 'oom_disabled' => false, - 'memory' => 256, - 'swap' => 128, - 'io' => 600, - 'cpu' => 150, - 'threads' => '1,2', - 'disk' => 1024, - 'backup_limit' => null, - 'database_limit' => 10, - 'allocation_limit' => 20, - ]); - - $this->assertFalse($response->oom_disabled); - $this->assertSame(256, $response->memory); - $this->assertSame(128, $response->swap); - $this->assertSame(600, $response->io); - $this->assertSame(150, $response->cpu); - $this->assertSame('1,2', $response->threads); - $this->assertSame(1024, $response->disk); - $this->assertSame(0, $response->backup_limit); - $this->assertSame(10, $response->database_limit); - $this->assertSame(20, $response->allocation_limit); - } - - /** - * Test that an exception when connecting to the Wings instance is properly ignored - * when making updates. This allows for a server to be modified even when the Wings - * node is offline. - */ - public function testConnectionExceptionIsIgnoredWhenUpdatingServerSettings() - { - $server = $this->createServerModel(); - - $this->daemonServerRepository->expects('setServer->sync')->andThrows( - new DaemonConnectionException( - new RequestException('Bad request', new Request('GET', '/test'), new Response()) - ) - ); - - $response = $this->getService()->handle($server, ['memory' => 256, 'disk' => 10240]); - - $this->assertInstanceOf(Server::class, $response); - $this->assertSame(256, $response->memory); - $this->assertSame(10240, $response->disk); - - $this->assertDatabaseHas('servers', ['id' => $response->id, 'memory' => 256, 'disk' => 10240]); - } - - /** - * Test that no exception is thrown if we are only removing an allocation. - */ - public function testNoExceptionIsThrownIfOnlyRemovingAllocation() - { - $server = $this->createServerModel(); - /** @var \Pterodactyl\Models\Allocation $allocation */ - $allocation = Allocation::factory()->create(['node_id' => $server->node_id, 'server_id' => $server->id]); - - $this->daemonServerRepository->expects('setServer->sync')->andReturnUndefined(); - - $this->getService()->handle($server, [ - 'remove_allocations' => [$allocation->id], - ]); - - $this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => null]); - } - - /** - * Test that allocations in both the add and remove arrays are only added, and not removed. - * This scenario wouldn't really happen in the UI, but it is possible to perform via the API - * so we want to make sure that the logic being used doesn't break if the allocation exists - * in both arrays. - * - * We'll default to adding the allocation in this case. - */ - public function testAllocationInBothAddAndRemoveIsAdded() - { - $server = $this->createServerModel(); - /** @var \Pterodactyl\Models\Allocation $allocation */ - $allocation = Allocation::factory()->create(['node_id' => $server->node_id]); - - $this->daemonServerRepository->expects('setServer->sync')->andReturnUndefined(); - - $this->getService()->handle($server, [ - 'add_allocations' => [$allocation->id], - 'remove_allocations' => [$allocation->id], - ]); - - $this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => $server->id]); - } - - /** - * Test that using the same allocation ID multiple times in the array does not cause an error. - */ - public function testUsingSameAllocationIdMultipleTimesDoesNotError() - { - $server = $this->createServerModel(); - /** @var \Pterodactyl\Models\Allocation $allocation */ - $allocation = Allocation::factory()->create(['node_id' => $server->node_id, 'server_id' => $server->id]); - /** @var \Pterodactyl\Models\Allocation $allocation2 */ - $allocation2 = Allocation::factory()->create(['node_id' => $server->node_id]); - - $this->daemonServerRepository->expects('setServer->sync')->andReturnUndefined(); - - $this->getService()->handle($server, [ - 'add_allocations' => [$allocation2->id, $allocation2->id], - 'remove_allocations' => [$allocation->id, $allocation->id], - ]); - - $this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => null]); - $this->assertDatabaseHas('allocations', ['id' => $allocation2->id, 'server_id' => $server->id]); - } - - /** - * Test that any changes we made to the server or allocations are rolled back if there is an - * exception while performing any action. This is different than the connection exception - * test which should properly ignore connection issues. We want any other type of exception - * to properly be thrown back to the caller. - */ - public function testThatUpdatesAreRolledBackIfExceptionIsEncountered() - { - $server = $this->createServerModel(); - /** @var \Pterodactyl\Models\Allocation $allocation */ - $allocation = Allocation::factory()->create(['node_id' => $server->node_id]); - - $this->daemonServerRepository->expects('setServer->sync')->andThrows(new DisplayException('Test')); - - $this->expectException(DisplayException::class); - - $this->getService()->handle($server, ['add_allocations' => [$allocation->id]]); - - $this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => null]); - } - - /** - * @return \Pterodactyl\Services\Servers\BuildModificationService - */ - private function getService() - { - return $this->app->make(BuildModificationService::class); - } -} diff --git a/tests/Integration/Services/Servers/ServerCreationServiceTest.php b/tests/Integration/Services/Servers/ServerCreationServiceTest.php deleted file mode 100644 index 329e3be1d1..0000000000 --- a/tests/Integration/Services/Servers/ServerCreationServiceTest.php +++ /dev/null @@ -1,210 +0,0 @@ -daemonServerRepository = Mockery::mock(DaemonServerRepository::class); - $this->swap(DaemonServerRepository::class, $this->daemonServerRepository); - } - - /** - * Test that a server can be created when a deployment object is provided to the service. - * - * This doesn't really do anything super complicated, we'll rely on other more specific - * tests to cover that the logic being used does indeed find suitable nodes and ports. For - * this test we just care that it is recognized and passed off to those functions. - */ - public function testServerIsCreatedWithDeploymentObject() - { - /** @var \Pterodactyl\Models\User $user */ - $user = User::factory()->create(); - - /** @var \Pterodactyl\Models\Location $location */ - $location = Location::factory()->create(); - - /** @var \Pterodactyl\Models\Node $node */ - $node = Node::factory()->create([ - 'location_id' => $location->id, - ]); - - /** @var \Pterodactyl\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations */ - $allocations = Allocation::factory()->times(5)->create([ - 'node_id' => $node->id, - ]); - - $deployment = (new DeploymentObject())->setDedicated(true)->setLocations([$node->location_id])->setPorts([ - $allocations[0]->port, - ]); - - $egg = $this->cloneEggAndVariables(Egg::query()->findOrFail(1)); - // We want to make sure that the validator service runs as an admin, and not as a regular - // user when saving variables. - $egg->variables()->first()->update([ - 'user_editable' => false, - ]); - - $data = [ - 'name' => $this->faker->name, - 'description' => $this->faker->sentence, - 'owner_id' => $user->id, - 'memory' => 256, - 'swap' => 128, - 'disk' => 100, - 'io' => 500, - 'cpu' => 0, - 'startup' => 'java server2.jar', - 'image' => 'java:8', - 'egg_id' => $egg->id, - 'allocation_additional' => [ - $allocations[4]->id, - ], - 'environment' => [ - 'BUNGEE_VERSION' => '123', - 'SERVER_JARFILE' => 'server2.jar', - ], - 'start_on_completion' => true, - ]; - - $this->daemonServerRepository->expects('setServer->create')->with(true)->andReturnUndefined(); - - try { - $this->getService()->handle(array_merge($data, [ - 'environment' => [ - 'BUNGEE_VERSION' => '', - 'SERVER_JARFILE' => 'server2.jar', - ], - ]), $deployment); - - $this->fail('This execution pathway should not be reached.'); - } catch (ValidationException $exception) { - $this->assertCount(1, $exception->errors()); - $this->assertArrayHasKey('environment.BUNGEE_VERSION', $exception->errors()); - $this->assertSame('The Bungeecord Version variable field is required.', $exception->errors()['environment.BUNGEE_VERSION'][0]); - } - - $response = $this->getService()->handle($data, $deployment); - - $this->assertInstanceOf(Server::class, $response); - $this->assertNotNull($response->uuid); - $this->assertSame($response->uuidShort, substr($response->uuid, 0, 8)); - $this->assertSame($egg->id, $response->egg_id); - $this->assertCount(2, $response->variables); - $this->assertSame('123', $response->variables[0]->server_value); - $this->assertSame('server2.jar', $response->variables[1]->server_value); - - foreach ($data as $key => $value) { - if (in_array($key, ['allocation_additional', 'environment', 'start_on_completion'])) { - continue; - } - - $this->assertSame($value, $response->{$key}, "Failed asserting equality of '$key' in server response. Got: [{$response->{$key}}] Expected: [$value]"); - } - - $this->assertCount(2, $response->allocations); - $this->assertSame($response->allocation_id, $response->allocations[0]->id); - $this->assertSame($allocations[0]->id, $response->allocations[0]->id); - $this->assertSame($allocations[4]->id, $response->allocations[1]->id); - - $this->assertFalse($response->isSuspended()); - $this->assertTrue($response->oom_disabled); - $this->assertSame(0, $response->database_limit); - $this->assertSame(0, $response->allocation_limit); - $this->assertSame(0, $response->backup_limit); - } - - /** - * Test that a server is deleted from the Panel if Wings returns an error during the creation - * process. - */ - public function testErrorEncounteredByWingsCausesServerToBeDeleted() - { - /** @var \Pterodactyl\Models\User $user */ - $user = User::factory()->create(); - - /** @var \Pterodactyl\Models\Location $location */ - $location = Location::factory()->create(); - - /** @var \Pterodactyl\Models\Node $node */ - $node = Node::factory()->create([ - 'location_id' => $location->id, - ]); - - /** @var \Pterodactyl\Models\Allocation $allocation */ - $allocation = Allocation::factory()->create([ - 'node_id' => $node->id, - ]); - - $data = [ - 'name' => $this->faker->name, - 'description' => $this->faker->sentence, - 'owner_id' => $user->id, - 'allocation_id' => $allocation->id, - 'node_id' => $allocation->node_id, - 'memory' => 256, - 'swap' => 128, - 'disk' => 100, - 'io' => 500, - 'cpu' => 0, - 'startup' => 'java server2.jar', - 'image' => 'java:8', - 'egg_id' => 1, - 'environment' => [ - 'BUNGEE_VERSION' => '123', - 'SERVER_JARFILE' => 'server2.jar', - ], - ]; - - $this->daemonServerRepository->expects('setServer->create')->andThrows( - new DaemonConnectionException( - new BadResponseException('Bad request', new Request('POST', '/create'), new Response(500)) - ) - ); - - $this->daemonServerRepository->expects('setServer->delete')->andReturnUndefined(); - - $this->expectException(DaemonConnectionException::class); - - $this->getService()->handle($data); - - $this->assertDatabaseMissing('servers', ['owner_id' => $user->id]); - } - - /** - * @return \Pterodactyl\Services\Servers\ServerCreationService - */ - private function getService() - { - return $this->app->make(ServerCreationService::class); - } -} diff --git a/tests/Integration/Services/Servers/ServerDeletionServiceTest.php b/tests/Integration/Services/Servers/ServerDeletionServiceTest.php deleted file mode 100644 index 197e6b38f8..0000000000 --- a/tests/Integration/Services/Servers/ServerDeletionServiceTest.php +++ /dev/null @@ -1,167 +0,0 @@ -set('logging.default', 'null'); - - $this->daemonServerRepository = Mockery::mock(DaemonServerRepository::class); - $this->databaseManagementService = Mockery::mock(DatabaseManagementService::class); - - $this->app->instance(DaemonServerRepository::class, $this->daemonServerRepository); - $this->app->instance(DatabaseManagementService::class, $this->databaseManagementService); - } - - /** - * Reset the log driver. - */ - protected function tearDown(): void - { - config()->set('logging.default', self::$defaultLogger); - self::$defaultLogger = null; - - parent::tearDown(); - } - - /** - * Test that a server is not deleted if the force option is not set and an error - * is returned by wings. - */ - public function testRegularDeleteFailsIfWingsReturnsError() - { - $server = $this->createServerModel(); - - $this->expectException(DaemonConnectionException::class); - - $this->daemonServerRepository->expects('setServer->delete')->withNoArgs()->andThrows( - new DaemonConnectionException(new BadResponseException('Bad request', new Request('GET', '/test'), new Response())) - ); - - $this->getService()->handle($server); - - $this->assertDatabaseHas('servers', ['id' => $server->id]); - } - - /** - * Test that a 404 from Wings while deleting a server does not cause the deletion to fail. - */ - public function testRegularDeleteIgnores404FromWings() - { - $server = $this->createServerModel(); - - $this->daemonServerRepository->expects('setServer->delete')->withNoArgs()->andThrows( - new DaemonConnectionException(new BadResponseException('Bad request', new Request('GET', '/test'), new Response(404))) - ); - - $this->getService()->handle($server); - - $this->assertDatabaseMissing('servers', ['id' => $server->id]); - } - - /** - * Test that an error from Wings does not cause the deletion to fail if the server is being - * force deleted. - */ - public function testForceDeleteIgnoresExceptionFromWings() - { - $server = $this->createServerModel(); - - $this->daemonServerRepository->expects('setServer->delete')->withNoArgs()->andThrows( - new DaemonConnectionException(new BadResponseException('Bad request', new Request('GET', '/test'), new Response(500))) - ); - - $this->getService()->withForce(true)->handle($server); - - $this->assertDatabaseMissing('servers', ['id' => $server->id]); - } - - /** - * Test that a non-force-delete call does not delete the server if one of the databases - * cannot be deleted from the host. - */ - public function testExceptionWhileDeletingStopsProcess() - { - $server = $this->createServerModel(); - $host = DatabaseHost::factory()->create(); - - /** @var \Pterodactyl\Models\Database $db */ - $db = Database::factory()->create(['database_host_id' => $host->id, 'server_id' => $server->id]); - - $server->refresh(); - - $this->daemonServerRepository->expects('setServer->delete')->withNoArgs()->andReturnUndefined(); - $this->databaseManagementService->expects('delete')->with(Mockery::on(function ($value) use ($db) { - return $value instanceof Database && $value->id === $db->id; - }))->andThrows(new Exception()); - - $this->expectException(Exception::class); - $this->getService()->handle($server); - - $this->assertDatabaseHas('servers', ['id' => $server->id]); - $this->assertDatabaseHas('databases', ['id' => $db->id]); - } - - /** - * Test that a server is deleted even if the server databases cannot be deleted from the host. - */ - public function testExceptionWhileDeletingDatabasesDoesNotAbortIfForceDeleted() - { - $server = $this->createServerModel(); - $host = DatabaseHost::factory()->create(); - - /** @var \Pterodactyl\Models\Database $db */ - $db = Database::factory()->create(['database_host_id' => $host->id, 'server_id' => $server->id]); - - $server->refresh(); - - $this->daemonServerRepository->expects('setServer->delete')->withNoArgs()->andReturnUndefined(); - $this->databaseManagementService->expects('delete')->with(Mockery::on(function ($value) use ($db) { - return $value instanceof Database && $value->id === $db->id; - }))->andThrows(new Exception()); - - $this->getService()->withForce(true)->handle($server); - - $this->assertDatabaseMissing('servers', ['id' => $server->id]); - $this->assertDatabaseMissing('databases', ['id' => $db->id]); - } - - /** - * @return \Pterodactyl\Services\Servers\ServerDeletionService - */ - private function getService() - { - return $this->app->make(ServerDeletionService::class); - } -} diff --git a/tests/Integration/Services/Servers/StartupModificationServiceTest.php b/tests/Integration/Services/Servers/StartupModificationServiceTest.php deleted file mode 100644 index 8f61e1a182..0000000000 --- a/tests/Integration/Services/Servers/StartupModificationServiceTest.php +++ /dev/null @@ -1,173 +0,0 @@ -createServerModel(['egg_id' => 1]); - - try { - $this->app->make(StartupModificationService::class)->handle($server, [ - 'egg_id' => $server->egg_id + 1, - 'environment' => [ - 'BUNGEE_VERSION' => '$$', - 'SERVER_JARFILE' => 'server.jar', - ], - ]); - - $this->assertTrue(false, 'This assertion should not be called.'); - } catch (Exception $exception) { - $this->assertInstanceOf(ValidationException::class, $exception); - - /** @var \Illuminate\Validation\ValidationException $exception */ - $errors = $exception->validator->errors()->toArray(); - - $this->assertCount(1, $errors); - $this->assertArrayHasKey('environment.BUNGEE_VERSION', $errors); - $this->assertCount(1, $errors['environment.BUNGEE_VERSION']); - $this->assertSame('The Bungeecord Version variable may only contain letters and numbers.', $errors['environment.BUNGEE_VERSION'][0]); - } - - ServerVariable::query()->where('variable_id', $server->variables[1]->id)->delete(); - - $result = $this->getService() - ->handle($server, [ - 'egg_id' => $server->egg_id + 1, - 'startup' => 'random gibberish', - 'environment' => [ - 'BUNGEE_VERSION' => '1234', - 'SERVER_JARFILE' => 'test.jar', - ], - ]); - - $this->assertInstanceOf(Server::class, $result); - $this->assertCount(2, $result->variables); - $this->assertSame($server->startup, $result->startup); - $this->assertSame('1234', $result->variables[0]->server_value); - $this->assertSame('test.jar', $result->variables[1]->server_value); - } - - /** - * Test that modifying an egg as an admin properly updates the data for the server. - */ - public function testServerIsProperlyModifiedAsAdminUser() - { - /** @var \Pterodactyl\Models\Egg $nextEgg */ - $nextEgg = Nest::query()->findOrFail(2)->eggs()->firstOrFail(); - - $server = $this->createServerModel(['egg_id' => 1]); - - $this->assertNotSame($nextEgg->id, $server->egg_id); - $this->assertNotSame($nextEgg->nest_id, $server->nest_id); - - $response = $this->getService() - ->setUserLevel(User::USER_LEVEL_ADMIN) - ->handle($server, [ - 'egg_id' => $nextEgg->id, - 'startup' => 'sample startup', - 'skip_scripts' => true, - 'docker_image' => 'docker/hodor', - ]); - - $this->assertInstanceOf(Server::class, $response); - $this->assertSame($nextEgg->id, $response->egg_id); - $this->assertSame($nextEgg->nest_id, $response->nest_id); - $this->assertSame('sample startup', $response->startup); - $this->assertSame('docker/hodor', $response->image); - $this->assertTrue($response->skip_scripts); - // Make sure we don't revert back to a lurking bug that causes servers to get marked - // as not installed when you modify the startup... - $this->assertTrue($response->isInstalled()); - } - - /** - * Test that hidden variables can be updated by an admin but are not affected by a - * regular user who attempts to pass them through. - */ - public function testEnvironmentVariablesCanBeUpdatedByAdmin() - { - $server = $this->createServerModel(); - $server->loadMissing(['egg', 'variables']); - - $clone = $this->cloneEggAndVariables($server->egg); - // This makes the BUNGEE_VERSION variable not user editable. - $clone->variables()->first()->update([ - 'user_editable' => false, - ]); - - $server->fill(['egg_id' => $clone->id])->saveOrFail(); - $server->refresh(); - - ServerVariable::query()->updateOrCreate([ - 'server_id' => $server->id, - 'variable_id' => $server->variables[0]->id, - ], ['variable_value' => 'EXIST']); - - $response = $this->getService()->handle($server, [ - 'environment' => [ - 'BUNGEE_VERSION' => '1234', - 'SERVER_JARFILE' => 'test.jar', - ], - ]); - - $this->assertCount(2, $response->variables); - $this->assertSame('EXIST', $response->variables[0]->server_value); - $this->assertSame('test.jar', $response->variables[1]->server_value); - - $response = $this->getService() - ->setUserLevel(User::USER_LEVEL_ADMIN) - ->handle($server, [ - 'environment' => [ - 'BUNGEE_VERSION' => '1234', - 'SERVER_JARFILE' => 'test.jar', - ], - ]); - - $this->assertCount(2, $response->variables); - $this->assertSame('1234', $response->variables[0]->server_value); - $this->assertSame('test.jar', $response->variables[1]->server_value); - } - - /** - * Test that passing an invalid egg ID into the function throws an exception - * rather than silently failing or skipping. - */ - public function testInvalidEggIdTriggersException() - { - $server = $this->createServerModel(); - - $this->expectException(ModelNotFoundException::class); - - $this->getService() - ->setUserLevel(User::USER_LEVEL_ADMIN) - ->handle($server, ['egg_id' => 123456789]); - } - - /** - * @return \Pterodactyl\Services\Servers\StartupModificationService - */ - private function getService() - { - return $this->app->make(StartupModificationService::class); - } -} diff --git a/tests/Integration/Services/Servers/SuspensionServiceTest.php b/tests/Integration/Services/Servers/SuspensionServiceTest.php deleted file mode 100644 index 258a8ca2ec..0000000000 --- a/tests/Integration/Services/Servers/SuspensionServiceTest.php +++ /dev/null @@ -1,76 +0,0 @@ -repository = Mockery::mock(DaemonServerRepository::class); - $this->app->instance(DaemonServerRepository::class, $this->repository); - } - - public function testServerIsSuspendedAndUnsuspended() - { - $server = $this->createServerModel(); - - $this->repository->expects('setServer->sync')->twice()->andReturnSelf(); - - $this->getService()->toggle($server, SuspensionService::ACTION_SUSPEND); - - $this->assertTrue($server->refresh()->isSuspended()); - - $this->getService()->toggle($server, SuspensionService::ACTION_UNSUSPEND); - - $this->assertFalse($server->refresh()->isSuspended()); - } - - public function testNoActionIsTakenIfSuspensionStatusIsUnchanged() - { - $server = $this->createServerModel(); - - $this->getService()->toggle($server, SuspensionService::ACTION_UNSUSPEND); - - $server->refresh(); - $this->assertFalse($server->isSuspended()); - - $server->update(['status' => Server::STATUS_SUSPENDED]); - $this->getService()->toggle($server, SuspensionService::ACTION_SUSPEND); - - $server->refresh(); - $this->assertTrue($server->isSuspended()); - } - - public function testExceptionIsThrownIfInvalidActionsArePassed() - { - $server = $this->createServerModel(); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Expected one of: "suspend", "unsuspend". Got: "foo"'); - - $this->getService()->toggle($server, 'foo'); - } - - /** - * @return \Pterodactyl\Services\Servers\SuspensionService - */ - private function getService() - { - return $this->app->make(SuspensionService::class); - } -} diff --git a/tests/Integration/Services/Servers/VariableValidatorServiceTest.php b/tests/Integration/Services/Servers/VariableValidatorServiceTest.php deleted file mode 100644 index 01f6f7f020..0000000000 --- a/tests/Integration/Services/Servers/VariableValidatorServiceTest.php +++ /dev/null @@ -1,136 +0,0 @@ -cloneEggAndVariables(Egg::query()->findOrFail(1)); - - try { - $this->getService()->handle($egg->id, [ - 'BUNGEE_VERSION' => '1.2.3', - ]); - - $this->assertTrue(false, 'This statement should not be reached.'); - } catch (ValidationException $exception) { - $errors = $exception->errors(); - - $this->assertCount(2, $errors); - $this->assertArrayHasKey('environment.BUNGEE_VERSION', $errors); - $this->assertArrayHasKey('environment.SERVER_JARFILE', $errors); - $this->assertSame('The Bungeecord Version variable may only contain letters and numbers.', $errors['environment.BUNGEE_VERSION'][0]); - $this->assertSame('The Bungeecord Jar File variable field is required.', $errors['environment.SERVER_JARFILE'][0]); - } - - $response = $this->getService()->handle($egg->id, [ - 'BUNGEE_VERSION' => '1234', - 'SERVER_JARFILE' => 'server.jar', - ]); - - $this->assertInstanceOf(Collection::class, $response); - $this->assertCount(2, $response); - $this->assertSame('BUNGEE_VERSION', $response->get(0)->key); - $this->assertSame('1234', $response->get(0)->value); - $this->assertSame('SERVER_JARFILE', $response->get(1)->key); - $this->assertSame('server.jar', $response->get(1)->value); - } - - /** - * Test that variables that are user_editable=false do not get validated (or returned) by - * the handler. - */ - public function testNormalUserCannotValidateNonUserEditableVariables() - { - /** @noinspection PhpParamsInspection */ - $egg = $this->cloneEggAndVariables(Egg::query()->findOrFail(1)); - $egg->variables()->first()->update([ - 'user_editable' => false, - ]); - - $response = $this->getService()->handle($egg->id, [ - // This is an invalid value, but it shouldn't cause any issues since it should be skipped. - 'BUNGEE_VERSION' => '1.2.3', - 'SERVER_JARFILE' => 'server.jar', - ]); - - $this->assertInstanceOf(Collection::class, $response); - $this->assertCount(1, $response); - $this->assertSame('SERVER_JARFILE', $response->get(0)->key); - $this->assertSame('server.jar', $response->get(0)->value); - } - - public function testEnvironmentVariablesCanBeUpdatedAsAdmin() - { - /** @noinspection PhpParamsInspection */ - $egg = $this->cloneEggAndVariables(Egg::query()->findOrFail(1)); - $egg->variables()->first()->update([ - 'user_editable' => false, - ]); - - try { - $this->getService()->setUserLevel(User::USER_LEVEL_ADMIN)->handle($egg->id, [ - 'BUNGEE_VERSION' => '1.2.3', - 'SERVER_JARFILE' => 'server.jar', - ]); - - $this->assertTrue(false, 'This statement should not be reached.'); - } catch (ValidationException $exception) { - $this->assertCount(1, $exception->errors()); - $this->assertArrayHasKey('environment.BUNGEE_VERSION', $exception->errors()); - } - - $response = $this->getService()->setUserLevel(User::USER_LEVEL_ADMIN)->handle($egg->id, [ - 'BUNGEE_VERSION' => '123', - 'SERVER_JARFILE' => 'server.jar', - ]); - - $this->assertInstanceOf(Collection::class, $response); - $this->assertCount(2, $response); - $this->assertSame('BUNGEE_VERSION', $response->get(0)->key); - $this->assertSame('123', $response->get(0)->value); - $this->assertSame('SERVER_JARFILE', $response->get(1)->key); - $this->assertSame('server.jar', $response->get(1)->value); - } - - public function testNullableEnvironmentVariablesCanBeUsedCorrectly() - { - /** @noinspection PhpParamsInspection */ - $egg = $this->cloneEggAndVariables(Egg::query()->findOrFail(1)); - $egg->variables()->where('env_variable', '!=', 'BUNGEE_VERSION')->delete(); - - $egg->variables()->update(['rules' => 'nullable|string']); - - $response = $this->getService()->handle($egg->id, []); - $this->assertCount(1, $response); - $this->assertNull($response->get(0)->value); - - $response = $this->getService()->handle($egg->id, ['BUNGEE_VERSION' => null]); - $this->assertCount(1, $response); - $this->assertNull($response->get(0)->value); - - $response = $this->getService()->handle($egg->id, ['BUNGEE_VERSION' => '']); - $this->assertCount(1, $response); - $this->assertSame('', $response->get(0)->value); - } - - /** - * @return \Pterodactyl\Services\Servers\VariableValidatorService - */ - private function getService() - { - return $this->app->make(VariableValidatorService::class); - } -} diff --git a/yarn.lock b/yarn.lock index 135d5e9878..7d33261052 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1760,6 +1760,14 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -1771,6 +1779,11 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" +arg@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.1.tgz#eb0c9a8f77786cad2af8ff2b862899842d7b6adb" + integrity sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -2298,6 +2311,14 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chart.js@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.8.0.tgz#b703b10d0f4ec5079eaefdcd6ca32dc8f826e0e9" @@ -2371,6 +2392,21 @@ chokidar@^3.4.2: optionalDependencies: fsevents "~2.3.1" +chokidar@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" + integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chownr@^1.0.1, chownr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" @@ -2474,6 +2510,14 @@ color-string@^1.5.4: color-name "^1.0.0" simple-swizzle "^0.2.2" +color-string@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa" + integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + color@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" @@ -2482,6 +2526,14 @@ color@^3.1.3: color-convert "^1.9.1" color-string "^1.5.4" +color@^4.0.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.0.tgz#0c782459a3e98838ea01e4bc0fb43310ca35af78" + integrity sha512-hHTcrbvEnGjC7WBMk6ibQWFVDgEFTVmjrz2Q5HlU6ltwxv0JJN2Z8I7uRbWeQLF04dikxs8zgyZkazRJvSMtyQ== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + colorette@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" @@ -2496,6 +2548,11 @@ commander@^6.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== +commander@^8.0.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -2627,6 +2684,17 @@ cosmiconfig@^6.0.0: path-type "^4.0.0" yaml "^1.7.2" +cosmiconfig@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + create-ecdh@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" @@ -2710,6 +2778,11 @@ css-color-keywords@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" +css-color-names@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" + integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= + css-loader@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.2.1.tgz#62849b45a414b7bde0bfba17325a026471040eae" @@ -2942,6 +3015,11 @@ didyoumean@^1.2.1: resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.1.tgz#e92edfdada6537d484d73c0172fd1eba0c4976ff" integrity sha1-6S7f2tplN9SE1zwBcv0eugxJdv8= +didyoumean@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" + integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -2957,6 +3035,11 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +dlv@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" + integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== + dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" @@ -3555,6 +3638,17 @@ fast-glob@^3.1.1: micromatch "^4.0.2" picomatch "^2.2.1" +fast-glob@^3.2.7: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" @@ -3783,6 +3877,15 @@ from2@^2.1.0: inherits "^2.0.1" readable-stream "^2.0.0" +fs-extra@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" + integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^9.0.0, fs-extra@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" @@ -3836,7 +3939,7 @@ fsevents@~2.1.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== -fsevents@~2.3.1: +fsevents@~2.3.1, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -3904,13 +4007,20 @@ glob-parent@^5.0.0, glob-parent@~5.1.0: dependencies: is-glob "^4.0.1" -glob-parent@^5.1.0: +glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + glob@^7.0.0: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -3946,6 +4056,18 @@ glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.7: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-modules@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" @@ -4130,6 +4252,11 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.0" +hex-color-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" + integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== + history@^4.9.0: version "4.9.0" resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca" @@ -4187,6 +4314,16 @@ hpack.js@^2.1.6: readable-stream "^2.0.1" wbuf "^1.1.0" +hsl-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" + integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4= + +hsla-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" + integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= + html-entities@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44" @@ -4506,6 +4643,18 @@ is-callable@^1.2.3: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== +is-color-stop@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" + integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U= + dependencies: + css-color-names "^0.0.4" + hex-color-regex "^1.1.0" + hsl-regex "^1.0.0" + hsla-regex "^1.0.0" + rgb-regex "^1.0.1" + rgba-regex "^1.0.0" + is-core-module@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" @@ -4599,6 +4748,13 @@ is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + is-negative-zero@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" @@ -4831,6 +4987,11 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lilconfig@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082" + integrity sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA== + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" @@ -4929,6 +5090,11 @@ lodash.toarray@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" +lodash.topath@^4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/lodash.topath/-/lodash.topath-4.5.2.tgz#3616351f3bba61994a0931989660bd03254fd009" + integrity sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak= + lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" @@ -4943,6 +5109,11 @@ lodash@^4.17.19, lodash@^4.17.20: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + loglevel@^1.6.8: version "1.6.8" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" @@ -5063,7 +5234,7 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.2: +micromatch@^4.0.2, micromatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== @@ -5240,6 +5411,11 @@ modern-normalize@^1.0.0: resolved "https://registry.yarnpkg.com/modern-normalize/-/modern-normalize-1.0.0.tgz#539d84a1e141338b01b346f3e27396d0ed17601e" integrity sha512-1lM+BMLGuDfsdwf3rsgBSrxJwAZHFIrQ8YR61xIqdHo0uNKI9M52wNpHSrliZATJp51On6JD0AfRxd4YGSU0lw== +modern-normalize@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/modern-normalize/-/modern-normalize-1.1.0.tgz#da8e80140d9221426bd4f725c6e11283d34f90b7" + integrity sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA== + moment@^2.10.2: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" @@ -5283,6 +5459,11 @@ nanoid@^3.1.20: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== +nanoid@^3.1.30: + version "3.2.0" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" + integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== + nanomatch@^1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.9.tgz#879f7150cb2dab7a471259066c104eee6e0fa7c2" @@ -5324,6 +5505,13 @@ nice-try@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4" +node-emoji@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" + integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== + dependencies: + lodash "^4.17.21" + node-emoji@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.8.1.tgz#6eec6bfb07421e2148c75c6bba72421f8530a826" @@ -5471,6 +5659,11 @@ object-hash@^2.0.3: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea" integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg== +object-hash@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + object-inspect@^1.10.3, object-inspect@^1.9.0: version "1.10.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369" @@ -5812,6 +6005,11 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + picomatch@^2.0.4, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" @@ -5903,6 +6101,14 @@ postcss-js@^3.0.3: camelcase-css "^2.0.1" postcss "^8.1.6" +postcss-load-config@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.1.tgz#2f53a17f2f543d9e63864460af42efdac0d41f87" + integrity sha512-c/9XYboIbSEUZpiD1UQD0IKiUe8n9WHYV7YFe7X7J+ZwCsEKkUJSFWjS9hBU1RR9THR7jMXst8sxiqP0jjo2mg== + dependencies: + lilconfig "^2.0.4" + yaml "^1.10.2" + postcss-modules-extract-imports@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" @@ -5932,6 +6138,13 @@ postcss-modules-values@^3.0.0: icss-utils "^4.0.0" postcss "^7.0.6" +postcss-nested@5.0.6: + version "5.0.6" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-5.0.6.tgz#466343f7fc8d3d46af3e7dba3fcd47d052a945bc" + integrity sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA== + dependencies: + postcss-selector-parser "^6.0.6" + postcss-nested@^5.0.1: version "5.0.3" resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-5.0.3.tgz#2f46d77a06fc98d9c22344fd097396f5431386db" @@ -5957,6 +6170,14 @@ postcss-selector-parser@^6.0.4: uniq "^1.0.1" util-deprecate "^1.0.2" +postcss-selector-parser@^6.0.6: + version "6.0.8" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.8.tgz#f023ed7a9ea736cd7ef70342996e8e78645a7914" + integrity sha512-D5PG53d209Z1Uhcc0qAZ5U3t5HagH3cxu+WLZ22jt3gLUpXM4eXXfiO14jiDWST3NNooX/E8wISfOhZ9eIjGTQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-value-parser@^3.3.0: version "3.3.1" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" @@ -5995,6 +6216,15 @@ postcss@^8.1.6, postcss@^8.1.8, postcss@^8.2.1: nanoid "^3.1.20" source-map "^0.6.1" +postcss@^8.3.5: + version "8.4.5" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95" + integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg== + dependencies: + nanoid "^3.1.30" + picocolors "^1.0.0" + source-map-js "^1.0.1" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -6103,6 +6333,16 @@ purgecss@^3.1.3: postcss "^8.2.1" postcss-selector-parser "^6.0.2" +purgecss@^4.0.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-4.1.3.tgz#683f6a133c8c4de7aa82fe2746d1393b214918f7" + integrity sha512-99cKy4s+VZoXnPxaoM23e5ABcP851nC2y2GROkkjS8eJaJtlciGavd7iYAw2V84WeBqggZ12l8ef44G99HmTaw== + dependencies: + commander "^8.0.0" + glob "^7.1.7" + postcss "^8.3.5" + postcss-selector-parser "^6.0.6" + qr.js@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f" @@ -6146,6 +6386,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" @@ -6375,6 +6620,13 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + reaptcha@^1.7.2: version "1.7.2" resolved "https://registry.yarnpkg.com/reaptcha/-/reaptcha-1.7.2.tgz#d829f54270c241f46501e92a5a7badeb1fcf372d" @@ -6388,6 +6640,14 @@ reduce-css-calc@^2.1.6: css-unit-converter "^1.1.1" postcss-value-parser "^3.3.0" +reduce-css-calc@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz#7ef8761a28d614980dc0c982f772c93f7a99de03" + integrity sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg== + dependencies: + css-unit-converter "^1.1.1" + postcss-value-parser "^3.3.0" + redux-devtools-extension@^2.13.8: version "2.13.8" resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz#37b982688626e5e4993ff87220c9bbb7cd2d96e1" @@ -6599,13 +6859,23 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rgb-regex@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" + integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE= + +rgba-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" + integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= + rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" dependencies: glob "^7.1.3" -rimraf@^3.0.2: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -6922,6 +7192,11 @@ source-list-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" +source-map-js@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf" + integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA== + source-map-loader@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-1.0.1.tgz#703df5345b0816734f0336c1ccee8af66e082061" @@ -7299,7 +7574,7 @@ table@^6.0.9: string-width "^4.2.0" strip-ansi "^6.0.0" -tailwindcss@^2.0.1, tailwindcss@^2.0.2: +tailwindcss@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-2.0.2.tgz#28e1573d29dd4547b26782facb05bcfaa92be366" integrity sha512-nO9JRE1pO7SF9RnYAl6g7uzeHdrmKAFqNjT9NtZUfxqimJZAOOLOEyIEUiMq12+xIc7mC2Ey3Vf90XjHpWKfbw== @@ -7325,6 +7600,44 @@ tailwindcss@^2.0.1, tailwindcss@^2.0.2: reduce-css-calc "^2.1.6" resolve "^1.19.0" +tailwindcss@^2.2.7: + version "2.2.19" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-2.2.19.tgz#540e464832cd462bb9649c1484b0a38315c2653c" + integrity sha512-6Ui7JSVtXadtTUo2NtkBBacobzWiQYVjYW0ZnKaP9S1ZCKQ0w7KVNz+YSDI/j7O7KCMHbOkz94ZMQhbT9pOqjw== + dependencies: + arg "^5.0.1" + bytes "^3.0.0" + chalk "^4.1.2" + chokidar "^3.5.2" + color "^4.0.1" + cosmiconfig "^7.0.1" + detective "^5.2.0" + didyoumean "^1.2.2" + dlv "^1.1.3" + fast-glob "^3.2.7" + fs-extra "^10.0.0" + glob-parent "^6.0.1" + html-tags "^3.1.0" + is-color-stop "^1.1.0" + is-glob "^4.0.1" + lodash "^4.17.21" + lodash.topath "^4.5.2" + modern-normalize "^1.1.0" + node-emoji "^1.11.0" + normalize-path "^3.0.0" + object-hash "^2.2.0" + postcss-js "^3.0.3" + postcss-load-config "^3.1.0" + postcss-nested "5.0.6" + postcss-selector-parser "^6.0.6" + postcss-value-parser "^4.1.0" + pretty-hrtime "^1.0.3" + purgecss "^4.0.3" + quick-lru "^5.1.1" + reduce-css-calc "^2.1.8" + resolve "^1.20.0" + tmp "^0.2.1" + tapable@^1.0.0, tapable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" @@ -7433,6 +7746,13 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.2.tgz#1dfae771ee1a04396bdfde27a3adcebc6b648b28" +tmp@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + to-arraybuffer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" @@ -7643,6 +7963,11 @@ universalify@^1.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -8071,6 +8396,11 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@^1.10.0, yaml@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + yaml@^1.7.2: version "1.10.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"