Merge branch 'develop' into feature/api-v1

This commit is contained in:
Dane Everitt 2017-12-14 21:12:17 -06:00
commit a1da8a3c9d
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
68 changed files with 1785 additions and 524 deletions

View file

@ -16,3 +16,4 @@ MAIL_DRIVER=array
QUEUE_DRIVER=sync QUEUE_DRIVER=sync
HASHIDS_SALT=test123 HASHIDS_SALT=test123
APP_ENVIRONMENT_ONLY=true

View file

@ -51,5 +51,5 @@ return PhpCsFixer\Config::create()
'equal' => false, 'equal' => false,
'identical' => false, 'identical' => false,
'less_and_greater' => false, 'less_and_greater' => false,
] ],
])->setRiskyAllowed(true)->setFinder($finder); ])->setRiskyAllowed(true)->setFinder($finder);

View file

@ -1,9 +1,13 @@
language: php language: php
dist: trusty dist: trusty
php: php:
- '7.0' - 7.0
- '7.1' - 7.1
# - '7.2' - 7.2
matrix:
fast_finish: true
allow_failures:
- php: 7.2
sudo: false sudo: false
cache: cache:
directories: directories:

View file

@ -10,6 +10,17 @@ This project follows [Semantic Versioning](http://semver.org) guidelines.
* `[beta.2]` — Fixes a bug that would throw a red page of death when submitting an invalid egg variable value for a server in the Admin CP. * `[beta.2]` — Fixes a bug that would throw a red page of death when submitting an invalid egg variable value for a server in the Admin CP.
* `[beta.2]` — Someone found a `@todo` that I never `@todid` and thus database hosts could not be created without being linked to a node. This is fixed... * `[beta.2]` — Someone found a `@todo` that I never `@todid` and thus database hosts could not be created without being linked to a node. This is fixed...
* `[beta.2]` — Fixes bug that caused incorrect rendering of CPU usage on server graphs due to missing variable. * `[beta.2]` — Fixes bug that caused incorrect rendering of CPU usage on server graphs due to missing variable.
* `[beta.2]` — Fixes bug causing schedules to be un-deletable.
* `[beta.2]` — Fixes bug that prevented the deletion of nodes due to an allocation deletion cascade issue with the SQL schema.
### Changed
* Revoking the administrative status for an admin will revoke all authentication tokens currently assigned to their account.
### Added
* Added star indicators to user listing in Admin CP to indicate users who are set as a root admin.
### Changed
* API keys have been changed to only use a single public key passed in a bearer token. All existing keys can continue being used, however only the first 32 characters should be sent.
### Changed ### Changed
* API keys have been changed to only use a single public key passed in a bearer token. All existing keys can continue being used, however only the first 32 characters should be sent. * API keys have been changed to only use a single public key passed in a bearer token. All existing keys can continue being used, however only the first 32 characters should be sent.

View file

@ -78,8 +78,10 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface
/** /**
* Revoke an access key on the daemon before the time is expired. * Revoke an access key on the daemon before the time is expired.
* *
* @param string $key * @param string|array $key
* @return \Psr\Http\Message\ResponseInterface * @return \Psr\Http\Message\ResponseInterface
*
* @throws \GuzzleHttp\Exception\RequestException
*/ */
public function revokeAccessKey($key); public function revokeAccessKey($key);
} }

View file

@ -24,7 +24,9 @@
namespace Pterodactyl\Contracts\Repository; namespace Pterodactyl\Contracts\Repository;
use Pterodactyl\Models\User;
use Pterodactyl\Models\DaemonKey; use Pterodactyl\Models\DaemonKey;
use Illuminate\Support\Collection;
interface DaemonKeyRepositoryInterface extends RepositoryInterface interface DaemonKeyRepositoryInterface extends RepositoryInterface
{ {
@ -59,4 +61,22 @@ interface DaemonKeyRepositoryInterface extends RepositoryInterface
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function getKeyWithServer($key); public function getKeyWithServer($key);
/**
* Get all of the keys for a specific user including the information needed
* from their server relation for revocation on the daemon.
*
* @param \Pterodactyl\Models\User $user
* @return \Illuminate\Support\Collection
*/
public function getKeysForRevocation(User $user): Collection;
/**
* Delete an array of daemon keys from the database. Used primarily in
* conjunction with getKeysForRevocation.
*
* @param array $ids
* @return bool|int
*/
public function deleteKeys(array $ids);
} }

View file

@ -0,0 +1,32 @@
<?php
namespace Pterodactyl\Contracts\Repository;
interface SettingsRepositoryInterface extends RepositoryInterface
{
/**
* Store a new persistent setting in the database.
*
* @param string $key
* @param string $value
* @return mixed
*/
public function set(string $key, string $value);
/**
* Retrieve a persistent setting from the database.
*
* @param string $key
* @param mixed $default
* @return mixed
*/
public function get(string $key, $default);
/**
* Remove a key from the database cache.
*
* @param string $key
* @return mixed
*/
public function forget(string $key);
}

View file

@ -1,11 +1,4 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Exceptions\Http\Connection; namespace Pterodactyl\Exceptions\Http\Connection;

View file

@ -1,51 +1,25 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Http\Controllers\Admin; namespace Pterodactyl\Http\Controllers\Admin;
use Krucas\Settings\Settings; use Illuminate\View\View;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Http\Requests\Admin\BaseFormRequest;
use Pterodactyl\Services\Helpers\SoftwareVersionService; use Pterodactyl\Services\Helpers\SoftwareVersionService;
class BaseController extends Controller class BaseController extends Controller
{ {
/**
* @var \Prologue\Alerts\AlertsMessageBag
*/
protected $alert;
/**
* @var \Krucas\Settings\Settings
*/
protected $settings;
/** /**
* @var \Pterodactyl\Services\Helpers\SoftwareVersionService * @var \Pterodactyl\Services\Helpers\SoftwareVersionService
*/ */
protected $version; private $version;
/** /**
* BaseController constructor. * BaseController constructor.
* *
* @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Krucas\Settings\Settings $settings
* @param \Pterodactyl\Services\Helpers\SoftwareVersionService $version * @param \Pterodactyl\Services\Helpers\SoftwareVersionService $version
*/ */
public function __construct( public function __construct(SoftwareVersionService $version)
AlertsMessageBag $alert, {
Settings $settings,
SoftwareVersionService $version
) {
$this->alert = $alert;
$this->settings = $settings;
$this->version = $version; $this->version = $version;
} }
@ -54,34 +28,8 @@ class BaseController extends Controller
* *
* @return \Illuminate\View\View * @return \Illuminate\View\View
*/ */
public function getIndex() public function index(): View
{ {
return view('admin.index', ['version' => $this->version]); return view('admin.index', ['version' => $this->version]);
} }
/**
* Return the admin settings view.
*
* @return \Illuminate\View\View
*/
public function getSettings()
{
return view('admin.settings');
}
/**
* Handle settings post request.
*
* @param \Pterodactyl\Http\Requests\Admin\BaseFormRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function postSettings(BaseFormRequest $request)
{
$this->settings->set('company', $request->input('company'));
$this->settings->set('2fa', $request->input('2fa'));
$this->alert->success('Settings have been successfully updated.')->flash();
return redirect()->route('admin.settings');
}
} }

View file

@ -0,0 +1,91 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Settings;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\Contracts\Console\Kernel;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
use Pterodactyl\Http\Requests\Admin\Settings\AdvancedSettingsFormRequest;
class AdvancedController extends Controller
{
/**
* @var \Prologue\Alerts\AlertsMessageBag
*/
private $alert;
/**
* @var \Illuminate\Contracts\Config\Repository
*/
private $config;
/**
* @var \Illuminate\Contracts\Console\Kernel
*/
private $kernel;
/**
* @var \Pterodactyl\Contracts\Repository\SettingsRepositoryInterface
*/
private $settings;
/**
* AdvancedController constructor.
*
* @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Illuminate\Contracts\Console\Kernel $kernel
* @param \Pterodactyl\Contracts\Repository\SettingsRepositoryInterface $settings
*/
public function __construct(
AlertsMessageBag $alert,
ConfigRepository $config,
Kernel $kernel,
SettingsRepositoryInterface $settings
) {
$this->alert = $alert;
$this->config = $config;
$this->kernel = $kernel;
$this->settings = $settings;
}
/**
* Render advanced Panel settings UI.
*
* @return \Illuminate\View\View
*/
public function index(): View
{
$showRecaptchaWarning = false;
if (
$this->config->get('recaptcha._shipped_secret_key') === $this->config->get('recaptcha.secret_key')
|| $this->config->get('recaptcha._shipped_website_key') === $this->config->get('recaptcha.website_key')
) {
$showRecaptchaWarning = true;
}
return view('admin.settings.advanced', [
'showRecaptchaWarning' => $showRecaptchaWarning,
]);
}
/**
* @param \Pterodactyl\Http\Requests\Admin\Settings\AdvancedSettingsFormRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function update(AdvancedSettingsFormRequest $request): RedirectResponse
{
foreach ($request->normalize() as $key => $value) {
$this->settings->set('settings::' . $key, $value);
}
$this->kernel->call('queue:restart');
$this->alert->success('Advanced settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash();
return redirect()->route('admin.settings.advanced');
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Settings;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\Contracts\Console\Kernel;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Traits\Helpers\AvailableLanguages;
use Pterodactyl\Services\Helpers\SoftwareVersionService;
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
use Pterodactyl\Http\Requests\Admin\Settings\BaseSettingsFormRequest;
class IndexController extends Controller
{
use AvailableLanguages;
/**
* @var \Prologue\Alerts\AlertsMessageBag
*/
private $alert;
/**
* @var \Illuminate\Contracts\Console\Kernel
*/
private $kernel;
/**
* @var \Pterodactyl\Contracts\Repository\SettingsRepositoryInterface
*/
private $settings;
/**
* @var \Pterodactyl\Services\Helpers\SoftwareVersionService
*/
private $versionService;
/**
* IndexController constructor.
*
* @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Illuminate\Contracts\Console\Kernel $kernel
* @param \Pterodactyl\Contracts\Repository\SettingsRepositoryInterface $settings
* @param \Pterodactyl\Services\Helpers\SoftwareVersionService $versionService
*/
public function __construct(
AlertsMessageBag $alert,
Kernel $kernel,
SettingsRepositoryInterface $settings,
SoftwareVersionService $versionService)
{
$this->alert = $alert;
$this->kernel = $kernel;
$this->settings = $settings;
$this->versionService = $versionService;
}
/**
* Render the UI for basic Panel settings.
*
* @return \Illuminate\View\View
*/
public function index(): View
{
return view('admin.settings.index', [
'version' => $this->versionService,
'languages' => $this->getAvailableLanguages(true),
]);
}
/**
* Handle settings update.
*
* @param \Pterodactyl\Http\Requests\Admin\Settings\BaseSettingsFormRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function update(BaseSettingsFormRequest $request): RedirectResponse
{
foreach ($request->normalize() as $key => $value) {
$this->settings->set('settings::' . $key, $value);
}
$this->kernel->call('queue:restart');
$this->alert->success('Panel settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash();
return redirect()->route('admin.settings');
}
}

View file

@ -0,0 +1,112 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Settings;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\Contracts\Console\Kernel;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Providers\SettingsServiceProvider;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
use Pterodactyl\Http\Requests\Admin\Settings\MailSettingsFormRequest;
class MailController extends Controller
{
/**
* @var \Prologue\Alerts\AlertsMessageBag
*/
private $alert;
/**
* @var \Illuminate\Contracts\Config\Repository
*/
private $config;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
private $encrypter;
/**
* @var \Illuminate\Contracts\Console\Kernel
*/
private $kernel;
/**
* @var \Pterodactyl\Contracts\Repository\SettingsRepositoryInterface
*/
private $settings;
/**
* MailController constructor.
*
* @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
* @param \Illuminate\Contracts\Console\Kernel $kernel
* @param \Pterodactyl\Contracts\Repository\SettingsRepositoryInterface $settings
*/
public function __construct(
AlertsMessageBag $alert,
ConfigRepository $config,
Encrypter $encrypter,
Kernel $kernel,
SettingsRepositoryInterface $settings
) {
$this->alert = $alert;
$this->config = $config;
$this->encrypter = $encrypter;
$this->kernel = $kernel;
$this->settings = $settings;
}
/**
* Render UI for editing mail settings. This UI should only display if
* the server is configured to send mail using SMTP.
*
* @return \Illuminate\View\View
*/
public function index(): View
{
return view('admin.settings.mail', [
'disabled' => $this->config->get('mail.driver') !== 'smtp',
]);
}
/**
* Handle request to update SMTP mail settings.
*
* @param \Pterodactyl\Http\Requests\Admin\Settings\MailSettingsFormRequest $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public function update(MailSettingsFormRequest $request): RedirectResponse
{
if ($this->config->get('mail.driver') !== 'smtp') {
throw new DisplayException('This feature is only available if SMTP is the selected email driver for the Panel.');
}
$values = $request->normalize();
if (array_get($values, 'mail:password') === '!e') {
$values['mail:password'] = '';
}
foreach ($values as $key => $value) {
if (in_array($key, SettingsServiceProvider::getEncryptedKeys()) && ! empty($value)) {
$value = $this->encrypter->encrypt($value);
}
$this->settings->set('settings::' . $key, $value);
}
$this->kernel->call('queue:restart');
$this->alert->success('Mail settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash();
return redirect()->route('admin.settings.mail');
}
}

View file

@ -1,11 +1,4 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Http\Controllers\Admin; namespace Pterodactyl\Http\Controllers\Admin;
@ -160,10 +153,30 @@ class UserController 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(UserFormRequest $request, User $user) public function update(UserFormRequest $request, User $user)
{ {
$this->updateService->handle($user->id, $request->normalize()); $this->updateService->setUserLevel(User::USER_LEVEL_ADMIN);
$data = $this->updateService->handle($user, $request->normalize());
if (! empty($data->get('exceptions'))) {
foreach ($data->get('exceptions') as $node => $exception) {
/** @var \GuzzleHttp\Exception\RequestException $exception */
/** @var \GuzzleHttp\Psr7\Response|null $response */
$response = method_exists($exception, 'getResponse') ? $exception->getResponse() : null;
$message = trans('admin/server.exceptions.daemon_exception', [
'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(),
]);
$this->alert->danger(trans('exceptions.users.node_revocation_failed', [
'node' => $node,
'error' => $message,
'link' => route('admin.nodes.view', $node),
]))->flash();
}
}
$this->alert->success($this->translator->trans('admin/user.notices.account_updated'))->flash(); $this->alert->success($this->translator->trans('admin/user.notices.account_updated'))->flash();
return redirect()->route('admin.users.view', $user->id); return redirect()->route('admin.users.view', $user->id);

