mirror of
https://github.com/pterodactyl/panel.git
synced 2024-11-25 18:42:31 +01:00
Merge branch 'develop' into enhancement/wings-improved-server-loading
This commit is contained in:
commit
94d46affb8
@ -148,8 +148,8 @@ class NodeViewController extends Controller
|
||||
public function servers(Request $request, Node $node)
|
||||
{
|
||||
$this->plainInject([
|
||||
'node' => Collection::wrap($node->makeVisible('daemonSecret'))
|
||||
->only(['scheme', 'fqdn', 'daemonListen', 'daemonSecret']),
|
||||
'node' => Collection::wrap($node->makeVisible(['daemon_token_id', 'daemon_token']))
|
||||
->only(['scheme', 'fqdn', 'daemonListen', 'daemon_token_id', 'daemon_token']),
|
||||
]);
|
||||
|
||||
return $this->view->make('admin.nodes.view.servers', [
|
||||
|
@ -67,7 +67,7 @@ class StatisticsController extends Controller
|
||||
|
||||
$tokens = [];
|
||||
foreach ($nodes as $node) {
|
||||
$tokens[$node->id] = $node->daemonSecret;
|
||||
$tokens[$node->id] = decrypt($node->daemon_token);
|
||||
}
|
||||
|
||||
$this->injectJavascript([
|
||||
|
@ -145,7 +145,7 @@ class ServerTransferController extends Controller
|
||||
->canOnlyBeUsedAfter($now->getTimestamp())
|
||||
->expiresAt($now->addMinutes(15)->getTimestamp())
|
||||
->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
|
||||
// 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 Pterodactyl\Http\Middleware\Api\Client\SubstituteClientApiBindings;
|
||||
use Pterodactyl\Http\Middleware\Api\Application\AuthenticateApplicationUser;
|
||||
use Pterodactyl\Http\Middleware\DaemonAuthenticate as OldDaemonAuthenticate;
|
||||
|
||||
class Kernel extends HttpKernel
|
||||
{
|
||||
@ -107,7 +106,6 @@ class Kernel extends HttpKernel
|
||||
'server' => AccessingValidServer::class,
|
||||
'subuser.auth' => AuthenticateAsSubuser::class,
|
||||
'admin' => AdminAuthenticate::class,
|
||||
'daemon-old' => OldDaemonAuthenticate::class,
|
||||
'csrf' => VerifyCsrfToken::class,
|
||||
'throttle' => ThrottleRequests::class,
|
||||
'can' => Authorize::class,
|
||||
|
@ -4,6 +4,7 @@ namespace Pterodactyl\Http\Middleware\Api\Daemon;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Contracts\Encryption\Encrypter;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
|
||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
||||
@ -25,14 +26,21 @@ class DaemonAuthenticate
|
||||
'daemon.configuration',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Encryption\Encrypter
|
||||
*/
|
||||
private $encrypter;
|
||||
|
||||
/**
|
||||
* DaemonAuthenticate constructor.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
|
||||
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
|
||||
*/
|
||||
public function __construct(NodeRepositoryInterface $repository)
|
||||
public function __construct(Encrypter $encrypter, NodeRepositoryInterface $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->encrypter = $encrypter;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,20 +58,31 @@ class DaemonAuthenticate
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$token = $request->bearerToken();
|
||||
|
||||
if (is_null($token)) {
|
||||
throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']);
|
||||
if (is_null($bearer = $request->bearerToken())) {
|
||||
throw new HttpException(
|
||||
401, 'Access this this endpoint must include an Authorization header.', null, ['WWW-Authenticate' => 'Bearer']
|
||||
);
|
||||
}
|
||||
|
||||
[$identifier, $token] = explode('.', $bearer);
|
||||
|
||||
try {
|
||||
$node = $this->repository->findFirstWhere([['daemonSecret', '=', $token]]);
|
||||
/** @var \Pterodactyl\Models\Node $node */
|
||||
$node = $this->repository->findFirstWhere([
|
||||
'daemon_token_id' => $identifier,
|
||||
]);
|
||||
|
||||
if (hash_equals((string) $this->encrypter->decrypt($node->daemon_token), $token)) {
|
||||
$request->attributes->set('node', $node);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
} catch (RecordNotFoundException $exception) {
|
||||
throw new AccessDeniedHttpException;
|
||||
// Do nothing, we don't want to expose a node not existing at all.
|
||||
}
|
||||
|
||||
$request->attributes->set('node', $node);
|
||||
|
||||
return $next($request);
|
||||
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;
|
||||
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Pterodactyl\Models\Traits\Searchable;
|
||||
use Illuminate\Contracts\Encryption\Encrypter;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $uuid
|
||||
* @property bool $public
|
||||
* @property string $name
|
||||
* @property string $description
|
||||
@ -21,7 +24,8 @@ use Pterodactyl\Models\Traits\Searchable;
|
||||
* @property int $disk
|
||||
* @property int $disk_overallocate
|
||||
* @property int $upload_size
|
||||
* @property string $daemonSecret
|
||||
* @property string $daemon_token_id
|
||||
* @property string $daemon_token
|
||||
* @property int $daemonListen
|
||||
* @property int $daemonSFTP
|
||||
* @property string $daemonBase
|
||||
@ -43,7 +47,8 @@ class Node extends Model
|
||||
*/
|
||||
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.
|
||||
@ -57,7 +62,7 @@ class Node extends Model
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = ['daemonSecret'];
|
||||
protected $hidden = ['daemon_token_id', 'daemon_token'];
|
||||
|
||||
/**
|
||||
* Cast values to correct type.
|
||||
@ -84,8 +89,7 @@ class Node extends Model
|
||||
'public', 'name', 'location_id',
|
||||
'fqdn', 'scheme', 'behind_proxy',
|
||||
'memory', 'memory_overallocate', 'disk',
|
||||
'disk_overallocate', 'upload_size',
|
||||
'daemonSecret', 'daemonBase',
|
||||
'disk_overallocate', 'upload_size', 'daemonBase',
|
||||
'daemonSFTP', 'daemonListen',
|
||||
'description', 'maintenance_mode',
|
||||
];
|
||||
@ -153,12 +157,15 @@ class Node extends Model
|
||||
/**
|
||||
* Returns the configuration as an array.
|
||||
*
|
||||
* @return string
|
||||
* @return array
|
||||
*/
|
||||
private function getConfiguration()
|
||||
public function getConfiguration()
|
||||
{
|
||||
return [
|
||||
'debug' => false,
|
||||
'uuid' => $this->uuid,
|
||||
'token_id' => $this->daemon_token_id,
|
||||
'token' => Container::getInstance()->make(Encrypter::class)->decrypt($this->daemon_token),
|
||||
'api' => [
|
||||
'host' => '0.0.0.0',
|
||||
'port' => $this->daemonListen,
|
||||
@ -202,7 +209,6 @@ class Node extends Model
|
||||
'check_interval' => 100,
|
||||
],
|
||||
'remote' => route('index'),
|
||||
'token' => $this->daemonSecret,
|
||||
];
|
||||
}
|
||||
|
||||
@ -211,17 +217,32 @@ class Node extends Model
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getYamlConfiguration() {
|
||||
public function getYamlConfiguration()
|
||||
{
|
||||
return Yaml::dump($this->getConfiguration(), 4, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns the configuration in JSON format.
|
||||
*
|
||||
* @param bool $pretty
|
||||
* @return string
|
||||
*/
|
||||
public function getJsonConfiguration(bool $pretty = false)
|
||||
{
|
||||
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 getJsonConfiguration(bool $pretty = false) {
|
||||
return json_encode($this->getConfiguration(), $pretty ? JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT : JSON_UNESCAPED_SLASHES);
|
||||
public function getDecryptedKey(): string
|
||||
{
|
||||
return (string) Container::getInstance()->make(Encrypter::class)->decrypt(
|
||||
$this->daemon_token
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,9 +49,5 @@ class RouteServiceProvider extends ServiceProvider
|
||||
Route::middleware(['daemon'])->prefix('/api/remote')
|
||||
->namespace($this->namespace . '\Api\Remote')
|
||||
->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
|
||||
{
|
||||
$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')
|
||||
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
|
||||
->where('nodes.id', $node_id);
|
||||
|
@ -23,4 +23,22 @@ class DaemonConfigurationRepository extends DaemonRepository
|
||||
|
||||
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'),
|
||||
'connect_timeout' => config('pterodactyl.guzzle.connect_timeout'),
|
||||
'headers' => array_merge($headers, [
|
||||
'Authorization' => 'Bearer ' . $this->node->daemonSecret,
|
||||
'Authorization' => 'Bearer ' . $this->node->getDecryptedKey(),
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json',
|
||||
]),
|
||||
|
@ -1,33 +1,34 @@
|
||||
<?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;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Pterodactyl\Models\Node;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
|
||||
|
||||
class NodeCreationService
|
||||
{
|
||||
const DAEMON_SECRET_LENGTH = 36;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Encryption\Encrypter
|
||||
*/
|
||||
private $encrypter;
|
||||
|
||||
/**
|
||||
* CreationService constructor.
|
||||
*
|
||||
* @param \Illuminate\Encryption\Encrypter $encrypter
|
||||
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
|
||||
*/
|
||||
public function __construct(NodeRepositoryInterface $repository)
|
||||
public function __construct(Encrypter $encrypter, NodeRepositoryInterface $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->encrypter = $encrypter;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -40,8 +41,9 @@ class NodeCreationService
|
||||
*/
|
||||
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
|
||||
->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;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Pterodactyl\Models\Node;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Contracts\Encryption\Encrypter;
|
||||
use Pterodactyl\Repositories\Daemon\ConfigurationRepository;
|
||||
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
|
||||
use Pterodactyl\Repositories\Wings\DaemonConfigurationRepository;
|
||||
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException;
|
||||
|
||||
@ -18,31 +21,39 @@ class NodeUpdateService
|
||||
*/
|
||||
private $connection;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface
|
||||
*/
|
||||
private $configRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Repositories\Wings\DaemonConfigurationRepository
|
||||
*/
|
||||
private $configurationRepository;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Encryption\Encrypter
|
||||
*/
|
||||
private $encrypter;
|
||||
|
||||
/**
|
||||
* UpdateService constructor.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public function __construct(
|
||||
ConnectionInterface $connection,
|
||||
ConfigurationRepository $configurationRepository,
|
||||
Encrypter $encrypter,
|
||||
DaemonConfigurationRepository $configurationRepository,
|
||||
NodeRepositoryInterface $repository
|
||||
) {
|
||||
$this->connection = $connection;
|
||||
$this->configRepository = $configurationRepository;
|
||||
$this->repository = $repository;
|
||||
$this->configurationRepository = $configurationRepository;
|
||||
$this->encrypter = $encrypter;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -58,13 +69,14 @@ class NodeUpdateService
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
|
||||
* @throws \Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException
|
||||
*
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
*/
|
||||
public function handle(Node $node, array $data, bool $resetToken = false)
|
||||
{
|
||||
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();
|
||||
@ -77,14 +89,15 @@ class NodeUpdateService
|
||||
// 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
|
||||
// override on the call.
|
||||
$cloned = $updatedModel->replicate(['daemonSecret']);
|
||||
$cloned->setAttribute('daemonSecret', $node->getAttribute('daemonSecret'));
|
||||
$cloned = $updatedModel->replicate(['daemon_token']);
|
||||
$cloned->setAttribute('daemon_token', $node->getAttribute('daemon_token'));
|
||||
|
||||
$this->configRepository->setNode($cloned)->update([
|
||||
'keys' => [$data['daemonSecret']],
|
||||
$this->configurationRepository->setNode($cloned)->update([
|
||||
'daemon_token_id' => $updatedModel->daemon_token_id,
|
||||
'daemon_token' => $updatedModel->getDecryptedKey(),
|
||||
]);
|
||||
} else {
|
||||
$this->configRepository->setNode($updatedModel)->update();
|
||||
$this->configurationRepository->setNode($updatedModel)->update();
|
||||
}
|
||||
|
||||
$this->connection->commit();
|
||||
|
@ -1,7 +1,10 @@
|
||||
<?php
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Cake\Chronos\Chronos;
|
||||
use Illuminate\Support\Str;
|
||||
use Faker\Generator as Faker;
|
||||
use Pterodactyl\Models\Node;
|
||||
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) {
|
||||
return [
|
||||
'id' => $faker->unique()->randomNumber(),
|
||||
'uuid' => Uuid::uuid4()->toString(),
|
||||
'public' => true,
|
||||
'name' => $faker->firstName,
|
||||
'fqdn' => $faker->ipv4,
|
||||
@ -90,10 +94,11 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker $faker) {
|
||||
'disk' => 10240,
|
||||
'disk_overallocate' => 0,
|
||||
'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,
|
||||
'daemonSFTP' => 2022,
|
||||
'daemonBase' => '/srv/daemon',
|
||||
'daemonBase' => '/srv/daemon-data',
|
||||
];
|
||||
});
|
||||
|
||||
|
@ -18,15 +18,21 @@ class MergePermissionsTableIntoSubusers extends Migration
|
||||
$table->json('permissions')->nullable()->after('server_id');
|
||||
});
|
||||
|
||||
DB::statement('
|
||||
UPDATE subusers as s
|
||||
LEFT JOIN (
|
||||
SELECT subuser_id, JSON_ARRAYAGG(permission) as permissions
|
||||
FROM permissions
|
||||
GROUP BY subuser_id
|
||||
) as p ON p.subuser_id = s.id
|
||||
SET s.permissions = p.permissions
|
||||
');
|
||||
$cursor = DB::table('permissions')
|
||||
->select(['subuser_id'])
|
||||
->selectRaw('GROUP_CONCAT(permission) as permissions')
|
||||
->from('permissions')
|
||||
->groupBy(['subuser_id'])
|
||||
->cursor();
|
||||
|
||||
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()
|
||||
{
|
||||
Schema::dropIfExists('server_transfers');
|
||||
|
||||
Schema::create('server_transfers', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('server_id')->unsigned();
|
||||
$table->tinyInteger('successful')->unsigned()->default(0);
|
||||
$table->integer('old_node')->unsigned();
|
||||
$table->integer('new_node')->unsigned();
|
||||
$table->integer('old_allocation')->unsigned();
|
||||
@ -25,10 +24,8 @@ class AddTableServerTransfers extends Migration
|
||||
$table->string('old_additional_allocations')->nullable();
|
||||
$table->string('new_additional_allocations')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::table('server_transfers', function (Blueprint $table) {
|
||||
$table->foreign('server_id')->references('id')->on('servers');
|
||||
$table->foreign('server_id')->references('id')->on('servers')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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 { store } from '@/state';
|
||||
|
||||
const http: AxiosInstance = axios.create({
|
||||
timeout: 20000,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'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
|
||||
// ahead and route the response data through to it so things show up.
|
||||
// @ts-ignore
|
||||
|
@ -9,6 +9,7 @@ import AuthenticationRouter from '@/routers/AuthenticationRouter';
|
||||
import { Provider } from 'react-redux';
|
||||
import { SiteSettings } from '@/state/settings';
|
||||
import { DefaultTheme, ThemeProvider } from 'styled-components';
|
||||
import ProgressBar from '@/components/elements/ProgressBar';
|
||||
|
||||
interface ExtendedWindow extends Window {
|
||||
SiteConfiguration?: SiteSettings;
|
||||
@ -57,6 +58,7 @@ const App = () => {
|
||||
<ThemeProvider theme={theme}>
|
||||
<StoreProvider store={store}>
|
||||
<Provider store={store}>
|
||||
<ProgressBar/>
|
||||
<div className={'mx-auto w-auto'}>
|
||||
<BrowserRouter basename={'/'} key={'root-router'}>
|
||||
<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 { faServer } from '@fortawesome/free-solid-svg-icons/faServer';
|
||||
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 }) => {
|
||||
const interval = useRef<number>(null);
|
||||
const [ stats, setStats ] = useState<ServerStats | null>(null);
|
||||
|
||||
const getStats = () => getServerResourceUsage(server.uuid).then(data => setStats(data));
|
||||
|
||||
useEffect(() => {
|
||||
let interval: any = null;
|
||||
getStats().then(() => {
|
||||
interval = setInterval(() => getStats(), 20000);
|
||||
// @ts-ignore
|
||||
interval.current = setInterval(() => getStats(), 20000);
|
||||
});
|
||||
|
||||
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 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 useFlash from '@/plugins/useFlash';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
@ -9,7 +9,6 @@ import CreateBackupButton from '@/components/server/backups/CreateBackupButton';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import BackupRow from '@/components/server/backups/BackupRow';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import ListRefreshIndicator from '@/components/elements/ListRefreshIndicator';
|
||||
|
||||
export default () => {
|
||||
const { uuid } = useServer();
|
||||
@ -36,7 +35,6 @@ export default () => {
|
||||
|
||||
return (
|
||||
<div className={'mt-10 mb-6'}>
|
||||
<ListRefreshIndicator visible={loading}/>
|
||||
<FlashMessageRender byKey={'backups'} className={'mb-4'}/>
|
||||
{!backups.length ?
|
||||
<p className="text-center text-sm text-neutral-400">
|
||||
|
@ -1,15 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ServerDatabase } from '@/api/server/getServerDatabases';
|
||||
import Modal from '@/components/elements/Modal';
|
||||
import { Form, Formik, FormikHelpers } from 'formik';
|
||||
import Field from '@/components/elements/Field';
|
||||
import { object, string } from 'yup';
|
||||
import createServerDatabase from '@/api/server/createServerDatabase';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import useServer from '@/plugins/useServer';
|
||||
|
||||
interface Values {
|
||||
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.'),
|
||||
});
|
||||
|
||||
export default ({ onCreated }: { onCreated: (database: ServerDatabase) => void }) => {
|
||||
export default () => {
|
||||
const { uuid } = useServer();
|
||||
const { addError, clearFlashes } = useFlash();
|
||||
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>) => {
|
||||
clearFlashes();
|
||||
createServerDatabase(server.uuid, { ...values })
|
||||
clearFlashes('database:create');
|
||||
createServerDatabase(uuid, { ...values })
|
||||
.then(database => {
|
||||
onCreated(database);
|
||||
appendDatabase(database);
|
||||
setVisible(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
addFlash({
|
||||
key: 'create-database-modal',
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: httpErrorToHuman(error),
|
||||
});
|
||||
})
|
||||
.then(() => setSubmitting(false));
|
||||
addError({ key: 'database:create', message: httpErrorToHuman(error) });
|
||||
setSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@ -69,7 +65,7 @@ export default ({ onCreated }: { onCreated: (database: ServerDatabase) => void }
|
||||
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>
|
||||
<Form className={'m-0'}>
|
||||
<Field
|
||||
@ -105,7 +101,7 @@ export default ({ onCreated }: { onCreated: (database: ServerDatabase) => void }
|
||||
)
|
||||
}
|
||||
</Formik>
|
||||
<button className={'btn btn-primary btn-lg'} onClick={() => setVisible(true)}>
|
||||
<button className={'btn btn-primary btn-sm'} onClick={() => setVisible(true)}>
|
||||
New Database
|
||||
</button>
|
||||
</React.Fragment>
|
||||
|
@ -9,31 +9,28 @@ import { Form, Formik, FormikHelpers } from 'formik';
|
||||
import Field from '@/components/elements/Field';
|
||||
import { object, string } from 'yup';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import deleteServerDatabase from '@/api/server/deleteServerDatabase';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import RotatePasswordButton from '@/components/server/databases/RotatePasswordButton';
|
||||
import Can from '@/components/elements/Can';
|
||||
import { ServerDatabase } from '@/api/server/getServerDatabases';
|
||||
import useServer from '@/plugins/useServer';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
|
||||
interface Props {
|
||||
databaseId: string | number;
|
||||
database: ServerDatabase;
|
||||
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 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 { addFlash, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
const server = ServerContext.useStoreState(state => state.server.data!);
|
||||
|
||||
if (!database) {
|
||||
return null;
|
||||
}
|
||||
const appendDatabase = ServerContext.useStoreActions(actions => actions.databases.appendDatabase);
|
||||
const removeDatabase = ServerContext.useStoreActions(actions => actions.databases.removeDatabase);
|
||||
|
||||
const schema = object().shape({
|
||||
confirm: string()
|
||||
@ -43,20 +40,15 @@ export default ({ databaseId, className, onDelete }: Props) => {
|
||||
|
||||
const submit = (values: { confirm: string }, { setSubmitting }: FormikHelpers<{ confirm: string }>) => {
|
||||
clearFlashes();
|
||||
deleteServerDatabase(server.uuid, database.id)
|
||||
deleteServerDatabase(uuid, database.id)
|
||||
.then(() => {
|
||||
setVisible(false);
|
||||
setTimeout(() => onDelete(), 150);
|
||||
setTimeout(() => removeDatabase(database.id), 150);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
setSubmitting(false);
|
||||
addFlash({
|
||||
key: 'delete-database-modal',
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: httpErrorToHuman(error),
|
||||
});
|
||||
addError({ key: 'database:delete', message: httpErrorToHuman(error) });
|
||||
});
|
||||
};
|
||||
|
||||
@ -78,7 +70,7 @@ export default ({ databaseId, className, onDelete }: Props) => {
|
||||
resetForm();
|
||||
}}
|
||||
>
|
||||
<FlashMessageRender byKey={'delete-database-modal'} className={'mb-6'}/>
|
||||
<FlashMessageRender byKey={'database:delete'} className={'mb-6'}/>
|
||||
<h3 className={'mb-6'}>Confirm database deletion</h3>
|
||||
<p className={'text-sm'}>
|
||||
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 getServerDatabases from '@/api/server/getServerDatabases';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import DatabaseRow from '@/components/server/databases/DatabaseRow';
|
||||
@ -10,35 +8,34 @@ import Spinner from '@/components/elements/Spinner';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import CreateDatabaseButton from '@/components/server/databases/CreateDatabaseButton';
|
||||
import Can from '@/components/elements/Can';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import useServer from '@/plugins/useServer';
|
||||
|
||||
export default () => {
|
||||
const { uuid, featureLimits } = useServer();
|
||||
const { addError, clearFlashes } = useFlash();
|
||||
const [ loading, setLoading ] = useState(true);
|
||||
const server = ServerContext.useStoreState(state => state.server.data!);
|
||||
const databases = ServerContext.useStoreState(state => state.databases.items);
|
||||
const { setDatabases, appendDatabase, removeDatabase } = ServerContext.useStoreActions(state => state.databases);
|
||||
const { addFlash, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
const databases = ServerContext.useStoreState(state => state.databases.data);
|
||||
const setDatabases = ServerContext.useStoreActions(state => state.databases.setDatabases);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(!databases.length);
|
||||
clearFlashes('databases');
|
||||
|
||||
getServerDatabases(server.uuid)
|
||||
.then(databases => {
|
||||
setDatabases(databases);
|
||||
setLoading(false);
|
||||
getServerDatabases(uuid)
|
||||
.then(databases => setDatabases(databases))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
addError({ key: 'databases', message: httpErrorToHuman(error) });
|
||||
})
|
||||
.catch(error => addFlash({
|
||||
key: 'databases',
|
||||
title: 'Error',
|
||||
message: httpErrorToHuman(error),
|
||||
type: 'error',
|
||||
}));
|
||||
.then(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={'my-10 mb-6'}>
|
||||
<FlashMessageRender byKey={'databases'}/>
|
||||
{loading ?
|
||||
<FlashMessageRender byKey={'databases'} className={'mb-4'}/>
|
||||
{(!databases.length && loading) ?
|
||||
<Spinner size={'large'} centered={true}/>
|
||||
:
|
||||
<CSSTransition classNames={'fade'} timeout={250}>
|
||||
@ -47,14 +44,13 @@ export default () => {
|
||||
databases.map((database, index) => (
|
||||
<DatabaseRow
|
||||
key={database.id}
|
||||
databaseId={database.id}
|
||||
onDelete={() => removeDatabase(database)}
|
||||
database={database}
|
||||
className={index > 0 ? 'mt-1' : undefined}
|
||||
/>
|
||||
))
|
||||
:
|
||||
<p className={'text-center text-sm text-neutral-400'}>
|
||||
{server.featureLimits.databases > 0 ?
|
||||
{featureLimits.databases > 0 ?
|
||||
`It looks like you have no databases.`
|
||||
:
|
||||
`Databases cannot be created for this server.`
|
||||
@ -62,9 +58,9 @@ export default () => {
|
||||
</p>
|
||||
}
|
||||
<Can action={'database.create'}>
|
||||
{server.featureLimits.databases > 0 &&
|
||||
{featureLimits.databases > 0 &&
|
||||
<div className={'mt-6 flex justify-end'}>
|
||||
<CreateDatabaseButton onCreated={appendDatabase}/>
|
||||
<CreateDatabaseButton/>
|
||||
</div>
|
||||
}
|
||||
</Can>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { NavLink, useParams } from 'react-router-dom';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { cleanDirectoryPath } from '@/helpers';
|
||||
|
||||
interface Props {
|
||||
withinFileEditor?: boolean;
|
||||
@ -8,21 +9,17 @@ interface Props {
|
||||
}
|
||||
|
||||
export default ({ withinFileEditor, isNewFile }: Props) => {
|
||||
const { action } = useParams();
|
||||
const [ file, setFile ] = useState<string | null>(null);
|
||||
const id = ServerContext.useStoreState(state => state.server.data!.id);
|
||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
|
||||
|
||||
useEffect(() => {
|
||||
const parts = window.location.hash.replace(/^#(\/)*/, '/').split('/');
|
||||
const parts = cleanDirectoryPath(window.location.hash).split('/');
|
||||
|
||||
if (withinFileEditor && !isNewFile) {
|
||||
setFile(parts.pop() || null);
|
||||
}
|
||||
|
||||
setDirectory(parts.join('/'));
|
||||
}, [ withinFileEditor, isNewFile, setDirectory ]);
|
||||
}, [ withinFileEditor, isNewFile ]);
|
||||
|
||||
const breadcrumbs = (): { name: string; path?: string }[] => directory.split('/')
|
||||
.filter(directory => !!directory)
|
||||
@ -39,7 +36,6 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
|
||||
/<span className={'px-1 text-neutral-300'}>home</span>/
|
||||
<NavLink
|
||||
to={`/server/${id}/files`}
|
||||
onClick={() => setDirectory('/')}
|
||||
className={'px-1 text-neutral-200 no-underline hover:text-neutral-100'}
|
||||
>
|
||||
container
|
||||
@ -50,7 +46,6 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
|
||||
<React.Fragment key={index}>
|
||||
<NavLink
|
||||
to={`/server/${id}/files#${crumb.path}`}
|
||||
onClick={() => setDirectory(crumb.path!)}
|
||||
className={'px-1 text-neutral-200 no-underline hover:text-neutral-100'}
|
||||
>
|
||||
{crumb.name}
|
||||
|
@ -22,20 +22,20 @@ export default () => {
|
||||
const [ loading, setLoading ] = useState(true);
|
||||
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
clearFlashes();
|
||||
|
||||
getDirectoryContents(window.location.hash.replace(/^#(\/)*/, '/'))
|
||||
getDirectoryContents(window.location.hash)
|
||||
.then(() => setLoading(false))
|
||||
.catch(error => {
|
||||
console.error(error.message, { error });
|
||||
addError({ message: httpErrorToHuman(error), key: 'files' });
|
||||
});
|
||||
}, [ directory ]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<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 { faFileAlt } from '@fortawesome/free-solid-svg-icons/faFileAlt';
|
||||
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 format from 'date-fns/format';
|
||||
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 }) => {
|
||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
|
||||
const { match } = useRouter();
|
||||
const { match, history } = useRouter();
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -27,7 +27,7 @@ export default ({ file }: { file: FileObject }) => {
|
||||
`}
|
||||
>
|
||||
<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'}
|
||||
onClick={e => {
|
||||
// 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) {
|
||||
e.preventDefault();
|
||||
|
||||
window.location.hash = `#${directory}/${file.name}`;
|
||||
history.push(`#${cleanDirectoryPath(`${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 createOrUpdateSchedule from '@/api/server/schedules/createOrUpdateSchedule';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import useServer from '@/plugins/useServer';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
|
||||
type Props = {
|
||||
schedule?: Schedule;
|
||||
onScheduleUpdated: (schedule: Schedule) => void;
|
||||
} & RequiredModalProps;
|
||||
|
||||
interface Values {
|
||||
@ -73,15 +72,17 @@ const EditScheduleModal = ({ schedule, ...props }: Omit<Props, 'onScheduleUpdate
|
||||
);
|
||||
};
|
||||
|
||||
export default ({ schedule, onScheduleUpdated, visible, ...props }: Props) => {
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
export default ({ schedule, visible, ...props }: Props) => {
|
||||
const { uuid } = useServer();
|
||||
const { addError, clearFlashes } = useFlash();
|
||||
const [ modalVisible, setModalVisible ] = useState(visible);
|
||||
|
||||
const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule);
|
||||
|
||||
useEffect(() => {
|
||||
setModalVisible(visible);
|
||||
clearFlashes('schedule:edit');
|
||||
}, [visible]);
|
||||
}, [ visible ]);
|
||||
|
||||
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
clearFlashes('schedule:edit');
|
||||
@ -98,7 +99,7 @@ export default ({ schedule, onScheduleUpdated, visible, ...props }: Props) => {
|
||||
})
|
||||
.then(schedule => {
|
||||
setSubmitting(false);
|
||||
onScheduleUpdated(schedule);
|
||||
appendSchedule(schedule);
|
||||
setModalVisible(false);
|
||||
})
|
||||
.catch(error => {
|
||||
|
@ -1,25 +1,21 @@
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
scheduleId: number;
|
||||
onTaskAdded: (task: Task) => void;
|
||||
schedule: Schedule;
|
||||
}
|
||||
|
||||
export default ({ scheduleId, onTaskAdded }: Props) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
export default ({ schedule }: Props) => {
|
||||
const [ visible, setVisible ] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{visible &&
|
||||
<TaskDetailsModal
|
||||
scheduleId={scheduleId}
|
||||
onDismissed={task => {
|
||||
task && onTaskAdded(task);
|
||||
setVisible(false);
|
||||
}}
|
||||
/>
|
||||
<TaskDetailsModal
|
||||
schedule={schedule}
|
||||
onDismissed={() => setVisible(false)}
|
||||
/>
|
||||
}
|
||||
<button className={'btn btn-primary btn-sm'} onClick={() => setVisible(true)}>
|
||||
New Task
|
||||
|
@ -1,36 +1,40 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import getServerSchedules, { Schedule } from '@/api/server/schedules/getServerSchedules';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import getServerSchedules from '@/api/server/schedules/getServerSchedules';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import ScheduleRow from '@/components/server/schedules/ScheduleRow';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import EditScheduleModal from '@/components/server/schedules/EditScheduleModal';
|
||||
import Can from '@/components/elements/Can';
|
||||
import useServer from '@/plugins/useServer';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
|
||||
export default ({ match, history }: RouteComponentProps) => {
|
||||
const { uuid } = ServerContext.useStoreState(state => state.server.data!);
|
||||
const [ schedules, setSchedules ] = useState<Schedule[] | null>(null);
|
||||
const { uuid } = useServer();
|
||||
const { clearFlashes, addError } = useFlash();
|
||||
const [ loading, setLoading ] = useState(true);
|
||||
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');
|
||||
getServerSchedules(uuid)
|
||||
.then(schedules => setSchedules(schedules))
|
||||
.catch(error => {
|
||||
addError({ message: httpErrorToHuman(error), key: 'schedules' });
|
||||
console.error(error);
|
||||
});
|
||||
}, [ setSchedules ]);
|
||||
})
|
||||
.then(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={'my-10 mb-6'}>
|
||||
<FlashMessageRender byKey={'schedules'} className={'mb-4'}/>
|
||||
{!schedules ?
|
||||
{(!schedules.length && loading) ?
|
||||
<Spinner size={'large'} centered={true}/>
|
||||
:
|
||||
<>
|
||||
@ -59,7 +63,6 @@ export default ({ match, history }: RouteComponentProps) => {
|
||||
{visible && <EditScheduleModal
|
||||
appear={true}
|
||||
visible={true}
|
||||
onScheduleUpdated={schedule => setSchedules(s => [ ...(s || []), schedule ])}
|
||||
onDismissed={() => setVisible(false)}
|
||||
/>}
|
||||
<button
|
||||
|
@ -2,11 +2,8 @@ import React, { useEffect, useState } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { Schedule } from '@/api/server/schedules/getServerSchedules';
|
||||
import getServerSchedule from '@/api/server/schedules/getServerSchedule';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import ScheduleRow from '@/components/server/schedules/ScheduleRow';
|
||||
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 DeleteScheduleButton from '@/components/server/schedules/DeleteScheduleButton';
|
||||
import Can from '@/components/elements/Can';
|
||||
import useServer from '@/plugins/useServer';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
interface Params {
|
||||
id: string;
|
||||
@ -24,11 +24,13 @@ interface 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 [ 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(() => {
|
||||
if (schedule?.id === Number(match.params.id)) {
|
||||
@ -38,13 +40,13 @@ export default ({ match, history, location: { state } }: RouteComponentProps<Par
|
||||
|
||||
clearFlashes('schedules');
|
||||
getServerSchedule(uuid, Number(match.params.id))
|
||||
.then(schedule => setSchedule(schedule))
|
||||
.then(schedule => appendSchedule(schedule))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
addError({ message: httpErrorToHuman(error), key: 'schedules' });
|
||||
})
|
||||
.then(() => setIsLoading(false));
|
||||
}, [ schedule, match ]);
|
||||
}, [ match ]);
|
||||
|
||||
return (
|
||||
<div className={'my-10 mb-6'}>
|
||||
@ -59,7 +61,6 @@ export default ({ match, history, location: { state } }: RouteComponentProps<Par
|
||||
<EditScheduleModal
|
||||
visible={showEditModal}
|
||||
schedule={schedule}
|
||||
onScheduleUpdated={schedule => setSchedule(schedule)}
|
||||
onDismissed={() => setShowEditModal(false)}
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
{schedule?.tasks.length > 0 ?
|
||||
{schedule.tasks.length > 0 ?
|
||||
<>
|
||||
{
|
||||
schedule.tasks
|
||||
.sort((a, b) => a.sequenceId - b.sequenceId)
|
||||
.map(task => (
|
||||
<ScheduleTaskRow
|
||||
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),
|
||||
}))}
|
||||
/>
|
||||
<ScheduleTaskRow key={task.id} task={task} schedule={schedule}/>
|
||||
))
|
||||
}
|
||||
{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)}>
|
||||
Edit
|
||||
</button>
|
||||
<NewTaskButton
|
||||
scheduleId={schedule.id}
|
||||
onTaskAdded={task => setSchedule(s => ({
|
||||
...s!, tasks: [ ...s!.tasks, task ],
|
||||
}))}
|
||||
/>
|
||||
<NewTaskButton schedule={schedule}/>
|
||||
</Can>
|
||||
</div>
|
||||
</>
|
||||
|
@ -1,39 +1,41 @@
|
||||
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 { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt';
|
||||
import { faCode } from '@fortawesome/free-solid-svg-icons/faCode';
|
||||
import { faToggleOn } from '@fortawesome/free-solid-svg-icons/faToggleOn';
|
||||
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 { httpErrorToHuman } from '@/api/http';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import TaskDetailsModal from '@/components/server/schedules/TaskDetailsModal';
|
||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons/faPencilAlt';
|
||||
import Can from '@/components/elements/Can';
|
||||
import useServer from '@/plugins/useServer';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
interface Props {
|
||||
schedule: number;
|
||||
schedule: Schedule;
|
||||
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 [ isLoading, setIsLoading ] = useState(false);
|
||||
const [ isEditing, setIsEditing ] = useState(false);
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const { clearFlashes, addError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule);
|
||||
|
||||
const onConfirmDeletion = () => {
|
||||
setIsLoading(true);
|
||||
clearFlashes('schedules');
|
||||
deleteScheduleTask(uuid, schedule, task.id)
|
||||
.then(() => onTaskRemoved())
|
||||
deleteScheduleTask(uuid, schedule.id, task.id)
|
||||
.then(() => appendSchedule({
|
||||
...schedule,
|
||||
tasks: schedule.tasks.filter(t => t.id !== task.id),
|
||||
}))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
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'}>
|
||||
<SpinnerOverlay visible={isLoading} fixed={true} size={'large'}/>
|
||||
{isEditing && <TaskDetailsModal
|
||||
scheduleId={schedule}
|
||||
schedule={schedule}
|
||||
task={task}
|
||||
onDismissed={task => {
|
||||
task && onTaskUpdated(task);
|
||||
setIsEditing(false);
|
||||
}}
|
||||
onDismissed={() => setIsEditing(false)}
|
||||
/>}
|
||||
<ConfirmTaskDeletionModal
|
||||
visible={visible}
|
||||
|
@ -1,22 +1,22 @@
|
||||
import React, { useEffect } from 'react';
|
||||
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 { ServerContext } from '@/state/server';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import createOrUpdateScheduleTask from '@/api/server/schedules/createOrUpdateScheduleTask';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import Field from '@/components/elements/Field';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { number, object, string } from 'yup';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import useServer from '@/plugins/useServer';
|
||||
|
||||
interface Props {
|
||||
scheduleId: number;
|
||||
schedule: Schedule;
|
||||
// If a task is provided we can assume we're editing it. If not provided,
|
||||
// we are creating a new one.
|
||||
task?: Task;
|
||||
onDismissed: (task: Task | undefined | void) => void;
|
||||
onDismissed: () => void;
|
||||
}
|
||||
|
||||
interface Values {
|
||||
@ -29,9 +29,11 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
|
||||
const { values: { action }, setFieldValue, setFieldTouched } = useFormikContext<Values>();
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue('payload', '');
|
||||
setFieldTouched('payload', false);
|
||||
}, [action]);
|
||||
return () => {
|
||||
setFieldValue('payload', '');
|
||||
setFieldTouched('payload', false);
|
||||
};
|
||||
}, [ action ]);
|
||||
|
||||
return (
|
||||
<Form className={'m-0'}>
|
||||
@ -80,9 +82,10 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ({ task, scheduleId, onDismissed }: Props) => {
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const { clearFlashes, addError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
export default ({ task, schedule, onDismissed }: Props) => {
|
||||
const { uuid } = useServer();
|
||||
const { clearFlashes, addError } = useFlash();
|
||||
const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule);
|
||||
|
||||
useEffect(() => {
|
||||
clearFlashes('schedule:task');
|
||||
@ -90,8 +93,16 @@ export default ({ task, scheduleId, onDismissed }: Props) => {
|
||||
|
||||
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
clearFlashes('schedule:task');
|
||||
createOrUpdateScheduleTask(uuid, scheduleId, task?.id, values)
|
||||
.then(task => onDismissed(task))
|
||||
createOrUpdateScheduleTask(uuid, schedule.id, task?.id, values)
|
||||
.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 => {
|
||||
console.error(error);
|
||||
setSubmitting(false);
|
||||
|
@ -21,10 +21,6 @@ export default () => {
|
||||
const getPermissions = useStoreActions((actions: Actions<ApplicationStore>) => actions.permissions.getPermissions);
|
||||
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
useEffect(() => {
|
||||
getPermissions().catch(error => console.error(error));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
clearFlashes('users');
|
||||
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}/>;
|
||||
}
|
||||
|
||||
|
8
resources/scripts/easy-peasy.d.ts
vendored
8
resources/scripts/easy-peasy.d.ts
vendored
@ -1,9 +1,13 @@
|
||||
// noinspection ES6UnusedImports
|
||||
import EasyPeasy from 'easy-peasy';
|
||||
import EasyPeasy, { Actions, State } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
|
||||
declare module 'easy-peasy' {
|
||||
export function useStoreState<Result>(
|
||||
mapState: (state: ApplicationStore) => Result,
|
||||
mapState: (state: State<ApplicationStore>) => Result,
|
||||
): Result;
|
||||
|
||||
export function useStoreActions<Result>(
|
||||
mapActions: (actions: Actions<ApplicationStore>) => Result,
|
||||
): Result;
|
||||
}
|
||||
|
@ -9,3 +9,7 @@ export function bytesToHuman (bytes: number): string {
|
||||
}
|
||||
|
||||
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'}/>
|
||||
</div>
|
||||
:
|
||||
<Switch location={location} key={'server-switch'}>
|
||||
<Switch location={location}>
|
||||
<Route path={`${match.path}`} component={ServerConsole} exact/>
|
||||
<Route path={`${match.path}/files`} component={FileManagerContainer} exact/>
|
||||
<Route
|
||||
|
@ -3,12 +3,14 @@ import flashes, { FlashStore } from '@/state/flashes';
|
||||
import user, { UserStore } from '@/state/user';
|
||||
import permissions, { GloablPermissionsStore } from '@/state/permissions';
|
||||
import settings, { SettingsStore } from '@/state/settings';
|
||||
import progress, { ProgressStore } from '@/state/progress';
|
||||
|
||||
export interface ApplicationStore {
|
||||
permissions: GloablPermissionsStore;
|
||||
flashes: FlashStore;
|
||||
user: UserStore;
|
||||
settings: SettingsStore;
|
||||
progress: ProgressStore;
|
||||
}
|
||||
|
||||
const state: ApplicationStore = {
|
||||
@ -16,6 +18,7 @@ const state: ApplicationStore = {
|
||||
flashes,
|
||||
user,
|
||||
settings,
|
||||
progress,
|
||||
};
|
||||
|
||||
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 { action, Action, thunk, Thunk } from 'easy-peasy';
|
||||
import { ServerStore } from '@/state/server/index';
|
||||
import { cleanDirectoryPath } from '@/helpers';
|
||||
|
||||
export interface ServerFileStore {
|
||||
directory: string;
|
||||
@ -22,7 +23,7 @@ const files: ServerFileStore = {
|
||||
return;
|
||||
}
|
||||
|
||||
const contents = await loadDirectory(server.uuid, payload);
|
||||
const contents = await loadDirectory(server.uuid, cleanDirectoryPath(payload));
|
||||
|
||||
actions.setDirectory(payload.length === 0 ? '/' : payload);
|
||||
actions.setContents(contents);
|
||||
@ -47,7 +48,7 @@ const files: ServerFileStore = {
|
||||
}),
|
||||
|
||||
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 { action, Action, createContextStore, thunk, Thunk } from 'easy-peasy';
|
||||
import socket, { SocketStore } from './socket';
|
||||
import { ServerDatabase } from '@/api/server/getServerDatabases';
|
||||
import files, { ServerFileStore } from '@/state/server/files';
|
||||
import subusers, { ServerSubuserStore } from '@/state/server/subusers';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||
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';
|
||||
|
||||
@ -22,7 +23,7 @@ const server: ServerDataStore = {
|
||||
permissions: [],
|
||||
|
||||
getServer: thunk(async (actions, payload) => {
|
||||
const [server, permissions] = await getServer(payload);
|
||||
const [ server, permissions ] = await getServer(payload);
|
||||
|
||||
actions.setServer(server);
|
||||
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 {
|
||||
server: ServerDataStore;
|
||||
subusers: ServerSubuserStore;
|
||||
databases: ServerDatabaseStore;
|
||||
files: ServerFileStore;
|
||||
schedules: ServerScheduleStore;
|
||||
backups: ServerBackupStore;
|
||||
socket: SocketStore;
|
||||
status: ServerStatusStore;
|
||||
@ -88,14 +70,16 @@ export const ServerContext = createContextStore<ServerStore>({
|
||||
files,
|
||||
subusers,
|
||||
backups,
|
||||
schedules,
|
||||
clearServerState: action(state => {
|
||||
state.server.data = undefined;
|
||||
state.server.permissions = [];
|
||||
state.databases.items = [];
|
||||
state.databases.data = [];
|
||||
state.subusers.data = [];
|
||||
state.files.directory = '/';
|
||||
state.files.contents = [];
|
||||
state.backups.backups = [];
|
||||
state.backups.data = [];
|
||||
state.schedules.data = [];
|
||||
|
||||
if (state.socket.instance) {
|
||||
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>
|
||||
@foreach ($nodes as $node)
|
||||
<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->location->short }}</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 New Issue
Block a user