diff --git a/CHANGELOG.md b/CHANGELOG.md index 449c129ef..27eca5765 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ This file is a running track of new features and fixes to each version of the pa This project follows [Semantic Versioning](http://semver.org) guidelines. +## v0.7.8 (Derelict Dermodactylus) +### Added +* Nodes can now be put into maintenance mode to deny access to servers temporarily. +* Basic statistics about your panel are now available in the Admin CP. + +### Fixed +* Hitting Ctrl+Z when editing a file on the web now works as expected. +* Logo now links to the correct location on all pages. + +### Changed +* Attempting to upload a folder via the web file manager will now display a warning telling the user to use SFTP. + ## v0.7.7 (Derelict Dermodactylus) ### Fixed * Fixes an issue with the sidebar logo not working correctly in some browsers due to the CSS being assigned. diff --git a/app/Contracts/Repository/NodeRepositoryInterface.php b/app/Contracts/Repository/NodeRepositoryInterface.php index 0ebcbe3a0..c533032c0 100644 --- a/app/Contracts/Repository/NodeRepositoryInterface.php +++ b/app/Contracts/Repository/NodeRepositoryInterface.php @@ -21,6 +21,14 @@ interface NodeRepositoryInterface extends RepositoryInterface, SearchableInterfa */ public function getUsageStats(Node $node): array; + /** + * Return the usage stats for a single node. + * + * @param \Pterodactyl\Models\Node $node + * @return array + */ + public function getUsageStatsRaw(Node $node): array; + /** * Return all available nodes with a searchable interface. * diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php index c6f5b7b15..1a26eed7e 100644 --- a/app/Contracts/Repository/RepositoryInterface.php +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -200,4 +200,11 @@ interface RepositoryInterface * @return bool */ public function insertIgnore(array $values): bool; + + /** + * Get the amount of entries in the database + * + * @return int + */ + public function count(): int; } diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index c949b7609..344fa248c 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -145,4 +145,11 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter * @return bool */ public function isUniqueUuidCombo(string $uuid, string $short): bool; + + /** + * Get the amount of servers that are suspended + * + * @return int + */ + public function getSuspendedServersCount(): int; } diff --git a/app/Http/Controllers/Admin/StatisticsController.php b/app/Http/Controllers/Admin/StatisticsController.php new file mode 100644 index 000000000..2327fd88d --- /dev/null +++ b/app/Http/Controllers/Admin/StatisticsController.php @@ -0,0 +1,101 @@ +allocationRepository = $allocationRepository; + $this->databaseRepository = $databaseRepository; + $this->eggRepository = $eggRepository; + $this->nodeRepository = $nodeRepository; + $this->serverRepository = $serverRepository; + $this->userRepository = $userRepository; + } + + public function index() + { + $servers = $this->serverRepository->all(); + $nodes = $this->nodeRepository->all(); + $usersCount = $this->userRepository->count(); + $eggsCount = $this->eggRepository->count(); + $databasesCount = $this->databaseRepository->count(); + $totalAllocations = $this->allocationRepository->count(); + $suspendedServersCount = $this->serverRepository->getSuspendedServersCount(); + + $totalServerRam = 0; + $totalNodeRam = 0; + $totalServerDisk = 0; + $totalNodeDisk = 0; + foreach ($nodes as $node) { + $stats = $this->nodeRepository->getUsageStatsRaw($node); + $totalServerRam += $stats['memory']['value']; + $totalNodeRam += $stats['memory']['max']; + $totalServerDisk += $stats['disk']['value']; + $totalNodeDisk += $stats['disk']['max']; + } + + $tokens = []; + foreach ($nodes as $node) { + $tokens[$node->id] = $node->daemonSecret; + } + + $this->injectJavascript([ + 'servers' => $servers, + 'suspendedServers' => $suspendedServersCount, + 'totalServerRam' => $totalServerRam, + 'totalNodeRam' => $totalNodeRam, + 'totalServerDisk' => $totalServerDisk, + 'totalNodeDisk' => $totalNodeDisk, + 'nodes' => $nodes, + 'tokens' => $tokens, + ]); + + return view('admin.statistics', [ + 'servers' => $servers, + 'nodes' => $nodes, + 'usersCount' => $usersCount, + 'eggsCount' => $eggsCount, + 'totalServerRam' => $totalServerRam, + 'databasesCount' => $databasesCount, + 'totalNodeRam' => $totalNodeRam, + 'totalNodeDisk' => $totalNodeDisk, + 'totalServerDisk' => $totalServerDisk, + 'totalAllocations' => $totalAllocations, + ]); + } + +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 72158193f..d21c8d3c8 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http; +use Pterodactyl\Http\Middleware\MaintenanceMiddleware; use Pterodactyl\Models\ApiKey; use Illuminate\Auth\Middleware\Authorize; use Illuminate\Auth\Middleware\Authenticate; @@ -108,6 +109,7 @@ class Kernel extends HttpKernel 'can' => Authorize::class, 'bindings' => SubstituteBindings::class, 'recaptcha' => VerifyReCaptcha::class, + 'node.maintenance' => MaintenanceMiddleware::class, // Server specific middleware (used for authenticating access to resources) // diff --git a/app/Http/Middleware/MaintenanceMiddleware.php b/app/Http/Middleware/MaintenanceMiddleware.php new file mode 100644 index 000000000..c67a3f051 --- /dev/null +++ b/app/Http/Middleware/MaintenanceMiddleware.php @@ -0,0 +1,44 @@ +response = $response; + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle($request, Closure $next) + { + /** @var \Pterodactyl\Models\Server $server */ + $server = $request->attributes->get('server'); + $node = $server->getRelation('node'); + + if ($node->maintenance_mode) { + return $this->response->view('errors.maintenance'); + } + + return $next($request); + } +} diff --git a/app/Models/Node.php b/app/Models/Node.php index 26d9eb443..2643d062a 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -48,6 +48,7 @@ class Node extends Model implements CleansAttributes, ValidableContract 'daemonSFTP' => 'integer', 'behind_proxy' => 'boolean', 'public' => 'boolean', + 'maintenance_mode' => 'boolean', ]; /** @@ -62,7 +63,7 @@ class Node extends Model implements CleansAttributes, ValidableContract 'disk_overallocate', 'upload_size', 'daemonSecret', 'daemonBase', 'daemonSFTP', 'daemonListen', - 'description', + 'description', 'maintenance_mode', ]; /** @@ -111,6 +112,7 @@ class Node extends Model implements CleansAttributes, ValidableContract 'daemonBase' => 'regex:/^([\/][\d\w.\-\/]+)$/', 'daemonSFTP' => 'numeric|between:1024,65535', 'daemonListen' => 'numeric|between:1024,65535', + 'maintenance_mode' => 'boolean', ]; /** @@ -126,6 +128,7 @@ class Node extends Model implements CleansAttributes, ValidableContract 'daemonBase' => '/srv/daemon-data', 'daemonSFTP' => 2022, 'daemonListen' => 8080, + 'maintenance_mode' => false, ]; /** diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 3de307d9a..f0e978116 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -33,7 +33,7 @@ class RouteServiceProvider extends ServiceProvider ->namespace($this->namespace . '\Auth') ->group(base_path('routes/auth.php')); - Route::middleware(['web', 'csrf', 'auth', 'server', 'subuser.auth'])->prefix('/server/{server}') + Route::middleware(['web', 'csrf', 'auth', 'server', 'subuser.auth', 'node.maintenance'])->prefix('/server/{server}') ->namespace($this->namespace . '\Server') ->group(base_path('routes/server.php')); diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index 74ec809fe..64e7cfb60 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -296,4 +296,14 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf return $this->getBuilder()->getConnection()->statement($statement, $bindings); } + + /** + * Get the amount of entries in the database + * + * @return int + */ + public function count(): int + { + return $this->getBuilder()->count(); + } } diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index b4d6ba6b0..4f59fddce 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -56,6 +56,33 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa })->toArray(); } + /** + * Return the usage stats for a single node. + * + * @param \Pterodactyl\Models\Node $node + * @return array + */ + public function getUsageStatsRaw(Node $node): array + { + $stats = $this->getBuilder()->select( + $this->getBuilder()->raw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk') + )->join('servers', 'servers.node_id', '=', 'nodes.id')->where('node_id', $node->id)->first(); + + return collect(['disk' => $stats->sum_disk, 'memory' => $stats->sum_memory])->mapWithKeys(function ($value, $key) use ($node) { + $maxUsage = $node->{$key}; + if ($node->{$key . '_overallocate'} > 0) { + $maxUsage = $node->{$key} * (1 + ($node->{$key . '_overallocate'} / 100)); + } + + return [ + $key => [ + 'value' => $value, + 'max' => $maxUsage, + ], + ]; + })->toArray(); + } + /** * Return all available nodes with a searchable interface. * diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index 90c2601ea..f448f0b78 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -328,4 +328,14 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt $this->app->make(SubuserRepository::class)->getBuilder()->select('server_id')->where('user_id', $user) )->pluck('id')->all(); } + + /** + * Get the amount of servers that are suspended + * + * @return int + */ + public function getSuspendedServersCount(): int + { + return $this->getBuilder()->where('suspended', true)->count(); + } } diff --git a/app/Traits/Controllers/PlainJavascriptInjection.php b/app/Traits/Controllers/PlainJavascriptInjection.php new file mode 100644 index 000000000..eae53bfbc --- /dev/null +++ b/app/Traits/Controllers/PlainJavascriptInjection.php @@ -0,0 +1,24 @@ +boolean('maintenance_mode')->after('behind_proxy')->default(false); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('nodes', function (Blueprint $table) { + $table->dropColumn('maintenance_mode'); + }); + } +} diff --git a/public/themes/pterodactyl/css/pterodactyl.css b/public/themes/pterodactyl/css/pterodactyl.css index 9e7e6a822..41f163f3b 100644 --- a/public/themes/pterodactyl/css/pterodactyl.css +++ b/public/themes/pterodactyl/css/pterodactyl.css @@ -473,3 +473,7 @@ label.control-label > span.field-optional:before { height: 42px; width: auto; } + +.number-info-box-content { + padding: 15px 10px 0; +} diff --git a/public/themes/pterodactyl/js/admin/statistics.js b/public/themes/pterodactyl/js/admin/statistics.js new file mode 100644 index 000000000..7433f3221 --- /dev/null +++ b/public/themes/pterodactyl/js/admin/statistics.js @@ -0,0 +1,118 @@ +var freeDisk = Pterodactyl.totalNodeDisk - Pterodactyl.totalServerDisk; +let diskChart = new Chart($('#disk_chart'), { + type: 'pie', + data: { + labels: ['Free Disk', 'Used Disk'], + datasets: [ + { + label: 'Disk (in MB)', + backgroundColor: ['#51B060', '#ff0000'], + data: [freeDisk, Pterodactyl.totalServerDisk] + } + ] + } +}); + +var freeRam = Pterodactyl.totalNodeRam - Pterodactyl.totalServerRam; +let ramChart = new Chart($('#ram_chart'), { + type: 'pie', + data: { + labels: ['Free RAM', 'Used RAM'], + datasets: [ + { + label: 'Memory (in MB)', + backgroundColor: ['#51B060', '#ff0000'], + data: [freeRam, Pterodactyl.totalServerRam] + } + ] + } +}); + +var activeServers = Pterodactyl.servers.length - Pterodactyl.suspendedServers; +let serversChart = new Chart($('#servers_chart'), { + type: 'pie', + data: { + labels: ['Active', 'Suspended'], + datasets: [ + { + label: 'Servers', + backgroundColor: ['#51B060', '#E08E0B'], + data: [activeServers, Pterodactyl.suspendedServers] + } + ] + } +}); + +let statusChart = new Chart($('#status_chart'), { + type: 'pie', + data: { + labels: ['Online', 'Offline', 'Installing', 'Error'], + datasets: [ + { + label: '', + backgroundColor: ['#51B060', '#b7b7b7', '#E08E0B', '#ff0000'], + data: [0,0,0,0] + } + ] + } +}); + +var servers = Pterodactyl.servers; +var nodes = Pterodactyl.nodes; + +for (let i = 0; i < servers.length; i++) { + setTimeout(getStatus, 200 * i, servers[i]); +} + +function getStatus(server) { + var uuid = server.uuid; + var node = getNodeByID(server.node_id); + + $.ajax({ + type: 'GET', + url: node.scheme + '://' + node.fqdn + ':'+node.daemonListen+'/v1/server', + timeout: 5000, + headers: { + 'X-Access-Server': uuid, + 'X-Access-Token': Pterodactyl.tokens[node.id], + } + }).done(function (data) { + + if (typeof data.status === 'undefined') { + // Error + statusChart.data.datasets[0].data[3]++; + return; + } + + switch (data.status) { + case 0: + case 3: + case 30: + // Offline + statusChart.data.datasets[0].data[1]++; + break; + case 1: + case 2: + // Online + statusChart.data.datasets[0].data[0]++; + break; + case 20: + // Installing + statusChart.data.datasets[0].data[2]++; + break; + } + statusChart.update(); + }).fail(function (jqXHR) { + // Error + statusChart.data.datasets[0].data[3]++; + statusChart.update(); + }); +} + +function getNodeByID(id) { + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].id === id) { + return nodes[i]; + } + } +} \ No newline at end of file diff --git a/public/themes/pterodactyl/js/frontend/files/upload.js b/public/themes/pterodactyl/js/frontend/files/upload.js index 2bb1dd8c7..873fbf636 100644 --- a/public/themes/pterodactyl/js/frontend/files/upload.js +++ b/public/themes/pterodactyl/js/frontend/files/upload.js @@ -61,6 +61,18 @@ event.preventDefault(); }, false); + window.foldersDetectedInDrag = function (event) { + var folderDetected = false; + var files = event.dataTransfer.files; + for (var i = 0, f; f = files[i]; i++) { + if (!f.type && f.size === 0) { + return true; + } + } + + return folderDetected; + }; + var dropCounter = 0; $('#load_files').bind({ dragenter: function (event) { @@ -75,6 +87,15 @@ } }, drop: function (event) { + if (window.foldersDetectedInDrag(event.originalEvent)) { + $.notify({ + message: 'Folder uploads are not supported. Please use SFTP to upload whole directories.', + }, { + type: 'warning', + delay: 0 + }); + } + dropCounter = 0; $(this).removeClass('hasFileHover'); } diff --git a/resources/lang/en/base.php b/resources/lang/en/base.php index 2c0f6c62b..01ac79b1e 100644 --- a/resources/lang/en/base.php +++ b/resources/lang/en/base.php @@ -21,6 +21,11 @@ return [ 'header' => 'Server Suspended', 'desc' => 'This server has been suspended and cannot be accessed.', ], + 'maintenance' => [ + 'header' => 'Node Under Maintenance', + 'title' => 'Temporarily Unavailable', + 'desc' => 'This node is under maintenance, therefore your server can temporarily not be accessed.', + ], ], 'index' => [ 'header' => 'Your Servers', diff --git a/resources/lang/en/strings.php b/resources/lang/en/strings.php index 4f1d611ef..ebdd60d23 100644 --- a/resources/lang/en/strings.php +++ b/resources/lang/en/strings.php @@ -74,6 +74,7 @@ return [ 'tasks' => 'Tasks', 'seconds' => 'Seconds', 'minutes' => 'Minutes', + 'under_maintenance' => 'Under Maintenance', 'days' => [ 'sun' => 'Sunday', 'mon' => 'Monday', diff --git a/resources/themes/pterodactyl/admin/nodes/index.blade.php b/resources/themes/pterodactyl/admin/nodes/index.blade.php index abaf25e54..b4ea579a1 100644 --- a/resources/themes/pterodactyl/admin/nodes/index.blade.php +++ b/resources/themes/pterodactyl/admin/nodes/index.blade.php @@ -56,7 +56,7 @@ @foreach ($nodes as $node) - {{ $node->name }} + {!! $node->maintenance_mode ? ' ' : '' !!}{{ $node->name }} {{ $node->location->short }} {{ $node->memory }} MB {{ $node->disk }} MB diff --git a/resources/themes/pterodactyl/admin/nodes/view/index.blade.php b/resources/themes/pterodactyl/admin/nodes/view/index.blade.php index 9f385c9d8..71eb346d2 100644 --- a/resources/themes/pterodactyl/admin/nodes/view/index.blade.php +++ b/resources/themes/pterodactyl/admin/nodes/view/index.blade.php @@ -96,6 +96,17 @@
+ @if($node->maintenance_mode) +
+
+ +
+ This node is under + Maintenance +
+
+
+ @endif
diff --git a/resources/themes/pterodactyl/admin/nodes/view/settings.blade.php b/resources/themes/pterodactyl/admin/nodes/view/settings.blade.php index 7de4582f6..5afd65ed4 100644 --- a/resources/themes/pterodactyl/admin/nodes/view/settings.blade.php +++ b/resources/themes/pterodactyl/admin/nodes/view/settings.blade.php @@ -108,6 +108,20 @@

If you are running the daemon behind a proxy such as Cloudflare, select this to have the daemon skip looking for certificates on boot.

+
+ +
+
+ maintenance_mode) == false) ? 'checked' : '' }}> + +
+
+ maintenance_mode) == true) ? 'checked' : '' }}> + +
+
+