View file

@ -1,30 +1,8 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>
* Some Modifications (c) 2015 Dylan Seidt <dylan.seidt@gmail.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Http\Controllers\Base; namespace Pterodactyl\Http\Controllers\Base;
use Pterodactyl\Models\User;
use Prologue\Alerts\AlertsMessageBag; use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Users\UserUpdateService; use Pterodactyl\Services\Users\UserUpdateService;
@ -48,10 +26,8 @@ class AccountController extends Controller
* @param \Prologue\Alerts\AlertsMessageBag $alert * @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Pterodactyl\Services\Users\UserUpdateService $updateService * @param \Pterodactyl\Services\Users\UserUpdateService $updateService
*/ */
public function __construct( public function __construct(AlertsMessageBag $alert, UserUpdateService $updateService)
AlertsMessageBag $alert, {
UserUpdateService $updateService
) {
$this->alert = $alert; $this->alert = $alert;
$this->updateService = $updateService; $this->updateService = $updateService;
} }
@ -74,6 +50,7 @@ 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)
{ {
@ -86,7 +63,8 @@ class AccountController extends Controller
$data = $request->only(['name_first', 'name_last', 'username']); $data = $request->only(['name_first', 'name_last', 'username']);
} }
$this->updateService->handle($request->user()->id, $data); $this->updateService->setUserLevel(User::USER_LEVEL_USER);
$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

