Merge branch 'develop' into issues/1902
This commit is contained in:
commit
c9ad4ea811
57 changed files with 602 additions and 1101 deletions
|
@ -148,8 +148,8 @@ class NodeViewController extends Controller
|
||||||
public function servers(Request $request, Node $node)
|
public function servers(Request $request, Node $node)
|
||||||
{
|
{
|
||||||
$this->plainInject([
|
$this->plainInject([
|
||||||
'node' => Collection::wrap($node->makeVisible('daemonSecret'))
|
'node' => Collection::wrap($node->makeVisible(['daemon_token_id', 'daemon_token']))
|
||||||
->only(['scheme', 'fqdn', 'daemonListen', 'daemonSecret']),
|
->only(['scheme', 'fqdn', 'daemonListen', 'daemon_token_id', 'daemon_token']),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $this->view->make('admin.nodes.view.servers', [
|
return $this->view->make('admin.nodes.view.servers', [
|
||||||
|
|
|
@ -67,7 +67,7 @@ class StatisticsController extends Controller
|
||||||
|
|
||||||
$tokens = [];
|
$tokens = [];
|
||||||
foreach ($nodes as $node) {
|
foreach ($nodes as $node) {
|
||||||
$tokens[$node->id] = $node->daemonSecret;
|
$tokens[$node->id] = decrypt($node->daemon_token);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->injectJavascript([
|
$this->injectJavascript([
|
||||||
|
|
|
@ -145,7 +145,7 @@ class ServerTransferController extends Controller
|
||||||
->canOnlyBeUsedAfter($now->getTimestamp())
|
->canOnlyBeUsedAfter($now->getTimestamp())
|
||||||
->expiresAt($now->addMinutes(15)->getTimestamp())
|
->expiresAt($now->addMinutes(15)->getTimestamp())
|
||||||
->relatedTo($server->uuid, true)
|
->relatedTo($server->uuid, true)
|
||||||
->getToken($signer, new Key($server->node->daemonSecret));
|
->getToken($signer, new Key($server->node->getDecryptedKey()));
|
||||||
|
|
||||||
// On the daemon transfer repository, make sure to set the node after the server
|
// 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
|
// because setServer() tells the repository to use the server's node and not the one
|
||||||
|
|
|
@ -1,107 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Pterodactyl\Http\Controllers\Daemon;
|
|
||||||
|
|
||||||
use Cache;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Pterodactyl\Models\Node;
|
|
||||||
use Illuminate\Http\Response;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Pterodactyl\Http\Controllers\Controller;
|
|
||||||
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
|
||||||
use Pterodactyl\Events\Server\Installed as ServerInstalled;
|
|
||||||
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
|
|
||||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
|
||||||
|
|
||||||
class ActionController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var \Illuminate\Contracts\Events\Dispatcher
|
|
||||||
*/
|
|
||||||
private $eventDispatcher;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
|
|
||||||
*/
|
|
||||||
private $repository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ActionController constructor.
|
|
||||||
*
|
|
||||||
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
|
|
||||||
* @param \Illuminate\Contracts\Events\Dispatcher $eventDispatcher
|
|
||||||
*/
|
|
||||||
public function __construct(ServerRepository $repository, EventDispatcher $eventDispatcher)
|
|
||||||
{
|
|
||||||
$this->eventDispatcher = $eventDispatcher;
|
|
||||||
$this->repository = $repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles install toggle request from daemon.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
*
|
|
||||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
|
||||||
*/
|
|
||||||
public function markInstall(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
/** @var \Pterodactyl\Models\Server $server */
|
|
||||||
$server = $this->repository->findFirstWhere([
|
|
||||||
'uuid' => $request->input('server'),
|
|
||||||
]);
|
|
||||||
} catch (RecordNotFoundException $exception) {
|
|
||||||
return JsonResponse::create([
|
|
||||||
'error' => 'No server by that ID was found on the system.',
|
|
||||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $server->relationLoaded('node')) {
|
|
||||||
$server->load('node');
|
|
||||||
}
|
|
||||||
|
|
||||||
$hmac = $request->input('signed');
|
|
||||||
$status = $request->input('installed');
|
|
||||||
|
|
||||||
if (! hash_equals(base64_decode($hmac), hash_hmac('sha256', $server->uuid, $server->getRelation('node')->daemonSecret, true))) {
|
|
||||||
return JsonResponse::create([
|
|
||||||
'error' => 'Signed HMAC was invalid.',
|
|
||||||
], Response::HTTP_FORBIDDEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->repository->update($server->id, [
|
|
||||||
'installed' => ($status === 'installed') ? 1 : 2,
|
|
||||||
], true, true);
|
|
||||||
|
|
||||||
// Only fire event if server installed successfully.
|
|
||||||
if ($status === 'installed') {
|
|
||||||
$this->eventDispatcher->dispatch(new ServerInstalled($server));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't use a 204 here, the daemon is hard-checking for a 200 code.
|
|
||||||
return JsonResponse::create([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles configuration data request from daemon.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param string $token
|
|
||||||
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response
|
|
||||||
*/
|
|
||||||
public function configuration(Request $request, $token)
|
|
||||||
{
|
|
||||||
$nodeId = Cache::pull('Node:Configuration:' . $token);
|
|
||||||
if (is_null($nodeId)) {
|
|
||||||
return response()->json(['error' => 'token_invalid'], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$node = Node::findOrFail($nodeId);
|
|
||||||
|
|
||||||
// Manually as getConfigurationAsJson() returns it in correct format already
|
|
||||||
return $node->getJsonConfiguration();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,73 +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\Http\Controllers\Daemon;
|
|
||||||
|
|
||||||
use Storage;
|
|
||||||
use Pterodactyl\Models;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Pterodactyl\Http\Controllers\Controller;
|
|
||||||
|
|
||||||
class PackController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Pulls an install pack archive from the system.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param string $uuid
|
|
||||||
* @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse
|
|
||||||
*/
|
|
||||||
public function pull(Request $request, $uuid)
|
|
||||||
{
|
|
||||||
$pack = Models\Pack::where('uuid', $uuid)->first();
|
|
||||||
|
|
||||||
if (! $pack) {
|
|
||||||
return response()->json(['error' => 'No such pack.'], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! Storage::exists('packs/' . $pack->uuid . '/archive.tar.gz')) {
|
|
||||||
return response()->json(['error' => 'There is no archive available for this pack.'], 503);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->download(storage_path('app/packs/' . $pack->uuid . '/archive.tar.gz'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the hash information for a pack.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param string $uuid
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
*/
|
|
||||||
public function hash(Request $request, $uuid)
|
|
||||||
{
|
|
||||||
$pack = Models\Pack::where('uuid', $uuid)->first();
|
|
||||||
|
|
||||||
if (! $pack) {
|
|
||||||
return response()->json(['error' => 'No such pack.'], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! Storage::exists('packs/' . $pack->uuid . '/archive.tar.gz')) {
|
|
||||||
return response()->json(['error' => 'There is no archive available for this pack.'], 503);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'archive.tar.gz' => sha1_file(storage_path('app/packs/' . $pack->uuid . '/archive.tar.gz')),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pulls an update pack archive from the system.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
*/
|
|
||||||
public function pullUpdate(Request $request)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -38,7 +38,6 @@ use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
|
||||||
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
|
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
|
||||||
use Pterodactyl\Http\Middleware\Api\Client\SubstituteClientApiBindings;
|
use Pterodactyl\Http\Middleware\Api\Client\SubstituteClientApiBindings;
|
||||||
use Pterodactyl\Http\Middleware\Api\Application\AuthenticateApplicationUser;
|
use Pterodactyl\Http\Middleware\Api\Application\AuthenticateApplicationUser;
|
||||||
use Pterodactyl\Http\Middleware\DaemonAuthenticate as OldDaemonAuthenticate;
|
|
||||||
|
|
||||||
class Kernel extends HttpKernel
|
class Kernel extends HttpKernel
|
||||||
{
|
{
|
||||||
|
@ -107,7 +106,6 @@ class Kernel extends HttpKernel
|
||||||
'server' => AccessingValidServer::class,
|
'server' => AccessingValidServer::class,
|
||||||
'subuser.auth' => AuthenticateAsSubuser::class,
|
'subuser.auth' => AuthenticateAsSubuser::class,
|
||||||
'admin' => AdminAuthenticate::class,
|
'admin' => AdminAuthenticate::class,
|
||||||
'daemon-old' => OldDaemonAuthenticate::class,
|
|
||||||
'csrf' => VerifyCsrfToken::class,
|
'csrf' => VerifyCsrfToken::class,
|
||||||
'throttle' => ThrottleRequests::class,
|
'throttle' => ThrottleRequests::class,
|
||||||
'can' => Authorize::class,
|
'can' => Authorize::class,
|
||||||
|
|
|
@ -4,6 +4,7 @@ namespace Pterodactyl\Http\Middleware\Api\Daemon;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Contracts\Encryption\Encrypter;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
|
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
|
||||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
||||||
|
@ -25,14 +26,21 @@ class DaemonAuthenticate
|
||||||
'daemon.configuration',
|
'daemon.configuration',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Illuminate\Contracts\Encryption\Encrypter
|
||||||
|
*/
|
||||||
|
private $encrypter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DaemonAuthenticate constructor.
|
* DaemonAuthenticate constructor.
|
||||||
*
|
*
|
||||||
|
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
|
||||||
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
|
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
|
||||||
*/
|
*/
|
||||||
public function __construct(NodeRepositoryInterface $repository)
|
public function __construct(Encrypter $encrypter, NodeRepositoryInterface $repository)
|
||||||
{
|
{
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
|
$this->encrypter = $encrypter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,20 +58,31 @@ class DaemonAuthenticate
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
$token = $request->bearerToken();
|
if (is_null($bearer = $request->bearerToken())) {
|
||||||
|
throw new HttpException(
|
||||||
if (is_null($token)) {
|
401, 'Access this this endpoint must include an Authorization header.', null, ['WWW-Authenticate' => 'Bearer']
|
||||||
throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[$identifier, $token] = explode('.', $bearer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$node = $this->repository->findFirstWhere([['daemonSecret', '=', $token]]);
|
/** @var \Pterodactyl\Models\Node $node */
|
||||||
} catch (RecordNotFoundException $exception) {
|
$node = $this->repository->findFirstWhere([
|
||||||
throw new AccessDeniedHttpException;
|
'daemon_token_id' => $identifier,
|
||||||
}
|
]);
|
||||||
|
|
||||||
|
if (hash_equals((string) $this->encrypter->decrypt($node->daemon_token), $token)) {
|
||||||
$request->attributes->set('node', $node);
|
$request->attributes->set('node', $node);
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
} catch (RecordNotFoundException $exception) {
|
||||||
|
// Do nothing, we don't want to expose a node not existing at all.
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AccessDeniedHttpException(
|
||||||
|
'You are not authorized to access this resource.'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,69 +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\Http\Middleware;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
|
|
||||||
class DaemonAuthenticate
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* An array of route names to not apply this middleware to.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
private $except = [
|
|
||||||
'daemon.configuration',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
|
|
||||||
*/
|
|
||||||
private $repository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new filter instance.
|
|
||||||
*
|
|
||||||
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
public function __construct(NodeRepositoryInterface $repository)
|
|
||||||
{
|
|
||||||
$this->repository = $repository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle an incoming request.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param \Closure $next
|
|
||||||
* @return mixed
|
|
||||||
*
|
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
|
||||||
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
|
|
||||||
*/
|
|
||||||
public function handle(Request $request, Closure $next)
|
|
||||||
{
|
|
||||||
if (in_array($request->route()->getName(), $this->except)) {
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $request->header('X-Access-Node')) {
|
|
||||||
throw new AccessDeniedHttpException;
|
|
||||||
}
|
|
||||||
|
|
||||||
$node = $this->repository->findFirstWhere(['daemonSecret' => $request->header('X-Access-Node')]);
|
|
||||||
$request->attributes->set('node', $node);
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,11 +3,14 @@
|
||||||
namespace Pterodactyl\Models;
|
namespace Pterodactyl\Models;
|
||||||
|
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
use Illuminate\Container\Container;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Pterodactyl\Models\Traits\Searchable;
|
use Pterodactyl\Models\Traits\Searchable;
|
||||||
|
use Illuminate\Contracts\Encryption\Encrypter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
|
* @property string $uuid
|
||||||
* @property bool $public
|
* @property bool $public
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property string $description
|
* @property string $description
|
||||||
|
@ -21,7 +24,8 @@ use Pterodactyl\Models\Traits\Searchable;
|
||||||
* @property int $disk
|
* @property int $disk
|
||||||
* @property int $disk_overallocate
|
* @property int $disk_overallocate
|
||||||
* @property int $upload_size
|
* @property int $upload_size
|
||||||
* @property string $daemonSecret
|
* @property string $daemon_token_id
|
||||||
|
* @property string $daemon_token
|
||||||
* @property int $daemonListen
|
* @property int $daemonListen
|
||||||
* @property int $daemonSFTP
|
* @property int $daemonSFTP
|
||||||
* @property string $daemonBase
|
* @property string $daemonBase
|
||||||
|
@ -43,7 +47,8 @@ class Node extends Model
|
||||||
*/
|
*/
|
||||||
const RESOURCE_NAME = 'node';
|
const RESOURCE_NAME = 'node';
|
||||||
|
|
||||||
const DAEMON_SECRET_LENGTH = 36;
|
const DAEMON_TOKEN_ID_LENGTH = 16;
|
||||||
|
const DAEMON_TOKEN_LENGTH = 64;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The table associated with the model.
|
* The table associated with the model.
|
||||||
|
@ -57,7 +62,7 @@ class Node extends Model
|
||||||
*
|
*
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $hidden = ['daemonSecret'];
|
protected $hidden = ['daemon_token_id', 'daemon_token'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cast values to correct type.
|
* Cast values to correct type.
|
||||||
|
@ -84,8 +89,7 @@ class Node extends Model
|
||||||
'public', 'name', 'location_id',
|
'public', 'name', 'location_id',
|
||||||
'fqdn', 'scheme', 'behind_proxy',
|
'fqdn', 'scheme', 'behind_proxy',
|
||||||
'memory', 'memory_overallocate', 'disk',
|
'memory', 'memory_overallocate', 'disk',
|
||||||
'disk_overallocate', 'upload_size',
|
'disk_overallocate', 'upload_size', 'daemonBase',
|
||||||
'daemonSecret', 'daemonBase',
|
|
||||||
'daemonSFTP', 'daemonListen',
|
'daemonSFTP', 'daemonListen',
|
||||||
'description', 'maintenance_mode',
|
'description', 'maintenance_mode',
|
||||||
];
|
];
|
||||||
|
@ -153,12 +157,15 @@ class Node extends Model
|
||||||
/**
|
/**
|
||||||
* Returns the configuration as an array.
|
* Returns the configuration as an array.
|
||||||
*
|
*
|
||||||
* @return string
|
* @return array
|
||||||
*/
|
*/
|
||||||
private function getConfiguration()
|
public function getConfiguration()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'debug' => false,
|
'debug' => false,
|
||||||
|
'uuid' => $this->uuid,
|
||||||
|
'token_id' => $this->daemon_token_id,
|
||||||
|
'token' => Container::getInstance()->make(Encrypter::class)->decrypt($this->daemon_token),
|
||||||
'api' => [
|
'api' => [
|
||||||
'host' => '0.0.0.0',
|
'host' => '0.0.0.0',
|
||||||
'port' => $this->daemonListen,
|
'port' => $this->daemonListen,
|
||||||
|
@ -202,7 +209,6 @@ class Node extends Model
|
||||||
'check_interval' => 100,
|
'check_interval' => 100,
|
||||||
],
|
],
|
||||||
'remote' => route('index'),
|
'remote' => route('index'),
|
||||||
'token' => $this->daemonSecret,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,19 +217,34 @@ class Node extends Model
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getYamlConfiguration() {
|
public function getYamlConfiguration()
|
||||||
|
{
|
||||||
return Yaml::dump($this->getConfiguration(), 4, 2);
|
return Yaml::dump($this->getConfiguration(), 4, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the configuration in JSON format.
|
* Returns the configuration in JSON format.
|
||||||
*
|
*
|
||||||
|
* @param bool $pretty
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getJsonConfiguration(bool $pretty = false) {
|
public function getJsonConfiguration(bool $pretty = false)
|
||||||
|
{
|
||||||
return json_encode($this->getConfiguration(), $pretty ? JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT : JSON_UNESCAPED_SLASHES);
|
return json_encode($this->getConfiguration(), $pretty ? JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT : JSON_UNESCAPED_SLASHES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to return the decrypted key for a node.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getDecryptedKey(): string
|
||||||
|
{
|
||||||
|
return (string) Container::getInstance()->make(Encrypter::class)->decrypt(
|
||||||
|
$this->daemon_token
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the location associated with a node.
|
* Gets the location associated with a node.
|
||||||
*
|
*
|
||||||
|
|
|
@ -49,9 +49,5 @@ class RouteServiceProvider extends ServiceProvider
|
||||||
Route::middleware(['daemon'])->prefix('/api/remote')
|
Route::middleware(['daemon'])->prefix('/api/remote')
|
||||||
->namespace($this->namespace . '\Api\Remote')
|
->namespace($this->namespace . '\Api\Remote')
|
||||||
->group(base_path('routes/api-remote.php'));
|
->group(base_path('routes/api-remote.php'));
|
||||||
|
|
||||||
Route::middleware(['web', 'daemon-old'])->prefix('/daemon')
|
|
||||||
->namespace($this->namespace . '\Daemon')
|
|
||||||
->group(base_path('routes/daemon.php'));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,154 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Pterodactyl\Repositories\Daemon;
|
|
||||||
|
|
||||||
use RuntimeException;
|
|
||||||
use GuzzleHttp\Client;
|
|
||||||
use Pterodactyl\Models\Node;
|
|
||||||
use Pterodactyl\Models\Server;
|
|
||||||
use Illuminate\Foundation\Application;
|
|
||||||
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
|
|
||||||
use Pterodactyl\Contracts\Repository\Daemon\BaseRepositoryInterface;
|
|
||||||
|
|
||||||
abstract class BaseRepository implements BaseRepositoryInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var \Illuminate\Foundation\Application
|
|
||||||
*/
|
|
||||||
private $app;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Models\Server
|
|
||||||
*/
|
|
||||||
private $server;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string|null
|
|
||||||
*/
|
|
||||||
private $token;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Models\Node|null
|
|
||||||
*/
|
|
||||||
private $node;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
|
|
||||||
*/
|
|
||||||
private $nodeRepository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BaseRepository constructor.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Foundation\Application $app
|
|
||||||
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $nodeRepository
|
|
||||||
*/
|
|
||||||
public function __construct(Application $app, NodeRepositoryInterface $nodeRepository)
|
|
||||||
{
|
|
||||||
$this->app = $app;
|
|
||||||
$this->nodeRepository = $nodeRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the node model to be used for this daemon connection.
|
|
||||||
*
|
|
||||||
* @param \Pterodactyl\Models\Node $node
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setNode(Node $node)
|
|
||||||
{
|
|
||||||
$this->node = $node;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the node model being used.
|
|
||||||
*
|
|
||||||
* @return \Pterodactyl\Models\Node|null
|
|
||||||
*/
|
|
||||||
public function getNode()
|
|
||||||
{
|
|
||||||
return $this->node;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the Server model to use when requesting information from the Daemon.
|
|
||||||
*
|
|
||||||
* @param \Pterodactyl\Models\Server $server
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setServer(Server $server)
|
|
||||||
{
|
|
||||||
$this->server = $server;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the Server model.
|
|
||||||
*
|
|
||||||
* @return \Pterodactyl\Models\Server|null
|
|
||||||
*/
|
|
||||||
public function getServer()
|
|
||||||
{
|
|
||||||
return $this->server;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the token to be used in the X-Access-Token header for requests to the daemon.
|
|
||||||
*
|
|
||||||
* @param string $token
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setToken(string $token)
|
|
||||||
{
|
|
||||||
$this->token = $token;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the access token being used for requests.
|
|
||||||
*
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public function getToken()
|
|
||||||
{
|
|
||||||
return $this->token;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an instance of the Guzzle HTTP Client to be used for requests.
|
|
||||||
*
|
|
||||||
* @param array $headers
|
|
||||||
* @return \GuzzleHttp\Client
|
|
||||||
*/
|
|
||||||
public function getHttpClient(array $headers = []): Client
|
|
||||||
{
|
|
||||||
// If no node is set, load the relationship onto the Server model
|
|
||||||
// and pass that to the setNode function.
|
|
||||||
if (! $this->getNode() instanceof Node) {
|
|
||||||
if (! $this->getServer() instanceof Server) {
|
|
||||||
throw new RuntimeException('An instance of ' . Node::class . ' or ' . Server::class . ' must be set on this repository in order to return a client.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->getServer()->loadMissing('node');
|
|
||||||
$this->setNode($this->getServer()->getRelation('node'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->getServer() instanceof Server) {
|
|
||||||
$headers['X-Access-Server'] = $this->getServer()->uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
$headers['X-Access-Token'] = $this->getToken() ?? $this->getNode()->daemonSecret;
|
|
||||||
|
|
||||||
return new Client([
|
|
||||||
'verify' => config('app.env') === 'production',
|
|
||||||
'base_uri' => sprintf('%s://%s:%s/v1/', $this->getNode()->scheme, $this->getNode()->fqdn, $this->getNode()->daemonListen),
|
|
||||||
'timeout' => config('pterodactyl.guzzle.timeout'),
|
|
||||||
'connect_timeout' => config('pterodactyl.guzzle.connect_timeout'),
|
|
||||||
'headers' => $headers,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Pterodactyl\Repositories\Daemon;
|
|
||||||
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
|
||||||
use Pterodactyl\Contracts\Repository\Daemon\CommandRepositoryInterface;
|
|
||||||
|
|
||||||
class CommandRepository extends BaseRepository implements CommandRepositoryInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Send a command to a server.
|
|
||||||
*
|
|
||||||
* @param string $command
|
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
|
||||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
||||||
*/
|
|
||||||
public function send(string $command): ResponseInterface
|
|
||||||
{
|
|
||||||
return $this->getHttpClient()->request('POST', 'server/command', [
|
|
||||||
'json' => [
|
|
||||||
'command' => $command,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Pterodactyl\Repositories\Daemon;
|
|
||||||
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
|
||||||
use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface;
|
|
||||||
|
|
||||||
class ConfigurationRepository extends BaseRepository implements ConfigurationRepositoryInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Update the configuration details for the specified node using data from the database.
|
|
||||||
*
|
|
||||||
* @param array $overrides
|
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
|
||||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
||||||
*/
|
|
||||||
public function update(array $overrides = []): ResponseInterface
|
|
||||||
{
|
|
||||||
$node = $this->getNode();
|
|
||||||
$structure = [
|
|
||||||
'web' => [
|
|
||||||
'listen' => $node->daemonListen,
|
|
||||||
'ssl' => [
|
|
||||||
'enabled' => (! $node->behind_proxy && $node->scheme === 'https'),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'sftp' => [
|
|
||||||
'path' => $node->daemonBase,
|
|
||||||
'port' => $node->daemonSFTP,
|
|
||||||
],
|
|
||||||
'remote' => [
|
|
||||||
'base' => config('app.url'),
|
|
||||||
],
|
|
||||||
'uploads' => [
|
|
||||||
'size_limit' => $node->upload_size,
|
|
||||||
],
|
|
||||||
'keys' => [
|
|
||||||
$node->daemonSecret,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
return $this->getHttpClient()->request('PATCH', 'config', [
|
|
||||||
'json' => array_merge($structure, $overrides),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Pterodactyl\Repositories\Daemon;
|
|
||||||
|
|
||||||
use stdClass;
|
|
||||||
use RuntimeException;
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
|
||||||
use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface;
|
|
||||||
|
|
||||||
class FileRepository extends BaseRepository implements FileRepositoryInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Return stat information for a given file.
|
|
||||||
*
|
|
||||||
* @param string $path
|
|
||||||
* @return \stdClass
|
|
||||||
*
|
|
||||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
||||||
*/
|
|
||||||
public function getFileStat(string $path): stdClass
|
|
||||||
{
|
|
||||||
$file = str_replace('\\', '/', pathinfo($path));
|
|
||||||
$file['dirname'] = in_array($file['dirname'], ['.', './', '/']) ? null : trim($file['dirname'], '/') . '/';
|
|
||||||
|
|
||||||
$response = $this->getHttpClient()->request('GET', sprintf(
|
|
||||||
'server/file/stat/%s',
|
|
||||||
rawurlencode($file['dirname'] . $file['basename'])
|
|
||||||
));
|
|
||||||
|
|
||||||
return json_decode($response->getBody());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the contents of a given file if it can be edited in the Panel.
|
|
||||||
*
|
|
||||||
* @param string $path
|
|
||||||
* @return string
|
|
||||||
*
|
|
||||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
||||||
*/
|
|
||||||
public function getContent(string $path): string
|
|
||||||
{
|
|
||||||
$file = str_replace('\\', '/', pathinfo($path));
|
|
||||||
$file['dirname'] = in_array($file['dirname'], ['.', './', '/']) ? null : trim($file['dirname'], '/') . '/';
|
|
||||||
|
|
||||||
$response = $this->getHttpClient()->request('GET', sprintf(
|
|
||||||
'server/file/f/%s',
|
|
||||||
rawurlencode($file['dirname'] . $file['basename'])
|
|
||||||
));
|
|
||||||
|
|
||||||
return object_get(json_decode($response->getBody()), 'content');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save new contents to a given file.
|
|
||||||
*
|
|
||||||
* @param string $path
|
|
||||||
* @param string $content
|
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
|
||||||
*
|
|
||||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
||||||
*/
|
|
||||||
public function putContent(string $path, string $content): ResponseInterface
|
|
||||||
{
|
|
||||||
$file = str_replace('\\', '/', pathinfo($path));
|
|
||||||
$file['dirname'] = in_array($file['dirname'], ['.', './', '/']) ? null : trim($file['dirname'], '/') . '/';
|
|
||||||
|
|
||||||
return $this->getHttpClient()->request('POST', 'server/file/save', [
|
|
||||||
'json' => [
|
|
||||||
'path' => rawurlencode($file['dirname'] . $file['basename']),
|
|
||||||
'content' => $content,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a directory listing for a given path.
|
|
||||||
*
|
|
||||||
* @param string $path
|
|
||||||
* @return array
|
|
||||||
*
|
|
||||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
||||||
*/
|
|
||||||
public function getDirectory(string $path): array
|
|
||||||
{
|
|
||||||
$response = $this->getHttpClient()->request('GET', sprintf('server/directory/%s', rawurlencode($path)));
|
|
||||||
|
|
||||||
return json_decode($response->getBody());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new directory for the server in the given $path.
|
|
||||||
*
|
|
||||||
* @param string $name
|
|
||||||
* @param string $path
|
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
|
||||||
*
|
|
||||||
* @throws \RuntimeException
|
|
||||||
*/
|
|
||||||
public function createDirectory(string $name, string $path): ResponseInterface
|
|
||||||
{
|
|
||||||
throw new RuntimeException('Not implemented.');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Pterodactyl\Repositories\Daemon;
|
|
||||||
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
|
||||||
use Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface;
|
|
||||||
use Pterodactyl\Exceptions\Repository\Daemon\InvalidPowerSignalException;
|
|
||||||
|
|
||||||
class PowerRepository extends BaseRepository implements PowerRepositoryInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Send a power signal to a server.
|
|
||||||
*
|
|
||||||
* @param string $signal
|
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
|
||||||
*
|
|
||||||
* @throws InvalidPowerSignalException
|
|
||||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
||||||
*/
|
|
||||||
public function sendSignal(string $signal): ResponseInterface
|
|
||||||
{
|
|
||||||
switch ($signal) {
|
|
||||||
case self::SIGNAL_START:
|
|
||||||
case self::SIGNAL_STOP:
|
|
||||||
case self::SIGNAL_RESTART:
|
|
||||||
case self::SIGNAL_KILL:
|
|
||||||
return $this->getHttpClient()->request('PUT', 'server/power', [
|
|
||||||
'json' => [
|
|
||||||
'action' => $signal,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
default:
|
|
||||||
throw new InvalidPowerSignalException('The signal "' . $signal . '" is not defined and could not be processed.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,134 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Pterodactyl\Repositories\Daemon;
|
|
||||||
|
|
||||||
use Webmozart\Assert\Assert;
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
|
||||||
use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface;
|
|
||||||
|
|
||||||
class ServerRepository extends BaseRepository implements ServerRepositoryInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Create a new server on the daemon for the panel.
|
|
||||||
*
|
|
||||||
* @param array $structure
|
|
||||||
* @param array $overrides
|
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
|
||||||
*
|
|
||||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
||||||
*/
|
|
||||||
public function create(array $structure, array $overrides = []): ResponseInterface
|
|
||||||
{
|
|
||||||
foreach ($overrides as $key => $value) {
|
|
||||||
$structure[$key] = value($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->getHttpClient()->request('POST', 'servers', [
|
|
||||||
'json' => $structure,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update server details on the daemon.
|
|
||||||
*
|
|
||||||
* @param array $data
|
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
|
||||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
||||||
*/
|
|
||||||
public function update(array $data): ResponseInterface
|
|
||||||
{
|
|
||||||
return $this->getHttpClient()->request('PATCH', 'server', [
|
|
||||||
'json' => $data,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark a server to be reinstalled on the system.
|
|
||||||
*
|
|
||||||
* @param array|null $data
|
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
|
||||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
||||||
*/
|
|
||||||
public function reinstall(array $data = null): ResponseInterface
|
|
||||||
{
|
|
||||||
return $this->getHttpClient()->request('POST', 'server/reinstall', [
|
|
||||||
'json' => $data ?? [],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark a server as needing a container rebuild the next time the server is booted.
|
|
||||||
*
|
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
|
||||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
||||||
*/
|
|
||||||
public function rebuild(): ResponseInterface
|
|
||||||
{
|
|
||||||
return $this->getHttpClient()->request('POST', 'server/rebuild');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Suspend a server on the daemon.
|
|
||||||
*
|
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
|
||||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
||||||
*/
|
|
||||||
public function suspend(): ResponseInterface
|
|
||||||
{
|
|
||||||
return $this->getHttpClient()->request('POST', 'server/suspend');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Un-suspend a server on the daemon.
|
|
||||||
*
|
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
|
||||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
||||||
*/
|
|
||||||
public function unsuspend(): ResponseInterface
|
|
||||||
{
|
|
||||||
return $this->getHttpClient()->request('POST', 'server/unsuspend');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a server on the daemon.
|
|
||||||
*
|
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
|
||||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
||||||
*/
|
|
||||||
public function delete(): ResponseInterface
|
|
||||||
{
|
|
||||||
return $this->getHttpClient()->request('DELETE', 'servers');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return details on a specific server.
|
|
||||||
*
|
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
|
||||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
||||||
*/
|
|
||||||
public function details(): ResponseInterface
|
|
||||||
{
|
|
||||||
return $this->getHttpClient()->request('GET', 'server');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Revoke an access key on the daemon before the time is expired.
|
|
||||||
*
|
|
||||||
* @param string|array $key
|
|
||||||
* @return \Psr\Http\Message\ResponseInterface
|
|
||||||
*
|
|
||||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
||||||
*/
|
|
||||||
public function revokeAccessKey($key): ResponseInterface
|
|
||||||
{
|
|
||||||
if (is_array($key)) {
|
|
||||||
return $this->getHttpClient()->request('POST', 'keys/batch-delete', [
|
|
||||||
'json' => ['keys' => $key],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Assert::stringNotEmpty($key, 'First argument passed to revokeAccessKey must be a non-empty string or array, received %s.');
|
|
||||||
|
|
||||||
return $this->getHttpClient()->request('DELETE', 'keys/' . $key);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -183,7 +183,7 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa
|
||||||
public function getNodeWithResourceUsage(int $node_id): Node
|
public function getNodeWithResourceUsage(int $node_id): Node
|
||||||
{
|
{
|
||||||
$instance = $this->getBuilder()
|
$instance = $this->getBuilder()
|
||||||
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemonSecret', 'nodes.daemonListen', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate'])
|
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', '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')
|
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
|
||||||
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
|
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
|
||||||
->where('nodes.id', $node_id);
|
->where('nodes.id', $node_id);
|
||||||
|
|
|
@ -23,4 +23,22 @@ class DaemonConfigurationRepository extends DaemonRepository
|
||||||
|
|
||||||
return json_decode($response->getBody()->__toString(), true);
|
return json_decode($response->getBody()->__toString(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the configuration information for a daemon.
|
||||||
|
*
|
||||||
|
* @param array $attributes
|
||||||
|
* @return \Psr\Http\Message\ResponseInterface
|
||||||
|
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||||
|
*/
|
||||||
|
public function update(array $attributes = [])
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->getHttpClient()->post(
|
||||||
|
'/api/update', array_merge($this->node->getConfiguration(), $attributes)
|
||||||
|
);
|
||||||
|
} catch (TransferException $exception) {
|
||||||
|
throw new DaemonConnectionException($exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,7 @@ abstract class DaemonRepository
|
||||||
'timeout' => config('pterodactyl.guzzle.timeout'),
|
'timeout' => config('pterodactyl.guzzle.timeout'),
|
||||||
'connect_timeout' => config('pterodactyl.guzzle.connect_timeout'),
|
'connect_timeout' => config('pterodactyl.guzzle.connect_timeout'),
|
||||||
'headers' => array_merge($headers, [
|
'headers' => array_merge($headers, [
|
||||||
'Authorization' => 'Bearer ' . $this->node->daemonSecret,
|
'Authorization' => 'Bearer ' . $this->node->getDecryptedKey(),
|
||||||
'Accept' => 'application/json',
|
'Accept' => 'application/json',
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
]),
|
]),
|
||||||
|
|
|
@ -1,33 +1,34 @@
|
||||||
<?php
|
<?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\Nodes;
|
namespace Pterodactyl\Services\Nodes;
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Pterodactyl\Models\Node;
|
||||||
|
use Illuminate\Encryption\Encrypter;
|
||||||
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
|
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
|
||||||
|
|
||||||
class NodeCreationService
|
class NodeCreationService
|
||||||
{
|
{
|
||||||
const DAEMON_SECRET_LENGTH = 36;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
|
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
|
||||||
*/
|
*/
|
||||||
protected $repository;
|
protected $repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Illuminate\Encryption\Encrypter
|
||||||
|
*/
|
||||||
|
private $encrypter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CreationService constructor.
|
* CreationService constructor.
|
||||||
*
|
*
|
||||||
|
* @param \Illuminate\Encryption\Encrypter $encrypter
|
||||||
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
|
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
|
||||||
*/
|
*/
|
||||||
public function __construct(NodeRepositoryInterface $repository)
|
public function __construct(Encrypter $encrypter, NodeRepositoryInterface $repository)
|
||||||
{
|
{
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
|
$this->encrypter = $encrypter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,8 +41,9 @@ class NodeCreationService
|
||||||
*/
|
*/
|
||||||
public function handle(array $data)
|
public function handle(array $data)
|
||||||
{
|
{
|
||||||
$data['daemonSecret'] = str_random(self::DAEMON_SECRET_LENGTH);
|
$data['daemon_token'] = Str::random(Node::DAEMON_TOKEN_LENGTH);
|
||||||
|
$data['daemon_token_id'] = $this->encrypter->encrypt(Str::random(Node::DAEMON_TOKEN_ID_LENGTH));
|
||||||
|
|
||||||
return $this->repository->create($data);
|
return $this->repository->create($data, true, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,6 @@ class NodeJWTService
|
||||||
|
|
||||||
return $builder
|
return $builder
|
||||||
->withClaim('unique_id', Str::random(16))
|
->withClaim('unique_id', Str::random(16))
|
||||||
->getToken($signer, new Key($node->daemonSecret));
|
->getToken($signer, new Key($node->getDecryptedKey()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,15 @@
|
||||||
|
|
||||||
namespace Pterodactyl\Services\Nodes;
|
namespace Pterodactyl\Services\Nodes;
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Pterodactyl\Models\Node;
|
use Pterodactyl\Models\Node;
|
||||||
use GuzzleHttp\Exception\ConnectException;
|
use GuzzleHttp\Exception\ConnectException;
|
||||||
use GuzzleHttp\Exception\RequestException;
|
use GuzzleHttp\Exception\RequestException;
|
||||||
use Illuminate\Database\ConnectionInterface;
|
use Illuminate\Database\ConnectionInterface;
|
||||||
|
use Illuminate\Contracts\Encryption\Encrypter;
|
||||||
use Pterodactyl\Repositories\Daemon\ConfigurationRepository;
|
use Pterodactyl\Repositories\Daemon\ConfigurationRepository;
|
||||||
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
|
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
|
||||||
|
use Pterodactyl\Repositories\Wings\DaemonConfigurationRepository;
|
||||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||||
use Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException;
|
use Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException;
|
||||||
|
|
||||||
|
@ -18,31 +21,39 @@ class NodeUpdateService
|
||||||
*/
|
*/
|
||||||
private $connection;
|
private $connection;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface
|
|
||||||
*/
|
|
||||||
private $configRepository;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
|
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
|
||||||
*/
|
*/
|
||||||
private $repository;
|
private $repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Pterodactyl\Repositories\Wings\DaemonConfigurationRepository
|
||||||
|
*/
|
||||||
|
private $configurationRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Illuminate\Contracts\Encryption\Encrypter
|
||||||
|
*/
|
||||||
|
private $encrypter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UpdateService constructor.
|
* UpdateService constructor.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Database\ConnectionInterface $connection
|
* @param \Illuminate\Database\ConnectionInterface $connection
|
||||||
* @param \Pterodactyl\Repositories\Daemon\ConfigurationRepository $configurationRepository
|
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
|
||||||
|
* @param \Pterodactyl\Repositories\Wings\DaemonConfigurationRepository $configurationRepository
|
||||||
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
|
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ConnectionInterface $connection,
|
ConnectionInterface $connection,
|
||||||
ConfigurationRepository $configurationRepository,
|
Encrypter $encrypter,
|
||||||
|
DaemonConfigurationRepository $configurationRepository,
|
||||||
NodeRepositoryInterface $repository
|
NodeRepositoryInterface $repository
|
||||||
) {
|
) {
|
||||||
$this->connection = $connection;
|
$this->connection = $connection;
|
||||||
$this->configRepository = $configurationRepository;
|
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
|
$this->configurationRepository = $configurationRepository;
|
||||||
|
$this->encrypter = $encrypter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -58,13 +69,14 @@ class NodeUpdateService
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||||
* @throws \Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException
|
* @throws \Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException
|
||||||
*
|
|
||||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
||||||
*/
|
*/
|
||||||
public function handle(Node $node, array $data, bool $resetToken = false)
|
public function handle(Node $node, array $data, bool $resetToken = false)
|
||||||
{
|
{
|
||||||
if ($resetToken) {
|
if ($resetToken) {
|
||||||
$data['daemonSecret'] = str_random(Node::DAEMON_SECRET_LENGTH);
|
$data['daemon_token'] = Str::random(Node::DAEMON_TOKEN_LENGTH);
|
||||||
|
$data['daemon_token_id'] = $this->encrypter->encrypt(
|
||||||
|
Str::random(Node::DAEMON_TOKEN_ID_LENGTH)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->connection->beginTransaction();
|
$this->connection->beginTransaction();
|
||||||
|
@ -77,14 +89,15 @@ class NodeUpdateService
|
||||||
// We need to clone the new model and set it's authentication token to be the
|
// We need to clone the new model and set it's authentication token to be the
|
||||||
// old one so we can connect. Then we will pass the new token through as an
|
// old one so we can connect. Then we will pass the new token through as an
|
||||||
// override on the call.
|
// override on the call.
|
||||||
$cloned = $updatedModel->replicate(['daemonSecret']);
|
$cloned = $updatedModel->replicate(['daemon_token']);
|
||||||
$cloned->setAttribute('daemonSecret', $node->getAttribute('daemonSecret'));
|
$cloned->setAttribute('daemon_token', $node->getAttribute('daemon_token'));
|
||||||
|
|
||||||
$this->configRepository->setNode($cloned)->update([
|
$this->configurationRepository->setNode($cloned)->update([
|
||||||
'keys' => [$data['daemonSecret']],
|
'daemon_token_id' => $updatedModel->daemon_token_id,
|
||||||
|
'daemon_token' => $updatedModel->getDecryptedKey(),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
$this->configRepository->setNode($updatedModel)->update();
|
$this->configurationRepository->setNode($updatedModel)->update();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->connection->commit();
|
$this->connection->commit();
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Faker\Generator as Faker;
|
use Faker\Generator as Faker;
|
||||||
|
use Pterodactyl\Models\Node;
|
||||||
use Pterodactyl\Models\ApiKey;
|
use Pterodactyl\Models\ApiKey;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -80,6 +83,7 @@ $factory->define(Pterodactyl\Models\Location::class, function (Faker $faker) {
|
||||||
$factory->define(Pterodactyl\Models\Node::class, function (Faker $faker) {
|
$factory->define(Pterodactyl\Models\Node::class, function (Faker $faker) {
|
||||||
return [
|
return [
|
||||||
'id' => $faker->unique()->randomNumber(),
|
'id' => $faker->unique()->randomNumber(),
|
||||||
|
'uuid' => Uuid::uuid4()->toString(),
|
||||||
'public' => true,
|
'public' => true,
|
||||||
'name' => $faker->firstName,
|
'name' => $faker->firstName,
|
||||||
'fqdn' => $faker->ipv4,
|
'fqdn' => $faker->ipv4,
|
||||||
|
@ -90,10 +94,11 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker $faker) {
|
||||||
'disk' => 10240,
|
'disk' => 10240,
|
||||||
'disk_overallocate' => 0,
|
'disk_overallocate' => 0,
|
||||||
'upload_size' => 100,
|
'upload_size' => 100,
|
||||||
'daemonSecret' => $faker->uuid,
|
'daemon_token_id' => Str::random(Node::DAEMON_TOKEN_ID_LENGTH),
|
||||||
|
'daemon_token' => Str::random(Node::DAEMON_TOKEN_LENGTH),
|
||||||
'daemonListen' => 8080,
|
'daemonListen' => 8080,
|
||||||
'daemonSFTP' => 2022,
|
'daemonSFTP' => 2022,
|
||||||
'daemonBase' => '/srv/daemon',
|
'daemonBase' => '/srv/daemon-data',
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -18,15 +18,21 @@ class MergePermissionsTableIntoSubusers extends Migration
|
||||||
$table->json('permissions')->nullable()->after('server_id');
|
$table->json('permissions')->nullable()->after('server_id');
|
||||||
});
|
});
|
||||||
|
|
||||||
DB::statement('
|
$cursor = DB::table('permissions')
|
||||||
UPDATE subusers as s
|
->select(['subuser_id'])
|
||||||
LEFT JOIN (
|
->selectRaw('GROUP_CONCAT(permission) as permissions')
|
||||||
SELECT subuser_id, JSON_ARRAYAGG(permission) as permissions
|
->from('permissions')
|
||||||
FROM permissions
|
->groupBy(['subuser_id'])
|
||||||
GROUP BY subuser_id
|
->cursor();
|
||||||
) as p ON p.subuser_id = s.id
|
|
||||||
SET s.permissions = p.permissions
|
DB::transaction(function () use (&$cursor) {
|
||||||
');
|
$cursor->each(function ($datum) {
|
||||||
|
DB::update('UPDATE subusers SET permissions = ? WHERE id = ?', [
|
||||||
|
json_encode(explode(',', $datum->permissions)),
|
||||||
|
$datum->subuser_id,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -13,11 +13,10 @@ class AddTableServerTransfers extends Migration
|
||||||
*/
|
*/
|
||||||
public function up()
|
public function up()
|
||||||
{
|
{
|
||||||
Schema::dropIfExists('server_transfers');
|
|
||||||
|
|
||||||
Schema::create('server_transfers', function (Blueprint $table) {
|
Schema::create('server_transfers', function (Blueprint $table) {
|
||||||
$table->increments('id');
|
$table->increments('id');
|
||||||
$table->integer('server_id')->unsigned();
|
$table->integer('server_id')->unsigned();
|
||||||
|
$table->tinyInteger('successful')->unsigned()->default(0);
|
||||||
$table->integer('old_node')->unsigned();
|
$table->integer('old_node')->unsigned();
|
||||||
$table->integer('new_node')->unsigned();
|
$table->integer('new_node')->unsigned();
|
||||||
$table->integer('old_allocation')->unsigned();
|
$table->integer('old_allocation')->unsigned();
|
||||||
|
@ -25,10 +24,8 @@ class AddTableServerTransfers extends Migration
|
||||||
$table->string('old_additional_allocations')->nullable();
|
$table->string('old_additional_allocations')->nullable();
|
||||||
$table->string('new_additional_allocations')->nullable();
|
$table->string('new_additional_allocations')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
|
||||||
|
|
||||||
Schema::table('server_transfers', function (Blueprint $table) {
|
$table->foreign('server_id')->references('id')->on('servers')->onDelete('cascade');
|
||||||
$table->foreign('server_id')->references('id')->on('servers');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
<?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');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Container\Container;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Contracts\Encryption\Encrypter;
|
||||||
|
|
||||||
|
class StoreNodeTokensAsEncryptedValue extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('nodes', function (Blueprint $table) {
|
||||||
|
$table->dropUnique(['daemonSecret']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('nodes', function (Blueprint $table) {
|
||||||
|
$table->char('uuid', 36)->after('id')->unique();
|
||||||
|
$table->char('daemon_token_id', 16)->after('upload_size')->unique();
|
||||||
|
$table->renameColumn('daemonSecret', 'daemon_token');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('nodes', function (Blueprint $table) {
|
||||||
|
$table->text('daemon_token')->change();
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::transaction(function () {
|
||||||
|
/** @var \Illuminate\Contracts\Encryption\Encrypter $encrypter */
|
||||||
|
$encrypter = Container::getInstance()->make(Encrypter::class);
|
||||||
|
|
||||||
|
foreach (DB::select('SELECT id, daemon_token FROM nodes') as $datum) {
|
||||||
|
DB::update('UPDATE nodes SET uuid = ?, daemon_token_id = ?, daemon_token = ? WHERE id = ?', [
|
||||||
|
Uuid::uuid4()->toString(),
|
||||||
|
substr($datum->daemon_token, 0, 16),
|
||||||
|
$encrypter->encrypt(substr($datum->daemon_token, 16)),
|
||||||
|
$datum->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
DB::transaction(function () {
|
||||||
|
/** @var \Illuminate\Contracts\Encryption\Encrypter $encrypter */
|
||||||
|
$encrypter = Container::getInstance()->make(Encrypter::class);
|
||||||
|
|
||||||
|
foreach (DB::select('SELECT id, daemon_token_id, daemon_token FROM nodes') as $datum) {
|
||||||
|
DB::update('UPDATE nodes SET daemon_token = ? WHERE id = ?', [
|
||||||
|
$datum->daemon_token_id . $encrypter->decrypt($datum->daemon_token),
|
||||||
|
$datum->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('nodes', function (Blueprint $table) {
|
||||||
|
$table->dropUnique(['uuid']);
|
||||||
|
$table->dropUnique(['daemon_token_id']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('nodes', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['uuid', 'daemon_token_id']);
|
||||||
|
$table->renameColumn('daemon_token', 'daemonSecret');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('nodes', function (Blueprint $table) {
|
||||||
|
$table->string('daemonSecret', 36)->change();
|
||||||
|
$table->unique(['daemonSecret']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
import axios, { AxiosInstance } from 'axios';
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
import { store } from '@/state';
|
||||||
|
|
||||||
const http: AxiosInstance = axios.create({
|
const http: AxiosInstance = axios.create({
|
||||||
|
timeout: 20000,
|
||||||
headers: {
|
headers: {
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
|
@ -9,6 +11,18 @@ const http: AxiosInstance = axios.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
http.interceptors.request.use(req => {
|
||||||
|
store.getActions().progress.startContinuous();
|
||||||
|
|
||||||
|
return req;
|
||||||
|
});
|
||||||
|
|
||||||
|
http.interceptors.response.use(resp => {
|
||||||
|
store.getActions().progress.setComplete();
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
});
|
||||||
|
|
||||||
// If we have a phpdebugbar instance registered at this point in time go
|
// If we have a phpdebugbar instance registered at this point in time go
|
||||||
// ahead and route the response data through to it so things show up.
|
// ahead and route the response data through to it so things show up.
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
@ -9,6 +9,7 @@ import AuthenticationRouter from '@/routers/AuthenticationRouter';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { SiteSettings } from '@/state/settings';
|
import { SiteSettings } from '@/state/settings';
|
||||||
import { DefaultTheme, ThemeProvider } from 'styled-components';
|
import { DefaultTheme, ThemeProvider } from 'styled-components';
|
||||||
|
import ProgressBar from '@/components/elements/ProgressBar';
|
||||||
|
|
||||||
interface ExtendedWindow extends Window {
|
interface ExtendedWindow extends Window {
|
||||||
SiteConfiguration?: SiteSettings;
|
SiteConfiguration?: SiteSettings;
|
||||||
|
@ -57,6 +58,7 @@ const App = () => {
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<StoreProvider store={store}>
|
<StoreProvider store={store}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
|
<ProgressBar/>
|
||||||
<div className={'mx-auto w-auto'}>
|
<div className={'mx-auto w-auto'}>
|
||||||
<BrowserRouter basename={'/'} key={'root-router'}>
|
<BrowserRouter basename={'/'} key={'root-router'}>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faServer } from '@fortawesome/free-solid-svg-icons/faServer';
|
import { faServer } from '@fortawesome/free-solid-svg-icons/faServer';
|
||||||
import { faEthernet } from '@fortawesome/free-solid-svg-icons/faEthernet';
|
import { faEthernet } from '@fortawesome/free-solid-svg-icons/faEthernet';
|
||||||
|
@ -21,18 +21,19 @@ const isAlarmState = (current: number, limit: number): boolean => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ({ server, className }: { server: Server; className: string | undefined }) => {
|
export default ({ server, className }: { server: Server; className: string | undefined }) => {
|
||||||
|
const interval = useRef<number>(null);
|
||||||
const [ stats, setStats ] = useState<ServerStats | null>(null);
|
const [ stats, setStats ] = useState<ServerStats | null>(null);
|
||||||
|
|
||||||
const getStats = () => getServerResourceUsage(server.uuid).then(data => setStats(data));
|
const getStats = () => getServerResourceUsage(server.uuid).then(data => setStats(data));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let interval: any = null;
|
|
||||||
getStats().then(() => {
|
getStats().then(() => {
|
||||||
interval = setInterval(() => getStats(), 20000);
|
// @ts-ignore
|
||||||
|
interval.current = setInterval(() => getStats(), 20000);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
interval && clearInterval(interval);
|
interval.current && clearInterval(interval.current);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import Spinner from '@/components/elements/Spinner';
|
|
||||||
import { CSSTransition } from 'react-transition-group';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
visible: boolean;
|
|
||||||
children?: React.ReactChild;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ListRefreshIndicator = ({ visible, children }: Props) => (
|
|
||||||
<CSSTransition timeout={250} in={visible} appear={true} unmountOnExit={true} classNames={'fade'}>
|
|
||||||
<div className={'flex items-center mb-2'}>
|
|
||||||
<Spinner size={'tiny'}/>
|
|
||||||
<p className={'ml-2 text-sm text-neutral-400'}>{children || 'Refreshing listing...'}</p>
|
|
||||||
</div>
|
|
||||||
</CSSTransition>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default ListRefreshIndicator;
|
|
73
resources/scripts/components/elements/ProgressBar.tsx
Normal file
73
resources/scripts/components/elements/ProgressBar.tsx
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { useStoreActions, useStoreState } from 'easy-peasy';
|
||||||
|
import { randomInt } from '@/helpers';
|
||||||
|
import { CSSTransition } from 'react-transition-group';
|
||||||
|
|
||||||
|
const BarFill = styled.div`
|
||||||
|
${tw`h-full bg-cyan-400`};
|
||||||
|
transition: 250ms ease-in-out;
|
||||||
|
box-shadow: 0 -2px 10px 2px hsl(178, 78%, 57%);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const interval = useRef<number>(null);
|
||||||
|
const timeout = useRef<number>(null);
|
||||||
|
const [ visible, setVisible ] = useState(false);
|
||||||
|
const progress = useStoreState(state => state.progress.progress);
|
||||||
|
const continuous = useStoreState(state => state.progress.continuous);
|
||||||
|
const setProgress = useStoreActions(actions => actions.progress.setProgress);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
timeout.current && clearTimeout(timeout.current);
|
||||||
|
interval.current && clearInterval(interval.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setVisible((progress || 0) > 0);
|
||||||
|
|
||||||
|
if (progress === 100) {
|
||||||
|
// @ts-ignore
|
||||||
|
timeout.current = setTimeout(() => setProgress(undefined), 500);
|
||||||
|
}
|
||||||
|
}, [ progress ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!continuous) {
|
||||||
|
interval.current && clearInterval(interval.current);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!progress || progress === 0) {
|
||||||
|
setProgress(randomInt(20, 30));
|
||||||
|
}
|
||||||
|
}, [ continuous ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (continuous) {
|
||||||
|
interval.current && clearInterval(interval.current);
|
||||||
|
if ((progress || 0) >= 90) {
|
||||||
|
setProgress(90);
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
interval.current = setTimeout(() => setProgress(progress + randomInt(1, 5)), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [ progress, continuous ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'w-full fixed'} style={{ height: '2px' }}>
|
||||||
|
<CSSTransition
|
||||||
|
timeout={250}
|
||||||
|
appear={true}
|
||||||
|
in={visible}
|
||||||
|
unmountOnExit={true}
|
||||||
|
classNames={'fade'}
|
||||||
|
>
|
||||||
|
<BarFill style={{ width: progress === undefined ? '100%' : `${progress}%` }}/>
|
||||||
|
</CSSTransition>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import Spinner from '@/components/elements/Spinner';
|
import Spinner from '@/components/elements/Spinner';
|
||||||
import getServerBackups, { ServerBackup } from '@/api/server/backups/getServerBackups';
|
import getServerBackups from '@/api/server/backups/getServerBackups';
|
||||||
import useServer from '@/plugins/useServer';
|
import useServer from '@/plugins/useServer';
|
||||||
import useFlash from '@/plugins/useFlash';
|
import useFlash from '@/plugins/useFlash';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
|
@ -9,7 +9,6 @@ import CreateBackupButton from '@/components/server/backups/CreateBackupButton';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import BackupRow from '@/components/server/backups/BackupRow';
|
import BackupRow from '@/components/server/backups/BackupRow';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import ListRefreshIndicator from '@/components/elements/ListRefreshIndicator';
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { uuid } = useServer();
|
const { uuid } = useServer();
|
||||||
|
@ -36,7 +35,6 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'mt-10 mb-6'}>
|
<div className={'mt-10 mb-6'}>
|
||||||
<ListRefreshIndicator visible={loading}/>
|
|
||||||
<FlashMessageRender byKey={'backups'} className={'mb-4'}/>
|
<FlashMessageRender byKey={'backups'} className={'mb-4'}/>
|
||||||
{!backups.length ?
|
{!backups.length ?
|
||||||
<p className="text-center text-sm text-neutral-400">
|
<p className="text-center text-sm text-neutral-400">
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { ServerDatabase } from '@/api/server/getServerDatabases';
|
|
||||||
import Modal from '@/components/elements/Modal';
|
import Modal from '@/components/elements/Modal';
|
||||||
import { Form, Formik, FormikHelpers } from 'formik';
|
import { Form, Formik, FormikHelpers } from 'formik';
|
||||||
import Field from '@/components/elements/Field';
|
import Field from '@/components/elements/Field';
|
||||||
import { object, string } from 'yup';
|
import { object, string } from 'yup';
|
||||||
import createServerDatabase from '@/api/server/createServerDatabase';
|
import createServerDatabase from '@/api/server/createServerDatabase';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
|
||||||
import { ApplicationStore } from '@/state';
|
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
|
import useFlash from '@/plugins/useFlash';
|
||||||
|
import useServer from '@/plugins/useServer';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
databaseName: string;
|
databaseName: string;
|
||||||
|
@ -27,28 +26,25 @@ const schema = object().shape({
|
||||||
.matches(/^([1-9]{1,3}|%)(\.([0-9]{1,3}|%))?(\.([0-9]{1,3}|%))?(\.([0-9]{1,3}|%))?$/, 'A valid connection address must be provided.'),
|
.matches(/^([1-9]{1,3}|%)(\.([0-9]{1,3}|%))?(\.([0-9]{1,3}|%))?(\.([0-9]{1,3}|%))?$/, 'A valid connection address must be provided.'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default ({ onCreated }: { onCreated: (database: ServerDatabase) => void }) => {
|
export default () => {
|
||||||
|
const { uuid } = useServer();
|
||||||
|
const { addError, clearFlashes } = useFlash();
|
||||||
const [ visible, setVisible ] = useState(false);
|
const [ visible, setVisible ] = useState(false);
|
||||||
const { addFlash, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
|
||||||
const server = ServerContext.useStoreState(state => state.server.data!);
|
const appendDatabase = ServerContext.useStoreActions(actions => actions.databases.appendDatabase);
|
||||||
|
|
||||||
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||||
clearFlashes();
|
clearFlashes('database:create');
|
||||||
createServerDatabase(server.uuid, { ...values })
|
createServerDatabase(uuid, { ...values })
|
||||||
.then(database => {
|
.then(database => {
|
||||||
onCreated(database);
|
appendDatabase(database);
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
addFlash({
|
addError({ key: 'database:create', message: httpErrorToHuman(error) });
|
||||||
key: 'create-database-modal',
|
setSubmitting(false);
|
||||||
type: 'error',
|
|
||||||
title: 'Error',
|
|
||||||
message: httpErrorToHuman(error),
|
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.then(() => setSubmitting(false));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -69,7 +65,7 @@ export default ({ onCreated }: { onCreated: (database: ServerDatabase) => void }
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FlashMessageRender byKey={'create-database-modal'} className={'mb-6'}/>
|
<FlashMessageRender byKey={'database:create'} className={'mb-6'}/>
|
||||||
<h3 className={'mb-6'}>Create new database</h3>
|
<h3 className={'mb-6'}>Create new database</h3>
|
||||||
<Form className={'m-0'}>
|
<Form className={'m-0'}>
|
||||||
<Field
|
<Field
|
||||||
|
@ -105,7 +101,7 @@ export default ({ onCreated }: { onCreated: (database: ServerDatabase) => void }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</Formik>
|
</Formik>
|
||||||
<button className={'btn btn-primary btn-lg'} onClick={() => setVisible(true)}>
|
<button className={'btn btn-primary btn-sm'} onClick={() => setVisible(true)}>
|
||||||
New Database
|
New Database
|
||||||
</button>
|
</button>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
@ -9,31 +9,28 @@ import { Form, Formik, FormikHelpers } from 'formik';
|
||||||
import Field from '@/components/elements/Field';
|
import Field from '@/components/elements/Field';
|
||||||
import { object, string } from 'yup';
|
import { object, string } from 'yup';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
|
||||||
import { ApplicationStore } from '@/state';
|
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import deleteServerDatabase from '@/api/server/deleteServerDatabase';
|
import deleteServerDatabase from '@/api/server/deleteServerDatabase';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import RotatePasswordButton from '@/components/server/databases/RotatePasswordButton';
|
import RotatePasswordButton from '@/components/server/databases/RotatePasswordButton';
|
||||||
import Can from '@/components/elements/Can';
|
import Can from '@/components/elements/Can';
|
||||||
|
import { ServerDatabase } from '@/api/server/getServerDatabases';
|
||||||
|
import useServer from '@/plugins/useServer';
|
||||||
|
import useFlash from '@/plugins/useFlash';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
databaseId: string | number;
|
database: ServerDatabase;
|
||||||
className?: string;
|
className?: string;
|
||||||
onDelete: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ databaseId, className, onDelete }: Props) => {
|
export default ({ database, className }: Props) => {
|
||||||
|
const { uuid } = useServer();
|
||||||
|
const { addError, clearFlashes } = useFlash();
|
||||||
const [ visible, setVisible ] = useState(false);
|
const [ visible, setVisible ] = useState(false);
|
||||||
const database = ServerContext.useStoreState(state => state.databases.items.find(item => item.id === databaseId));
|
|
||||||
const appendDatabase = ServerContext.useStoreActions(actions => actions.databases.appendDatabase);
|
|
||||||
const [ connectionVisible, setConnectionVisible ] = useState(false);
|
const [ connectionVisible, setConnectionVisible ] = useState(false);
|
||||||
const { addFlash, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
|
||||||
const server = ServerContext.useStoreState(state => state.server.data!);
|
|
||||||
|
|
||||||
if (!database) {
|
const appendDatabase = ServerContext.useStoreActions(actions => actions.databases.appendDatabase);
|
||||||
return null;
|
const removeDatabase = ServerContext.useStoreActions(actions => actions.databases.removeDatabase);
|
||||||
}
|
|
||||||
|
|
||||||
const schema = object().shape({
|
const schema = object().shape({
|
||||||
confirm: string()
|
confirm: string()
|
||||||
|
@ -43,20 +40,15 @@ export default ({ databaseId, className, onDelete }: Props) => {
|
||||||
|
|
||||||
const submit = (values: { confirm: string }, { setSubmitting }: FormikHelpers<{ confirm: string }>) => {
|
const submit = (values: { confirm: string }, { setSubmitting }: FormikHelpers<{ confirm: string }>) => {
|
||||||
clearFlashes();
|
clearFlashes();
|
||||||
deleteServerDatabase(server.uuid, database.id)
|
deleteServerDatabase(uuid, database.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
setTimeout(() => onDelete(), 150);
|
setTimeout(() => removeDatabase(database.id), 150);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
addFlash({
|
addError({ key: 'database:delete', message: httpErrorToHuman(error) });
|
||||||
key: 'delete-database-modal',
|
|
||||||
type: 'error',
|
|
||||||
title: 'Error',
|
|
||||||
message: httpErrorToHuman(error),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -78,7 +70,7 @@ export default ({ databaseId, className, onDelete }: Props) => {
|
||||||
resetForm();
|
resetForm();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FlashMessageRender byKey={'delete-database-modal'} className={'mb-6'}/>
|
<FlashMessageRender byKey={'database:delete'} className={'mb-6'}/>
|
||||||
<h3 className={'mb-6'}>Confirm database deletion</h3>
|
<h3 className={'mb-6'}>Confirm database deletion</h3>
|
||||||
<p className={'text-sm'}>
|
<p className={'text-sm'}>
|
||||||
Deleting a database is a permanent action, it cannot be undone. This will permanetly
|
Deleting a database is a permanent action, it cannot be undone. This will permanetly
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import getServerDatabases from '@/api/server/getServerDatabases';
|
import getServerDatabases from '@/api/server/getServerDatabases';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
|
||||||
import { ApplicationStore } from '@/state';
|
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import DatabaseRow from '@/components/server/databases/DatabaseRow';
|
import DatabaseRow from '@/components/server/databases/DatabaseRow';
|
||||||
|
@ -10,35 +8,34 @@ import Spinner from '@/components/elements/Spinner';
|
||||||
import { CSSTransition } from 'react-transition-group';
|
import { CSSTransition } from 'react-transition-group';
|
||||||
import CreateDatabaseButton from '@/components/server/databases/CreateDatabaseButton';
|
import CreateDatabaseButton from '@/components/server/databases/CreateDatabaseButton';
|
||||||
import Can from '@/components/elements/Can';
|
import Can from '@/components/elements/Can';
|
||||||
|
import useFlash from '@/plugins/useFlash';
|
||||||
|
import useServer from '@/plugins/useServer';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
|
const { uuid, featureLimits } = useServer();
|
||||||
|
const { addError, clearFlashes } = useFlash();
|
||||||
const [ loading, setLoading ] = useState(true);
|
const [ loading, setLoading ] = useState(true);
|
||||||
const server = ServerContext.useStoreState(state => state.server.data!);
|
|
||||||
const databases = ServerContext.useStoreState(state => state.databases.items);
|
const databases = ServerContext.useStoreState(state => state.databases.data);
|
||||||
const { setDatabases, appendDatabase, removeDatabase } = ServerContext.useStoreActions(state => state.databases);
|
const setDatabases = ServerContext.useStoreActions(state => state.databases.setDatabases);
|
||||||
const { addFlash, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(!databases.length);
|
setLoading(!databases.length);
|
||||||
clearFlashes('databases');
|
clearFlashes('databases');
|
||||||
|
|
||||||
getServerDatabases(server.uuid)
|
getServerDatabases(uuid)
|
||||||
.then(databases => {
|
.then(databases => setDatabases(databases))
|
||||||
setDatabases(databases);
|
.catch(error => {
|
||||||
setLoading(false);
|
console.error(error);
|
||||||
|
addError({ key: 'databases', message: httpErrorToHuman(error) });
|
||||||
})
|
})
|
||||||
.catch(error => addFlash({
|
.then(() => setLoading(false));
|
||||||
key: 'databases',
|
|
||||||
title: 'Error',
|
|
||||||
message: httpErrorToHuman(error),
|
|
||||||
type: 'error',
|
|
||||||
}));
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'my-10 mb-6'}>
|
<div className={'my-10 mb-6'}>
|
||||||
<FlashMessageRender byKey={'databases'}/>
|
<FlashMessageRender byKey={'databases'} className={'mb-4'}/>
|
||||||
{loading ?
|
{(!databases.length && loading) ?
|
||||||
<Spinner size={'large'} centered={true}/>
|
<Spinner size={'large'} centered={true}/>
|
||||||
:
|
:
|
||||||
<CSSTransition classNames={'fade'} timeout={250}>
|
<CSSTransition classNames={'fade'} timeout={250}>
|
||||||
|
@ -47,14 +44,13 @@ export default () => {
|
||||||
databases.map((database, index) => (
|
databases.map((database, index) => (
|
||||||
<DatabaseRow
|
<DatabaseRow
|
||||||
key={database.id}
|
key={database.id}
|
||||||
databaseId={database.id}
|
database={database}
|
||||||
onDelete={() => removeDatabase(database)}
|
|
||||||
className={index > 0 ? 'mt-1' : undefined}
|
className={index > 0 ? 'mt-1' : undefined}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
:
|
:
|
||||||
<p className={'text-center text-sm text-neutral-400'}>
|
<p className={'text-center text-sm text-neutral-400'}>
|
||||||
{server.featureLimits.databases > 0 ?
|
{featureLimits.databases > 0 ?
|
||||||
`It looks like you have no databases.`
|
`It looks like you have no databases.`
|
||||||
:
|
:
|
||||||
`Databases cannot be created for this server.`
|
`Databases cannot be created for this server.`
|
||||||
|
@ -62,9 +58,9 @@ export default () => {
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
<Can action={'database.create'}>
|
<Can action={'database.create'}>
|
||||||
{server.featureLimits.databases > 0 &&
|
{featureLimits.databases > 0 &&
|
||||||
<div className={'mt-6 flex justify-end'}>
|
<div className={'mt-6 flex justify-end'}>
|
||||||
<CreateDatabaseButton onCreated={appendDatabase}/>
|
<CreateDatabaseButton/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</Can>
|
</Can>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import { NavLink, useParams } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import { cleanDirectoryPath } from '@/helpers';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
withinFileEditor?: boolean;
|
withinFileEditor?: boolean;
|
||||||
|
@ -8,21 +9,17 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ withinFileEditor, isNewFile }: Props) => {
|
export default ({ withinFileEditor, isNewFile }: Props) => {
|
||||||
const { action } = useParams();
|
|
||||||
const [ file, setFile ] = useState<string | null>(null);
|
const [ file, setFile ] = useState<string | null>(null);
|
||||||
const id = ServerContext.useStoreState(state => state.server.data!.id);
|
const id = ServerContext.useStoreState(state => state.server.data!.id);
|
||||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||||
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const parts = window.location.hash.replace(/^#(\/)*/, '/').split('/');
|
const parts = cleanDirectoryPath(window.location.hash).split('/');
|
||||||
|
|
||||||
if (withinFileEditor && !isNewFile) {
|
if (withinFileEditor && !isNewFile) {
|
||||||
setFile(parts.pop() || null);
|
setFile(parts.pop() || null);
|
||||||
}
|
}
|
||||||
|
}, [ withinFileEditor, isNewFile ]);
|
||||||
setDirectory(parts.join('/'));
|
|
||||||
}, [ withinFileEditor, isNewFile, setDirectory ]);
|
|
||||||
|
|
||||||
const breadcrumbs = (): { name: string; path?: string }[] => directory.split('/')
|
const breadcrumbs = (): { name: string; path?: string }[] => directory.split('/')
|
||||||
.filter(directory => !!directory)
|
.filter(directory => !!directory)
|
||||||
|
@ -39,7 +36,6 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
|
||||||
/<span className={'px-1 text-neutral-300'}>home</span>/
|
/<span className={'px-1 text-neutral-300'}>home</span>/
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/server/${id}/files`}
|
to={`/server/${id}/files`}
|
||||||
onClick={() => setDirectory('/')}
|
|
||||||
className={'px-1 text-neutral-200 no-underline hover:text-neutral-100'}
|
className={'px-1 text-neutral-200 no-underline hover:text-neutral-100'}
|
||||||
>
|
>
|
||||||
container
|
container
|
||||||
|
@ -50,7 +46,6 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/server/${id}/files#${crumb.path}`}
|
to={`/server/${id}/files#${crumb.path}`}
|
||||||
onClick={() => setDirectory(crumb.path!)}
|
|
||||||
className={'px-1 text-neutral-200 no-underline hover:text-neutral-100'}
|
className={'px-1 text-neutral-200 no-underline hover:text-neutral-100'}
|
||||||
>
|
>
|
||||||
{crumb.name}
|
{crumb.name}
|
||||||
|
|
|
@ -22,20 +22,20 @@ export default () => {
|
||||||
const [ loading, setLoading ] = useState(true);
|
const [ loading, setLoading ] = useState(true);
|
||||||
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||||
const { id } = ServerContext.useStoreState(state => state.server.data!);
|
const { id } = ServerContext.useStoreState(state => state.server.data!);
|
||||||
const { contents: files, directory } = ServerContext.useStoreState(state => state.files);
|
const { contents: files } = ServerContext.useStoreState(state => state.files);
|
||||||
const { getDirectoryContents } = ServerContext.useStoreActions(actions => actions.files);
|
const { getDirectoryContents } = ServerContext.useStoreActions(actions => actions.files);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
clearFlashes();
|
clearFlashes();
|
||||||
|
|
||||||
getDirectoryContents(window.location.hash.replace(/^#(\/)*/, '/'))
|
getDirectoryContents(window.location.hash)
|
||||||
.then(() => setLoading(false))
|
.then(() => setLoading(false))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error.message, { error });
|
console.error(error.message, { error });
|
||||||
addError({ message: httpErrorToHuman(error), key: 'files' });
|
addError({ message: httpErrorToHuman(error), key: 'files' });
|
||||||
});
|
});
|
||||||
}, [ directory ]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'my-10 mb-6'}>
|
<div className={'my-10 mb-6'}>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faFileImport } from '@fortawesome/free-solid-svg-icons/faFileImport';
|
import { faFileImport } from '@fortawesome/free-solid-svg-icons/faFileImport';
|
||||||
import { faFileAlt } from '@fortawesome/free-solid-svg-icons/faFileAlt';
|
import { faFileAlt } from '@fortawesome/free-solid-svg-icons/faFileAlt';
|
||||||
import { faFolder } from '@fortawesome/free-solid-svg-icons/faFolder';
|
import { faFolder } from '@fortawesome/free-solid-svg-icons/faFolder';
|
||||||
import { bytesToHuman } from '@/helpers';
|
import { bytesToHuman, cleanDirectoryPath } from '@/helpers';
|
||||||
import differenceInHours from 'date-fns/difference_in_hours';
|
import differenceInHours from 'date-fns/difference_in_hours';
|
||||||
import format from 'date-fns/format';
|
import format from 'date-fns/format';
|
||||||
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
|
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
|
||||||
|
@ -16,7 +16,7 @@ import useRouter from 'use-react-router';
|
||||||
export default ({ file }: { file: FileObject }) => {
|
export default ({ file }: { file: FileObject }) => {
|
||||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||||
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
|
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
|
||||||
const { match } = useRouter();
|
const { match, history } = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -27,7 +27,7 @@ export default ({ file }: { file: FileObject }) => {
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`${match.url}/${file.isFile ? 'edit/' : ''}#${directory}/${file.name}`}
|
to={`${match.url}/${file.isFile ? 'edit/' : ''}#${cleanDirectoryPath(`${directory}/${file.name}`)}`}
|
||||||
className={'flex flex-1 text-neutral-300 no-underline p-3'}
|
className={'flex flex-1 text-neutral-300 no-underline p-3'}
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
// Don't rely on the onClick to work with the generated URL. Because of the way this
|
// Don't rely on the onClick to work with the generated URL. Because of the way this
|
||||||
|
@ -38,7 +38,7 @@ export default ({ file }: { file: FileObject }) => {
|
||||||
if (!file.isFile) {
|
if (!file.isFile) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
window.location.hash = `#${directory}/${file.name}`;
|
history.push(`#${cleanDirectoryPath(`${directory}/${file.name}`)}`);
|
||||||
setDirectory(`${directory}/${file.name}`);
|
setDirectory(`${directory}/${file.name}`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -6,14 +6,13 @@ import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||||
import Switch from '@/components/elements/Switch';
|
import Switch from '@/components/elements/Switch';
|
||||||
import createOrUpdateSchedule from '@/api/server/schedules/createOrUpdateSchedule';
|
import createOrUpdateSchedule from '@/api/server/schedules/createOrUpdateSchedule';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
|
||||||
import { ApplicationStore } from '@/state';
|
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
|
import useServer from '@/plugins/useServer';
|
||||||
|
import useFlash from '@/plugins/useFlash';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
schedule?: Schedule;
|
schedule?: Schedule;
|
||||||
onScheduleUpdated: (schedule: Schedule) => void;
|
|
||||||
} & RequiredModalProps;
|
} & RequiredModalProps;
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
|
@ -73,15 +72,17 @@ const EditScheduleModal = ({ schedule, ...props }: Omit<Props, 'onScheduleUpdate
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ({ schedule, onScheduleUpdated, visible, ...props }: Props) => {
|
export default ({ schedule, visible, ...props }: Props) => {
|
||||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
const { uuid } = useServer();
|
||||||
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
const { addError, clearFlashes } = useFlash();
|
||||||
const [ modalVisible, setModalVisible ] = useState(visible);
|
const [ modalVisible, setModalVisible ] = useState(visible);
|
||||||
|
|
||||||
|
const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setModalVisible(visible);
|
setModalVisible(visible);
|
||||||
clearFlashes('schedule:edit');
|
clearFlashes('schedule:edit');
|
||||||
}, [visible]);
|
}, [ visible ]);
|
||||||
|
|
||||||
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||||
clearFlashes('schedule:edit');
|
clearFlashes('schedule:edit');
|
||||||
|
@ -98,7 +99,7 @@ export default ({ schedule, onScheduleUpdated, visible, ...props }: Props) => {
|
||||||
})
|
})
|
||||||
.then(schedule => {
|
.then(schedule => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
onScheduleUpdated(schedule);
|
appendSchedule(schedule);
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
|
|
@ -1,24 +1,20 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Task } from '@/api/server/schedules/getServerSchedules';
|
import { Schedule } from '@/api/server/schedules/getServerSchedules';
|
||||||
import TaskDetailsModal from '@/components/server/schedules/TaskDetailsModal';
|
import TaskDetailsModal from '@/components/server/schedules/TaskDetailsModal';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
scheduleId: number;
|
schedule: Schedule;
|
||||||
onTaskAdded: (task: Task) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ scheduleId, onTaskAdded }: Props) => {
|
export default ({ schedule }: Props) => {
|
||||||
const [visible, setVisible] = useState(false);
|
const [ visible, setVisible ] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{visible &&
|
{visible &&
|
||||||
<TaskDetailsModal
|
<TaskDetailsModal
|
||||||
scheduleId={scheduleId}
|
schedule={schedule}
|
||||||
onDismissed={task => {
|
onDismissed={() => setVisible(false)}
|
||||||
task && onTaskAdded(task);
|
|
||||||
setVisible(false);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<button className={'btn btn-primary btn-sm'} onClick={() => setVisible(true)}>
|
<button className={'btn btn-primary btn-sm'} onClick={() => setVisible(true)}>
|
||||||
|
|
|
@ -1,36 +1,40 @@
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import getServerSchedules, { Schedule } from '@/api/server/schedules/getServerSchedules';
|
import getServerSchedules from '@/api/server/schedules/getServerSchedules';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import Spinner from '@/components/elements/Spinner';
|
import Spinner from '@/components/elements/Spinner';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import ScheduleRow from '@/components/server/schedules/ScheduleRow';
|
import ScheduleRow from '@/components/server/schedules/ScheduleRow';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
|
||||||
import { ApplicationStore } from '@/state';
|
|
||||||
import EditScheduleModal from '@/components/server/schedules/EditScheduleModal';
|
import EditScheduleModal from '@/components/server/schedules/EditScheduleModal';
|
||||||
import Can from '@/components/elements/Can';
|
import Can from '@/components/elements/Can';
|
||||||
|
import useServer from '@/plugins/useServer';
|
||||||
|
import useFlash from '@/plugins/useFlash';
|
||||||
|
|
||||||
export default ({ match, history }: RouteComponentProps) => {
|
export default ({ match, history }: RouteComponentProps) => {
|
||||||
const { uuid } = ServerContext.useStoreState(state => state.server.data!);
|
const { uuid } = useServer();
|
||||||
const [ schedules, setSchedules ] = useState<Schedule[] | null>(null);
|
const { clearFlashes, addError } = useFlash();
|
||||||
|
const [ loading, setLoading ] = useState(true);
|
||||||
const [ visible, setVisible ] = useState(false);
|
const [ visible, setVisible ] = useState(false);
|
||||||
const { clearFlashes, addError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
|
||||||
|
|
||||||
useMemo(() => {
|
const schedules = ServerContext.useStoreState(state => state.schedules.data);
|
||||||
|
const setSchedules = ServerContext.useStoreActions(actions => actions.schedules.setSchedules);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
clearFlashes('schedules');
|
clearFlashes('schedules');
|
||||||
getServerSchedules(uuid)
|
getServerSchedules(uuid)
|
||||||
.then(schedules => setSchedules(schedules))
|
.then(schedules => setSchedules(schedules))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
addError({ message: httpErrorToHuman(error), key: 'schedules' });
|
addError({ message: httpErrorToHuman(error), key: 'schedules' });
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
})
|
||||||
}, [ setSchedules ]);
|
.then(() => setLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'my-10 mb-6'}>
|
<div className={'my-10 mb-6'}>
|
||||||
<FlashMessageRender byKey={'schedules'} className={'mb-4'}/>
|
<FlashMessageRender byKey={'schedules'} className={'mb-4'}/>
|
||||||
{!schedules ?
|
{(!schedules.length && loading) ?
|
||||||
<Spinner size={'large'} centered={true}/>
|
<Spinner size={'large'} centered={true}/>
|
||||||
:
|
:
|
||||||
<>
|
<>
|
||||||
|
@ -59,7 +63,6 @@ export default ({ match, history }: RouteComponentProps) => {
|
||||||
{visible && <EditScheduleModal
|
{visible && <EditScheduleModal
|
||||||
appear={true}
|
appear={true}
|
||||||
visible={true}
|
visible={true}
|
||||||
onScheduleUpdated={schedule => setSchedules(s => [ ...(s || []), schedule ])}
|
|
||||||
onDismissed={() => setVisible(false)}
|
onDismissed={() => setVisible(false)}
|
||||||
/>}
|
/>}
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -2,11 +2,8 @@ import React, { useEffect, useState } from 'react';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import { Schedule } from '@/api/server/schedules/getServerSchedules';
|
import { Schedule } from '@/api/server/schedules/getServerSchedules';
|
||||||
import getServerSchedule from '@/api/server/schedules/getServerSchedule';
|
import getServerSchedule from '@/api/server/schedules/getServerSchedule';
|
||||||
import { ServerContext } from '@/state/server';
|
|
||||||
import Spinner from '@/components/elements/Spinner';
|
import Spinner from '@/components/elements/Spinner';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
|
||||||
import { ApplicationStore } from '@/state';
|
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import ScheduleRow from '@/components/server/schedules/ScheduleRow';
|
import ScheduleRow from '@/components/server/schedules/ScheduleRow';
|
||||||
import ScheduleTaskRow from '@/components/server/schedules/ScheduleTaskRow';
|
import ScheduleTaskRow from '@/components/server/schedules/ScheduleTaskRow';
|
||||||
|
@ -14,6 +11,9 @@ import EditScheduleModal from '@/components/server/schedules/EditScheduleModal';
|
||||||
import NewTaskButton from '@/components/server/schedules/NewTaskButton';
|
import NewTaskButton from '@/components/server/schedules/NewTaskButton';
|
||||||
import DeleteScheduleButton from '@/components/server/schedules/DeleteScheduleButton';
|
import DeleteScheduleButton from '@/components/server/schedules/DeleteScheduleButton';
|
||||||
import Can from '@/components/elements/Can';
|
import Can from '@/components/elements/Can';
|
||||||
|
import useServer from '@/plugins/useServer';
|
||||||
|
import useFlash from '@/plugins/useFlash';
|
||||||
|
import { ServerContext } from '@/state/server';
|
||||||
|
|
||||||
interface Params {
|
interface Params {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -24,11 +24,13 @@ interface State {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ match, history, location: { state } }: RouteComponentProps<Params, {}, State>) => {
|
export default ({ match, history, location: { state } }: RouteComponentProps<Params, {}, State>) => {
|
||||||
const { id, uuid } = ServerContext.useStoreState(state => state.server.data!);
|
const { id, uuid } = useServer();
|
||||||
|
const { clearFlashes, addError } = useFlash();
|
||||||
const [ isLoading, setIsLoading ] = useState(true);
|
const [ isLoading, setIsLoading ] = useState(true);
|
||||||
const [ showEditModal, setShowEditModal ] = useState(false);
|
const [ showEditModal, setShowEditModal ] = useState(false);
|
||||||
const [ schedule, setSchedule ] = useState<Schedule | undefined>(state?.schedule);
|
|
||||||
const { clearFlashes, addError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
const schedule = ServerContext.useStoreState(st => st.schedules.data.find(s => s.id === state.schedule?.id), [ match ]);
|
||||||
|
const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (schedule?.id === Number(match.params.id)) {
|
if (schedule?.id === Number(match.params.id)) {
|
||||||
|
@ -38,13 +40,13 @@ export default ({ match, history, location: { state } }: RouteComponentProps<Par
|
||||||
|
|
||||||
clearFlashes('schedules');
|
clearFlashes('schedules');
|
||||||
getServerSchedule(uuid, Number(match.params.id))
|
getServerSchedule(uuid, Number(match.params.id))
|
||||||
.then(schedule => setSchedule(schedule))
|
.then(schedule => appendSchedule(schedule))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
addError({ message: httpErrorToHuman(error), key: 'schedules' });
|
addError({ message: httpErrorToHuman(error), key: 'schedules' });
|
||||||
})
|
})
|
||||||
.then(() => setIsLoading(false));
|
.then(() => setIsLoading(false));
|
||||||
}, [ schedule, match ]);
|
}, [ match ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'my-10 mb-6'}>
|
<div className={'my-10 mb-6'}>
|
||||||
|
@ -59,7 +61,6 @@ export default ({ match, history, location: { state } }: RouteComponentProps<Par
|
||||||
<EditScheduleModal
|
<EditScheduleModal
|
||||||
visible={showEditModal}
|
visible={showEditModal}
|
||||||
schedule={schedule}
|
schedule={schedule}
|
||||||
onScheduleUpdated={schedule => setSchedule(schedule)}
|
|
||||||
onDismissed={() => setShowEditModal(false)}
|
onDismissed={() => setShowEditModal(false)}
|
||||||
/>
|
/>
|
||||||
<div className={'flex items-center mt-8 mb-4'}>
|
<div className={'flex items-center mt-8 mb-4'}>
|
||||||
|
@ -67,23 +68,13 @@ export default ({ match, history, location: { state } }: RouteComponentProps<Par
|
||||||
<h2>Configured Tasks</h2>
|
<h2>Configured Tasks</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{schedule?.tasks.length > 0 ?
|
{schedule.tasks.length > 0 ?
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
schedule.tasks
|
schedule.tasks
|
||||||
.sort((a, b) => a.sequenceId - b.sequenceId)
|
.sort((a, b) => a.sequenceId - b.sequenceId)
|
||||||
.map(task => (
|
.map(task => (
|
||||||
<ScheduleTaskRow
|
<ScheduleTaskRow key={task.id} task={task} schedule={schedule}/>
|
||||||
key={task.id}
|
|
||||||
task={task}
|
|
||||||
schedule={schedule.id}
|
|
||||||
onTaskUpdated={task => setSchedule(s => ({
|
|
||||||
...s!, tasks: s!.tasks.map(t => t.id === task.id ? task : t),
|
|
||||||
}))}
|
|
||||||
onTaskRemoved={() => setSchedule(s => ({
|
|
||||||
...s!, tasks: s!.tasks.filter(t => t.id !== task.id),
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
{schedule.tasks.length > 1 &&
|
{schedule.tasks.length > 1 &&
|
||||||
|
@ -108,12 +99,7 @@ export default ({ match, history, location: { state } }: RouteComponentProps<Par
|
||||||
<button className={'btn btn-primary btn-sm mr-4'} onClick={() => setShowEditModal(true)}>
|
<button className={'btn btn-primary btn-sm mr-4'} onClick={() => setShowEditModal(true)}>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
<NewTaskButton
|
<NewTaskButton schedule={schedule}/>
|
||||||
scheduleId={schedule.id}
|
|
||||||
onTaskAdded={task => setSchedule(s => ({
|
|
||||||
...s!, tasks: [ ...s!.tasks, task ],
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</Can>
|
</Can>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,39 +1,41 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Task } from '@/api/server/schedules/getServerSchedules';
|
import { Schedule, Task } from '@/api/server/schedules/getServerSchedules';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt';
|
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt';
|
||||||
import { faCode } from '@fortawesome/free-solid-svg-icons/faCode';
|
import { faCode } from '@fortawesome/free-solid-svg-icons/faCode';
|
||||||
import { faToggleOn } from '@fortawesome/free-solid-svg-icons/faToggleOn';
|
import { faToggleOn } from '@fortawesome/free-solid-svg-icons/faToggleOn';
|
||||||
import ConfirmTaskDeletionModal from '@/components/server/schedules/ConfirmTaskDeletionModal';
|
import ConfirmTaskDeletionModal from '@/components/server/schedules/ConfirmTaskDeletionModal';
|
||||||
import { ServerContext } from '@/state/server';
|
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
|
||||||
import { ApplicationStore } from '@/state';
|
|
||||||
import deleteScheduleTask from '@/api/server/schedules/deleteScheduleTask';
|
import deleteScheduleTask from '@/api/server/schedules/deleteScheduleTask';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||||
import TaskDetailsModal from '@/components/server/schedules/TaskDetailsModal';
|
import TaskDetailsModal from '@/components/server/schedules/TaskDetailsModal';
|
||||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons/faPencilAlt';
|
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons/faPencilAlt';
|
||||||
import Can from '@/components/elements/Can';
|
import Can from '@/components/elements/Can';
|
||||||
|
import useServer from '@/plugins/useServer';
|
||||||
|
import useFlash from '@/plugins/useFlash';
|
||||||
|
import { ServerContext } from '@/state/server';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
schedule: number;
|
schedule: Schedule;
|
||||||
task: Task;
|
task: Task;
|
||||||
onTaskUpdated: (task: Task) => void;
|
|
||||||
onTaskRemoved: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ schedule, task, onTaskUpdated, onTaskRemoved }: Props) => {
|
export default ({ schedule, task }: Props) => {
|
||||||
|
const { uuid } = useServer();
|
||||||
|
const { clearFlashes, addError } = useFlash();
|
||||||
const [ visible, setVisible ] = useState(false);
|
const [ visible, setVisible ] = useState(false);
|
||||||
const [ isLoading, setIsLoading ] = useState(false);
|
const [ isLoading, setIsLoading ] = useState(false);
|
||||||
const [ isEditing, setIsEditing ] = useState(false);
|
const [ isEditing, setIsEditing ] = useState(false);
|
||||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule);
|
||||||
const { clearFlashes, addError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
|
||||||
|
|
||||||
const onConfirmDeletion = () => {
|
const onConfirmDeletion = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
clearFlashes('schedules');
|
clearFlashes('schedules');
|
||||||
deleteScheduleTask(uuid, schedule, task.id)
|
deleteScheduleTask(uuid, schedule.id, task.id)
|
||||||
.then(() => onTaskRemoved())
|
.then(() => appendSchedule({
|
||||||
|
...schedule,
|
||||||
|
tasks: schedule.tasks.filter(t => t.id !== task.id),
|
||||||
|
}))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
@ -45,12 +47,9 @@ export default ({ schedule, task, onTaskUpdated, onTaskRemoved }: Props) => {
|
||||||
<div className={'flex items-center bg-neutral-700 border border-neutral-600 mb-2 px-6 py-4 rounded'}>
|
<div className={'flex items-center bg-neutral-700 border border-neutral-600 mb-2 px-6 py-4 rounded'}>
|
||||||
<SpinnerOverlay visible={isLoading} fixed={true} size={'large'}/>
|
<SpinnerOverlay visible={isLoading} fixed={true} size={'large'}/>
|
||||||
{isEditing && <TaskDetailsModal
|
{isEditing && <TaskDetailsModal
|
||||||
scheduleId={schedule}
|
schedule={schedule}
|
||||||
task={task}
|
task={task}
|
||||||
onDismissed={task => {
|
onDismissed={() => setIsEditing(false)}
|
||||||
task && onTaskUpdated(task);
|
|
||||||
setIsEditing(false);
|
|
||||||
}}
|
|
||||||
/>}
|
/>}
|
||||||
<ConfirmTaskDeletionModal
|
<ConfirmTaskDeletionModal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import Modal from '@/components/elements/Modal';
|
import Modal from '@/components/elements/Modal';
|
||||||
import { Task } from '@/api/server/schedules/getServerSchedules';
|
import { Schedule, Task } from '@/api/server/schedules/getServerSchedules';
|
||||||
import { Field as FormikField, Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
import { Field as FormikField, Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
|
||||||
import { ApplicationStore } from '@/state';
|
|
||||||
import createOrUpdateScheduleTask from '@/api/server/schedules/createOrUpdateScheduleTask';
|
import createOrUpdateScheduleTask from '@/api/server/schedules/createOrUpdateScheduleTask';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import Field from '@/components/elements/Field';
|
import Field from '@/components/elements/Field';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import { number, object, string } from 'yup';
|
import { number, object, string } from 'yup';
|
||||||
|
import useFlash from '@/plugins/useFlash';
|
||||||
|
import useServer from '@/plugins/useServer';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
scheduleId: number;
|
schedule: Schedule;
|
||||||
// If a task is provided we can assume we're editing it. If not provided,
|
// If a task is provided we can assume we're editing it. If not provided,
|
||||||
// we are creating a new one.
|
// we are creating a new one.
|
||||||
task?: Task;
|
task?: Task;
|
||||||
onDismissed: (task: Task | undefined | void) => void;
|
onDismissed: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
|
@ -29,9 +29,11 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
|
||||||
const { values: { action }, setFieldValue, setFieldTouched } = useFormikContext<Values>();
|
const { values: { action }, setFieldValue, setFieldTouched } = useFormikContext<Values>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
setFieldValue('payload', '');
|
setFieldValue('payload', '');
|
||||||
setFieldTouched('payload', false);
|
setFieldTouched('payload', false);
|
||||||
}, [action]);
|
};
|
||||||
|
}, [ action ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form className={'m-0'}>
|
<Form className={'m-0'}>
|
||||||
|
@ -80,9 +82,10 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ({ task, scheduleId, onDismissed }: Props) => {
|
export default ({ task, schedule, onDismissed }: Props) => {
|
||||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
const { uuid } = useServer();
|
||||||
const { clearFlashes, addError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
const { clearFlashes, addError } = useFlash();
|
||||||
|
const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearFlashes('schedule:task');
|
clearFlashes('schedule:task');
|
||||||
|
@ -90,8 +93,16 @@ export default ({ task, scheduleId, onDismissed }: Props) => {
|
||||||
|
|
||||||
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||||
clearFlashes('schedule:task');
|
clearFlashes('schedule:task');
|
||||||
createOrUpdateScheduleTask(uuid, scheduleId, task?.id, values)
|
createOrUpdateScheduleTask(uuid, schedule.id, task?.id, values)
|
||||||
.then(task => onDismissed(task))
|
.then(task => {
|
||||||
|
let tasks = schedule.tasks.map(t => t.id === task.id ? task : t);
|
||||||
|
if (!schedule.tasks.find(t => t.id === task.id)) {
|
||||||
|
tasks = [ ...tasks, task ];
|
||||||
|
}
|
||||||
|
|
||||||
|
appendSchedule({ ...schedule, tasks });
|
||||||
|
onDismissed();
|
||||||
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
|
|
@ -21,10 +21,6 @@ export default () => {
|
||||||
const getPermissions = useStoreActions((actions: Actions<ApplicationStore>) => actions.permissions.getPermissions);
|
const getPermissions = useStoreActions((actions: Actions<ApplicationStore>) => actions.permissions.getPermissions);
|
||||||
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getPermissions().catch(error => console.error(error));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearFlashes('users');
|
clearFlashes('users');
|
||||||
getServerSubusers(uuid)
|
getServerSubusers(uuid)
|
||||||
|
@ -38,7 +34,14 @@ export default () => {
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (loading || !Object.keys(permissions).length) {
|
useEffect(() => {
|
||||||
|
getPermissions().catch(error => {
|
||||||
|
addError({ key: 'users', message: httpErrorToHuman(error) });
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!subusers.length && (loading || !Object.keys(permissions).length)) {
|
||||||
return <Spinner size={'large'} centered={true}/>;
|
return <Spinner size={'large'} centered={true}/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
8
resources/scripts/easy-peasy.d.ts
vendored
8
resources/scripts/easy-peasy.d.ts
vendored
|
@ -1,9 +1,13 @@
|
||||||
// noinspection ES6UnusedImports
|
// noinspection ES6UnusedImports
|
||||||
import EasyPeasy from 'easy-peasy';
|
import EasyPeasy, { Actions, State } from 'easy-peasy';
|
||||||
import { ApplicationStore } from '@/state';
|
import { ApplicationStore } from '@/state';
|
||||||
|
|
||||||
declare module 'easy-peasy' {
|
declare module 'easy-peasy' {
|
||||||
export function useStoreState<Result>(
|
export function useStoreState<Result>(
|
||||||
mapState: (state: ApplicationStore) => Result,
|
mapState: (state: State<ApplicationStore>) => Result,
|
||||||
|
): Result;
|
||||||
|
|
||||||
|
export function useStoreActions<Result>(
|
||||||
|
mapActions: (actions: Actions<ApplicationStore>) => Result,
|
||||||
): Result;
|
): Result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,3 +9,7 @@ export function bytesToHuman (bytes: number): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bytesToMegabytes = (bytes: number) => Math.floor(bytes / 1000 / 1000);
|
export const bytesToMegabytes = (bytes: number) => Math.floor(bytes / 1000 / 1000);
|
||||||
|
|
||||||
|
export const randomInt = (low: number, high: number) => Math.floor(Math.random() * (high - low) + low);
|
||||||
|
|
||||||
|
export const cleanDirectoryPath = (path: string) => path.replace(/(^#\/*)|(\/(\/*))|(^$)/g, '/');
|
||||||
|
|
|
@ -64,7 +64,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
||||||
<Spinner size={'large'}/>
|
<Spinner size={'large'}/>
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
<Switch location={location} key={'server-switch'}>
|
<Switch location={location}>
|
||||||
<Route path={`${match.path}`} component={ServerConsole} exact/>
|
<Route path={`${match.path}`} component={ServerConsole} exact/>
|
||||||
<Route path={`${match.path}/files`} component={FileManagerContainer} exact/>
|
<Route path={`${match.path}/files`} component={FileManagerContainer} exact/>
|
||||||
<Route
|
<Route
|
||||||
|
|
|
@ -3,12 +3,14 @@ import flashes, { FlashStore } from '@/state/flashes';
|
||||||
import user, { UserStore } from '@/state/user';
|
import user, { UserStore } from '@/state/user';
|
||||||
import permissions, { GloablPermissionsStore } from '@/state/permissions';
|
import permissions, { GloablPermissionsStore } from '@/state/permissions';
|
||||||
import settings, { SettingsStore } from '@/state/settings';
|
import settings, { SettingsStore } from '@/state/settings';
|
||||||
|
import progress, { ProgressStore } from '@/state/progress';
|
||||||
|
|
||||||
export interface ApplicationStore {
|
export interface ApplicationStore {
|
||||||
permissions: GloablPermissionsStore;
|
permissions: GloablPermissionsStore;
|
||||||
flashes: FlashStore;
|
flashes: FlashStore;
|
||||||
user: UserStore;
|
user: UserStore;
|
||||||
settings: SettingsStore;
|
settings: SettingsStore;
|
||||||
|
progress: ProgressStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state: ApplicationStore = {
|
const state: ApplicationStore = {
|
||||||
|
@ -16,6 +18,7 @@ const state: ApplicationStore = {
|
||||||
flashes,
|
flashes,
|
||||||
user,
|
user,
|
||||||
settings,
|
settings,
|
||||||
|
progress,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const store = createStore(state);
|
export const store = createStore(state);
|
||||||
|
|
30
resources/scripts/state/progress.ts
Normal file
30
resources/scripts/state/progress.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { action, Action } from 'easy-peasy';
|
||||||
|
|
||||||
|
export interface ProgressStore {
|
||||||
|
continuous: boolean;
|
||||||
|
progress?: number;
|
||||||
|
|
||||||
|
startContinuous: Action<ProgressStore>;
|
||||||
|
setProgress: Action<ProgressStore, number | undefined>;
|
||||||
|
setComplete: Action<ProgressStore>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress: ProgressStore = {
|
||||||
|
continuous: false,
|
||||||
|
progress: undefined,
|
||||||
|
|
||||||
|
startContinuous: action(state => {
|
||||||
|
state.continuous = true;
|
||||||
|
}),
|
||||||
|
|
||||||
|
setProgress: action((state, payload) => {
|
||||||
|
state.progress = payload;
|
||||||
|
}),
|
||||||
|
|
||||||
|
setComplete: action(state => {
|
||||||
|
state.progress = 100;
|
||||||
|
state.continuous = false;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default progress;
|
31
resources/scripts/state/server/databases.ts
Normal file
31
resources/scripts/state/server/databases.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { action, Action } from 'easy-peasy';
|
||||||
|
import { ServerDatabase } from '@/api/server/getServerDatabases';
|
||||||
|
|
||||||
|
export interface ServerDatabaseStore {
|
||||||
|
data: ServerDatabase[];
|
||||||
|
setDatabases: Action<ServerDatabaseStore, ServerDatabase[]>;
|
||||||
|
appendDatabase: Action<ServerDatabaseStore, ServerDatabase>;
|
||||||
|
removeDatabase: Action<ServerDatabaseStore, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const databases: ServerDatabaseStore = {
|
||||||
|
data: [],
|
||||||
|
|
||||||
|
setDatabases: action((state, payload) => {
|
||||||
|
state.data = payload;
|
||||||
|
}),
|
||||||
|
|
||||||
|
appendDatabase: action((state, payload) => {
|
||||||
|
if (state.data.find(database => database.id === payload.id)) {
|
||||||
|
state.data = state.data.map(database => database.id === payload.id ? payload : database);
|
||||||
|
} else {
|
||||||
|
state.data = [ ...state.data, payload ];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
removeDatabase: action((state, payload) => {
|
||||||
|
state.data = [ ...state.data.filter(database => database.id !== payload) ];
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default databases;
|
|
@ -1,6 +1,7 @@
|
||||||
import loadDirectory, { FileObject } from '@/api/server/files/loadDirectory';
|
import loadDirectory, { FileObject } from '@/api/server/files/loadDirectory';
|
||||||
import { action, Action, thunk, Thunk } from 'easy-peasy';
|
import { action, Action, thunk, Thunk } from 'easy-peasy';
|
||||||
import { ServerStore } from '@/state/server/index';
|
import { ServerStore } from '@/state/server/index';
|
||||||
|
import { cleanDirectoryPath } from '@/helpers';
|
||||||
|
|
||||||
export interface ServerFileStore {
|
export interface ServerFileStore {
|
||||||
directory: string;
|
directory: string;
|
||||||
|
@ -22,7 +23,7 @@ const files: ServerFileStore = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contents = await loadDirectory(server.uuid, payload);
|
const contents = await loadDirectory(server.uuid, cleanDirectoryPath(payload));
|
||||||
|
|
||||||
actions.setDirectory(payload.length === 0 ? '/' : payload);
|
actions.setDirectory(payload.length === 0 ? '/' : payload);
|
||||||
actions.setContents(contents);
|
actions.setContents(contents);
|
||||||
|
@ -47,7 +48,7 @@ const files: ServerFileStore = {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setDirectory: action((state, payload) => {
|
setDirectory: action((state, payload) => {
|
||||||
state.directory = payload.length === 0 ? '/' : payload;
|
state.directory = cleanDirectoryPath(payload)
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import getServer, { Server } from '@/api/server/getServer';
|
import getServer, { Server } from '@/api/server/getServer';
|
||||||
import { action, Action, createContextStore, thunk, Thunk } from 'easy-peasy';
|
import { action, Action, createContextStore, thunk, Thunk } from 'easy-peasy';
|
||||||
import socket, { SocketStore } from './socket';
|
import socket, { SocketStore } from './socket';
|
||||||
import { ServerDatabase } from '@/api/server/getServerDatabases';
|
|
||||||
import files, { ServerFileStore } from '@/state/server/files';
|
import files, { ServerFileStore } from '@/state/server/files';
|
||||||
import subusers, { ServerSubuserStore } from '@/state/server/subusers';
|
import subusers, { ServerSubuserStore } from '@/state/server/subusers';
|
||||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||||
import backups, { ServerBackupStore } from '@/state/server/backups';
|
import backups, { ServerBackupStore } from '@/state/server/backups';
|
||||||
|
import schedules, { ServerScheduleStore } from '@/state/server/schedules';
|
||||||
|
import databases, { ServerDatabaseStore } from '@/state/server/databases';
|
||||||
|
|
||||||
export type ServerStatus = 'offline' | 'starting' | 'stopping' | 'running';
|
export type ServerStatus = 'offline' | 'starting' | 'stopping' | 'running';
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ const server: ServerDataStore = {
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
|
||||||
getServer: thunk(async (actions, payload) => {
|
getServer: thunk(async (actions, payload) => {
|
||||||
const [server, permissions] = await getServer(payload);
|
const [ server, permissions ] = await getServer(payload);
|
||||||
|
|
||||||
actions.setServer(server);
|
actions.setServer(server);
|
||||||
actions.setPermissions(permissions);
|
actions.setPermissions(permissions);
|
||||||
|
@ -49,31 +50,12 @@ const status: ServerStatusStore = {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ServerDatabaseStore {
|
|
||||||
items: ServerDatabase[];
|
|
||||||
setDatabases: Action<ServerDatabaseStore, ServerDatabase[]>;
|
|
||||||
appendDatabase: Action<ServerDatabaseStore, ServerDatabase>;
|
|
||||||
removeDatabase: Action<ServerDatabaseStore, ServerDatabase>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const databases: ServerDatabaseStore = {
|
|
||||||
items: [],
|
|
||||||
setDatabases: action((state, payload) => {
|
|
||||||
state.items = payload;
|
|
||||||
}),
|
|
||||||
appendDatabase: action((state, payload) => {
|
|
||||||
state.items = state.items.filter(item => item.id !== payload.id).concat(payload);
|
|
||||||
}),
|
|
||||||
removeDatabase: action((state, payload) => {
|
|
||||||
state.items = state.items.filter(item => item.id !== payload.id);
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ServerStore {
|
export interface ServerStore {
|
||||||
server: ServerDataStore;
|
server: ServerDataStore;
|
||||||
subusers: ServerSubuserStore;
|
subusers: ServerSubuserStore;
|
||||||
databases: ServerDatabaseStore;
|
databases: ServerDatabaseStore;
|
||||||
files: ServerFileStore;
|
files: ServerFileStore;
|
||||||
|
schedules: ServerScheduleStore;
|
||||||
backups: ServerBackupStore;
|
backups: ServerBackupStore;
|
||||||
socket: SocketStore;
|
socket: SocketStore;
|
||||||
status: ServerStatusStore;
|
status: ServerStatusStore;
|
||||||
|
@ -88,14 +70,16 @@ export const ServerContext = createContextStore<ServerStore>({
|
||||||
files,
|
files,
|
||||||
subusers,
|
subusers,
|
||||||
backups,
|
backups,
|
||||||
|
schedules,
|
||||||
clearServerState: action(state => {
|
clearServerState: action(state => {
|
||||||
state.server.data = undefined;
|
state.server.data = undefined;
|
||||||
state.server.permissions = [];
|
state.server.permissions = [];
|
||||||
state.databases.items = [];
|
state.databases.data = [];
|
||||||
state.subusers.data = [];
|
state.subusers.data = [];
|
||||||
state.files.directory = '/';
|
state.files.directory = '/';
|
||||||
state.files.contents = [];
|
state.files.contents = [];
|
||||||
state.backups.backups = [];
|
state.backups.data = [];
|
||||||
|
state.schedules.data = [];
|
||||||
|
|
||||||
if (state.socket.instance) {
|
if (state.socket.instance) {
|
||||||
state.socket.instance.removeAllListeners();
|
state.socket.instance.removeAllListeners();
|
||||||
|
|
31
resources/scripts/state/server/schedules.ts
Normal file
31
resources/scripts/state/server/schedules.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { action, Action } from 'easy-peasy';
|
||||||
|
import { Schedule } from '@/api/server/schedules/getServerSchedules';
|
||||||
|
|
||||||
|
export interface ServerScheduleStore {
|
||||||
|
data: Schedule[];
|
||||||
|
setSchedules: Action<ServerScheduleStore, Schedule[]>;
|
||||||
|
appendSchedule: Action<ServerScheduleStore, Schedule>;
|
||||||
|
removeSchedule: Action<ServerScheduleStore, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schedules: ServerScheduleStore = {
|
||||||
|
data: [],
|
||||||
|
|
||||||
|
setSchedules: action((state, payload) => {
|
||||||
|
state.data = payload;
|
||||||
|
}),
|
||||||
|
|
||||||
|
appendSchedule: action((state, payload) => {
|
||||||
|
if (state.data.find(schedule => schedule.id === payload.id)) {
|
||||||
|
state.data = state.data.map(schedule => schedule.id === payload.id ? payload : schedule);
|
||||||
|
} else {
|
||||||
|
state.data = [ ...state.data, payload ];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
removeSchedule: action((state, payload) => {
|
||||||
|
state.data = [ ...state.data.filter(schedule => schedule.id !== payload) ];
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default schedules;
|
|
@ -55,7 +55,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
@foreach ($nodes as $node)
|
@foreach ($nodes as $node)
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center text-muted left-icon" data-action="ping" data-secret="{{ $node->daemonSecret }}" data-location="{{ $node->scheme }}://{{ $node->fqdn }}:{{ $node->daemonListen }}/api/system"><i class="fa fa-fw fa-refresh fa-spin"></i></td>
|
<td class="text-center text-muted left-icon" data-action="ping" data-secret="{{ $node->getDecryptedKey() }}" data-location="{{ $node->scheme }}://{{ $node->fqdn }}:{{ $node->daemonListen }}/api/system"><i class="fa fa-fw fa-refresh fa-spin"></i></td>
|
||||||
<td>{!! $node->maintenance_mode ? '<span class="label label-warning"><i class="fa fa-wrench"></i></span> ' : '' !!}<a href="{{ route('admin.nodes.view', $node->id) }}">{{ $node->name }}</a></td>
|
<td>{!! $node->maintenance_mode ? '<span class="label label-warning"><i class="fa fa-wrench"></i></span> ' : '' !!}<a href="{{ route('admin.nodes.view', $node->id) }}">{{ $node->name }}</a></td>
|
||||||
<td>{{ $node->location->short }}</td>
|
<td>{{ $node->location->short }}</td>
|
||||||
<td>{{ $node->memory }} MB</td>
|
<td>{{ $node->memory }} MB</td>
|
||||||
|
|
|
@ -1,13 +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
|
|
||||||
*/
|
|
||||||
Route::get('/packs/pull/{uuid}', 'PackController@pull')->name('daemon.pack.pull');
|
|
||||||
Route::get('/packs/pull/{uuid}/hash', 'PackController@hash')->name('daemon.pack.hash');
|
|
||||||
Route::get('/configure/{token}', 'ActionController@configuration')->name('daemon.configuration');
|
|
||||||
|
|
||||||
Route::post('/install', 'ActionController@markInstall')->name('daemon.install');
|
|
Loading…
Reference in a new issue