Merge branch 'release/v0.7.8'

This commit is contained in:
Dane Everitt 2018-07-02 21:00:16 -07:00
commit 28442cead3
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
70 changed files with 1463 additions and 498 deletions

View file

@ -1,26 +0,0 @@
<!---
Please take a little time to submit a good issue. It makes our life easier and the issue will be resolved quicker.
!!! GitHub is NOT the place for difficulties setting up this software. Please use it for bugs and feature requests only. If you have issues setting up the panel or the daemon visit our Discord server: https://pterodactyl.io/discord
If you are submitting a feature request please remove everything in here. Then give a detailed explanation what you want to have implemented and why that would be a good addition.
Please also try to give the issue a good title: It should summarize your issue in a few words and help us see what the issue is about in a glance. Things like "Panel is not working" do not help.
--- You can delete everything above this line. --->
<!--- Please fill in the following basic information --->
* Panel or Daemon:
* Version of Panel/Daemon:
* Server's OS:
* Your Computer's OS & Browser:
------------------------
<!---
Please provide as much information about your issue as needed. Include precise steps to reproduce the issue and provide logs of the components that didn't work as expected.
Please provide additional information, depending on what you have issues with:
Panel: `php -v` (the php version in use).
Daemon: `uname -a` and `docker info` (your kernel version and information regarding docker)
--->

27
.github/ISSUE_TEMPLATE/---bug-report.md vendored Normal file
View file

@ -0,0 +1,27 @@
---
name: "\U0001F41B Bug Report"
about: Create a report to help us resolve a bug or error
---
**Background (please complete the following information):**
* Panel or Daemon:
* Version of Panel/Daemon:
* Server's OS:
* Your Computer's OS & Browser:
**Describe the bug**
A clear and concise description of what the bug is.
Please provide additional information too, depending on what you have issues with:
Panel: `php -v` (the php version in use).
Daemon: `uname -a` and `docker info` (your kernel version and information regarding docker)
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen. If applicable, add screenshots or a recording to help explain your problem.

View file

@ -0,0 +1,17 @@
---
name: "\U0001F680 Feature Request"
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -0,0 +1,8 @@
---
name: "⛔ Installation Help"
about: 'Visit our Discord for installation help: https://pterodactyl.io/discord'
---
We use GitHub issues only to discuss about Pterodactyl bugs and new features. For
this kind of questions about using Pterodactyl, please visit our Discord for assistance: https://pterodactyl.io/discord

View file

@ -1,5 +1,17 @@
language: php language: php
dist: trusty dist: trusty
git:
depth: 3
quiet: true
matrix:
fast_finish: true
allow_failures:
- env: TEST_SUITE=Coverage
env:
matrix:
- TEST_SUITE=Unit
- TEST_SUITE=Coverage
- TEST_SUITE=Integration
php: php:
- 7.2 - 7.2
sudo: false sudo: false
@ -15,8 +27,9 @@ before_script:
- cp .env.travis .env - cp .env.travis .env
- travis_retry composer install --no-interaction --prefer-dist --no-suggest - travis_retry composer install --no-interaction --prefer-dist --no-suggest
script: script:
- vendor/bin/phpunit --bootstrap vendor/autoload.php --coverage-clover coverage.xml tests/Unit - if [ "$TEST_SUITE" = "Unit" ]; then vendor/bin/phpunit --bootstrap vendor/autoload.php tests/Unit; fi;
- vendor/bin/phpunit tests/Integration - if [ "$TEST_SUITE" = "Coverage" ]; then vendor/bin/phpunit --bootstrap vendor/autoload.php --coverage-clover coverage.xml tests/Unit; fi;
- if [ "$TEST_SUITE" = "Integration" ]; then vendor/bin/phpunit tests/Integration; fi;
notifications: notifications:
email: false email: false
webhooks: webhooks:

View file

@ -3,6 +3,27 @@ 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. 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.
* Added support for using a MySQL socket location for connections rather than a TCP connection. Set a `DB_SOCKET` variable in your `.env` file to use this.
### 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.
* Permissions checking to determine if a user can see the task management page now works correctly.
* Fixed `pterodactyl.environment_variables` to be used correctly for global environment variables. The wrong config variable name was being using previously.
* Fixes tokens being sent to users when their account is created to actually work. Implements Laravel's internal token creation mechanisms rather than trying to do it custom.
* Updates some eggs to ensure they have the correct data and will continue working down the road. Fixes autoupdating on some source servers and MC related download links.
* Emails should send properly now when a server is marked as installed to let the owner know it is ready for action.
* Cancelling a file manager operation should cancel correctly across all browsers now.
### Changed
* Attempting to upload a folder via the web file manager will now display a warning telling the user to use SFTP.
* Changing your account password will now log out all other sessions that currently exist for that user.
* Subusers with no permissions selected can be created.
## v0.7.7 (Derelict Dermodactylus) ## v0.7.7 (Derelict Dermodactylus)
### Fixed ### Fixed
* Fixes an issue with the sidebar logo not working correctly in some browsers due to the CSS being assigned. * Fixes an issue with the sidebar logo not working correctly in some browsers due to the CSS being assigned.

View file

@ -0,0 +1,15 @@
<?php
namespace Pterodactyl\Contracts\Core;
use Pterodactyl\Events\Event;
interface ReceivesEvents
{
/**
* Handles receiving an event from the application.
*
* @param \Pterodactyl\Events\Event $notification
*/
public function handle(Event $notification): void;
}

View file

@ -21,6 +21,14 @@ interface NodeRepositoryInterface extends RepositoryInterface, SearchableInterfa
*/ */
public function getUsageStats(Node $node): array; 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. * Return all available nodes with a searchable interface.
* *

View file

@ -200,4 +200,11 @@ interface RepositoryInterface
* @return bool * @return bool
*/ */
public function insertIgnore(array $values): bool; public function insertIgnore(array $values): bool;
/**
* Get the amount of entries in the database.
*
* @return int
*/
public function count(): int;
} }

View file

@ -145,4 +145,11 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter
* @return bool * @return bool
*/ */
public function isUniqueUuidCombo(string $uuid, string $short): bool; public function isUniqueUuidCombo(string $uuid, string $short): bool;
/**
* Get the amount of servers that are suspended.
*
* @return int
*/
public function getSuspendedServersCount(): int;
} }

View file

@ -0,0 +1,27 @@
<?php
namespace Pterodactyl\Events\Server;
use Pterodactyl\Events\Event;
use Pterodactyl\Models\Server;
use Illuminate\Queue\SerializesModels;
class Installed extends Event
{
use SerializesModels;
/**
* @var \Pterodactyl\Models\Server
*/
public $server;
/**
* Create a new event instance.
*
* @var \Pterodactyl\Models\Server
*/
public function __construct(Server $server)
{
$this->server = $server;
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Traits\Controllers\PlainJavascriptInjection;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
class StatisticsController extends Controller
{
use PlainJavascriptInjection;
private $allocationRepository;
private $databaseRepository;
private $eggRepository;
private $nodeRepository;
private $serverRepository;
private $userRepository;
public function __construct(
AllocationRepositoryInterface $allocationRepository,
DatabaseRepositoryInterface $databaseRepository,
EggRepositoryInterface $eggRepository,
NodeRepositoryInterface $nodeRepository,
ServerRepositoryInterface $serverRepository,
UserRepositoryInterface $userRepository
) {
$this->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,
]);
}
}

View file