@ -149,8 +149,6 @@ class TaskManagementController extends Controller
* *
* @param \Pterodactyl\Http\Requests\Server\ScheduleCreationFormRequest $request * @param \Pterodactyl\Http\Requests\Server\ScheduleCreationFormRequest $request
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/ */
public function update(ScheduleCreationFormRequest $request): RedirectResponse public function update(ScheduleCreationFormRequest $request): RedirectResponse
{ {
@ -177,7 +175,7 @@ class TaskManagementController extends Controller
*/ */
public function delete(Request $request): Response public function delete(Request $request): Response
{ {
$server = $request->attributes->get('server_data.model'); $server = $request->attributes->get('server');
$schedule = $request->attributes->get('schedule'); $schedule = $request->attributes->get('schedule');
$this->authorize('delete-schedule', $server); $this->authorize('delete-schedule', $server);

View file

@ -21,6 +21,8 @@ class AdminAuthenticate
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \Closure $next * @param \Closure $next
* @return mixed * @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {

View file

@ -46,6 +46,8 @@ class DaemonAuthenticate
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \Closure $next * @param \Closure $next
* @return mixed * @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {

View file

@ -11,7 +11,6 @@ namespace Pterodactyl\Http\Middleware;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Krucas\Settings\Settings;
use Prologue\Alerts\AlertsMessageBag; use Prologue\Alerts\AlertsMessageBag;
class RequireTwoFactorAuthentication class RequireTwoFactorAuthentication
@ -25,11 +24,6 @@ class RequireTwoFactorAuthentication
*/ */
private $alert; private $alert;
/**
* @var \Krucas\Settings\Settings
*/
private $settings;
/** /**
* The names of routes that should be accessable without 2FA enabled. * The names of routes that should be accessable without 2FA enabled.
* *
@ -56,12 +50,10 @@ class RequireTwoFactorAuthentication
* RequireTwoFactorAuthentication constructor. * RequireTwoFactorAuthentication constructor.
* *
* @param \Prologue\Alerts\AlertsMessageBag $alert * @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Krucas\Settings\Settings $settings
*/ */
public function __construct(AlertsMessageBag $alert, Settings $settings) public function __construct(AlertsMessageBag $alert)
{ {
$this->alert = $alert; $this->alert = $alert;
$this->settings = $settings;
} }
/** /**
@ -81,10 +73,7 @@ class RequireTwoFactorAuthentication
return $next($request); return $next($request);
} }
switch ((int) $this->settings->get('2fa', 0)) { switch ((int) config('pterodactyl.auth.2fa_required')) {
case self::LEVEL_NONE:
return $next($request);
break;
case self::LEVEL_ADMIN: case self::LEVEL_ADMIN:
if (! $request->user()->root_admin || $request->user()->use_totp) { if (! $request->user()->root_admin || $request->user()->use_totp) {
return $next($request); return $next($request);
@ -95,6 +84,9 @@ class RequireTwoFactorAuthentication
return $next($request); return $next($request);
} }
break; break;
case self::LEVEL_NONE:
default:
return $next($request);
} }
$this->alert->danger(trans('auth.2fa_must_be_enabled'))->flash(); $this->alert->danger(trans('auth.2fa_must_be_enabled'))->flash();

View file

@ -47,9 +47,8 @@ class AuthenticateAsSubuser
* @param \Closure $next * @param \Closure $next
* @return mixed * @return mixed
* *
* @throws \Illuminate\Auth\AuthenticationException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {

View file

@ -49,7 +49,7 @@ class ScheduleBelongsToServer
$scheduleId = $this->hashids->decodeFirst($request->route()->parameter('schedule'), 0); $scheduleId = $this->hashids->decodeFirst($request->route()->parameter('schedule'), 0);
$schedule = $this->repository->getScheduleWithTasks($scheduleId); $schedule = $this->repository->getScheduleWithTasks($scheduleId);
if (object_get($schedule, 'server_id') !== $server->id) { if ($schedule->server_id !== $server->id) {
throw new NotFoundHttpException; throw new NotFoundHttpException;
} }

View file

@ -0,0 +1,42 @@
<?php
namespace Pterodactyl\Http\Requests\Admin\Settings;
use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
class AdvancedSettingsFormRequest extends AdminFormRequest
{
/**
* Return all of the rules to apply to this request's data.
*
* @return array
*/
public function rules()
{
return [
'recaptcha:enabled' => 'required|in:true,false',
'recaptcha:secret_key' => 'required|string|max:255',
'recaptcha:website_key' => 'required|string|max:255',
'pterodactyl:guzzle:timeout' => 'required|integer|between:1,60',
'pterodactyl:guzzle:connect_timeout' => 'required|integer|between:1,60',
'pterodactyl:console:count' => 'required|integer|min:1',
'pterodactyl:console:frequency' => 'required|integer|min:10',
];
}
/**
* @return array
*/
public function attributes()
{
return [
'recaptcha:enabled' => 'reCAPTCHA Enabled',
'recaptcha:secret_key' => 'reCAPTCHA Secret Key',
'recaptcha:website_key' => 'reCAPTCHA Website Key',
'pterodactyl:guzzle:timeout' => 'HTTP Request Timeout',
'pterodactyl:guzzle:connect_timeout' => 'HTTP Connection Timeout',
'pterodactyl:console:count' => 'Console Message Count',
'pterodactyl:console:frequency' => 'Console Frequency Tick',
];
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Pterodactyl\Http\Requests\Admin\Settings;
use Illuminate\Validation\Rule;
use Pterodactyl\Traits\Helpers\AvailableLanguages;
use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
class BaseSettingsFormRequest extends AdminFormRequest
{
use AvailableLanguages;
/**
* @return array
*/
public function rules()
{
return [
'app:name' => 'required|string|max:255',
'pterodactyl:auth:2fa_required' => 'required|integer|in:0,1,2',
'app:locale' => ['required', 'string', Rule::in(array_keys($this->getAvailableLanguages()))],
];
}
/**
* @return array
*/
public function attributes()
{
return [
'app:name' => 'Company Name',
'pterodactyl:auth:2fa_required' => 'Require 2-Factor Authentication',
'app:locale' => 'Default Language',
];
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Pterodactyl\Http\Requests\Admin\Settings;
use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
class MailSettingsFormRequest extends AdminFormRequest
{
/**
* Return rules to validate mail settings POST data aganist.
*
* @return array
*/
public function rules()
{
return [
'mail:host' => 'required|string',
'mail:port' => 'required|integer|between:1,65535',
'mail:encryption' => 'present|string|in:"",tls,ssl',
'mail:username' => 'string|max:255',
'mail:password' => 'string|max:255',
'mail:from:address' => 'required|string|email',
'mail:from:name' => 'string|max:255',
];
}
/**
* Override the default normalization function for this type of request
* as we need to accept empty values on the keys.
*
* @param array $only
* @return array
*/
public function normalize($only = [])
{
$keys = array_flip(array_keys($this->rules()));
if (empty($this->input('mail:password'))) {
unset($keys['mail:password']);
}
return $this->only(array_flip($keys));
}
}

View file

@ -19,7 +19,11 @@ class UserFormRequest extends AdminFormRequest
public function rules() public function rules()
{ {
if ($this->method() === 'PATCH') { if ($this->method() === 'PATCH') {
return User::getUpdateRulesForId($this->route()->parameter('user')->id); $rules = User::getUpdateRulesForId($this->route()->parameter('user')->id);
return array_merge($rules, [
'ignore_connection_error' => 'sometimes|nullable|boolean',
]);
} }
return User::getCreateRules(); return User::getCreateRules();
@ -30,7 +34,7 @@ class UserFormRequest extends AdminFormRequest
if ($this->method === 'PATCH') { if ($this->method === 'PATCH') {
return array_merge( return array_merge(
$this->intersect('password'), $this->intersect('password'),
$this->only(['email', 'username', 'name_first', 'name_last', 'root_admin']) $this->only(['email', 'username', 'name_first', 'name_last', 'root_admin', 'ignore_connection_error'])
); );
} }

39
app/Models/Setting.php Normal file
View file

@ -0,0 +1,39 @@
<?php
namespace Pterodactyl\Models;
use Sofa\Eloquence\Eloquence;
use Sofa\Eloquence\Validable;
use Illuminate\Database\Eloquent\Model;
use Sofa\Eloquence\Contracts\CleansAttributes;
use Sofa\Eloquence\Contracts\Validable as ValidableContract;
class Setting extends Model implements CleansAttributes, ValidableContract
{
use Eloquence, Validable;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'settings';
/**
* @var bool
*/
public $timestamps = false;
/**
* @var array
*/
protected $fillable = ['key', 'value'];
/**
* @var array
*/
protected static $applicationRules = [
'key' => 'required|string|between:1,255',
'value' => 'string',
];
}

View file

@ -13,7 +13,6 @@ use Pterodactyl\Observers\UserObserver;
use Pterodactyl\Observers\ServerObserver; use Pterodactyl\Observers\ServerObserver;
use Pterodactyl\Observers\SubuserObserver; use Pterodactyl\Observers\SubuserObserver;
use Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider; use Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider;
use DaneEveritt\LoginNotifications\NotificationServiceProvider;
use Barryvdh\Debugbar\ServiceProvider as DebugbarServiceProvider; use Barryvdh\Debugbar\ServiceProvider as DebugbarServiceProvider;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
@ -42,10 +41,6 @@ class AppServiceProvider extends ServiceProvider
$this->app->register(DebugbarServiceProvider::class); $this->app->register(DebugbarServiceProvider::class);
$this->app->register(IdeHelperServiceProvider::class); $this->app->register(IdeHelperServiceProvider::class);
} }
if (config('pterodactyl.auth.notifications')) {
$this->app->register(NotificationServiceProvider::class);
}
} }
/** /**

View file

@ -26,6 +26,7 @@ use Pterodactyl\Repositories\Eloquent\SubuserRepository;
use Pterodactyl\Repositories\Eloquent\DatabaseRepository; use Pterodactyl\Repositories\Eloquent\DatabaseRepository;
use Pterodactyl\Repositories\Eloquent\LocationRepository; use Pterodactyl\Repositories\Eloquent\LocationRepository;
use Pterodactyl\Repositories\Eloquent\ScheduleRepository; use Pterodactyl\Repositories\Eloquent\ScheduleRepository;
use Pterodactyl\Repositories\Eloquent\SettingsRepository;
use Pterodactyl\Repositories\Eloquent\DaemonKeyRepository; use Pterodactyl\Repositories\Eloquent\DaemonKeyRepository;
use Pterodactyl\Repositories\Eloquent\AllocationRepository; use Pterodactyl\Repositories\Eloquent\AllocationRepository;
use Pterodactyl\Repositories\Eloquent\PermissionRepository; use Pterodactyl\Repositories\Eloquent\PermissionRepository;
@ -47,6 +48,7 @@ use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface; use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface;
@ -86,10 +88,13 @@ class RepositoryServiceProvider extends ServiceProvider
$this->app->bind(ServerRepositoryInterface::class, ServerRepository::class); $this->app->bind(ServerRepositoryInterface::class, ServerRepository::class);
$this->app->bind(ServerVariableRepositoryInterface::class, ServerVariableRepository::class); $this->app->bind(ServerVariableRepositoryInterface::class, ServerVariableRepository::class);
$this->app->bind(SessionRepositoryInterface::class, SessionRepository::class); $this->app->bind(SessionRepositoryInterface::class, SessionRepository::class);
$this->app->bind(SettingsRepositoryInterface::class, SettingsRepository::class);
$this->app->bind(SubuserRepositoryInterface::class, SubuserRepository::class); $this->app->bind(SubuserRepositoryInterface::class, SubuserRepository::class);
$this->app->bind(TaskRepositoryInterface::class, TaskRepository::class); $this->app->bind(TaskRepositoryInterface::class, TaskRepository::class);
$this->app->bind(UserRepositoryInterface::class, UserRepository::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class);
$this->app->alias(SettingsRepositoryInterface::class, 'settings');
// Daemon Repositories // Daemon Repositories
if ($this->app->make('config')->get('pterodactyl.daemon.use_new_daemon')) { if ($this->app->make('config')->get('pterodactyl.daemon.use_new_daemon')) {
$this->app->bind(ConfigurationRepositoryInterface::class, \Pterodactyl\Repositories\Wings\ConfigurationRepository::class); $this->app->bind(ConfigurationRepositoryInterface::class, \Pterodactyl\Repositories\Wings\ConfigurationRepository::class);

View file

@ -0,0 +1,101 @@
<?php
namespace Pterodactyl\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
class SettingsServiceProvider extends ServiceProvider
{
/**
* An array of configuration keys to override with database values
* if they exist.
*
* @var array
*/
protected $keys = [
'app:name',
'app:locale',
'recaptcha:enabled',
'recaptcha:secret_key',
'recaptcha:website_key',
'pterodactyl:guzzle:timeout',
'pterodactyl:guzzle:connect_timeout',
'pterodactyl:console:count',
'pterodactyl:console:frequency',
'pterodactyl:auth:2fa_required',
];
/**
* Keys specific to the mail driver that are only grabbed from the database
* when using the SMTP driver.
*
* @var array
*/
protected $emailKeys = [
'mail:host',
'mail:port',
'mail:from:address',
'mail:from:name',
'mail:encryption',
'mail:username',
'mail:password',
];
/**
* Keys that are encrypted and should be decrypted when set in the
* configuration array.
*
* @var array
*/
protected static $encrypted = [
'mail:password',
];
/**
* Boot the service provider.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
* @param \Pterodactyl\Contracts\Repository\SettingsRepositoryInterface $settings
*/
public function boot(ConfigRepository $config, Encrypter $encrypter, SettingsRepositoryInterface $settings)
{
if ($config->get('pterodactyl.load_environment_only', false)) {
return;
}
// Only set the email driver settings from the database if we
// are configured using SMTP as the driver.
if ($config->get('mail.driver') === 'smtp') {
$this->keys = array_merge($this->keys, $this->emailKeys);
}
$values = $settings->all()->mapWithKeys(function ($setting) {
return [$setting->key => $setting->value];
})->toArray();
foreach ($this->keys as $key) {
$value = array_get($values, 'settings::' . $key, $config->get(str_replace(':', '.', $key)));
if (in_array($key, self::$encrypted)) {
try {
$value = $encrypter->decrypt($value);
} catch (DecryptException $exception) {
}
}
$config->set(str_replace(':', '.', $key), $value);
}
}
/**
* @return array
*/
public static function getEncryptedKeys(): array
{
return self::$encrypted;
}
}

View file

@ -107,7 +107,13 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa
*/ */
public function revokeAccessKey($key) public function revokeAccessKey($key)
{ {
Assert::stringNotEmpty($key, 'First argument passed to revokeAccessKey must be a non-empty string, received %s.'); if (is_array($key)) {
return $this->getHttpClient()->request('POST', 'keys', [
'json' => $key,
]);
}
Assert::stringNotEmpty($key, 'First argument passed to revokeAccessKey must be a non-empty string or array, received %s.');
return $this->getHttpClient()->request('DELETE', 'keys/' . $key); return $this->getHttpClient()->request('DELETE', 'keys/' . $key);
} }

View file

@ -24,8 +24,10 @@
namespace Pterodactyl\Repositories\Eloquent; namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\User;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use Pterodactyl\Models\DaemonKey; use Pterodactyl\Models\DaemonKey;
use Illuminate\Support\Collection;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
@ -83,4 +85,28 @@ class DaemonKeyRepository extends EloquentRepository implements DaemonKeyReposit
return $instance; return $instance;
} }
/**
* Get all of the keys for a specific user including the information needed
* from their server relation for revocation on the daemon.
*
* @param \Pterodactyl\Models\User $user
* @return \Illuminate\Support\Collection
*/
public function getKeysForRevocation(User $user): Collection
{
return $this->getBuilder()->with('server:id,uuid,node_id')->where('user_id', $user->id)->get($this->getColumns());
}
/**
* Delete an array of daemon keys from the database. Used primarily in
* conjunction with getKeysForRevocation.
*
* @param array $ids
* @return bool|int
*/
public function deleteKeys(array $ids)
{
return $this->getBuilder()->whereIn('id', $ids)->delete();
}
} }

View file

@ -18,11 +18,6 @@ class LocationRepository extends EloquentRepository implements LocationRepositor
{ {
use Searchable; use Searchable;
/**
* @var string
*/
protected $searchTerm;
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */

View file

@ -0,0 +1,96 @@
<?php
namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\Setting;
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
class SettingsRepository extends EloquentRepository implements SettingsRepositoryInterface
{
/**
* @var array
*/
private $cache = [];
/**
* @var array
*/
private $databaseMiss = [];
/**
* Return an instance of the model that acts as the base for
* this repository.
*
* @return string
*/
public function model()
{
return Setting::class;
}
/**
* Store a new persistent setting in the database.
*
* @param string $key
* @param string $value
*/
public function set(string $key, string $value)
{
// Clear item from the cache.
$this->clearCache($key);
$this->withoutFresh()->updateOrCreate(['key' => $key], ['value' => $value]);
}
/**
* Retrieve a persistent setting from the database.
*
* @param string $key
* @param mixed $default
* @return mixed
*/
public function get(string $key, $default = null)
{
// If item has already been requested return it from the cache. If
// we already know it is missing, immediately return the default
// value.
if (array_key_exists($key, $this->cache)) {
return $this->cache[$key];
} elseif (array_key_exists($key, $this->databaseMiss)) {
return $default;
}
$instance = $this->getBuilder()->where('key', $key)->first();
if (is_null($instance)) {
$this->databaseMiss[$key] = true;
return $default;
}
$this->cache[$key] = $instance->value;
return $this->cache[$key];
}
/**
* Remove a key from the database cache.
*
* @param string $key
* @return mixed
*/
public function forget(string $key)
{
$this->clearCache($key);
$this->deleteWhere(['key' => $key]);
}
/**
* Remove a key from the cache.
*
* @param string $key
*/
protected function clearCache(string $key)
{
unset($this->cache[$key], $this->databaseMiss[$key]);
}
}

View file

@ -0,0 +1,91 @@
<?php
namespace Pterodactyl\Services\DaemonKeys;
use Pterodactyl\Models\User;
use GuzzleHttp\Exception\RequestException;
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepository;
class RevokeMultipleDaemonKeysService
{
/**
* @var array
*/
protected $exceptions = [];
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface
*/
private $daemonRepository;
/**
* @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface
*/
private $repository;
/**
* RevokeMultipleDaemonKeysService constructor.
*
* @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $repository
* @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonRepository
*/
public function __construct(
DaemonKeyRepositoryInterface $repository,
DaemonServerRepository $daemonRepository
) {
$this->daemonRepository = $daemonRepository;
$this->repository = $repository;
}
/**
* Grab all of the keys that exist for a single user and delete them from all
* daemon's that they are assigned to. If connection fails, this function will
* return an error.
*
* @param \Pterodactyl\Models\User $user
* @param bool $ignoreConnectionErrors
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function handle(User $user, bool $ignoreConnectionErrors = false)
{
$keys = $this->repository->getKeysForRevocation($user);
$keys->groupBy('server.node_id')->each(function ($group, $node) use ($ignoreConnectionErrors) {
try {
$this->daemonRepository->setNode($node)->revokeAccessKey(collect($group)->pluck('secret')->toArray());
} catch (RequestException $exception) {
if (! $ignoreConnectionErrors) {
throw new DaemonConnectionException($exception);
}
$this->setConnectionException($node, $exception);
}
$this->repository->deleteKeys(collect($group)->pluck('id')->toArray());
});
}
/**
* Returns an array of exceptions that were returned by the handle function.
*
* @return RequestException[]
*/
public function getExceptions()
{
return $this->exceptions;
}
/**
* Add an exception for a node to the array.
*
* @param int $node
* @param \GuzzleHttp\Exception\RequestException $exception
*/
protected function setConnectionException(int $node, RequestException $exception)
{
$this->exceptions[$node] = $exception;
}
}

View file

@ -1,59 +1,79 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Services\Users; namespace Pterodactyl\Services\Users;
use Pterodactyl\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Contracts\Hashing\Hasher;
use Pterodactyl\Traits\Services\HasUserLevels;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService;
class UserUpdateService class UserUpdateService
{ {
use HasUserLevels;
/** /**
* @var \Illuminate\Contracts\Hashing\Hasher * @var \Illuminate\Contracts\Hashing\Hasher
*/ */
protected $hasher; private $hasher;
/** /**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
*/ */
protected $repository; private $repository;
/**
* @var \Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService
*/
private $revocationService;
/** /**
* UpdateService constructor. * UpdateService constructor.
* *
* @param \Illuminate\Contracts\Hashing\Hasher $hasher * @param \Illuminate\Contracts\Hashing\Hasher $hasher
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository * @param \Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService $revocationService
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
*/ */
public function __construct( public function __construct(
Hasher $hasher, Hasher $hasher,
RevokeMultipleDaemonKeysService $revocationService,
UserRepositoryInterface $repository UserRepositoryInterface $repository
) { ) {
$this->hasher = $hasher; $this->hasher = $hasher;
$this->repository = $repository; $this->repository = $repository;
$this->revocationService = $revocationService;
} }
/** /**
* Update the user model instance. * Update the user model instance. If the user has been removed as an administrator
* revoke all of the authentication tokens that have beenn assigned to their account.
* *
* @param int $id * @param \Pterodactyl\Models\User $user
* @param array $data * @param array $data
* @return mixed * @return \Illuminate\Support\Collection
* *
* @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 handle($id, array $data) public function handle(User $user, array $data): Collection
{ {
if (isset($data['password'])) { if (array_has($data, 'password')) {
$data['password'] = $this->hasher->make($data['password']); $data['password'] = $this->hasher->make($data['password']);
} }
return $this->repository->update($id, $data); if ($this->isUserLevel(User::USER_LEVEL_ADMIN)) {
if (array_get($data, 'root_admin', 0) == 0 && $user->root_admin) {
$this->revocationService->handle($user, array_get($data, 'ignore_connection_error', false));
}
} else {
unset($data['root_admin']);
}
return collect([
'model' => $this->repository->update($user->id, $data),
'exceptions' => $this->revocationService->getExceptions(),
]);
} }
} }

View file

@ -0,0 +1,56 @@
<?php
namespace Pterodactyl\Traits\Helpers;
use Matriphe\ISO639\ISO639;
use Illuminate\Filesystem\Filesystem;
trait AvailableLanguages
{
/**
* @var \Illuminate\Filesystem\Filesystem
*/
private $filesystem;
/**
* @var \Matriphe\ISO639\ISO639
*/
private $iso639;
/**
* Return all of the available languages on the Panel based on those
* that are present in the language folder.
*
* @param bool $localize
* @return array
*/
public function getAvailableLanguages($localize = false): array
{
return collect($this->getFilesystemInstance()->directories(resource_path('lang')))->mapWithKeys(function ($path) use ($localize) {
$code = basename($path);
$value = $localize ? $this->getIsoInstance()->nativeByCode1($code) : $this->getIsoInstance()->languageByCode1($code);
return [$code => title_case($value)];
})->toArray();
}
/**
* Return an instance of the filesystem for getting a folder listing.
*
* @return \Illuminate\Filesystem\Filesystem
*/
private function getFilesystemInstance(): Filesystem
{
return $this->filesystem = $this->filesystem ?: app()->make(Filesystem::class);
}
/**
* Return an instance of the ISO639 class for generating names.
*
* @return \Matriphe\ISO639\ISO639
*/
private function getIsoInstance(): ISO639
{
return $this->iso639 = $this->iso639 ?: app()->make(ISO639::class);
}
}

View file

@ -28,6 +28,7 @@
"laravel/framework": "5.4.27", "laravel/framework": "5.4.27",
"laravel/tinker": "1.0.1", "laravel/tinker": "1.0.1",
"lord/laroute": "~2.4.5", "lord/laroute": "~2.4.5",
"matriphe/iso-639": "^1.2",
"mtdowling/cron-expression": "^1.2", "mtdowling/cron-expression": "^1.2",
"nesbot/carbon": "^1.22", "nesbot/carbon": "^1.22",
"nicolaslopezj/searchable": "^1.9", "nicolaslopezj/searchable": "^1.9",

97
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "a393763d136e25a93fd5b636229496cf", "content-hash": "bd42f43877e96cca4d4af755c590eb25",
"packages": [ "packages": [
{ {
"name": "appstract/laravel-blade-directives", "name": "appstract/laravel-blade-directives",
@ -687,57 +687,6 @@
], ],
"time": "2014-09-09T13:34:57+00:00" "time": "2014-09-09T13:34:57+00:00"
}, },
{
"name": "edvinaskrucas/settings",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/edvinaskrucas/settings.git",
"reference": "23f2a912ca8f5b6ba550721a6fc0e6d1acaa9022"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/edvinaskrucas/settings/zipball/23f2a912ca8f5b6ba550721a6fc0e6d1acaa9022",
"reference": "23f2a912ca8f5b6ba550721a6fc0e6d1acaa9022",
"shasum": ""
},
"require": {
"illuminate/console": "^5.2",
"illuminate/database": "^5.2",
"illuminate/filesystem": "^5.2",
"illuminate/support": "^5.2",
"php": "^5.5|^7.0"
},
"require-dev": {
"mockery/mockery": "0.9.*"
},
"type": "library",
"autoload": {
"psr-0": {
"Krucas\\Settings\\": "src/"
},
"files": [
"src/helpers.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Edvinas Kručas",
"email": "edv.krucas@gmail.com"
}
],
"description": "Persistent settings package for Laravel framework.",
"keywords": [
"Settings",
"laravel",
"persistent settings"
],
"time": "2016-01-19T13:50:39+00:00"
},
{ {
"name": "erusev/parsedown", "name": "erusev/parsedown",
"version": "1.6.3", "version": "1.6.3",
@ -1674,6 +1623,50 @@
], ],
"time": "2017-09-04T02:25:29+00:00" "time": "2017-09-04T02:25:29+00:00"
}, },
{
"name": "matriphe/iso-639",
"version": "1.2",
"source": {
"type": "git",
"url": "https://github.com/matriphe/php-iso-639.git",
"reference": "0245d844daeefdd22a54b47103ffdb0e03c323e1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/matriphe/php-iso-639/zipball/0245d844daeefdd22a54b47103ffdb0e03c323e1",
"reference": "0245d844daeefdd22a54b47103ffdb0e03c323e1",
"shasum": ""
},
"require-dev": {
"phpunit/phpunit": "^4.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Matriphe\\ISO639\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Muhammad Zamroni",
"email": "halo@matriphe.com"
}
],
"description": "PHP library to convert ISO-639-1 code to language name.",
"keywords": [
"639",
"iso",
"iso-639",
"lang",
"language",
"laravel"
],
"time": "2017-07-19T15:11:19+00:00"
},
{ {
"name": "monolog/monolog", "name": "monolog/monolog",
"version": "1.23.0", "version": "1.23.0",

View file

@ -1,5 +1,6 @@
<?php <?php
return [ return [
'env' => env('APP_ENV', 'production'), 'env' => env('APP_ENV', 'production'),
@ -14,7 +15,7 @@ return [
| framework needs to place the application's name in a notification or | framework needs to place the application's name in a notification or
| any other location as required by the application or its packages. | any other location as required by the application or its packages.
*/ */
'name' => 'Pterodactyl', 'name' => env('APP_NAME', 'Pterodactyl'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -158,6 +159,7 @@ return [
/* /*
* Application Service Providers... * Application Service Providers...
*/ */
Pterodactyl\Providers\SettingsServiceProvider::class,
Pterodactyl\Providers\AppServiceProvider::class, Pterodactyl\Providers\AppServiceProvider::class,
Pterodactyl\Providers\AuthServiceProvider::class, Pterodactyl\Providers\AuthServiceProvider::class,
Pterodactyl\Providers\EventServiceProvider::class, Pterodactyl\Providers\EventServiceProvider::class,
@ -173,7 +175,6 @@ return [
*/ */
igaster\laravelTheme\themeServiceProvider::class, igaster\laravelTheme\themeServiceProvider::class,
Prologue\Alerts\AlertsServiceProvider::class, Prologue\Alerts\AlertsServiceProvider::class,
Krucas\Settings\Providers\SettingsServiceProvider::class,
Fideloper\Proxy\TrustedProxyServiceProvider::class, Fideloper\Proxy\TrustedProxyServiceProvider::class,
Laracasts\Utilities\JavaScript\JavaScriptServiceProvider::class, Laracasts\Utilities\JavaScript\JavaScriptServiceProvider::class,
Lord\Laroute\LarouteServiceProvider::class, Lord\Laroute\LarouteServiceProvider::class,
@ -228,7 +229,6 @@ return [
'Response' => Illuminate\Support\Facades\Response::class, 'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class, 'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class, 'Schema' => Illuminate\Support\Facades\Schema::class,
'Settings' => Krucas\Settings\Facades\Settings::class,
'Session' => Illuminate\Support\Facades\Session::class, 'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class, 'Storage' => Illuminate\Support\Facades\Storage::class,
'Theme' => igaster\laravelTheme\Facades\Theme::class, 'Theme' => igaster\laravelTheme\Facades\Theme::class,

View file

@ -1,6 +1,18 @@
<?php <?php
return [ return [
/*
|--------------------------------------------------------------------------
| Restricted Environment
|--------------------------------------------------------------------------
|
| Set this environment variable to true to enable a restricted configuration
| setup on the panel. When set to true, configurations stored in the
| database will not be applied.
*/
'load_environment_only' => (bool) env('APP_ENVIRONMENT_ONLY', false),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Service Author | Service Author
@ -22,7 +34,7 @@ return [
| Should login success and failure events trigger an email to the user? | Should login success and failure events trigger an email to the user?
*/ */
'auth' => [ 'auth' => [
'notifications' => env('LOGIN_NOTIFICATIONS', false), '2fa_required' => env('APP_2FA_REQUIRED', 0),
'2fa' => [ '2fa' => [
'bytes' => 32, 'bytes' => 32,
'window' => env('APP_2FA_WINDOW', 4), 'window' => env('APP_2FA_WINDOW', 4),

View file

@ -14,12 +14,14 @@ return [
/* /*
* Use a custom secret key, we use our public one by default * Use a custom secret key, we use our public one by default
*/ */
'secret_key' => env('RECAPTCHA_SECRET_KEY', '6LekAxoUAAAAAPW-PxNWaCLH76WkClMLSa2jImwD'), 'secret_key' => env('RECAPTCHA_SECRET_KEY', '6LcJcjwUAAAAALOcDJqAEYKTDhwELCkzUkNDQ0J5'),
'_shipped_secret_key' => '6LcJcjwUAAAAALOcDJqAEYKTDhwELCkzUkNDQ0J5',
/* /*
* Use a custom website key, we use our public one by default * Use a custom website key, we use our public one by default
*/ */
'website_key' => env('RECAPTCHA_WEBSITE_KEY', '6LekAxoUAAAAADjWZJ4ufcDRZBBiH9vfHawqRbup'), 'website_key' => env('RECAPTCHA_WEBSITE_KEY', '6LcJcjwUAAAAAO_Xqjrtj9wWufUpYRnK6BW8lnfn'),
'_shipped_website_key' => '6LcJcjwUAAAAAO_Xqjrtj9wWufUpYRnK6BW8lnfn',
/* /*
* Domain verification is enabled by default and compares the domain used when solving the captcha * Domain verification is enabled by default and compares the domain used when solving the captcha

View file

@ -1,113 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Settings Driver
|--------------------------------------------------------------------------
|
| Settings driver used to store persistent settings.
|
| Supported: "database"
|
*/
'default' => env('SETTINGS_DRIVER', 'database'),
/*
|--------------------------------------------------------------------------
| Enable / Disable caching
|--------------------------------------------------------------------------
|
| If it is enabled all values gets cached after accessing it.
|
*/
'cache' => true,
/*
|--------------------------------------------------------------------------
| Enable / Disable value encryption
|--------------------------------------------------------------------------
|
| If it is enabled all values gets encrypted and decrypted.
|
*/
'encryption' => env('SETTINGS_ENCRYPTION', false),
/*
|--------------------------------------------------------------------------
| Enable / Disable events
|--------------------------------------------------------------------------
|
| If it is enabled various settings related events will be fired.
|
*/
'events' => true,
/*
|--------------------------------------------------------------------------
| Repositories Configuration
|--------------------------------------------------------------------------
|
| Here you may configure the driver information for each repository that
| is used by your application. A default configuration has been added
| for each back-end shipped with this package. You are free to add more.
|
*/
'repositories' => [
'database' => [
'driver' => 'database',
'connection' => env('DB_CONNECTION', 'mysql'),
'table' => 'settings',
],
],
/*
|--------------------------------------------------------------------------
| Key generator class
|--------------------------------------------------------------------------
|
| Key generator is used to generate keys based on setting key and context.
|
*/
'key_generator' => \Krucas\Settings\KeyGenerators\KeyGenerator::class,
/*
|--------------------------------------------------------------------------
| Context serializer class
|--------------------------------------------------------------------------
|
| Context serializer serializes context.
| It is used with "Krucas\Settings\KeyGenerators\KeyGenerator" class.
|
*/
'context_serializer' => \Krucas\Settings\ContextSerializers\ContextSerializer::class,
/*
|--------------------------------------------------------------------------
| Value serializer class
|--------------------------------------------------------------------------
|
| Value serializer serializes / unserializes given value.
|
*/
'value_serializer' => \Krucas\Settings\ValueSerializers\ValueSerializer::class,
/*
|--------------------------------------------------------------------------
| Override application config values
|--------------------------------------------------------------------------
|
| If defined, settings package will override these config values from persistent
| settings repository.
|
| Sample:
| "app.fallback_locale",
| "app.locale" => "settings.locale",
|
*/
'override' => [
],
];

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class DropAllocationsWhenNodeIsDeleted extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::table('allocations', function (Blueprint $table) {
$table->dropForeign(['node_id']);
$table->foreign('node_id')->references('id')->on('nodes')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down()
{
Schema::table('allocations', function (Blueprint $table) {
$table->dropForeign(['node_id']);
$table->foreign('node_id')->references('id')->on('nodes');
});
}
}

View file

@ -0,0 +1,30 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class MigrateSettingsTableToNewFormat extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
DB::table('settings')->truncate();
Schema::table('settings', function (Blueprint $table) {
$table->increments('id')->first();
});
}
/**
* Reverse the migrations.
*/
public function down()
{
Schema::table('settings', function (Blueprint $table) {
$table->dropColumn('id');
});
}
}

View file

@ -17,7 +17,7 @@
}, },
"scripts": { "scripts": {
"installation": { "installation": {
"script": "#!\/bin\/bash\n# Garry's Mod Installation Script\n#\n# Server Files: \/mnt\/server\napt -y update\napt -y --no-install-recommends install curl lib32gcc1 ca-certificates\n\ncd \/tmp\ncurl -sSL -o steamcmd.tar.gz http:\/\/media.steampowered.com\/installer\/steamcmd_linux.tar.gz\n\nmkdir -p \/mnt\/server\/steamcmd\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\ncd \/mnt\/server\/steamcmd\n\n# SteamCMD fails otherwise for some reason, even running as root.\n# This is changed at the end of the install process anyways.\nchown -R root:root \/mnt\n\nexport HOME=\/mnt\/server\n.\/steamcmd.sh +login anonymous +force_install_dir \/mnt\/server +app_update 4020 +quit\n\nmkdir -p \/mnt\/server\/.steam\/sdk32\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so", "script": "#!\/bin\/bash\n# Garry's Mod Installation Script\n#\n# Server Files: \/mnt\/server\napt -y update\napt -y --no-install-recommends install curl lib32gcc1 lib32stdc++6 ca-certificates\n\ncd \/tmp\ncurl -sSL -o steamcmd.tar.gz http:\/\/media.steampowered.com\/installer\/steamcmd_linux.tar.gz\n\nmkdir -p \/mnt\/server\/steamcmd\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\ncd \/mnt\/server\/steamcmd\n\n# SteamCMD fails otherwise for some reason, even running as root.\n# This is changed at the end of the install process anyways.\nchown -R root:root \/mnt\n\nexport HOME=\/mnt\/server\n.\/steamcmd.sh +login anonymous +force_install_dir \/mnt\/server +app_update 4020 +quit\n\nmkdir -p \/mnt\/server\/.steam\/sdk32\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so",
"container": "ubuntu:16.04", "container": "ubuntu:16.04",
"entrypoint": "bash" "entrypoint": "bash"
} }
@ -42,4 +42,4 @@
"rules": "required|string|alpha_num|size:32" "rules": "required|string|alpha_num|size:32"
} }
] ]
} }

View file

@ -32,5 +32,6 @@
<env name="SESSION_DRIVER" value="array"/> <env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/> <env name="QUEUE_DRIVER" value="sync"/>
<env name="MAIL_DRIVER" value="array"/> <env name="MAIL_DRIVER" value="array"/>
<env name="APP_ENVIRONMENT_ONLY" value="true"/>
</php> </php>
</phpunit> </phpunit>

View file

@ -66,6 +66,7 @@ var Server = (function () {
delay: 0, delay: 0,
}); });
} }
setStatusIcon(999);
}); });
Socket.io.on('connect_error', function (err) { Socket.io.on('connect_error', function (err) {
@ -77,6 +78,7 @@ var Server = (function () {
delay: 0, delay: 0,
}); });
} }
setStatusIcon(999);
}); });
// Connected to Socket Successfully // Connected to Socket Successfully
@ -111,6 +113,7 @@ var Server = (function () {
$('#server_status_icon').html('<i class="fa fa-circle text-warning"></i> Stopping'); $('#server_status_icon').html('<i class="fa fa-circle text-warning"></i> Stopping');
break; break;
default: default:
$('#server_status_icon').html('<i class="fa fa-question-circle text-danger"></i> Connection Error');
break; break;
} }
} }