If the node is marked as 'Under Maintenance' users won't be able to access servers that are on this node.

+
diff --git a/resources/themes/pterodactyl/admin/statistics.blade.php b/resources/themes/pterodactyl/admin/statistics.blade.php new file mode 100644 index 000000000..529107bb4 --- /dev/null +++ b/resources/themes/pterodactyl/admin/statistics.blade.php @@ -0,0 +1,141 @@ +@extends('layouts.admin') +@include('partials/admin.settings.nav', ['activeTab' => 'basic']) + +@section('title') + Statistics Overview +@endsection + +@section('content-header') +

Statistics OverviewMonitor your panel usage.

+ +@endsection + +@section('content') +
+
+
+
+ Servers +
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+ Servers + {{ count($servers) }} +
+
+
+ +
+ Total used Memory (in MB) + {{ $totalServerRam }}MB +
+
+
+ +
+ Total used Disk (in MB) + {{ $totalServerDisk }}MB +
+
+
+
+
+
+
+
+ Nodes +
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+ Total RAM + {{ $totalNodeRam }}MB +
+
+
+ +
+ Total Disk Space + {{ $totalNodeDisk }}MB +
+
+
+ +
+ Total Allocations + {{ $totalAllocations }} +
+
+
+
+
+
+
+ +
+ Total Eggs + {{ $eggsCount }} +
+
+
+
+
+ +
+ Total Users + {{ $usersCount }} +
+
+
+
+
+ +
+ Total Nodes + {{ count($nodes) }} +
+
+
+
+
+ +
+ Total Databases + {{ $databasesCount }} +
+
+
+
+@endsection + +@section('footer-scripts') + @parent + {!! Theme::js('vendor/chartjs/chart.min.js') !!} + {!! Theme::js('js/admin/statistics.js') !!} +@endsection \ No newline at end of file diff --git a/resources/themes/pterodactyl/base/index.blade.php b/resources/themes/pterodactyl/base/index.blade.php index 95cce6128..28f4c4d57 100644 --- a/resources/themes/pterodactyl/base/index.blade.php +++ b/resources/themes/pterodactyl/base/index.blade.php @@ -64,9 +64,15 @@ @lang('strings.subuser') @endif - - - + @if($server->node->maintenance_mode) + + @lang('strings.under_maintenance') + + @else + + + + @endif @if (! empty($server->description)) diff --git a/resources/themes/pterodactyl/errors/maintenance.blade.php b/resources/themes/pterodactyl/errors/maintenance.blade.php new file mode 100644 index 000000000..8cc8eea27 --- /dev/null +++ b/resources/themes/pterodactyl/errors/maintenance.blade.php @@ -0,0 +1,30 @@ +{{-- Pterodactyl - Panel --}} +{{-- Copyright (c) 2015 - 2017 Dane Everitt --}} + +{{-- This software is licensed under the terms of the MIT license. --}} +{{-- https://opensource.org/licenses/MIT --}} +@extends('layouts.error') + +@section('title') + @lang('base.errors.maintenance.header') +@endsection + +@section('content-header') +@endsection + +@section('content') +
+
+
+
+

@lang('base.errors.maintenance.title')

+

@lang('base.errors.maintenance.desc')

+
+ +
+
+
+@endsection diff --git a/resources/themes/pterodactyl/layouts/admin.blade.php b/resources/themes/pterodactyl/layouts/admin.blade.php index d67eb6b41..aaf8f4cac 100644 --- a/resources/themes/pterodactyl/layouts/admin.blade.php +++ b/resources/themes/pterodactyl/layouts/admin.blade.php @@ -80,6 +80,11 @@ Overview +
  • + + Statistics + +
  • Settings diff --git a/resources/themes/pterodactyl/layouts/master.blade.php b/resources/themes/pterodactyl/layouts/master.blade.php index c46db68b8..644cb5bca 100644 --- a/resources/themes/pterodactyl/layouts/master.blade.php +++ b/resources/themes/pterodactyl/layouts/master.blade.php @@ -44,7 +44,7 @@