@ -3,7 +3,9 @@
namespace Pterodactyl\Http\Controllers\Base; namespace Pterodactyl\Http\Controllers\Base;
use Pterodactyl\Models\User; use Pterodactyl\Models\User;
use Illuminate\Auth\AuthManager;
use Prologue\Alerts\AlertsMessageBag; use Prologue\Alerts\AlertsMessageBag;
use Illuminate\Contracts\Session\Session;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Users\UserUpdateService; use Pterodactyl\Services\Users\UserUpdateService;
use Pterodactyl\Http\Requests\Base\AccountDataFormRequest; use Pterodactyl\Http\Requests\Base\AccountDataFormRequest;
@ -15,6 +17,11 @@ class AccountController extends Controller
*/ */
protected $alert; protected $alert;
/**
* @var \Illuminate\Auth\SessionGuard
*/
protected $sessionGuard;
/** /**
* @var \Pterodactyl\Services\Users\UserUpdateService * @var \Pterodactyl\Services\Users\UserUpdateService
*/ */
@ -24,12 +31,14 @@ class AccountController extends Controller
* AccountController constructor. * AccountController constructor.
* *
* @param \Prologue\Alerts\AlertsMessageBag $alert * @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Illuminate\Auth\AuthManager $authManager
* @param \Pterodactyl\Services\Users\UserUpdateService $updateService * @param \Pterodactyl\Services\Users\UserUpdateService $updateService
*/ */
public function __construct(AlertsMessageBag $alert, UserUpdateService $updateService) public function __construct(AlertsMessageBag $alert, AuthManager $authManager, UserUpdateService $updateService)
{ {
$this->alert = $alert; $this->alert = $alert;
$this->updateService = $updateService; $this->updateService = $updateService;
$this->sessionGuard = $authManager->guard();
} }
/** /**
@ -50,21 +59,26 @@ class AccountController extends Controller
* *
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/ */
public function update(AccountDataFormRequest $request) public function update(AccountDataFormRequest $request)
{ {
$data = []; // Prevent logging this specific session out when the password is changed. This will
// automatically update the user's password anyways, so no need to do anything else here.
if ($request->input('do_action') === 'password') { if ($request->input('do_action') === 'password') {
$data['password'] = $request->input('new_password'); $this->sessionGuard->logoutOtherDevices($request->input('new_password'));
} elseif ($request->input('do_action') === 'email') { } else {
$data['email'] = $request->input('new_email'); if ($request->input('do_action') === 'email') {
$data = ['email' => $request->input('new_email')];
} elseif ($request->input('do_action') === 'identity') { } elseif ($request->input('do_action') === 'identity') {
$data = $request->only(['name_first', 'name_last', 'username']); $data = $request->only(['name_first', 'name_last', 'username']);
} else {
$data = [];
} }
$this->updateService->setUserLevel(User::USER_LEVEL_USER); $this->updateService->setUserLevel(User::USER_LEVEL_USER);
$this->updateService->handle($request->user(), $data); $this->updateService->handle($request->user(), $data);
}
$this->alert->success(trans('base.account.details_updated'))->flash(); $this->alert->success(trans('base.account.details_updated'))->flash();
return redirect()->route('account'); return redirect()->route('account');

View file

@ -7,9 +7,26 @@ use Illuminate\Http\Request;
use Pterodactyl\Models\Node; use Pterodactyl\Models\Node;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Events\Server\Installed as ServerInstalled;
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
class ActionController extends Controller class ActionController extends Controller
{ {
/**
* @var \Illuminate\Contracts\Events\Dispatcher
*/
private $eventDispatcher;
/**
* ActionController constructor.
*
* @param \Illuminate\Contracts\Events\Dispatcher $eventDispatcher
*/
public function __construct(EventDispatcher $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
/** /**
* Handles install toggle request from daemon. * Handles install toggle request from daemon.
* *
@ -37,6 +54,11 @@ class ActionController extends Controller
$server->installed = ($status === 'installed') ? 1 : 2; $server->installed = ($status === 'installed') ? 1 : 2;
$server->save(); $server->save();
// Only fire event if server installed successfully.
if ($server->installed === 1) {
$this->eventDispatcher->dispatch(new ServerInstalled($server));
}
return response()->json([]); return response()->json([]);
} }

View file

@ -157,7 +157,6 @@ class SubuserController extends Controller
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* *
* @throws \Exception * @throws \Exception
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException * @throws \Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException
@ -171,7 +170,7 @@ class SubuserController extends Controller
$this->alert->success(trans('server.users.user_assigned'))->flash(); $this->alert->success(trans('server.users.user_assigned'))->flash();
return redirect()->route('server.subusers.view', [ return redirect()->route('server.subusers.view', [
'uuid' => $server->uuid, 'uuid' => $server->uuidShort,
'id' => $subuser->hashid, 'id' => $subuser->hashid,
]); ]);
} }

View file

@ -19,7 +19,9 @@ use Pterodactyl\Http\Middleware\Api\AuthenticateKey;
use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Routing\Middleware\SubstituteBindings;
use Pterodactyl\Http\Middleware\AccessingValidServer; use Pterodactyl\Http\Middleware\AccessingValidServer;
use Pterodactyl\Http\Middleware\Api\SetSessionDriver; use Pterodactyl\Http\Middleware\Api\SetSessionDriver;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\View\Middleware\ShareErrorsFromSession; use Illuminate\View\Middleware\ShareErrorsFromSession;
use Pterodactyl\Http\Middleware\MaintenanceMiddleware;
use Pterodactyl\Http\Middleware\RedirectIfAuthenticated; use Pterodactyl\Http\Middleware\RedirectIfAuthenticated;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Pterodactyl\Http\Middleware\Api\AuthenticateIPAccess; use Pterodactyl\Http\Middleware\Api\AuthenticateIPAccess;
@ -63,6 +65,7 @@ class Kernel extends HttpKernel
EncryptCookies::class, EncryptCookies::class,
AddQueuedCookiesToResponse::class, AddQueuedCookiesToResponse::class,
StartSession::class, StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class, ShareErrorsFromSession::class,
VerifyCsrfToken::class, VerifyCsrfToken::class,
SubstituteBindings::class, SubstituteBindings::class,
@ -108,6 +111,7 @@ class Kernel extends HttpKernel
'can' => Authorize::class, 'can' => Authorize::class,
'bindings' => SubstituteBindings::class, 'bindings' => SubstituteBindings::class,
'recaptcha' => VerifyReCaptcha::class, 'recaptcha' => VerifyReCaptcha::class,
'node.maintenance' => MaintenanceMiddleware::class,
// Server specific middleware (used for authenticating access to resources) // Server specific middleware (used for authenticating access to resources)
// //

View file

@ -0,0 +1,44 @@
<?php
namespace Pterodactyl\Http\Middleware;
use Closure;
use Illuminate\Contracts\Routing\ResponseFactory;
class MaintenanceMiddleware
{
/**
* @var \Illuminate\Contracts\Routing\ResponseFactory
*/
private $response;
/**
* MaintenanceMiddleware constructor.
*
* @param \Illuminate\Contracts\Routing\ResponseFactory $response
*/
public function __construct(ResponseFactory $response)
{
$this->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);
}
}

View file

@ -74,7 +74,7 @@ class StoreNodeRequest extends ApplicationApiRequest
$response = parent::validated(); $response = parent::validated();
$response['daemonListen'] = $response['daemon_listen']; $response['daemonListen'] = $response['daemon_listen'];
$response['daemonSFTP'] = $response['daemon_sftp']; $response['daemonSFTP'] = $response['daemon_sftp'];
$response['daemonBase'] = $response['daemon_base']; $response['daemonBase'] = $response['daemon_base'] ?? (new Node)->getAttribute('daemonBase');
unset($response['daemon_base'], $response['daemon_listen'], $response['daemon_sftp']); unset($response['daemon_base'], $response['daemon_listen'], $response['daemon_sftp']);

View file

@ -25,7 +25,7 @@ class SubuserStoreFormRequest extends ServerFormRequest
{ {
return [ return [
'email' => 'required|email', 'email' => 'required|email',
'permissions' => 'present|array', 'permissions' => 'sometimes|array',
]; ];
} }
} }

View file

View file

@ -48,6 +48,7 @@ class Node extends Model implements CleansAttributes, ValidableContract
'daemonSFTP' => 'integer', 'daemonSFTP' => 'integer',
'behind_proxy' => 'boolean', 'behind_proxy' => 'boolean',
'public' => 'boolean', 'public' => 'boolean',
'maintenance_mode' => 'boolean',
]; ];
/** /**
@ -62,7 +63,7 @@ class Node extends Model implements CleansAttributes, ValidableContract
'disk_overallocate', 'upload_size', 'disk_overallocate', 'upload_size',
'daemonSecret', 'daemonBase', 'daemonSecret', 'daemonBase',
'daemonSFTP', 'daemonListen', 'daemonSFTP', 'daemonListen',
'description', 'description', 'maintenance_mode',
]; ];
/** /**
@ -111,6 +112,7 @@ class Node extends Model implements CleansAttributes, ValidableContract
'daemonBase' => 'regex:/^([\/][\d\w.\-\/]+)$/', 'daemonBase' => 'regex:/^([\/][\d\w.\-\/]+)$/',
'daemonSFTP' => 'numeric|between:1024,65535', 'daemonSFTP' => 'numeric|between:1024,65535',
'daemonListen' => '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', 'daemonBase' => '/srv/daemon-data',
'daemonSFTP' => 2022, 'daemonSFTP' => 2022,
'daemonListen' => 8080, 'daemonListen' => 8080,
'maintenance_mode' => false,
]; ];
/** /**

View file

@ -23,7 +23,7 @@ class AccountCreated extends Notification implements ShouldQueue
/** /**
* The user model for the created user. * The user model for the created user.
* *
* @var object * @var \Pterodactyl\Models\User
*/ */
public $user; public $user;
@ -65,7 +65,7 @@ class AccountCreated extends Notification implements ShouldQueue
->line('Email: ' . $this->user->email); ->line('Email: ' . $this->user->email);
if (! is_null($this->token)) { if (! is_null($this->token)) {
return $message->action('Setup Your Account', url('/auth/password/reset/' . $this->token . '?email=' . $this->user->email)); return $message->action('Setup Your Account', url('/auth/password/reset/' . $this->token . '?email=' . urlencode($this->user->email)));
} }
return $message; return $message;