View file

@ -59,4 +59,7 @@ return [
'locations' => [ 'locations' => [
'has_nodes' => 'Cannot delete a location that has active nodes attached to it.', 'has_nodes' => 'Cannot delete a location that has active nodes attached to it.',
], ],
'users' => [
'node_revocation_failed' => 'Failed to revoke keys on <a href=":link">Node #:node</a>. :error',
],
]; ];

View file

@ -1,69 +0,0 @@
{{-- 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.admin')
@section('title')
Settings
@endsection
@section('content-header')
<h1>Panel Settings<small>Configure Pterodactyl to your liking.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li class="active">Settings</li>
</ol>
@endsection
@section('content')
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Panel Settings</h3>
</div>
<form action="{{ route('admin.settings') }}" method="POST">
<div class="box-body">
<div class="row">
<div class="form-group col-md-6">
<label class="control-label">Company Name:</label>
<div>
<input type="text" class="form-control" name="company" value="{{ old('company', Settings::get('company')) }}" />
<p class="text-muted"><small>This is the name that is used throughout the panel and in emails sent to clients.</small></p>
</div>
</div>
<div class="form-group col-md-6">
<label class="control-label">2FA Required</label>
<div>
<div class="btn-group" data-toggle="buttons">
<label class="btn btn-primary @if (old('2fa', Settings::get('2fa', 0)) == 0) active @endif">
<input type="radio" name="2fa" autocomplete="off" value="0" @if (old('2fa', Settings::get('2fa', 0)) == 0) checked @endif> Nobody
</label>
<label class="btn btn-primary @if (old('2fa', Settings::get('2fa', 0)) == 1) active @endif">
<input type="radio" name="2fa" autocomplete="off" value="1" @if (old('2fa', Settings::get('2fa', 0)) == 1) checked @endif> Admins
</label>
<label class="btn btn-primary @if (old('2fa', Settings::get('2fa', 0)) == 2) active @endif">
<input type="radio" name="2fa" autocomplete="off" value="2" @if (old('2fa', Settings::get('2fa', 0)) == 2) checked @endif> Everybody
</label>
</div>
<p class="text-muted"><small>For improved security you can require all administrators to have 2-Factor authentication enabled, or even require it for all users on the Panel.</small></p>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="alert alert-info">In order to modify your SMTP settings for sending mail you will need to run <code>php artisan p:environment:mail</code> in this project's root folder.</div>
</div>
</div>
</div>
<div class="box-footer">
{!! csrf_field() !!}
<input type="submit" class="btn btn-sm btn-primary" value="Modify Settings">
</div>
</form>
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,117 @@
@extends('layouts.admin')
@include('partials/admin.settings.nav', ['activeTab' => 'advanced'])
@section('title')
Advanced Settings
@endsection
@section('content-header')
<h1>Advanced Settings<small>Configure advanced settings for Pterodactyl.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li class="active">Settings</li>
</ol>
@endsection
@section('content')
@yield('settings::nav')
<div class="row">
<div class="col-xs-12">
<form action="" method="POST">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">reCAPTCHA</h3>
</div>
<div class="box-body">
<div class="row">
<div class="form-group col-md-4">
<label class="control-label">Status</label>
<div>
<select class="form-control" name="recaptcha:enabled">
<option value="true">Enabled</option>
<option value="false" @if(old('recaptcha:enabled', config('recaptcha.enabled')) == '0') selected @endif>Disabled</option>
</select>
<p class="text-muted small">If enabled, login forms and password reset forms will do a silent captcha check and display a visible captcha if needed.</p>
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Secret Key</label>
<div>
<input type="text" required class="form-control" name="recaptcha:secret_key" value="{{ old('recaptcha:secret_key', config('recaptcha.secret_key')) }}">
<p class="text-muted small">Used for communication between your site and Google. Be sure to keep it a secret.</p>
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Website Key</label>
<div>
<input type="text" required class="form-control" name="recaptcha:website_key" value="{{ old('recaptcha:website_key', config('recaptcha.website_key')) }}">
</div>
</div>
</div>
@if($showRecaptchaWarning)
<div class="row">
<div class="col-xs-12">
<div class="alert alert-warning no-margin">
You are currently using reCAPTCHA keys that were shipped with this Panel. For improved security it is recommended to <a href="https://www.google.com/recaptcha/admin">generate new invisible reCAPTCHA keys</a> that tied specifically to your website.
</div>
</div>
</div>
@endif
</div>
</div>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">HTTP Connections</h3>
</div>
<div class="box-body">
<div class="row">
<div class="form-group col-md-6">
<label class="control-label">Connection Timeout</label>
<div>
<input type="number" required class="form-control" name="pterodactyl:guzzle:connect_timeout" value="{{ old('pterodactyl:guzzle:connect_timeout', config('pterodactyl.guzzle.connect_timeout')) }}">
<p class="text-muted small">The amount of time in seconds to wait for a connection to be opened before throwing an error.</p>
</div>
</div>
<div class="form-group col-md-6">
<label class="control-label">Request Timeout</label>
<div>
<input type="number" required class="form-control" name="pterodactyl:guzzle:timeout" value="{{ old('pterodactyl:guzzle:timeout', config('pterodactyl.guzzle.timeout')) }}">
<p class="text-muted small">The amount of time in seconds to wait for a request to be completed before throwing an error.</p>
</div>
</div>
</div>
</div>
</div>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Console</h3>
</div>
<div class="box-body">
<div class="row">
<div class="form-group col-md-6">
<label class="control-label">Message Count</label>
<div>
<input type="number" required class="form-control" name="pterodactyl:console:count" value="{{ old('pterodactyl:console:count', config('pterodactyl.console.count')) }}">
<p class="text-muted small">The number of messages to be pushed to the console per frequency tick.</p>
</div>
</div>
<div class="form-group col-md-6">
<label class="control-label">Frequency Tick</label>
<div>
<input type="number" required class="form-control" name="pterodactyl:console:frequency" value="{{ old('pterodactyl:console:frequency', config('pterodactyl.console.frequency')) }}">
<p class="text-muted small">The amount of time in milliseconds between each console message sending tick.</p>
</div>
</div>
</div>
</div>
</div>
<div class="box box-primary">
<div class="box-footer">
{{ csrf_field() }}
<button type="submit" name="_method" value="PATCH" class="btn btn-sm btn-primary pull-right">Save</button>
</div>
</div>
</form>
</div>
</div>
@endsection

View file

@ -0,0 +1,75 @@
@extends('layouts.admin')
@include('partials/admin.settings.nav', ['activeTab' => 'basic'])
@section('title')
Settings
@endsection
@section('content-header')
<h1>Panel Settings<small>Configure Pterodactyl to your liking.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li class="active">Settings</li>
</ol>
@endsection
@section('content')
@yield('settings::nav')
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Panel Settings</h3>
</div>
<form action="{{ route('admin.settings') }}" method="POST">
<div class="box-body">
<div class="row">
<div class="form-group col-md-4">
<label class="control-label">Company Name</label>
<div>
<input type="text" class="form-control" name="app:name" value="{{ old('app:name', config('app.name')) }}" />
<p class="text-muted"><small>This is the name that is used throughout the panel and in emails sent to clients.</small></p>
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Require 2-Factor Authentication</label>
<div>
<div class="btn-group" data-toggle="buttons">
@php
$level = old('pterodactyl:auth:2fa_required', config('pterodactyl.auth.2fa_required'));
@endphp
<label class="btn btn-primary @if ($level == 0) active @endif">
<input type="radio" name="pterodactyl:auth:2fa_required" autocomplete="off" value="0" @if ($level == 0) checked @endif> Not Required
</label>
<label class="btn btn-primary @if ($level == 1) active @endif">
<input type="radio" name="pterodactyl:auth:2fa_required" autocomplete="off" value="1" @if ($level == 1) checked @endif> Admin Only
</label>
<label class="btn btn-primary @if ($level == 2) active @endif">
<input type="radio" name="pterodactyl:auth:2fa_required" autocomplete="off" value="2" @if ($level == 2) checked @endif> All Users
</label>
</div>
<p class="text-muted"><small>If enabled, any account falling into the selected grouping will be required to have 2-Factor authentication enabled to use the Panel.</small></p>
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Default Langauge</label>
<div>
<select name="app:locale" class="form-control">
@foreach($languages as $key => $value)
<option value="{{ $key }}" @if(config('app.locale') === $key) selected @endif>{{ $value }}</option>
@endforeach
</select>
<p class="text-muted"><small>The default language to use when rendering UI components.</small></p>
</div>
</div>
</div>
</div>
<div class="box-footer">
{!! csrf_field() !!}
<button type="submit" name="_method" value="PATCH" class="btn btn-sm btn-primary pull-right">Save</button>
</div>
</form>
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,108 @@
@extends('layouts.admin')
@include('partials/admin.settings.nav', ['activeTab' => 'mail'])
@section('title')
Mail Settings
@endsection
@section('content-header')
<h1>Mail Settings<small>Configure how Pterodactyl should handle sending emails.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li class="active">Settings</li>
</ol>
@endsection
@section('content')
@yield('settings::nav')
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Email Settings</h3>
</div>
@if($disabled)
<div class="box-body">
<div class="row">
<div class="col-xs-12">
<div class="alert alert-info no-margin-bottom">
This interface is limited to instances using SMTP as the mail driver. Please either use <code>php artisan p:environment:mail</code> command to update your email settings, or set <code>MAIL_DRIVER=smtp</code> in your environment file.
</div>
</div>
</div>
</div>
@else
<form action="{{ route('admin.settings.mail') }}" method="POST">
<div class="box-body">
<div class="row">
<div class="form-group col-md-6">
<label class="control-label">SMTP Host</label>
<div>
<input required type="text" class="form-control" name="mail:host" value="{{ old('mail:host', config('mail.host')) }}" />
<p class="text-muted small">Enter the SMTP server address that mail should be sent through.</p>
</div>
</div>
<div class="form-group col-md-2">
<label class="control-label">SMTP Port</label>
<div>
<input required type="number" class="form-control" name="mail:port" value="{{ old('mail:port', config('mail.port')) }}" />
<p class="text-muted small">Enter the SMTP server port that mail should be sent through.</p>
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Encryption</label>
<div>
@php
$encryption = old('mail:encryption', config('mail.encryption'));
@endphp
<select name="mail:encryption" class="form-control">
<option value="" @if($encryption === '') selected @endif>None</option>
<option value="tls" @if($encryption === 'tls') selected @endif>Transport Layer Security (TLS)</option>
<option value="ssl" @if($encryption === 'ssl') selected @endif>Secure Sockets Layer (SSL)</option>
</select>
<p class="text-muted small">Select the type of encryption to use when sending mail.</p>
</div>
</div>
<div class="form-group col-md-6">
<label class="control-label">Username <span class="field-optional"></span></label>
<div>
<input type="text" class="form-control" name="mail:username" value="{{ old('mail:username', config('mail.username')) }}" />
<p class="text-muted small">The username to use when connecting to the SMTP server.</p>
</div>
</div>
<div class="form-group col-md-6">
<label class="control-label">Password <span class="field-optional"></span></label>
<div>
<input type="password" class="form-control" name="mail:password"/>
<p class="text-muted small">The password to use in conjunction with the SMTP username. Leave blank to continue using the existing password. To set the password to an empty value enter <code>!e</code> into the field.</p>
</div>
</div>
</div>
<div class="row">
<hr />
<div class="form-group col-md-6">
<label class="control-label">Mail From</label>
<div>
<input required type="email" class="form-control" name="mail:from:address" value="{{ old('mail:from:address', config('mail.from.address')) }}" />
<p class="text-muted small">Enter an email address that all outgoing emails will originate from.</p>
</div>
</div>
<div class="form-group col-md-6">
<label class="control-label">Mail From Name <span class="field-optional"></span></label>
<div>
<input type="text" class="form-control" name="mail:from:name" value="{{ old('mail:from:name', config('mail.from.name')) }}" />
<p class="text-muted small">The name that emails should appear to come from.</p>
</div>
</div>
</div>
</div>
<div class="box-footer">
{{ csrf_field() }}
<button type="submit" name="_method" value="PATCH" class="btn btn-sm btn-primary pull-right">Save</button>
</div>
</form>
@endif
</div>
</div>
</div>
@endsection

View file

@ -53,7 +53,7 @@
@foreach ($users as $user) @foreach ($users as $user)
<tr class="align-middle"> <tr class="align-middle">
<td><code>{{ $user->id }}</code></td> <td><code>{{ $user->id }}</code></td>
<td><a href="{{ route('admin.users.view', $user->id) }}">{{ $user->email }}</a></td> <td><a href="{{ route('admin.users.view', $user->id) }}">{{ $user->email }}</a> @if($user->root_admin)<i class="fa fa-star text-yellow"></i>@endif</td>
<td>{{ $user->name_last }}, {{ $user->name_first }}</td> <td>{{ $user->name_last }}, {{ $user->name_first }}</td>
<td>{{ $user->username }}</td> <td>{{ $user->username }}</td>
<td class="text-center"> <td class="text-center">

View file

@ -66,10 +66,11 @@
</div> </div>
<div class="box-body"> <div class="box-body">
<div class="alert alert-success" style="display:none;margin-bottom:10px;" id="gen_pass"></div> <div class="alert alert-success" style="display:none;margin-bottom:10px;" id="gen_pass"></div>
<div class="form-group"> <div class="form-group no-margin-bottom">
<label for="password" class="control-label">Password</label> <label for="password" class="control-label">Password <span class="field-optional"></span></label>
<div> <div>
<input readonly type="password" id="password" name="password" class="form-control form-autocomplete-stop"> <input readonly type="password" id="password" name="password" class="form-control form-autocomplete-stop">
<p class="text-muted small">Leave blank to keep this user's password the same. User will not receive any notification if password is changed.</p>
</div> </div>
</div> </div>
</div> </div>
@ -90,6 +91,11 @@
</select> </select>
<p class="text-muted"><small>Setting this to 'Yes' gives a user full administrative access.</small></p> <p class="text-muted"><small>Setting this to 'Yes' gives a user full administrative access.</small></p>
</div> </div>
<div class="checkbox checkbox-primary">
<input type="checkbox" id="pIgnoreConnectionError" value="1" name="ignore_connection_error">
<label for="pIgnoreConnectionError"> Ignore exceptions raised while revoking keys.</label>
<p class="text-muted small">If checked, any errors thrown while revoking keys across nodes will be ignored. You should avoid this checkbox if possible as any non-revoked keys could continue to be active for up to 24 hours after this account is changed. If you are needing to revoke account permissions immediately and are facing node issues, you should check this box and then restart any nodes that failed to be updated to clear out any stored keys.</p>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -68,6 +68,7 @@
</div> </div>
<div class="col-xs-offset-1 col-xs-7"> <div class="col-xs-offset-1 col-xs-7">
{!! csrf_field() !!} {!! csrf_field() !!}
<input type="hidden" name="token" value="{{ $token }}" />
<button type="submit" class="btn btn-block g-recaptcha pterodactyl-login-button--main" @if(config('recaptcha.enabled')) data-sitekey="{{ config('recaptcha.website_key') }}" data-callback='onSubmit' @endif>@lang('auth.reset_password')</button> <button type="submit" class="btn btn-block g-recaptcha pterodactyl-login-button--main" @if(config('recaptcha.enabled')) data-sitekey="{{ config('recaptcha.website_key') }}" data-callback='onSubmit' @endif>@lang('auth.reset_password')</button>
</div> </div>
</div> </div>

View file

@ -8,7 +8,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ Settings::get('company', 'Pterodactyl') }} - @yield('title')</title> <title>{{ config('app.name', 'Pterodactyl') }} - @yield('title')</title>
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<meta name="_token" content="{{ csrf_token() }}"> <meta name="_token" content="{{ csrf_token() }}">
@ -44,7 +44,7 @@
<div class="wrapper"> <div class="wrapper">
<header class="main-header"> <header class="main-header">
<a href="{{ route('index') }}" class="logo"> <a href="{{ route('index') }}" class="logo">
<span>{{ Settings::get('company', 'Pterodactyl') }}</span> <span>{{ config('app.name', 'Pterodactyl') }}</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">

