Merge pull request #4 from pterodactyl/release/v1.0.1

Release/v1.0.1
This commit is contained in:
Caleb 2020-10-13 15:28:25 -04:00 committed by GitHub
commit b725e45ed1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
187 changed files with 3717 additions and 5821 deletions

View file

@ -3,6 +3,49 @@ This file is a running track of new features and fixes to each version of the pa
This project follows [Semantic Versioning](http://semver.org) guidelines.
## v1.0.0
Pterodactyl 1.0 represents the culmination of over two years of work, almost 2,000 commits, endless bug and feature requests, and a dream that
has been in the making since 2013. 🎉
Due to the sheer size and timeline of this release I've massively truncated the listing below. There are numerous smaller
bug fixes and changes that would simply be too difficult to keep track of here. Please feel free to browse through the releases
tab for this repository to see more specific changes that have been made.
### Added
* Adds a new client-facing API allowing a user to control all aspects of their individual servers, or servers
which they have been granted access to as a subuser.
* Adds the ability for backups to be created for a server both manually and via a scheduled task.
* Adds the ability for users to modify their server allocations on the fly and include notes for each allocation.
* Adds the ability for users to generate recovery tokens for 2FA protected logins which can be used in place of
a code should their device be inaccessible.
* Adds support for transfering servers between Nodes via the Panel.
* Adds the ability to assign specific CPU cores to a server (CPU Pinning) process.
* Server owners can now reinstall their assigned server egg automatically with a button on the frontend.
### Changed
* The entire user frontend has been replaced with a responsive, React backed design implemented using Tailwind CSS.
* Replaces a large amount of complex daemon authentication logic by funneling most API calls through the Panel, and using
JSON Web Tokens where necessary to handle one-time direct authentication with Wings.
* Frontend server listing now includes a toggle to show or hide servers which an administrator has access to, rather
than always showing all servers on the system when logged into an admin account.
* We've replaced Ace Editor on the frontend with a better solution to allow lighter builds and more end-user functionality.
* Server permissions have been overhauled to be both easier to understand in the codebase, and allows plugins to better
hook into the permission system.
### Removed
* Removes large swaths of code complexity and confusing interface designs that caused a lot of pain to new developers
trying to jump into the codebase. We've simplified this to stick to more established Laravel design standards to make
it easy to parse through the project and make contributions.
## v0.7.19 (Derelict Dermodactylus)
### Fixed
* **[Security]** Fixes XSS in the admin area's server owner selection.
## v0.7.18 (Derelict Dermodactylus)
### Fixed
* **[Security]** Re-addressed missed endpoint that would not properly limit a user account to 5 API keys.
* **[Security]** Addresses a Client API vulnerability that would allow a user to list all servers on the system ([`GHSA-6888-7f3w-92jx`](https://github.com/pterodactyl/panel/security/advisories/GHSA-6888-7f3w-92jx))
## v0.7.17 (Derelict Dermodactylus)
### Fixed
* Limited accounts to 5 API keys at a time.
@ -301,7 +344,7 @@ the response from the server `GET` endpoint.
* Nest and Egg listings now show the associated ID in order to make API requests easier.
* Added star indicators to user listing in Admin CP to indicate users who are set as a root admin.
* Creating a new node will now requires a SSL connection if the Panel is configured to use SSL as well.
* Connector error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure.
* Socketio error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure.
* File manager now supports mass deletion option for files and folders.
* Support for CS:GO as a default service option selection.
* Support for GMOD as a default service option selection.
@ -431,7 +474,7 @@ the response from the server `GET` endpoint.
* Changed 2FA login process to be more secure. Previously authentication checking happened on the 2FA post page, now it happens prior and is passed along to the 2FA page to avoid storing any credentials.
### Added
* Connector error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure.
* Socketio error messages due to permissions are now rendered correctly in the UI rather than causing a silent failure.
## v0.7.0-beta.1 (Derelict Dermodactylus)
### Added

View file

@ -1,8 +1,10 @@
[![Logo Image](https://cdn.pterodactyl.io/logos/new/pterodactyl_logo.png)](https://pterodactyl.io)
[![Build status](https://img.shields.io/travis/pterodactyl/panel/develop.svg?style=flat-square)](https://travis-ci.org/pterodactyl/panel)
[![Codecov](https://img.shields.io/codecov/c/github/pterodactyl/panel/develop.svg?style=flat-square)](https://codecov.io/gh/Pterodactyl/Panel)
[![Discord](https://img.shields.io/discord/122900397965705216.svg?style=flat-square&label=Discord)](https://pterodactyl.io/discord)
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/pterodactyl/panel/tests?label=Tests&style=for-the-badge)
![Discord](https://img.shields.io/discord/122900397965705216?label=Discord&logo=Discord&logoColor=white&style=for-the-badge)
![GitHub Releases](https://img.shields.io/github/downloads/pterodactyl/panel/latest/total?style=for-the-badge)
![GitHub Pre-Releases](https://img.shields.io/github/downloads-pre/pterodactyl/panel/v1.0.0-rc.7/total?style=for-the-badge)
![GitHub contributors](https://img.shields.io/github/contributors/pterodactyl/panel?style=for-the-badge)
# Pterodactyl Panel
Pterodactyl is an open-source game server management panel built with PHP 7, React, and Go. Designed with security
@ -25,6 +27,7 @@ I would like to extend my sincere thanks to the following sponsors for helping f
| [**DedicatedMC**](https://dedicatedmc.io/) | DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance and giving you the best performance money can buy. |
| [**Skynode**](https://www.skynode.pro/) | Skynode provides blazing fast game servers along with a top-notch user experience. Whatever our clients are looking for, we're able to provide it! |
| [**XCORE-SERVER.de**](https://xcore-server.de/) | XCORE-SERVER.de offers High-End Servers for hosting and gaming since 2012. Fast, excellent and well-known for eSports Gaming. |
| [**RoyaleHosting**](https://royalehosting.net/) | Build your dreams and deploy them with RoyaleHostings reliable servers and network. Easy to use, provisioned in a couple of minutes. |
## Documentation
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)

View file

@ -0,0 +1,26 @@
<?php
namespace Pterodactyl\Console\Commands\Overrides;
use Pterodactyl\Console\RequiresDatabaseMigrations;
use Illuminate\Database\Console\Seeds\SeedCommand as BaseSeedCommand;
class SeedCommand extends BaseSeedCommand
{
use RequiresDatabaseMigrations;
/**
* Block someone from running this seed command if they have not completed the migration
* process.
*
* @return int
*/
public function handle()
{
if (!$this->hasCompletedMigrations()) {
return $this->showMigrationWarning();
}
return parent::handle();
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Pterodactyl\Console\Commands\Overrides;
use Pterodactyl\Console\RequiresDatabaseMigrations;
use Illuminate\Foundation\Console\UpCommand as BaseUpCommand;
class UpCommand extends BaseUpCommand
{
use RequiresDatabaseMigrations;
/**
* @return bool|int
*/
public function handle()
{
if (!$this->hasCompletedMigrations()) {
return $this->showMigrationWarning();
}
return parent::handle();
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Pterodactyl\Console;
/**
* @mixin \Illuminate\Console\Command
*/
trait RequiresDatabaseMigrations
{
/**
* Checks if the migrations have finished running by comparing the last migration file.
*
* @return bool
*/
protected function hasCompletedMigrations(): bool
{
/** @var \Illuminate\Database\Migrations\Migrator $migrator */
$migrator = $this->getLaravel()->make('migrator');
$files = $migrator->getMigrationFiles(database_path('migrations'));
if (! $migrator->repositoryExists()) {
return false;
}
if (array_diff(array_keys($files), $migrator->getRepository()->getRan())) {
return false;
}
return true;
}
/**
* Throw a massive error into the console to hopefully catch the users attention and get
* them to properly run the migrations rather than ignoring all of the other previous
* errors...
*
* @return int
*/
protected function showMigrationWarning(): int
{
$this->getOutput()->writeln("<options=bold>
| @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
| |
| Your database has not been properly migrated! |
| |
| @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |</>
You must run the following command to finish migrating your database:
<fg=green;options=bold>php artisan migrate --step --force</>
You will not be able to use Pterodactyl Panel as expected without fixing your
database state by running the command above.
");
$this->getOutput()->error("You must correct the error above before continuing.");
return 1;
}
}

View file

@ -42,18 +42,6 @@ interface DatabaseRepositoryInterface extends RepositoryInterface
*/
public function getDatabasesForHost(int $host, int $count = 25): LengthAwarePaginator;
/**
* Create a new database if it does not already exist on the host with
* the provided details.
*
* @param array $data
* @return \Pterodactyl\Models\Database
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException
*/
public function createIfNotExists(array $data): Database;
/**
* Create a new database on a given connection.
*

View file

@ -54,15 +54,4 @@ interface NodeRepositoryInterface extends RepositoryInterface
* @return \Illuminate\Support\Collection
*/
public function getNodesForServerCreation(): Collection;
/**
* Return the IDs of all nodes that exist in the provided locations and have the space
* available to support the additional disk and memory provided.
*
* @param array $locations
* @param int $disk
* @param int $memory
* @return \Illuminate\Support\LazyCollection
*/
public function getNodesWithResourceUse(array $locations, int $disk, int $memory): LazyCollection;
}

View file

@ -17,6 +17,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class Handler extends ExceptionHandler
{
@ -190,7 +191,7 @@ class Handler extends ExceptionHandler
$converted = self::convertToArray($exception)['errors'][0];
$converted['detail'] = $error;
$converted['meta'] = is_array($converted['meta']) ? array_merge($converted['meta'], $meta) : $meta;
$converted['meta'] = is_array($converted['meta'] ?? null) ? array_merge($converted['meta'], $meta) : $meta;
$response[] = $converted;
}
@ -217,7 +218,9 @@ class Handler extends ExceptionHandler
'status' => method_exists($exception, 'getStatusCode')
? strval($exception->getStatusCode())
: ($exception instanceof ValidationException ? '422' : '500'),
'detail' => 'An error was encountered while processing this request.',
'detail' => $exception instanceof HttpExceptionInterface
? $exception->getMessage()
: 'An unexpected error was encountered while processing this request, please try again.',
];
if ($exception instanceof ModelNotFoundException || $exception->getPrevious() instanceof ModelNotFoundException) {

View file

@ -2,10 +2,12 @@
namespace Pterodactyl\Http\Controllers\Admin;
use Ramsey\Uuid\Uuid;
use Illuminate\Http\Request;
use Pterodactyl\Models\Nest;
use Pterodactyl\Models\Mount;
use Pterodactyl\Models\Location;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Mounts\MountUpdateService;
use Pterodactyl\Http\Requests\Admin\MountFormRequest;
@ -37,21 +39,6 @@ class MountController extends Controller
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Mounts\MountCreationService
*/
protected $creationService;
/**
* @var \Pterodactyl\Services\Mounts\MountDeletionService
*/
protected $deletionService;
/**
* @var \Pterodactyl\Services\Mounts\MountUpdateService
*/
protected $updateService;
/**
* MountController constructor.
*
@ -59,26 +46,17 @@ class MountController extends Controller
* @param \Pterodactyl\Contracts\Repository\NestRepositoryInterface $nestRepository
* @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository
* @param \Pterodactyl\Repositories\Eloquent\MountRepository $repository
* @param \Pterodactyl\Services\Mounts\MountCreationService $creationService
* @param \Pterodactyl\Services\Mounts\MountDeletionService $deletionService
* @param \Pterodactyl\Services\Mounts\MountUpdateService $updateService
*/
public function __construct(
AlertsMessageBag $alert,
NestRepositoryInterface $nestRepository,
LocationRepositoryInterface $locationRepository,
MountRepository $repository,
MountCreationService $creationService,
MountDeletionService $deletionService,
MountUpdateService $updateService
MountRepository $repository
) {
$this->alert = $alert;
$this->nestRepository = $nestRepository;
$this->locationRepository = $locationRepository;
$this->repository = $repository;
$this->creationService = $creationService;
$this->deletionService = $deletionService;
$this->updateService = $updateService;
}
/**
@ -103,11 +81,8 @@ class MountController extends Controller
*/
public function view($id)
{
$nests = $this->nestRepository->all();
$nests->load('eggs');
$locations = $this->locationRepository->all();
$locations->load('nodes');
$nests = Nest::query()->with('eggs')->get();
$locations = Location::query()->with('nodes')->get();
return view('admin.mounts.view', [
'mount' => $this->repository->getWithRelations($id),
@ -126,7 +101,13 @@ class MountController extends Controller
*/
public function create(MountFormRequest $request)
{
$mount = $this->creationService->handle($request->normalize());
/** @var \Pterodactyl\Models\Mount $mount */
$model = (new Mount())->fill($request->validated());
$model->forceFill(['uuid' => Uuid::uuid4()->toString()]);
$model->saveOrFail();
$mount = $model->fresh();
$this->alert->success('Mount was created successfully.')->flash();
return redirect()->route('admin.mounts.view', $mount->id);
@ -147,7 +128,8 @@ class MountController extends Controller
return $this->delete($mount);
}
$this->updateService->handle($mount->id, $request->normalize());
$mount->forceFill($request->validated())->save();
$this->alert->success('Mount was updated successfully.')->flash();
return redirect()->route('admin.mounts.view', $mount->id);
@ -163,15 +145,9 @@ class MountController extends Controller
*/
public function delete(Mount $mount)
{
try {
$this->deletionService->handle($mount->id);
$mount->delete();
return redirect()->route('admin.mounts');
} catch (DisplayException $ex) {
$this->alert->danger($ex->getMessage())->flash();
}
return redirect()->route('admin.mounts.view', $mount->id);
return redirect()->route('admin.mounts');
}
/**
@ -188,11 +164,12 @@ class MountController extends Controller
]);
$eggs = $validatedData['eggs'] ?? [];
if (sizeof($eggs) > 0) {
$mount->eggs()->attach(array_map('intval', $eggs));
$this->alert->success('Mount was updated successfully.')->flash();
if (count($eggs) > 0) {
$mount->eggs()->attach($eggs);
}
$this->alert->success('Mount was updated successfully.')->flash();
return redirect()->route('admin.mounts.view', $mount->id);
}
@ -205,16 +182,15 @@ class MountController extends Controller
*/
public function addNodes(Request $request, Mount $mount)
{
$validatedData = $request->validate([
'nodes' => 'required|exists:nodes,id',
]);
$data = $request->validate(['nodes' => 'required|exists:nodes,id']);
$nodes = $validatedData['nodes'] ?? [];
if (sizeof($nodes) > 0) {
$mount->nodes()->attach(array_map('intval', $nodes));
$this->alert->success('Mount was updated successfully.')->flash();
$nodes = $data['nodes'] ?? [];
if (count($nodes) > 0) {
$mount->nodes()->attach($nodes);
}
$this->alert->success('Mount was updated successfully.')->flash();
return redirect()->route('admin.mounts.view', $mount->id);
}

View file

@ -1,15 +1,9 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Http\Controllers\Admin\Nests;
use Illuminate\View\View;
use Pterodactyl\Models\Egg;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller;
@ -81,14 +75,14 @@ class EggScriptController extends Controller
* Handle a request to update the installation script for an Egg.
*
* @param \Pterodactyl\Http\Requests\Admin\Egg\EggScriptFormRequest $request
* @param int $egg
* @param \Pterodactyl\Models\Egg $egg
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Egg\InvalidCopyFromException
*/
public function update(EggScriptFormRequest $request, int $egg): RedirectResponse
public function update(EggScriptFormRequest $request, Egg $egg): RedirectResponse
{
$this->installScriptService->handle($egg, $request->normalize());
$this->alert->success(trans('admin/nests.eggs.notices.script_updated'))->flash();

View file

@ -102,7 +102,7 @@ class EggShareController extends Controller
* Update an existing Egg using a new imported file.
*
* @param \Pterodactyl\Http\Requests\Admin\Egg\EggImportFormRequest $request
* @param int $egg
* @param \Pterodactyl\Models\Egg $egg
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
@ -110,7 +110,7 @@ class EggShareController extends Controller
* @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
*/
public function update(EggImportFormRequest $request, int $egg): RedirectResponse
public function update(EggImportFormRequest $request, Egg $egg): RedirectResponse
{
$this->updateImporterService->handle($egg, $request->file('import_file'));
$this->alert->success(trans('admin/nests.eggs.notices.updated_via_import'))->flash();

View file

@ -12,7 +12,9 @@ namespace Pterodactyl\Http\Controllers\Admin;
use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Mount;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\MountServer;
use Prologue\Alerts\AlertsMessageBag;
use GuzzleHttp\Exception\RequestException;
use Pterodactyl\Exceptions\DisplayException;
@ -251,7 +253,7 @@ class ServersController extends Controller
*/
public function reinstallServer(Server $server)
{
$this->reinstallService->reinstall($server);
$this->reinstallService->handle($server);
$this->alert->success(trans('admin/server.alerts.server_reinstalled'))->flash();
return redirect()->route('admin.servers.view.manage', $server->id);
@ -332,13 +334,18 @@ class ServersController extends Controller
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Illuminate\Validation\ValidationException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function saveStartup(Request $request, Server $server)
{
$this->startupModificationService->setUserLevel(User::USER_LEVEL_ADMIN);
$this->startupModificationService->handle($server, $request->except('_token'));
try {
$this->startupModificationService
->setUserLevel(User::USER_LEVEL_ADMIN)
->handle($server, $request->except('_token'));
} catch (DataValidationException $exception) {
throw new ValidationException($exception->validator);
}
$this->alert->success(trans('admin/server.alerts.startup_changed'))->flash();
return redirect()->route('admin.servers.view.startup', $server->id);
@ -356,7 +363,7 @@ class ServersController extends Controller
public function newDatabase(StoreServerDatabaseRequest $request, Server $server)
{
$this->databaseManagementService->create($server, [
'database' => $request->input('database'),
'database' => DatabaseManagementService::generateUniqueDatabaseName($request->input('database'), $server->id),
'remote' => $request->input('remote'),
'database_host_id' => $request->input('database_host_id'),
'max_connections' => $request->input('max_connections'),
@ -403,7 +410,7 @@ class ServersController extends Controller
['id', '=', $database],
]);
$this->databaseManagementService->delete($database->id);
$this->databaseManagementService->delete($database);
return response('', 204);
}
@ -412,12 +419,17 @@ class ServersController extends Controller
* Add a mount to a server.
*
* @param Server $server
* @param int $mount_id
* @param \Pterodactyl\Models\Mount $mount
*
* @return \Illuminate\Http\RedirectResponse
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException|\Throwable
*/
public function addMount(Server $server, int $mount_id)
public function addMount(Server $server, Mount $mount)
{
$server->mounts()->attach($mount_id);
$mountServer = new MountServer;
$mountServer->mount_id = $mount->id;
$mountServer->server_id = $server->id;
$mountServer->saveOrFail();
$data = $this->serverConfigurationStructureService->handle($server);
@ -438,15 +450,15 @@ class ServersController extends Controller
* Remove a mount from a server.
*
* @param Server $server
* @param int $mount_id
* @param \Pterodactyl\Models\Mount $mount
* @return \Illuminate\Http\RedirectResponse
*
* @throws DaemonConnectionException
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function deleteMount(Server $server, int $mount_id)
public function deleteMount(Server $server, Mount $mount)
{
$server->mounts()->detach($mount_id);
MountServer::where('mount_id', $mount->id)->where('server_id', $server->id)->delete();
$data = $this->serverConfigurationStructureService->handle($server);

View file

@ -84,7 +84,14 @@ class UserController extends Controller
*/
public function index(Request $request)
{
$users = QueryBuilder::for(User::query()->withCount('servers'))
$users = QueryBuilder::for(
User::query()->select('users.*')
->selectRaw('COUNT(DISTINCT(subusers.id)) as subuser_of_count')
->selectRaw('COUNT(DISTINCT(servers.id)) as servers_count')
->leftJoin('subusers', 'subusers.user_id', '=', 'users.id')
->leftJoin('servers', 'servers.owner_id', '=', 'users.id')
->groupBy('users.id')
)
->allowedFilters(['username', 'email', 'uuid'])
->allowedSorts(['id', 'uuid'])
->paginate(50);

View file

@ -110,7 +110,9 @@ class DatabaseController extends ApplicationApiController
*/
public function store(StoreServerDatabaseRequest $request, Server $server): JsonResponse
{
$database = $this->databaseManagementService->create($server, $request->validated());
$database = $this->databaseManagementService->create($server, array_merge($request->validated(), [
'database' => $request->databaseName(),
]));
return $this->fractal->item($database)
->transformWith($this->getTransformer(ServerDatabaseTransformer::class))
@ -133,7 +135,7 @@ class DatabaseController extends ApplicationApiController
*/
public function delete(ServerDatabaseWriteRequest $request): Response
{
$this->databaseManagementService->delete($request->getModel(Database::class)->id);
$this->databaseManagementService->delete($request->getModel(Database::class));
return response('', 204);
}

View file

@ -82,7 +82,7 @@ class ServerManagementController extends ApplicationApiController
*/
public function reinstall(ServerWriteRequest $request, Server $server): Response
{
$this->reinstallServerService->reinstall($server);
$this->reinstallServerService->handle($server);
return $this->returnNoContent();
}

View file

@ -129,7 +129,7 @@ class DatabaseController extends ClientApiController
*/
public function delete(DeleteDatabaseRequest $request, Server $server, Database $database): Response
{
$this->managementService->delete($database->id);
$this->managementService->delete($database);
return Response::create('', Response::HTTP_NO_CONTENT);
}

View file

@ -70,7 +70,7 @@ class FileController extends ClientApiController
{
$contents = $this->fileRepository
->setServer($server)
->getDirectory($request->get('directory') ?? '/');
->getDirectory(urlencode(urldecode($request->get('directory') ?? '/')));
return $this->fractal->collection($contents)
->transformWith($this->getTransformer(FileObjectTransformer::class))
@ -91,7 +91,7 @@ class FileController extends ClientApiController
{
return new Response(
$this->fileRepository->setServer($server)->getContent(
$request->get('file'), config('pterodactyl.files.max_edit_size')
urlencode(urldecode($request->get('file'))), config('pterodactyl.files.max_edit_size')
),
Response::HTTP_OK,
['Content-Type' => 'text/plain']

View file

@ -120,15 +120,27 @@ class ScheduleController extends ClientApiController
*/
public function update(UpdateScheduleRequest $request, Server $server, Schedule $schedule)
{
$this->repository->update($schedule->id, [
$active = (bool) $request->input('is_active');
$data = [
'name' => $request->input('name'),
'cron_day_of_week' => $request->input('day_of_week'),
'cron_day_of_month' => $request->input('day_of_month'),
'cron_hour' => $request->input('hour'),
'cron_minute' => $request->input('minute'),
'is_active' => (bool) $request->input('is_active'),
'is_active' => $active,
'next_run_at' => $this->getNextRunAt($request),
]);
];
// Toggle the processing state of the scheduled task when it is enabled or disabled so that an
// invalid state can be reset without manual database intervention.
//
// @see https://github.com/pterodactyl/panel/issues/2425
if ($schedule->is_active !== $active) {
$data['is_processing'] = false;
}
$this->repository->update($schedule->id, $data);
return $this->fractal->item($schedule->refresh())
->transformWith($this->getTransformer(ScheduleTransformer::class))

View file

@ -69,7 +69,7 @@ class SettingsController extends ClientApiController
*/
public function reinstall(ReinstallServerRequest $request, Server $server)
{
$this->reinstallServerService->reinstall($server);
$this->reinstallServerService->handle($server);
return new JsonResponse([], Response::HTTP_ACCEPTED);
}

View file

@ -2,15 +2,12 @@
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
use Carbon\CarbonImmutable;
use Pterodactyl\Models\Server;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Services\Servers\StartupCommandService;
use Pterodactyl\Services\Servers\VariableValidatorService;
use Pterodactyl\Repositories\Eloquent\ServerVariableRepository;
use Pterodactyl\Transformers\Api\Client\EggVariableTransformer;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Pterodactyl\Http\Requests\Api\Client\Servers\Startup\GetStartupRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Startup\UpdateStartupVariableRequest;
@ -59,7 +56,9 @@ class StartupController extends ClientApiController
{
$startup = $this->startupCommandService->handle($server, false);
return $this->fractal->collection($server->variables)
return $this->fractal->collection(
$server->variables()->where('user_viewable', true)->get()
)
->transformWith($this->getTransformer(EggVariableTransformer::class))
->addMeta([
'startup_command' => $startup,
@ -84,7 +83,7 @@ class StartupController extends ClientApiController
/** @var \Pterodactyl\Models\EggVariable $variable */
$variable = $server->variables()->where('env_variable', $request->input('key'))->first();
if (is_null($variable) || !$variable->user_viewable) {
if (is_null($variable) || ! $variable->user_viewable) {
throw new BadRequestHttpException(
"The environment variable you are trying to edit does not exist."
);
@ -101,7 +100,7 @@ class StartupController extends ClientApiController
'server_id' => $server->id,
'variable_id' => $variable->id,
], [
'variable_value' => $request->input('value'),
'variable_value' => $request->input('value') ?? '',
]);
$variable = $variable->refresh();

View file

@ -49,11 +49,11 @@ class SubstituteClientApiBindings extends ApiSubstituteBindings
return Database::query()->where('id', $id)->firstOrFail();
});
$this->router->model('backup', Backup::class, function ($value) {
$this->router->bind('backup', function ($value) {
return Backup::query()->where('uuid', $value)->firstOrFail();
});
$this->router->model('user', User::class, function ($value) {
$this->router->bind('user', function ($value) {
return User::query()->where('uuid', $value)->firstOrFail();
});

View file

@ -19,12 +19,12 @@ class EggFormRequest extends AdminFormRequest
public function rules()
{
$rules = [
'name' => 'required|string|max:255',
'name' => 'required|string|max:191',
'description' => 'nullable|string',
'docker_image' => 'required|string|max:255',
'docker_image' => 'required|string|max:191',
'startup' => 'required|string',
'config_from' => 'sometimes|bail|nullable|numeric',
'config_stop' => 'required_without:config_from|nullable|string|max:255',
'config_stop' => 'required_without:config_from|nullable|string|max:191',
'config_startup' => 'required_without:config_from|nullable|json',
'config_logs' => 'required_without:config_from|nullable|json',
'config_files' => 'required_without:config_from|nullable|json',

View file

@ -15,9 +15,9 @@ class EggVariableFormRequest extends AdminFormRequest
public function rules()
{
return [
'name' => 'required|string|min:1|max:255',
'name' => 'required|string|min:1|max:191',
'description' => 'sometimes|nullable|string',
'env_variable' => 'required|regex:/^[\w]{1,255}$/|notIn:' . EggVariable::RESERVED_ENV_NAMES,
'env_variable' => 'required|regex:/^[\w]{1,191}$/|notIn:' . EggVariable::RESERVED_ENV_NAMES,
'options' => 'sometimes|required|array',
'rules' => 'bail|required|string',
'default_value' => 'present',

View file

@ -1,11 +1,4 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Http\Requests\Admin;

View file

@ -19,7 +19,7 @@ class StoreNestFormRequest extends AdminFormRequest
public function rules()
{
return [
'name' => 'required|string|min:1|max:255',
'name' => 'required|string|min:1|max:191',
'description' => 'string|nullable',
];
}

View file

@ -20,7 +20,7 @@ class AllocationFormRequest extends AdminFormRequest
{
return [
'allocation_ip' => 'required|string',
'allocation_alias' => 'sometimes|nullable|string|max:255',
'allocation_alias' => 'sometimes|nullable|string|max:191',
'allocation_ports' => 'required|array',
];
}

View file

@ -15,12 +15,10 @@ class AdvancedSettingsFormRequest extends AdminFormRequest
{
return [
'recaptcha:enabled' => 'required|in:true,false',
'recaptcha:secret_key' => 'required|string|max:255',
'recaptcha:website_key' => 'required|string|max:255',
'recaptcha:secret_key' => 'required|string|max:191',
'recaptcha:website_key' => 'required|string|max:191',
'pterodactyl:guzzle:timeout' => 'required|integer|between:1,60',
'pterodactyl:guzzle:connect_timeout' => 'required|integer|between:1,60',
'pterodactyl:console:count' => 'required|integer|min:1',
'pterodactyl:console:frequency' => 'required|integer|min:10',
];
}
@ -35,8 +33,6 @@ class AdvancedSettingsFormRequest extends AdminFormRequest
'recaptcha:website_key' => 'reCAPTCHA Website Key',
'pterodactyl:guzzle:timeout' => 'HTTP Request Timeout',
'pterodactyl:guzzle:connect_timeout' => 'HTTP Connection Timeout',
'pterodactyl:console:count' => 'Console Message Count',
'pterodactyl:console:frequency' => 'Console Frequency Tick',
];
}
}

View file

@ -16,7 +16,7 @@ class BaseSettingsFormRequest extends AdminFormRequest
public function rules()
{
return [
'app:name' => 'required|string|max:255',
'app:name' => 'required|string|max:191',
'pterodactyl:auth:2fa_required' => 'required|integer|in:0,1,2',
'app:locale' => ['required', 'string', Rule::in(array_keys($this->getAvailableLanguages()))],
'app:analytics' => 'nullable|string',

View file

@ -18,10 +18,10 @@ class MailSettingsFormRequest extends AdminFormRequest
'mail:host' => 'required|string',
'mail:port' => 'required|integer|between:1,65535',
'mail:encryption' => ['present', Rule::in([null, 'tls', 'ssl'])],
'mail:username' => 'nullable|string|max:255',
'mail:password' => 'nullable|string|max:255',
'mail:username' => 'nullable|string|max:191',
'mail:password' => 'nullable|string|max:191',
'mail:from:address' => 'required|string|email',
'mail:from:name' => 'nullable|string|max:255',
'mail:from:name' => 'nullable|string|max:191',
];
}

View file

@ -24,7 +24,7 @@ class StoreAllocationRequest extends ApplicationApiRequest
{
return [
'ip' => 'required|string',
'alias' => 'sometimes|nullable|string|max:255',
'alias' => 'sometimes|nullable|string|max:191',
'ports' => 'required|array',
'ports.*' => 'string',
];

View file

@ -2,9 +2,12 @@
namespace Pterodactyl\Http\Requests\Api\Application\Servers\Databases;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server;
use Illuminate\Validation\Rule;
use Illuminate\Database\Query\Builder;
use Pterodactyl\Services\Acl\Api\AdminAcl;
use Pterodactyl\Services\Databases\DatabaseManagementService;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreServerDatabaseRequest extends ApplicationApiRequest
@ -26,14 +29,16 @@ class StoreServerDatabaseRequest extends ApplicationApiRequest
*/
public function rules(): array
{
$server = $this->route()->parameter('server');
return [
'database' => [
'required',
'string',
'alpha_dash',
'min:1',
'max:24',
Rule::unique('databases')->where(function (Builder $query) {
$query->where('database_host_id', $this->input('host') ?? 0);
'max:48',
Rule::unique('databases')->where(function (Builder $query) use ($server) {
$query->where('server_id', $server->id)->where('database', $this->databaseName());
}),
],
'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/',
@ -68,4 +73,18 @@ class StoreServerDatabaseRequest extends ApplicationApiRequest
'database' => 'Database Name',
];
}
/**
* Returns the database name in the expected format.
*
* @return string
*/
public function databaseName(): string
{
$server = $this->route()->parameter('server');
Assert::isInstanceOf($server, Server::class);
return DatabaseManagementService::generateUniqueDatabaseName($this->input('database'), $server->id);
}
}

View file

@ -2,6 +2,7 @@
namespace Pterodactyl\Http\Requests\Api\Client\Account;
use Pterodactyl\Models\ApiKey;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class StoreApiKeyRequest extends ClientApiRequest
@ -11,9 +12,11 @@ class StoreApiKeyRequest extends ClientApiRequest
*/
public function rules(): array
{
$rules = ApiKey::getRules();
return [
'description' => 'required|string|min:4',
'allowed_ips' => 'array',
'description' => $rules['memo'],
'allowed_ips' => $rules['allowed_ips'],
'allowed_ips.*' => 'ip',
];
}

View file

@ -21,7 +21,7 @@ class StoreBackupRequest extends ClientApiRequest
public function rules(): array
{
return [
'name' => 'nullable|string|max:255',
'name' => 'nullable|string|max:191',
'ignored' => 'nullable|string',
];
}

View file

@ -2,9 +2,14 @@
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Databases;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server;
use Illuminate\Validation\Rule;
use Pterodactyl\Models\Permission;
use Illuminate\Database\Query\Builder;
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
use Pterodactyl\Services\Databases\DatabaseManagementService;
class StoreDatabaseRequest extends ClientApiRequest implements ClientPermissionsRequest
{
@ -21,9 +26,35 @@ class StoreDatabaseRequest extends ClientApiRequest implements ClientPermissions
*/
public function rules(): array
{
$server = $this->route()->parameter('server');
Assert::isInstanceOf($server, Server::class);
return [
'database' => 'required|alpha_dash|min:1|max:100',
'database' => [
'required',
'alpha_dash',
'min:1',
'max:48',
// Yes, I am aware that you could have the same database name across two unique hosts. However,
// I don't really care about that for this validation. We just want to make sure it is unique to
// the server itself. No need for complexity.
Rule::unique('databases')->where(function (Builder $query) use ($server) {
$query->where('server_id', $server->id)
->where('database', DatabaseManagementService::generateUniqueDatabaseName($this->input('database'), $server->id));
}),
],
'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/',
];
}
/**
* @return array
*/
public function messages()
{
return [
'database.unique' => 'The database name you have selected is already in use by this server.',
];
}
}

View file

@ -2,6 +2,7 @@
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Schedules;
use Pterodactyl\Models\Schedule;
use Pterodactyl\Models\Permission;
class StoreScheduleRequest extends ViewScheduleRequest
@ -19,13 +20,15 @@ class StoreScheduleRequest extends ViewScheduleRequest
*/
public function rules(): array
{
$rules = Schedule::getRules();
return [
'name' => 'required|string|min:1',
'is_active' => 'filled|boolean',
'minute' => 'required|string',
'hour' => 'required|string',
'day_of_month' => 'required|string',
'day_of_week' => 'required|string',
'name' => $rules['name'],
'is_active' => array_merge(['filled'], $rules['is_active']),
'minute' => $rules['cron_minute'],
'hour' => $rules['cron_hour'],
'day_of_month' => $rules['cron_day_of_month'],
'day_of_week' => $rules['cron_day_of_week'],
];
}
}

View file

@ -24,7 +24,7 @@ class UpdateStartupVariableRequest extends ClientApiRequest
{
return [
'key' => 'required|string',
'value' => 'present|string',
'value' => 'present',
];
}
}

View file

@ -20,7 +20,7 @@ class StoreSubuserRequest extends SubuserRequest
public function rules(): array
{
return [
'email' => 'required|email',
'email' => 'required|email|between:1,191',
'permissions' => 'required|array',
'permissions.*' => 'string',
];

View file

@ -3,10 +3,10 @@
namespace Pterodactyl\Jobs\Schedule;
use Exception;
use Carbon\Carbon;
use Pterodactyl\Jobs\Job;
use Carbon\CarbonImmutable;
use Pterodactyl\Models\Task;
use InvalidArgumentException;
use Illuminate\Container\Container;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -15,39 +15,25 @@ use Pterodactyl\Repositories\Eloquent\TaskRepository;
use Pterodactyl\Services\Backups\InitiateBackupService;
use Pterodactyl\Repositories\Wings\DaemonPowerRepository;
use Pterodactyl\Repositories\Wings\DaemonCommandRepository;
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
class RunTaskJob extends Job implements ShouldQueue
{
use DispatchesJobs, InteractsWithQueue, SerializesModels;
/**
* @var int
*/
public $schedule;
/**
* @var int
* @var \Pterodactyl\Models\Task
*/
public $task;
/**
* @var \Pterodactyl\Repositories\Eloquent\TaskRepository
*/
protected $taskRepository;
/**
* RunTaskJob constructor.
*
* @param int $task
* @param int $schedule
* @param \Pterodactyl\Models\Task $task
*/
public function __construct(int $task, int $schedule)
public function __construct(Task $task)
{
$this->queue = config('pterodactyl.queues.standard');
$this->task = $task;
$this->schedule = $schedule;
}
/**
@ -58,7 +44,6 @@ class RunTaskJob extends Job implements ShouldQueue
* @param \Pterodactyl\Repositories\Wings\DaemonPowerRepository $powerRepository
* @param \Pterodactyl\Repositories\Eloquent\TaskRepository $taskRepository
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Throwable
*/
public function handle(
@ -67,36 +52,32 @@ class RunTaskJob extends Job implements ShouldQueue
DaemonPowerRepository $powerRepository,
TaskRepository $taskRepository
) {
$this->taskRepository = $taskRepository;
$task = $this->taskRepository->getTaskForJobProcess($this->task);
$server = $task->getRelation('server');
// Do not process a task that is not set to active.
if (! $task->getRelation('schedule')->is_active) {
if (! $this->task->schedule->is_active) {
$this->markTaskNotQueued();
$this->markScheduleComplete();
return;
}
$server = $this->task->server;
// Perform the provided task against the daemon.
switch ($task->action) {
switch ($this->task->action) {
case 'power':
$powerRepository->setServer($server)->send($task->payload);
$powerRepository->setServer($server)->send($this->task->payload);
break;
case 'command':
$commandRepository->setServer($server)->send($task->payload);
$commandRepository->setServer($server)->send($this->task->payload);
break;
case 'backup':
$backupService->setIgnoredFiles(explode(PHP_EOL, $task->payload))->handle($server, null);
$backupService->setIgnoredFiles(explode(PHP_EOL, $this->task->payload))->handle($server, null);
break;
default:
throw new InvalidArgumentException('Cannot run a task that points to a non-existent action.');
}
$this->markTaskNotQueued();
$this->queueNextTask($task->sequence_id);
$this->queueNextTask();
}
/**
@ -112,23 +93,23 @@ class RunTaskJob extends Job implements ShouldQueue
/**
* Get the next task in the schedule and queue it for running after the defined period of wait time.
*
* @param int $sequence
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
private function queueNextTask($sequence)
private function queueNextTask()
{
$nextTask = $this->taskRepository->getNextTask($this->schedule, $sequence);
/** @var \Pterodactyl\Models\Task|null $nextTask */
$nextTask = Task::query()->where('schedule_id', $this->task->schedule_id)
->where('sequence_id', $this->task->sequence_id + 1)
->first();
if (is_null($nextTask)) {
$this->markScheduleComplete();
return;
}
$this->taskRepository->update($nextTask->id, ['is_queued' => true]);
$this->dispatch((new self($nextTask->id, $this->schedule))->delay($nextTask->time_offset));
$nextTask->update(['is_queued' => true]);
$this->dispatch((new self($nextTask))->delay($nextTask->time_offset));
}
/**
@ -136,13 +117,10 @@ class RunTaskJob extends Job implements ShouldQueue
*/
private function markScheduleComplete()
{
Container::getInstance()
->make(ScheduleRepositoryInterface::class)
->withoutFreshModel()
->update($this->schedule, [
'is_processing' => false,
'last_run_at' => Carbon::now()->toDateTimeString(),
]);
$this->task->schedule()->update([
'is_processing' => false,
'last_run_at' => CarbonImmutable::now()->toDateTimeString(),
]);
}
/**
@ -150,8 +128,6 @@ class RunTaskJob extends Job implements ShouldQueue
*/
private function markTaskNotQueued()
{
Container::getInstance()
->make(TaskRepositoryInterface::class)
->update($this->task, ['is_queued' => false]);
$this->task->update(['is_queued' => false]);
}
}

View file

@ -65,7 +65,7 @@ class Database extends Model
public static $validationRules = [
'server_id' => 'required|numeric|exists:servers,id',
'database_host_id' => 'required|exists:database_hosts,id',
'database' => 'required|string|alpha_dash|between:3,100',
'database' => 'required|string|alpha_dash|between:3,48',
'username' => 'string|alpha_dash|between:3,100',
'max_connections' => 'nullable|integer',
'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/',

View file

@ -50,7 +50,7 @@ class DatabaseHost extends Model
* @var array
*/
public static $validationRules = [
'name' => 'required|string|max:255',
'name' => 'required|string|max:191',
'host' => 'required|string',
'port' => 'required|numeric|between:1,65535',
'username' => 'required|string|max:32',

View file

@ -93,13 +93,13 @@ class Egg extends Model
public static $validationRules = [
'nest_id' => 'required|bail|numeric|exists:nests,id',
'uuid' => 'required|string|size:36',
'name' => 'required|string|max:255',
'name' => 'required|string|max:191',
'description' => 'string|nullable',
'author' => 'required|string|email',
'docker_image' => 'required|string|max:255',
'docker_image' => 'required|string|max:191',
'startup' => 'required|nullable|string',
'config_from' => 'sometimes|bail|nullable|numeric|exists:eggs,id',
'config_stop' => 'required_without:config_from|nullable|string|max:255',
'config_stop' => 'required_without:config_from|nullable|string|max:191',
'config_startup' => 'required_without:config_from|nullable|json',
'config_logs' => 'required_without:config_from|nullable|json',
'config_files' => 'required_without:config_from|nullable|json',

21
app/Models/EggMount.php Normal file
View file

@ -0,0 +1,21 @@
<?php
namespace Pterodactyl\Models;
class EggMount extends Model
{
/**
* @var string
*/
protected $table = 'egg_mount';
/**
* @var null
*/
protected $primaryKey = null;
/**
* @var bool
*/
public $incrementing = false;
}

View file

@ -73,9 +73,9 @@ class EggVariable extends Model
*/
public static $validationRules = [
'egg_id' => 'exists:eggs,id',
'name' => 'required|string|between:1,255',
'name' => 'required|string|between:1,191',
'description' => 'string',
'env_variable' => 'required|regex:/^[\w]{1,255}$/|notIn:' . self::RESERVED_ENV_NAMES,
'env_variable' => 'required|regex:/^[\w]{1,191}$/|notIn:' . self::RESERVED_ENV_NAMES,
'default_value' => 'string',
'user_viewable' => 'boolean',
'user_editable' => 'boolean',

View file

@ -41,7 +41,7 @@ class Location extends Model
*/
public static $validationRules = [
'short' => 'required|string|between:1,60|unique:locations,short',
'long' => 'string|nullable|between:1,255',
'long' => 'string|nullable|between:1,191',
];
/**

View file

@ -7,6 +7,7 @@ use Illuminate\Validation\Rule;
use Illuminate\Container\Container;
use Illuminate\Contracts\Validation\Factory;
use Illuminate\Database\Eloquent\Model as IlluminateModel;
use Pterodactyl\Exceptions\Model\DataValidationException;
abstract class Model extends IlluminateModel
{
@ -55,7 +56,11 @@ abstract class Model extends IlluminateModel
static::$validatorFactory = Container::getInstance()->make(Factory::class);
static::saving(function (Model $model) {
return $model->validate();
if (! $model->validate()) {
throw new DataValidationException($model->getValidator());
}
return true;
});
}
@ -147,9 +152,9 @@ abstract class Model extends IlluminateModel
}
return $this->getValidator()->setData(
// Trying to do self::toArray() here will leave out keys based on the whitelist/blacklist
// for that model. Doing this will return all of the attributes in a format that can
// properly be validated.
// Trying to do self::toArray() here will leave out keys based on the whitelist/blacklist
// for that model. Doing this will return all of the attributes in a format that can
// properly be validated.
$this->addCastAttributesToArray(
$this->getAttributes(), $this->getMutatedAttributes()
)

View file

@ -56,7 +56,7 @@ class Mount extends Model
*/
public static $validationRules = [
'name' => 'required|string|min:2|max:64|unique:mounts,name',
'description' => 'nullable|string|max:255',
'description' => 'nullable|string|max:191',
'source' => 'required|string',
'target' => 'required|string',
'read_only' => 'sometimes|boolean',

View file

@ -11,6 +11,11 @@ class MountServer extends Model
*/
protected $table = 'mount_server';
/**
* @var bool
*/
public $timestamps = false;
/**
* @var null
*/

View file

@ -44,7 +44,7 @@ class Nest extends Model
*/
public static $validationRules = [
'author' => 'required|string|email',
'name' => 'required|string|max:255',
'name' => 'required|string|max:191',
'description' => 'nullable|string',
];

View file

@ -181,7 +181,7 @@ class Node extends Model
*/
public function getYamlConfiguration()
{
return Yaml::dump($this->getConfiguration(), 4, 2);
return Yaml::dump($this->getConfiguration(), 4, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE);
}
/**

View file

@ -219,80 +219,4 @@ class Permission extends Model
{
return Collection::make(self::$permissions);
}
/**
* A list of all permissions available for a user.
*
* @var array
* @deprecated
*/
protected static $deprecatedPermissions = [
'power' => [
'power-start' => 's:power:start',
'power-stop' => 's:power:stop',
'power-restart' => 's:power:restart',
'power-kill' => 's:power:kill',
'send-command' => 's:command',
],
'subuser' => [
'list-subusers' => null,
'view-subuser' => null,
'edit-subuser' => null,
'create-subuser' => null,
'delete-subuser' => null,
],
'server' => [
'view-allocations' => null,
'edit-allocation' => null,
'view-startup' => null,
'edit-startup' => null,
],
'database' => [
'view-databases' => null,
'reset-db-password' => null,
'delete-database' => null,
'create-database' => null,
],
'file' => [
'access-sftp' => null,
'list-files' => 's:files:get',
'edit-files' => 's:files:read',
'save-files' => 's:files:post',
'move-files' => 's:files:move',
'copy-files' => 's:files:copy',
'compress-files' => 's:files:compress',
'decompress-files' => 's:files:decompress',
'create-files' => 's:files:create',
'upload-files' => 's:files:upload',
'delete-files' => 's:files:delete',
'download-files' => 's:files:download',
],
'task' => [
'list-schedules' => null,
'view-schedule' => null,
'toggle-schedule' => null,
'queue-schedule' => null,
'edit-schedule' => null,
'create-schedule' => null,
'delete-schedule' => null,
],
];
/**
* Return a collection of permissions available.
*
* @param bool $array
* @return array|\Illuminate\Database\Eloquent\Collection
* @deprecated
*/
public static function getPermissions($array = false)
{
if ($array) {
return collect(self::$deprecatedPermissions)->mapWithKeys(function ($item) {
return $item;
})->all();
}
return collect(self::$deprecatedPermissions);
}
}

View file

@ -103,7 +103,7 @@ class Schedule extends Model
*/
public static $validationRules = [
'server_id' => 'required|exists:servers,id',
'name' => 'nullable|string|max:255',
'name' => 'required|string|max:191',
'cron_day_of_week' => 'required|string',
'cron_day_of_month' => 'required|string',
'cron_hour' => 'required|string',

View file

@ -15,7 +15,7 @@ use Znck\Eloquent\Traits\BelongsToThrough;
* @property string $name
* @property string $description
* @property bool $skip_scripts
* @property int $suspended
* @property bool $suspended
* @property int $owner_id
* @property int $memory
* @property int $swap
@ -103,7 +103,7 @@ class Server extends Model
public static $validationRules = [
'external_id' => 'sometimes|nullable|string|between:1,191|unique:servers',
'owner_id' => 'required|integer|exists:users,id',
'name' => 'required|string|min:1|max:255',
'name' => 'required|string|min:1|max:191',
'node_id' => 'required|exists:nodes,id',
'description' => 'string',
'memory' => 'required|numeric|min:0',
@ -118,7 +118,7 @@ class Server extends Model
'egg_id' => 'required|exists:eggs,id',
'startup' => 'required|string',
'skip_scripts' => 'sometimes|boolean',
'image' => 'required|string|max:255',
'image' => 'required|string|max:191',
'installed' => 'in:0,1,2',
'database_limit' => 'present|nullable|integer|min:0',
'allocation_limit' => 'sometimes|nullable|integer|min:0',
@ -133,7 +133,7 @@ class Server extends Model
protected $casts = [
'node_id' => 'integer',
'skip_scripts' => 'boolean',
'suspended' => 'integer',
'suspended' => 'boolean',
'owner_id' => 'integer',
'memory' => 'integer',
'swap' => 'integer',

View file

@ -25,7 +25,7 @@ class Setting extends Model
* @var array
*/
public static $validationRules = [
'key' => 'required|string|between:1,255',
'key' => 'required|string|between:1,191',
'value' => 'string',
];
}

View file

@ -137,11 +137,11 @@ class User extends Model implements
*/
public static $validationRules = [
'uuid' => 'required|string|size:36|unique:users,uuid',
'email' => 'required|email|unique:users,email',
'external_id' => 'sometimes|nullable|string|max:255|unique:users,external_id',
'username' => 'required|between:1,255|unique:users,username',
'name_first' => 'required|string|between:1,255',
'name_last' => 'required|string|between:1,255',
'email' => 'required|email|between:1,191|unique:users,email',
'external_id' => 'sometimes|nullable|string|max:191|unique:users,external_id',
'username' => 'required|between:1,191|unique:users,username',
'name_first' => 'required|string|between:1,191',
'name_last' => 'required|string|between:1,191',
'password' => 'sometimes|nullable|string',
'root_admin' => 'boolean',
'language' => 'string',

View file

@ -93,31 +93,6 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor
->paginate($count, $this->getColumns());
}
/**
* Create a new database if it does not already exist on the host with
* the provided details.
*
* @param array $data
* @return \Pterodactyl\Models\Database
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException
*/
public function createIfNotExists(array $data): Database
{
$count = $this->getBuilder()->where([
['server_id', '=', array_get($data, 'server_id')],
['database_host_id', '=', array_get($data, 'database_host_id')],
['database', '=', array_get($data, 'database')],
])->count();
if ($count > 0) {
throw new DuplicateDatabaseNameException('A database with those details already exists for the specified server.');
}
return $this->create($data);
}
/**
* Create a new database on a given connection.
*

View file

@ -171,28 +171,4 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa
return $instance->first();
}
/**
* Return the IDs of all nodes that exist in the provided locations and have the space
* available to support the additional disk and memory provided.
*
* @param array $locations
* @param int $disk
* @param int $memory
* @return \Illuminate\Support\LazyCollection
*/
public function getNodesWithResourceUse(array $locations, int $disk, int $memory): LazyCollection
{
$instance = $this->getBuilder()
->select(['nodes.id', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate'])
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
->where('nodes.public', 1);
if (! empty($locations)) {
$instance->whereIn('nodes.location_id', $locations);
}
return $instance->groupBy('nodes.id')->cursor();
}
}

View file

@ -117,8 +117,8 @@ class DaemonFileRepository extends DaemonRepository
sprintf('/api/servers/%s/files/create-directory', $this->server->uuid),
[
'json' => [
'name' => urldecode($name),
'path' => urldecode($path),
'name' => $name,
'path' => $path,
],
]
);
@ -172,7 +172,7 @@ class DaemonFileRepository extends DaemonRepository
sprintf('/api/servers/%s/files/copy', $this->server->uuid),
[
'json' => [
'location' => urldecode($location),
'location' => $location,
],
]
);

View file

@ -3,18 +3,29 @@
namespace Pterodactyl\Services\Databases;
use Exception;
use InvalidArgumentException;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database;
use Pterodactyl\Helpers\Utilities;
use Illuminate\Database\ConnectionInterface;
use Symfony\Component\VarDumper\Cloner\Data;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Repositories\Eloquent\DatabaseRepository;
use Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException;
use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException;
use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;
class DatabaseManagementService
{
/**
* The regex used to validate that the database name passed through to the function is
* in the expected format.
*
* @see \Pterodactyl\Services\Databases\DatabaseManagementService::generateUniqueDatabaseName()
*/
private const MATCH_NAME_REGEX = '/^(s[\d]+_)(.*)$/';
/**
* @var \Illuminate\Database\ConnectionInterface
*/
@ -31,7 +42,7 @@ class DatabaseManagementService
private $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
* @var \Pterodactyl\Repositories\Eloquent\DatabaseRepository
*/
private $repository;
@ -50,13 +61,13 @@ class DatabaseManagementService
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
* @param \Pterodactyl\Repositories\Eloquent\DatabaseRepository $repository
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
*/
public function __construct(
ConnectionInterface $connection,
DynamicDatabaseConnection $dynamic,
DatabaseRepositoryInterface $repository,
DatabaseRepository $repository,
Encrypter $encrypter
) {
$this->connection = $connection;
@ -65,6 +76,21 @@ class DatabaseManagementService
$this->repository = $repository;
}
/**
* Generates a unique database name for the given server. This name should be passed through when
* calling this handle function for this service, otherwise the database will be created with
* whatever name is provided.
*
* @param string $name
* @param int $serverId
* @return string
*/
public static function generateUniqueDatabaseName(string $name, int $serverId): string
{
// Max of 48 characters, including the s123_ that we append to the front.
return sprintf('s%d_%s', $serverId, substr($name, 0, 48 - strlen("s{$serverId}_")));
}
/**
* Set wether or not this class should validate that the server has enough slots
* left before creating the new database.
@ -104,9 +130,15 @@ class DatabaseManagementService
}
}
// Protect against developer mistakes...
if (empty($data['database']) || ! preg_match(self::MATCH_NAME_REGEX, $data['database'])) {
throw new InvalidArgumentException(
'The database name passed to DatabaseManagementService::handle MUST be prefixed with "s{server_id}_".'
);
}
$data = array_merge($data, [
'server_id' => $server->id,
'database' => sprintf('s%d_%s', $server->id, $data['database']),
'username' => sprintf('u%d_%s', $server->id, str_random(10)),
'password' => $this->encrypter->encrypt(
Utilities::randomStringWithSpecialCharacters(24)
@ -117,7 +149,8 @@ class DatabaseManagementService
try {
return $this->connection->transaction(function () use ($data, &$database) {
$database = $this->repository->createIfNotExists($data);
$database = $this->createModel($data);
$this->dynamic->set('dynamic', $data['database_host_id']);
$this->repository->createDatabase($database->database);
@ -136,7 +169,7 @@ class DatabaseManagementService
$this->repository->dropUser($database->username, $database->remote);
$this->repository->flush();
}
} catch (Exception $exception) {
} catch (Exception $deletionException) {
// Do nothing here. We've already encountered an issue before this point so no
// reason to prioritize this error over the initial one.
}
@ -148,20 +181,48 @@ class DatabaseManagementService
/**
* Delete a database from the given host server.
*
* @param int $id
* @param \Pterodactyl\Models\Database $database
* @return bool|null
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Exception
*/
public function delete($id)
public function delete(Database $database)
{
$database = $this->repository->find($id);
$this->dynamic->set('dynamic', $database->database_host_id);
$this->repository->dropDatabase($database->database);
$this->repository->dropUser($database->username, $database->remote);
$this->repository->flush();
return $this->repository->delete($id);
return $database->delete();
}
/**
* Create the database if there is not an identical match in the DB. While you can technically
* have the same name across multiple hosts, for the sake of keeping this logic easy to understand
* and avoiding user confusion we will ignore the specific host and just look across all hosts.
*
* @param array $data
* @return \Pterodactyl\Models\Database
*
* @throws \Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException
* @throws \Throwable
*/
protected function createModel(array $data): Database
{
$exists = Database::query()->where('server_id', $data['server_id'])
->where('database', $data['database'])
->exists();
if ($exists) {
throw new DuplicateDatabaseNameException(
'A database with that name already exists for this server.'
);
}
$database = (new Database)->forceFill($data);
$database->saveOrFail();
return $database;
}
}

View file

@ -2,44 +2,27 @@
namespace Pterodactyl\Services\Databases;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
use Pterodactyl\Models\DatabaseHost;
use Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException;
class DeployServerDatabaseService
{
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface
*/
private $databaseHostRepository;
/**
* @var \Pterodactyl\Services\Databases\DatabaseManagementService
*/
private $managementService;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
*/
private $repository;
/**
* ServerDatabaseCreationService constructor.
*
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
* @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $databaseHostRepository
* @param \Pterodactyl\Services\Databases\DatabaseManagementService $managementService
*/
public function __construct(
DatabaseRepositoryInterface $repository,
DatabaseHostRepositoryInterface $databaseHostRepository,
DatabaseManagementService $managementService
) {
$this->databaseHostRepository = $databaseHostRepository;
public function __construct(DatabaseManagementService $managementService)
{
$this->managementService = $managementService;
$this->repository = $repository;
}
/**
@ -53,28 +36,26 @@ class DeployServerDatabaseService
*/
public function handle(Server $server, array $data): Database
{
$allowRandom = config('pterodactyl.client_features.databases.allow_random');
$hosts = $this->databaseHostRepository->setColumns(['id'])->findWhere([
['node_id', '=', $server->node_id],
]);
if ($hosts->isEmpty() && ! $allowRandom) {
throw new NoSuitableDatabaseHostException;
}
Assert::notEmpty($data['database'] ?? null);
Assert::notEmpty($data['remote'] ?? null);
$hosts = DatabaseHost::query()->get()->toBase();
if ($hosts->isEmpty()) {
$hosts = $this->databaseHostRepository->setColumns(['id'])->all();
if ($hosts->isEmpty()) {
throw new NoSuitableDatabaseHostException;
} else {
$nodeHosts = $hosts->where('node_id', $server->node_id)->toBase();
if ($nodeHosts->isEmpty() && ! config('pterodactyl.client_features.databases.allow_random')) {
throw new NoSuitableDatabaseHostException;
}
}
$host = $hosts->random();
return $this->managementService->create($server, [
'database_host_id' => $host->id,
'database' => array_get($data, 'database'),
'remote' => array_get($data, 'remote'),
'database_host_id' => $nodeHosts->isEmpty()
? $hosts->random()->id
: $nodeHosts->random()->id,
'database' => DatabaseManagementService::generateUniqueDatabaseName($data['database'], $server->id),
'remote' => $data['remote'],
]);
}
}

View file

@ -3,16 +3,12 @@
namespace Pterodactyl\Services\Deployment;
use Webmozart\Assert\Assert;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Models\Node;
use Illuminate\Support\LazyCollection;
use Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException;
class FindViableNodesService
{
/**
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
*/
private $repository;
/**
* @var array
*/
@ -28,16 +24,6 @@ class FindViableNodesService
*/
protected $memory;
/**
* FindViableNodesService constructor.
*
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
*/
public function __construct(NodeRepositoryInterface $repository)
{
$this->repository = $repository;
}
/**
* Set the locations that should be searched through to locate available nodes.
*
@ -46,6 +32,8 @@ class FindViableNodesService
*/
public function setLocations(array $locations): self
{
Assert::allInteger($locations, 'An array of location IDs should be provided when calling setLocations.');
$this->locations = $locations;
return $this;
@ -90,32 +78,34 @@ class FindViableNodesService
* are tossed out, as are any nodes marked as non-public, meaning automatic
* deployments should not be done against them.
*
* @return int[]
* @return \Pterodactyl\Models\Node[]|\Illuminate\Support\Collection
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException
*/
public function handle(): array
public function handle()
{
Assert::integer($this->disk, 'Calls to ' . __METHOD__ . ' must have the disk space set as an integer, received %s');
Assert::integer($this->memory, 'Calls to ' . __METHOD__ . ' must have the memory usage set as an integer, received %s');
Assert::integer($this->disk, 'Disk space must be an int, got %s');
Assert::integer($this->memory, 'Memory usage must be an int, got %s');
$nodes = $this->repository->getNodesWithResourceUse($this->locations, $this->disk, $this->memory);
$viable = [];
$query = Node::query()->select('nodes.*')
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory')
->selectRaw('IFNULL(SUM(servers.disk), 0) as sum_disk')
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
->where('nodes.public', 1);
foreach ($nodes as $node) {
$memoryLimit = $node->memory * (1 + ($node->memory_overallocate / 100));
$diskLimit = $node->disk * (1 + ($node->disk_overallocate / 100));
if (($node->sum_memory + $this->memory) > $memoryLimit || ($node->sum_disk + $this->disk) > $diskLimit) {
continue;
}
$viable[] = $node->id;
if (! empty($this->locations)) {
$query = $query->whereIn('nodes.location_id', $this->locations);
}
if (empty($viable)) {
$results = $query->groupBy('nodes.id')
->havingRaw('(IFNULL(SUM(servers.memory), 0) + ?) <= (nodes.memory * (1 + (nodes.memory_overallocate / 100)))', [ $this->memory ])
->havingRaw('(IFNULL(SUM(servers.disk), 0) + ?) <= (nodes.disk * (1 + (nodes.disk_overallocate / 100)))', [ $this->disk ])
->get()
->toBase();
if ($results->isEmpty()) {
throw new NoViableNodeException(trans('exceptions.deployment.no_viable_nodes'));
}
return $viable;
return $results;
}
}

View file

@ -1,11 +1,4 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Services\Eggs\Scripts;
@ -40,12 +33,8 @@ class InstallScriptService
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Egg\InvalidCopyFromException
*/
public function handle($egg, array $data)
public function handle(Egg $egg, array $data)
{
if (! $egg instanceof Egg) {
$egg = $this->repository->find($egg);
}
if (! is_null(array_get($data, 'copy_script_from'))) {
if (! $this->repository->isCopyableScript(array_get($data, 'copy_script_from'), $egg->nest_id)) {
throw new InvalidCopyFromException(trans('exceptions.nest.egg.invalid_copy_id'));

View file

@ -1,11 +1,4 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Services\Eggs\Sharing;

View file

@ -1,11 +1,4 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Services\Eggs\Sharing;

View file

@ -2,6 +2,7 @@
namespace Pterodactyl\Services\Eggs\Sharing;
use Pterodactyl\Models\Egg;
use Illuminate\Http\UploadedFile;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
@ -46,7 +47,7 @@ class EggUpdateImporterService
/**
* Update an existing Egg using an uploaded JSON file.
*
* @param int $egg
* @param \Pterodactyl\Models\Egg $egg
* @param \Illuminate\Http\UploadedFile $file
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
@ -54,7 +55,7 @@ class EggUpdateImporterService
* @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
*/
public function handle(int $egg, UploadedFile $file)
public function handle(Egg $egg, UploadedFile $file)
{
if ($file->getError() !== UPLOAD_ERR_OK || ! $file->isFile()) {
throw new InvalidFileUploadException(
@ -81,7 +82,7 @@ class EggUpdateImporterService
}
$this->connection->beginTransaction();
$this->repository->update($egg, [
$this->repository->update($egg->id, [
'author' => object_get($parsed, 'author'),
'name' => object_get($parsed, 'name'),
'description' => object_get($parsed, 'description'),
@ -99,19 +100,19 @@ class EggUpdateImporterService
// Update Existing Variables
collect($parsed->variables)->each(function ($variable) use ($egg) {
$this->variableRepository->withoutFreshModel()->updateOrCreate([
'egg_id' => $egg,
'egg_id' => $egg->id,
'env_variable' => $variable->env_variable,
], collect($variable)->except(['egg_id', 'env_variable'])->toArray());
});
$imported = collect($parsed->variables)->pluck('env_variable')->toArray();
$existing = $this->variableRepository->setColumns(['id', 'env_variable'])->findWhere([['egg_id', '=', $egg]]);
$existing = $this->variableRepository->setColumns(['id', 'env_variable'])->findWhere([['egg_id', '=', $egg->id]]);
// Delete variables not present in the import.
collect($existing)->each(function ($variable) use ($egg, $imported) {
if (! in_array($variable->env_variable, $imported)) {
$this->variableRepository->deleteWhere([
['egg_id', '=', $egg],
['egg_id', '=', $egg->id],
['env_variable', '=', $variable->env_variable],
]);
}

View file

@ -1,40 +0,0 @@
<?php
namespace Pterodactyl\Services\Mounts;
use Ramsey\Uuid\Uuid;
use Pterodactyl\Repositories\Eloquent\MountRepository;
class MountCreationService
{
/**
* @var \Pterodactyl\Repositories\Eloquent\MountRepository
*/
protected $repository;
/**
* MountCreationService constructor.
*
* @param \Pterodactyl\Repositories\Eloquent\MountRepository $repository
*/
public function __construct(MountRepository $repository)
{
$this->repository = $repository;
}
/**
* Create a new mount.
*
* @param array $data
* @return \Pterodactyl\Models\Mount
*
* @throws \Exception
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function handle(array $data)
{
return $this->repository->create(array_merge($data, [
'uuid' => Uuid::uuid4()->toString(),
]), true, true);
}
}

View file

@ -1,40 +0,0 @@
<?php
namespace Pterodactyl\Services\Mounts;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Mount;
use Pterodactyl\Repositories\Eloquent\MountRepository;
class MountDeletionService
{
/**
* @var \Pterodactyl\Repositories\Eloquent\MountRepository
*/
protected $repository;
/**
* MountDeletionService constructor.
*
* @param \Pterodactyl\Repositories\Eloquent\MountRepository $repository
*/
public function __construct(MountRepository $repository)
{
$this->repository = $repository;
}
/**
* Delete an existing location.
*
* @param int|\Pterodactyl\Models\Mount $mount
* @return int|null
*/
public function handle($mount)
{
$mount = ($mount instanceof Mount) ? $mount->id : $mount;
Assert::integerish($mount, 'First argument passed to handle must be numeric or an instance of ' . Mount::class . ', received %s.');
return $this->repository->delete($mount);
}
}

View file

@ -1,41 +0,0 @@
<?php
namespace Pterodactyl\Services\Mounts;
use Pterodactyl\Models\Mount;
use Pterodactyl\Repositories\Eloquent\MountRepository;
class MountUpdateService
{
/**
* @var \Pterodactyl\Repositories\Eloquent\MountRepository
*/
protected $repository;
/**
* MountUpdateService constructor.
*
* @param \Pterodactyl\Repositories\Eloquent\MountRepository $repository
*/
public function __construct(MountRepository $repository)
{
$this->repository = $repository;
}
/**
* Update an existing location.
*
* @param int|\Pterodactyl\Models\Mount $mount
* @param array $data
* @return \Pterodactyl\Models\Mount
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle($mount, array $data)
{
$mount = ($mount instanceof Mount) ? $mount->id : $mount;
return $this->repository->update($mount, $data);
}
}

View file

@ -73,7 +73,7 @@ class ProcessScheduleService
$this->taskRepository->update($task->id, ['is_queued' => true]);
$this->dispatcher->dispatch(
(new RunTaskJob($task->id, $schedule->id))->delay($task->time_offset)
(new RunTaskJob($task))->delay($task->time_offset)
);
}
}

View file

@ -4,8 +4,6 @@ namespace Pterodactyl\Services\Servers;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\EggVariable;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class EnvironmentService
{
@ -14,28 +12,6 @@ class EnvironmentService
*/
private $additional = [];
/**
* @var \Illuminate\Contracts\Config\Repository
*/
private $config;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* EnvironmentService constructor.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
*/
public function __construct(ConfigRepository $config, ServerRepositoryInterface $repository)
{
$this->config = $config;
$this->repository = $repository;
}
/**
* Dynamically configure additional environment variables to be assigned
* with a specific server.
@ -79,7 +55,7 @@ class EnvironmentService
}
// Process variables set in the configuration file.
foreach ($this->config->get('pterodactyl.environment_variables', []) as $key => $object) {
foreach (config('pterodactyl.environment_variables', []) as $key => $object) {
$variables->put(
$key, is_callable($object) ? call_user_func($object, $server) : object_get($server, $object)
);

View file

@ -19,26 +19,18 @@ class ReinstallServerService
*/
private $connection;
/**
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
*/
private $repository;
/**
* ReinstallService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
*/
public function __construct(
ConnectionInterface $connection,
DaemonServerRepository $daemonServerRepository,
ServerRepository $repository
DaemonServerRepository $daemonServerRepository
) {
$this->daemonServerRepository = $daemonServerRepository;
$this->connection = $connection;
$this->repository = $repository;
}
/**
@ -49,16 +41,14 @@ class ReinstallServerService
*
* @throws \Throwable
*/
public function reinstall(Server $server)
public function handle(Server $server)
{
return $this->connection->transaction(function () use ($server) {
$updated = $this->repository->update($server->id, [
'installed' => Server::STATUS_INSTALLING,
], true, true);
$server->forceFill(['installed' => Server::STATUS_INSTALLING])->save();
$this->daemonServerRepository->setServer($server)->reinstall();
return $updated;
return $server->refresh();
});
}
}

View file

@ -1,17 +1,9 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Services\Servers;
use Pterodactyl\Models\Mount;
use Pterodactyl\Models\Server;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class ServerConfigurationStructureService
{
@ -22,22 +14,13 @@ class ServerConfigurationStructureService
*/
private $environment;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* ServerConfigurationStructureService constructor.
*
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
* @param \Pterodactyl\Services\Servers\EnvironmentService $environment
*/
public function __construct(
ServerRepositoryInterface $repository,
EnvironmentService $environment
) {
$this->repository = $repository;
public function __construct(EnvironmentService $environment)
{
$this->environment = $environment;
}
@ -50,8 +33,6 @@ class ServerConfigurationStructureService
* @param \Pterodactyl\Models\Server $server
* @param bool $legacy
* @return array
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle(Server $server, bool $legacy = false): array
{
@ -72,7 +53,7 @@ class ServerConfigurationStructureService
{
return [
'uuid' => $server->uuid,
'suspended' => (bool) $server->suspended,
'suspended' => $server->suspended,
'environment' => $this->environment->handle($server),
'invocation' => $server->startup,
'skip_egg_scripts' => $server->skip_scripts,
@ -112,8 +93,6 @@ class ServerConfigurationStructureService
*
* @param \Pterodactyl\Models\Server $server
* @return array
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
protected function returnLegacyFormat(Server $server)
{

View file

@ -4,7 +4,9 @@ namespace Pterodactyl\Services\Servers;
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Arr;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\User;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server;
use Illuminate\Support\Collection;
use Pterodactyl\Models\Allocation;
@ -13,7 +15,6 @@ use Pterodactyl\Models\Objects\DeploymentObject;
use Pterodactyl\Repositories\Eloquent\EggRepository;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Repositories\Eloquent\AllocationRepository;
use Pterodactyl\Services\Deployment\FindViableNodesService;
use Pterodactyl\Repositories\Eloquent\ServerVariableRepository;
use Pterodactyl\Services\Deployment\AllocationSelectionService;
@ -21,11 +22,6 @@ use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class ServerCreationService
{
/**
* @var \Pterodactyl\Repositories\Eloquent\AllocationRepository
*/
private $allocationRepository;
/**
* @var \Pterodactyl\Services\Deployment\AllocationSelectionService
*/
@ -79,7 +75,6 @@ class ServerCreationService
/**
* CreationService constructor.
*
* @param \Pterodactyl\Repositories\Eloquent\AllocationRepository $allocationRepository
* @param \Pterodactyl\Services\Deployment\AllocationSelectionService $allocationSelectionService
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
@ -92,7 +87,6 @@ class ServerCreationService
* @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService
*/
public function __construct(
AllocationRepository $allocationRepository,
AllocationSelectionService $allocationSelectionService,
ConnectionInterface $connection,
DaemonServerRepository $daemonServerRepository,
@ -105,7 +99,6 @@ class ServerCreationService
VariableValidatorService $validatorService
) {
$this->allocationSelectionService = $allocationSelectionService;
$this->allocationRepository = $allocationRepository;
$this->configurationStructureService = $configurationStructureService;
$this->connection = $connection;
$this->findViableNodesService = $findViableNodesService;
@ -130,15 +123,12 @@ class ServerCreationService
* @throws \Throwable
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Illuminate\Validation\ValidationException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException
*/
public function handle(array $data, DeploymentObject $deployment = null): Server
{
$this->connection->beginTransaction();
// If a deployment object has been passed we need to get the allocation
// that the server should use, and assign the node from that allocation.
if ($deployment instanceof DeploymentObject) {
@ -149,37 +139,42 @@ class ServerCreationService
// Auto-configure the node based on the selected allocation
// if no node was defined.
if (is_null(Arr::get($data, 'node_id'))) {
$data['node_id'] = $this->getNodeFromAllocation($data['allocation_id']);
if (empty($data['node_id'])) {
Assert::false(empty($data['allocation_id']), 'Expected a non-empty allocation_id in server creation data.');
$data['node_id'] = Allocation::query()->findOrFail($data['allocation_id'])->node_id;
}
if (is_null(Arr::get($data, 'nest_id'))) {
/** @var \Pterodactyl\Models\Egg $egg */
$egg = $this->eggRepository->setColumns(['id', 'nest_id'])->find(Arr::get($data, 'egg_id'));
$data['nest_id'] = $egg->nest_id;
if (empty($data['nest_id'])) {
Assert::false(empty($data['egg_id']), 'Expected a non-empty egg_id in server creation data.');
$data['nest_id'] = Egg::query()->findOrFail($data['egg_id'])->nest_id;
}
$eggVariableData = $this->validatorService
->setUserLevel(User::USER_LEVEL_ADMIN)
->handle(Arr::get($data, 'egg_id'), Arr::get($data, 'environment', []));
// Create the server and assign any additional allocations to it.
$server = $this->createModel($data);
$this->storeAssignedAllocations($server, $data);
$this->storeEggVariables($server, $eggVariableData);
// Due to the design of the Daemon, we need to persist this server to the disk
// before we can actually create it on the Daemon.
//
// If that connection fails out we will attempt to perform a cleanup by just
// deleting the server itself from the system.
$this->connection->commit();
/** @var \Pterodactyl\Models\Server $server */
$server = $this->connection->transaction(function () use ($data, $eggVariableData) {
// Create the server and assign any additional allocations to it.
$server = $this->createModel($data);
$structure = $this->configurationStructureService->handle($server);
$this->storeAssignedAllocations($server, $data);
$this->storeEggVariables($server, $eggVariableData);
return $server;
});
try {
$this->daemonServerRepository->setServer($server)->create($structure);
$this->daemonServerRepository->setServer($server)->create(
$this->configurationStructureService->handle($server)
);
} catch (DaemonConnectionException $exception) {
$this->serverDeletionService->withForce(true)->handle($server);
@ -208,7 +203,7 @@ class ServerCreationService
->handle();
return $this->allocationSelectionService->setDedicated($deployment->isDedicated())
->setNodes($nodes)
->setNodes($nodes->pluck('id')->toArray())
->setPorts($deployment->getPorts())
->handle();
}
@ -269,7 +264,7 @@ class ServerCreationService
$records = array_merge($records, $data['allocation_additional']);
}
$this->allocationRepository->updateWhereIn('id', $records, [
Allocation::query()->whereIn('id', $records)->update([
'server_id' => $server->id,
]);
}
@ -295,22 +290,6 @@ class ServerCreationService
}
}
/**
* Get the node that an allocation belongs to.
*
* @param int $id
* @return int
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
private function getNodeFromAllocation(int $id): int
{
/** @var \Pterodactyl\Models\Allocation $allocation */
$allocation = $this->allocationRepository->setColumns(['id', 'node_id'])->find($id);
return $allocation->node_id;
}
/**
* Create a unique UUID and UUID-Short combo for a server.
*

View file

@ -3,11 +3,10 @@
namespace Pterodactyl\Services\Servers;
use Exception;
use Psr\Log\LoggerInterface;
use Illuminate\Http\Response;
use Pterodactyl\Models\Server;
use Illuminate\Support\Facades\Log;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Repositories\Eloquent\DatabaseRepository;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Services\Databases\DatabaseManagementService;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
@ -29,50 +28,26 @@ class ServerDeletionService
*/
private $daemonServerRepository;
/**
* @var \Pterodactyl\Repositories\Eloquent\DatabaseRepository
*/
private $databaseRepository;
/**
* @var \Pterodactyl\Services\Databases\DatabaseManagementService
*/
private $databaseManagementService;
/**
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
*/
private $repository;
/**
* @var \Psr\Log\LoggerInterface
*/
private $writer;
/**
* DeletionService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
* @param \Pterodactyl\Repositories\Eloquent\DatabaseRepository $databaseRepository
* @param \Pterodactyl\Services\Databases\DatabaseManagementService $databaseManagementService
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
* @param \Psr\Log\LoggerInterface $writer
*/
public function __construct(
ConnectionInterface $connection,
DaemonServerRepository $daemonServerRepository,
DatabaseRepository $databaseRepository,
DatabaseManagementService $databaseManagementService,
ServerRepository $repository,
LoggerInterface $writer
DatabaseManagementService $databaseManagementService
) {
$this->connection = $connection;
$this->daemonServerRepository = $daemonServerRepository;
$this->databaseRepository = $databaseRepository;
$this->databaseManagementService = $databaseManagementService;
$this->repository = $repository;
$this->writer = $writer;
}
/**
@ -101,27 +76,39 @@ class ServerDeletionService
try {
$this->daemonServerRepository->setServer($server)->delete();
} catch (DaemonConnectionException $exception) {
if ($this->force) {
$this->writer->warning($exception);
} else {
// If there is an error not caused a 404 error and this isn't a forced delete,
// go ahead and bail out. We specifically ignore a 404 since that can be assumed
// to be a safe error, meaning the server doesn't exist at all on Wings so there
// is no reason we need to bail out from that.
if (! $this->force && $exception->getStatusCode() !== Response::HTTP_NOT_FOUND) {
throw $exception;
}
Log::warning($exception);
}
$this->connection->transaction(function () use ($server) {
$this->databaseRepository->setColumns('id')->findWhere([['server_id', '=', $server->id]])->each(function ($item) {
foreach ($server->databases as $database) {
try {
$this->databaseManagementService->delete($item->id);
$this->databaseManagementService->delete($database);
} catch (Exception $exception) {
if ($this->force) {
$this->writer->warning($exception);
} else {
if (!$this->force) {
throw $exception;
}
}
});
$this->repository->delete($server->id);
// Oh well, just try to delete the database entry we have from the database
// so that the server itself can be deleted. This will leave it dangling on
// the host instance, but we couldn't delete it anyways so not sure how we would
// handle this better anyways.
//
// @see https://github.com/pterodactyl/panel/issues/2085
$database->delete();
Log::warning($exception);
}
}
$server->delete();
});
}
}

View file

@ -2,13 +2,13 @@
namespace Pterodactyl\Services\Servers;
use Illuminate\Support\Arr;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\ServerVariable;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Traits\Services\HasUserLevels;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface;
class StartupModificationService
{
@ -19,63 +19,21 @@ class StartupModificationService
*/
private $connection;
/**
* @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface
*/
private $eggRepository;
/**
* @var \Pterodactyl\Services\Servers\EnvironmentService
*/
private $environmentService;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface
*/
private $serverVariableRepository;
/**
* @var \Pterodactyl\Services\Servers\VariableValidatorService
*/
private $validatorService;
/**
* @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService
*/
private $structureService;
/**
* StartupModificationService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Contracts\Repository\EggRepositoryInterface $eggRepository
* @param \Pterodactyl\Services\Servers\EnvironmentService $environmentService
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
* @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $structureService
* @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository
* @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService
*/
public function __construct(
ConnectionInterface $connection,
EggRepositoryInterface $eggRepository,
EnvironmentService $environmentService,
ServerRepositoryInterface $repository,
ServerConfigurationStructureService $structureService,
ServerVariableRepositoryInterface $serverVariableRepository,
VariableValidatorService $validatorService
) {
public function __construct(ConnectionInterface $connection, VariableValidatorService $validatorService)
{
$this->connection = $connection;
$this->eggRepository = $eggRepository;
$this->environmentService = $environmentService;
$this->repository = $repository;
$this->serverVariableRepository = $serverVariableRepository;
$this->validatorService = $validatorService;
$this->structureService = $structureService;
}
/**
@ -85,34 +43,42 @@ class StartupModificationService
* @param array $data
* @return \Pterodactyl\Models\Server
*
* @throws \Illuminate\Validation\ValidationException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Throwable
*/
public function handle(Server $server, array $data): Server
{
$this->connection->beginTransaction();
if (! is_null(array_get($data, 'environment'))) {
$this->validatorService->setUserLevel($this->getUserLevel());
$results = $this->validatorService->handle(array_get($data, 'egg_id', $server->egg_id), array_get($data, 'environment', []));
return $this->connection->transaction(function () use ($server, $data) {
if (! empty($data['environment'])) {
$egg = $this->isUserLevel(User::USER_LEVEL_ADMIN) ? ($data['egg_id'] ?? $server->egg_id) : $server->egg_id;
$results->each(function ($result) use ($server) {
$this->serverVariableRepository->withoutFreshModel()->updateOrCreate([
'server_id' => $server->id,
'variable_id' => $result->id,
], [
'variable_value' => $result->value ?? '',
]);
});
}
$results = $this->validatorService
->setUserLevel($this->getUserLevel())
->handle($egg, $data['environment']);
if ($this->isUserLevel(User::USER_LEVEL_ADMIN)) {
$this->updateAdministrativeSettings($data, $server);
}
foreach ($results as $result) {
ServerVariable::query()->updateOrCreate(
[
'server_id' => $server->id,
'variable_id' => $result->id,
],
['variable_value' => $result->value ?? '']
);
}
}
$this->connection->commit();
if ($this->isUserLevel(User::USER_LEVEL_ADMIN)) {
$this->updateAdministrativeSettings($data, $server);
}
return $server;
// Calling ->refresh() rather than ->fresh() here causes it to return the
// variables as triplicates for some reason? Not entirely sure, should dig
// in more to figure it out, but luckily we have a test case covering this
// specific call so we can be assured we're not breaking it _here_ at least.
//
// TODO(dane): this seems like a red-flag for the code powering the relationship
// that should be looked into more.
return $server->fresh();
});
}
/**
@ -120,28 +86,25 @@ class StartupModificationService
*
* @param array $data
* @param \Pterodactyl\Models\Server $server
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
private function updateAdministrativeSettings(array $data, Server &$server)
protected function updateAdministrativeSettings(array $data, Server &$server)
{
if (
is_digit(array_get($data, 'egg_id'))
&& $data['egg_id'] != $server->egg_id
&& is_null(array_get($data, 'nest_id'))
) {
$egg = $this->eggRepository->setColumns(['id', 'nest_id'])->find($data['egg_id']);
$data['nest_id'] = $egg->nest_id;
$eggId = Arr::get($data, 'egg_id');
if (is_digit($eggId) && $server->egg_id !== (int)$eggId) {
/** @var \Pterodactyl\Models\Egg $egg */
$egg = Egg::query()->findOrFail($data['egg_id']);
$server = $server->forceFill([
'egg_id' => $egg->id,
'nest_id' => $egg->nest_id,
]);
}
$server = $this->repository->update($server->id, [
'installed' => 0,
'startup' => array_get($data, 'startup', $server->startup),
'nest_id' => array_get($data, 'nest_id', $server->nest_id),
'egg_id' => array_get($data, 'egg_id', $server->egg_id),
'skip_scripts' => array_get($data, 'skip_scripts') ?? isset($data['skip_scripts']),
'image' => array_get($data, 'docker_image', $server->image),
]);
$server->fill([
'startup' => $data['startup'] ?? $server->startup,
'skip_scripts' => $data['skip_scripts'] ?? isset($data['skip_scripts']),
'image' => $data['docker_image'] ?? $server->image,
])->save();
}
}

View file

@ -2,12 +2,10 @@
namespace Pterodactyl\Services\Servers;
use Psr\Log\LoggerInterface;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class SuspensionService
{
@ -19,16 +17,6 @@ class SuspensionService
*/
private $connection;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* @var \Psr\Log\LoggerInterface
*/
private $writer;
/**
* @var \Pterodactyl\Repositories\Wings\DaemonServerRepository
*/
@ -39,25 +27,19 @@ class SuspensionService
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
* @param \Psr\Log\LoggerInterface $writer
*/
public function __construct(
ConnectionInterface $connection,
DaemonServerRepository $daemonServerRepository,
ServerRepositoryInterface $repository,
LoggerInterface $writer
DaemonServerRepository $daemonServerRepository
) {
$this->connection = $connection;
$this->repository = $repository;
$this->writer = $writer;
$this->daemonServerRepository = $daemonServerRepository;
}
/**
* Suspends a server on the system.
*
* @param int|\Pterodactyl\Models\Server $server
* @param \Pterodactyl\Models\Server $server
* @param string $action
*
* @throws \Throwable
@ -66,15 +48,16 @@ class SuspensionService
{
Assert::oneOf($action, [self::ACTION_SUSPEND, self::ACTION_UNSUSPEND]);
if (
$action === self::ACTION_SUSPEND && $server->suspended ||
$action === self::ACTION_UNSUSPEND && ! $server->suspended
) {
$isSuspending = $action === self::ACTION_SUSPEND;
// Nothing needs to happen if we're suspending the server and it is already
// suspended in the database. Additionally, nothing needs to happen if the server
// is not suspended and we try to un-suspend the instance.
if ($isSuspending === $server->suspended) {
return;
}
$this->connection->transaction(function () use ($action, $server) {
$this->repository->withoutFreshModel()->update($server->id, [
$server->update([
'suspended' => $action === self::ACTION_SUSPEND,
]);

View file

@ -11,32 +11,15 @@ namespace Pterodactyl\Services\Servers;
use Pterodactyl\Models\User;
use Illuminate\Support\Collection;
use Pterodactyl\Models\EggVariable;
use Illuminate\Validation\ValidationException;
use Pterodactyl\Traits\Services\HasUserLevels;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface;
class VariableValidatorService
{
use HasUserLevels;
/**
* @var \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface
*/
private $optionVariableRepository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $serverRepository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface
*/
private $serverVariableRepository;
/**
* @var \Illuminate\Contracts\Validation\Factory
*/
@ -45,20 +28,10 @@ class VariableValidatorService
/**
* VariableValidatorService constructor.
*
* @param \Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface $optionVariableRepository
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository
* @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository
* @param \Illuminate\Contracts\Validation\Factory $validator
*/
public function __construct(
EggVariableRepositoryInterface $optionVariableRepository,
ServerRepositoryInterface $serverRepository,
ServerVariableRepositoryInterface $serverVariableRepository,
ValidationFactory $validator
) {
$this->optionVariableRepository = $optionVariableRepository;
$this->serverRepository = $serverRepository;
$this->serverVariableRepository = $serverVariableRepository;
public function __construct(ValidationFactory $validator)
{
$this->validator = $validator;
}
@ -72,16 +45,18 @@ class VariableValidatorService
*/
public function handle(int $egg, array $fields = []): Collection
{
$variables = $this->optionVariableRepository->findWhere([['egg_id', '=', $egg]]);
$query = EggVariable::query()->where('egg_id', $egg);
if (! $this->isUserLevel(User::USER_LEVEL_ADMIN)) {
// Don't attempt to validate variables if they aren't user editable
// and we're not running this at an admin level.
$query = $query->where('user_editable', true)->where('user_viewable', true);
}
/** @var \Pterodactyl\Models\EggVariable[] $variables */
$variables = $query->get();
$data = $rules = $customAttributes = [];
foreach ($variables as $variable) {
// Don't attempt to validate variables if they aren't user editable
// and we're not running this at an admin level.
if (! $variable->user_editable && ! $this->isUserLevel(User::USER_LEVEL_ADMIN)) {
continue;
}
$data['environment'][$variable->env_variable] = array_get($fields, $variable->env_variable);
$rules['environment.' . $variable->env_variable] = $variable->rules;
$customAttributes['environment.' . $variable->env_variable] = trans('validation.internal.variable_value', ['env' => $variable->name]);
@ -92,23 +67,12 @@ class VariableValidatorService
throw new ValidationException($validator);
}
$response = $variables->filter(function ($item) {
// Skip doing anything if user is not an admin and variable is not user viewable or editable.
if (! $this->isUserLevel(User::USER_LEVEL_ADMIN) && (! $item->user_editable || ! $item->user_viewable)) {
return false;
}
return true;
})->map(function ($item) use ($fields) {
return (object) [
return Collection::make($variables)->map(function ($item) use ($fields) {
return (object)[
'id' => $item->id,
'key' => $item->env_variable,
'value' => array_get($fields, $item->env_variable),
'value' => $fields[$item->env_variable] ?? null,
];
})->filter(function ($item) {
return is_object($item);
});
return $response;
}
}

View file

@ -2,6 +2,7 @@
namespace Pterodactyl\Services\Subusers;
use Illuminate\Support\Str;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Subuser;
use Illuminate\Database\ConnectionInterface;
@ -84,9 +85,13 @@ class SubuserCreationService
throw new ServerSubuserExistsException(trans('exceptions.subusers.subuser_exists'));
}
} catch (RecordNotFoundException $exception) {
// Just cap the username generated at 64 characters at most and then append a random string
// to the end to make it "unique"...
$username = substr(preg_replace('/([^\w\.-]+)/', '', strtok($email, '@')), 0, 64) . Str::random(3);
$user = $this->userCreationService->handle([
'email' => $email,
'username' => preg_replace('/([^\w\.-]+)/', '', strtok($email, '@')) . str_random(3),
'username' => $username,
'name_first' => 'Server',
'name_last' => 'Subuser',
'root_admin' => false,

View file

@ -44,6 +44,13 @@ class NodeTransformer extends BaseTransformer
$response[$node->getUpdatedAtColumn()] = $this->formatTimestamp($node->updated_at);
$response[$node->getCreatedAtColumn()] = $this->formatTimestamp($node->created_at);
$resources = $node->servers()->select(['memory', 'disk'])->get();
$response['allocated_resources'] = [
'memory' => $resources->sum('memory'),
'disk' => $resources->sum('disk'),
];
return $response;
}

View file

@ -37,9 +37,7 @@ class SubuserTransformer extends BaseTransformer
'id' => $subuser->id,
'user_id' => $subuser->user_id,
'server_id' => $subuser->server_id,
'permissions' => $subuser->permissions->map(function (Permission $permission) {
return $permission->permission;
}),
'permissions' => $subuser->permissions,
'created_at' => $this->formatTimestamp($subuser->created_at),
'updated_at' => $this->formatTimestamp($subuser->updated_at),
];

View file

@ -2,6 +2,8 @@
namespace Pterodactyl\Transformers\Api\Client;
use BadMethodCallException;
use InvalidArgumentException;
use Pterodactyl\Models\EggVariable;
class EggVariableTransformer extends BaseClientTransformer
@ -20,6 +22,15 @@ class EggVariableTransformer extends BaseClientTransformer
*/
public function transform(EggVariable $variable)
{
// This guards against someone incorrectly retrieving variables (haha, me) and then passing
// them into the transformer and along to the user. Just throw an exception and break the entire
// pathway since you should never be exposing these types of variables to a client.
if (!$variable->user_viewable) {
throw new BadMethodCallException(
'Cannot transform a hidden egg variable in a client transformer.'
);
}
return [
'name' => $variable->name,
'description' => $variable->description,

View file

@ -67,7 +67,7 @@ class ServerTransformer extends BaseClientTransformer
'allocations' => $server->allocation_limit,
'backups' => $server->backup_limit,
],
'is_suspended' => $server->suspended !== 0,
'is_suspended' => $server->suspended,
'is_installing' => $server->installed !== 1,
];
}

View file

@ -9,7 +9,7 @@ return [
| change this value if you are not maintaining your own internal versions.
*/
'version' => 'canary',
'version' => '1.0.1',
/*
|--------------------------------------------------------------------------

View file

@ -13,7 +13,7 @@ return [
*/
'rate_limit' => [
'client_period' => 1,
'client' => env('APP_API_CLIENT_RATELIMIT', 240),
'client' => env('APP_API_CLIENT_RATELIMIT', 720),
'application_period' => 1,
'application' => env('APP_API_APPLICATION_RATELIMIT', 240),

View file

@ -1,5 +1,6 @@
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
return [
@ -75,5 +76,10 @@ return [
'driver' => 'errorlog',
'level' => 'debug',
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
],
];

View file

@ -1,5 +1,6 @@
<?php
use Carbon\Carbon;
use Ramsey\Uuid\Uuid;
use Cake\Chronos\Chronos;
use Illuminate\Support\Str;
@ -7,6 +8,7 @@ use Pterodactyl\Models\Node;
use Faker\Generator as Faker;
use Pterodactyl\Models\ApiKey;
/** @var \Illuminate\Database\Eloquent\Factory $factory */
/*
|--------------------------------------------------------------------------
| Model Factories
@ -35,8 +37,8 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker $faker) {
'installed' => 1,
'database_limit' => null,
'allocation_limit' => null,
'created_at' => \Carbon\Carbon::now(),
'updated_at' => \Carbon\Carbon::now(),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];
});
@ -160,8 +162,8 @@ $factory->define(Pterodactyl\Models\Database::class, function (Faker $faker) {
'username' => str_random(10),
'remote' => '%',
'password' => $password ?: bcrypt('test123'),
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
'created_at' => Carbon::now()->toDateTimeString(),
'updated_at' => Carbon::now()->toDateTimeString(),
];
});
@ -190,7 +192,7 @@ $factory->define(Pterodactyl\Models\ApiKey::class, function (Faker $faker) {
'token' => $token ?: $token = encrypt(str_random(Pterodactyl\Models\ApiKey::KEY_LENGTH)),
'allowed_ips' => null,
'memo' => 'Test Function Key',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
'created_at' => Carbon::now()->toDateTimeString(),
'updated_at' => Carbon::now()->toDateTimeString(),
];
});

View file

@ -1,12 +1,64 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Collection;
use Pterodactyl\Models\Permission;
use Illuminate\Support\Facades\Schema;
use Pterodactyl\Models\Permission as P;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class MergePermissionsTableIntoSubusers extends Migration
{
/**
* A list of all pre-1.0 permissions available to a user and their associated
* casting for the new permissions system.
*
* @var array
*/
protected static $permissionsMap = [
'power-start' => P::ACTION_CONTROL_START,
'power-stop' => P::ACTION_CONTROL_STOP,
'power-restart' => P::ACTION_CONTROL_RESTART,
'power-kill' => P::ACTION_CONTROL_STOP,
'send-command' => P::ACTION_CONTROL_CONSOLE,
'list-subusers' => P::ACTION_USER_READ,
'view-subuser' => P::ACTION_USER_READ,
'edit-subuser' => P::ACTION_USER_UPDATE,
'create-subuser' => P::ACTION_USER_CREATE,
'delete-subuser' => P::ACTION_USER_DELETE,
'view-allocations' => P::ACTION_ALLOCATION_READ,
'edit-allocation' => P::ACTION_ALLOCATION_UPDATE,
'view-startup' => P::ACTION_STARTUP_READ,
'edit-startup' => P::ACTION_STARTUP_UPDATE,
'view-databases' => P::ACTION_DATABASE_READ,
// Better to just break this flow a bit than accidentally grant a dangerous permission.
'reset-db-password' => P::ACTION_DATABASE_UPDATE,
'delete-database' => P::ACTION_DATABASE_DELETE,
'create-database' => P::ACTION_DATABASE_CREATE,
'access-sftp' => P::ACTION_FILE_SFTP,
'list-files' => P::ACTION_FILE_READ,
'edit-files' => P::ACTION_FILE_READ_CONTENT,
'save-files' => P::ACTION_FILE_UPDATE,
'create-files' => P::ACTION_FILE_CREATE,
'delete-files' => P::ACTION_FILE_DELETE,
'compress-files' => P::ACTION_FILE_ARCHIVE,
'list-schedules' => P::ACTION_SCHEDULE_READ,
'view-schedule' => P::ACTION_SCHEDULE_READ,
'edit-schedule' => P::ACTION_SCHEDULE_UPDATE,
'create-schedule' => P::ACTION_SCHEDULE_CREATE,
'delete-schedule' => P::ACTION_SCHEDULE_DELETE,
// Skipping these permissions as they are granted if you have more specific read/write permissions.
'move-files' => null,
'copy-files' => null,
'decompress-files' => null,
'upload-files' => null,
'download-files' => null,
// These permissions do not exist in 1.0
'toggle-schedule' => null,
'queue-schedule' => null,
];
/**
* Run the migrations.
*
@ -27,10 +79,19 @@ class MergePermissionsTableIntoSubusers extends Migration
DB::transaction(function () use (&$cursor) {
$cursor->each(function ($datum) {
DB::update('UPDATE subusers SET permissions = ? WHERE id = ?', [
json_encode(explode(',', $datum->permissions)),
$datum->subuser_id,
]);
$updated = Collection::make(explode(',', $datum->permissions))
->map(function ($value) {
return self::$permissionsMap[$value] ?? null;
})->filter(function ($value) {
return !is_null($value) && $value !== Permission::ACTION_WEBSOCKET_CONNECT;
})
// All subusers get this permission, so make sure it gets pushed into the array.
->merge([ Permission::ACTION_WEBSOCKET_CONNECT ])
->unique()
->values()
->toJson();
DB::update('UPDATE subusers SET permissions = ? WHERE id = ?', [$updated, $datum->subuser_id]);
});
});
}
@ -42,11 +103,15 @@ class MergePermissionsTableIntoSubusers extends Migration
*/
public function down()
{
$flipped = array_flip(self::$permissionsMap);
foreach (DB::select('SELECT id, permissions FROM subusers') as $datum) {
$values = [];
foreach (json_decode($datum->permissions, true) as $permission) {
$values[] = $datum->id;
$values[] = $permission;
if (!empty($v = $flipped[$permission])) {
$values[] = $datum->id;
$values[] = $v;
}
}
if (! empty($values)) {

View file

@ -13,6 +13,10 @@ class AddTableServerTransfers extends Migration
*/
public function up()
{
// Nuclear approach to whatever plugins are out there and not properly namespacing their own tables
// leading to constant support requests from people...
Schema::dropIfExists('server_transfers');
Schema::create('server_transfers', function (Blueprint $table) {
$table->increments('id');
$table->integer('server_id')->unsigned();

View file

@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ChangeUniqueDatabaseNameToAccountForServer extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('databases', function (Blueprint $table) {
$table->dropUnique(['database_host_id', 'database']);
});
Schema::table('databases', function (Blueprint $table) {
$table->unique(['database_host_id', 'server_id', 'database']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('databases', function (Blueprint $table) {
$table->dropUnique(['database_host_id', 'server_id', 'database']);
});
Schema::table('databases', function (Blueprint $table) {
$table->unique(['database_host_id', 'database']);
});
}
}

View file

@ -130,7 +130,7 @@ class EggSeeder extends Seeder
['nest_id', '=', $nest->id],
]);
$this->updateImporterService->handle($egg->id, $file);
$this->updateImporterService->handle($egg, $file);
$this->command->info('Updated ' . $decoded->name);
} catch (RecordNotFoundException $exception) {

View file

@ -153,6 +153,12 @@ function updateAdditionalAllocations() {
}
function initUserIdSelect(data) {
function escapeHtml(str) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
$('#pUserId').select2({
ajax: {
url: '/admin/users/accounts.json',
@ -176,28 +182,27 @@ function initUserIdSelect(data) {
data: data,
escapeMarkup: function (markup) { return markup; },
minimumInputLength: 2,
templateResult: function (data) {
if (data.loading) return data.text;
if (data.loading) return escapeHtml(data.text);
return '<div class="user-block"> \
<img class="img-circle img-bordered-xs" src="https://www.gravatar.com/avatar/' + data.md5 + '?s=120" alt="User Image"> \
<span class="username"> \
<a href="#">' + data.name_first + ' ' + data.name_last +'</a> \
</span> \
<span class="description"><strong>' + data.email + '</strong> - ' + data.username + '</span> \
</div>';
<img class="img-circle img-bordered-xs" src="https://www.gravatar.com/avatar/' + escapeHtml(data.md5) + '?s=120" alt="User Image"> \
<span class="username"> \
<a href="#">' + escapeHtml(data.name_first) + ' ' + escapeHtml(data.name_last) +'</a> \
</span> \
<span class="description"><strong>' + escapeHtml(data.email) + '</strong> - ' + escapeHtml(data.username) + '</span> \
</div>';
},
templateSelection: function (data) {
return '<div> \
<span> \
<img class="img-rounded img-bordered-xs" src="https://www.gravatar.com/avatar/' + data.md5 + '?s=120" style="height:28px;margin-top:-4px;" alt="User Image"> \
</span> \
<span style="padding-left:5px;"> \
' + data.name_first + ' ' + data.name_last + ' (<strong>' + data.email + '</strong>) \
</span> \
</div>';
<span> \
<img class="img-rounded img-bordered-xs" src="https://www.gravatar.com/avatar/' + escapeHtml(data.md5) + '?s=120" style="height:28px;margin-top:-4px;" alt="User Image"> \
</span> \
<span style="padding-left:5px;"> \
' + escapeHtml(data.name_first) + ' ' + escapeHtml(data.name_last) + ' (<strong>' + escapeHtml(data.email) + '</strong>) \
</span> \
</div>';
}
});
}

View file

@ -1,113 +0,0 @@
// Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.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.
(function initSocket() {
if (typeof $.notifyDefaults !== 'function') {
console.error('Notify does not appear to be loaded.');
return;
}
if (typeof io !== 'function') {
console.error('Socket.io is reqired to use this panel.');
return;
}
$.notifyDefaults({
placement: {
from: 'bottom',
align: 'right'
},
newest_on_top: true,
delay: 2000,
animate: {
enter: 'animated zoomInDown',
exit: 'animated zoomOutDown'
}
});
var notifySocketError = false;
// Main Socket Object
window.Socket = io(Pterodactyl.node.scheme + '://' + Pterodactyl.node.fqdn + ':' + Pterodactyl.node.daemonListen + '/v1/stats/', {
'query': 'token=' + Pterodactyl.node.daemonSecret,
});
// Socket Failed to Connect
Socket.io.on('connect_error', function (err) {
if(typeof notifySocketError !== 'object') {
notifySocketError = $.notify({
message: 'There was an error attempting to establish a WebSocket connection to the Daemon. This panel will not work as expected.<br /><br />' + err,
}, {
type: 'danger',
delay: 0
});
}
});
// Connected to Socket Successfully
Socket.on('connect', function () {
if (notifySocketError !== false) {
notifySocketError.close();
notifySocketError = false;
}
});
Socket.on('error', function (err) {
console.error('There was an error while attemping to connect to the websocket: ' + err + '\n\nPlease try loading this page again.');
});
Socket.on('live-stats', function (data) {
$.each(data.servers, function (uuid, info) {
var element = $('tr[data-server="' + uuid + '"]');
switch (info.status) {
case 0:
element.find('[data-action="status"]').html('<span class="label label-danger">Offline</span>');
break;
case 1:
element.find('[data-action="status"]').html('<span class="label label-success">Online</span>');
break;
case 2:
element.find('[data-action="status"]').html('<span class="label label-info">Starting</span>');
break;
case 3:
element.find('[data-action="status"]').html('<span class="label label-info">Stopping</span>');
break;
case 20:
element.find('[data-action="status"]').html('<span class="label label-warning">Installing</span>');
break;
case 30:
element.find('[data-action="status"]').html('<span class="label label-warning">Suspended</span>');
break;
}
if (info.status !== 0) {
var cpuMax = element.find('[data-action="cpu"]').data('cpumax');
var currentCpu = info.proc.cpu.total;
if (cpuMax !== 0) {
currentCpu = parseFloat(((info.proc.cpu.total / cpuMax) * 100).toFixed(2).toString());
}
element.find('[data-action="memory"]').html(parseInt(info.proc.memory.total / (1024 * 1024)));
element.find('[data-action="disk"]').html(parseInt(info.proc.disk.used));
element.find('[data-action="cpu"]').html(currentCpu);
} else {
element.find('[data-action="memory"]').html('--');
element.find('[data-action="disk"]').html('--');
element.find('[data-action="cpu"]').html('--');
}
});
});
})();

View file

@ -9,7 +9,6 @@ interface Response {
}
export default (uuid: string, initialData?: Response) => useSWR([ uuid, '/startup' ], async (): Promise<Response> => {
console.log('firing getServerStartup');
const { data } = await http.get(`/api/client/servers/${uuid}/startup`);
const variables = ((data as FractalResponseList).data || []).map(rawDataToServerEggVariable);

View file

@ -94,7 +94,6 @@ export default () => {
}
<div css={tw`mt-6 text-center`}>
<Link
type={'button'}
to={'/auth/login'}
css={tw`text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`}
>

View file

@ -14,26 +14,8 @@ import { httpErrorToHuman } from '@/api/http';
import { format } from 'date-fns';
import PageContentBlock from '@/components/elements/PageContentBlock';
import tw from 'twin.macro';
import { breakpoint } from '@/theme';
import styled from 'styled-components/macro';
import GreyRowBox from '@/components/elements/GreyRowBox';
const Container = styled.div`
${tw`flex flex-wrap my-10`};
& > div {
${tw`w-full`};
${breakpoint('md')`
width: calc(50% - 1rem);
`}
${breakpoint('xl')`
${tw`w-auto flex-1`};
`}
}
`;
export default () => {
const [ deleteIdentifier, setDeleteIdentifier ] = useState('');
const [ keys, setKeys ] = useState<ApiKey[]>([]);
@ -67,12 +49,12 @@ export default () => {
return (
<PageContentBlock title={'Account API'}>
<FlashMessageRender byKey={'account'} css={tw`mb-4`}/>
<Container>
<ContentBox title={'Create API Key'}>
<FlashMessageRender byKey={'account'}/>
<div css={tw`md:flex flex-no-wrap my-10`}>
<ContentBox title={'Create API Key'} css={tw`flex-none w-full md:w-1/2`}>
<CreateApiKeyForm onKeyCreated={key => setKeys(s => ([ ...s!, key ]))}/>
</ContentBox>
<ContentBox title={'API Keys'} css={tw`mt-8 md:mt-0 md:ml-8`}>
<ContentBox title={'API Keys'} css={tw`flex-1 overflow-hidden mt-8 md:mt-0 md:ml-8`}>
<SpinnerOverlay visible={loading}/>
<ConfirmationModal
visible={!!deleteIdentifier}
@ -99,14 +81,14 @@ export default () => {
css={[ tw`bg-neutral-600 flex items-center`, index > 0 && tw`mt-2` ]}
>
<FontAwesomeIcon icon={faKey} css={tw`text-neutral-300`}/>
<div css={tw`ml-4 flex-1`}>
<p css={tw`text-sm`}>{key.description}</p>
<div css={tw`ml-4 flex-1 overflow-hidden`}>
<p css={tw`text-sm break-words`}>{key.description}</p>
<p css={tw`text-2xs text-neutral-300 uppercase`}>
Last used:&nbsp;
{key.lastUsedAt ? format(key.lastUsedAt, 'MMM do, yyyy HH:mm') : 'Never'}
</p>
</div>
<p css={tw`text-sm ml-4`}>
<p css={tw`text-sm ml-4 hidden md:block`}>
<code css={tw`font-mono py-1 px-2 bg-neutral-900 rounded`}>
{key.identifier}
</code>
@ -124,7 +106,7 @@ export default () => {
))
}
</ContentBox>
</Container>
</div>
</PageContentBlock>
);
};

View file

@ -1,21 +1,43 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { memo, useEffect, useRef, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEthernet, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons';
import { Link } from 'react-router-dom';
import { Server } from '@/api/server/getServer';
import getServerResourceUsage, { ServerStats } from '@/api/server/getServerResourceUsage';
import getServerResourceUsage, { ServerPowerState, ServerStats } from '@/api/server/getServerResourceUsage';
import { bytesToHuman, megabytesToHuman } from '@/helpers';
import tw from 'twin.macro';
import GreyRowBox from '@/components/elements/GreyRowBox';
import Spinner from '@/components/elements/Spinner';
import styled from 'styled-components/macro';
import isEqual from 'react-fast-compare';
// Determines if the current value is in an alarm threshold so we can show it in red rather
// than the more faded default style.
const isAlarmState = (current: number, limit: number): boolean => {
const limitInBytes = limit * 1024 * 1024;
const isAlarmState = (current: number, limit: number): boolean => limit > 0 && (current / (limit * 1024 * 1024) >= 0.90);
return current / limitInBytes >= 0.90;
};
const Icon = memo(styled(FontAwesomeIcon)<{ $alarm: boolean }>`
${props => props.$alarm ? tw`text-red-400` : tw`text-neutral-500`};
`, isEqual);
const IconDescription = styled.p<{ $alarm: boolean }>`
${tw`text-sm ml-2`};
${props => props.$alarm ? tw`text-white` : tw`text-neutral-400`};
`;
const StatusIndicatorBox = styled(GreyRowBox)<{ $status: ServerPowerState | undefined }>`
${tw`grid grid-cols-12 gap-4 relative`};
& .status-bar {
${tw`w-2 bg-red-500 absolute right-0 z-20 rounded-full m-1 opacity-50 transition-all duration-150`};
height: calc(100% - 0.5rem);
${({ $status }) => (!$status || $status === 'offline') ? tw`bg-red-500` : ($status === 'running' ? tw`bg-green-500` : tw`bg-yellow-500`)};
}
&:hover .status-bar {
${tw`opacity-75`};
}
`;
export default ({ server, className }: { server: Server; className?: string }) => {
const interval = useRef<number>(null);
@ -54,29 +76,31 @@ export default ({ server, className }: { server: Server; className?: string }) =
const memorylimit = server.limits.memory !== 0 ? megabytesToHuman(server.limits.memory) : 'Unlimited';
return (
<GreyRowBox as={Link} to={`/server/${server.id}`} className={className}>
<div className={'icon'} css={tw`hidden md:block`}>
<FontAwesomeIcon icon={faServer}/>
</div>
<div css={tw`flex-1 md:ml-4`}>
<p css={tw`text-lg`}>{server.name}</p>
{!!server.description &&
<p css={tw`text-sm text-neutral-300`}>{server.description}</p>
}
</div>
<div css={tw`w-48 overflow-hidden self-start hidden lg:block`}>
<div css={tw`flex ml-4 justify-end`}>
<FontAwesomeIcon icon={faEthernet} css={tw`text-neutral-500`}/>
<p css={tw`text-sm text-neutral-400 ml-2`}>
{
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || allocation.ip}:{allocation.port}</span>
))
}
</p>
<StatusIndicatorBox as={Link} to={`/server/${server.id}`} className={className} $status={stats?.status}>
<div css={tw`flex items-center col-span-12 sm:col-span-5 lg:col-span-6`}>
<div className={'icon'} css={tw`mr-4`}>
<FontAwesomeIcon icon={faServer}/>
</div>
<div>
<p css={tw`text-lg break-words`}>{server.name}</p>
{!!server.description &&
<p css={tw`text-sm text-neutral-300 break-words`}>{server.description}</p>
}
</div>
</div>
<div css={tw`w-1/3 sm:w-1/2 lg:w-1/3 flex items-baseline justify-center relative`}>
<div css={tw`hidden lg:col-span-2 lg:flex ml-4 justify-end h-full`}>
<FontAwesomeIcon icon={faEthernet} css={tw`text-neutral-500`}/>
<p css={tw`text-sm text-neutral-400 ml-2`}>
{
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
<React.Fragment key={allocation.ip + allocation.port.toString()}>
{allocation.alias || allocation.ip}:{allocation.port}
</React.Fragment>
))
}
</p>
</div>
<div css={tw`hidden col-span-7 lg:col-span-4 sm:flex items-baseline justify-center`}>
{!stats ?
!statsError ?
<Spinner size={'small'}/>
@ -96,80 +120,33 @@ export default ({ server, className }: { server: Server; className?: string }) =
:
<React.Fragment>
<div css={tw`flex-1 flex md:ml-4 sm:flex hidden justify-center`}>
<FontAwesomeIcon
icon={faMicrochip}
css={[
!alarms.cpu && tw`text-neutral-500`,
alarms.cpu && tw`text-red-400`,
]}
/>
<p
css={[
tw`text-sm ml-2`,
!alarms.cpu && tw`text-neutral-400`,
alarms.cpu && tw`text-white`,
]}
>
<Icon icon={faMicrochip} $alarm={alarms.cpu}/>
<IconDescription $alarm={alarms.cpu}>
{stats.cpuUsagePercent} %
</p>
</IconDescription>
</div>
<div css={tw`flex-1 ml-4 sm:block hidden`}>
<div css={tw`flex justify-center`}>
<FontAwesomeIcon
icon={faMemory}
css={[
!alarms.memory && tw`text-neutral-500`,
alarms.memory && tw`text-red-400`,
]}
/>
<p
css={[
tw`text-sm ml-2`,
!alarms.memory && tw`text-neutral-400`,
alarms.memory && tw`text-white`,
]}
>
<Icon icon={faMemory} $alarm={alarms.memory}/>
<IconDescription $alarm={alarms.memory}>
{bytesToHuman(stats.memoryUsageInBytes)}
</p>
</IconDescription>
</div>
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {memorylimit}</p>
</div>
<div css={tw`flex-1 ml-4 sm:block hidden`}>
<div css={tw`flex justify-center`}>
<FontAwesomeIcon
icon={faHdd}
css={[
!alarms.disk && tw`text-neutral-500`,
alarms.disk && tw`text-red-400`,
]}
/>
<p
css={[
tw`text-sm ml-2`,
!alarms.disk && tw`text-neutral-400`,
alarms.disk && tw`text-white`,
]}
>
<Icon icon={faHdd} $alarm={alarms.disk}/>
<IconDescription $alarm={alarms.disk}>
{bytesToHuman(stats.diskUsageInBytes)}
</p>
</IconDescription>
</div>
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {disklimit}</p>
</div>
<div css={tw`flex-1 flex justify-end sm:hidden`}>
<div css={tw`flex items-end text-right`}>
<div
css={[
tw`w-3 h-3 rounded-full`,
(!stats?.status || stats?.status === 'offline')
? tw`bg-red-500`
: (stats?.status === 'running' ? tw`bg-green-500` : tw`bg-yellow-500`),
]}
/>
</div>
</div>
</React.Fragment>
}
</div>
</GreyRowBox>
<div className={'status-bar'}/>
</StatusIndicatorBox>
);
};

View file

@ -69,6 +69,7 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
{({ isSubmitting }) => (
<Modal
{...props}
top={false}
onDismissed={dismiss}
dismissable={!isSubmitting}
showSpinnerOverlay={loading || isSubmitting}

View file

@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import CodeMirror from 'codemirror';
import styled from 'styled-components/macro';
import tw from 'twin.macro';
import modes, { Mode } from '@/modes';
import modes from '@/modes';
require('codemirror/lib/codemirror.css');
require('codemirror/theme/ayu-mirage.css');

View file

@ -2,11 +2,11 @@ import styled from 'styled-components/macro';
import tw from 'twin.macro';
export default styled.div<{ $hoverable?: boolean }>`
${tw`flex rounded no-underline text-neutral-200 items-center bg-neutral-700 p-4 border border-transparent transition-colors duration-150`};
${tw`flex rounded no-underline text-neutral-200 items-center bg-neutral-700 p-4 border border-transparent transition-colors duration-150 overflow-hidden`};
${props => props.$hoverable !== false && tw`hover:border-neutral-500`};
& > div.icon {
& .icon {
${tw`rounded-full bg-neutral-500 p-3`};
}
`;

View file

@ -36,6 +36,8 @@ const ModalContainer = styled.div<{ alignTop?: boolean }>`
${breakpoint('md')`margin-top: 10%`};
`};
margin-bottom: auto;
& > .close-icon {
${tw`absolute right-0 p-2 text-white cursor-pointer opacity-50 transition-all duration-150 ease-linear hover:opacity-100`};
top: -2.5rem;

View file

@ -1,7 +1,8 @@
import React from 'react';
import React, { memo } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import tw from 'twin.macro';
import isEqual from 'react-fast-compare';
interface Props {
icon?: IconProp;
@ -27,4 +28,4 @@ const TitledGreyBox = ({ icon, title, children, className }: Props) => (
</div>
);
export default TitledGreyBox;
export default memo(TitledGreyBox, isEqual);

Some files were not shown because too many files have changed in this diff Show more