View file

@ -0,0 +1,69 @@
<?php
namespace Pterodactyl\Notifications;
use Illuminate\Bus\Queueable;
use Pterodactyl\Events\Event;
use Illuminate\Container\Container;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Pterodactyl\Contracts\Core\ReceivesEvents;
use Illuminate\Contracts\Notifications\Dispatcher;
use Illuminate\Notifications\Messages\MailMessage;
class ServerInstalled extends Notification implements ShouldQueue, ReceivesEvents
{
use Queueable;
/**
* @var \Pterodactyl\Models\Server
*/
public $server;
/**
* @var \Pterodactyl\Models\User
*/
public $user;
/**
* Handle a direct call to this notification from the server installed event. This is configured
* in the event service provider.
*
* @param \Pterodactyl\Events\Event|\Pterodactyl\Events\Server\Installed $event
*/
public function handle(Event $event): void
{
$event->server->loadMissing('user');
$this->server = $event->server;
$this->user = $event->server->user;
// Since we are calling this notification directly from an event listener we need to fire off the dispatcher
// to send the email now. Don't use send() or you'll end up firing off two different events.
Container::getInstance()->make(Dispatcher::class)->sendNow($this->user, $this);
}
/**
* Get the notification's delivery channels.
*
* @return array
*/
public function via()
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail()
{
return (new MailMessage)
->greeting('Hello ' . $this->user->username . '.')
->line('Your server has finished installing and is now ready for you to use.')
->line('Server Name: ' . $this->server->name)
->action('Login and Begin Using', route('index'));
}
}

View file

@ -2,6 +2,8 @@
namespace Pterodactyl\Providers; namespace Pterodactyl\Providers;
use Pterodactyl\Events\Server\Installed as ServerInstalledEvent;
use Pterodactyl\Notifications\ServerInstalled as ServerInstalledNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider class EventServiceProvider extends ServiceProvider
@ -11,5 +13,9 @@ class EventServiceProvider extends ServiceProvider
* *
* @var array * @var array
*/ */
protected $listen = []; protected $listen = [
ServerInstalledEvent::class => [
ServerInstalledNotification::class,
],
];
} }

View file

@ -33,7 +33,7 @@ class RouteServiceProvider extends ServiceProvider
->namespace($this->namespace . '\Auth') ->namespace($this->namespace . '\Auth')
->group(base_path('routes/auth.php')); ->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') ->namespace($this->namespace . '\Server')
->group(base_path('routes/server.php')); ->group(base_path('routes/server.php'));

View file

@ -144,6 +144,7 @@ abstract class BaseRepository implements BaseRepositoryInterface
$headers['X-Access-Token'] = $this->getToken() ?? $this->getNode()->daemonSecret; $headers['X-Access-Token'] = $this->getToken() ?? $this->getNode()->daemonSecret;
return new Client([ return new Client([
'verify' => config('app.env') === 'production',
'base_uri' => sprintf('%s://%s:%s/v1/', $this->getNode()->scheme, $this->getNode()->fqdn, $this->getNode()->daemonListen), 'base_uri' => sprintf('%s://%s:%s/v1/', $this->getNode()->scheme, $this->getNode()->fqdn, $this->getNode()->daemonListen),
'timeout' => config('pterodactyl.guzzle.timeout'), 'timeout' => config('pterodactyl.guzzle.timeout'),
'connect_timeout' => config('pterodactyl.guzzle.connect_timeout'), 'connect_timeout' => config('pterodactyl.guzzle.connect_timeout'),

View file

@ -296,4 +296,14 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
return $this->getBuilder()->getConnection()->statement($statement, $bindings); 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();
}
} }

View file

@ -56,6 +56,33 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa
})->toArray(); })->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. * Return all available nodes with a searchable interface.
* *

View file

@ -328,4 +328,14 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
$this->app->make(SubuserRepository::class)->getBuilder()->select('server_id')->where('user_id', $user) $this->app->make(SubuserRepository::class)->getBuilder()->select('server_id')->where('user_id', $user)
)->pluck('id')->all(); )->pluck('id')->all();
} }
/**
* Get the amount of servers that are suspended.
*
* @return int
*/
public function getSuspendedServersCount(): int
{
return $this->getBuilder()->where('suspended', true)->count();
}
} }

View file

@ -1,59 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Services\Helpers;
use Ramsey\Uuid\Uuid;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Database\ConnectionInterface;
class TemporaryPasswordService
{
const HMAC_ALGO = 'sha256';
/**
* @var \Illuminate\Database\ConnectionInterface
*/
protected $connection;
/**
* @var \Illuminate\Contracts\Hashing\Hasher
*/
protected $hasher;
/**
* TemporaryPasswordService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Contracts\Hashing\Hasher $hasher
*/
public function __construct(ConnectionInterface $connection, Hasher $hasher)
{
$this->connection = $connection;
$this->hasher = $hasher;
}
/**
* Store a password reset token for a specific email address.
*
* @param string $email
* @return string
*/
public function handle($email)
{
$token = hash_hmac(self::HMAC_ALGO, Uuid::uuid4()->toString(), config('app.key'));
$this->connection->table('password_resets')->insert([
'email' => $email,
'token' => $this->hasher->make($token),
]);
return $token;
}
}

View file

@ -78,7 +78,7 @@ class EnvironmentService
} }
// Process variables set in the configuration file. // Process variables set in the configuration file.
foreach ($this->config->get('pterodactyl.environment_mappings', []) as $key => $object) { foreach ($this->config->get('pterodactyl.environment_variables', []) as $key => $object) {
if (is_callable($object)) { if (is_callable($object)) {
$variables[$key] = call_user_func($object, $server); $variables[$key] = call_user_func($object, $server);
} else { } else {

View file

@ -56,6 +56,8 @@ class PermissionCreationService
} }
} }
if (! empty($insertPermissions)) {
$this->repository->withoutFreshModel()->insert($insertPermissions); $this->repository->withoutFreshModel()->insert($insertPermissions);
} }
} }
}

View file