View file

@ -8,7 +8,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ Settings::get('company', 'Pterodactyl') }} - @yield('title')</title> <title>{{ config('app.name', 'Pterodactyl') }} - @yield('title')</title>
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png">
@ -37,7 +37,7 @@
<div class="container"> <div class="container">
<div id="login-position-elements"> <div id="login-position-elements">
<div class="login-logo"> <div class="login-logo">
{{ Settings::get('company', 'Pterodactyl') }} {{ config('app.name', 'Pterodactyl') }}
</div> </div>
@yield('content') @yield('content')
<p class="small login-copyright text-center"> <p class="small login-copyright text-center">

View file

@ -8,7 +8,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ Settings::get('company', 'Pterodactyl') }} - @yield('title')</title> <title>{{ config('app.name', 'Pterodactyl') }} - @yield('title')</title>
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<meta name="_token" content="{{ csrf_token() }}"> <meta name="_token" content="{{ csrf_token() }}">
@ -40,7 +40,7 @@
<div class="wrapper"> <div class="wrapper">
<header class="main-header"> <header class="main-header">
<a href="{{ route('index') }}" class="logo"> <a href="{{ route('index') }}" class="logo">
<span>{{ Settings::get('company', 'Pterodactyl') }}</span> <span>{{ config('app.name', 'Pterodactyl') }}</span>
</a> </a>
<nav class="navbar navbar-static-top"></nav> <nav class="navbar navbar-static-top"></nav>
</header> </header>

