Merge branch 'develop' into feature/cross-env
This commit is contained in:
commit
0a53e75fd1
149 changed files with 5712 additions and 2350 deletions
1
.php_cs
1
.php_cs
|
@ -33,6 +33,7 @@ return PhpCsFixer\Config::create()
|
|||
'new_with_braces' => false,
|
||||
'no_alias_functions' => true,
|
||||
'no_multiline_whitespace_before_semicolons' => true,
|
||||
'no_superfluous_phpdoc_tags' => false,
|
||||
'no_unreachable_default_argument_value' => true,
|
||||
'no_useless_return' => true,
|
||||
'not_operator_with_successor_space' => true,
|
||||
|
|
177
app/Http/Controllers/Admin/Servers/ServerTransferController.php
Normal file
177
app/Http/Controllers/Admin/Servers/ServerTransferController.php
Normal file
|
@ -0,0 +1,177 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Admin\Servers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Pterodactyl\Models\ServerTransfer;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Pterodactyl\Services\Servers\TransferService;
|
||||
use Pterodactyl\Services\Servers\SuspensionService;
|
||||
use Pterodactyl\Repositories\Eloquent\NodeRepository;
|
||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||
use Pterodactyl\Repositories\Eloquent\LocationRepository;
|
||||
use Pterodactyl\Repositories\Wings\DaemonConfigurationRepository;
|
||||
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
|
||||
|
||||
class ServerTransferController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var \Prologue\Alerts\AlertsMessageBag
|
||||
*/
|
||||
private $alert;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
|
||||
*/
|
||||
private $allocationRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\LocationRepository
|
||||
*/
|
||||
private $locationRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\NodeRepository
|
||||
*/
|
||||
private $nodeRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Servers\SuspensionService
|
||||
*/
|
||||
private $suspensionService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Servers\TransferService
|
||||
*/
|
||||
private $transferService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Wings\DaemonConfigurationRepository
|
||||
*/
|
||||
private $daemonConfigurationRepository;
|
||||
|
||||
/**
|
||||
* ServerTransferController constructor.
|
||||
*
|
||||
* @param \Prologue\Alerts\AlertsMessageBag $alert
|
||||
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\LocationRepository $locationRepository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\NodeRepository $nodeRepository
|
||||
* @param \Pterodactyl\Services\Servers\SuspensionService $suspensionService
|
||||
* @param \Pterodactyl\Services\Servers\TransferService $transferService
|
||||
* @param \Pterodactyl\Repositories\Wings\DaemonConfigurationRepository $daemonConfigurationRepository
|
||||
*/
|
||||
public function __construct(
|
||||
AlertsMessageBag $alert,
|
||||
AllocationRepositoryInterface $allocationRepository,
|
||||
ServerRepository $repository,
|
||||
LocationRepository $locationRepository,
|
||||
NodeRepository $nodeRepository,
|
||||
SuspensionService $suspensionService,
|
||||
TransferService $transferService,
|
||||
DaemonConfigurationRepository $daemonConfigurationRepository
|
||||
) {
|
||||
$this->alert = $alert;
|
||||
$this->allocationRepository = $allocationRepository;
|
||||
$this->repository = $repository;
|
||||
$this->locationRepository = $locationRepository;
|
||||
$this->nodeRepository = $nodeRepository;
|
||||
$this->suspensionService = $suspensionService;
|
||||
$this->transferService = $transferService;
|
||||
$this->daemonConfigurationRepository = $daemonConfigurationRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a transfer of a server to a new node.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function transfer(Request $request, Server $server)
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'node_id' => 'required|exists:nodes,id',
|
||||
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
|
||||
'allocation_additional' => 'nullable',
|
||||
]);
|
||||
|
||||
$node_id = $validatedData['node_id'];
|
||||
$allocation_id = intval($validatedData['allocation_id']);
|
||||
$additional_allocations = array_map('intval', $validatedData['allocation_additional'] ?? []);
|
||||
|
||||
// Check if the node is viable for the transfer.
|
||||
$node = $this->nodeRepository->getNodeWithResourceUsage($node_id);
|
||||
if ($node->isViable($server->memory, $server->disk)) {
|
||||
// Check if the selected daemon is online.
|
||||
$this->daemonConfigurationRepository->setNode($node)->getSystemInformation();
|
||||
|
||||
// Suspend the server and request an archive to be created.
|
||||
$this->suspensionService->toggle($server, 'suspend');
|
||||
|
||||
// Create a new ServerTransfer entry.
|
||||
$transfer = new ServerTransfer;
|
||||
|
||||
$transfer->server_id = $server->id;
|
||||
$transfer->old_node = $server->node_id;
|
||||
$transfer->new_node = $node_id;
|
||||
$transfer->old_allocation = $server->allocation_id;
|
||||
$transfer->new_allocation = $allocation_id;
|
||||
$transfer->old_additional_allocations = json_encode($server->allocations->where('id', '!=', $server->allocation_id)->pluck('id'));
|
||||
$transfer->new_additional_allocations = json_encode($additional_allocations);
|
||||
|
||||
$transfer->save();
|
||||
|
||||
// Add the allocations to the server so they cannot be automatically assigned while the transfer is in progress.
|
||||
$this->assignAllocationsToServer($server, $node_id, $allocation_id, $additional_allocations);
|
||||
|
||||
// Request an archive from the server's current daemon. (this also checks if the daemon is online)
|
||||
$this->transferService->requestArchive($server);
|
||||
|
||||
$this->alert->success(trans('admin/server.alerts.transfer_started'))->flash();
|
||||
} else {
|
||||
$this->alert->danger(trans('admin/server.alerts.transfer_not_viable'))->flash();
|
||||
}
|
||||
|
||||
return redirect()->route('admin.servers.view.manage', $server->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the specified allocations to the specified server.
|
||||
*
|
||||
* @param Server $server
|
||||
* @param int $node_id
|
||||
* @param int $allocation_id
|
||||
* @param array $additional_allocations
|
||||
*/
|
||||
private function assignAllocationsToServer(Server $server, int $node_id, int $allocation_id, array $additional_allocations)
|
||||
{
|
||||
$allocations = $additional_allocations;
|
||||
array_push($allocations, $allocation_id);
|
||||
|
||||
$unassigned = $this->allocationRepository->getUnassignedAllocationIds($node_id);
|
||||
|
||||
$updateIds = [];
|
||||
foreach ($allocations as $allocation) {
|
||||
if (! in_array($allocation, $unassigned)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$updateIds[] = $allocation;
|
||||
}
|
||||
|
||||
if (! empty($updateIds)) {
|
||||
$this->allocationRepository->updateWhereIn('id', $updateIds, ['server_id' => $server->id]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Pterodactyl\Http\Controllers\Admin\Servers;
|
||||
|
||||
use JavaScript;
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Nest;
|
||||
use Pterodactyl\Models\Server;
|
||||
|
@ -9,8 +10,10 @@ use Illuminate\Contracts\View\Factory;
|
|||
use Pterodactyl\Exceptions\DisplayException;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Pterodactyl\Repositories\Eloquent\NestRepository;
|
||||
use Pterodactyl\Repositories\Eloquent\NodeRepository;
|
||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||
use Pterodactyl\Traits\Controllers\JavascriptInjection;
|
||||
use Pterodactyl\Repositories\Eloquent\LocationRepository;
|
||||
use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository;
|
||||
|
||||
class ServerViewController extends Controller
|
||||
|
@ -37,17 +40,31 @@ class ServerViewController extends Controller
|
|||
*/
|
||||
private $nestRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\LocationRepository
|
||||
*/
|
||||
private $locationRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\NodeRepository
|
||||
*/
|
||||
private $nodeRepository;
|
||||
|
||||
/**
|
||||
* ServerViewController constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Repositories\Eloquent\DatabaseHostRepository $databaseHostRepository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\NestRepository $nestRepository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\LocationRepository $locationRepository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\NodeRepository $nodeRepository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
|
||||
* @param \Illuminate\Contracts\View\Factory $view
|
||||
*/
|
||||
public function __construct(
|
||||
DatabaseHostRepository $databaseHostRepository,
|
||||
NestRepository $nestRepository,
|
||||
LocationRepository $locationRepository,
|
||||
NodeRepository $nodeRepository,
|
||||
ServerRepository $repository,
|
||||
Factory $view
|
||||
) {
|
||||
|
@ -55,6 +72,8 @@ class ServerViewController extends Controller
|
|||
$this->databaseHostRepository = $databaseHostRepository;
|
||||
$this->repository = $repository;
|
||||
$this->nestRepository = $nestRepository;
|
||||
$this->nodeRepository = $nodeRepository;
|
||||
$this->locationRepository = $locationRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -150,6 +169,7 @@ class ServerViewController extends Controller
|
|||
* @return \Illuminate\Contracts\View\View
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function manage(Request $request, Server $server)
|
||||
{
|
||||
|
@ -159,7 +179,22 @@ class ServerViewController extends Controller
|
|||
);
|
||||
}
|
||||
|
||||
return $this->view->make('admin.servers.view.manage', compact('server'));
|
||||
// Check if the panel doesn't have at least 2 nodes configured.
|
||||
$nodes = $this->nodeRepository->all();
|
||||
$canTransfer = false;
|
||||
if (count($nodes) >= 2) {
|
||||
$canTransfer = true;
|
||||
}
|
||||
|
||||
Javascript::put([
|
||||
'nodeData' => $this->nodeRepository->getNodesForServerCreation(),
|
||||
]);
|
||||
|
||||
return $this->view->make('admin.servers.view.manage', [
|
||||
'server' => $server,
|
||||
'locations' => $this->locationRepository->all(),
|
||||
'canTransfer' => $canTransfer,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -262,7 +262,7 @@ class ServersController extends Controller
|
|||
{
|
||||
$this->buildModificationService->handle($server, $request->only([
|
||||
'allocation_id', 'add_allocations', 'remove_allocations',
|
||||
'memory', 'swap', 'io', 'cpu', 'disk',
|
||||
'memory', 'swap', 'io', 'cpu', 'threads', 'disk',
|
||||
'database_limit', 'allocation_limit', 'oom_disabled',
|
||||
]));
|
||||
$this->alert->success(trans('admin/server.alerts.build_updated'))->flash();
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
namespace Pterodactyl\Http\Controllers\Api\Client;
|
||||
|
||||
use Pterodactyl\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||
use Pterodactyl\Transformers\Api\Client\ServerTransformer;
|
||||
|
@ -72,16 +71,10 @@ class ClientController extends ClientApiController
|
|||
*/
|
||||
public function permissions()
|
||||
{
|
||||
$permissions = Permission::permissions()->map(function ($values, $key) {
|
||||
return Collection::make($values)->map(function ($permission) use ($key) {
|
||||
return $key . '.' . $permission;
|
||||
})->values()->toArray();
|
||||
})->flatten();
|
||||
|
||||
return [
|
||||
'object' => 'system_permissions',
|
||||
'attributes' => [
|
||||
'permissions' => $permissions,
|
||||
'permissions' => Permission::permissions(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
77
app/Http/Controllers/Api/Client/Servers/BackupController.php
Normal file
77
app/Http/Controllers/Api/Client/Servers/BackupController.php
Normal file
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Services\Backups\InitiateBackupService;
|
||||
use Pterodactyl\Transformers\Api\Client\BackupTransformer;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\GetBackupsRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\StoreBackupRequest;
|
||||
|
||||
class BackupController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Backups\InitiateBackupService
|
||||
*/
|
||||
private $initiateBackupService;
|
||||
|
||||
/**
|
||||
* BackupController constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Services\Backups\InitiateBackupService $initiateBackupService
|
||||
*/
|
||||
public function __construct(InitiateBackupService $initiateBackupService)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->initiateBackupService = $initiateBackupService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all of the backups for a given server instance in a paginated
|
||||
* result set.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Backups\GetBackupsRequest $request
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @return array
|
||||
*/
|
||||
public function index(GetBackupsRequest $request, Server $server)
|
||||
{
|
||||
return $this->fractal->collection($server->backups()->paginate(20))
|
||||
->transformWith($this->getTransformer(BackupTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the backup process for a server.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Backups\StoreBackupRequest $request
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @return array
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function store(StoreBackupRequest $request, Server $server)
|
||||
{
|
||||
$backup = $this->initiateBackupService
|
||||
->setIgnoredFiles($request->input('ignored'))
|
||||
->handle($server, $request->input('name'));
|
||||
|
||||
return $this->fractal->item($backup)
|
||||
->transformWith($this->getTransformer(BackupTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function view()
|
||||
{
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
|
||||
use Lcobucci\JWT\Builder;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Str;
|
||||
use Lcobucci\JWT\Signer\Key;
|
||||
use Pterodactyl\Models\Backup;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Contracts\Routing\ResponseFactory;
|
||||
use Pterodactyl\Repositories\Wings\DaemonBackupRepository;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\DownloadBackupRequest;
|
||||
|
||||
class DownloadBackupController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Wings\DaemonBackupRepository
|
||||
*/
|
||||
private $daemonBackupRepository;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Routing\ResponseFactory
|
||||
*/
|
||||
private $responseFactory;
|
||||
|
||||
/**
|
||||
* DownloadBackupController constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Repositories\Wings\DaemonBackupRepository $daemonBackupRepository
|
||||
* @param \Illuminate\Contracts\Routing\ResponseFactory $responseFactory
|
||||
*/
|
||||
public function __construct(
|
||||
DaemonBackupRepository $daemonBackupRepository,
|
||||
ResponseFactory $responseFactory
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$this->daemonBackupRepository = $daemonBackupRepository;
|
||||
$this->responseFactory = $responseFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the backup for a given server instance. For daemon local files, the file
|
||||
* will be streamed back through the Panel. For AWS S3 files, a signed URL will be generated
|
||||
* which the user is redirected to.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Backups\DownloadBackupRequest $request
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @param \Pterodactyl\Models\Backup $backup
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function __invoke(DownloadBackupRequest $request, Server $server, Backup $backup)
|
||||
{
|
||||
$signer = new Sha256;
|
||||
$now = CarbonImmutable::now();
|
||||
|
||||
$token = (new Builder)->issuedBy(config('app.url'))
|
||||
->permittedFor($server->node->getConnectionAddress())
|
||||
->identifiedBy(hash('sha256', $request->user()->id . $server->uuid), true)
|
||||
->issuedAt($now->getTimestamp())
|
||||
->canOnlyBeUsedAfter($now->subMinutes(5)->getTimestamp())
|
||||
->expiresAt($now->addMinutes(15)->getTimestamp())
|
||||
->withClaim('unique_id', Str::random(16))
|
||||
->withClaim('backup_uuid', $backup->uuid)
|
||||
->withClaim('server_uuid', $server->uuid)
|
||||
->getToken($signer, new Key($server->node->daemonSecret));
|
||||
|
||||
$location = sprintf(
|
||||
'%s/download/backup?token=%s',
|
||||
$server->node->getConnectionAddress(),
|
||||
$token->__toString()
|
||||
);
|
||||
|
||||
return RedirectResponse::create($location);
|
||||
}
|
||||
}
|
|
@ -4,9 +4,12 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
|||
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||
use Pterodactyl\Services\Servers\ReinstallServerService;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Settings\RenameServerRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Settings\ReinstallServerRequest;
|
||||
|
||||
class SettingsController extends ClientApiController
|
||||
{
|
||||
|
@ -15,16 +18,25 @@ class SettingsController extends ClientApiController
|
|||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Servers\ReinstallServerService
|
||||
*/
|
||||
private $reinstallServerService;
|
||||
|
||||
/**
|
||||
* SettingsController constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
|
||||
* @param \Pterodactyl\Services\Servers\ReinstallServerService $reinstallServerService
|
||||
*/
|
||||
public function __construct(ServerRepository $repository)
|
||||
{
|
||||
public function __construct(
|
||||
ServerRepository $repository,
|
||||
ReinstallServerService $reinstallServerService
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$this->repository = $repository;
|
||||
$this->reinstallServerService = $reinstallServerService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -32,7 +44,7 @@ class SettingsController extends ClientApiController
|
|||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Settings\RenameServerRequest $request
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @return \Illuminate\Http\Response
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
|
@ -43,6 +55,22 @@ class SettingsController extends ClientApiController
|
|||
'name' => $request->input('name'),
|
||||
]);
|
||||
|
||||
return Response::create('', Response::HTTP_NO_CONTENT);
|
||||
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinstalls the server on the daemon.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Settings\ReinstallServerRequest $request
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function reinstall(ReinstallServerRequest $request, Server $server)
|
||||
{
|
||||
$this->reinstallServerService->reinstall($server);
|
||||
|
||||
return JsonResponse::create([], Response::HTTP_ACCEPTED);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,11 +2,17 @@
|
|||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Repositories\Eloquent\SubuserRepository;
|
||||
use Pterodactyl\Services\Subusers\SubuserCreationService;
|
||||
use Pterodactyl\Transformers\Api\Client\SubuserTransformer;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\GetSubuserRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\StoreSubuserRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\DeleteSubuserRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\UpdateSubuserRequest;
|
||||
|
||||
class SubuserController extends ClientApiController
|
||||
{
|
||||
|
@ -15,16 +21,25 @@ class SubuserController extends ClientApiController
|
|||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Subusers\SubuserCreationService
|
||||
*/
|
||||
private $creationService;
|
||||
|
||||
/**
|
||||
* SubuserController constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Repositories\Eloquent\SubuserRepository $repository
|
||||
* @param \Pterodactyl\Services\Subusers\SubuserCreationService $creationService
|
||||
*/
|
||||
public function __construct(SubuserRepository $repository)
|
||||
{
|
||||
public function __construct(
|
||||
SubuserRepository $repository,
|
||||
SubuserCreationService $creationService
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$this->repository = $repository;
|
||||
$this->creationService = $creationService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -36,10 +51,78 @@ class SubuserController extends ClientApiController
|
|||
*/
|
||||
public function index(GetSubuserRequest $request, Server $server)
|
||||
{
|
||||
$server->subusers->load('user');
|
||||
|
||||
return $this->fractal->collection($server->subusers)
|
||||
->transformWith($this->getTransformer(SubuserTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new subuser for the given server.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\StoreSubuserRequest $request
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @return array
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException
|
||||
* @throws \Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function store(StoreSubuserRequest $request, Server $server)
|
||||
{
|
||||
$response = $this->creationService->handle(
|
||||
$server, $request->input('email'), $this->getDefaultPermissions($request)
|
||||
);
|
||||
|
||||
return $this->fractal->item($response)
|
||||
->transformWith($this->getTransformer(SubuserTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a given subuser in the system for the server.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\UpdateSubuserRequest $request
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @return array
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function update(UpdateSubuserRequest $request, Server $server): array
|
||||
{
|
||||
$subuser = $request->endpointSubuser();
|
||||
$this->repository->update($subuser->id, [
|
||||
'permissions' => $this->getDefaultPermissions($request),
|
||||
]);
|
||||
|
||||
return $this->fractal->item($subuser->refresh())
|
||||
->transformWith($this->getTransformer(SubuserTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a subusers from a server's assignment.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\DeleteSubuserRequest $request
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function delete(DeleteSubuserRequest $request, Server $server)
|
||||
{
|
||||
$this->repository->delete($request->endpointSubuser()->id);
|
||||
|
||||
return JsonResponse::create([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default permissions for all subusers to ensure none are ever removed wrongly.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
*/
|
||||
protected function getDefaultPermissions(Request $request): array
|
||||
{
|
||||
return array_unique(array_merge($request->input('permissions') ?? [], ['websocket.*']));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Remote\Servers;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Pterodactyl\Repositories\Eloquent\BackupRepository;
|
||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||
use Pterodactyl\Http\Requests\Api\Remote\ReportBackupCompleteRequest;
|
||||
|
||||
class ServerBackupController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\BackupRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
|
||||
*/
|
||||
private $serverRepository;
|
||||
|
||||
/**
|
||||
* ServerBackupController constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Repositories\Eloquent\BackupRepository $repository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $serverRepository
|
||||
*/
|
||||
public function __construct(BackupRepository $repository, ServerRepository $serverRepository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->serverRepository = $serverRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a server backup's state in the database depending on wether or not
|
||||
* it was successful.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Remote\ReportBackupCompleteRequest $request
|
||||
* @param string $uuid
|
||||
* @param string $backup
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function __invoke(ReportBackupCompleteRequest $request, string $uuid, string $backup)
|
||||
{
|
||||
$server = $this->serverRepository->getByUuid($uuid);
|
||||
|
||||
$where = [
|
||||
['uuid', '=', $backup],
|
||||
['server_id', '=', $server->id],
|
||||
];
|
||||
|
||||
if ($request->input('successful')) {
|
||||
$this->repository->updateWhere($where, [
|
||||
'sha256_hash' => $request->input('sha256_hash'),
|
||||
'bytes' => $request->input('file_size'),
|
||||
'completed_at' => Carbon::now(),
|
||||
]);
|
||||
} else {
|
||||
$this->repository->deleteWhere($where);
|
||||
}
|
||||
|
||||
return JsonResponse::create([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Remote\Servers;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Lcobucci\JWT\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Lcobucci\JWT\Signer\Key;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Pterodactyl\Services\Servers\SuspensionService;
|
||||
use Pterodactyl\Repositories\Eloquent\NodeRepository;
|
||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
|
||||
use Pterodactyl\Repositories\Wings\DaemonTransferRepository;
|
||||
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use Pterodactyl\Services\Servers\ServerConfigurationStructureService;
|
||||
|
||||
class ServerTransferController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Database\ConnectionInterface
|
||||
*/
|
||||
private $connection;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface
|
||||
*/
|
||||
private $allocationRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\NodeRepository
|
||||
*/
|
||||
private $nodeRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Wings\DaemonServerRepository
|
||||
*/
|
||||
private $daemonServerRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Wings\DaemonTransferRepository
|
||||
*/
|
||||
private $daemonTransferRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService
|
||||
*/
|
||||
private $configurationStructureService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Servers\SuspensionService
|
||||
*/
|
||||
private $suspensionService;
|
||||
|
||||
/**
|
||||
* @var \Psr\Log\LoggerInterface
|
||||
*/
|
||||
private $writer;
|
||||
|
||||
/**
|
||||
* ServerTransferController constructor.
|
||||
*
|
||||
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
|
||||
* @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\NodeRepository $nodeRepository
|
||||
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
|
||||
* @param \Pterodactyl\Repositories\Wings\DaemonTransferRepository $daemonTransferRepository
|
||||
* @param \Pterodactyl\Services\Servers\ServerConfigurationStructureService $configurationStructureService
|
||||
* @param \Pterodactyl\Services\Servers\SuspensionService $suspensionService
|
||||
* @param \Psr\Log\LoggerInterface $writer
|
||||
*/
|
||||
public function __construct(
|
||||
ConnectionInterface $connection,
|
||||
ServerRepository $repository,
|
||||
AllocationRepositoryInterface $allocationRepository,
|
||||
NodeRepository $nodeRepository,
|
||||
DaemonServerRepository $daemonServerRepository,
|
||||
DaemonTransferRepository $daemonTransferRepository,
|
||||
ServerConfigurationStructureService $configurationStructureService,
|
||||
SuspensionService $suspensionService,
|
||||
LoggerInterface $writer
|
||||
) {
|
||||
$this->connection = $connection;
|
||||
$this->repository = $repository;
|
||||
$this->allocationRepository = $allocationRepository;
|
||||
$this->nodeRepository = $nodeRepository;
|
||||
$this->daemonServerRepository = $daemonServerRepository;
|
||||
$this->daemonTransferRepository = $daemonTransferRepository;
|
||||
$this->configurationStructureService = $configurationStructureService;
|
||||
$this->suspensionService = $suspensionService;
|
||||
$this->writer = $writer;
|
||||
}
|
||||
|
||||
/**
|
||||
* The daemon notifies us about the archive status.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string $uuid
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function archive(Request $request, string $uuid)
|
||||
{
|
||||
$server = $this->repository->getByUuid($uuid);
|
||||
|
||||
// Unsuspend the server and don't continue the transfer.
|
||||
if (! $request->input('successful')) {
|
||||
$this->suspensionService->toggle($server, 'unsuspend');
|
||||
|
||||
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
$server->node_id = $server->transfer->new_node;
|
||||
|
||||
$data = $this->configurationStructureService->handle($server);
|
||||
$data['suspended'] = false;
|
||||
$data['service']['skip_scripts'] = true;
|
||||
|
||||
$allocations = $server->getAllocationMappings();
|
||||
$data['allocations']['default']['ip'] = array_key_first($allocations);
|
||||
$data['allocations']['default']['port'] = $allocations[$data['allocations']['default']['ip']][0];
|
||||
|
||||
$now = Chronos::now();
|
||||
$signer = new Sha256;
|
||||
|
||||
$token = (new Builder)->issuedBy(config('app.url'))
|
||||
->permittedFor($server->node->getConnectionAddress())
|
||||
->identifiedBy(hash('sha256', $server->uuid), true)
|
||||
->issuedAt($now->getTimestamp())
|
||||
->canOnlyBeUsedAfter($now->getTimestamp())
|
||||
->expiresAt($now->addMinutes(15)->getTimestamp())
|
||||
->relatedTo($server->uuid, true)
|
||||
->getToken($signer, new Key($server->node->daemonSecret));
|
||||
|
||||
// On the daemon transfer repository, make sure to set the node after the server
|
||||
// because setServer() tells the repository to use the server's node and not the one
|
||||
// we want to specify.
|
||||
try {
|
||||
$this->daemonTransferRepository
|
||||
->setServer($server)
|
||||
->setNode($this->nodeRepository->find($server->transfer->new_node))
|
||||
->notify($server, $data, $server->node, $token->__toString());
|
||||
} catch (DaemonConnectionException $exception) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* The daemon notifies us about a transfer failure.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string $uuid
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function failure(string $uuid)
|
||||
{
|
||||
$server = $this->repository->getByUuid($uuid);
|
||||
$transfer = $server->transfer;
|
||||
|
||||
$allocationIds = json_decode($transfer->new_additional_allocations);
|
||||
array_push($allocationIds, $transfer->new_allocation);
|
||||
|
||||
// Remove the new allocations.
|
||||
$this->allocationRepository->updateWhereIn('id', $allocationIds, ['server_id' => null]);
|
||||
|
||||
// Unsuspend the server.
|
||||
$this->suspensionService->toggle($server, 'unsuspend');
|
||||
|
||||
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* The daemon notifies us about a transfer success.
|
||||
*
|
||||
* @param string $uuid
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function success(string $uuid)
|
||||
{
|
||||
$server = $this->repository->getByUuid($uuid);
|
||||
$transfer = $server->transfer;
|
||||
|
||||
$allocationIds = json_decode($transfer->old_additional_allocations);
|
||||
array_push($allocationIds, $transfer->old_allocation);
|
||||
|
||||
// Begin a transaction.
|
||||
$this->connection->beginTransaction();
|
||||
|
||||
// Remove the old allocations.
|
||||
$this->allocationRepository->updateWhereIn('id', $allocationIds, ['server_id' => null]);
|
||||
|
||||
// Update the server's allocation_id and node_id.
|
||||
$server->allocation_id = $transfer->new_allocation;
|
||||
$server->node_id = $transfer->new_node;
|
||||
$server->save();
|
||||
|
||||
// Mark the transfer as successful.
|
||||
$transfer->successful = true;
|
||||
$transfer->save();
|
||||
|
||||
// Commit the transaction.
|
||||
$this->connection->commit();
|
||||
|
||||
// Delete the server from the old node
|
||||
try {
|
||||
$this->daemonServerRepository->setServer($server)->delete();
|
||||
} catch (DaemonConnectionException $exception) {
|
||||
$this->writer->warning($exception);
|
||||
}
|
||||
|
||||
// Unsuspend the server
|
||||
$server->load('node');
|
||||
$this->suspensionService->toggle($server, $this->suspensionService::ACTION_UNSUSPEND);
|
||||
|
||||
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
|
@ -42,6 +42,16 @@ class AuthenticateServerAccess
|
|||
throw new NotFoundHttpException(trans('exceptions.api.resource_not_found'));
|
||||
}
|
||||
|
||||
// At the very least, ensure that the user trying to make this request is the
|
||||
// server owner, a subuser, or a root admin. We'll leave it up to the controllers
|
||||
// to authenticate more detailed permissions if needed.
|
||||
if ($request->user()->id !== $server->owner_id && ! $request->user()->root_admin) {
|
||||
// Check for subuser status.
|
||||
if (! $server->subusers->contains('user_id', $request->user()->id)) {
|
||||
throw new NotFoundHttpException(trans('exceptions.api.resource_not_found'));
|
||||
}
|
||||
}
|
||||
|
||||
if ($server->suspended) {
|
||||
throw new AccessDeniedHttpException('Cannot access a server that is marked as being suspended.');
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace Pterodactyl\Http\Middleware\Api\Client;
|
||||
|
||||
use Closure;
|
||||
use Pterodactyl\Models\Backup;
|
||||
use Illuminate\Container\Container;
|
||||
use Pterodactyl\Contracts\Extensions\HashidsInterface;
|
||||
use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings;
|
||||
|
@ -55,6 +56,10 @@ class SubstituteClientApiBindings extends ApiSubstituteBindings
|
|||
}
|
||||
});
|
||||
|
||||
$this->router->model('backup', Backup::class, function ($value) {
|
||||
return Backup::query()->where('uuid', $value)->firstOrFail();
|
||||
});
|
||||
|
||||
return parent::handle($request, $next);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ class StoreServerRequest extends ApplicationApiRequest
|
|||
'limits.swap' => $rules['swap'],
|
||||
'limits.disk' => $rules['disk'],
|
||||
'limits.io' => $rules['io'],
|
||||
'limits.threads' => $rules['threads'],
|
||||
'limits.cpu' => $rules['cpu'],
|
||||
|
||||
// Application Resource Limits
|
||||
|
@ -96,6 +97,7 @@ class StoreServerRequest extends ApplicationApiRequest
|
|||
'disk' => array_get($data, 'limits.disk'),
|
||||
'io' => array_get($data, 'limits.io'),
|
||||
'cpu' => array_get($data, 'limits.cpu'),
|
||||
'threads' => array_get($data, 'limits.threads'),
|
||||
'skip_scripts' => array_get($data, 'skip_scripts', false),
|
||||
'allocation_id' => array_get($data, 'allocation.default'),
|
||||
'allocation_additional' => array_get($data, 'allocation.additional'),
|
||||
|
|
|
@ -25,6 +25,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
|
|||
'limits.swap' => $this->requiredToOptional('swap', $rules['swap'], true),
|
||||
'limits.io' => $this->requiredToOptional('io', $rules['io'], true),
|
||||
'limits.cpu' => $this->requiredToOptional('cpu', $rules['cpu'], true),
|
||||
'limits.threads' => $this->requiredToOptional('threads', $rules['threads'], true),
|
||||
'limits.disk' => $this->requiredToOptional('disk', $rules['disk'], true),
|
||||
|
||||
// Legacy rules to maintain backwards compatable API support without requiring
|
||||
|
@ -35,6 +36,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
|
|||
'swap' => $this->requiredToOptional('swap', $rules['swap']),
|
||||
'io' => $this->requiredToOptional('io', $rules['io']),
|
||||
'cpu' => $this->requiredToOptional('cpu', $rules['cpu']),
|
||||
'threads' => $this->requiredToOptional('threads', $rules['threads']),
|
||||
'disk' => $this->requiredToOptional('disk', $rules['disk']),
|
||||
|
||||
'add_allocations' => 'bail|array',
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Backups;
|
||||
|
||||
use Pterodactyl\Models\Backup;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||
|
||||
class DownloadBackupRequest extends ClientApiRequest
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function permission()
|
||||
{
|
||||
return Permission::ACTION_BACKUP_DOWNLOAD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that this backup belongs to the server that is also present in the
|
||||
* request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function resourceExists(): bool
|
||||
{
|
||||
/** @var \Pterodactyl\Models\Server|mixed $server */
|
||||
$server = $this->route()->parameter('server');
|
||||
/** @var \Pterodactyl\Models\Backup|mixed $backup */
|
||||
$backup = $this->route()->parameter('backup');
|
||||
|
||||
if ($server instanceof Server && $backup instanceof Backup) {
|
||||
if ($server->exists && $backup->exists && $server->id === $backup->server_id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Backups;
|
||||
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||
|
||||
class GetBackupsRequest extends ClientApiRequest
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function permission()
|
||||
{
|
||||
return Permission::ACTION_BACKUP_READ;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Backups;
|
||||
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||
|
||||
class StoreBackupRequest extends ClientApiRequest
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function permission()
|
||||
{
|
||||
return Permission::ACTION_BACKUP_CREATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'nullable|string|max:255',
|
||||
'ignore' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -18,11 +18,10 @@ class SendPowerRequest extends ClientApiRequest
|
|||
case 'start':
|
||||
return Permission::ACTION_CONTROL_START;
|
||||
case 'stop':
|
||||
case 'kill':
|
||||
return Permission::ACTION_CONTROL_STOP;
|
||||
case 'restart':
|
||||
return Permission::ACTION_CONTROL_RESTART;
|
||||
case 'kill':
|
||||
return Permission::ACTION_CONTROL_KILL;
|
||||
}
|
||||
|
||||
return '__invalid';
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Settings;
|
||||
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||
|
||||
class ReinstallServerRequest extends ClientApiRequest
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function permission()
|
||||
{
|
||||
return Permission::ACTION_SETTINGS_REINSTALL;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Subusers;
|
||||
|
||||
use Pterodactyl\Models\Permission;
|
||||
|
||||
class DeleteSubuserRequest extends SubuserRequest
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function permission()
|
||||
{
|
||||
return Permission::ACTION_USER_DELETE;
|
||||
}
|
||||
}
|
|
@ -3,9 +3,8 @@
|
|||
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Subusers;
|
||||
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||
|
||||
class GetSubuserRequest extends ClientApiRequest
|
||||
class GetSubuserRequest extends SubuserRequest
|
||||
{
|
||||
/**
|
||||
* Confirm that a user is able to view subusers for the specified server.
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Subusers;
|
||||
|
||||
use Pterodactyl\Models\Permission;
|
||||
|
||||
class StoreSubuserRequest extends SubuserRequest
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function permission()
|
||||
{
|
||||
return Permission::ACTION_USER_CREATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => 'required|email',
|
||||
'permissions' => 'required|array',
|
||||
'permissions.*' => 'string',
|
||||
];
|
||||
}
|
||||
}
|
131
app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php
Normal file
131
app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php
Normal file
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Subusers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Exceptions\Http\HttpForbiddenException;
|
||||
use Pterodactyl\Repositories\Eloquent\SubuserRepository;
|
||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
abstract class SubuserRequest extends ClientApiRequest
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Models\Subuser|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* Authorize the request and ensure that a user is not trying to modify themselves.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
if (! parent::authorize()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there is a subuser present in the URL, validate that it is not the same as the
|
||||
// current request user. You're not allowed to modify yourself.
|
||||
if ($this->route()->hasParameter('subuser')) {
|
||||
if ($this->endpointSubuser()->user_id === $this->user()->id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a POST request, validate that the user can even assign the permissions they
|
||||
// have selected to assign.
|
||||
if ($this->method() === Request::METHOD_POST && $this->has('permissions')) {
|
||||
$this->validatePermissionsCanBeAssigned(
|
||||
$this->input('permissions') ?? []
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the permissions we are trying to assign can actually be assigned
|
||||
* by the user making the request.
|
||||
*
|
||||
* @param array $permissions
|
||||
*/
|
||||
protected function validatePermissionsCanBeAssigned(array $permissions)
|
||||
{
|
||||
$user = $this->user();
|
||||
/** @var \Pterodactyl\Models\Server $server */
|
||||
$server = $this->route()->parameter('server');
|
||||
|
||||
// If we are a root admin or the server owner, no need to perform these checks.
|
||||
if ($user->root_admin || $user->id === $server->owner_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, get the current subuser's permission set, and ensure that the
|
||||
// permissions they are trying to assign are not _more_ than the ones they
|
||||
// already have.
|
||||
if (count(array_diff($permissions, $this->currentUserPermissions())) > 0) {
|
||||
throw new HttpForbiddenException(
|
||||
'Cannot assign permissions to a subuser that your account does not actively possess.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently authenticated user's permissions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function currentUserPermissions(): array
|
||||
{
|
||||
/** @var \Pterodactyl\Repositories\Eloquent\SubuserRepository $repository */
|
||||
$repository = $this->container->make(SubuserRepository::class);
|
||||
|
||||
/* @var \Pterodactyl\Models\Subuser $model */
|
||||
try {
|
||||
$model = $repository->findFirstWhere([
|
||||
['server_id', $this->route()->parameter('server')->id],
|
||||
['user_id' => $this->user()->id],
|
||||
]);
|
||||
} catch (RecordNotFoundException $exception) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $model->permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the subuser model for the given request which can then be validated. If
|
||||
* required request parameters are missing a 404 error will be returned, otherwise
|
||||
* a model exception will be returned if the model is not found.
|
||||
*
|
||||
* This returns the subuser based on the endpoint being hit, not the actual subuser
|
||||
* for the account making the request.
|
||||
*
|
||||
* @return \Pterodactyl\Models\Subuser
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
|
||||
*/
|
||||
public function endpointSubuser()
|
||||
{
|
||||
/** @var \Pterodactyl\Repositories\Eloquent\SubuserRepository $repository */
|
||||
$repository = $this->container->make(SubuserRepository::class);
|
||||
|
||||
$parameters = $this->route()->parameters();
|
||||
if (
|
||||
! isset($parameters['server'], $parameters['server'])
|
||||
|| ! is_string($parameters['subuser'])
|
||||
|| ! $parameters['server'] instanceof Server
|
||||
) {
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
return $this->model ?: $this->model = $repository->getUserForServer(
|
||||
$parameters['server']->id, $parameters['subuser']
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Subusers;
|
||||
|
||||
use Pterodactyl\Models\Permission;
|
||||
|
||||
class UpdateSubuserRequest extends SubuserRequest
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function permission()
|
||||
{
|
||||
return Permission::ACTION_USER_UPDATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'permissions' => 'required|array',
|
||||
'permissions.*' => 'string',
|
||||
];
|
||||
}
|
||||
}
|
20
app/Http/Requests/Api/Remote/ReportBackupCompleteRequest.php
Normal file
20
app/Http/Requests/Api/Remote/ReportBackupCompleteRequest.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Api\Remote;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ReportBackupCompleteRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'successful' => 'boolean',
|
||||
'sha256_hash' => 'string|required_if:successful,true',
|
||||
'file_size' => 'numeric|required_if:successful,true',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@ namespace Pterodactyl\Models;
|
|||
* @property \Pterodactyl\Models\Server|null $server
|
||||
* @property \Pterodactyl\Models\Node $node
|
||||
*/
|
||||
class Allocation extends Validable
|
||||
class Allocation extends Model
|
||||
{
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
|
@ -75,7 +75,7 @@ class Allocation extends Validable
|
|||
/**
|
||||
* Accessor to automatically provide the IP alias if defined.
|
||||
*
|
||||
* @param null|string $value
|
||||
* @param string|null $value
|
||||
* @return string
|
||||
*/
|
||||
public function getAliasAttribute($value)
|
||||
|
@ -86,7 +86,7 @@ class Allocation extends Validable
|
|||
/**
|
||||
* Accessor to quickly determine if this allocation has an alias.
|
||||
*
|
||||
* @param null|string $value
|
||||
* @param string|null $value
|
||||
* @return bool
|
||||
*/
|
||||
public function getHasAliasAttribute($value)
|
||||
|
|
|
@ -16,7 +16,7 @@ use Pterodactyl\Services\Acl\Api\AdminAcl;
|
|||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
*/
|
||||
class ApiKey extends Validable
|
||||
class ApiKey extends Model
|
||||
{
|
||||
const RESOURCE_NAME = 'api_key';
|
||||
|
||||
|
@ -53,7 +53,7 @@ class ApiKey extends Validable
|
|||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'allowed_ips' => 'json',
|
||||
'allowed_ips' => 'array',
|
||||
'user_id' => 'int',
|
||||
'r_' . AdminAcl::RESOURCE_USERS => 'int',
|
||||
'r_' . AdminAcl::RESOURCE_ALLOCATIONS => 'int',
|
||||
|
@ -99,7 +99,8 @@ class ApiKey extends Validable
|
|||
'identifier' => 'required|string|size:16|unique:api_keys,identifier',
|
||||
'token' => 'required|string',
|
||||
'memo' => 'required|nullable|string|max:500',
|
||||
'allowed_ips' => 'nullable|json',
|
||||
'allowed_ips' => 'nullable|array',
|
||||
'allowed_ips.*' => 'string',
|
||||
'last_used_at' => 'nullable|date',
|
||||
'r_' . AdminAcl::RESOURCE_USERS => 'integer|min:0|max:3',
|
||||
'r_' . AdminAcl::RESOURCE_ALLOCATIONS => 'integer|min:0|max:3',
|
||||
|
|
85
app/Models/Backup.php
Normal file
85
app/Models/Backup.php
Normal file
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $server_id
|
||||
* @property int $uuid
|
||||
* @property string $name
|
||||
* @property string $ignored_files
|
||||
* @property string $disk
|
||||
* @property string|null $sha256_hash
|
||||
* @property int $bytes
|
||||
* @property \Carbon\CarbonImmutable|null $completed_at
|
||||
* @property \Carbon\CarbonImmutable $created_at
|
||||
* @property \Carbon\CarbonImmutable $updated_at
|
||||
* @property \Carbon\CarbonImmutable|null $deleted_at
|
||||
*
|
||||
* @property \Pterodactyl\Models\Server $server
|
||||
*/
|
||||
class Backup extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
const RESOURCE_NAME = 'backup';
|
||||
|
||||
const DISK_LOCAL = 'local';
|
||||
const DISK_AWS_S3 = 's3';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'backups';
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $immutableDates = true;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'id' => 'int',
|
||||
'bytes' => 'int',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = [
|
||||
'completed_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $attributes = [
|
||||
'sha256_hash' => null,
|
||||
'bytes' => 0,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public static $validationRules = [
|
||||
'server_id' => 'bail|required|numeric|exists:servers,id',
|
||||
'uuid' => 'required|uuid',
|
||||
'name' => 'required|string',
|
||||
'ignored_files' => 'string',
|
||||
'disk' => 'required|string',
|
||||
'sha256_hash' => 'nullable|string',
|
||||
'bytes' => 'numeric',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function server()
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ namespace Pterodactyl\Models;
|
|||
|
||||
use Znck\Eloquent\Traits\BelongsToThrough;
|
||||
|
||||
class DaemonKey extends Validable
|
||||
class DaemonKey extends Model
|
||||
{
|
||||
use BelongsToThrough;
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
class Database extends Validable
|
||||
class Database extends Model
|
||||
{
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
class DatabaseHost extends Validable
|
||||
class DatabaseHost extends Model
|
||||
{
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
|
|
|
@ -39,7 +39,7 @@ namespace Pterodactyl\Models;
|
|||
* @property \Pterodactyl\Models\Egg|null $scriptFrom
|
||||
* @property \Pterodactyl\Models\Egg|null $configFrom
|
||||
*/
|
||||
class Egg extends Validable
|
||||
class Egg extends Model
|
||||
{
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
class EggVariable extends Validable
|
||||
class EggVariable extends Model
|
||||
{
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
class Location extends Validable
|
||||
class Location extends Model
|
||||
{
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
|
|
|
@ -5,11 +5,18 @@ namespace Pterodactyl\Models;
|
|||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Contracts\Validation\Factory;
|
||||
use Illuminate\Database\Eloquent\Model as IlluminateModel;
|
||||
|
||||
abstract class Validable extends Model
|
||||
abstract class Model extends IlluminateModel
|
||||
{
|
||||
/**
|
||||
* Set to true to return immutable Carbon date instances from the model.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $immutableDates = false;
|
||||
|
||||
/**
|
||||
* Determines if the model should undergo data validation before it is saved
|
||||
* to the database.
|
||||
|
@ -47,7 +54,7 @@ abstract class Validable extends Model
|
|||
|
||||
static::$validatorFactory = Container::getInstance()->make(Factory::class);
|
||||
|
||||
static::saving(function (Validable $model) {
|
||||
static::saving(function (Model $model) {
|
||||
return $model->validate();
|
||||
});
|
||||
}
|
||||
|
@ -140,7 +147,27 @@ abstract class Validable extends Model
|
|||
}
|
||||
|
||||
return $this->getValidator()->setData(
|
||||
$this->getAttributes()
|
||||
// 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()
|
||||
)
|
||||
)->passes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a timestamp as DateTime object.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return \Illuminate\Support\Carbon|\Carbon\CarbonImmutable
|
||||
*/
|
||||
protected function asDateTime($value)
|
||||
{
|
||||
if (! $this->immutableDates) {
|
||||
return parent::asDateTime($value);
|
||||
}
|
||||
|
||||
return parent::asDateTime($value)->toImmutable();
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ namespace Pterodactyl\Models;
|
|||
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Egg[] $eggs
|
||||
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Pack[] $packs
|
||||
*/
|
||||
class Nest extends Validable
|
||||
class Nest extends Model
|
||||
{
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
|
|
|
@ -32,9 +32,10 @@ use Pterodactyl\Models\Traits\Searchable;
|
|||
* @property \Pterodactyl\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers
|
||||
* @property \Pterodactyl\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations
|
||||
*/
|
||||
class Node extends Validable
|
||||
class Node extends Model
|
||||
{
|
||||
use Notifiable, Searchable;
|
||||
use Notifiable;
|
||||
use Searchable;
|
||||
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
|
@ -170,6 +171,7 @@ class Node extends Validable
|
|||
],
|
||||
'system' => [
|
||||
'data' => $this->daemonBase,
|
||||
'archive_directory' => $this->daemonBase . '/.archives',
|
||||
'username' => 'pterodactyl',
|
||||
'timezone_path' => '/etc/timezone',
|
||||
'set_permissions_on_boot' => true,
|
||||
|
@ -235,4 +237,19 @@ class Node extends Validable
|
|||
{
|
||||
return $this->hasMany(Allocation::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean if the node is viable for an additional server to be placed on it.
|
||||
*
|
||||
* @param int $memory
|
||||
* @param int $disk
|
||||
* @return bool
|
||||
*/
|
||||
public function isViable(int $memory, int $disk): bool
|
||||
{
|
||||
$memoryLimit = $this->memory * (1 + ($this->memory_overallocate / 100));
|
||||
$diskLimit = $this->disk * (1 + ($this->disk_overallocate / 100));
|
||||
|
||||
return ($this->sum_memory + $memory) <= $memoryLimit && ($this->sum_disk + $disk) <= $diskLimit;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ use Pterodactyl\Models\Traits\Searchable;
|
|||
* @property \Pterodactyl\Models\Egg|null $egg
|
||||
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Server[] $servers
|
||||
*/
|
||||
class Pack extends Validable
|
||||
class Pack extends Model
|
||||
{
|
||||
use Searchable;
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace Pterodactyl\Models;
|
|||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class Permission extends Validable
|
||||
class Permission extends Model
|
||||
{
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
|
@ -20,7 +20,6 @@ class Permission extends Validable
|
|||
const ACTION_CONTROL_START = 'control.start';
|
||||
const ACTION_CONTROL_STOP = 'control.stop';
|
||||
const ACTION_CONTROL_RESTART = 'control.restart';
|
||||
const ACTION_CONTROL_KILL = 'control.kill';
|
||||
|
||||
const ACTION_DATABASE_READ = 'database.read';
|
||||
const ACTION_DATABASE_CREATE = 'database.create';
|
||||
|
@ -38,6 +37,12 @@ class Permission extends Validable
|
|||
const ACTION_USER_UPDATE = 'user.update';
|
||||
const ACTION_USER_DELETE = 'user.delete';
|
||||
|
||||
const ACTION_BACKUP_READ = 'backup.read';
|
||||
const ACTION_BACKUP_CREATE = 'backup.create';
|
||||
const ACTION_BACKUP_UPDATE = 'backup.update';
|
||||
const ACTION_BACKUP_DELETE = 'backup.delete';
|
||||
const ACTION_BACKUP_DOWNLOAD = 'backup.download';
|
||||
|
||||
const ACTION_ALLOCATION_READ = 'allocation.read';
|
||||
const ACTION_ALLOCIATION_UPDATE = 'allocation.update';
|
||||
|
||||
|
@ -98,105 +103,100 @@ class Permission extends Validable
|
|||
*/
|
||||
protected static $permissions = [
|
||||
'websocket' => [
|
||||
// Allows the user to connect to the server websocket, this will give them
|
||||
// access to view the console output as well as realtime server stats (CPU
|
||||
// and Memory usage).
|
||||
'*',
|
||||
'description' => 'Allows the user to connect to the server websocket, giving them access to view console output and realtime server stats.',
|
||||
'keys' => [
|
||||
'*' => 'Gives user full read access to the websocket.',
|
||||
],
|
||||
],
|
||||
|
||||
'control' => [
|
||||
// Allows the user to send data to the server console process. A user with this
|
||||
// permission will not be able to stop the server directly by issuing the specified
|
||||
// stop command for the Egg, however depending on plugins and server configuration
|
||||
// they may still be able to control the server power state.
|
||||
'console', // power.send-command
|
||||
|
||||
// Allows the user to start/stop/restart/kill the server process.
|
||||
'start', // power.power-start
|
||||
'stop', // power.power-stop
|
||||
'restart', // power.power-restart
|
||||
'kill', // power.power-kill
|
||||
'description' => 'Permissions that control a user\'s ability to control the power state of a server, or send commands.',
|
||||
'keys' => [
|
||||
'console' => 'Allows a user to send commands to the server instance via the console.',
|
||||
'start' => 'Allows a user to start the server if it is stopped.',
|
||||
'stop' => 'Allows a user to stop a server if it is running.',
|
||||
'restart' => 'Allows a user to perform a server restart. This allows them to start the server if it is offline, but not put the server in a completely stopped state.',
|
||||
],
|
||||
],
|
||||
|
||||
'user' => [
|
||||
// Allows a user to create a new user assigned to the server. They will not be able
|
||||
// to assign any permissions they do not already have on their account as well.
|
||||
'create', // subuser.create-subuser
|
||||
'read', // subuser.list-subusers, subuser.view-subuser
|
||||
'update', // subuser.edit-subuser
|
||||
'delete', // subuser.delete-subuser
|
||||
'description' => 'Permissions that allow a user to manage other subusers on a server. They will never be able to edit their own account, or assign permissions they do not have themselves.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create new subusers for the server.',
|
||||
'read' => 'Allows the user to view subusers and their permissions for the server.',
|
||||
'update' => 'Allows a user to modify other subusers.',
|
||||
'delete' => 'Allows a user to delete a subuser from the server.',
|
||||
],
|
||||
],
|
||||
|
||||
'file' => [
|
||||
// Allows a user to create additional files and folders either via the Panel,
|
||||
// or via a direct upload.
|
||||
'create', // files.create-files, files.upload-files, files.copy-files, files.move-files
|
||||
'description' => 'Permissions that control a user\'s ability to modify the filesystem for this server.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create additional files and folders via the Panel or direct upload.',
|
||||
'read' => 'Allows a user to view the contents of a directory and read the contents of a file. Users with this permission can also download files.',
|
||||
'update' => 'Allows a user to update the contents of an existing file or directory.',
|
||||
'delete' => 'Allows a user to delete files or directories.',
|
||||
'archive' => 'Allows a user to archive the contents of a directory as well as decompress existing archives on the system.',
|
||||
'sftp' => 'Allows a user to connect to SFTP and manage server files using the other assigned file permissions.',
|
||||
],
|
||||
],
|
||||
|
||||
// Allows a user to view the contents of a directory as well as read the contents
|
||||
// of a given file. A user with this permission will be able to download files
|
||||
// as well.
|
||||
'read', // files.list-files, files.download-files
|
||||
|
||||
// Allows a user to update the contents of an existing file or directory.
|
||||
'update', // files.edit-files, files.save-files
|
||||
|
||||
// Allows a user to delete a file or directory.
|
||||
'delete', // files.delete-files
|
||||
|
||||
// Allows a user to archive the contents of a directory as well as decompress existing
|
||||
// archives on the system.
|
||||
'archive', // files.compress-files, files.decompress-files
|
||||
|
||||
// Allows the user to connect and manage server files using their account
|
||||
// credentials and a SFTP client.
|
||||
'sftp', // files.access-sftp
|
||||
'backup' => [
|
||||
'description' => 'Permissions that control a user\'s ability to generate and manage server backups.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create new backups for this server.',
|
||||
'read' => 'Allows a user to view all backups that exist for this server.',
|
||||
'update' => '',
|
||||
'delete' => 'Allows a user to remove backups from the system.',
|
||||
'download' => 'Allows a user to download backups.',
|
||||
],
|
||||
],
|
||||
|
||||
// Controls permissions for editing or viewing a server's allocations.
|
||||
'allocation' => [
|
||||
'read', // server.view-allocations
|
||||
'update', // server.edit-allocation
|
||||
'description' => 'Permissions that control a user\'s ability to modify the port allocations for this server.',
|
||||
'keys' => [
|
||||
'read' => 'Allows a user to view the allocations assigned to this server.',
|
||||
'update' => 'Allows a user to modify the allocations assigned to this server.',
|
||||
],
|
||||
],
|
||||
|
||||
// Controls permissions for editing or viewing a server's startup parameters.
|
||||
'startup' => [
|
||||
'read', // server.view-startup
|
||||
'update', // server.edit-startup
|
||||
'description' => 'Permissions that control a user\'s ability to view this server\'s startup parameters.',
|
||||
'keys' => [
|
||||
'read' => '',
|
||||
'update' => '',
|
||||
],
|
||||
],
|
||||
|
||||
'database' => [
|
||||
// Allows a user to create a new database for a server.
|
||||
'create', // database.create-database
|
||||
|
||||
// Allows a user to view the databases associated with the server. If they do not also
|
||||
// have the view_password permission they will only be able to see the connection address
|
||||
// and the name of the user.
|
||||
'read', // database.view-databases
|
||||
|
||||
// Allows a user to rotate the password on a database instance. If the user does not
|
||||
// alow have the view_password permission they will not be able to see the updated password
|
||||
// anywhere, but it will still be rotated.
|
||||
'update', // database.reset-db-password
|
||||
|
||||
// Allows a user to delete a database instance.
|
||||
'delete', // database.delete-database
|
||||
|
||||
// Allows a user to view the password associated with a database instance for the
|
||||
// server. Note that a user without this permission may still be able to access these
|
||||
// credentials by viewing files or the console.
|
||||
'view_password', // database.reset-db-password
|
||||
'description' => 'Permissions that control a user\'s access to the database management for this server.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create a new database for this server.',
|
||||
'read' => 'Allows a user to view the database associated with this server.',
|
||||
'update' => 'Allows a user to rotate the password on a database instance. If the user does not have the view_password permission they will not see the updated password.',
|
||||
'delete' => 'Allows a user to remove a database instance from this server.',
|
||||
'view_password' => 'Allows a user to view the password associated with a database instance for this server.',
|
||||
],
|
||||
],
|
||||
|
||||
'schedule' => [
|
||||
'create', // task.create-schedule
|
||||
'read', // task.view-schedule, task.list-schedules
|
||||
'update', // task.edit-schedule, task.queue-schedule, task.toggle-schedule
|
||||
'delete', // task.delete-schedule
|
||||
'description' => 'Permissions that control a user\'s access to the schedule management for this server.',
|
||||
'keys' => [
|
||||
'create' => '', // task.create-schedule
|
||||
'read' => '', // task.view-schedule, task.list-schedules
|
||||
'update' => '', // task.edit-schedule, task.queue-schedule, task.toggle-schedule
|
||||
'delete' => '', // task.delete-schedule
|
||||
],
|
||||
],
|
||||
|
||||
'settings' => [
|
||||
'rename',
|
||||
'reinstall',
|
||||
'description' => 'Permissions that control a user\'s access to the settings for this server.',
|
||||
'keys' => [
|
||||
'rename' => '',
|
||||
'reinstall' => '',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ use Pterodactyl\Contracts\Extensions\HashidsInterface;
|
|||
* @property \Pterodactyl\Models\Server $server
|
||||
* @property \Pterodactyl\Models\Task[]|\Illuminate\Support\Collection $tasks
|
||||
*/
|
||||
class Schedule extends Validable
|
||||
class Schedule extends Model
|
||||
{
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
|
|
|
@ -23,6 +23,7 @@ use Znck\Eloquent\Traits\BelongsToThrough;
|
|||
* @property int $disk
|
||||
* @property int $io
|
||||
* @property int $cpu
|
||||
* @property string $threads
|
||||
* @property bool $oom_disabled
|
||||
* @property int $allocation_id
|
||||
* @property int $nest_id
|
||||
|
@ -50,10 +51,14 @@ use Znck\Eloquent\Traits\BelongsToThrough;
|
|||
* @property \Pterodactyl\Models\Location $location
|
||||
* @property \Pterodactyl\Models\DaemonKey $key
|
||||
* @property \Pterodactyl\Models\DaemonKey[]|\Illuminate\Database\Eloquent\Collection $keys
|
||||
* @property \Pterodactyl\Models\ServerTransfer $transfer
|
||||
* @property \Pterodactyl\Models\Backup[]|\Illuminate\Database\Eloquent\Collection $backups
|
||||
*/
|
||||
class Server extends Validable
|
||||
class Server extends Model
|
||||
{
|
||||
use BelongsToThrough, Notifiable, Searchable;
|
||||
use BelongsToThrough;
|
||||
use Notifiable;
|
||||
use Searchable;
|
||||
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
|
@ -61,6 +66,10 @@ class Server extends Validable
|
|||
*/
|
||||
const RESOURCE_NAME = 'server';
|
||||
|
||||
const STATUS_INSTALLING = 0;
|
||||
const STATUS_INSTALLED = 1;
|
||||
const STATUS_INSTALL_FAILED = 2;
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
|
@ -105,6 +114,7 @@ class Server extends Validable
|
|||
'swap' => 'required|numeric|min:-1',
|
||||
'io' => 'required|numeric|between:10,1000',
|
||||
'cpu' => 'required|numeric|min:0',
|
||||
'threads' => 'nullable|regex:/^[0-9-,]+$/',
|
||||
'oom_disabled' => 'sometimes|boolean',
|
||||
'disk' => 'required|numeric|min:0',
|
||||
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
|
||||
|
@ -177,7 +187,7 @@ class Server extends Validable
|
|||
*/
|
||||
public function getAllocationMappings(): array
|
||||
{
|
||||
return $this->allocations->groupBy('ip')->map(function ($item) {
|
||||
return $this->allocations->where('node_id', $this->node_id)->groupBy('ip')->map(function ($item) {
|
||||
return $item->pluck('port');
|
||||
})->toArray();
|
||||
}
|
||||
|
@ -331,4 +341,22 @@ class Server extends Validable
|
|||
{
|
||||
return $this->hasMany(DaemonKey::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the associated server transfer.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function transfer()
|
||||
{
|
||||
return $this->hasOne(ServerTransfer::class)->orderByDesc('id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function backups()
|
||||
{
|
||||
return $this->hasMany(Backup::class);
|
||||
}
|
||||
}
|
||||
|
|
81
app/Models/ServerTransfer.php
Normal file
81
app/Models/ServerTransfer.php
Normal file
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $server_id
|
||||
* @property int $old_node
|
||||
* @property int $new_node
|
||||
* @property int $old_allocation
|
||||
* @property int $new_allocation
|
||||
* @property string $old_additional_allocations
|
||||
* @property string $new_additional_allocations
|
||||
* @property bool $successful
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
*
|
||||
* @property \Pterodactyl\Models\Server $server
|
||||
*/
|
||||
class ServerTransfer extends Validable
|
||||
{
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
* API representation using fractal.
|
||||
*/
|
||||
const RESOURCE_NAME = 'server_transfer';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'server_transfers';
|
||||
|
||||
/**
|
||||
* Fields that are not mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guarded = ['id', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* Cast values to correct type.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'server_id' => 'int',
|
||||
'old_node' => 'int',
|
||||
'new_node' => 'int',
|
||||
'old_allocation' => 'int',
|
||||
'new_allocation' => 'int',
|
||||
'old_additional_allocations' => 'string',
|
||||
'new_additional_allocations' => 'string',
|
||||
'successful' => 'bool',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public static $validationRules = [
|
||||
'server_id' => 'required|numeric|exists:servers,id',
|
||||
'old_node' => 'required|numeric',
|
||||
'new_node' => 'required|numeric',
|
||||
'old_allocation' => 'required|numeric',
|
||||
'new_allocation' => 'required|numeric',
|
||||
'old_additional_allocations' => 'nullable',
|
||||
'new_additional_allocations' => 'nullable',
|
||||
'successful' => 'sometimes|boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Gets the server associated with a server transfer.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function server()
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
class Setting extends Validable
|
||||
class Setting extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
|
|
|
@ -15,7 +15,7 @@ use Illuminate\Notifications\Notifiable;
|
|||
* @property \Pterodactyl\Models\User $user
|
||||
* @property \Pterodactyl\Models\Server $server
|
||||
*/
|
||||
class Subuser extends Validable
|
||||
class Subuser extends Model
|
||||
{
|
||||
use Notifiable;
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ use Pterodactyl\Contracts\Extensions\HashidsInterface;
|
|||
* @property \Pterodactyl\Models\Schedule $schedule
|
||||
* @property \Pterodactyl\Models\Server $server
|
||||
*/
|
||||
class Task extends Validable
|
||||
class Task extends Model
|
||||
{
|
||||
use BelongsToThrough;
|
||||
|
||||
|
|
|
@ -37,17 +37,20 @@ use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification;
|
|||
*
|
||||
* @property string $name
|
||||
* @property \Pterodactyl\Models\ApiKey[]|\Illuminate\Database\Eloquent\Collection $apiKeys
|
||||
* @property \Pterodactyl\Models\Permission[]|\Illuminate\Database\Eloquent\Collection $permissions
|
||||
* @property \Pterodactyl\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers
|
||||
* @property \Pterodactyl\Models\Subuser[]|\Illuminate\Database\Eloquent\Collection $subuserOf
|
||||
* @property \Pterodactyl\Models\DaemonKey[]|\Illuminate\Database\Eloquent\Collection $keys
|
||||
*/
|
||||
class User extends Validable implements
|
||||
class User extends Model implements
|
||||
AuthenticatableContract,
|
||||
AuthorizableContract,
|
||||
CanResetPasswordContract
|
||||
{
|
||||
use Authenticatable, Authorizable, AvailableLanguages, CanResetPassword, Notifiable, Searchable;
|
||||
use Authenticatable;
|
||||
use Authorizable;
|
||||
use AvailableLanguages;
|
||||
use CanResetPassword;
|
||||
use Notifiable;
|
||||
use Searchable;
|
||||
|
||||
const USER_LEVEL_USER = 0;
|
||||
const USER_LEVEL_ADMIN = 1;
|
||||
|
@ -220,16 +223,6 @@ class User extends Validable implements
|
|||
return trim($this->name_first . ' ' . $this->name_last);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all permissions that a user has.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
|
||||
*/
|
||||
public function permissions()
|
||||
{
|
||||
return $this->hasManyThrough(Permission::class, Subuser::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all servers that a user owns.
|
||||
*
|
||||
|
@ -240,16 +233,6 @@ class User extends Validable implements
|
|||
return $this->hasMany(Server::class, 'owner_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all servers that user is listed as a subuser of directly.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function subuserOf()
|
||||
{
|
||||
return $this->hasMany(Subuser::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all of the daemon keys that a user belongs to.
|
||||
*
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
<?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\Policies;
|
||||
|
||||
use Cache;
|
||||
use Carbon;
|
||||
use Carbon\Carbon;
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Contracts\Cache\Repository as CacheRepository;
|
||||
|
||||
class ServerPolicy
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Cache\Repository
|
||||
*/
|
||||
private $cache;
|
||||
|
||||
/**
|
||||
* ServerPolicy constructor.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Cache\Repository $cache
|
||||
*/
|
||||
public function __construct(CacheRepository $cache)
|
||||
{
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has the given permission on/for the server.
|
||||
*
|
||||
|
@ -26,13 +34,16 @@ class ServerPolicy
|
|||
*/
|
||||
protected function checkPermission(User $user, Server $server, $permission)
|
||||
{
|
||||
$permissions = Cache::remember('ServerPolicy.' . $user->uuid . $server->uuid, Carbon::now()->addSeconds(5), function () use ($user, $server) {
|
||||
return $user->permissions()->server($server)->get()->transform(function ($item) {
|
||||
return $item->permission;
|
||||
})->values();
|
||||
$key = sprintf('ServerPolicy.%s.%s', $user->uuid, $server->uuid);
|
||||
|
||||
$permissions = $this->cache->remember($key, Carbon::now()->addSeconds(5), function () use ($user, $server) {
|
||||
/** @var \Pterodactyl\Models\Subuser|null $subuser */
|
||||
$subuser = $server->subusers()->where('user_id', $user->id)->first();
|
||||
|
||||
return $subuser ? $subuser->permissions : [];
|
||||
});
|
||||
|
||||
return $permissions->search($permission, true) !== false;
|
||||
return in_array($permission, $permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
16
app/Repositories/Eloquent/BackupRepository.php
Normal file
16
app/Repositories/Eloquent/BackupRepository.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Repositories\Eloquent;
|
||||
|
||||
use Pterodactyl\Models\Backup;
|
||||
|
||||
class BackupRepository extends EloquentRepository
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function model()
|
||||
{
|
||||
return Backup::class;
|
||||
}
|
||||
}
|
|
@ -177,6 +177,18 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
|
|||
return ($this->withFresh) ? $instance->fresh() : $saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a model using the attributes passed.
|
||||
*
|
||||
* @param array|\Closure $attributes
|
||||
* @param array $values
|
||||
* @return int
|
||||
*/
|
||||
public function updateWhere($attributes, array $values)
|
||||
{
|
||||
return $this->getBuilder()->where($attributes)->update($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a mass update where matching records are updated using whereIn.
|
||||
* This does not perform any model data validation.
|
||||
|
|
|
@ -174,6 +174,23 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa
|
|||
})->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a node with the given id with the Node's resource usage.
|
||||
*
|
||||
* @param int $node_id
|
||||
* @return Node
|
||||
*/
|
||||
public function getNodeWithResourceUsage(int $node_id): Node
|
||||
{
|
||||
$instance = $this->getBuilder()
|
||||
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemonSecret', 'nodes.daemonListen', '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.id', $node_id);
|
||||
|
||||
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.
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
namespace Pterodactyl\Repositories\Eloquent;
|
||||
|
||||
use Pterodactyl\Models\Subuser;
|
||||
use Illuminate\Support\Collection;
|
||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
||||
use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
|
||||
|
||||
|
@ -20,19 +19,27 @@ class SubuserRepository extends EloquentRepository implements SubuserRepositoryI
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the subusers for the given server instance with the associated user
|
||||
* and permission relationships pre-loaded.
|
||||
* Returns a subuser model for the given user and server combination. If no record
|
||||
* exists an exception will be thrown.
|
||||
*
|
||||
* @param int $server
|
||||
* @return \Illuminate\Support\Collection
|
||||
* @param string $uuid
|
||||
* @return \Pterodactyl\Models\Subuser
|
||||
*
|
||||
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
|
||||
*/
|
||||
public function getSubusersForServer(int $server): Collection
|
||||
public function getUserForServer(int $server, string $uuid): Subuser
|
||||
{
|
||||
return $this->getBuilder()
|
||||
->with('user', 'permissions')
|
||||
->where('server_id', $server)
|
||||
->get()
|
||||
->toBase();
|
||||
/** @var \Pterodactyl\Models\Subuser $model */
|
||||
$model = $this->getBuilder()
|
||||
->with('server', 'user')
|
||||
->select('subusers.*')
|
||||
->join('users', 'users.id', '=', 'subusers.user_id')
|
||||
->where('subusers.server_id', $server)
|
||||
->where('users.uuid', $uuid)
|
||||
->firstOrFail();
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -29,7 +29,7 @@ class UserRepository extends EloquentRepository implements UserRepositoryInterfa
|
|||
*/
|
||||
public function getAllUsersWithCounts(): LengthAwarePaginator
|
||||
{
|
||||
return $this->getBuilder()->withCount('servers', 'subuserOf')
|
||||
return $this->getBuilder()->withCount('servers')
|
||||
->search($this->getSearchTerm())
|
||||
->paginate(50, $this->getColumns());
|
||||
}
|
||||
|
|
63
app/Repositories/Wings/DaemonBackupRepository.php
Normal file
63
app/Repositories/Wings/DaemonBackupRepository.php
Normal file
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Repositories\Wings;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Backup;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
class DaemonBackupRepository extends DaemonRepository
|
||||
{
|
||||
/**
|
||||
* Tells the remote Daemon to begin generating a backup for the server.
|
||||
*
|
||||
* @param \Pterodactyl\Models\Backup $backup
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function backup(Backup $backup): ResponseInterface
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
return $this->getHttpClient()->post(
|
||||
sprintf('/api/servers/%s/backup', $this->server->uuid),
|
||||
[
|
||||
'json' => [
|
||||
'uuid' => $backup->uuid,
|
||||
'ignored_files' => explode(PHP_EOL, $backup->ignored_files),
|
||||
],
|
||||
]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a stream of a backup's contents from the Wings instance so that we
|
||||
* do not need to send the user directly to the Daemon.
|
||||
*
|
||||
* @param string $backup
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function getBackup(string $backup): ResponseInterface
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
return $this->getHttpClient()->get(
|
||||
sprintf('/api/servers/%s/backup/%s', $this->server->uuid, $backup),
|
||||
['stream' => true]
|
||||
);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace Pterodactyl\Repositories\Wings;
|
||||
|
||||
use BadMethodCallException;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Server;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
|
@ -13,7 +12,6 @@ class DaemonServerRepository extends DaemonRepository
|
|||
/**
|
||||
* Returns details about a server from the Daemon instance.
|
||||
*
|
||||
* @return array
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function getDetails(): array
|
||||
|
@ -89,10 +87,20 @@ class DaemonServerRepository extends DaemonRepository
|
|||
|
||||
/**
|
||||
* Reinstall a server on the daemon.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function reinstall(): void
|
||||
{
|
||||
throw new BadMethodCallException('Method is not implemented.');
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
$this->getHttpClient()->post(sprintf(
|
||||
'/api/servers/%s/reinstall', $this->server->uuid
|
||||
));
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -116,4 +124,24 @@ class DaemonServerRepository extends DaemonRepository
|
|||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the daemon to create a full archive of the server.
|
||||
* Once the daemon is finished they will send a POST request to
|
||||
* "/api/remote/servers/{uuid}/archive" with a boolean.
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function requestArchive(): void
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
try {
|
||||
$this->getHttpClient()->post(sprintf(
|
||||
'/api/servers/%s/archive', $this->server->uuid
|
||||
));
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
35
app/Repositories/Wings/DaemonTransferRepository.php
Normal file
35
app/Repositories/Wings/DaemonTransferRepository.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Repositories\Wings;
|
||||
|
||||
use Pterodactyl\Models\Node;
|
||||
use Pterodactyl\Models\Server;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
class DaemonTransferRepository extends DaemonRepository
|
||||
{
|
||||
/**
|
||||
* @param Server $server
|
||||
* @param array $data
|
||||
* @param Node $node
|
||||
* @param string $token
|
||||
*
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function notify(Server $server, array $data, Node $node, string $token): void
|
||||
{
|
||||
try {
|
||||
$this->getHttpClient()->post('/api/transfer', [
|
||||
'json' => [
|
||||
'server_id' => $server->uuid,
|
||||
'url' => $node->getConnectionAddress() . sprintf('/api/servers/%s/archive', $server->uuid),
|
||||
'token' => 'Bearer ' . $token,
|
||||
'server' => $data,
|
||||
],
|
||||
]);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
}
|
91
app/Services/Backups/InitiateBackupService.php
Normal file
91
app/Services/Backups/InitiateBackupService.php
Normal file
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Services\Backups;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Pterodactyl\Models\Backup;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Repositories\Eloquent\BackupRepository;
|
||||
use Pterodactyl\Repositories\Wings\DaemonBackupRepository;
|
||||
|
||||
class InitiateBackupService
|
||||
{
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $ignoredFiles;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Eloquent\BackupRepository
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Database\ConnectionInterface
|
||||
*/
|
||||
private $connection;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Wings\DaemonBackupRepository
|
||||
*/
|
||||
private $daemonBackupRepository;
|
||||
|
||||
/**
|
||||
* InitiateBackupService constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Repositories\Eloquent\BackupRepository $repository
|
||||
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||
* @param \Pterodactyl\Repositories\Wings\DaemonBackupRepository $daemonBackupRepository
|
||||
*/
|
||||
public function __construct(
|
||||
BackupRepository $repository,
|
||||
ConnectionInterface $connection,
|
||||
DaemonBackupRepository $daemonBackupRepository
|
||||
) {
|
||||
$this->repository = $repository;
|
||||
$this->connection = $connection;
|
||||
$this->daemonBackupRepository = $daemonBackupRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the files to be ignored by this backup.
|
||||
*
|
||||
* @param string|null $ignored
|
||||
* @return $this
|
||||
*/
|
||||
public function setIgnoredFiles(?string $ignored)
|
||||
{
|
||||
$this->ignoredFiles = $ignored;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates the backup process for a server on the daemon.
|
||||
*
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @param string|null $name
|
||||
* @return \Pterodactyl\Models\Backup
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function handle(Server $server, string $name = null): Backup
|
||||
{
|
||||
return $this->connection->transaction(function () use ($server, $name) {
|
||||
/** @var \Pterodactyl\Models\Backup $backup */
|
||||
$backup = $this->repository->create([
|
||||
'server_id' => $server->id,
|
||||
'uuid' => Uuid::uuid4()->toString(),
|
||||
'name' => trim($name) ?: sprintf('Backup at %s', CarbonImmutable::now()->toDateTimeString()),
|
||||
'ignored_files' => $this->ignoredFiles ?? '',
|
||||
'disk' => 'local',
|
||||
], true, true);
|
||||
|
||||
$this->daemonBackupRepository->setServer($server)->backup($backup);
|
||||
|
||||
return $backup;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -98,6 +98,7 @@ class BuildModificationService
|
|||
'swap' => array_get($data, 'swap'),
|
||||
'io' => array_get($data, 'io'),
|
||||
'cpu' => array_get($data, 'cpu'),
|
||||
'threads' => array_get($data, 'threads'),
|
||||
'disk' => array_get($data, 'disk'),
|
||||
'allocation_id' => array_get($data, 'allocation_id'),
|
||||
'database_limit' => array_get($data, 'database_limit'),
|
||||
|
|
|
@ -3,11 +3,9 @@
|
|||
namespace Pterodactyl\Services\Servers;
|
||||
|
||||
use Pterodactyl\Models\Server;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
|
||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
class ReinstallServerService
|
||||
{
|
||||
|
@ -44,28 +42,23 @@ class ReinstallServerService
|
|||
}
|
||||
|
||||
/**
|
||||
* @param int|\Pterodactyl\Models\Server $server
|
||||
* Reinstall a server on the remote daemon.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @return \Pterodactyl\Models\Server
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function reinstall($server)
|
||||
public function reinstall(Server $server)
|
||||
{
|
||||
if (! $server instanceof Server) {
|
||||
$server = $this->repository->find($server);
|
||||
}
|
||||
$this->database->transaction(function () use ($server) {
|
||||
$this->repository->withoutFreshModel()->update($server->id, [
|
||||
'installed' => Server::STATUS_INSTALLING,
|
||||
]);
|
||||
|
||||
$this->database->beginTransaction();
|
||||
$this->repository->withoutFreshModel()->update($server->id, [
|
||||
'installed' => 0,
|
||||
], true, true);
|
||||
|
||||
try {
|
||||
$this->daemonServerRepository->setServer($server)->reinstall();
|
||||
$this->database->commit();
|
||||
} catch (RequestException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
});
|
||||
|
||||
return $server->refresh();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,6 +81,7 @@ class ServerConfigurationStructureService
|
|||
'swap' => $server->swap,
|
||||
'io_weight' => $server->io,
|
||||
'cpu_limit' => $server->cpu,
|
||||
'threads' => $server->threads,
|
||||
'disk_space' => $server->disk,
|
||||
],
|
||||
'service' => [
|
||||
|
@ -130,6 +131,7 @@ class ServerConfigurationStructureService
|
|||
'swap' => (int) $server->swap,
|
||||
'io' => (int) $server->io,
|
||||
'cpu' => (int) $server->cpu,
|
||||
'threads' => $server->threads,
|
||||
'disk' => (int) $server->disk,
|
||||
'image' => $server->image,
|
||||
],
|
||||
|
|
|
@ -4,7 +4,6 @@ namespace Pterodactyl\Services\Servers;
|
|||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Illuminate\Support\Arr;
|
||||
use Pterodactyl\Models\Node;
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Support\Collection;
|
||||
|
@ -242,6 +241,7 @@ class ServerCreationService
|
|||
'disk' => Arr::get($data, 'disk'),
|
||||
'io' => Arr::get($data, 'io'),
|
||||
'cpu' => Arr::get($data, 'cpu'),
|
||||
'threads' => Arr::get($data, 'threads'),
|
||||
'oom_disabled' => Arr::get($data, 'oom_disabled', true),
|
||||
'allocation_id' => Arr::get($data, 'allocation_id'),
|
||||
'nest_id' => Arr::get($data, 'nest_id'),
|
||||
|
|
46
app/Services/Servers/TransferService.php
Normal file
46
app/Services/Servers/TransferService.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Services\Servers;
|
||||
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
|
||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||
|
||||
class TransferService
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Wings\DaemonServerRepository
|
||||
*/
|
||||
private $daemonServerRepository;
|
||||
|
||||
/**
|
||||
* TransferService constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository
|
||||
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
|
||||
*/
|
||||
public function __construct(
|
||||
DaemonServerRepository $daemonServerRepository,
|
||||
ServerRepositoryInterface $repository
|
||||
) {
|
||||
$this->repository = $repository;
|
||||
$this->daemonServerRepository = $daemonServerRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests an archive from the daemon.
|
||||
*
|
||||
* @param int|\Pterodactyl\Models\Server $server
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function requestArchive(Server $server)
|
||||
{
|
||||
$this->daemonServerRepository->setServer($server)->requestArchive();
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
<?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\Subusers;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface;
|
||||
|
||||
class PermissionCreationService
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* PermissionCreationService constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface $repository
|
||||
*/
|
||||
public function __construct(PermissionRepositoryInterface $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign permissions to a given subuser.
|
||||
*
|
||||
* @param int $subuser
|
||||
* @param array $permissions
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function handle($subuser, array $permissions)
|
||||
{
|
||||
Assert::integerish($subuser, 'First argument passed to handle must be an integer, received %s.');
|
||||
|
||||
$permissionMappings = Permission::getPermissions(true);
|
||||
$insertPermissions = [];
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
if (array_key_exists($permission, $permissionMappings)) {
|
||||
Assert::stringNotEmpty($permission, 'Permission argument provided must be a non-empty string, received %s.');
|
||||
|
||||
array_push($insertPermissions, [
|
||||
'subuser_id' => $subuser,
|
||||
'permission' => $permission,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($insertPermissions)) {
|
||||
$this->repository->withoutFreshModel()->insert($insertPermissions);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +1,14 @@
|
|||
<?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\Subusers;
|
||||
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\Subuser;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Services\Users\UserCreationService;
|
||||
use Pterodactyl\Repositories\Eloquent\SubuserRepository;
|
||||
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
|
||||
use Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService;
|
||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
||||
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
|
||||
use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
|
||||
use Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException;
|
||||
use Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException;
|
||||
|
||||
|
@ -25,113 +17,87 @@ class SubuserCreationService
|
|||
/**
|
||||
* @var \Illuminate\Database\ConnectionInterface
|
||||
*/
|
||||
protected $connection;
|
||||
private $connection;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService
|
||||
* @var \Pterodactyl\Repositories\Eloquent\SubuserRepository
|
||||
*/
|
||||
protected $keyCreationService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Subusers\PermissionCreationService
|
||||
*/
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface
|
||||
*/
|
||||
protected $subuserRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
|
||||
*/
|
||||
protected $serverRepository;
|
||||
private $subuserRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Users\UserCreationService
|
||||
*/
|
||||
protected $userCreationService;
|
||||
private $userCreationService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
|
||||
*/
|
||||
protected $userRepository;
|
||||
private $userRepository;
|
||||
|
||||
/**
|
||||
* SubuserCreationService constructor.
|
||||
*
|
||||
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||
* @param \Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService $keyCreationService
|
||||
* @param \Pterodactyl\Services\Subusers\PermissionCreationService $permissionService
|
||||
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository
|
||||
* @param \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface $subuserRepository
|
||||
* @param \Pterodactyl\Repositories\Eloquent\SubuserRepository $subuserRepository
|
||||
* @param \Pterodactyl\Services\Users\UserCreationService $userCreationService
|
||||
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository
|
||||
*/
|
||||
public function __construct(
|
||||
ConnectionInterface $connection,
|
||||
DaemonKeyCreationService $keyCreationService,
|
||||
PermissionCreationService $permissionService,
|
||||
ServerRepositoryInterface $serverRepository,
|
||||
SubuserRepositoryInterface $subuserRepository,
|
||||
SubuserRepository $subuserRepository,
|
||||
UserCreationService $userCreationService,
|
||||
UserRepositoryInterface $userRepository
|
||||
) {
|
||||
$this->connection = $connection;
|
||||
$this->keyCreationService = $keyCreationService;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->serverRepository = $serverRepository;
|
||||
$this->subuserRepository = $subuserRepository;
|
||||
$this->userRepository = $userRepository;
|
||||
$this->userCreationService = $userCreationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|\Pterodactyl\Models\Server $server
|
||||
* Creates a new user on the system and assigns them access to the provided server.
|
||||
* If the email address already belongs to a user on the system a new user will not
|
||||
* be created.
|
||||
*
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @param string $email
|
||||
* @param array $permissions
|
||||
* @return \Pterodactyl\Models\Subuser
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
* @throws \Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException
|
||||
* @throws \Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function handle($server, $email, array $permissions)
|
||||
public function handle(Server $server, string $email, array $permissions): Subuser
|
||||
{
|
||||
if (! $server instanceof Server) {
|
||||
$server = $this->serverRepository->find($server);
|
||||
}
|
||||
return $this->connection->transaction(function () use ($server, $email, $permissions) {
|
||||
try {
|
||||
$user = $this->userRepository->findFirstWhere([['email', '=', $email]]);
|
||||
|
||||
$this->connection->beginTransaction();
|
||||
try {
|
||||
$user = $this->userRepository->findFirstWhere([['email', '=', $email]]);
|
||||
if ($server->owner_id === $user->id) {
|
||||
throw new UserIsServerOwnerException(trans('exceptions.subusers.user_is_owner'));
|
||||
}
|
||||
|
||||
if ($server->owner_id === $user->id) {
|
||||
throw new UserIsServerOwnerException(trans('exceptions.subusers.user_is_owner'));
|
||||
$subuserCount = $this->subuserRepository->findCountWhere([['user_id', '=', $user->id], ['server_id', '=', $server->id]]);
|
||||
if ($subuserCount !== 0) {
|
||||
throw new ServerSubuserExistsException(trans('exceptions.subusers.subuser_exists'));
|
||||
}
|
||||
} catch (RecordNotFoundException $exception) {
|
||||
$user = $this->userCreationService->handle([
|
||||
'email' => $email,
|
||||
'username' => preg_replace('/([^\w\.-]+)/', '', strtok($email, '@')) . str_random(3),
|
||||
'name_first' => 'Server',
|
||||
'name_last' => 'Subuser',
|
||||
'root_admin' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
$subuserCount = $this->subuserRepository->findCountWhere([['user_id', '=', $user->id], ['server_id', '=', $server->id]]);
|
||||
if ($subuserCount !== 0) {
|
||||
throw new ServerSubuserExistsException(trans('exceptions.subusers.subuser_exists'));
|
||||
}
|
||||
} catch (RecordNotFoundException $exception) {
|
||||
$username = preg_replace('/([^\w\.-]+)/', '', strtok($email, '@'));
|
||||
$user = $this->userCreationService->handle([
|
||||
'email' => $email,
|
||||
'username' => $username . str_random(3),
|
||||
'name_first' => 'Server',
|
||||
'name_last' => 'Subuser',
|
||||
'root_admin' => false,
|
||||
return $this->subuserRepository->create([
|
||||
'user_id' => $user->id,
|
||||
'server_id' => $server->id,
|
||||
'permissions' => array_unique($permissions),
|
||||
]);
|
||||
}
|
||||
|
||||
$subuser = $this->subuserRepository->create(['user_id' => $user->id, 'server_id' => $server->id]);
|
||||
$this->keyCreationService->handle($server->id, $user->id);
|
||||
$this->permissionService->handle($subuser->id, $permissions);
|
||||
$this->connection->commit();
|
||||
|
||||
return $subuser;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Services\Subusers;
|
||||
|
||||
use Pterodactyl\Models\Subuser;
|
||||
use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
|
||||
|
||||
class SubuserDeletionService
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* SubuserDeletionService constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface $repository
|
||||
*/
|
||||
public function __construct(
|
||||
SubuserRepositoryInterface $repository
|
||||
) {
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a subuser and their associated permissions from the Panel and Daemon.
|
||||
*
|
||||
* @param \Pterodactyl\Models\Subuser $subuser
|
||||
*/
|
||||
public function handle(Subuser $subuser)
|
||||
{
|
||||
$this->repository->delete($subuser->id);
|
||||
}
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
<?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\Subusers;
|
||||
|
||||
use Pterodactyl\Models\Subuser;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService;
|
||||
use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
|
||||
use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface;
|
||||
|
||||
class SubuserUpdateService
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Database\ConnectionInterface
|
||||
*/
|
||||
private $connection;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface
|
||||
*/
|
||||
private $daemonRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService
|
||||
*/
|
||||
private $keyProviderService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface
|
||||
*/
|
||||
private $permissionRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Subusers\PermissionCreationService
|
||||
*/
|
||||
private $permissionService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* SubuserUpdateService constructor.
|
||||
*
|
||||
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||
* @param \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService $keyProviderService
|
||||
* @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonRepository
|
||||
* @param \Pterodactyl\Services\Subusers\PermissionCreationService $permissionService
|
||||
* @param \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface $permissionRepository
|
||||
* @param \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface $repository
|
||||
*/
|
||||
public function __construct(
|
||||
ConnectionInterface $connection,
|
||||
DaemonKeyProviderService $keyProviderService,
|
||||
DaemonServerRepositoryInterface $daemonRepository,
|
||||
PermissionCreationService $permissionService,
|
||||
PermissionRepositoryInterface $permissionRepository,
|
||||
SubuserRepositoryInterface $repository
|
||||
) {
|
||||
$this->connection = $connection;
|
||||
$this->daemonRepository = $daemonRepository;
|
||||
$this->keyProviderService = $keyProviderService;
|
||||
$this->permissionRepository = $permissionRepository;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update permissions for a given subuser.
|
||||
*
|
||||
* @param \Pterodactyl\Models\Subuser $subuser
|
||||
* @param array $permissions
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function handle(Subuser $subuser, array $permissions)
|
||||
{
|
||||
$subuser = $this->repository->loadServerAndUserRelations($subuser);
|
||||
|
||||
$this->connection->beginTransaction();
|
||||
$this->permissionRepository->deleteWhere([['subuser_id', '=', $subuser->id]]);
|
||||
$this->permissionService->handle($subuser->id, $permissions);
|
||||
|
||||
try {
|
||||
$token = $this->keyProviderService->handle($subuser->getRelation('server'), $subuser->getRelation('user'), false);
|
||||
$this->daemonRepository->setServer($subuser->getRelation('server'))->revokeAccessKey($token);
|
||||
} catch (RequestException $exception) {
|
||||
$this->connection->rollBack();
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
|
||||
$this->connection->commit();
|
||||
}
|
||||
}
|
|
@ -75,6 +75,7 @@ class ServerTransformer extends BaseTransformer
|
|||
'disk' => $server->disk,
|
||||
'io' => $server->io,
|
||||
'cpu' => $server->cpu,
|
||||
'threads' => $server->threads,
|
||||
],
|
||||
'feature_limits' => [
|
||||
'databases' => $server->database_limit,
|
||||
|
|
33
app/Transformers/Api/Client/BackupTransformer.php
Normal file
33
app/Transformers/Api/Client/BackupTransformer.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Transformers\Api\Client;
|
||||
|
||||
use Pterodactyl\Models\Backup;
|
||||
|
||||
class BackupTransformer extends BaseClientTransformer
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getResourceName(): string
|
||||
{
|
||||
return Backup::RESOURCE_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Pterodactyl\Models\Backup $backup
|
||||
* @return array
|
||||
*/
|
||||
public function transform(Backup $backup)
|
||||
{
|
||||
return [
|
||||
'uuid' => $backup->uuid,
|
||||
'name' => $backup->name,
|
||||
'ignored_files' => $backup->ignored_files,
|
||||
'sha256_hash' => $backup->sha256_hash,
|
||||
'bytes' => $backup->bytes,
|
||||
'created_at' => $backup->created_at->toIso8601String(),
|
||||
'completed_at' => $backup->completed_at ? $backup->completed_at->toIso8601String() : null,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -2,16 +2,10 @@
|
|||
|
||||
namespace Pterodactyl\Transformers\Api\Client;
|
||||
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\Subuser;
|
||||
|
||||
class SubuserTransformer extends BaseClientTransformer
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultIncludes = ['user'];
|
||||
|
||||
/**
|
||||
* Return the resource name for the JSONAPI output.
|
||||
*
|
||||
|
@ -27,23 +21,13 @@ class SubuserTransformer extends BaseClientTransformer
|
|||
*
|
||||
* @param \Pterodactyl\Models\Subuser $model
|
||||
* @return array|void
|
||||
* @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function transform(Subuser $model)
|
||||
{
|
||||
return [
|
||||
'permissions' => $model->permissions->pluck('permission'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Include the permissions associated with this subuser.
|
||||
*
|
||||
* @param \Pterodactyl\Models\Subuser $model
|
||||
* @return \League\Fractal\Resource\Item
|
||||
* @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeUser(Subuser $model)
|
||||
{
|
||||
return $this->item($model->user, $this->makeTransformer(UserTransformer::class), User::RESOURCE_NAME);
|
||||
return array_merge(
|
||||
$this->makeTransformer(UserTransformer::class)->transform($model->user),
|
||||
['permissions' => $model->permissions]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,16 +15,16 @@
|
|||
"ext-mbstring": "*",
|
||||
"ext-pdo_mysql": "*",
|
||||
"ext-zip": "*",
|
||||
"appstract/laravel-blade-directives": "^1.6",
|
||||
"aws/aws-sdk-php": "^3.110",
|
||||
"cakephp/chronos": "^1.2",
|
||||
"doctrine/dbal": "^2.9",
|
||||
"appstract/laravel-blade-directives": "^1.8",
|
||||
"aws/aws-sdk-php": "^3.134",
|
||||
"cakephp/chronos": "^1.3",
|
||||
"doctrine/dbal": "^2.10",
|
||||
"fideloper/proxy": "^4.2",
|
||||
"guzzlehttp/guzzle": "^6.3",
|
||||
"guzzlehttp/guzzle": "^6.5",
|
||||
"hashids/hashids": "^4.0",
|
||||
"laracasts/utilities": "^3.0",
|
||||
"laravel/framework": "^6.0.0",
|
||||
"laravel/helpers": "^1.1",
|
||||
"laracasts/utilities": "^3.1",
|
||||
"laravel/framework": "^6.18",
|
||||
"laravel/helpers": "^1.2",
|
||||
"laravel/tinker": "^1.0",
|
||||
"lcobucci/jwt": "^3.3",
|
||||
"matriphe/iso-639": "^1.2",
|
||||
|
@ -32,18 +32,18 @@
|
|||
"predis/predis": "^1.1",
|
||||
"prologue/alerts": "^0.4",
|
||||
"s1lentium/iptools": "^1.1",
|
||||
"spatie/laravel-fractal": "^5.6",
|
||||
"staudenmeir/belongs-to-through": "^2.6",
|
||||
"symfony/yaml": "^4.0",
|
||||
"webmozart/assert": "^1.5"
|
||||
"spatie/laravel-fractal": "^5.7",
|
||||
"staudenmeir/belongs-to-through": "^2.9",
|
||||
"symfony/yaml": "^4.4",
|
||||
"webmozart/assert": "^1.7"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "^3.2",
|
||||
"barryvdh/laravel-ide-helper": "^2.6",
|
||||
"codedungeon/phpunit-result-printer": "0.25.1",
|
||||
"friendsofphp/php-cs-fixer": "^2.15.1",
|
||||
"laravel/dusk": "^5.5",
|
||||
"php-mock/php-mock-phpunit": "^2.4",
|
||||
"friendsofphp/php-cs-fixer": "^2.16.1",
|
||||
"laravel/dusk": "^5.11",
|
||||
"php-mock/php-mock-phpunit": "^2.6",
|
||||
"phpunit/phpunit": "^7"
|
||||
},
|
||||
"autoload": {
|
||||
|
|
2323
composer.lock
generated
2323
composer.lock
generated
File diff suppressed because it is too large
Load diff
29
config/backups.php
Normal file
29
config/backups.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
// The backup driver to use for this Panel instance. All client generated server backups
|
||||
// will be stored in this location by default. It is possible to change this once backups
|
||||
// have been made, without losing data.
|
||||
'driver' => env('APP_BACKUP_DRIVER', 'local'),
|
||||
|
||||
'disks' => [
|
||||
// There is no configuration for the local disk for Wings. That configuration
|
||||
// is determined by the Daemon configuration, and not the Panel.
|
||||
'local' => [],
|
||||
|
||||
// Configuration for storing backups in Amazon S3.
|
||||
's3' => [
|
||||
'region' => '',
|
||||
'access_key' => '',
|
||||
'access_secret_key' => '',
|
||||
|
||||
// The S3 bucket to use for backups.
|
||||
'bucket' => '',
|
||||
|
||||
// The location within the S3 bucket where backups will be stored. Backups
|
||||
// are stored within a folder using the server's UUID as the name. Each
|
||||
// backup for that server lives within that folder.
|
||||
'location' => '',
|
||||
],
|
||||
],
|
||||
];
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddThreadsColumnToServersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->string('threads')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropColumn('threads');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateBackupsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('backups', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->unsignedInteger('server_id');
|
||||
$table->char('uuid', 36);
|
||||
$table->string('name');
|
||||
$table->text('ignored_files');
|
||||
$table->string('disk');
|
||||
$table->string('sha256_hash')->nullable();
|
||||
$table->integer('bytes')->default(0);
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique('uuid');
|
||||
$table->foreign('server_id')->references('id')->on('servers')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('backups');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddTableServerTransfers extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::dropIfExists('server_transfers');
|
||||
|
||||
Schema::create('server_transfers', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('server_id')->unsigned();
|
||||
$table->integer('old_node')->unsigned();
|
||||
$table->integer('new_node')->unsigned();
|
||||
$table->integer('old_allocation')->unsigned();
|
||||
$table->integer('new_allocation')->unsigned();
|
||||
$table->string('old_additional_allocations')->nullable();
|
||||
$table->string('new_additional_allocations')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::table('server_transfers', function (Blueprint $table) {
|
||||
$table->foreign('server_id')->references('id')->on('servers');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('server_transfers');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddSuccessfulColumnToServerTransfers extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('server_transfers', function (Blueprint $table) {
|
||||
$table->tinyInteger('successful')->unsigned()->default(0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('server_transfers', function (Blueprint $table) {
|
||||
$table->dropColumn('successful');
|
||||
});
|
||||
}
|
||||
}
|
56
public/themes/pterodactyl/js/admin/server/transfer.js
Normal file
56
public/themes/pterodactyl/js/admin/server/transfer.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
$(document).ready(function () {
|
||||
$('#pNodeId').select2({
|
||||
placeholder: 'Select a Node',
|
||||
}).change();
|
||||
|
||||
$('#pAllocation').select2({
|
||||
placeholder: 'Select a Default Allocation',
|
||||
});
|
||||
|
||||
$('#pAllocationAdditional').select2({
|
||||
placeholder: 'Select Additional Allocations',
|
||||
});
|
||||
});
|
||||
|
||||
$('#pNodeId').on('change', function () {
|
||||
let currentNode = $(this).val();
|
||||
|
||||
$.each(Pterodactyl.nodeData, function (i, v) {
|
||||
if (v.id == currentNode) {
|
||||
$('#pAllocation').html('').select2({
|
||||
data: v.allocations,
|
||||
placeholder: 'Select a Default Allocation',
|
||||
});
|
||||
|
||||
updateAdditionalAllocations();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#pAllocation').on('change', function () {
|
||||
updateAdditionalAllocations();
|
||||
});
|
||||
|
||||
function updateAdditionalAllocations() {
|
||||
let currentAllocation = $('#pAllocation').val();
|
||||
let currentNode = $('#pNodeId').val();
|
||||
|
||||
$.each(Pterodactyl.nodeData, function (i, v) {
|
||||
if (v.id == currentNode) {
|
||||
let allocations = [];
|
||||
|
||||
for (let i = 0; i < v.allocations.length; i++) {
|
||||
const allocation = v.allocations[i];
|
||||
|
||||
if (allocation.id != currentAllocation) {
|
||||
allocations.push(allocation);
|
||||
}
|
||||
}
|
||||
|
||||
$('#pAllocationAdditional').html('').select2({
|
||||
data: allocations,
|
||||
placeholder: 'Select Additional Allocations',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -27,5 +27,8 @@ return [
|
|||
'details_updated' => 'Server details have been successfully updated.',
|
||||
'docker_image_updated' => 'Successfully changed the default Docker image to use for this server. A reboot is required to apply this change.',
|
||||
'node_required' => 'You must have at least one node configured before you can add a server to this panel.',
|
||||
'transfer_nodes_required' => 'You must have at least two nodes configured before you can transfer servers.',
|
||||
'transfer_started' => 'Server transfer has been started.',
|
||||
'transfer_not_viable' => 'The node you selected is not viable for this transfer.',
|
||||
],
|
||||
];
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { rawDataToServerObject, Server } from '@/api/server/getServer';
|
||||
import http, { getPaginationSet, PaginatedResult } from '@/api/http';
|
||||
|
||||
export default (): Promise<PaginatedResult<Server>> => {
|
||||
export default (query?: string): Promise<PaginatedResult<Server>> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client`, { params: { include: [ 'allocation' ] } })
|
||||
http.get(`/api/client`, { params: { include: [ 'allocation' ], query } })
|
||||
.then(({ data }) => resolve({
|
||||
items: (data.data || []).map((datum: any) => rawDataToServerObject(datum.attributes)),
|
||||
pagination: getPaginationSet(data.meta.pagination),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { SubuserPermission } from '@/state/server/subusers';
|
||||
import { PanelPermissions } from '@/state/permissions';
|
||||
import http from '@/api/http';
|
||||
|
||||
export default (): Promise<SubuserPermission[]> => {
|
||||
export default (): Promise<PanelPermissions> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client/permissions`)
|
||||
.then(({ data }) => resolve(data.attributes.permissions))
|
||||
|
|
12
resources/scripts/api/server/backups/createServerBackup.ts
Normal file
12
resources/scripts/api/server/backups/createServerBackup.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { rawDataToServerBackup, ServerBackup } from '@/api/server/backups/getServerBackups';
|
||||
import http from '@/api/http';
|
||||
|
||||
export default (uuid: string, name?: string, ignore?: string): Promise<ServerBackup> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.post(`/api/client/servers/${uuid}/backups`, {
|
||||
name, ignore,
|
||||
})
|
||||
.then(({ data }) => resolve(rawDataToServerBackup(data)))
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
32
resources/scripts/api/server/backups/getServerBackups.ts
Normal file
32
resources/scripts/api/server/backups/getServerBackups.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http';
|
||||
|
||||
export interface ServerBackup {
|
||||
uuid: string;
|
||||
name: string;
|
||||
ignoredFiles: string;
|
||||
sha256Hash: string;
|
||||
bytes: number;
|
||||
createdAt: Date;
|
||||
completedAt: Date | null;
|
||||
}
|
||||
|
||||
export const rawDataToServerBackup = ({ attributes }: FractalResponseData): ServerBackup => ({
|
||||
uuid: attributes.uuid,
|
||||
name: attributes.name,
|
||||
ignoredFiles: attributes.ignored_files,
|
||||
sha256Hash: attributes.sha256_hash,
|
||||
bytes: attributes.bytes,
|
||||
createdAt: new Date(attributes.created_at),
|
||||
completedAt: attributes.completed_at ? new Date(attributes.completed_at) : null,
|
||||
});
|
||||
|
||||
export default (uuid: string, page?: number | string): Promise<PaginatedResult<ServerBackup>> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client/servers/${uuid}/backups`, { params: { page } })
|
||||
.then(({ data }) => resolve({
|
||||
items: (data.data || []).map(rawDataToServerBackup),
|
||||
pagination: getPaginationSet(data.meta.pagination),
|
||||
}))
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
|
@ -24,6 +24,7 @@ export interface Server {
|
|||
disk: number;
|
||||
io: number;
|
||||
cpu: number;
|
||||
threads: string;
|
||||
};
|
||||
featureLimits: {
|
||||
databases: number;
|
||||
|
@ -41,20 +42,23 @@ export const rawDataToServerObject = (data: any): Server => ({
|
|||
port: data.sftp_details.port,
|
||||
},
|
||||
description: data.description ? ((data.description.length > 0) ? data.description : null) : null,
|
||||
allocations: [{
|
||||
allocations: [ {
|
||||
ip: data.allocation.ip,
|
||||
alias: null,
|
||||
port: data.allocation.port,
|
||||
default: true,
|
||||
}],
|
||||
} ],
|
||||
limits: { ...data.limits },
|
||||
featureLimits: { ...data.feature_limits },
|
||||
});
|
||||
|
||||
export default (uuid: string): Promise<Server> => {
|
||||
export default (uuid: string): Promise<[ Server, string[] ]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client/servers/${uuid}`)
|
||||
.then(response => resolve(rawDataToServerObject(response.data.attributes)))
|
||||
.then(({ data }) => resolve([
|
||||
rawDataToServerObject(data.attributes),
|
||||
data.meta?.is_server_owner ? ['*'] : (data.meta?.user_permissions || []),
|
||||
]))
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
|
9
resources/scripts/api/server/reinstallServer.ts
Normal file
9
resources/scripts/api/server/reinstallServer.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import http from '@/api/http';
|
||||
|
||||
export default (uuid: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.post(`/api/client/servers/${uuid}/settings/reinstall`)
|
||||
.then(() => resolve())
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
18
resources/scripts/api/server/users/createOrUpdateSubuser.ts
Normal file
18
resources/scripts/api/server/users/createOrUpdateSubuser.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import http from '@/api/http';
|
||||
import { rawDataToServerSubuser } from '@/api/server/users/getServerSubusers';
|
||||
import { Subuser } from '@/state/server/subusers';
|
||||
|
||||
interface Params {
|
||||
email: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export default (uuid: string, params: Params, subuser?: Subuser): Promise<Subuser> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.post(`/api/client/servers/${uuid}/users${subuser ? `/${subuser.uuid}` : ''}`, {
|
||||
...params,
|
||||
})
|
||||
.then(data => resolve(rawDataToServerSubuser(data.data)))
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
9
resources/scripts/api/server/users/deleteSubuser.ts
Normal file
9
resources/scripts/api/server/users/deleteSubuser.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import http from '@/api/http';
|
||||
|
||||
export default (uuid: string, userId: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.delete(`/api/client/servers/${uuid}/users/${userId}`)
|
||||
.then(() => resolve())
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
|
@ -8,13 +8,13 @@ export const rawDataToServerSubuser = (data: FractalResponseData): Subuser => ({
|
|||
image: data.attributes.image,
|
||||
twoFactorEnabled: data.attributes['2fa_enabled'],
|
||||
createdAt: new Date(data.attributes.created_at),
|
||||
permissions: data.attributes.relationships!.permissions.attributes.permissions,
|
||||
can: permission => data.attributes.relationships!.permissions.attributes.permissions.indexOf(permission) >= 0,
|
||||
permissions: data.attributes.permissions || [],
|
||||
can: permission => (data.attributes.permissions || []).indexOf(permission) >= 0,
|
||||
});
|
||||
|
||||
export default (uuid: string): Promise<Subuser[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client/servers/${uuid}/users`, { params: { include: [ 'permissions' ] } })
|
||||
http.get(`/api/client/servers/${uuid}/users`)
|
||||
.then(({ data }) => resolve((data.data || []).map(rawDataToServerSubuser)))
|
||||
.catch(reject);
|
||||
});
|
||||
|
|
|
@ -8,6 +8,8 @@ import { faSwatchbook } from '@fortawesome/free-solid-svg-icons/faSwatchbook';
|
|||
import { faCogs } from '@fortawesome/free-solid-svg-icons/faCogs';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons/faSearch';
|
||||
import SearchContainer from '@/components/dashboard/search/SearchContainer';
|
||||
|
||||
export default () => {
|
||||
const user = useStoreState((state: ApplicationStore) => state.user.data!);
|
||||
|
@ -22,6 +24,7 @@ export default () => {
|
|||
</Link>
|
||||
</div>
|
||||
<div className={'right-navigation'}>
|
||||
<SearchContainer/>
|
||||
<NavLink to={'/'} exact={true}>
|
||||
<FontAwesomeIcon icon={faLayerGroup}/>
|
||||
</NavLink>
|
||||
|
|
|
@ -4,25 +4,24 @@ import requestPasswordResetEmail from '@/api/auth/requestPasswordResetEmail';
|
|||
import { httpErrorToHuman } from '@/api/http';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import Field from '@/components/elements/Field';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { object, string } from 'yup';
|
||||
|
||||
interface Values {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const [ isSubmitting, setSubmitting ] = React.useState(false);
|
||||
const [ email, setEmail ] = React.useState('');
|
||||
|
||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
const handleFieldUpdate = (e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value);
|
||||
|
||||
const handleSubmission = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const handleSubmission = ({ email }: Values, { setSubmitting, resetForm }: FormikHelpers<Values>) => {
|
||||
setSubmitting(true);
|
||||
clearFlashes();
|
||||
requestPasswordResetEmail(email)
|
||||
.then(response => {
|
||||
setEmail('');
|
||||
resetForm();
|
||||
addFlash({ type: 'success', title: 'Success', message: response });
|
||||
})
|
||||
.catch(error => {
|
||||
|
@ -33,46 +32,50 @@ export default () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
|
||||
Request Password Reset
|
||||
</h2>
|
||||
<FlashMessageRender/>
|
||||
<LoginFormContainer onSubmit={handleSubmission}>
|
||||
<label htmlFor={'email'}>Email</label>
|
||||
<input
|
||||
id={'email'}
|
||||
type={'email'}
|
||||
required={true}
|
||||
className={'input'}
|
||||
value={email}
|
||||
onChange={handleFieldUpdate}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<p className={'input-help'}>
|
||||
Enter your account email address to receive instructions on resetting your password.
|
||||
</p>
|
||||
<div className={'mt-6'}>
|
||||
<button
|
||||
className={'btn btn-primary btn-jumbo flex justify-center'}
|
||||
disabled={isSubmitting || email.length < 5}
|
||||
>
|
||||
{isSubmitting ?
|
||||
<div className={'spinner-circle spinner-sm spinner-white'}></div>
|
||||
:
|
||||
'Send Email'
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
<div className={'mt-6 text-center'}>
|
||||
<Link
|
||||
to={'/auth/login'}
|
||||
className={'text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'}
|
||||
>
|
||||
Return to Login
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
</div>
|
||||
<Formik
|
||||
onSubmit={handleSubmission}
|
||||
initialValues={{ email: '' }}
|
||||
validationSchema={object().shape({
|
||||
email: string().email('A valid email address must be provided to continue.')
|
||||
.required('A valid email address must be provided to continue.'),
|
||||
})}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<LoginFormContainer
|
||||
title={'Request Password Reset'}
|
||||
className={'w-full flex'}
|
||||
>
|
||||
<Field
|
||||
light={true}
|
||||
label={'Email'}
|
||||
description={'Enter your account email address to receive instructions on resetting your password.'}
|
||||
name={'email'}
|
||||
type={'email'}
|
||||
/>
|
||||
<div className={'mt-6'}>
|
||||
<button
|
||||
type={'submit'}
|
||||
className={'btn btn-primary btn-jumbo flex justify-center'}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ?
|
||||
<div className={'spinner-circle spinner-sm spinner-white'}></div>
|
||||
:
|
||||
'Send Email'
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
<div className={'mt-6 text-center'}>
|
||||
<Link
|
||||
type={'button'}
|
||||
to={'/auth/login'}
|
||||
className={'text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'}
|
||||
>
|
||||
Return to Login
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,7 +2,6 @@ import React, { useRef } from 'react';
|
|||
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||
import login, { LoginData } from '@/api/auth/login';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { ActionCreator, Actions, useStoreActions, useStoreState } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { FormikProps, withFormik } from 'formik';
|
||||
|
@ -12,33 +11,12 @@ import { httpErrorToHuman } from '@/api/http';
|
|||
import { FlashMessage } from '@/state/flashes';
|
||||
import ReCAPTCHA from 'react-google-recaptcha';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import styled from 'styled-components';
|
||||
import { breakpoint } from 'styled-components-breakpoint';
|
||||
|
||||
type OwnProps = RouteComponentProps & {
|
||||
clearFlashes: ActionCreator<void>;
|
||||
addFlash: ActionCreator<FlashMessage>;
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
${breakpoint('sm')`
|
||||
${tw`w-4/5 mx-auto`}
|
||||
`};
|
||||
|
||||
${breakpoint('md')`
|
||||
${tw`p-10`}
|
||||
`};
|
||||
|
||||
${breakpoint('lg')`
|
||||
${tw`w-3/5`}
|
||||
`};
|
||||
|
||||
${breakpoint('xl')`
|
||||
${tw`w-full`}
|
||||
max-width: 660px;
|
||||
`};
|
||||
`;
|
||||
|
||||
const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handleSubmit }: OwnProps & FormikProps<LoginData>) => {
|
||||
const ref = useRef<ReCAPTCHA | null>(null);
|
||||
const { enabled: recaptchaEnabled, siteKey } = useStoreState<ApplicationStore, any>(state => state.settings.data!.recaptcha);
|
||||
|
@ -56,66 +34,61 @@ const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handl
|
|||
return (
|
||||
<React.Fragment>
|
||||
{ref.current && ref.current.render()}
|
||||
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
|
||||
Login to Continue
|
||||
</h2>
|
||||
<Container>
|
||||
<FlashMessageRender className={'mb-2 px-1'}/>
|
||||
<LoginFormContainer
|
||||
className={'w-full flex'}
|
||||
onSubmit={submit}
|
||||
>
|
||||
<label htmlFor={'username'}>Username or Email</label>
|
||||
<LoginFormContainer
|
||||
title={'Login to Continue'}
|
||||
className={'w-full flex'}
|
||||
onSubmit={submit}
|
||||
>
|
||||
<label htmlFor={'username'}>Username or Email</label>
|
||||
<Field
|
||||
type={'text'}
|
||||
id={'username'}
|
||||
name={'username'}
|
||||
className={'input'}
|
||||
/>
|
||||
<div className={'mt-6'}>
|
||||
<label htmlFor={'password'}>Password</label>
|
||||
<Field
|
||||
type={'text'}
|
||||
id={'username'}
|
||||
name={'username'}
|
||||
type={'password'}
|
||||
id={'password'}
|
||||
name={'password'}
|
||||
className={'input'}
|
||||
/>
|
||||
<div className={'mt-6'}>
|
||||
<label htmlFor={'password'}>Password</label>
|
||||
<Field
|
||||
type={'password'}
|
||||
id={'password'}
|
||||
name={'password'}
|
||||
className={'input'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<button
|
||||
type={'submit'}
|
||||
className={'btn btn-primary btn-jumbo'}
|
||||
>
|
||||
{isSubmitting ?
|
||||
<Spinner size={'tiny'} className={'mx-auto'}/>
|
||||
:
|
||||
'Login'
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
{recaptchaEnabled &&
|
||||
<ReCAPTCHA
|
||||
ref={ref}
|
||||
size={'invisible'}
|
||||
sitekey={siteKey || '_invalid_key'}
|
||||
onChange={token => {
|
||||
ref.current && ref.current.reset();
|
||||
setFieldValue('recaptchaData', token);
|
||||
submitForm();
|
||||
}}
|
||||
onExpired={() => setFieldValue('recaptchaData', null)}
|
||||
/>
|
||||
}
|
||||
<div className={'mt-6 text-center'}>
|
||||
<Link
|
||||
to={'/auth/password'}
|
||||
className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
</Container>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<button
|
||||
type={'submit'}
|
||||
className={'btn btn-primary btn-jumbo'}
|
||||
>
|
||||
{isSubmitting ?
|
||||
<Spinner size={'tiny'} className={'mx-auto'}/>
|
||||
:
|
||||
'Login'
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
{recaptchaEnabled &&
|
||||
<ReCAPTCHA
|
||||
ref={ref}
|
||||
size={'invisible'}
|
||||
sitekey={siteKey || '_invalid_key'}
|
||||
onChange={token => {
|
||||
ref.current && ref.current.reset();
|
||||
setFieldValue('recaptchaData', token);
|
||||
submitForm();
|
||||
}}
|
||||
onExpired={() => setFieldValue('recaptchaData', null)}
|
||||
/>
|
||||
}
|
||||
<div className={'mt-6 text-center'}>
|
||||
<Link
|
||||
to={'/auth/password'}
|
||||
className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,17 +1,47 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import { Form } from 'formik';
|
||||
import styled from 'styled-components';
|
||||
import { breakpoint } from 'styled-components-breakpoint';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
|
||||
type Props = React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>;
|
||||
type Props = React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement> & {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export default forwardRef<any, Props>(({ ...props }, ref) => (
|
||||
<Form {...props}>
|
||||
<div className={'md:flex w-full bg-white shadow-lg rounded-lg p-6 mx-1'}>
|
||||
<div className={'flex-none select-none mb-6 md:mb-0 self-center'}>
|
||||
<img src={'/assets/pterodactyl.svg'} className={'block w-48 md:w-64 mx-auto'}/>
|
||||
const Container = styled.div`
|
||||
${breakpoint('sm')`
|
||||
${tw`w-4/5 mx-auto`}
|
||||
`};
|
||||
|
||||
${breakpoint('md')`
|
||||
${tw`p-10`}
|
||||
`};
|
||||
|
||||
${breakpoint('lg')`
|
||||
${tw`w-3/5`}
|
||||
`};
|
||||
|
||||
${breakpoint('xl')`
|
||||
${tw`w-full`}
|
||||
max-width: 700px;
|
||||
`};
|
||||
`;
|
||||
|
||||
export default forwardRef<HTMLFormElement, Props>(({ title, ...props }, ref) => (
|
||||
<Container>
|
||||
{title && <h2 className={'text-center text-neutral-100 font-medium py-4'}>
|
||||
{title}
|
||||
</h2>}
|
||||
<FlashMessageRender className={'mb-2 px-1'}/>
|
||||
<Form {...props} ref={ref}>
|
||||
<div className={'md:flex w-full bg-white shadow-lg rounded-lg p-6 md:pl-0 mx-1'}>
|
||||
<div className={'flex-none select-none mb-6 md:mb-0 self-center'}>
|
||||
<img src={'/assets/pterodactyl.svg'} className={'block w-48 md:w-64 mx-auto'}/>
|
||||
</div>
|
||||
<div className={'flex-1'}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
<div className={'flex-1'}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</Form>
|
||||
</Container>
|
||||
));
|
||||
|
|
|
@ -5,106 +5,109 @@ import { Link } from 'react-router-dom';
|
|||
import performPasswordReset from '@/api/auth/performPasswordReset';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { object, ref, string } from 'yup';
|
||||
import Field from '@/components/elements/Field';
|
||||
|
||||
type Props = Readonly<RouteComponentProps<{ token: string }> & {}>;
|
||||
|
||||
export default (props: Props) => {
|
||||
const [ isLoading, setIsLoading ] = useState(false);
|
||||
interface Values {
|
||||
password: string;
|
||||
passwordConfirmation: string;
|
||||
}
|
||||
|
||||
export default ({ match, history, location }: Props) => {
|
||||
const [ email, setEmail ] = useState('');
|
||||
const [ password, setPassword ] = useState('');
|
||||
const [ passwordConfirm, setPasswordConfirm ] = useState('');
|
||||
|
||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
const parsed = parse(props.location.search);
|
||||
const parsed = parse(location.search);
|
||||
if (email.length === 0 && parsed.email) {
|
||||
setEmail(parsed.email as string);
|
||||
}
|
||||
|
||||
const canSubmit = () => password && email && password.length >= 8 && password === passwordConfirm;
|
||||
|
||||
const submit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!password || !email || !passwordConfirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const submit = ({ password, passwordConfirmation }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
clearFlashes();
|
||||
|
||||
performPasswordReset(email, {
|
||||
token: props.match.params.token, password, passwordConfirmation: passwordConfirm,
|
||||
})
|
||||
performPasswordReset(email, { token: match.params.token, password, passwordConfirmation })
|
||||
.then(() => {
|
||||
addFlash({ type: 'success', message: 'Your password has been reset, please login to continue.' });
|
||||
props.history.push('/auth/login');
|
||||
// @ts-ignore
|
||||
window.location = '/';
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
|
||||
setSubmitting(false);
|
||||
addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
|
||||
})
|
||||
.then(() => setIsLoading(false));
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
|
||||
Reset Password
|
||||
</h2>
|
||||
<FlashMessageRender/>
|
||||
<LoginFormContainer onSubmit={submit}>
|
||||
<label>Email</label>
|
||||
<input className={'input'} value={email} disabled={true}/>
|
||||
<div className={'mt-6'}>
|
||||
<label htmlFor={'new_password'}>New Password</label>
|
||||
<input
|
||||
id={'new_password'}
|
||||
className={'input'}
|
||||
type={'password'}
|
||||
required={true}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
<p className={'input-help'}>
|
||||
Passwords must be at least 8 characters in length.
|
||||
</p>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<label htmlFor={'new_password_confirm'}>Confirm New Password</label>
|
||||
<input
|
||||
id={'new_password_confirm'}
|
||||
className={'input'}
|
||||
type={'password'}
|
||||
required={true}
|
||||
onChange={e => setPasswordConfirm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<button
|
||||
type={'submit'}
|
||||
className={'btn btn-primary btn-jumbo'}
|
||||
disabled={isLoading || !canSubmit()}
|
||||
>
|
||||
{isLoading ?
|
||||
<Spinner size={'tiny'} className={'mx-auto'}/>
|
||||
:
|
||||
'Reset Password'
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
<div className={'mt-6 text-center'}>
|
||||
<Link
|
||||
to={'/auth/login'}
|
||||
className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
|
||||
>
|
||||
Return to Login
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
</div>
|
||||
<Formik
|
||||
onSubmit={submit}
|
||||
initialValues={{
|
||||
password: '',
|
||||
passwordConfirmation: '',
|
||||
}}
|
||||
validationSchema={object().shape({
|
||||
password: string().required('A new password is required.')
|
||||
.min(8, 'Your new password should be at least 8 characters in length.'),
|
||||
passwordConfirmation: string()
|
||||
.required('Your new password does not match.')
|
||||
.oneOf([ref('password'), null], 'Your new password does not match.'),
|
||||
})}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<LoginFormContainer
|
||||
title={'Reset Password'}
|
||||
className={'w-full flex'}
|
||||
>
|
||||
<div>
|
||||
<label>Email</label>
|
||||
<input className={'input'} value={email} disabled={true}/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<Field
|
||||
light={true}
|
||||
label={'New Password'}
|
||||
name={'password'}
|
||||
type={'password'}
|
||||
description={'Passwords must be at least 8 characters in length.'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<Field
|
||||
light={true}
|
||||
label={'Confirm New Password'}
|
||||
name={'passwordConfirmation'}
|
||||
type={'password'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<button
|
||||
type={'submit'}
|
||||
className={'btn btn-primary btn-jumbo'}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ?
|
||||
<Spinner size={'tiny'} className={'mx-auto'}/>
|
||||
:
|
||||
'Reset Password'
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
<div className={'mt-6 text-center'}>
|
||||
<Link
|
||||
to={'/auth/login'}
|
||||
className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
|
||||
>
|
||||
Return to Login
|
||||
</Link>
|
||||
</div>
|
||||
</LoginFormContainer>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -46,62 +46,67 @@ export default () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={'my-10 flex'}>
|
||||
<div className={'my-10'}>
|
||||
<FlashMessageRender byKey={'account'} className={'mb-4'}/>
|
||||
<ContentBox title={'Create API Key'} className={'flex-1'}>
|
||||
<CreateApiKeyForm onKeyCreated={key => setKeys(s => ([...s!, key]))}/>
|
||||
</ContentBox>
|
||||
<ContentBox title={'API Keys'} className={'ml-10 flex-1'}>
|
||||
<SpinnerOverlay visible={loading}/>
|
||||
{deleteIdentifier &&
|
||||
<ConfirmationModal
|
||||
title={'Confirm key deletion'}
|
||||
buttonText={'Yes, delete key'}
|
||||
visible={true}
|
||||
onConfirmed={() => {
|
||||
doDeletion(deleteIdentifier);
|
||||
setDeleteIdentifier('');
|
||||
}}
|
||||
onCanceled={() => setDeleteIdentifier('')}
|
||||
>
|
||||
Are you sure you wish to delete this API key? All requests using it will immediately be
|
||||
invalidated and will fail.
|
||||
</ConfirmationModal>
|
||||
}
|
||||
{
|
||||
keys.length === 0 ?
|
||||
<p className={'text-center text-sm'}>
|
||||
{loading ? 'Loading...' : 'No API keys exist for this account.'}
|
||||
</p>
|
||||
:
|
||||
keys.map(key => (
|
||||
<div key={key.identifier} className={'grey-row-box bg-neutral-600 mb-2 flex items-center'}>
|
||||
<FontAwesomeIcon icon={faKey} className={'text-neutral-300'}/>
|
||||
<div className={'ml-4 flex-1'}>
|
||||
<p className={'text-sm'}>{key.description}</p>
|
||||
<p className={'text-2xs text-neutral-300 uppercase'}>
|
||||
Last
|
||||
used: {key.lastUsedAt ? format(key.lastUsedAt, 'MMM Do, YYYY HH:mm') : 'Never'}
|
||||
</p>
|
||||
</div>
|
||||
<p className={'text-sm ml-4'}>
|
||||
<code className={'font-mono py-1 px-2 bg-neutral-900 rounded'}>
|
||||
{key.identifier}
|
||||
</code>
|
||||
</p>
|
||||
<button
|
||||
className={'ml-4 p-2 text-sm'}
|
||||
onClick={() => setDeleteIdentifier(key.identifier)}
|
||||
<div className={'flex'}>
|
||||
<ContentBox title={'Create API Key'} className={'flex-1'}>
|
||||
<CreateApiKeyForm onKeyCreated={key => setKeys(s => ([ ...s!, key ]))}/>
|
||||
</ContentBox>
|
||||
<ContentBox title={'API Keys'} className={'ml-10 flex-1'}>
|
||||
<SpinnerOverlay visible={loading}/>
|
||||
{deleteIdentifier &&
|
||||
<ConfirmationModal
|
||||
title={'Confirm key deletion'}
|
||||
buttonText={'Yes, delete key'}
|
||||
visible={true}
|
||||
onConfirmed={() => {
|
||||
doDeletion(deleteIdentifier);
|
||||
setDeleteIdentifier('');
|
||||
}}
|
||||
onDismissed={() => setDeleteIdentifier('')}
|
||||
>
|
||||
Are you sure you wish to delete this API key? All requests using it will immediately be
|
||||
invalidated and will fail.
|
||||
</ConfirmationModal>
|
||||
}
|
||||
{
|
||||
keys.length === 0 ?
|
||||
<p className={'text-center text-sm'}>
|
||||
{loading ? 'Loading...' : 'No API keys exist for this account.'}
|
||||
</p>
|
||||
:
|
||||
keys.map(key => (
|
||||
<div
|
||||
key={key.identifier}
|
||||
className={'grey-row-box bg-neutral-600 mb-2 flex items-center'}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faTrashAlt}
|
||||
className={'text-neutral-400 hover:text-red-400 transition-color duration-150'}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</ContentBox>
|
||||
<FontAwesomeIcon icon={faKey} className={'text-neutral-300'}/>
|
||||
<div className={'ml-4 flex-1'}>
|
||||
<p className={'text-sm'}>{key.description}</p>
|
||||
<p className={'text-2xs text-neutral-300 uppercase'}>
|
||||
Last
|
||||
used: {key.lastUsedAt ? format(key.lastUsedAt, 'MMM Do, YYYY HH:mm') : 'Never'}
|
||||
</p>
|
||||
</div>
|
||||
<p className={'text-sm ml-4'}>
|
||||
<code className={'font-mono py-1 px-2 bg-neutral-900 rounded'}>
|
||||
{key.identifier}
|
||||
</code>
|
||||
</p>
|
||||
<button
|
||||
className={'ml-4 p-2 text-sm'}
|
||||
onClick={() => setDeleteIdentifier(key.identifier)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faTrashAlt}
|
||||
className={'text-neutral-400 hover:text-red-400 transition-colors duration-150'}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</ContentBox>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons/faSearch';
|
||||
import useEventListener from '@/plugins/useEventListener';
|
||||
import SearchModal from '@/components/dashboard/search/SearchModal';
|
||||
|
||||
export default () => {
|
||||
const [ visible, setVisible ] = useState(false);
|
||||
|
||||
useEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if ([ 'input', 'textarea' ].indexOf(((e.target as HTMLElement).tagName || 'input').toLowerCase()) < 0) {
|
||||
if (!visible && e.key.toLowerCase() === 'k') {
|
||||
setVisible(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{visible &&
|
||||
<SearchModal
|
||||
appear={true}
|
||||
visible={visible}
|
||||
onDismissed={() => setVisible(false)}
|
||||
/>
|
||||
}
|
||||
<div className={'navigation-link'} onClick={() => setVisible(true)}>
|
||||
<FontAwesomeIcon icon={faSearch}/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
123
resources/scripts/components/dashboard/search/SearchModal.tsx
Normal file
123
resources/scripts/components/dashboard/search/SearchModal.tsx
Normal file
|
@ -0,0 +1,123 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
|
||||
import { Field, Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||
import { Actions, useStoreActions, useStoreState } from 'easy-peasy';
|
||||
import { object, string } from 'yup';
|
||||
import { debounce } from 'lodash-es';
|
||||
import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper';
|
||||
import InputSpinner from '@/components/elements/InputSpinner';
|
||||
import getServers from '@/api/getServers';
|
||||
import { Server } from '@/api/server/getServer';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
type Props = RequiredModalProps;
|
||||
|
||||
interface Values {
|
||||
term: string;
|
||||
}
|
||||
|
||||
const SearchWatcher = () => {
|
||||
const { values, submitForm } = useFormikContext<Values>();
|
||||
|
||||
useEffect(() => {
|
||||
if (values.term.length >= 3) {
|
||||
submitForm();
|
||||
}
|
||||
}, [ values.term ]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ({ ...props }: Props) => {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const [ loading, setLoading ] = useState(false);
|
||||
const [ servers, setServers ] = useState<Server[]>([]);
|
||||
const isAdmin = useStoreState(state => state.user.data!.rootAdmin);
|
||||
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
const search = debounce(({ term }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
setLoading(true);
|
||||
setSubmitting(false);
|
||||
clearFlashes('search');
|
||||
getServers(term)
|
||||
.then(servers => setServers(servers.items.filter((_, index) => index < 5)))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
addError({ key: 'search', message: httpErrorToHuman(error) });
|
||||
})
|
||||
.then(() => setLoading(false));
|
||||
}, 500);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.visible) {
|
||||
setTimeout(() => ref.current?.focus(), 250);
|
||||
}
|
||||
}, [ props.visible ]);
|
||||
|
||||
return (
|
||||
<Formik
|
||||
onSubmit={search}
|
||||
validationSchema={object().shape({
|
||||
term: string()
|
||||
.min(3, 'Please enter at least three characters to begin searching.')
|
||||
.required('A search term must be provided.'),
|
||||
})}
|
||||
initialValues={{ term: '' } as Values}
|
||||
>
|
||||
<Modal {...props}>
|
||||
<Form>
|
||||
<FormikFieldWrapper
|
||||
name={'term'}
|
||||
label={'Search term'}
|
||||
description={
|
||||
isAdmin
|
||||
? 'Enter a server name, user email, or uuid to begin searching.'
|
||||
: 'Enter a server name to begin searching.'
|
||||
}
|
||||
>
|
||||
<SearchWatcher/>
|
||||
<InputSpinner visible={loading}>
|
||||
<Field
|
||||
innerRef={ref}
|
||||
name={'term'}
|
||||
className={'input-dark'}
|
||||
/>
|
||||
</InputSpinner>
|
||||
</FormikFieldWrapper>
|
||||
</Form>
|
||||
{servers.length > 0 &&
|
||||
<div className={'mt-6'}>
|
||||
{
|
||||
servers.map(server => (
|
||||
<Link
|
||||
key={server.uuid}
|
||||
to={`/server/${server.id}`}
|
||||
className={'flex items-center block bg-neutral-900 p-4 rounded border-l-4 border-neutral-900 no-underline hover:shadow hover:border-cyan-500 transition-colors duration-250'}
|
||||
onClick={() => props.onDismissed()}
|
||||
>
|
||||
<div>
|
||||
<p className={'text-sm'}>{server.name}</p>
|
||||
<p className={'mt-1 text-xs text-neutral-400'}>
|
||||
{
|
||||
server.allocations.filter(alloc => alloc.default).map(allocation => (
|
||||
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || allocation.ip}:{allocation.port}</span>
|
||||
))
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div className={'flex-1 text-right'}>
|
||||
<span className={'text-xs py-1 px-2 bg-cyan-800 text-cyan-100 rounded'}>
|
||||
{server.node}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</Modal>
|
||||
</Formik>
|
||||
);
|
||||
};
|
26
resources/scripts/components/elements/Can.tsx
Normal file
26
resources/scripts/components/elements/Can.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import { usePermissions } from '@/plugins/usePermissions';
|
||||
|
||||
interface Props {
|
||||
action: string | string[];
|
||||
matchAny?: boolean;
|
||||
renderOnError?: React.ReactNode | null;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Can = ({ action, matchAny = false, renderOnError, children }: Props) => {
|
||||
const can = usePermissions(action);
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
((matchAny && can.filter(p => p).length > 0) || (!matchAny && can.every(p => p))) ?
|
||||
children
|
||||
:
|
||||
renderOnError
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Can;
|
|
@ -1,25 +1,25 @@
|
|||
import React from 'react';
|
||||
import Modal from '@/components/elements/Modal';
|
||||
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
title: string;
|
||||
buttonText: string;
|
||||
children: string;
|
||||
visible: boolean;
|
||||
onConfirmed: () => void;
|
||||
onCanceled: () => void;
|
||||
}
|
||||
showSpinnerOverlay?: boolean;
|
||||
} & RequiredModalProps;
|
||||
|
||||
const ConfirmationModal = ({ title, children, visible, buttonText, onConfirmed, onCanceled }: Props) => (
|
||||
const ConfirmationModal = ({ title, appear, children, visible, buttonText, onConfirmed, showSpinnerOverlay, onDismissed }: Props) => (
|
||||
<Modal
|
||||
appear={true}
|
||||
appear={appear || true}
|
||||
visible={visible}
|
||||
onDismissed={() => onCanceled()}
|
||||
showSpinnerOverlay={showSpinnerOverlay}
|
||||
onDismissed={() => onDismissed()}
|
||||
>
|
||||
<h3 className={'mb-6'}>{title}</h3>
|
||||
<p className={'text-sm'}>{children}</p>
|
||||
<div className={'flex items-center justify-end mt-8'}>
|
||||
<button className={'btn btn-secondary btn-sm'} onClick={() => onCanceled()}>
|
||||
<button className={'btn btn-secondary btn-sm'} onClick={() => onDismissed()}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className={'btn btn-red btn-sm ml-4'} onClick={() => onConfirmed()}>
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
|
||||
type Props = Readonly<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||
title?: string;
|
||||
borderColor?: string;
|
||||
showFlashes?: string | boolean;
|
||||
showLoadingOverlay?: boolean;
|
||||
}>;
|
||||
|
||||
const ContentBox = ({ title, borderColor, showFlashes, children, ...props }: Props) => (
|
||||
const ContentBox = ({ title, borderColor, showFlashes, showLoadingOverlay, children, ...props }: Props) => (
|
||||
<div {...props}>
|
||||
{title && <h2 className={'text-neutral-300 mb-4 px-4'}>{title}</h2>}
|
||||
{showFlashes &&
|
||||
|
@ -20,6 +22,7 @@ const ContentBox = ({ title, borderColor, showFlashes, children, ...props }: Pro
|
|||
<div className={classNames('bg-neutral-700 p-4 rounded shadow-lg relative', borderColor, {
|
||||
'border-t-4': !!borderColor,
|
||||
})}>
|
||||
<SpinnerOverlay visible={showLoadingOverlay || false}/>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,6 +4,7 @@ import classNames from 'classnames';
|
|||
|
||||
interface OwnProps {
|
||||
name: string;
|
||||
light?: boolean;
|
||||
label?: string;
|
||||
description?: string;
|
||||
validate?: (value: any) => undefined | string | Promise<any>;
|
||||
|
@ -11,19 +12,19 @@ interface OwnProps {
|
|||
|
||||
type Props = OwnProps & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name'>;
|
||||
|
||||
const Field = ({ id, name, label, description, validate, className, ...props }: Props) => (
|
||||
const Field = ({ id, name, light = false, label, description, validate, className, ...props }: Props) => (
|
||||
<FormikField name={name} validate={validate}>
|
||||
{
|
||||
({ field, form: { errors, touched } }: FieldProps) => (
|
||||
<React.Fragment>
|
||||
{label &&
|
||||
<label htmlFor={id} className={'input-dark-label'}>{label}</label>
|
||||
<label htmlFor={id} className={light ? undefined : 'input-dark-label'}>{label}</label>
|
||||
}
|
||||
<input
|
||||
id={id}
|
||||
{...field}
|
||||
{...props}
|
||||
className={classNames((className || 'input-dark'), {
|
||||
className={classNames((className || (light ? 'input' : 'input-dark')), {
|
||||
error: touched[field.name] && errors[field.name],
|
||||
})}
|
||||
/>
|
||||
|
|
22
resources/scripts/components/elements/InputSpinner.tsx
Normal file
22
resources/scripts/components/elements/InputSpinner.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
const InputSpinner = ({ visible, children }: { visible: boolean, children: React.ReactNode }) => (
|
||||
<div className={'relative'}>
|
||||
<CSSTransition
|
||||
timeout={250}
|
||||
in={visible}
|
||||
unmountOnExit={true}
|
||||
appear={true}
|
||||
classNames={'fade'}
|
||||
>
|
||||
<div className={'absolute pin-r h-full flex items-center justify-end pr-3'}>
|
||||
<Spinner size={'tiny'}/>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default InputSpinner;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue