1
1
mirror of https://github.com/pterodactyl/panel.git synced 2024-11-22 00:52:43 +01:00

Merge branch 'develop' into feature/vuejs

This commit is contained in:
Dane Everitt 2018-12-16 14:20:35 -08:00
commit 21ffa08d66
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
54 changed files with 407 additions and 243 deletions

View File

@ -11,7 +11,7 @@ server {
listen 443 ssl http2;
server_name <domain>;
root /var/www/pterodactyl/public;
root /app/public;
index index.php;
access_log /var/log/nginx/pterodactyl.app-access.log;
@ -49,7 +49,7 @@ server {
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/pterodactyl.sock;
fastcgi_pass unix:/var/run/php/php-fpm7.2.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size=100M";

View File

@ -3,19 +3,24 @@
cd /app
mkdir -p /var/log/panel/logs/ /var/log/supervisord/ /var/log/nginx/ /var/log/php7/ \
&& rmdir /app/storage/logs/ \
&& chmod 777 /var/log/panel/logs/ \
&& ln -s /var/log/panel/logs/ /app/storage/
## check for .env file and generate app keys if missing
if [ -f /app/var/.env ]; then
echo "external vars exist"
echo "external vars exist."
rm /app/.env
ln -s /app/var/.env /app/
else
echo "external vars don't exist"
echo "external vars don't exist."
rm /app/.env
touch /app/var/.env
## manually generate a key because key generate --force fails
echo -e "Generating key"
echo -e "Generating key."
APP_KEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
echo -e "Generated app key: $APP_KEY"
echo -e "APP_KEY=$APP_KEY" > /app/var/.env
@ -23,6 +28,25 @@ else
ln -s /app/var/.env /app/
fi
echo "Checking if https is required."
if [ -f /etc/nginx/conf.d/default.conf ]; then
echo "Using nginx config already in place."
else
echo "Checking if letsencrypt email is set."
if [ -z $LE_EMAIL ]; then
echo "No letsencrypt email is set Failing to http."
cp .dev/docker/default.conf /etc/nginx/conf.d/default.conf
else
echo "writing ssl config"
cp .dev/docker/default_ssl.conf /etc/nginx/conf.d/default.conf
echo "updating ssl config for domain"
sed -i "s|<domain>|$(echo $APP_URL | sed 's~http[s]*://~~g')|g" /etc/nginx/conf.d/default.conf
echo "generating certs"
certbot certonly -d $(echo $APP_URL | sed 's~http[s]*://~~g') --standalone -m $LE_EMAIL --agree-tos -n
fi
fi
## check for DB up before starting the panel
echo "Checking database status."
until nc -z -v -w30 $DB_HOST 3306
@ -34,13 +58,13 @@ do
done
## make sure the db is set up
echo -e "Migrating and Seeding DB"
echo -e "Migrating and Seeding D.B"
php artisan migrate --force
php artisan db:seed --force
## start cronjobs for the queue
echo -e "Starting cron jobs"
crond
echo -e "Starting cron jobs."
crond -L /var/log/crond -l 5
echo -e "Starting supervisord"
echo -e "Starting supervisord."
exec "$@"

View File

@ -1,29 +0,0 @@
phraseapp:
project_id: 94f8b39450cd749ae9c3cc0ab8cdb61d
file_format: laravel
push:
sources:
- file: ./resources/lang/<locale_code>/<tag>.php
pull:
targets:
- file: ./resources/lang/<locale_code>/auth.php
params:
tag: "auth"
- file: ./resources/lang/<locale_code>/base.php
params:
tag: "base"
- file: ./resources/lang/<locale_code>/pagination.php
params:
tag: "pagination"
- file: ./resources/lang/<locale_code>/passwords.php
params:
tag: "passwords"
- file: ./resources/lang/<locale_code>/server.php
params:
tag: "server"
- file: ./resources/lang/<locale_code>/strings.php
params:
tag: "strings"
- file: ./resources/lang/<locale_code>/validation.php
params:
tag: "validation"

View File

@ -3,6 +3,34 @@ This file is a running track of new features and fixes to each version of the pa
This project follows [Semantic Versioning](http://semver.org) guidelines.
## v0.7.12 (Derelict Dermodactylus)
### Fixed
* Fixes an issue with the locations API endpoint referencing an invalid namespace.
* Fixes the `store()` function on the locations API not working due to an incorrect return typehint.
* Fixes daemon secrets not being able to be reset on a Node.
### Updated
* Upgraded core to use Laravel `5.7.14`.
### Added
* Added support for opening and editing Python files through the web editor.
## v0.7.11 (Derelict Dermodactylus)
### Fixed
* Fixes an issue with certain systems not handling an API folder that was named `API` but referenced as `Api` in the namespace.
* TS3 egg updated to use CLI arguments correctly and have a more minimalistic installation script.
* Terminal was not properly displaying longer lines leading to some visual inconsistency.
* Assorted translation updates.
* Pagination for server listing now properly respects configuration setting.
* Client API now properly respects permissions that are set and allows subusers to access their assigned servers.
### Changed
* Removed PhraseApp integration from Panel code as it is no longer used.
* SFTP login endpoint now returns the permissions for that user rather than requiring additional queries to get that data.
### Added
* You can now test your mail settings from the Admin CP without waiting to see if things are working correctly.
## v0.7.10 (Derelict Dermodactylus)
### Fixed
* Scheduled tasks triggered manually no longer improperly change the `next_run_at` time and do not run twice in a row anymore.

View File

@ -2,7 +2,7 @@ FROM alpine:3.8
WORKDIR /app
RUN apk add --no-cache --update ca-certificates certbot nginx dcron curl tini php7 php7-bcmath php7-common php7-dom php7-fpm php7-gd php7-mbstring php7-openssl php7-zip php7-pdo php7-phar php7-json php7-pdo_mysql php7-session php7-ctype php7-tokenizer php7-zlib php7-simplexml supervisor \
RUN apk add --no-cache --update ca-certificates certbot nginx dcron curl tini php7 php7-bcmath php7-common php7-dom php7-fpm php7-gd php7-mbstring php7-openssl php7-zip php7-pdo php7-phar php7-json php7-pdo_mysql php7-session php7-ctype php7-tokenizer php7-zlib php7-simplexml php7-fileinfo supervisor \
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
COPY . ./
@ -15,9 +15,9 @@ RUN cp .env.example .env \
RUN cp .dev/docker/default.conf /etc/nginx/conf.d/default.conf \
&& cp .dev/docker/www.conf /etc/php7/php-fpm.d/www.conf \
&& cat .dev/docker/supervisord.conf > /etc/supervisord.conf \
&& echo "* * * * * /usr/bin/php /app/pterodactyl/artisan schedule:run >> /dev/null 2>&1" >> /var/spool/cron/crontabs/root \
&& mkdir -p /var/run/php /var/run/nginx \
&& mkdir -p /var/log/supervisord/
&& echo "* * * * * /usr/bin/php /app/artisan schedule:run >> /dev/null 2>&1" >> /var/spool/cron/crontabs/root \
&& sed -i s/ssl_session_cache/#ssl_session_cache/g /etc/nginx/nginx.conf \
&& mkdir -p /var/run/php /var/run/nginx
EXPOSE 80 443

View File

@ -41,8 +41,7 @@ In addition to our standard nest of supported games, our community is constantly
* Discord ATLBot
## Credits
A huge thank you to [PhraseApp](https://phraseapp.com) who provide us the software to help translate this project. This software would not be possible
without the work of other open-source authors who provide tools such as:
This software would not be possible without the work of other open-source authors who provide tools such as:
[Ace Editor](https://ace.c9.io), [AdminLTE](https://almsaeedstudio.com), [Animate.css](http://daneden.github.io/animate.css/), [AnsiUp](https://github.com/drudru/ansi_up), [Async.js](https://github.com/caolan/async),
[Bootstrap](http://getbootstrap.com), [Bootstrap Notify](http://bootstrap-notify.remabledesigns.com), [Chart.js](http://www.chartjs.org), [FontAwesome](http://fontawesome.io),

View File

@ -132,7 +132,7 @@ class AppSettingsCommand extends Command
);
$selected = $this->config->get('queue.default', 'redis');
$this->variables['QUEUE_DRIVER'] = $this->option('queue') ?? $this->choice(
$this->variables['QUEUE_CONNECTION'] = $this->option('queue') ?? $this->choice(
trans('command/messages.environment.app.queue_driver'),
self::ALLOWED_QUEUE_DRIVERS,
array_key_exists($selected, self::ALLOWED_QUEUE_DRIVERS) ? $selected : null

View File

@ -103,10 +103,10 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter
*
* @param \Pterodactyl\Models\User $user
* @param int $level
* @param bool $paginate
* @param bool|int $paginate
* @return \Illuminate\Pagination\LengthAwarePaginator|\Illuminate\Database\Eloquent\Collection
*/
public function filterUserAccessServers(User $user, int $level, bool $paginate = true);
public function filterUserAccessServers(User $user, int $level, $paginate = 25);
/**
* Return a server by UUID.

View File

@ -1,31 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Extensions;
use Illuminate\Translation\Translator as LaravelTranslator;
class PhraseAppTranslator extends LaravelTranslator
{
/**
* Get the translation for the given key.
*
* @param string $key
* @param array $replace
* @param string|null $locale
* @param bool $fallback
* @return string
*/
public function get($key, array $replace = [], $locale = null, $fallback = true)
{
$key = substr($key, strpos($key, '.') + 1);
return "{{__phrase_${key}__}}";
}
}

View File

@ -263,7 +263,7 @@ class NodesController extends Controller
*/
public function updateSettings(NodeFormRequest $request, Node $node)
{
$this->updateService->handle($node, $request->normalize());
$this->updateService->handle($node, $request->normalize(), $request->input('reset_secret') === 'on');
$this->alert->success(trans('admin/node.notices.node_updated'))->flash();
return redirect()->route('admin.nodes.view.settings', $node->id)->withInput();
@ -289,7 +289,7 @@ class NodesController extends Controller
* Removes multiple individual allocations from a node.
*
* @param \Illuminate\Http\Request $request
* @param int $node
* @param int $node
* @return \Illuminate\Http\Response
*
* @throws \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException

View File

@ -2,10 +2,14 @@
namespace Pterodactyl\Http\Controllers\Admin\Settings;
use Exception;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\Contracts\Console\Kernel;
use Pterodactyl\Notifications\MailTested;
use Illuminate\Support\Facades\Notification;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\Encryption\Encrypter;
@ -81,13 +85,13 @@ class MailController extends Controller
* Handle request to update SMTP mail settings.
*
* @param \Pterodactyl\Http\Requests\Admin\Settings\MailSettingsFormRequest $request
* @return \Illuminate\Http\RedirectResponse
* @return \Illuminate\Http\Response
*
* @throws DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(MailSettingsFormRequest $request): RedirectResponse
public function update(MailSettingsFormRequest $request): Response
{
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.');
@ -107,8 +111,25 @@ class MailController extends Controller
}
$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');
return response('', 204);
}
/**
* Submit a request to send a test mail message.
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function test(Request $request): Response
{
try {
Notification::route('mail', $request->user()->email)
->notify(new MailTested($request->user()));
} catch (Exception $exception) {
return response($exception->getMessage(), 500);
}
return response('', 204);
}
}

View File

@ -14,6 +14,7 @@ use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
use Pterodactyl\Http\Requests\Api\Application\Locations\GetLocationRequest;
use Pterodactyl\Http\Requests\Api\Application\Locations\GetLocationsRequest;
use Pterodactyl\Http\Requests\Api\Application\Locations\DeleteLocationRequest;
use Pterodactyl\Http\Requests\Api\Application\Locations\StoreLocationRequest;
use Pterodactyl\Http\Requests\Api\Application\Locations\UpdateLocationRequest;
class LocationController extends ApplicationApiController
@ -92,7 +93,7 @@ class LocationController extends ApplicationApiController
* Store a new location on the Panel and return a HTTP/201 response code with the
* new location attached.
*
* @param \Pterodactyl\Http\Controllers\Api\Application\Locations\StoreLocationRequest $request
* @param \Pterodactyl\Http\Requests\Api\Application\Locations\StoreLocationRequest $request
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException

View File

@ -71,14 +71,14 @@ class AllocationController extends ApplicationApiController
* Store new allocations for a given node.
*
* @param \Pterodactyl\Http\Requests\Api\Application\Allocations\StoreAllocationRequest $request
* @return array
* @return \Illuminate\Http\Response
*
* @throws \Pterodactyl\Exceptions\Service\Allocation\CidrOutOfRangeException
* @throws \Pterodactyl\Exceptions\Service\Allocation\InvalidPortMappingException
* @throws \Pterodactyl\Exceptions\Service\Allocation\PortOutOfRangeException
* @throws \Pterodactyl\Exceptions\Service\Allocation\TooManyPortsInRangeException
*/
public function store(StoreAllocationRequest $request): array
public function store(StoreAllocationRequest $request): Response
{
$this->assignmentService->handle($request->getModel(Node::class), $request->validated());

View File

@ -125,7 +125,7 @@ class NodeController extends ApplicationApiController
public function update(UpdateNodeRequest $request): array
{
$node = $this->updateService->handle(
$request->getModel(Node::class), $request->validated()
$request->getModel(Node::class), $request->validated(), $request->input('reset_secret') === true
);
return $this->fractal->item($node)

View File

@ -7,7 +7,6 @@ use Pterodactyl\Models\Server;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\RequestException;
use Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Pterodactyl\Http\Requests\Api\Client\Servers\SendCommandRequest;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
@ -16,11 +15,6 @@ use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
class CommandController extends ClientApiController
{
/**
* @var \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService
*/
private $keyProviderService;
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\CommandRepositoryInterface
*/
@ -30,13 +24,11 @@ class CommandController extends ClientApiController
* CommandController constructor.
*
* @param \Pterodactyl\Contracts\Repository\Daemon\CommandRepositoryInterface $repository
* @param \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService $keyProviderService
*/
public function __construct(CommandRepositoryInterface $repository, DaemonKeyProviderService $keyProviderService)
public function __construct(CommandRepositoryInterface $repository)
{
parent::__construct();
$this->keyProviderService = $keyProviderService;
$this->repository = $repository;
}
@ -46,14 +38,12 @@ class CommandController extends ClientApiController
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\SendCommandRequest $request
* @return \Illuminate\Http\Response
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function index(SendCommandRequest $request): Response
{
$server = $request->getModel(Server::class);
$token = $this->keyProviderService->handle($server, $request->user());
$token = $request->attributes->get('server_token');
try {
$this->repository->setServer($server)

View File

@ -4,18 +4,12 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
use Illuminate\Http\Response;
use Pterodactyl\Models\Server;
use Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Pterodactyl\Http\Requests\Api\Client\Servers\SendPowerRequest;
use Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface;
class PowerController extends ClientApiController
{
/**
* @var \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService
*/
private $keyProviderService;
/**
* @var \Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface
*/
@ -24,14 +18,12 @@ class PowerController extends ClientApiController
/**
* PowerController constructor.
*
* @param \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService $keyProviderService
* @param \Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface $repository
*/
public function __construct(DaemonKeyProviderService $keyProviderService, PowerRepositoryInterface $repository)
public function __construct(PowerRepositoryInterface $repository)
{
parent::__construct();
$this->keyProviderService = $keyProviderService;
$this->repository = $repository;
}
@ -41,14 +33,12 @@ class PowerController extends ClientApiController
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\SendPowerRequest $request
* @return \Illuminate\Http\Response
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Repository\Daemon\InvalidPowerSignalException
*/
public function index(SendPowerRequest $request): Response
{
$server = $request->getModel(Server::class);
$token = $this->keyProviderService->handle($server, $request->user());
$token = $request->attributes->get('server_token');
$this->repository->setServer($server)->setToken($token)->sendSignal($request->input('signal'));

View File

@ -27,7 +27,7 @@ class ForgotPasswordController extends Controller
// exist on the system.
event(new FailedPasswordReset($request->ip(), $request->input('email')));
return $this->sendResetLinkResponse(Password::RESET_LINK_SENT);
return $this->sendResetLinkResponse($request, Password::RESET_LINK_SENT);
}
/**

View File

@ -56,7 +56,7 @@ class IndexController extends Controller
public function index(Request $request)
{
$servers = $this->repository->setSearchTerm($request->input('query'))->filterUserAccessServers(
$request->user(), User::FILTER_LEVEL_ALL
$request->user(), User::FILTER_LEVEL_ALL, config('pterodactyl.paginate.frontend.servers')
);
return view('templates/base.core', ['servers' => $servers]);

View File

@ -1,6 +1,6 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Application\Locations;
namespace Pterodactyl\Http\Requests\Api\Application\Locations;
use Pterodactyl\Models\Location;
use Pterodactyl\Services\Acl\Api\AdminAcl;

View File

@ -3,7 +3,6 @@
namespace Pterodactyl\Http\Requests\Api\Application\Locations;
use Pterodactyl\Models\Location;
use Pterodactyl\Http\Controllers\Api\Application\Locations\StoreLocationRequest;
class UpdateLocationRequest extends StoreLocationRequest
{

View File

@ -2,7 +2,6 @@
namespace Pterodactyl\Http\Requests\Api\Client\Servers;
use Pterodactyl\Models\Server;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class GetServerRequest extends ClientApiRequest
@ -18,14 +17,4 @@ class GetServerRequest extends ClientApiRequest
{
return true;
}
/**
* Determine if the user should even know that this server exists.
*
* @return bool
*/
public function resourceExists(): bool
{
return $this->user()->can('view-server', $this->getModel(Server::class));
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Pterodactyl\Notifications;
use Pterodactyl\Models\User;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
class MailTested extends Notification
{
/**
* @var \Pterodactyl\Models\User
*/
private $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function via()
{
return ['mail'];
}
public function toMail()
{
return (new MailMessage)
->subject('Pterodactyl Test Message')
->greeting('Hello ' . $this->user->name . '!')
->line('This is a test of the Pterodactyl mail system. You\'re good to go!');
}
}

View File

@ -1,44 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Providers;
use Pterodactyl\Extensions\PhraseAppTranslator;
use Illuminate\Translation\TranslationServiceProvider;
use Illuminate\Translation\Translator as IlluminateTranslator;
class PhraseAppTranslationProvider extends TranslationServiceProvider
{
/**
* Register the service provider.
*/
public function register()
{
$this->registerLoader();
$this->app->singleton('translator', function ($app) {
$loader = $app['translation.loader'];
// When registering the translator component, we'll need to set the default
// locale as well as the fallback locale. So, we'll grab the application
// configuration so we can easily get both of these values from there.
$locale = $app['config']['app.locale'];
if ($app['config']['pterodactyl.lang.in_context']) {
$trans = new PhraseAppTranslator($loader, $locale);
} else {
$trans = new IlluminateTranslator($loader, $locale);
}
$trans->setFallback($app['config']['app.fallback_locale']);
return $trans;
});
}
}

View File

@ -211,10 +211,10 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
*
* @param \Pterodactyl\Models\User $user
* @param int $level
* @param bool $paginate
* @param bool|int $paginate
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator|\Illuminate\Database\Eloquent\Collection
*/
public function filterUserAccessServers(User $user, int $level, bool $paginate = true)
public function filterUserAccessServers(User $user, int $level, $paginate = 25)
{
$instance = $this->getBuilder()->select($this->getColumns())->with(['user', 'node', 'allocation']);
@ -240,7 +240,7 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
$instance->search($this->getSearchTerm());
return $paginate ? $instance->paginate(25) : $instance->get();
return $paginate ? $instance->paginate($paginate) : $instance->get();
}
/**

View File

@ -50,24 +50,41 @@ class NodeUpdateService
*
* @param \Pterodactyl\Models\Node $node
* @param array $data
* @param bool $resetToken
*
* @return \Pterodactyl\Models\Node
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
* @throws \Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException
*/
public function handle(Node $node, array $data)
public function handle(Node $node, array $data, bool $resetToken = false)
{
if (! is_null(array_get($data, 'reset_secret'))) {
if ($resetToken) {
$data['daemonSecret'] = str_random(Node::DAEMON_SECRET_LENGTH);
unset($data['reset_secret']);
}
$this->connection->beginTransaction();
/** @var \Pterodactyl\Models\Node $updatedModel */
$updatedModel = $this->repository->update($node->id, $data);
try {
$this->configRepository->setNode($updatedModel)->update();
if ($resetToken) {
// We need to clone the new model and set it's authentication token to be the
// old one so we can connect. Then we will pass the new token through as an
// override on the call.
$cloned = $updatedModel->replicate(['daemonSecret']);
$cloned->setAttribute('daemonSecret', $node->getAttribute('daemonSecret'));
$this->configRepository->setNode($cloned)->update([
'keys' => [$data['daemonSecret']],
]);
} else {
$this->configRepository->setNode($updatedModel)->update();
}
$this->connection->commit();
} catch (RequestException $exception) {
// Failed to connect to the Daemon. Let's go ahead and save the configuration

View File

@ -102,6 +102,7 @@ class AuthenticateUsingPasswordService
return [
'server' => $server->uuid,
'token' => $this->keyProviderService->handle($server, $user),
'permissions' => $permissions ?? ['*'],
];
}
}

View File

@ -24,7 +24,7 @@
"hashids/hashids": "^2.0",
"igaster/laravel-theme": "^2.0.6",
"laracasts/utilities": "^3.0",
"laravel/framework": "5.6.*",
"laravel/framework": "~5.7.14",
"laravel/tinker": "^1.0",
"lord/laroute": "^2.4",
"matriphe/iso-639": "^1.2",
@ -34,15 +34,15 @@
"prologue/alerts": "^0.4",
"ramsey/uuid": "^3.7",
"s1lentium/iptools": "^1.1",
"sofa/eloquence-base": "v5.6",
"sofa/eloquence-base": "v5.6.2",
"sofa/eloquence-validable": "v5.6",
"spatie/laravel-fractal": "^5.3",
"spatie/laravel-fractal": "^5.4",
"webmozart/assert": "^1.2",
"znck/belongs-to-through": "^2.3"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.1",
"barryvdh/laravel-ide-helper": "^2.4",
"barryvdh/laravel-debugbar": "^3.2",
"barryvdh/laravel-ide-helper": "^2.5",
"codedungeon/phpunit-result-printer": "^0.17.1",
"filp/whoops": "^2.1",
"friendsofphp/php-cs-fixer": "^2.11.1",

View File

@ -9,7 +9,7 @@ return [
| change this value if you are not maintaining your own internal versions.
*/
'version' => '0.7.10',
'version' => 'canary',
/*
|--------------------------------------------------------------------------
@ -166,6 +166,7 @@ return [
Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
Illuminate\Session\SessionServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
@ -179,7 +180,6 @@ return [
Pterodactyl\Providers\HashidsServiceProvider::class,
Pterodactyl\Providers\RouteServiceProvider::class,
Pterodactyl\Providers\MacroServiceProvider::class,
Pterodactyl\Providers\PhraseAppTranslationProvider::class,
Pterodactyl\Providers\RepositoryServiceProvider::class,
Pterodactyl\Providers\ViewComposerServiceProvider::class,

View File

@ -153,23 +153,10 @@ return [
/*
|--------------------------------------------------------------------------
| Language Editor
| Client Features
|--------------------------------------------------------------------------
|
| Set `PHRASE_IN_CONTEXT` to true to enable the PhaseApp in-context editor
| on this site which allows you to translate the panel, from the panel.
*/
'lang' => [
'in_context' => env('PHRASE_IN_CONTEXT', false),
],
/*
|--------------------------------------------------------------------------
| Language Editor
|--------------------------------------------------------------------------
|
| Set `PHRASE_IN_CONTEXT` to true to enable the PhaseApp in-context editor
| on this site which allows you to translate the panel, from the panel.
| Allow clients to create their own databases.
*/
'client_features' => [
'databases' => [
@ -199,6 +186,7 @@ return [
'text/plain',
'text/x-perl',
'text/x-shellscript',
'text/x-python',
],
],

View File

@ -14,7 +14,7 @@ return [
|
*/
'default' => env('QUEUE_DRIVER', 'database'),
'default' => env('QUEUE_CONNECTION', env('QUEUE_DRIVER', 'database')),
/*
|--------------------------------------------------------------------------

View File

@ -3,7 +3,7 @@
"meta": {
"version": "PTDL_v1"
},
"exported_at": "2018-02-25T12:20:22-05:00",
"exported_at": "2018-11-16T02:14:51-05:00",
"name": "Spigot",
"author": "support@pterodactyl.io",
"description": "Spigot is the most widely-used modded Minecraft server software in the world. It powers many of the top Minecraft server networks around to ensure they can cope with their huge player base and ensure the satisfaction of their players. Spigot works by reducing and eliminating many causes of lag, as well as adding in handy features and settings that help make your job of server administration easier.",
@ -17,7 +17,7 @@
},
"scripts": {
"installation": {
"script": "#!\/bin\/ash\r\n# Spigot Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\n## Only download if a path is provided, otherwise continue.\r\nif [ ! -z \"${DL_PATH}\" ]; then\r\n apk update\r\n apk add curl\r\n\r\n cd \/mnt\/server\r\n\r\n MODIFIED_DOWNLOAD=`eval echo $(echo ${DL_PATH} | sed -e 's\/{{\/${\/g' -e 's\/}}\/}\/g')`\r\n curl -sSL -o ${SERVER_JARFILE} ${MODIFIED_DOWNLOAD}\r\nelse\r\n apk add --no-cache curl git openjdk8 openssl\r\n \r\n cd \/srv\/\r\n \r\n wget https:\/\/hub.spigotmc.org\/jenkins\/job\/BuildTools\/lastSuccessfulBuild\/artifact\/target\/BuildTools.jar\r\n \r\n mv BuildTools.jar \/srv\/\r\n\r\n java -jar BuildTools.jar --rev ${DL_VERSION}\r\n\r\n mv spigot-*.jar \/mnt\/server\/${SERVER_JARFILE}\r\nfi",
"script": "#!\/bin\/ash\r\n# Spigot Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\n## Only download if a path is provided, otherwise continue.\r\nif [ ! -z \"${DL_PATH}\" ]; then\r\n apk update\r\n apk add curl\r\n\r\n cd \/mnt\/server\r\n\r\n MODIFIED_DOWNLOAD=`eval echo $(echo ${DL_PATH} | sed -e 's\/{{\/${\/g' -e 's\/}}\/}\/g')`\r\n curl -sSL -o ${SERVER_JARFILE} ${MODIFIED_DOWNLOAD}\r\nelse\r\n apk add --no-cache curl git openjdk8 openssl bash\r\n \r\n cd \/srv\/\r\n \r\n wget https:\/\/hub.spigotmc.org\/jenkins\/job\/BuildTools\/lastSuccessfulBuild\/artifact\/target\/BuildTools.jar\r\n \r\n mv BuildTools.jar \/srv\/\r\n\r\n java -jar BuildTools.jar --rev ${DL_VERSION}\r\n\r\n mv spigot-*.jar \/mnt\/server\/${SERVER_JARFILE}\r\nfi",
"container": "alpine:3.7",
"entrypoint": "ash"
}

View File

@ -3,21 +3,21 @@
"meta": {
"version": "PTDL_v1"
},
"exported_at": "2018-01-21T17:01:45-06:00",
"exported_at": "2018-10-28T20:50:23+01:00",
"name": "Teamspeak3 Server",
"author": "support@pterodactyl.io",
"description": "VoIP software designed with security in mind, featuring crystal clear voice quality, endless customization options, and scalabilty up to thousands of simultaneous users.",
"image": "quay.io\/pterodactyl\/core:glibc",
"startup": ".\/ts3server_minimal_runscript.sh default_voice_port={{SERVER_PORT}} query_port={{SERVER_PORT}}",
"startup": ".\/ts3server_minimal_runscript.sh default_voice_port={{SERVER_PORT}} query_port={{SERVER_PORT}} license_accepted=1",
"config": {
"files": "{\"ts3server.ini\":{\"parser\": \"ini\", \"find\":{\"default_voice_port\": \"{{server.build.default.port}}\", \"voice_ip\": \"0.0.0.0\", \"query_port\": \"{{server.build.default.port}}\", \"query_ip\": \"0.0.0.0\"}}}",
"files": "{}",
"startup": "{\"done\": \"listening on 0.0.0.0:\", \"userInteraction\": []}",
"logs": "{\"custom\": true, \"location\": \"logs\/ts3.log\"}",
"stop": "^C"
},
"scripts": {
"installation": {
"script": "#!\/bin\/ash\n# TS3 Installation Script\n#\n# Server Files: \/mnt\/server\napk update\napk add tar curl\n\ncd \/tmp\n\ncurl -sSLO http:\/\/dl.4players.de\/ts\/releases\/${TS_VERSION}\/teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2\n\ntar -xjvf teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2\ncp -r teamspeak3-server_linux_amd64\/* \/mnt\/server\n\necho \"machine_id=\ndefault_voice_port=${SERVER_PORT}\nvoice_ip=0.0.0.0\nlicensepath=\nfiletransfer_port=30033\nfiletransfer_ip=\nquery_port=${SERVER_PORT}\nquery_ip=0.0.0.0\nquery_ip_whitelist=query_ip_whitelist.txt\nquery_ip_blacklist=query_ip_blacklist.txt\ndbplugin=ts3db_sqlite3\ndbpluginparameter=\ndbsqlpath=sql\/\ndbsqlcreatepath=create_sqlite\/\ndbconnections=10\nlogpath=logs\nlogquerycommands=0\ndbclientkeepdays=30\nlogappend=0\nquery_skipbruteforcecheck=0\" > \/mnt\/server\/ts3server.ini\n\ntouch \/mnt\/server\/.ts3server_license_accepted",
"script": "#!\/bin\/ash\n# TS3 Installation Script\n#\n# Server Files: \/mnt\/server\napk update\napk add tar curl\n\ncd \/mnt\/server\n\ncurl http:\/\/dl.4players.de\/ts\/releases\/${TS_VERSION}\/teamspeak3-server_linux_amd64-${TS_VERSION}.tar.bz2 | tar xj --strip-components=1",
"container": "alpine:3.4",
"entrypoint": "ash"
}
@ -27,7 +27,7 @@
"name": "Server Version",
"description": "The version of Teamspeak 3 to use when running the server.",
"env_variable": "TS_VERSION",
"default_value": "3.1.1",
"default_value": "3.5.0",
"user_viewable": 1,
"user_editable": 1,
"rules": "required|regex:\/^([0-9_\\.-]{5,10})$\/"

View File

@ -25,6 +25,9 @@ services:
- cache
volumes:
- "/srv/pterodactyl/var/:/app/var/"
- "/srv/pterodactyl/nginx/:/etc/nginx/conf.d/"
- "/srv/pterodactyl/certs/:/etc/letsencrypt/"
- "/srv/pterodactyl/logs/:/var/log/"
environment:
## These are defaults and should be left alone
- "APP_ENV=production"
@ -44,14 +47,14 @@ services:
- "REDIS_PASSWORD=null"
- "REDIS_PORT=6379"
## Domain settings
- "APP_URL=https://your.domain.here"
- "APP_URL=https://your.domain.here" ## if you are running this behind a reverse proxy with ssl app_url needs to be https still.
## Timezone settings
- "APP_TIMEZONE=America/New_York"
- "APP_TIMEZONE=UTC" ## http://php.net/manual/en/timezones.php
## Service egg settings
- "APP_SERVICE_AUTHOR=noreply@your.domain.here"
- "APP_SERVICE_AUTHOR=noreply@your.domain.here" ## this is the email that gets put on eggs you create
## Database settings
## change if you want it to be more secure.
- "DB_HOST=database"
## These can be left alone. Only change if you know what you are doing.
- "DB_HOST=database"
- "DB_PORT=3306"
- "DB_DATABASE=pterodb"
- "DB_USERNAME=ptero"
@ -64,6 +67,8 @@ services:
- "MAIL_USERNAME=''"
- "MAIL_PASSWORD=''"
- "MAIL_ENCRYPTION=true"
## certbot settings - Used to automatically generate ssl certs and
- "LE_EMAIL=''" ## leave blank unless you aree generating certs.
networks:
default:

View File

@ -31,7 +31,7 @@
<env name="DB_CONNECTION" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="MAIL_DRIVER" value="array"/>
</php>
</phpunit>

File diff suppressed because one or more lines are too long

View File

@ -26,6 +26,7 @@
#terminal > .cmd {
padding: 1px 0;
word-wrap: break-word;
}
#terminal_input {

View File

@ -30,7 +30,7 @@ var Server = (function () {
}
if (typeof io !== 'function') {
console.error('Socket.io is reqired to use this panel.');
console.error('Socket.io is required to use this panel.');
return;
}

View File

@ -55,7 +55,7 @@ $(document).ready(function () {
console.error(jqXHR);
swal({
title: 'Whoops!',
text: 'An error occured while attempting to set the EULA as accepted: ' . jqXHR.responseJSON.error,
text: 'An error occurred while attempting to set the EULA as accepted: ' + jqXHR.responseJSON.error,
type: 'error'
})
});

View File

@ -1,8 +0,0 @@
window.PHRASEAPP_CONFIG = {
projectId: '94f8b39450cd749ae9c3cc0ab8cdb61d'
};
(function() {
var phraseapp = document.createElement('script'); phraseapp.type = 'text/javascript'; phraseapp.async = true;
phraseapp.src = ['https://', 'phraseapp.com/assets/in-context-editor/2.0/app.js?', new Date().getTime()].join('');
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(phraseapp, s);
})();

View File

@ -340,7 +340,7 @@ return [
'2fa_disable_error' => 'Der bereitgestellte 2FA-Token war nicht gültig. Der Schutz wurde für dieses Konto nicht deaktiviert.',
'2fa_header' => '2-Faktor-Authentifizierung',
'2fa_qr' => '2FA konfigurieren',
'2fa_token_help' => 'Bitte gebe den 2FA Code von deiner 2FA APP ein (Google Authenticatior, Authy, etc.).',
'2fa_token_help' => 'Bitte gebe den 2FA Code von deiner 2FA APP ein (Google Authenticator, Authy, etc.).',
'disable_2fa' => '2-Factor-Authentifizierung deaktivieren',
'enable_2fa' => '2-Faktor-Authentifizierung aktivieren',
'header' => 'Kontosicherheit',

View File

@ -362,7 +362,7 @@ return [
'2fa_enabled' => '2-Factor de Autenticación está habilitada en esta cuenta y será necesario iniciar la sesión en el panel de. Si usted desea deshabilitar el 2FA, simplemente ingrese un token válido a continuación y envíe el formulario.',
'2fa_header' => '2-Factor De Autenticación',
'2fa_qr' => 'Confgure 2FA en Su Dispositivo',
'2fa_token_help' => 'Introduzca el 2FA Token generado por la aplicación (Google Authenticatior, Authy, etc.).',
'2fa_token_help' => 'Introduzca el 2FA Token generado por la aplicación (Google Authenticator, Authy, etc.).',
'disable_2fa' => 'Deshabilitar 2-Factor De Autenticación',
'enable_2fa' => 'Habilitar 2-Factor De Autenticación',
'header' => 'Seguridad De La Cuenta',

View File

@ -32,7 +32,7 @@
</div>
</div>
@else
<form action="{{ route('admin.settings.mail') }}" method="POST">
<form>
<div class="box-body">
<div class="row">
<div class="form-group col-md-6">
@ -98,7 +98,10 @@
</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 class="pull-right">
<button type="button" id="testButton" class="btn btn-sm btn-success">Test</button>
<button type="button" id="saveButton" class="btn btn-sm btn-primary">Save</button>
</div>
</div>
</form>
@endif
@ -106,3 +109,96 @@
</div>
</div>
@endsection
@section('footer-scripts')
{!! Theme::js('js/laroute.js?t={cache-version}') !!}
{!! Theme::js('vendor/jquery/jquery.min.js?t={cache-version}') !!}
{!! Theme::js('vendor/sweetalert/sweetalert.min.js?t={cache-version}') !!}
<script>
function saveSettings() {
return $.ajax({
method: 'PATCH',
url: Router.route('admin.settings.mail'),
contentType: 'application/json',
data: JSON.stringify({
'mail:host': $('input[name="mail:host"]').val(),
'mail:port': $('input[name="mail:port"]').val(),
'mail:encryption': $('select[name="mail:encryption"]').val(),
'mail:username': $('input[name="mail:username"]').val(),
'mail:password': $('input[name="mail:password"]').val(),
'mail:from:address': $('input[name="mail:from:address"]').val(),
'mail:from:name': $('input[name="mail:from:name"]').val()
}),
headers: { 'X-CSRF-Token': $('input[name="_token"]').val() }
}).fail(function (jqXHR) {
showErrorDialog(jqXHR, 'save');
});
}
function testSettings() {
swal({
type: 'info',
title: 'Test Mail Settings',
text: 'Click "Test" to begin the test.',
showCancelButton: true,
confirmButtonText: 'Test',
closeOnConfirm: false,
showLoaderOnConfirm: true
}, function () {
$.ajax({
method: 'GET',
url: Router.route('admin.settings.mail.test'),
headers: { 'X-CSRF-Token': $('input[name="_token"]').val() }
}).fail(function (jqXHR) {
showErrorDialog(jqXHR, 'test');
}).done(function () {
swal({
title: 'Success',
text: 'The test message was sent successfully.',
type: 'success'
});
});
});
}
function saveAndTestSettings() {
saveSettings().done(testSettings);
}
function showErrorDialog(jqXHR, verb) {
console.error(jqXHR);
var errorText = '';
if (!jqXHR.responseJSON) {
errorText = jqXHR.responseText;
} else if (jqXHR.responseJSON.error) {
errorText = jqXHR.responseJSON.error;
} else if (jqXHR.responseJSON.errors) {
$.each(jqXHR.responseJSON.errors, function (i, v) {
if (v.detail) {
errorText += v.detail + ' ';
}
});
}
swal({
title: 'Whoops!',
text: 'An error occurred while attempting to ' + verb + ' mail settings: ' + errorText,
type: 'error'
});
}
$(document).ready(function () {
$('#testButton').on('click', saveAndTestSettings);
$('#saveButton').on('click', function () {
saveSettings().done(function () {
swal({
title: 'Success',
text: 'Mail settings have been updated successfully and the queue worker was restarted to apply these changes.',
type: 'success'
});
});
});
});
</script>
@endsection

View File

@ -60,7 +60,5 @@
particlesJS.load('particles-js', '{!! Theme::url('vendor/particlesjs/particles.json?t={cache-version}') !!}', function() {});
})
</script>
@if(config('pterodactyl.lang.in_context')) {!! Theme::js('vendor/phraseapp/phraseapp.js?t={cache-version}') !!} @endif
</body>
</html>

View File

@ -286,9 +286,6 @@
{!! Theme::js('vendor/socketio/socket.io.v203.min.js?t={cache-version}') !!}
{!! Theme::js('vendor/bootstrap-notify/bootstrap-notify.min.js?t={cache-version}') !!}
{!! Theme::js('js/autocomplete.js?t={cache-version}') !!}
@if(config('pterodactyl.lang.in_context'))
{!! Theme::js('vendor/phraseapp/phraseapp.js?t={cache-version}') !!}
@endif
@if(Auth::user()->root_admin)
<script>

View File

@ -64,6 +64,7 @@ Route::group(['prefix' => 'databases'], function () {
Route::group(['prefix' => 'settings'], function () {
Route::get('/', 'Settings\IndexController@index')->name('admin.settings');
Route::get('/mail', 'Settings\MailController@index')->name('admin.settings.mail');
Route::get('/mail/test', 'Settings\MailController@test')->name('admin.settings.mail.test');
Route::get('/advanced', 'Settings\AdvancedController@index')->name('admin.settings.advanced');
Route::patch('/', 'Settings\IndexController@update');

View File

@ -1,2 +1,3 @@
*
!data/
!.gitignore

View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -0,0 +1,82 @@
<?php
namespace Tests\Unit\Http\Controllers;
use Mockery as m;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Http\Controllers\Admin\Settings\MailController;
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
class MailControllerTest extends ControllerTestCase
{
/**
* @var \Prologue\Alerts\AlertsMessageBag
*/
private $alert;
/**
* @var \Illuminate\Contracts\Config\Repository
*/
private $configRepository;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
private $encrypter;
/**
* @var \Illuminate\Contracts\Console\Kernel
*/
private $kernel;
/**
* @var \Pterodactyl\Contracts\Repository\SettingsRepositoryInterface
*/
private $settingsRepositoryInterface;
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
$this->alert = m::mock(AlertsMessageBag::class);
$this->configRepository = m::mock(ConfigRepository::class);
$this->encrypter = m::mock(Encrypter::class);
$this->kernel = m::mock(Kernel::class);
$this->settingsRepositoryInterface = m::mock(SettingsRepositoryInterface::class);
}
/**
* Test the mail controller for viewing mail settings page.
*/
public function testIndex()
{
$this->configRepository->shouldReceive('get');
$response = $this->getController()->index();
$this->assertIsViewResponse($response);
$this->assertViewNameEquals('admin.settings.mail', $response);
}
/**
* Prepare a MailController using our mocks.
*
* @return MailController
*/
public function getController()
{
return new MailController(
$this->alert,
$this->configRepository,
$this->encrypter,
$this->kernel,
$this->settingsRepositoryInterface
);
}
}

View File

@ -73,7 +73,7 @@ class IndexControllerTest extends ControllerTestCase
$this->request->shouldReceive('input')->with('query')->once()->andReturn('searchTerm');
$this->repository->shouldReceive('setSearchTerm')->with('searchTerm')->once()->andReturnSelf()
->shouldReceive('filterUserAccessServers')->with($model, User::FILTER_LEVEL_ALL)
->shouldReceive('filterUserAccessServers')->with($model, User::FILTER_LEVEL_ALL, config('pterodactyl.paginate.frontend.servers'))
->once()->andReturn($paginator);
$response = $this->controller->index($this->request);

View File

@ -51,21 +51,34 @@ class NodeUpdateServiceTest extends TestCase
public function testNodeIsUpdatedAndDaemonSecretIsReset()
{
$model = factory(Node::class)->make();
$updatedModel = factory(Node::class)->make([
'name' => 'New Name',
'daemonSecret' => 'abcd1234',
]);
$this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'str_random')
->expects($this->once())->willReturn('random_string');
->expects($this->once())->willReturn($updatedModel->daemonSecret);
$this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull();
$this->repository->shouldReceive('update')->with($model->id, [
'name' => 'NewName',
'daemonSecret' => 'random_string',
'name' => $updatedModel->name,
'daemonSecret' => $updatedModel->daemonSecret,
])->andReturn($model);
$this->configRepository->shouldReceive('setNode')->with($model)->once()->andReturnSelf()
->shouldReceive('update')->withNoArgs()->once()->andReturn(new Response);
$cloned = $updatedModel->replicate(['daemonSecret']);
$cloned->daemonSecret = $model->daemonSecret;
$this->configRepository->shouldReceive('setNode')->with(m::on(function ($model) use ($updatedModel) {
return $model->daemonSecret !== $updatedModel->daemonSecret;
}))->once()->andReturnSelf();
$this->configRepository->shouldReceive('update')->with([
'keys' => ['abcd1234'],
])->once()->andReturn(new Response);
$this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull();
$response = $this->getService()->handle($model, ['name' => 'NewName', 'reset_secret' => true]);
$response = $this->getService()->handle($model, ['name' => $updatedModel->name], true);
$this->assertInstanceOf(Node::class, $response);
}