@ -5,8 +5,8 @@ namespace Pterodactyl\Services\Users;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Auth\PasswordBroker;
use Pterodactyl\Notifications\AccountCreated; use Pterodactyl\Notifications\AccountCreated;
use Pterodactyl\Services\Helpers\TemporaryPasswordService;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
class UserCreationService class UserCreationService
@ -22,9 +22,9 @@ class UserCreationService
private $hasher; private $hasher;
/** /**
* @var \Pterodactyl\Services\Helpers\TemporaryPasswordService * @var \Illuminate\Contracts\Auth\PasswordBroker
*/ */
private $passwordService; private $passwordBroker;
/** /**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
@ -36,18 +36,18 @@ class UserCreationService
* *
* @param \Illuminate\Database\ConnectionInterface $connection * @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Contracts\Hashing\Hasher $hasher * @param \Illuminate\Contracts\Hashing\Hasher $hasher
* @param \Pterodactyl\Services\Helpers\TemporaryPasswordService $passwordService * @param \Illuminate\Contracts\Auth\PasswordBroker $passwordBroker
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
*/ */
public function __construct( public function __construct(
ConnectionInterface $connection, ConnectionInterface $connection,
Hasher $hasher, Hasher $hasher,
TemporaryPasswordService $passwordService, PasswordBroker $passwordBroker,
UserRepositoryInterface $repository UserRepositoryInterface $repository
) { ) {
$this->connection = $connection; $this->connection = $connection;
$this->hasher = $hasher; $this->hasher = $hasher;
$this->passwordService = $passwordService; $this->passwordBroker = $passwordBroker;
$this->repository = $repository; $this->repository = $repository;
} }
@ -68,8 +68,8 @@ class UserCreationService
$this->connection->beginTransaction(); $this->connection->beginTransaction();
if (! isset($data['password']) || empty($data['password'])) { if (! isset($data['password']) || empty($data['password'])) {
$generateResetToken = true;
$data['password'] = $this->hasher->make(str_random(30)); $data['password'] = $this->hasher->make(str_random(30));
$token = $this->passwordService->handle($data['email']);
} }
/** @var \Pterodactyl\Models\User $user */ /** @var \Pterodactyl\Models\User $user */
@ -77,6 +77,10 @@ class UserCreationService
'uuid' => Uuid::uuid4()->toString(), 'uuid' => Uuid::uuid4()->toString(),
]), true, true); ]), true, true);
if (isset($generateResetToken)) {
$token = $this->passwordBroker->createToken($user);
}
$this->connection->commit(); $this->connection->commit();
$user->notify(new AccountCreated($user, $token ?? null)); $user->notify(new AccountCreated($user, $token ?? null));

View file

@ -0,0 +1,22 @@
<?php
/**
* Created by PhpStorm.
* User: Stan
* Date: 26-5-2018
* Time: 20:56.
*/
namespace Pterodactyl\Traits\Controllers;
use JavaScript;
trait PlainJavascriptInjection
{
/**
* Injects statistics into javascript.
*/
public function injectJavascript($data)
{
Javascript::put($data);
}
}

View file

@ -1,8 +1,6 @@
coverage: coverage:
status: status:
project: project: off
default: patch: off
target: 35
threshold: 1
comment: comment:
layout: "diff" layout: "diff"

561
composer.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -34,6 +34,7 @@ return [
'mysql' => [ 'mysql' => [
'driver' => 'mysql', 'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'), 'host' => env('DB_HOST', '127.0.0.1'),
'unix_socket' => env('DB_SOCKET'),
'port' => env('DB_PORT', '3306'), 'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'panel'), 'database' => env('DB_DATABASE', 'panel'),
'username' => env('DB_USERNAME', 'pterodactyl'), 'username' => env('DB_USERNAME', 'pterodactyl'),

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddMaintenanceToNodes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('nodes', function (Blueprint $table) {
$table->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');
});
}
}

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2018-02-27T00:57:04-06:00", "exported_at": "2018-06-25T15:47:07-04:00",
"name": "Forge Minecraft", "name": "Forge Minecraft",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.", "description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.",
@ -17,7 +17,7 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/ash\r\n# Forge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napk update\r\napk add curl\r\n\r\nGET_VERSIONS=$(curl -sl http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/ | grep -A1 Latest | grep -o -e '[1]\\.[0-9][0-9]]\\?\\.\\?[0-9]\\?[0-9] - [0-9][0-9]\\.[0-9][0-9]\\.[0-9]\\?[0-9]\\.[0-9][0-9][0-9][0-9]')\r\nLATEST_VERSION=$(echo $GET_VERSIONS | sed 's\/ \/\/g')\r\n\r\ncd \/mnt\/server\r\n\r\ncurl -sS http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/$LATEST_VERSION\/forge-$LATEST_VERSION-installer.jar -o installer.jar\r\ncurl -sS http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/$LATEST_VERSION\/forge-$LATEST_VERSION-universal.jar -o server.jar\r\n\r\njava -jar installer.jar --installServer\r\nrm -rf installer.jar", "script": "#!\/bin\/ash\r\n# Forge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napk update\r\napk add curl\r\n\r\nif [ -z \"$MC_VERSION\" ] || [ \"$MC_VERSION\" == \"latest\" ]; then\r\n FORGE_VERSION=$(curl -sl http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/ | grep -A1 Latest | grep -o -e '[1]\\.[0-9][0-9]]\\?\\.\\?[0-9]\\?[0-9] - [0-9][0-9]\\.[0-9][0-9]\\.[0-9]\\?[0-9]\\.[0-9][0-9][0-9][0-9]' | sed 's\/ \/\/g')\r\nelse\r\n FORGE_VERSION=$(curl -sl http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/index_$MC_VERSION.html | grep -A1 Latest | grep -o -e '[1]\\.[0-9][0-9]]\\?\\.\\?[0-9]\\?[0-9] - [0-9][0-9]\\.[0-9][0-9]\\.[0-9]\\?[0-9]\\.[0-9][0-9][0-9][0-9]' | sed 's\/ \/\/g')\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\necho -e \"\\nDownloading Forge Version $FORGE_VERSION\\n\"\r\ncurl -sS http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/$FORGE_VERSION\/forge-$FORGE_VERSION-installer.jar -o installer.jar\r\ncurl -sS http:\/\/files.minecraftforge.net\/maven\/net\/minecraftforge\/forge\/$FORGE_VERSION\/forge-$FORGE_VERSION-universal.jar -o $SERVER_JARFILE\r\n\r\necho -e \"\\nInstalling forge server usint the installer jar file.\\n\"\r\njava -jar installer.jar --installServer\r\n\r\necho -e \"\\nDeleting installer jar file and cleaning up.\\n\"\r\nrm -rf installer.jar",
"container": "frolvlad\/alpine-oraclejdk8:cleaned", "container": "frolvlad\/alpine-oraclejdk8:cleaned",
"entrypoint": "ash" "entrypoint": "ash"
} }
@ -31,6 +31,15 @@
"user_viewable": 1, "user_viewable": 1,
"user_editable": 1, "user_editable": 1,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
},
{
"name": "Minecraft version",
"description": "The version of minecraft that you want to run. Example (1.10.2).",
"env_variable": "MC_VERSION",
"default_value": "latest",
"user_viewable": 1,
"user_editable": 1,
"rules": "required|string|max:20"
} }
] ]
} }

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2017-11-03T22:15:07-05:00", "exported_at": "2018-06-19T17:09:18-04:00",
"name": "Vanilla Minecraft", "name": "Vanilla Minecraft",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Minecraft is a game about placing blocks and going on adventures. Explore randomly generated worlds and build amazing things from the simplest of homes to the grandest of castles. Play in Creative Mode with unlimited resources or mine deep in Survival Mode, crafting weapons and armor to fend off dangerous mobs. Do all this alone or with friends.", "description": "Minecraft is a game about placing blocks and going on adventures. Explore randomly generated worlds and build amazing things from the simplest of homes to the grandest of castles. Play in Creative Mode with unlimited resources or mine deep in Survival Mode, crafting weapons and armor to fend off dangerous mobs. Do all this alone or with friends.",
@ -17,8 +17,8 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/ash\r\n# Vanilla MC Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napk update\r\napk add curl\r\n\r\ncd \/mnt\/server\r\n\r\nLATEST_VERSION=`curl -s https:\/\/s3.amazonaws.com\/Minecraft.Download\/versions\/versions.json | grep -o \"[[:digit:]]\\.[0-9]*\\.[0-9]\" | head -n 1`\r\n\r\nif [ -z \"$VANILLA_VERSION\" ] || [ \"$VANILLA_VERSION\" == \"latest\" ]; then\r\n DL_VERSION=$LATEST_VERSION\r\nelse\r\n DL_VERSION=$VANILLA_VERSION\r\nfi\r\n\r\ncurl -o ${SERVER_JARFILE} https:\/\/s3.amazonaws.com\/Minecraft.Download\/versions\/${DL_VERSION}\/minecraft_server.${DL_VERSION}.jar", "script": "#!\/bin\/ash\r\n# Vanilla MC Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\napk update\r\napk add curl jq\r\n\r\ncd \/mnt\/server\r\n\r\nLATEST_VERSION=`curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq -r '.latest.release'`\r\n\r\nif [ -z \"$VANILLA_VERSION\" ] || [ \"$VANILLA_VERSION\" == \"latest\" ]; then\r\n MANIFEST_URL=$(curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq .versions | jq -r '.[] | select(.id == \"'$LATEST_VERSION'\") | .url')\r\nelse\r\n MANIFEST_URL=$(curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq .versions | jq -r '.[] | select(.id == \"'$VANILLA_VERSION'\") | .url')\r\nfi\r\n\r\nDOWNLOAD_URL=`curl $MANIFEST_URL | jq .downloads.server | jq -r '. | .url'`\r\n\r\ncurl -o ${SERVER_JARFILE} $DOWNLOAD_URL",
"container": "alpine:3.4", "container": "alpine:3.7",
"entrypoint": "ash" "entrypoint": "ash"
} }
}, },
@ -39,7 +39,7 @@
"default_value": "latest", "default_value": "latest",
"user_viewable": 1, "user_viewable": 1,
"user_editable": 1, "user_editable": 1,
"rules": "required|string|between:3,7" "rules": "required|string|between:3,15"
} }
] ]
} }

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2018-01-21T16:59:47-06:00", "exported_at": "2018-06-19T07:46:06-04:00",
"name": "Counter-Strike: Global Offensive", "name": "Counter-Strike: Global Offensive",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Counter-Strike: Global Offensive is a multiplayer first-person shooter video game developed by Hidden Path Entertainment and Valve Corporation.", "description": "Counter-Strike: Global Offensive is a multiplayer first-person shooter video game developed by Hidden Path Entertainment and Valve Corporation.",
@ -11,7 +11,7 @@
"startup": ".\/srcds_run -game csgo -console -port {{SERVER_PORT}} +ip 0.0.0.0 +map {{SRCDS_MAP}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}}", "startup": ".\/srcds_run -game csgo -console -port {{SERVER_PORT}} +ip 0.0.0.0 +map {{SRCDS_MAP}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}}",
"config": { "config": {
"files": "{}", "files": "{}",
"startup": "{\r\n \"done\": \"gameserver Steam ID\",\r\n \"userInteraction\": []\r\n}", "startup": "{\r\n \"done\": \"Connection to Steam servers successful\",\r\n \"userInteraction\": []\r\n}",
"logs": "{\r\n \"custom\": true,\r\n \"location\": \"logs\/latest.log\"\r\n}", "logs": "{\r\n \"custom\": true,\r\n \"location\": \"logs\/latest.log\"\r\n}",
"stop": "quit" "stop": "quit"
}, },
@ -40,6 +40,15 @@
"user_viewable": 1, "user_viewable": 1,
"user_editable": 1, "user_editable": 1,
"rules": "required|string|alpha_num|size:32" "rules": "required|string|alpha_num|size:32"
},
{
"name": "Source AppID",
"description": "Required for game to update on server restart. Do not modify this.",
"env_variable": "SRCDS_APPID",
"default_value": "740",
"user_viewable": 0,
"user_editable": 0,
"rules": "required|string|max:20"
} }
] ]
} }