View file

@ -8,7 +8,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ Settings::get('company', 'Pterodactyl') }} - @yield('title')</title> <title>{{ config('app.name', 'Pterodactyl') }} - @yield('title')</title>
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<meta name="_token" content="{{ csrf_token() }}"> <meta name="_token" content="{{ csrf_token() }}">
@ -43,7 +43,7 @@
<div class="wrapper"> <div class="wrapper">
<header class="main-header"> <header class="main-header">
<a href="{{ route('index') }}" class="logo"> <a href="{{ route('index') }}" class="logo">
<span>{{ Settings::get('company', 'Pterodactyl') }}</span> <span>{{ config('app.name', 'Pterodactyl') }}</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">

View file

@ -0,0 +1,13 @@
@section('settings::nav')
<div class="row">
<div class="col-xs-12">
<div class="nav-tabs-custom nav-tabs-floating">
<ul class="nav nav-tabs">
<li @if($activeTab === 'basic')class="active"@endif><a href="{{ route('admin.settings') }}">General</a></li>
<li @if($activeTab === 'mail')class="active"@endif><a href="{{ route('admin.settings.mail') }}">Mail</a></li>
<li @if($activeTab === 'advanced')class="active"@endif><a href="{{ route('admin.settings.advanced') }}">Advanced</a></li>
</ul>
</div>
</div>
</div>
@endsection

View file

@ -6,7 +6,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>{{ Settings::get('company', 'Pterodactyl') }} - Console &rarr; {{ $server->name }}</title> <title>{{ config('app.name', 'Pterodactyl') }} - Console &rarr; {{ $server->name }}</title>
@include('layouts.scripts') @include('layouts.scripts')
{!! Theme::css('vendor/bootstrap/bootstrap.min.css') !!} {!! Theme::css('vendor/bootstrap/bootstrap.min.css') !!}
{!! Theme::css('css/terminal.css') !!} {!! Theme::css('css/terminal.css') !!}

View file

@ -19,4 +19,4 @@ if (! empty($outroLines)) {
} }
echo 'Regards,', "\n"; echo 'Regards,', "\n";
echo Settings::get('company'), "\n"; echo config('app.name'), "\n";

View file

@ -71,7 +71,7 @@ $style = [
<tr> <tr>
<td style="{{ $style['email-masthead'] }}"> <td style="{{ $style['email-masthead'] }}">
<a style="{{ $fontFamily }} {{ $style['email-masthead_name'] }}" href="{{ url('/') }}" target="_blank"> <a style="{{ $fontFamily }} {{ $style['email-masthead_name'] }}" href="{{ url('/') }}" target="_blank">
{{ Settings::get('company') }} {{ config('app.name') }}
</a> </a>
</td> </td>
</tr> </tr>
@ -140,7 +140,7 @@ $style = [
<!-- Salutation --> <!-- Salutation -->
<p style="{{ $style['paragraph'] }}"> <p style="{{ $style['paragraph'] }}">
Regards,<br>{{ Settings::get('company') }} Regards,<br>{{ config('app.name') }}
</p> </p>
<!-- Sub Copy --> <!-- Sub Copy -->
@ -176,7 +176,7 @@ $style = [
<td style="{{ $fontFamily }} {{ $style['email-footer_cell'] }}"> <td style="{{ $fontFamily }} {{ $style['email-footer_cell'] }}">
<p style="{{ $style['paragraph-sub'] }}"> <p style="{{ $style['paragraph-sub'] }}">
&copy; {{ date('Y') }} &copy; {{ date('Y') }}
<a style="{{ $style['anchor'] }}" href="{{ url('/') }}" target="_blank">{{ Settings::get('company') }}</a>. <a style="{{ $style['anchor'] }}" href="{{ url('/') }}" target="_blank">{{ config('app.name') }}</a>.
All rights reserved. All rights reserved.
</p> </p>
</td> </td>

View file

@ -1,12 +1,6 @@
<?php <?php
/**
* Pterodactyl - Panel Route::get('/', 'BaseController@index')->name('admin.index');
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
Route::get('/', 'BaseController@getIndex')->name('admin.index');
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -50,9 +44,13 @@ Route::group(['prefix' => 'databases'], function () {
| |
*/ */
Route::group(['prefix' => 'settings'], function () { Route::group(['prefix' => 'settings'], function () {
Route::get('/', 'BaseController@getSettings')->name('admin.settings'); Route::get('/', 'Settings\IndexController@index')->name('admin.settings');
Route::get('/mail', 'Settings\MailController@index')->name('admin.settings.mail');
Route::get('/advanced', 'Settings\AdvancedController@index')->name('admin.settings.advanced');
Route::post('/', 'BaseController@postSettings'); Route::patch('/', 'Settings\IndexController@update');
Route::patch('/mail', 'Settings\MailController@update');
Route::patch('/advanced', 'Settings\AdvancedController@update');
}); });
/* /*

View file

@ -33,16 +33,26 @@ trait RequestMockHelpers
} }
/** /**
* Set the active request object to be an instance of a mocked request. * Configure the user model that the request mock should return with.
*
* @param \Pterodactyl\Models\User|null $user
*/ */
protected function buildRequestMock() public function setRequestUserModel(User $user = null)
{ {
$this->request = m::mock($this->requestMockClass); $this->request->shouldReceive('user')->andReturn($user);
if (! $this->request instanceof Request) { }
throw new InvalidArgumentException('First argument passed to buildRequestMock must be an instance of \Illuminate\Http\Request when mocked.');
}
$this->request->attributes = new ParameterBag(); /**
* Generates a new request user model and also returns the generated model.
*
* @return \Pterodactyl\Models\User
*/
public function generateRequestUserModel(): User
{
$user = factory(User::class)->make();
$this->setRequestUserModel($user);
return $user;
} }
/** /**
@ -51,17 +61,41 @@ trait RequestMockHelpers
* @param string $attribute * @param string $attribute
* @param mixed $value * @param mixed $value
*/ */
protected function setRequestAttribute(string $attribute, $value) public function setRequestAttribute(string $attribute, $value)
{ {
$this->request->attributes->set($attribute, $value); $this->request->attributes->set($attribute, $value);
} }
/**
* Set the request route name.
*
* @param string $name
*/
public function setRequestRouteName(string $name)
{
$this->request->shouldReceive('route->getName')->andReturn($name);
}
/**
* Set the active request object to be an instance of a mocked request.
*/
protected function buildRequestMock()
{
$this->request = m::mock($this->requestMockClass);
if (! $this->request instanceof Request) {
throw new InvalidArgumentException('Request mock class must be an instance of ' . Request::class . ' when mocked.');
}
$this->request->attributes = new ParameterBag();
}
/** /**
* Sets the mocked request user. If a user model is not provided, a factory model * Sets the mocked request user. If a user model is not provided, a factory model
* will be created and returned. * will be created and returned.
* *
* @param \Pterodactyl\Models\User|null $user * @param \Pterodactyl\Models\User|null $user
* @return \Pterodactyl\Models\User * @return \Pterodactyl\Models\User
* @deprecated
*/ */
protected function setRequestUser(User $user = null): User protected function setRequestUser(User $user = null): User
{ {

View file

@ -1,43 +1,24 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Tests\Unit\Http\Controllers\Base; namespace Tests\Unit\Http\Controllers\Base;
use Mockery as m; use Mockery as m;
use Tests\TestCase; use Pterodactyl\Models\User;
use Prologue\Alerts\AlertsMessageBag; use Prologue\Alerts\AlertsMessageBag;
use Tests\Assertions\ControllerAssertionsTrait;
use Pterodactyl\Services\Users\UserUpdateService; use Pterodactyl\Services\Users\UserUpdateService;
use Tests\Unit\Http\Controllers\ControllerTestCase;
use Pterodactyl\Http\Controllers\Base\AccountController; use Pterodactyl\Http\Controllers\Base\AccountController;
use Pterodactyl\Http\Requests\Base\AccountDataFormRequest; use Pterodactyl\Http\Requests\Base\AccountDataFormRequest;
class AccountControllerTest extends TestCase class AccountControllerTest extends ControllerTestCase
{ {
use ControllerAssertionsTrait;
/** /**
* @var \Prologue\Alerts\AlertsMessageBag * @var \Prologue\Alerts\AlertsMessageBag|\Mockery\Mock
*/ */
protected $alert; protected $alert;
/** /**
* @var \Pterodactyl\Http\Controllers\Base\AccountController * @var \Pterodactyl\Services\Users\UserUpdateService|\Mockery\Mock
*/
protected $controller;
/**
* @var \Pterodactyl\Http\Requests\Base\AccountDataFormRequest
*/
protected $request;
/**
* @var \Pterodactyl\Services\Users\UserUpdateService
*/ */
protected $updateService; protected $updateService;
@ -49,10 +30,7 @@ class AccountControllerTest extends TestCase
parent::setUp(); parent::setUp();
$this->alert = m::mock(AlertsMessageBag::class); $this->alert = m::mock(AlertsMessageBag::class);
$this->request = m::mock(AccountDataFormRequest::class);
$this->updateService = m::mock(UserUpdateService::class); $this->updateService = m::mock(UserUpdateService::class);
$this->controller = new AccountController($this->alert, $this->updateService);
} }
/** /**
@ -60,7 +38,7 @@ class AccountControllerTest extends TestCase
*/ */
public function testIndexController() public function testIndexController()
{ {
$response = $this->controller->index(); $response = $this->getController()->index();
$this->assertIsViewResponse($response); $this->assertIsViewResponse($response);
$this->assertViewNameEquals('base.account', $response); $this->assertViewNameEquals('base.account', $response);
@ -71,14 +49,17 @@ class AccountControllerTest extends TestCase
*/ */
public function testUpdateControllerForPassword() public function testUpdateControllerForPassword()
{ {
$this->setRequestMockClass(AccountDataFormRequest::class);
$user = $this->setRequestUser();
$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->request->shouldReceive('user')->withNoArgs()->once()->andReturn((object) ['id' => 1]); $this->updateService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_USER)->once()->andReturnNull();
$this->updateService->shouldReceive('handle')->with(1, ['password' => 'test-password'])->once()->andReturnNull(); $this->updateService->shouldReceive('handle')->with($user, ['password' => 'test-password'])->once()->andReturnNull();
$this->alert->shouldReceive('success->flash')->once()->andReturnNull(); $this->alert->shouldReceive('success->flash')->once()->andReturnNull();
$response = $this->controller->update($this->request); $response = $this->getController()->update($this->request);
$this->assertIsRedirectResponse($response); $this->assertIsRedirectResponse($response);
$this->assertRedirectRouteEquals('account', $response); $this->assertRedirectRouteEquals('account', $response);
} }
@ -88,14 +69,17 @@ class AccountControllerTest extends TestCase
*/ */
public function testUpdateControllerForEmail() public function testUpdateControllerForEmail()
{ {
$this->setRequestMockClass(AccountDataFormRequest::class);
$user = $this->setRequestUser();
$this->request->shouldReceive('input')->with('do_action')->andReturn('email'); $this->request->shouldReceive('input')->with('do_action')->andReturn('email');
$this->request->shouldReceive('input')->with('new_email')->once()->andReturn('test@example.com'); $this->request->shouldReceive('input')->with('new_email')->once()->andReturn('test@example.com');
$this->request->shouldReceive('user')->withNoArgs()->once()->andReturn((object) ['id' => 1]); $this->updateService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_USER)->once()->andReturnNull();
$this->updateService->shouldReceive('handle')->with(1, ['email' => 'test@example.com'])->once()->andReturnNull(); $this->updateService->shouldReceive('handle')->with($user, ['email' => 'test@example.com'])->once()->andReturnNull();
$this->alert->shouldReceive('success->flash')->once()->andReturnNull(); $this->alert->shouldReceive('success->flash')->once()->andReturnNull();
$response = $this->controller->update($this->request); $response = $this->getController()->update($this->request);
$this->assertIsRedirectResponse($response); $this->assertIsRedirectResponse($response);
$this->assertRedirectRouteEquals('account', $response); $this->assertRedirectRouteEquals('account', $response);
} }
@ -105,17 +89,30 @@ class AccountControllerTest extends TestCase
*/ */
public function testUpdateControllerForIdentity() public function testUpdateControllerForIdentity()
{ {
$this->setRequestMockClass(AccountDataFormRequest::class);
$user = $this->setRequestUser();
$this->request->shouldReceive('input')->with('do_action')->andReturn('identity'); $this->request->shouldReceive('input')->with('do_action')->andReturn('identity');
$this->request->shouldReceive('only')->with(['name_first', 'name_last', 'username'])->once()->andReturn([ $this->request->shouldReceive('only')->with(['name_first', 'name_last', 'username'])->once()->andReturn([
'test_data' => 'value', 'test_data' => 'value',
]); ]);
$this->request->shouldReceive('user')->withNoArgs()->once()->andReturn((object) ['id' => 1]); $this->updateService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_USER)->once()->andReturnNull();
$this->updateService->shouldReceive('handle')->with(1, ['test_data' => 'value'])->once()->andReturnNull(); $this->updateService->shouldReceive('handle')->with($user, ['test_data' => 'value'])->once()->andReturnNull();
$this->alert->shouldReceive('success->flash')->once()->andReturnNull(); $this->alert->shouldReceive('success->flash')->once()->andReturnNull();
$response = $this->controller->update($this->request); $response = $this->getController()->update($this->request);
$this->assertIsRedirectResponse($response); $this->assertIsRedirectResponse($response);
$this->assertRedirectRouteEquals('account', $response); $this->assertRedirectRouteEquals('account', $response);
} }
/**
* Return an instance of the controller for testing.
*
* @return \Pterodactyl\Http\Controllers\Base\AccountController
*/
private function getController(): AccountController
{
return new AccountController($this->alert, $this->updateService);
}
} }

View file

@ -4,7 +4,6 @@ namespace Tests\Unit\Http\Middleware;
use Mockery as m; use Mockery as m;
use Pterodactyl\Models\User; use Pterodactyl\Models\User;
use Krucas\Settings\Settings;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag; use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication; use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
@ -16,11 +15,6 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
*/ */
private $alert; private $alert;
/**
* @var \Krucas\Settings\Settings|\Mockery\Mock
*/
private $settings;
/** /**
* Setup tests. * Setup tests.
*/ */
@ -29,7 +23,6 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
parent::setUp(); parent::setUp();
$this->alert = m::mock(AlertsMessageBag::class); $this->alert = m::mock(AlertsMessageBag::class);
$this->settings = m::mock(Settings::class);
} }
/** /**
@ -37,7 +30,7 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
*/ */
public function testRequestMissingUser() public function testRequestMissingUser()
{ {
$this->request->shouldReceive('user')->withNoArgs()->once()->andReturnNull(); $this->setRequestUserModel(null);
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
} }
@ -46,11 +39,12 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
* Test that the middleware is ignored on specific routes. * Test that the middleware is ignored on specific routes.
* *
* @dataProvider ignoredRoutesDataProvider * @dataProvider ignoredRoutesDataProvider
* @param string $route
*/ */
public function testRequestOnIgnoredRoute($route) public function testRequestOnIgnoredRoute($route)
{ {
$this->request->shouldReceive('user')->withNoArgs()->once()->andReturn(true); $this->generateRequestUserModel();
$this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn($route); $this->setRequestRouteName($route);
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
} }
@ -60,10 +54,21 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
*/ */
public function testTwoFactorRequirementDisabled() public function testTwoFactorRequirementDisabled()
{ {
$this->request->shouldReceive('user')->withNoArgs()->once()->andReturn(true); $this->generateRequestUserModel();
$this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('random.route'); $this->setRequestRouteName('random.route');
$this->setRequirementLevel(RequireTwoFactorAuthentication::LEVEL_NONE);
$this->settings->shouldReceive('get')->with('2fa', 0)->once()->andReturn(RequireTwoFactorAuthentication::LEVEL_NONE); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
}
/**
* Test that an invalid value for the level skips the check and continues with the request.
*/
public function testTwoFactorRequirementWithInvalidValue()
{
$this->generateRequestUserModel();
$this->setRequestRouteName('random.route');
$this->setRequirementLevel(333);
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
} }
@ -74,11 +79,10 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
public function testTwoFactorEnabledForAdminsAsAdminUserWith2FADisabled() public function testTwoFactorEnabledForAdminsAsAdminUserWith2FADisabled()
{ {
$user = factory(User::class)->make(['root_admin' => 1, 'use_totp' => 0]); $user = factory(User::class)->make(['root_admin' => 1, 'use_totp' => 0]);
$this->setRequestUserModel($user);
$this->setRequestRouteName('random.route');
$this->setRequirementLevel(RequireTwoFactorAuthentication::LEVEL_ADMIN);
$this->request->shouldReceive('user')->withNoArgs()->times(3)->andReturn($user);
$this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('random.route');
$this->settings->shouldReceive('get')->with('2fa', 0)->once()->andReturn(RequireTwoFactorAuthentication::LEVEL_ADMIN);
$this->alert->shouldReceive('danger')->with(trans('auth.2fa_must_be_enabled'))->once()->andReturnSelf(); $this->alert->shouldReceive('danger')->with(trans('auth.2fa_must_be_enabled'))->once()->andReturnSelf();
$this->alert->shouldReceive('flash')->withNoArgs()->once()->andReturnSelf(); $this->alert->shouldReceive('flash')->withNoArgs()->once()->andReturnSelf();
@ -93,11 +97,9 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
public function testTwoFactorEnabledForAdminsAsAdminUserWith2FAEnabled() public function testTwoFactorEnabledForAdminsAsAdminUserWith2FAEnabled()
{ {
$user = factory(User::class)->make(['root_admin' => 1, 'use_totp' => 1]); $user = factory(User::class)->make(['root_admin' => 1, 'use_totp' => 1]);
$this->setRequestUserModel($user);
$this->request->shouldReceive('user')->withNoArgs()->times(3)->andReturn($user); $this->setRequestRouteName('random.route');
$this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('random.route'); $this->setRequirementLevel(RequireTwoFactorAuthentication::LEVEL_ADMIN);
$this->settings->shouldReceive('get')->with('2fa', 0)->once()->andReturn(RequireTwoFactorAuthentication::LEVEL_ADMIN);
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
} }
@ -108,11 +110,9 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
public function testTwoFactorEnabledForAdminsAsNonAdmin() public function testTwoFactorEnabledForAdminsAsNonAdmin()
{ {
$user = factory(User::class)->make(['root_admin' => 0]); $user = factory(User::class)->make(['root_admin' => 0]);
$this->setRequestUserModel($user);
$this->request->shouldReceive('user')->withNoArgs()->twice()->andReturn($user); $this->setRequestRouteName('random.route');
$this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('random.route'); $this->setRequirementLevel(RequireTwoFactorAuthentication::LEVEL_ADMIN);
$this->settings->shouldReceive('get')->with('2fa', 0)->once()->andReturn(RequireTwoFactorAuthentication::LEVEL_ADMIN);
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
} }
@ -123,11 +123,10 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
public function testTwoFactorEnabledForAllUsersAsUserWith2FADisabled() public function testTwoFactorEnabledForAllUsersAsUserWith2FADisabled()
{ {
$user = factory(User::class)->make(['use_totp' => 0]); $user = factory(User::class)->make(['use_totp' => 0]);
$this->setRequestUserModel($user);
$this->setRequestRouteName('random.route');
$this->setRequirementLevel(RequireTwoFactorAuthentication::LEVEL_ALL);
$this->request->shouldReceive('user')->withNoArgs()->twice()->andReturn($user);
$this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('random.route');
$this->settings->shouldReceive('get')->with('2fa', 0)->once()->andReturn(RequireTwoFactorAuthentication::LEVEL_ALL);
$this->alert->shouldReceive('danger')->with(trans('auth.2fa_must_be_enabled'))->once()->andReturnSelf(); $this->alert->shouldReceive('danger')->with(trans('auth.2fa_must_be_enabled'))->once()->andReturnSelf();
$this->alert->shouldReceive('flash')->withNoArgs()->once()->andReturnSelf(); $this->alert->shouldReceive('flash')->withNoArgs()->once()->andReturnSelf();
@ -142,11 +141,9 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
public function testTwoFactorEnabledForAllUsersAsUserWith2FAEnabled() public function testTwoFactorEnabledForAllUsersAsUserWith2FAEnabled()
{ {
$user = factory(User::class)->make(['use_totp' => 1]); $user = factory(User::class)->make(['use_totp' => 1]);
$this->setRequestUserModel($user);
$this->request->shouldReceive('user')->withNoArgs()->twice()->andReturn($user); $this->setRequestRouteName('random.route');
$this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('random.route'); $this->setRequirementLevel(RequireTwoFactorAuthentication::LEVEL_ALL);
$this->settings->shouldReceive('get')->with('2fa', 0)->once()->andReturn(RequireTwoFactorAuthentication::LEVEL_ALL);
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
} }
@ -176,6 +173,16 @@ class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
*/ */
private function getMiddleware(): RequireTwoFactorAuthentication private function getMiddleware(): RequireTwoFactorAuthentication
{ {
return new RequireTwoFactorAuthentication($this->alert, $this->settings); return new RequireTwoFactorAuthentication($this->alert);
}
/**
* Set the authentication level requirement.
*
* @param int $level
*/
private function setRequirementLevel(int $level)
{
config()->set('pterodactyl.auth.2fa_required', $level);
} }
} }

View file

@ -0,0 +1,115 @@
<?php
namespace Tests\Unit\Services\DaemonKeys;
use Mockery as m;
use Tests\TestCase;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\DaemonKey;
use Tests\Traits\MocksRequestException;
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
use Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService;
use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface;
class RevokeMultipleDaemonKeysServiceTest extends TestCase
{
use MocksRequestException;
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface|\Mockery\Mock
*/
private $daemonRepository;
/**
* @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
$this->daemonRepository = m::mock(ServerRepositoryInterface::class);
$this->repository = m::mock(DaemonKeyRepositoryInterface::class);
}
/**
* Test that keys can be successfully revoked.
*/
public function testSuccessfulKeyRevocation()
{
$user = factory(User::class)->make();
$server = factory(Server::class)->make();
$key = factory(DaemonKey::class)->make(['user_id' => $user->id]);
$key->setRelation('server', $server);
$this->repository->shouldReceive('getKeysForRevocation')->with($user)->once()->andReturn(collect([$key]));
$this->daemonRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf();
$this->daemonRepository->shouldReceive('revokeAccessKey')->with([$key->secret])->once()->andReturnNull();
$this->repository->shouldReceive('deleteKeys')->with([$key->id])->once()->andReturnNull();
$this->getService()->handle($user);
$this->assertTrue(true);
}
/**
* Test that an exception thrown by a call to the daemon is handled.
*
* @expectedException \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function testExceptionThrownFromDaemonCallIsHandled()
{
$this->configureExceptionMock();
$user = factory(User::class)->make();
$server = factory(Server::class)->make();
$key = factory(DaemonKey::class)->make(['user_id' => $user->id]);
$key->setRelation('server', $server);
$this->repository->shouldReceive('getKeysForRevocation')->with($user)->once()->andReturn(collect([$key]));
$this->daemonRepository->shouldReceive('setNode->revokeAccessKey')->with([$key->secret])->once()->andThrow($this->getExceptionMock());
$this->getService()->handle($user);
}
/**
* Test that the behavior for handling exceptions that should not be thrown
* immediately is working correctly and adds them to the array.
*/
public function testIgnoredExceptionsAreHandledProperly()
{
$this->configureExceptionMock();
$user = factory(User::class)->make();
$server = factory(Server::class)->make();
$key = factory(DaemonKey::class)->make(['user_id' => $user->id]);
$key->setRelation('server', $server);
$this->repository->shouldReceive('getKeysForRevocation')->with($user)->once()->andReturn(collect([$key]));
$this->daemonRepository->shouldReceive('setNode->revokeAccessKey')->with([$key->secret])->once()->andThrow($this->getExceptionMock());
$this->repository->shouldReceive('deleteKeys')->with([$key->id])->once()->andReturnNull();
$service = $this->getService();
$service->handle($user, true);
$this->assertNotEmpty($service->getExceptions());
$this->assertArrayHasKey($server->node_id, $service->getExceptions());
$this->assertSame(array_get($service->getExceptions(), $server->node_id), $this->getExceptionMock());
$this->assertTrue(true);
}
/**
* Return an instance of the service for testing.
*
* @return \Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService
*/
private function getService(): RevokeMultipleDaemonKeysService
{
return new RevokeMultipleDaemonKeysService($this->repository, $this->daemonRepository);
}
}

View file

@ -1,36 +1,32 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Tests\Unit\Services\Users; namespace Tests\Unit\Services\Users;
use Mockery as m; use Mockery as m;
use Tests\TestCase; use Tests\TestCase;
use Pterodactyl\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Contracts\Hashing\Hasher;
use Pterodactyl\Services\Users\UserUpdateService; use Pterodactyl\Services\Users\UserUpdateService;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService;
class UserUpdateServiceTest extends TestCase class UserUpdateServiceTest extends TestCase
{ {
/** /**
* @var \Illuminate\Contracts\Hashing\Hasher * @var \Illuminate\Contracts\Hashing\Hasher|\Mockery\Mock
*/ */
protected $hasher; private $hasher;
/** /**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock
*/ */
protected $repository; private $repository;
/** /**
* @var \Pterodactyl\Services\Users\UserUpdateService * @var \Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService|\Mockery\Mock
*/ */
protected $service; private $revocationService;
/** /**
* Setup tests. * Setup tests.
@ -41,8 +37,7 @@ class UserUpdateServiceTest extends TestCase
$this->hasher = m::mock(Hasher::class); $this->hasher = m::mock(Hasher::class);
$this->repository = m::mock(UserRepositoryInterface::class); $this->repository = m::mock(UserRepositoryInterface::class);
$this->revocationService = m::mock(RevokeMultipleDaemonKeysService::class);
$this->service = new UserUpdateService($this->hasher, $this->repository);
} }
/** /**
@ -50,9 +45,14 @@ class UserUpdateServiceTest extends TestCase
*/ */
public function testUpdateUserWithoutTouchingHasherIfNoPasswordPassed() public function testUpdateUserWithoutTouchingHasherIfNoPasswordPassed()
{ {
$this->repository->shouldReceive('update')->with(1, ['test-data' => 'value'])->once()->andReturnNull(); $user = factory(User::class)->make();
$this->revocationService->shouldReceive('getExceptions')->withNoArgs()->once()->andReturn([]);
$this->repository->shouldReceive('update')->with($user->id, ['test-data' => 'value'])->once()->andReturnNull();
$this->assertNull($this->service->handle(1, ['test-data' => 'value'])); $response = $this->getService()->handle($user, ['test-data' => 'value']);
$this->assertInstanceOf(Collection::class, $response);
$this->assertTrue($response->has('model'));
$this->assertTrue($response->has('exceptions'));
} }
/** /**
@ -60,9 +60,61 @@ class UserUpdateServiceTest extends TestCase
*/ */
public function testUpdateUserAndHashPasswordIfProvided() public function testUpdateUserAndHashPasswordIfProvided()
{ {
$user = factory(User::class)->make();
$this->hasher->shouldReceive('make')->with('raw_pass')->once()->andReturn('enc_pass'); $this->hasher->shouldReceive('make')->with('raw_pass')->once()->andReturn('enc_pass');
$this->repository->shouldReceive('update')->with(1, ['password' => 'enc_pass'])->once()->andReturnNull(); $this->revocationService->shouldReceive('getExceptions')->withNoArgs()->once()->andReturn([]);
$this->repository->shouldReceive('update')->with($user->id, ['password' => 'enc_pass'])->once()->andReturnNull();
$this->assertNull($this->service->handle(1, ['password' => 'raw_pass'])); $response = $this->getService()->handle($user, ['password' => 'raw_pass']);
$this->assertInstanceOf(Collection::class, $response);
$this->assertTrue($response->has('model'));
$this->assertTrue($response->has('exceptions'));
}
/**
* Test that an admin can revoke a user's administrative status.
*/
public function testAdministrativeUserRevokingAdminStatus()
{
$user = factory(User::class)->make(['root_admin' => true]);
$service = $this->getService();
$service->setUserLevel(User::USER_LEVEL_ADMIN);
$this->revocationService->shouldReceive('handle')->with($user, false)->once()->andReturnNull();
$this->revocationService->shouldReceive('getExceptions')->withNoArgs()->once()->andReturn([]);
$this->repository->shouldReceive('update')->with($user->id, ['root_admin' => false])->once()->andReturnNull();
$response = $service->handle($user, ['root_admin' => false]);
$this->assertInstanceOf(Collection::class, $response);
$this->assertTrue($response->has('model'));
$this->assertTrue($response->has('exceptions'));
}
/**
* Test that a normal user is unable to set an administrative status for themselves.
*/
public function testNormalUserShouldNotRevokeAdminStatus()
{
$user = factory(User::class)->make(['root_admin' => false]);
$service = $this->getService();
$service->setUserLevel(User::USER_LEVEL_USER);
$this->revocationService->shouldReceive('getExceptions')->withNoArgs()->once()->andReturn([]);
$this->repository->shouldReceive('update')->with($user->id, [])->once()->andReturnNull();
$response = $service->handle($user, ['root_admin' => true]);
$this->assertInstanceOf(Collection::class, $response);
$this->assertTrue($response->has('model'));
$this->assertTrue($response->has('exceptions'));
}
/**
* Return an instance of the service for testing.
*
* @return \Pterodactyl\Services\Users\UserUpdateService
*/
private function getService(): UserUpdateService
{
return new UserUpdateService($this->hasher, $this->revocationService, $this->repository);
} }
} }