View file

@ -3,7 +3,7 @@
"meta": { "meta": {
"version": "PTDL_v1" "version": "PTDL_v1"
}, },
"exported_at": "2018-01-21T16:59:47-06:00", "exported_at": "2018-06-19T07:46:27-04:00",
"name": "Garrys Mod", "name": "Garrys Mod",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company, Facepunch Studios.", "description": "Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company, Facepunch Studios.",
@ -40,6 +40,15 @@
"user_viewable": 1, "user_viewable": 1,
"user_editable": 1, "user_editable": 1,
"rules": "required|string|alpha_num|size:32" "rules": "required|string|alpha_num|size:32"
},
{
"name": "Source AppID",
"description": "Required for game to update on server restart. Do not modify this.",
"env_variable": "SRCDS_APPID",
"default_value": "4020",
"user_viewable": 0,
"user_editable": 0,
"rules": "required|string|max:20"
} }
] ]
} }

View file

@ -473,3 +473,12 @@ label.control-label > span.field-optional:before {
height: 42px; height: 42px;
width: auto; width: auto;
} }
.search01 {
width: 30%;
}
.number-info-box-content {
padding: 15px 10px 0;
}

View file

@ -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];
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -55,6 +55,10 @@ class ActionsClass {
showLoaderOnConfirm: true, showLoaderOnConfirm: true,
inputValue: inputValue inputValue: inputValue
}, (val) => { }, (val) => {
if (val === false) {
return false;
}
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
headers: { headers: {
@ -100,6 +104,10 @@ class ActionsClass {
showLoaderOnConfirm: true, showLoaderOnConfirm: true,
inputValue: `${currentPath}${currentName}`, inputValue: `${currentPath}${currentName}`,
}, (val) => { }, (val) => {
if (val === false) {
return false;
}
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
headers: { headers: {
@ -233,6 +241,10 @@ class ActionsClass {
showLoaderOnConfirm: true, showLoaderOnConfirm: true,
inputValue: `${currentPath}${currentName}`, inputValue: `${currentPath}${currentName}`,
}, (val) => { }, (val) => {
if (val === false) {
return false;
}
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
headers: { headers: {

View file

@ -61,6 +61,18 @@
event.preventDefault(); event.preventDefault();
}, false); }, 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; var dropCounter = 0;
$('#load_files').bind({ $('#load_files').bind({
dragenter: function (event) { dragenter: function (event) {
@ -75,6 +87,15 @@
} }
}, },
drop: function (event) { 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; dropCounter = 0;
$(this).removeClass('hasFileHover'); $(this).removeClass('hasFileHover');
} }

View file

@ -21,6 +21,11 @@ return [
'header' => 'Server Suspended', 'header' => 'Server Suspended',
'desc' => 'This server has been suspended and cannot be accessed.', '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' => [ 'index' => [
'header' => 'Your Servers', 'header' => 'Your Servers',

View file

@ -74,6 +74,7 @@ return [
'tasks' => 'Tasks', 'tasks' => 'Tasks',
'seconds' => 'Seconds', 'seconds' => 'Seconds',
'minutes' => 'Minutes', 'minutes' => 'Minutes',
'under_maintenance' => 'Under Maintenance',
'days' => [ 'days' => [
'sun' => 'Sunday', 'sun' => 'Sunday',
'mon' => 'Monday', 'mon' => 'Monday',

View file

@ -56,7 +56,7 @@
@foreach ($nodes as $node) @foreach ($nodes as $node)
<tr> <tr>
<td class="text-center text-muted left-icon" data-action="ping" data-secret="{{ $node->daemonSecret }}" data-location="{{ $node->scheme }}://{{ $node->fqdn }}:{{ $node->daemonListen }}/v1"><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->daemonSecret }}" data-location="{{ $node->scheme }}://{{ $node->fqdn }}:{{ $node->daemonListen }}/v1"><i class="fa fa-fw fa-refresh fa-spin"></i></td>
<td><a href="{{ route('admin.nodes.view', $node->id) }}">{{ $node->name }}</a></td> <td>{!! $node->maintenance_mode ? '<span class="label label-warning"><i class="fa fa-wrench"></i></span> ' : '' !!}<a href="{{ route('admin.nodes.view', $node->id) }}">{{ $node->name }}</a></td>
<td>{{ $node->location->short }}</td> <td>{{ $node->location->short }}</td>
<td>{{ $node->memory }} MB</td> <td>{{ $node->memory }} MB</td>
<td>{{ $node->disk }} MB</td> <td>{{ $node->disk }} MB</td>

View file

@ -96,6 +96,17 @@
</div> </div>
<div class="box-body"> <div class="box-body">
<div class="row"> <div class="row">
@if($node->maintenance_mode)
<div class="col-sm-12">
<div class="info-box bg-orange">
<span class="info-box-icon"><i class="ion ion-wrench"></i></span>
<div class="info-box-content" style="padding: 23px 10px 0;">
<span class="info-box-text">This node is under</span>
<span class="info-box-number">Maintenance</span>
</div>
</div>
</div>
@endif
<div class="col-sm-12"> <div class="col-sm-12">
<div class="info-box bg-{{ $stats['disk']['css'] }}"> <div class="info-box bg-{{ $stats['disk']['css'] }}">
<span class="info-box-icon"><i class="ion ion-ios-folder-outline"></i></span> <span class="info-box-icon"><i class="ion ion-ios-folder-outline"></i></span>

View file

@ -108,6 +108,20 @@
</div> </div>
<p class="text-muted small">If you are running the daemon behind a proxy such as Cloudflare, select this to have the daemon skip looking for certificates on boot.</p> <p class="text-muted small">If you are running the daemon behind a proxy such as Cloudflare, select this to have the daemon skip looking for certificates on boot.</p>
</div> </div>
<div class="form-group col-xs-12">
<label class="form-label"><span class="label label-warning"><i class="fa fa-wrench"></i></span> Maintenance Mode</label>
<div>
<div class="radio radio-success radio-inline">
<input type="radio" id="pMaintenanceFalse" value="0" name="maintenance_mode" {{ (old('behind_proxy', $node->maintenance_mode) == false) ? 'checked' : '' }}>
<label for="pMaintenanceFalse"> Disabled</label>
</div>
<div class="radio radio-warning radio-inline">
<input type="radio" id="pMaintenanceTrue" value="1" name="maintenance_mode" {{ (old('behind_proxy', $node->maintenance_mode) == true) ? 'checked' : '' }}>
<label for="pMaintenanceTrue"> Enabled</label>
</div>
</div>
<p class="text-muted small">If the node is marked as 'Under Maintenance' users won't be able to access servers that are on this node.</p>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -62,7 +62,7 @@
<h3 class="box-title">Force Delete Server</h3> <h3 class="box-title">Force Delete Server</h3>
</div> </div>
<div class="box-body"> <div class="box-body">
<p>This action will attempt to delete the server from both the panel and daemon. The the daemon does not respond, or reports an error the deletion will continue.</p> <p>This action will attempt to delete the server from both the panel and daemon. If the daemon does not respond, or reports an error the deletion will continue.</p>
<p class="text-danger small">Deleting a server is an irreversible action. <strong>All server data</strong> (including files and users) will be removed from the system. This method may leave dangling files on your daemon if it reports an error.</p> <p class="text-danger small">Deleting a server is an irreversible action. <strong>All server data</strong> (including files and users) will be removed from the system. This method may leave dangling files on your daemon if it reports an error.</p>
</div> </div>
<div class="box-footer"> <div class="box-footer">

View file

@ -0,0 +1,141 @@
@extends('layouts.admin')
@include('partials/admin.settings.nav', ['activeTab' => 'basic'])
@section('title')
Statistics Overview
@endsection
@section('content-header')
<h1>Statistics Overview<small>Monitor your panel usage.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li class="active">Statistics</li>
</ol>
@endsection
@section('content')
<div class="row">
<div class="col-xs-12 col-md-8">
<div class="box box-primary">
<div class="box-header with-border">
Servers
</div>
<div class="box-body">
<div class="col-xs-12 col-md-6">
<canvas id="servers_chart" width="100%" height="50"></canvas>
</div>
<div class="col-xs-12 col-md-6">
<canvas id="status_chart" width="100%" height="50"></canvas>
</div>
</div>
</div>
</div>
<div class="col-xs-12 col-md-4">
<div class="info-box bg-blue">
<span class="info-box-icon"><i class="fa fa-server"></i></span>
<div class="info-box-content number-info-box-content">
<span class="info-box-text">Servers</span>
<span class="info-box-number">{{ count($servers) }}</span>
</div>
</div>
<div class="info-box bg-blue">
<span class="info-box-icon"><i class="ion ion-ios-barcode-outline"></i></span>
<div class="info-box-content number-info-box-content">
<span class="info-box-text">Total used Memory (in MB)</span>
<span class="info-box-number">{{ $totalServerRam }}MB</span>
</div>
</div>
<div class="info-box bg-blue">
<span class="info-box-icon"><i class="ion ion-stats-bars"></i></span>
<div class="info-box-content number-info-box-content">
<span class="info-box-text">Total used Disk (in MB)</span>
<span class="info-box-number">{{ $totalServerDisk }}MB</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-md-8">
<div class="box box-primary">
<div class="box-header with-border">
Nodes
</div>
<div class="box-body">
<div class="col-xs-12 col-md-6">
<canvas id="ram_chart" width="100%" height="50"></canvas>
</div>
<div class="col-xs-12 col-md-6">
<canvas id="disk_chart" width="100%" height="50"></canvas>
</div>
</div>
</div>
</div>
<div class="col-xs-12 col-md-4">
<div class="info-box bg-blue">
<span class="info-box-icon"><i class="ion ion-ios-barcode-outline"></i></span>
<div class="info-box-content number-info-box-content">
<span class="info-box-text">Total RAM</span>
<span class="info-box-number">{{ $totalNodeRam }}MB</span>
</div>
</div>
<div class="info-box bg-blue">
<span class="info-box-icon"><i class="ion ion-stats-bars"></i></span>
<div class="info-box-content number-info-box-content">
<span class="info-box-text">Total Disk Space</span>
<span class="info-box-number">{{ $totalNodeDisk }}MB</span>
</div>
</div>
<div class="info-box bg-blue">
<span class="info-box-icon"><i class="fa fa-location-arrow"></i></span>
<div class="info-box-content number-info-box-content">
<span class="info-box-text">Total Allocations</span>
<span class="info-box-number">{{ $totalAllocations }}</span>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-md-3">
<div class="info-box bg-blue">
<span class="info-box-icon"><i class="fa fa-gamepad"></i></span>
<div class="info-box-content number-info-box-content">
<span class="info-box-text">Total Eggs</span>
<span class="info-box-number">{{ $eggsCount }}</span>
</div>
</div>
</div>
<div class="col-xs-12 col-md-3">
<div class="info-box bg-blue">
<span class="info-box-icon"><i class="fa fa-users"></i></span>
<div class="info-box-content number-info-box-content">
<span class="info-box-text">Total Users</span>
<span class="info-box-number">{{ $usersCount }}</span>
</div>
</div>
</div>
<div class="col-xs-12 col-md-3">
<div class="info-box bg-blue">
<span class="info-box-icon"><i class="fa fa-server"></i></span>
<div class="info-box-content number-info-box-content">
<span class="info-box-text">Total Nodes</span>
<span class="info-box-number">{{ count($nodes) }}</span>
</div>
</div>
</div>
<div class="col-xs-12 col-md-3">
<div class="info-box bg-blue">
<span class="info-box-icon"><i class="fa fa-database"></i></span>
<div class="info-box-content number-info-box-content">
<span class="info-box-text">Total Databases</span>
<span class="info-box-number">{{ $databasesCount }}</span>
</div>
</div>
</div>
</div>
@endsection
@section('footer-scripts')
@parent
{!! Theme::js('vendor/chartjs/chart.min.js') !!}
{!! Theme::js('js/admin/statistics.js') !!}
@endsection

View file

@ -23,10 +23,10 @@
<div class="box"> <div class="box">
<div class="box-header"> <div class="box-header">
<h3 class="box-title">@lang('base.index.list')</h3> <h3 class="box-title">@lang('base.index.list')</h3>
<div class="box-tools"> <div class="box-tools search01">
<form action="{{ route('index') }}" method="GET"> <form action="{{ route('index') }}" method="GET">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input type="text" name="query" class="form-control pull-right" style="width:30%;" value="{{ request()->input('query') }}" placeholder="@lang('strings.search')"> <input type="text" name="query" class="form-control pull-right" value="{{ request()->input('query') }}" placeholder="@lang('strings.search')">
<div class="input-group-btn"> <div class="input-group-btn">
<button type="submit" class="btn btn-default"><i class="fa fa-search"></i></button> <button type="submit" class="btn btn-default"><i class="fa fa-search"></i></button>
</div> </div>
@ -64,9 +64,15 @@
<span class="label bg-blue">@lang('strings.subuser')</span> <span class="label bg-blue">@lang('strings.subuser')</span>
@endif @endif
</td> </td>
@if($server->node->maintenance_mode)
<td class="text-center">
<span class="label label-warning">@lang('strings.under_maintenance')</span>
</td>
@else
<td class="text-center" data-action="status"> <td class="text-center" data-action="status">
<span class="label label-default"><i class="fa fa-refresh fa-fw fa-spin"></i></span> <span class="label label-default"><i class="fa fa-refresh fa-fw fa-spin"></i></span>
</td> </td>
@endif
</tr> </tr>
@if (! empty($server->description)) @if (! empty($server->description))
<tr class="server-description"> <tr class="server-description">

View file

@ -0,0 +1,30 @@
{{-- 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 --}}
@extends('layouts.error')
@section('title')
@lang('base.errors.maintenance.header')
@endsection
@section('content-header')
@endsection
@section('content')
<div class="row">
<div class="col-md-8 col-md-offset-2 col-sm-10 col-sm-offset-1 col-xs-12">
<div class="box box-danger">
<div class="box-body text-center">
<h1 class="text-red" style="font-size: 3em !important;font-weight: 100 !important;">@lang('base.errors.maintenance.title')</h1>
<p class="text-muted">@lang('base.errors.maintenance.desc')</p>
</div>
<div class="box-footer with-border">
<a href="{{ URL::previous() }}"><button class="btn btn-danger">&larr; @lang('base.errors.return')</button></a>
<a href="/"><button class="btn btn-default">@lang('base.errors.home')</button></a>
</div>
</div>
</div>
</div>
@endsection

View file

@ -80,6 +80,11 @@
<i class="fa fa-home"></i> <span>Overview</span> <i class="fa fa-home"></i> <span>Overview</span>
</a> </a>
</li> </li>
<li class="{{ Route::currentRouteName() !== 'admin.statistics' ?: 'active' }}">
<a href="{{ route('admin.statistics') }}">
<i class="fa fa-tachometer"></i> <span>Statistics</span>
</a>
</li>
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.settings') ?: 'active' }}"> <li class="{{ ! starts_with(Route::currentRouteName(), 'admin.settings') ?: 'active' }}">
<a href="{{ route('admin.settings')}}"> <a href="{{ route('admin.settings')}}">
<i class="fa fa-wrench"></i> <span>Settings</span> <i class="fa fa-wrench"></i> <span>Settings</span>

View file

@ -44,7 +44,7 @@
<header class="main-header"> <header class="main-header">
<a href="{{ route('index') }}" class="logo"> <a href="{{ route('index') }}" class="logo">
<span class="logo-lg">{{ config('app.name', 'Pterodactyl') }}</span> <span class="logo-lg">{{ config('app.name', 'Pterodactyl') }}</span>
<span class="logo-mini"><img src="favicons/android-chrome-192x192.png"></span> <span class="logo-mini"><img src="/favicons/android-chrome-192x192.png"></span>
</a> </a>
<nav class="navbar navbar-static-top"> <nav class="navbar navbar-static-top">
<a href="#" class="sidebar-toggle" data-toggle="push-menu" role="button"> <a href="#" class="sidebar-toggle" data-toggle="push-menu" role="button">
@ -146,7 +146,7 @@
</a> </a>
</li> </li>
@endcan @endcan
@can('list-tasks', $server) @can('list-schedules', $server)
<li <li
@if(starts_with(Route::currentRouteName(), 'server.schedules')) @if(starts_with(Route::currentRouteName(), 'server.schedules'))
class="active" class="active"

View file

@ -52,6 +52,7 @@
<script> <script>
$(document).ready(function () { $(document).ready(function () {
Editor.setValue($('#editorSetContent').val(), -1); Editor.setValue($('#editorSetContent').val(), -1);
Editor.getSession().setUndoManager(new ace.UndoManager());
$('#editorLoadingOverlay').hide(); $('#editorLoadingOverlay').hide();
}); });
</script> </script>

View file

@ -44,7 +44,7 @@
{!! Theme::js('vendor/lodash/lodash.js') !!} {!! Theme::js('vendor/lodash/lodash.js') !!}
{!! Theme::js('vendor/siofu/client.min.js') !!} {!! Theme::js('vendor/siofu/client.min.js') !!}
@if(App::environment('production')) @if(App::environment('production'))
{!! Theme::js('js/frontend/files/filemanager.min.js') !!} {!! Theme::js('js/frontend/files/filemanager.min.js?updated-cancel-buttons') !!}
@else @else
{!! Theme::js('js/frontend/files/src/index.js') !!} {!! Theme::js('js/frontend/files/src/index.js') !!}
{!! Theme::js('js/frontend/files/src/contextmenu.js') !!} {!! Theme::js('js/frontend/files/src/contextmenu.js') !!}

View file

@ -1,6 +1,7 @@
<?php <?php
Route::get('/', 'BaseController@index')->name('admin.index'); Route::get('/', 'BaseController@index')->name('admin.index');
Route::get('/statistics', 'StatisticsController@index')->name('admin.statistics');
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View file

@ -0,0 +1,110 @@
<?php
/**
* Created by PhpStorm.
* User: Stan
* Date: 26-5-2018
* Time: 21:06.
*/
namespace Tests\Unit\Http\Controllers\Admin;
use Mockery as m;
use Pterodactyl\Models\Node;
use Tests\Assertions\ControllerAssertionsTrait;
use Tests\Unit\Http\Controllers\ControllerTestCase;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Http\Controllers\Admin\StatisticsController;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
class StatisticsControllerTest extends ControllerTestCase
{
use ControllerAssertionsTrait;
/**
* @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface|\Mockery\Mock
*/
private $allocationRepository;
/**
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface|\Mockery\Mock
*/
private $databaseRepository;
/**
* @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock
*/
private $eggRepository;
/**
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface|\Mockery\Mock
*/
private $nodeRepository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock
*/
private $serverRepository;
/**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock
*/
private $userRepository;
public function setUp()
{
parent::setUp();
$this->allocationRepository = m::mock(AllocationRepositoryInterface::class);
$this->databaseRepository = m::mock(DatabaseRepositoryInterface::class);
$this->eggRepository = m::mock(EggRepositoryInterface::class);
$this->nodeRepository = m::mock(NodeRepositoryInterface::class);
$this->serverRepository = m::mock(ServerRepositoryInterface::class);
$this->userRepository = m::mock(UserRepositoryInterface::class);
}
public function testIndexController()
{
$controller = $this->getController();
$this->serverRepository->shouldReceive('all')->withNoArgs();
$this->nodeRepository->shouldReceive('all')->withNoArgs()->andReturn(collect([factory(Node::class)->make(), factory(Node::class)->make()]));
$this->userRepository->shouldReceive('count')->withNoArgs();
$this->eggRepository->shouldReceive('count')->withNoArgs();
$this->databaseRepository->shouldReceive('count')->withNoArgs();
$this->allocationRepository->shouldReceive('count')->withNoArgs();
$this->serverRepository->shouldReceive('getSuspendedServersCount')->withNoArgs();
$this->nodeRepository->shouldReceive('getUsageStatsRaw')->twice()->andReturn([
'memory' => [
'value' => 1024,
'max' => 512,
],
'disk' => [
'value' => 1024,
'max' => 512,
],
]);
$controller->shouldReceive('injectJavascript')->once();
$response = $controller->index();
$this->assertIsViewResponse($response);
$this->assertViewNameEquals('admin.statistics', $response);
}
private function getController()
{
return $this->buildMockedController(StatisticsController::class, [$this->allocationRepository,
$this->databaseRepository,
$this->eggRepository,
$this->nodeRepository,
$this->serverRepository,
$this->userRepository, ]
);
}
}

View file

@ -4,6 +4,8 @@ namespace Tests\Unit\Http\Controllers\Base;
use Mockery as m; use Mockery as m;
use Pterodactyl\Models\User; use Pterodactyl\Models\User;
use Illuminate\Auth\AuthManager;
use Illuminate\Auth\SessionGuard;
use Prologue\Alerts\AlertsMessageBag; use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Services\Users\UserUpdateService; use Pterodactyl\Services\Users\UserUpdateService;
use Tests\Unit\Http\Controllers\ControllerTestCase; use Tests\Unit\Http\Controllers\ControllerTestCase;
@ -17,6 +19,16 @@ class AccountControllerTest extends ControllerTestCase
*/ */
protected $alert; protected $alert;
/**
* @var \Illuminate\Auth\AuthManager|\Mockery\Mock
*/
protected $authManager;
/**
* @var \Illuminate\Auth\SessionGuard|\Mockery\Mock
*/
protected $sessionGuard;
/** /**
* @var \Pterodactyl\Services\Users\UserUpdateService|\Mockery\Mock * @var \Pterodactyl\Services\Users\UserUpdateService|\Mockery\Mock
*/ */
@ -31,6 +43,10 @@ class AccountControllerTest extends ControllerTestCase
$this->alert = m::mock(AlertsMessageBag::class); $this->alert = m::mock(AlertsMessageBag::class);
$this->updateService = m::mock(UserUpdateService::class); $this->updateService = m::mock(UserUpdateService::class);
$this->authManager = m::mock(AuthManager::class);
$this->sessionGuard = m::mock(SessionGuard::class);
$this->authManager->shouldReceive('guard')->once()->andReturn($this->sessionGuard);
} }
/** /**
@ -50,13 +66,11 @@ class AccountControllerTest extends ControllerTestCase
public function testUpdateControllerForPassword() public function testUpdateControllerForPassword()
{ {
$this->setRequestMockClass(AccountDataFormRequest::class); $this->setRequestMockClass(AccountDataFormRequest::class);
$user = $this->generateRequestUserModel();
$this->request->shouldReceive('input')->with('do_action')->andReturn('password'); $this->request->shouldReceive('input')->with('do_action')->andReturn('password');
$this->request->shouldReceive('input')->with('new_password')->once()->andReturn('test-password'); $this->request->shouldReceive('input')->with('new_password')->once()->andReturn('test-password');
$this->sessionGuard->shouldReceive('logoutOtherDevices')->once()->with('test-password')->andReturnSelf();
$this->updateService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_USER)->once()->andReturnNull();
$this->updateService->shouldReceive('handle')->with($user, ['password' => 'test-password'])->once()->andReturn(collect());
$this->alert->shouldReceive('success->flash')->once()->andReturnNull(); $this->alert->shouldReceive('success->flash')->once()->andReturnNull();
$response = $this->getController()->update($this->request); $response = $this->getController()->update($this->request);
@ -113,6 +127,6 @@ class AccountControllerTest extends ControllerTestCase
*/ */
private function getController(): AccountController private function getController(): AccountController
{ {
return new AccountController($this->alert, $this->updateService); return new AccountController($this->alert, $this->authManager, $this->updateService);
} }
} }

View file

@ -182,7 +182,7 @@ class SubuserControllerTest extends ControllerTestCase
$response = $controller->store($this->request); $response = $controller->store($this->request);
$this->assertIsRedirectResponse($response); $this->assertIsRedirectResponse($response);
$this->assertRedirectRouteEquals('server.subusers.view', $response, [ $this->assertRedirectRouteEquals('server.subusers.view', $response, [
'uuid' => $server->uuid, 'uuid' => $server->uuidShort,
'id' => $subuser->hashid, 'id' => $subuser->hashid,
]); ]);
} }

View file

@ -0,0 +1,70 @@
<?php
namespace Tests\Unit\Http\Middleware;
use Mockery as m;
use Pterodactyl\Models\Node;
use Illuminate\Http\Response;
use Pterodactyl\Models\Server;
use Illuminate\Contracts\Routing\ResponseFactory;
use Pterodactyl\Http\Middleware\MaintenanceMiddleware;
class MaintenanceMiddlewareTest extends MiddlewareTestCase
{
/**
* @var \Illuminate\Contracts\Routing\ResponseFactory|\Mockery\Mock
*/
private $response;
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
$this->response = m::mock(ResponseFactory::class);
}
/**
* Test that a node not in maintenance mode continues through the request cycle.
*/
public function testHandle()
{
$server = factory(Server::class)->make();
$node = factory(Node::class)->make(['maintenance' => 0]);
$server->setRelation('node', $node);
$this->setRequestAttribute('server', $server);
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
}
/**
* Test that a node in maintenance mode returns an error view.
*/
public function testHandleInMaintenanceMode()
{
$server = factory(Server::class)->make();
$node = factory(Node::class)->make(['maintenance_mode' => 1]);
$server->setRelation('node', $node);
$this->setRequestAttribute('server', $server);
$this->response->shouldReceive('view')
->once()
->with('errors.maintenance')
->andReturn(new Response);
$response = $this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
$this->assertInstanceOf(Response::class, $response);
}
/**
* @return \Pterodactyl\Http\Middleware\MaintenanceMiddleware
*/
private function getMiddleware(): MaintenanceMiddleware
{
return new MaintenanceMiddleware($this->response);
}
}

View file

@ -1,62 +0,0 @@
<?php
namespace Tests\Unit\Services\Helpers;
use Mockery as m;
use Tests\TestCase;
use Tests\Traits\MocksUuids;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Services\Helpers\TemporaryPasswordService;
class TemporaryPasswordServiceTest extends TestCase
{
use MocksUuids;
/**
* @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock
*/
protected $connection;
/**
* @var \Illuminate\Contracts\Hashing\Hasher|\Mockery\Mock
*/
protected $hasher;
/**
* @var \Pterodactyl\Services\Helpers\TemporaryPasswordService
*/
protected $service;
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
$this->connection = m::mock(ConnectionInterface::class);
$this->hasher = m::mock(Hasher::class);
$this->service = new TemporaryPasswordService($this->connection, $this->hasher);
}
/**
* Test that a temporary password is stored and the token is returned.
*/
public function testTemporaryPasswordIsStored()
{
$token = hash_hmac(TemporaryPasswordService::HMAC_ALGO, $this->getKnownUuid(), config('app.key'));
$this->hasher->shouldReceive('make')->with($token)->once()->andReturn('hashed_token');
$this->connection->shouldReceive('table')->with('password_resets')->once()->andReturnSelf();
$this->connection->shouldReceive('insert')->with([
'email' => 'test@example.com',
'token' => 'hashed_token',
])->once()->andReturnNull();
$response = $this->service->handle('test@example.com');
$this->assertNotEmpty($response);
$this->assertEquals($token, $response);
}
}

View file

@ -12,7 +12,7 @@ use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class EnvironmentServiceTest extends TestCase class EnvironmentServiceTest extends TestCase
{ {
const CONFIG_MAPPING = 'pterodactyl.environment_mappings'; const CONFIG_MAPPING = 'pterodactyl.environment_variables';
/** /**
* @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock

View file

@ -9,9 +9,9 @@ use Tests\Traits\MocksUuids;
use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Notification;
use Illuminate\Contracts\Auth\PasswordBroker;
use Pterodactyl\Notifications\AccountCreated; use Pterodactyl\Notifications\AccountCreated;
use Pterodactyl\Services\Users\UserCreationService; use Pterodactyl\Services\Users\UserCreationService;
use Pterodactyl\Services\Helpers\TemporaryPasswordService;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
class UserCreationServiceTest extends TestCase class UserCreationServiceTest extends TestCase
@ -29,9 +29,9 @@ class UserCreationServiceTest extends TestCase
private $hasher; private $hasher;
/** /**
* @var \Pterodactyl\Services\Helpers\TemporaryPasswordService|\Mockery\Mock * @var \Illuminate\Contracts\Auth\PasswordBroker|\Mockery\Mock
*/ */
private $passwordService; private $passwordBroker;
/** /**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock
@ -48,7 +48,7 @@ class UserCreationServiceTest extends TestCase
Notification::fake(); Notification::fake();
$this->connection = m::mock(ConnectionInterface::class); $this->connection = m::mock(ConnectionInterface::class);
$this->hasher = m::mock(Hasher::class); $this->hasher = m::mock(Hasher::class);
$this->passwordService = m::mock(TemporaryPasswordService::class); $this->passwordBroker = m::mock(PasswordBroker::class);
$this->repository = m::mock(UserRepositoryInterface::class); $this->repository = m::mock(UserRepositoryInterface::class);
} }
@ -121,7 +121,7 @@ class UserCreationServiceTest extends TestCase
$this->hasher->shouldNotReceive('make'); $this->hasher->shouldNotReceive('make');
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->hasher->shouldReceive('make')->once()->andReturn('created-enc-password'); $this->hasher->shouldReceive('make')->once()->andReturn('created-enc-password');
$this->passwordService->shouldReceive('handle')->with($user->email)->once()->andReturn('random-token'); $this->passwordBroker->shouldReceive('createToken')->with($user)->once()->andReturn('random-token');
$this->repository->shouldReceive('create')->with([ $this->repository->shouldReceive('create')->with([
'password' => 'created-enc-password', 'password' => 'created-enc-password',
@ -152,6 +152,6 @@ class UserCreationServiceTest extends TestCase
*/ */
private function getService(): UserCreationService private function getService(): UserCreationService
{ {
return new UserCreationService($this->connection, $this->hasher, $this->passwordService, $this->repository); return new UserCreationService($this->connection, $this->hasher, $this->passwordBroker, $this->repository);
} }
} }