diff --git a/CHANGELOG.md b/CHANGELOG.md index 39a3a8b18..6b4508658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,13 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * `[beta.1]` — Fixes missing check in environment setup that would leave the Hashids salt empty. * `[beta.1]` — Fixes bug preventing loading of allocations when trying to create a new server. * `[beta.1]` — Fixes bug causing inability to create new servers on the Panel. +* `[beta.1]` — Fixes bug causing inability to delete an allocation due to misconfigured JS. +* `[beta.1]` — Fixes bug causing inability to set the IP alias for an allocation to an empty value. +* `[beta.1]` — Fixes bug that caused startup changes to not propigate to the server correctly on the first save. + +### Changed +* Moved Docker image setting to be on the startup management page for a server rather than the details page. This value changes based on the Nest and Egg that are selected. +* Two-Factor authentication tokens are now 32 bytes in length, and are stored encrypted at rest in the database. ## v0.7.0-beta.1 (Derelict Dermodactylus) ### Added diff --git a/app/Console/Commands/Maintenance/CleanServiceBackupFilesCommand.php b/app/Console/Commands/Maintenance/CleanServiceBackupFilesCommand.php index 1cbe6090d..f3982921e 100644 --- a/app/Console/Commands/Maintenance/CleanServiceBackupFilesCommand.php +++ b/app/Console/Commands/Maintenance/CleanServiceBackupFilesCommand.php @@ -15,6 +15,8 @@ use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; class CleanServiceBackupFilesCommand extends Command { + const BACKUP_THRESHOLD_MINUTES = 5; + /** * @var \Carbon\Carbon */ @@ -58,7 +60,7 @@ class CleanServiceBackupFilesCommand extends Command collect($files)->each(function ($file) { $lastModified = $this->carbon->timestamp($this->disk->lastModified($file)); - if ($lastModified->diffInMinutes($this->carbon->now()) > 5) { + if ($lastModified->diffInMinutes($this->carbon->now()) > self::BACKUP_THRESHOLD_MINUTES) { $this->disk->delete($file); $this->info(trans('command/messages.maintenance.deleting_service_backup', ['file' => $file])); } diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index e074d7059..2fb8349e3 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -95,14 +95,15 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter public function getWithDatabases($id); /** - * Return data about the daemon service in a consumable format. + * Get data for use when updating a server on the Daemon. Returns an array of + * the egg and pack UUID which are used for build and rebuild. Only loads relations + * if they are missing, or refresh is set to true. * - * @param int $id + * @param \Pterodactyl\Models\Server $server + * @param bool $refresh * @return array - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function getDaemonServiceData($id); + public function getDaemonServiceData(Server $server, bool $refresh = false): array; /** * Return an array of server IDs that a given user can access based on owner and subuser permissions. diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 203e7058c..0bf3610f7 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -410,25 +410,6 @@ class ServersController extends Controller return redirect()->route('admin.servers.view.details', $server->id); } - /** - * Set the new docker container for a server. - * - * @param \Illuminate\Http\Request $request - * @param \Pterodactyl\Models\Server $server - * @return \Illuminate\Http\RedirectResponse - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function setContainer(Request $request, Server $server) - { - $this->detailsModificationService->setDockerImage($server, $request->input('docker_image')); - $this->alert->success(trans('admin/server.alerts.docker_image_updated'))->flash(); - - return redirect()->route('admin.servers.view.details', $server->id); - } - /** * Toggles the install status for a server. * diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 12f3df533..9fab7b53e 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -202,7 +202,7 @@ class LoginController extends Controller return $this->sendFailedLoginResponse($request); } - if (! $G2FA->verifyKey($user->totp_secret, $request->input('2fa_token'), 2)) { + if (! $G2FA->verifyKey(Crypt::decrypt($user->totp_secret), $request->input('2fa_token'), 2)) { event(new \Illuminate\Auth\Events\Failed($user, $credentials)); return $this->sendFailedLoginResponse($request); diff --git a/app/Http/Controllers/Base/SecurityController.php b/app/Http/Controllers/Base/SecurityController.php index d22c0ddb9..62f07738c 100644 --- a/app/Http/Controllers/Base/SecurityController.php +++ b/app/Http/Controllers/Base/SecurityController.php @@ -27,7 +27,6 @@ namespace Pterodactyl\Http\Controllers\Base; use Illuminate\Http\Request; use Prologue\Alerts\AlertsMessageBag; -use Illuminate\Contracts\Session\Session; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Users\TwoFactorSetupService; use Pterodactyl\Services\Users\ToggleTwoFactorService; @@ -52,11 +51,6 @@ class SecurityController extends Controller */ protected $repository; - /** - * @var \Illuminate\Contracts\Session\Session - */ - protected $session; - /** * @var \Pterodactyl\Services\Users\ToggleTwoFactorService */ @@ -72,7 +66,6 @@ class SecurityController extends Controller * * @param \Prologue\Alerts\AlertsMessageBag $alert * @param \Illuminate\Contracts\Config\Repository $config - * @param \Illuminate\Contracts\Session\Session $session * @param \Pterodactyl\Contracts\Repository\SessionRepositoryInterface $repository * @param \Pterodactyl\Services\Users\ToggleTwoFactorService $toggleTwoFactorService * @param \Pterodactyl\Services\Users\TwoFactorSetupService $twoFactorSetupService @@ -80,7 +73,6 @@ class SecurityController extends Controller public function __construct( AlertsMessageBag $alert, ConfigRepository $config, - Session $session, SessionRepositoryInterface $repository, ToggleTwoFactorService $toggleTwoFactorService, TwoFactorSetupService $twoFactorSetupService @@ -88,7 +80,6 @@ class SecurityController extends Controller $this->alert = $alert; $this->config = $config; $this->repository = $repository; - $this->session = $session; $this->toggleTwoFactorService = $toggleTwoFactorService; $this->twoFactorSetupService = $twoFactorSetupService; } @@ -122,7 +113,9 @@ class SecurityController extends Controller */ public function generateTotp(Request $request) { - return response()->json($this->twoFactorSetupService->handle($request->user())); + return response()->json([ + 'qrImage' => $this->twoFactorSetupService->handle($request->user()), + ]); } /** diff --git a/app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php b/app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php index 6d607211c..2552114ab 100644 --- a/app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php +++ b/app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php @@ -19,7 +19,7 @@ class AllocationAliasFormRequest extends AdminFormRequest public function rules() { return [ - 'alias' => 'required|nullable|string', + 'alias' => 'present|nullable|string', 'allocation_id' => 'required|numeric|exists:allocations,id', ]; } diff --git a/app/Models/Allocation.php b/app/Models/Allocation.php index bb77647d9..2fce57e84 100644 --- a/app/Models/Allocation.php +++ b/app/Models/Allocation.php @@ -60,7 +60,7 @@ class Allocation extends Model implements CleansAttributes, ValidableContract 'node_id' => 'exists:nodes,id', 'ip' => 'ip', 'port' => 'numeric|between:1024,65553', - 'alias' => 'string', + 'ip_alias' => 'nullable|string', 'server_id' => 'nullable|exists:servers,id', ]; diff --git a/app/Models/User.php b/app/Models/User.php index 7b09165aa..39e4a0a03 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -63,6 +63,7 @@ class User extends Model implements 'language', 'use_totp', 'totp_secret', + 'totp_authenticated_at', 'gravatar', 'root_admin', ]; @@ -78,6 +79,11 @@ class User extends Model implements 'gravatar' => 'boolean', ]; + /** + * @var array + */ + protected $dates = [self::CREATED_AT, self::UPDATED_AT, 'totp_authenticated_at']; + /** * The attributes excluded from the model's JSON form. * diff --git a/app/Policies/APIKeyPolicy.php b/app/Policies/APIKeyPolicy.php index 7ca4e0a91..69ce45c04 100644 --- a/app/Policies/APIKeyPolicy.php +++ b/app/Policies/APIKeyPolicy.php @@ -13,7 +13,6 @@ use Cache; use Carbon; use Pterodactyl\Models\User; use Pterodactyl\Models\APIKey as Key; -use Pterodactyl\Models\APIPermission as Permission; class APIKeyPolicy { diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 1f48d33da..c7c928f1a 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Providers; -use Illuminate\Support\Facades\Event; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; class EventServiceProvider extends ServiceProvider diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 96bfb2ec1..57ae43fad 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Providers; -use Pterodactyl\Models\User; use Illuminate\Support\Facades\Route; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index ea333d2dc..d94cd5cac 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -10,7 +10,7 @@ namespace Pterodactyl\Repositories\Eloquent; use Webmozart\Assert\Assert; -use Pterodactyl\Repository\Repository; +use Pterodactyl\Repositories\Repository; use Illuminate\Database\Query\Expression; use Pterodactyl\Contracts\Repository\RepositoryInterface; use Pterodactyl\Exceptions\Model\DataValidationException; diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index 9a7067266..c1dd68a8a 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -18,6 +18,9 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa { use Searchable; + const THRESHOLD_PERCENTAGE_LOW = 75; + const THRESHOLD_PERCENTAGE_MEDIUM = 90; + /** * {@inheritdoc} */ @@ -56,7 +59,7 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa 'value' => number_format($value), 'max' => number_format($maxUsage), 'percent' => $percent, - 'css' => ($percent <= 75) ? 'green' : (($percent > 90) ? 'red' : 'yellow'), + 'css' => ($percent <= self::THRESHOLD_PERCENTAGE_LOW) ? 'green' : (($percent > self::THRESHOLD_PERCENTAGE_MEDIUM) ? 'red' : 'yellow'), ], ]; }) @@ -104,8 +107,7 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa $instance->setRelation( 'allocations', - $instance->allocations()->orderBy('ip', 'asc')->orderBy('port', 'asc') - ->with('server')->paginate(50) + $instance->allocations()->orderBy('ip', 'asc')->orderBy('port', 'asc')->with('server')->paginate(50) ); return $instance; diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index d66b68132..b8f5cc6fc 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -187,21 +187,27 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt } /** - * {@inheritdoc} + * Get data for use when updating a server on the Daemon. Returns an array of + * the egg and pack UUID which are used for build and rebuild. Only loads relations + * if they are missing, or refresh is set to true. + * + * @param \Pterodactyl\Models\Server $server + * @param bool $refresh + * @return array */ - public function getDaemonServiceData($id) + public function getDaemonServiceData(Server $server, bool $refresh = false): array { - Assert::integerish($id, 'First argument passed to getDaemonServiceData must be integer, received %s.'); + if (! $server->relationLoaded('egg') || $refresh) { + $server->load('egg'); + } - $instance = $this->getBuilder()->with('egg.nest', 'pack')->find($id, $this->getColumns()); - if (! $instance) { - throw new RecordNotFoundException(); + if (! $server->relationLoaded('pack') || $refresh) { + $server->load('pack'); } return [ - 'type' => $instance->egg->nest->folder, - 'option' => $instance->egg->tag, - 'pack' => (! is_null($instance->pack_id)) ? $instance->pack->uuid : null, + 'egg' => $server->getRelation('egg')->uuid, + 'pack' => is_null($server->getRelation('pack')) ? null : $server->getRelation('pack')->uuid, ]; } diff --git a/app/Repositories/Repository.php b/app/Repositories/Repository.php index f74b519fe..f9164d284 100644 --- a/app/Repositories/Repository.php +++ b/app/Repositories/Repository.php @@ -7,7 +7,7 @@ * https://opensource.org/licenses/MIT */ -namespace Pterodactyl\Repository; +namespace Pterodactyl\Repositories; use Illuminate\Foundation\Application; use Pterodactyl\Contracts\Repository\RepositoryInterface; diff --git a/app/Services/Schedules/Tasks/TaskCreationService.php b/app/Services/Schedules/Tasks/TaskCreationService.php index 0a015299d..9ea1c1783 100644 --- a/app/Services/Schedules/Tasks/TaskCreationService.php +++ b/app/Services/Schedules/Tasks/TaskCreationService.php @@ -16,6 +16,8 @@ use Pterodactyl\Exceptions\Service\Schedule\Task\TaskIntervalTooLongException; class TaskCreationService { + const MAX_INTERVAL_TIME_SECONDS = 900; + /** * @var \Pterodactyl\Contracts\Repository\TaskRepositoryInterface */ @@ -50,7 +52,7 @@ class TaskCreationService $schedule = ($schedule instanceof Schedule) ? $schedule->id : $schedule; $delay = $data['time_interval'] === 'm' ? $data['time_value'] * 60 : $data['time_value']; - if ($delay > 900) { + if ($delay > self::MAX_INTERVAL_TIME_SECONDS) { throw new TaskIntervalTooLongException(trans('exceptions.tasks.chain_interval_too_long')); } diff --git a/app/Services/Servers/StartupModificationService.php b/app/Services/Servers/StartupModificationService.php index ae91fb3ba..969d4310d 100644 --- a/app/Services/Servers/StartupModificationService.php +++ b/app/Services/Servers/StartupModificationService.php @@ -99,14 +99,17 @@ class StartupModificationService }); } - $daemonData = ['build' => [ - 'env|overwrite' => $this->environmentService->handle($server), - ]]; - + $daemonData = []; if ($this->isUserLevel(User::USER_LEVEL_ADMIN)) { $this->updateAdministrativeSettings($data, $server, $daemonData); } + $daemonData = array_merge_recursive($daemonData, [ + 'build' => [ + 'env|overwrite' => $this->environmentService->handle($server), + ], + ]); + try { $this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->update($daemonData); } catch (RequestException $exception) { @@ -136,17 +139,15 @@ class StartupModificationService 'egg_id' => array_get($data, 'egg_id', $server->egg_id), 'pack_id' => array_get($data, 'pack_id', $server->pack_id) > 0 ? array_get($data, 'pack_id', $server->pack_id) : null, 'skip_scripts' => isset($data['skip_scripts']), + 'image' => array_get($data, 'docker_image', $server->image), ]); - if ( - $server->nest_id != array_get($data, 'nest_id', $server->nest_id) || - $server->egg_id != array_get($data, 'egg_id', $server->egg_id) || - $server->pack_id != array_get($data, 'pack_id', $server->pack_id) - ) { - $daemonData['service'] = array_merge( - $this->repository->withColumns(['id', 'egg_id', 'pack_id'])->getDaemonServiceData($server->id), + $daemonData = array_merge($daemonData, [ + 'build' => ['image' => $server->image], + 'service' => array_merge( + $this->repository->getDaemonServiceData($server, true), ['skip_scripts' => isset($data['skip_scripts'])] - ); - } + ), + ]); } } diff --git a/app/Services/Users/ToggleTwoFactorService.php b/app/Services/Users/ToggleTwoFactorService.php index 56ec6953a..e03a76389 100644 --- a/app/Services/Users/ToggleTwoFactorService.php +++ b/app/Services/Users/ToggleTwoFactorService.php @@ -1,66 +1,82 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Users; +use Carbon\Carbon; use Pterodactyl\Models\User; -use PragmaRX\Google2FA\Contracts\Google2FA; +use PragmaRX\Google2FA\Google2FA; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid; class ToggleTwoFactorService { /** - * @var \PragmaRX\Google2FA\Contracts\Google2FA + * @var \Illuminate\Contracts\Config\Repository */ - protected $google2FA; + private $config; + + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + private $encrypter; + + /** + * @var \PragmaRX\Google2FA\Google2FA + */ + private $google2FA; /** * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface */ - protected $repository; + private $repository; /** * ToggleTwoFactorService constructor. * - * @param \PragmaRX\Google2FA\Contracts\Google2FA $google2FA + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \PragmaRX\Google2FA\Google2FA $google2FA + * @param \Illuminate\Contracts\Config\Repository $config * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository */ public function __construct( + Encrypter $encrypter, Google2FA $google2FA, + Repository $config, UserRepositoryInterface $repository ) { + $this->config = $config; + $this->encrypter = $encrypter; $this->google2FA = $google2FA; $this->repository = $repository; } /** - * @param int|\Pterodactyl\Models\User $user - * @param string $token - * @param null|bool $toggleState + * Toggle 2FA on an account only if the token provided is valid. + * + * @param \Pterodactyl\Models\User $user + * @param string $token + * @param bool|null $toggleState * @return bool * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid */ - public function handle($user, $token, $toggleState = null) + public function handle(User $user, string $token, bool $toggleState = null): bool { - if (! $user instanceof User) { - $user = $this->repository->find($user); - } + $window = $this->config->get('pterodactyl.auth.2fa.window'); + $secret = $this->encrypter->decrypt($user->totp_secret); - if (! $this->google2FA->verifyKey($user->totp_secret, $token, 2)) { + $isValidToken = $this->google2FA->verifyKey($secret, $token, $window); + + if (! $isValidToken) { throw new TwoFactorAuthenticationTokenInvalid; } $this->repository->withoutFresh()->update($user->id, [ + 'totp_authenticated_at' => Carbon::now(), 'use_totp' => (is_null($toggleState) ? ! $user->use_totp : $toggleState), ]); diff --git a/app/Services/Users/TwoFactorSetupService.php b/app/Services/Users/TwoFactorSetupService.php index 608a3643a..a8554ccfc 100644 --- a/app/Services/Users/TwoFactorSetupService.php +++ b/app/Services/Users/TwoFactorSetupService.php @@ -10,7 +10,8 @@ namespace Pterodactyl\Services\Users; use Pterodactyl\Models\User; -use PragmaRX\Google2FA\Contracts\Google2FA; +use PragmaRX\Google2FA\Google2FA; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Illuminate\Contracts\Config\Repository as ConfigRepository; @@ -19,58 +20,62 @@ class TwoFactorSetupService /** * @var \Illuminate\Contracts\Config\Repository */ - protected $config; + private $config; /** - * @var \PragmaRX\Google2FA\Contracts\Google2FA + * @var \Illuminate\Contracts\Encryption\Encrypter */ - protected $google2FA; + private $encrypter; + + /** + * @var \PragmaRX\Google2FA\Google2FA + */ + private $google2FA; /** * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface */ - protected $repository; + private $repository; /** * TwoFactorSetupService constructor. * * @param \Illuminate\Contracts\Config\Repository $config - * @param \PragmaRX\Google2FA\Contracts\Google2FA $google2FA + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \PragmaRX\Google2FA\Google2FA $google2FA * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository */ public function __construct( ConfigRepository $config, + Encrypter $encrypter, Google2FA $google2FA, UserRepositoryInterface $repository ) { $this->config = $config; + $this->encrypter = $encrypter; $this->google2FA = $google2FA; $this->repository = $repository; } /** - * Generate a 2FA token and store it in the database. + * Generate a 2FA token and store it in the database before returning the + * QR code image. * - * @param int|\Pterodactyl\Models\User $user - * @return array + * @param \Pterodactyl\Models\User $user + * @return string * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function handle($user) + public function handle(User $user): string { - if (! $user instanceof User) { - $user = $this->repository->find($user); - } - - $secret = $this->google2FA->generateSecretKey(); + $secret = $this->google2FA->generateSecretKey($this->config->get('pterodactyl.auth.2fa.bytes')); $image = $this->google2FA->getQRCodeGoogleUrl($this->config->get('app.name'), $user->email, $secret); - $this->repository->withoutFresh()->update($user->id, ['totp_secret' => $secret]); + $this->repository->withoutFresh()->update($user->id, [ + 'totp_secret' => $this->encrypter->encrypt($secret), + ]); - return [ - 'qrImage' => $image, - 'secret' => $secret, - ]; + return $image; } } diff --git a/app/Transformers/Admin/SubuserTransformer.php b/app/Transformers/Admin/SubuserTransformer.php index 0bc0ed01a..93ed25d52 100644 --- a/app/Transformers/Admin/SubuserTransformer.php +++ b/app/Transformers/Admin/SubuserTransformer.php @@ -11,7 +11,6 @@ namespace Pterodactyl\Transformers\Admin; use Illuminate\Http\Request; use Pterodactyl\Models\Subuser; -use Pterodactyl\Models\Permission; use League\Fractal\TransformerAbstract; class SubuserTransformer extends TransformerAbstract diff --git a/app/Transformers/User/SubuserTransformer.php b/app/Transformers/User/SubuserTransformer.php index 48d9b5ceb..faac5965c 100644 --- a/app/Transformers/User/SubuserTransformer.php +++ b/app/Transformers/User/SubuserTransformer.php @@ -10,7 +10,6 @@ namespace Pterodactyl\Transformers\User; use Pterodactyl\Models\Subuser; -use Pterodactyl\Models\Permission; use League\Fractal\TransformerAbstract; class SubuserTransformer extends TransformerAbstract diff --git a/composer.json b/composer.json index 58903551f..fabe01f0f 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "mtdowling/cron-expression": "^1.2", "nesbot/carbon": "^1.22", "nicolaslopezj/searchable": "^1.9", - "pragmarx/google2fa": "^1.0", + "pragmarx/google2fa": "^2.0", "predis/predis": "^1.1", "prologue/alerts": "^0.4", "ramsey/uuid": "^3.7", @@ -46,7 +46,7 @@ "require-dev": { "barryvdh/laravel-debugbar": "^2.4", "barryvdh/laravel-ide-helper": "^2.4", - "friendsofphp/php-cs-fixer": "^2.4", + "friendsofphp/php-cs-fixer": "^2.8.0", "fzaninotto/faker": "^1.6", "mockery/mockery": "^0.9", "php-mock/php-mock-phpunit": "^1.1", diff --git a/composer.lock b/composer.lock index 2979fad37..9895b8330 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "3758867d4fb2d20e4b4e45b7c410f79b", + "content-hash": "a393763d136e25a93fd5b636229496cf", "packages": [ { "name": "appstract/laravel-blade-directives", @@ -61,16 +61,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.36.37", + "version": "3.38.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "a6d7fd9f32c63d018a6603a36174b4cb971fccd9" + "reference": "9f704274f4748d2039a16d45b3388ed8dde74e89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a6d7fd9f32c63d018a6603a36174b4cb971fccd9", - "reference": "a6d7fd9f32c63d018a6603a36174b4cb971fccd9", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/9f704274f4748d2039a16d45b3388ed8dde74e89", + "reference": "9f704274f4748d2039a16d45b3388ed8dde74e89", "shasum": "" }, "require": { @@ -137,61 +137,7 @@ "s3", "sdk" ], - "time": "2017-11-03T16:39:35+00:00" - }, - { - "name": "christian-riesen/base32", - "version": "1.3.1", - "source": { - "type": "git", - "url": "https://github.com/ChristianRiesen/base32.git", - "reference": "0a31e50c0fa9b1692d077c86ac188eecdcbaf7fa" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ChristianRiesen/base32/zipball/0a31e50c0fa9b1692d077c86ac188eecdcbaf7fa", - "reference": "0a31e50c0fa9b1692d077c86ac188eecdcbaf7fa", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "4.*", - "satooshi/php-coveralls": "0.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Base32\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Riesen", - "email": "chris.riesen@gmail.com", - "homepage": "http://christianriesen.com", - "role": "Developer" - } - ], - "description": "Base32 encoder/decoder according to RFC 4648", - "homepage": "https://github.com/ChristianRiesen/base32", - "keywords": [ - "base32", - "decode", - "encode", - "rfc4648" - ], - "time": "2016-05-05T11:49:03+00:00" + "time": "2017-11-09T19:15:59+00:00" }, { "name": "daneeveritt/login-notifications", @@ -2055,6 +2001,68 @@ ], "time": "2017-11-04T11:48:34+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "9e7d88e6e4015c2f06a3fa22f06e1d5faa77e6c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/9e7d88e6e4015c2f06a3fa22f06e1d5faa77e6c4", + "reference": "9e7d88e6e4015c2f06a3fa22f06e1d5faa77e6c4", + "shasum": "" + }, + "require": { + "php": "^7" + }, + "require-dev": { + "phpunit/phpunit": "^6", + "vimeo/psalm": "^0.3|^1" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "time": "2017-09-22T14:55:37+00:00" + }, { "name": "paragonie/random_compat", "version": "v2.0.11", @@ -2105,26 +2113,28 @@ }, { "name": "pragmarx/google2fa", - "version": "v1.0.1", + "version": "v2.0.6", "source": { "type": "git", "url": "https://github.com/antonioribeiro/google2fa.git", - "reference": "b346dc138339b745c5831405d00cff7c1351aa0d" + "reference": "bc2d654305e4d09254125f8cd390a7fbc4742d46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/b346dc138339b745c5831405d00cff7c1351aa0d", - "reference": "b346dc138339b745c5831405d00cff7c1351aa0d", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/bc2d654305e4d09254125f8cd390a7fbc4742d46", + "reference": "bc2d654305e4d09254125f8cd390a7fbc4742d46", "shasum": "" }, "require": { - "christian-riesen/base32": "~1.3", + "paragonie/constant_time_encoding": "~1.0|~2.0", "paragonie/random_compat": "~1.4|~2.0", "php": ">=5.4", "symfony/polyfill-php56": "~1.2" }, "require-dev": { - "phpspec/phpspec": "~2.1" + "bacon/bacon-qr-code": "~1.0", + "phpspec/phpspec": "~2.1", + "phpunit/phpunit": "~4" }, "suggest": { "bacon/bacon-qr-code": "Required to generate inline QR Codes." @@ -2132,11 +2142,8 @@ "type": "library", "extra": { "component": "package", - "frameworks": [ - "Laravel" - ], "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -2157,12 +2164,13 @@ ], "description": "A One Time Password Authentication package, compatible with Google Authenticator.", "keywords": [ + "2fa", "Authentication", "Two Factor Authentication", "google2fa", "laravel" ], - "time": "2016-07-18T20:25:04+00:00" + "time": "2017-09-12T06:55:05+00:00" }, { "name": "predis/predis", @@ -3796,16 +3804,16 @@ }, { "name": "watson/validating", - "version": "3.1.1", + "version": "3.1.2", "source": { "type": "git", "url": "https://github.com/dwightwatson/validating.git", - "reference": "ade13078bf2e820e244603446114a28eda51b08c" + "reference": "22edd06d45893f5d4f79c9e901bd7fbce174a79f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dwightwatson/validating/zipball/ade13078bf2e820e244603446114a28eda51b08c", - "reference": "ade13078bf2e820e244603446114a28eda51b08c", + "url": "https://api.github.com/repos/dwightwatson/validating/zipball/22edd06d45893f5d4f79c9e901bd7fbce174a79f", + "reference": "22edd06d45893f5d4f79c9e901bd7fbce174a79f", "shasum": "" }, "require": { @@ -3842,7 +3850,7 @@ "laravel", "validation" ], - "time": "2017-10-08T22:42:01+00:00" + "time": "2017-11-06T21:35:49+00:00" }, { "name": "webmozart/assert", @@ -4291,16 +4299,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.8.0", + "version": "v2.8.1", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "89e7b083f27241e03dd776cb8d6781c77e341db6" + "reference": "04f71e56e03ba2627e345e8c949c80dcef0e683e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/89e7b083f27241e03dd776cb8d6781c77e341db6", - "reference": "89e7b083f27241e03dd776cb8d6781c77e341db6", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/04f71e56e03ba2627e345e8c949c80dcef0e683e", + "reference": "04f71e56e03ba2627e345e8c949c80dcef0e683e", "shasum": "" }, "require": { @@ -4367,7 +4375,7 @@ } ], "description": "A tool to automatically fix PHP code style", - "time": "2017-11-03T02:21:46+00:00" + "time": "2017-11-09T13:31:39+00:00" }, { "name": "fzaninotto/faker", @@ -4421,23 +4429,23 @@ }, { "name": "gecko-packages/gecko-php-unit", - "version": "v2.2", + "version": "v3.0", "source": { "type": "git", "url": "https://github.com/GeckoPackages/GeckoPHPUnit.git", - "reference": "ab525fac9a9ffea219687f261b02008b18ebf2d1" + "reference": "6a866551dffc2154c1b091bae3a7877d39c25ca3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GeckoPackages/GeckoPHPUnit/zipball/ab525fac9a9ffea219687f261b02008b18ebf2d1", - "reference": "ab525fac9a9ffea219687f261b02008b18ebf2d1", + "url": "https://api.github.com/repos/GeckoPackages/GeckoPHPUnit/zipball/6a866551dffc2154c1b091bae3a7877d39c25ca3", + "reference": "6a866551dffc2154c1b091bae3a7877d39c25ca3", "shasum": "" }, "require": { - "php": "^5.3.6 || ^7.0" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.4.3" + "phpunit/phpunit": "^6.0" }, "suggest": { "ext-dom": "When testing with xml.", @@ -4445,6 +4453,11 @@ "phpunit/phpunit": "This is an extension for it so make sure you have it some way." }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, "autoload": { "psr-4": { "GeckoPackages\\PHPUnit\\": "src/PHPUnit" @@ -4461,7 +4474,7 @@ "filesystem", "phpunit" ], - "time": "2017-08-23T07:39:54+00:00" + "time": "2017-08-23T07:46:41+00:00" }, { "name": "hamcrest/hamcrest-php", diff --git a/config/app.php b/config/app.php index c193e42e8..2f9da6704 100644 --- a/config/app.php +++ b/config/app.php @@ -171,7 +171,6 @@ return [ /* * Additional Dependencies */ - PragmaRX\Google2FA\Vendor\Laravel\ServiceProvider::class, igaster\laravelTheme\themeServiceProvider::class, Prologue\Alerts\AlertsServiceProvider::class, Krucas\Settings\Providers\SettingsServiceProvider::class, @@ -213,7 +212,6 @@ return [ 'File' => Illuminate\Support\Facades\File::class, 'Fractal' => Spatie\Fractal\FractalFacade::class, 'Gate' => Illuminate\Support\Facades\Gate::class, - 'Google2FA' => PragmaRX\Google2FA\Vendor\Laravel\Facade::class, 'Hash' => Illuminate\Support\Facades\Hash::class, 'Input' => Illuminate\Support\Facades\Input::class, 'Inspiring' => Illuminate\Foundation\Inspiring::class, diff --git a/config/pterodactyl.php b/config/pterodactyl.php index bd157df23..ad371bce9 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -23,6 +23,11 @@ return [ */ 'auth' => [ 'notifications' => env('LOGIN_NOTIFICATIONS', false), + '2fa' => [ + 'bytes' => 32, + 'window' => env('APP_2FA_WINDOW', 4), + 'verify_newer' => true, + ], ], /* diff --git a/database/migrations/2017_11_11_161922_Add2FaLastAuthorizationTimeColumn.php b/database/migrations/2017_11_11_161922_Add2FaLastAuthorizationTimeColumn.php new file mode 100644 index 000000000..53cb6526b --- /dev/null +++ b/database/migrations/2017_11_11_161922_Add2FaLastAuthorizationTimeColumn.php @@ -0,0 +1,60 @@ +text('totp_secret')->nullable()->change(); + $table->timestampTz('totp_authenticated_at')->after('totp_secret')->nullable(); + }); + + DB::transaction(function () { + DB::table('users')->get()->each(function ($user) { + if (is_null($user->totp_secret)) { + return; + } + + DB::table('users')->where('id', $user->id)->update([ + 'totp_secret' => Crypt::encrypt($user->totp_secret), + 'updated_at' => Carbon::now()->toIso8601String(), + ]); + }); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + DB::transaction(function () { + DB::table('users')->get()->each(function ($user) { + if (is_null($user->totp_secret)) { + return; + } + + DB::table('users')->where('id', $user->id)->update([ + 'totp_secret' => Crypt::decrypt($user->totp_secret), + 'updated_at' => Carbon::now()->toIso8601String(), + ]); + }); + }); + + DB::statement('ALTER TABLE users MODIFY totp_secret CHAR(16) DEFAULT NULL'); + + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('totp_authenticated_at'); + }); + } +} diff --git a/public/themes/pterodactyl/css/pterodactyl.css b/public/themes/pterodactyl/css/pterodactyl.css index 0cc55b214..a0b9da1e8 100644 --- a/public/themes/pterodactyl/css/pterodactyl.css +++ b/public/themes/pterodactyl/css/pterodactyl.css @@ -420,3 +420,7 @@ label.control-label > span.field-optional:before { content: "optional"; color: #bbbbbb; } + +.pagination > li > a, .pagination > li > span { + padding: 3px 10px !important; +} diff --git a/public/themes/pterodactyl/js/frontend/2fa-modal.js b/public/themes/pterodactyl/js/frontend/2fa-modal.js index 022ece2ff..d542b377c 100644 --- a/public/themes/pterodactyl/js/frontend/2fa-modal.js +++ b/public/themes/pterodactyl/js/frontend/2fa-modal.js @@ -42,7 +42,6 @@ var TwoFactorModal = (function () { $('#qr_image_insert').attr('src', image.src).slideDown(); }); }); - $('#2fa_secret_insert').html(data.secret); $('#open2fa').modal('show'); }).fail(function (jqXHR) { alert('An error occured while attempting to load the 2FA setup modal. Please try again.'); diff --git a/resources/lang/es/auth.php b/resources/lang/es/auth.php index 4c33b8f13..e8269476c 100644 --- a/resources/lang/es/auth.php +++ b/resources/lang/es/auth.php @@ -6,7 +6,7 @@ return [ 'authentication_required' => 'La autenticación es necesaria para continuar.', 'remember_me' => 'Recuérdame', 'sign_in' => 'Iniciar Sesión', - 'forgot_password' => 'Olvidé mi contraseña!', + 'forgot_password' => '¡Olvidé mi contraseña!', 'request_reset_text' => '¿Olvidaste tu contraseña? No es el fin del mundo, sólo proporcione su correo electrónico a continuación.', 'reset_password_text' => 'Restablece la contraseña de su cuenta.', 'reset_password' => 'Restablece contraseña de cuenta.', diff --git a/resources/lang/es/server.php b/resources/lang/es/server.php index 8bbb31b4f..b643d4216 100644 --- a/resources/lang/es/server.php +++ b/resources/lang/es/server.php @@ -259,7 +259,7 @@ return [ 'header' => 'El Administrador De Archivos', 'header_sub' => 'Administrar todos tus archivos directamente desde la web.', 'loading' => 'La carga inicial de la estructura del archivo, esto puede tardar unos segundos.', - 'path' => 'Cuando la configuración de rutas de archivo en su servidor de plugins o configuración que debe utilizar :path de acceso como base de la ruta. El tamaño máximo para la web basado en la carga de archivos a este nodo es :tamaño de la.', + 'path' => 'Cuando la configuración de rutas de archivo en su servidor de plugins o configuración que debe utilizar :path de acceso como base de la ruta. El tamaño máximo para la web basado en la carga de archivos a este nodo es :size de la.', 'seconds_ago' => 'hace segundos', 'file_name' => 'Nombre De Archivo', 'size' => 'Tamaño', diff --git a/resources/themes/pterodactyl/admin/nodes/view/allocation.blade.php b/resources/themes/pterodactyl/admin/nodes/view/allocation.blade.php index 730ed5770..50a6a84e5 100644 --- a/resources/themes/pterodactyl/admin/nodes/view/allocation.blade.php +++ b/resources/themes/pterodactyl/admin/nodes/view/allocation.blade.php @@ -72,9 +72,11 @@ @endforeach - + @if($node->allocations->hasPages()) + + @endif
@@ -177,7 +179,7 @@ }, function () { $.ajax({ method: 'DELETE', - url: Router.route('admin.nodes.view.allocation.removeSingle', { id: Pterodactyl.node.id, allocation: allocation }), + url: Router.route('admin.nodes.view.allocation.removeSingle', { node: Pterodactyl.node.id, allocation: allocation }), headers: { 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content') }, }).done(function (data) { element.parent().parent().addClass('warning').delay(100).fadeOut(); diff --git a/resources/themes/pterodactyl/admin/servers/view/details.blade.php b/resources/themes/pterodactyl/admin/servers/view/details.blade.php index 275f99ebd..60bcded6d 100644 --- a/resources/themes/pterodactyl/admin/servers/view/details.blade.php +++ b/resources/themes/pterodactyl/admin/servers/view/details.blade.php @@ -39,7 +39,7 @@
-
+

Base Information

@@ -63,15 +63,6 @@

A brief description of this server.

-
- - -

This token should not be shared with anyone as it has full control over this server.

-
-
- -

Resetting this token will cause any requests using the old token to fail.

-
-
-
-
-

Container Setup

-
-
-
-
- - -

The docker image to use for this server. The default image for this service and option combination is {{ $server->egg->docker_image }}.

-
-
- -
-
-
@endsection diff --git a/resources/themes/pterodactyl/admin/servers/view/startup.blade.php b/resources/themes/pterodactyl/admin/servers/view/startup.blade.php index df78cadb8..15bee661e 100644 --- a/resources/themes/pterodactyl/admin/servers/view/startup.blade.php +++ b/resources/themes/pterodactyl/admin/servers/view/startup.blade.php @@ -109,6 +109,18 @@
+
+
+

Docker Container Configuration

+
+
+
+ + +

The Docker image to use for this server. The default image for the selected egg is .

+
+
+
@@ -122,24 +134,9 @@ {!! Theme::js('vendor/lodash/lodash.js') !!} diff --git a/resources/themes/pterodactyl/admin/settings.blade.php b/resources/themes/pterodactyl/admin/settings.blade.php index 0de2e63fb..3e0407e67 100644 --- a/resources/themes/pterodactyl/admin/settings.blade.php +++ b/resources/themes/pterodactyl/admin/settings.blade.php @@ -34,24 +34,6 @@

This is the name that is used throughout the panel and in emails sent to clients.

- {{--
- -
- -

This is the default language that all clients will use unless they manually change it.

-
-
--}}
@@ -66,13 +48,13 @@ Everybody
-

Require your administrators or users to have 2FA enabled. Users include Admins. Everybody includes Sub Users.

+

For improved security you can require all administrators to have 2-Factor authentication enabled, or even require it for all users on the Panel.

-
In order to modify your SMTP settings for sending mail you will need to run php artisan pterodactyl:mail in this project's root folder.
+
In order to modify your SMTP settings for sending mail you will need to run php artisan p:environment:mail in this project's root folder.
diff --git a/resources/themes/pterodactyl/base/security.blade.php b/resources/themes/pterodactyl/base/security.blade.php index a3a6cc51c..7c4693dd4 100644 --- a/resources/themes/pterodactyl/base/security.blade.php +++ b/resources/themes/pterodactyl/base/security.blade.php @@ -106,8 +106,8 @@
-
-
Loading QR Code...
+
+ Loading QR Code...
@lang('base.security.2fa_checkpoint_help')
diff --git a/resources/themes/pterodactyl/partials/_internal/beta.blade.php b/resources/themes/pterodactyl/partials/_internal/beta.blade.php index d3ef3c46f..88570c8dc 100644 --- a/resources/themes/pterodactyl/partials/_internal/beta.blade.php +++ b/resources/themes/pterodactyl/partials/_internal/beta.blade.php @@ -2,7 +2,7 @@
- You are running a beta version of Pterodactyl Panel. Not all features are complete and bugs should be expected. Please report any bugs on Discord or via our Github issue tracker. + You are running a beta version of Pterodactyl Panel. Not all features are complete and bugs should be expected. Please report any bugs on Discord or via our GitHub Issue Tracker.
diff --git a/resources/themes/pterodactyl/vendor/pagination/bootstrap-4.blade.php b/resources/themes/pterodactyl/vendor/pagination/bootstrap-4.blade.php deleted file mode 100644 index 9d80428cc..000000000 --- a/resources/themes/pterodactyl/vendor/pagination/bootstrap-4.blade.php +++ /dev/null @@ -1,36 +0,0 @@ -@if ($paginator->count() > 1) -
    - - @if ($paginator->onFirstPage()) -
  • «
  • - @else -
  • - @endif - - - @foreach ($elements as $element) - - @if (is_string($element)) -
  • {{ $element }}
  • - @endif - - - @if (is_array($element)) - @foreach ($element as $page => $url) - @if ($page == $paginator->currentPage()) -
  • {{ $page }}
  • - @else -
  • {{ $page }}
  • - @endif - @endforeach - @endif - @endforeach - - - @if ($paginator->hasMorePages()) -
  • - @else -
  • »
  • - @endif -
-@endif diff --git a/resources/themes/pterodactyl/vendor/pagination/default.blade.php b/resources/themes/pterodactyl/vendor/pagination/default.blade.php index 26e56994f..1ecfac985 100644 --- a/resources/themes/pterodactyl/vendor/pagination/default.blade.php +++ b/resources/themes/pterodactyl/vendor/pagination/default.blade.php @@ -1,5 +1,5 @@ @if ($paginator->lastPage() > 1) -
    +
      @if ($paginator->onFirstPage()) {{--
    • «
    • --}} diff --git a/resources/themes/pterodactyl/vendor/pagination/simple-bootstrap-4.blade.php b/resources/themes/pterodactyl/vendor/pagination/simple-bootstrap-4.blade.php deleted file mode 100644 index 4b14efeb5..000000000 --- a/resources/themes/pterodactyl/vendor/pagination/simple-bootstrap-4.blade.php +++ /dev/null @@ -1,17 +0,0 @@ -@if ($paginator->count() > 1) -
        - - @if ($paginator->onFirstPage()) -
      • «
      • - @else -
      • - @endif - - - @if ($paginator->hasMorePages()) -
      • - @else -
      • »
      • - @endif -
      -@endif diff --git a/resources/themes/pterodactyl/vendor/pagination/simple-default.blade.php b/resources/themes/pterodactyl/vendor/pagination/simple-default.blade.php deleted file mode 100644 index a45097ee2..000000000 --- a/resources/themes/pterodactyl/vendor/pagination/simple-default.blade.php +++ /dev/null @@ -1,17 +0,0 @@ -@if ($paginator->count() > 1) -
        - - @if ($paginator->onFirstPage()) -
      • «
      • - @else -
      • - @endif - - - @if ($paginator->hasMorePages()) -
      • - @else -
      • »
      • - @endif -
      -@endif diff --git a/routes/admin.php b/routes/admin.php index 1dfdf9729..edb6fd9f6 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -105,7 +105,6 @@ Route::group(['prefix' => 'servers'], function () { Route::post('/view/{server}/delete', 'ServersController@delete'); Route::patch('/view/{server}/details', 'ServersController@setDetails'); - Route::patch('/view/{server}/details/container', 'ServersController@setContainer')->name('admin.servers.view.details.container'); Route::patch('/view/{server}/database', 'ServersController@resetDatabasePassword'); Route::delete('/view/{server}/database/{database}/delete', 'ServersController@deleteDatabase')->name('admin.servers.view.database.delete'); diff --git a/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php b/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php index 727f2ab54..3c821729e 100644 --- a/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php +++ b/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php @@ -1,69 +1,41 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Http\Controllers\Base; use Mockery as m; -use Tests\TestCase; -use Illuminate\Http\Request; -use Pterodactyl\Models\User; use Prologue\Alerts\AlertsMessageBag; -use Illuminate\Contracts\Session\Session; use Illuminate\Contracts\Config\Repository; -use Tests\Assertions\ControllerAssertionsTrait; +use Tests\Unit\Http\Controllers\ControllerTestCase; use Pterodactyl\Services\Users\TwoFactorSetupService; use Pterodactyl\Services\Users\ToggleTwoFactorService; use Pterodactyl\Http\Controllers\Base\SecurityController; use Pterodactyl\Contracts\Repository\SessionRepositoryInterface; use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid; -class SecurityControllerTest extends TestCase +class SecurityControllerTest extends ControllerTestCase { - use ControllerAssertionsTrait; - /** - * @var \Prologue\Alerts\AlertsMessageBag + * @var \Prologue\Alerts\AlertsMessageBag|\Mockery\Mock */ protected $alert; /** - * @var \Illuminate\Contracts\Config\Repository + * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock */ protected $config; /** - * @var \Pterodactyl\Http\Controllers\Base\SecurityController - */ - protected $controller; - - /** - * @var \Pterodactyl\Contracts\Repository\SessionRepositoryInterface + * @var \Pterodactyl\Contracts\Repository\SessionRepositoryInterface|\Mockery\Mock */ protected $repository; /** - * @var \Illuminate\Http\Request - */ - protected $request; - - /** - * @var \Illuminate\Contracts\Session\Session - */ - protected $session; - - /** - * @var \Pterodactyl\Services\Users\ToggleTwoFactorService + * @var \Pterodactyl\Services\Users\ToggleTwoFactorService|\Mockery\Mock */ protected $toggleTwoFactorService; /** - * @var \Pterodactyl\Services\Users\TwoFactorSetupService + * @var \Pterodactyl\Services\Users\TwoFactorSetupService|\Mockery\Mock */ protected $twoFactorSetupService; @@ -77,19 +49,8 @@ class SecurityControllerTest extends TestCase $this->alert = m::mock(AlertsMessageBag::class); $this->config = m::mock(Repository::class); $this->repository = m::mock(SessionRepositoryInterface::class); - $this->request = m::mock(Request::class); - $this->session = m::mock(Session::class); $this->toggleTwoFactorService = m::mock(ToggleTwoFactorService::class); $this->twoFactorSetupService = m::mock(TwoFactorSetupService::class); - - $this->controller = new SecurityController( - $this->alert, - $this->config, - $this->session, - $this->repository, - $this->toggleTwoFactorService, - $this->twoFactorSetupService - ); } /** @@ -97,13 +58,12 @@ class SecurityControllerTest extends TestCase */ public function testIndexControllerWithDatabaseDriver() { - $model = factory(User::class)->make(); + $model = $this->setRequestUser(); $this->config->shouldReceive('get')->with('session.driver')->once()->andReturn('database'); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); $this->repository->shouldReceive('getUserSessions')->with($model->id)->once()->andReturn(['sessions']); - $response = $this->controller->index($this->request); + $response = $this->getController()->index($this->request); $this->assertIsViewResponse($response); $this->assertViewNameEquals('base.security', $response); $this->assertViewHasKey('sessions', $response); @@ -117,7 +77,7 @@ class SecurityControllerTest extends TestCase { $this->config->shouldReceive('get')->with('session.driver')->once()->andReturn('redis'); - $response = $this->controller->index($this->request); + $response = $this->getController()->index($this->request); $this->assertIsViewResponse($response); $this->assertViewNameEquals('base.security', $response); $this->assertViewHasKey('sessions', $response); @@ -129,14 +89,13 @@ class SecurityControllerTest extends TestCase */ public function testGenerateTotpController() { - $model = factory(User::class)->make(); + $model = $this->setRequestUser(); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); - $this->twoFactorSetupService->shouldReceive('handle')->with($model)->once()->andReturn(['string']); + $this->twoFactorSetupService->shouldReceive('handle')->with($model)->once()->andReturn('qrCodeImage'); - $response = $this->controller->generateTotp($this->request); + $response = $this->getController()->generateTotp($this->request); $this->assertIsJsonResponse($response); - $this->assertResponseJsonEquals(['string'], $response); + $this->assertResponseJsonEquals(['qrImage' => 'qrCodeImage'], $response); } /** @@ -144,13 +103,12 @@ class SecurityControllerTest extends TestCase */ public function testDisableTotpControllerSuccess() { - $model = factory(User::class)->make(); + $model = $this->setRequestUser(); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); $this->request->shouldReceive('input')->with('token')->once()->andReturn('testToken'); $this->toggleTwoFactorService->shouldReceive('handle')->with($model, 'testToken', false)->once()->andReturnNull(); - $response = $this->controller->disableTotp($this->request); + $response = $this->getController()->disableTotp($this->request); $this->assertIsRedirectResponse($response); $this->assertRedirectRouteEquals('account.security', $response); } @@ -160,16 +118,14 @@ class SecurityControllerTest extends TestCase */ public function testDisableTotpControllerWhenExceptionIsThrown() { - $model = factory(User::class)->make(); + $model = $this->setRequestUser(); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); $this->request->shouldReceive('input')->with('token')->once()->andReturn('testToken'); - $this->toggleTwoFactorService->shouldReceive('handle')->with($model, 'testToken', false)->once() - ->andThrow(new TwoFactorAuthenticationTokenInvalid); - $this->alert->shouldReceive('danger')->with(trans('base.security.2fa_disable_error'))->once()->andReturnSelf() - ->shouldReceive('flash')->withNoArgs()->once()->andReturnNull(); + $this->toggleTwoFactorService->shouldReceive('handle')->with($model, 'testToken', false)->once()->andThrow(new TwoFactorAuthenticationTokenInvalid); + $this->alert->shouldReceive('danger')->with(trans('base.security.2fa_disable_error'))->once()->andReturnSelf(); + $this->alert->shouldReceive('flash')->withNoArgs()->once()->andReturnNull(); - $response = $this->controller->disableTotp($this->request); + $response = $this->getController()->disableTotp($this->request); $this->assertIsRedirectResponse($response); $this->assertRedirectRouteEquals('account.security', $response); } @@ -179,13 +135,28 @@ class SecurityControllerTest extends TestCase */ public function testRevokeController() { - $model = factory(User::class)->make(); + $model = $this->setRequestUser(); - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); $this->repository->shouldReceive('deleteUserSession')->with($model->id, 123)->once()->andReturnNull(); - $response = $this->controller->revoke($this->request, 123); + $response = $this->getController()->revoke($this->request, 123); $this->assertIsRedirectResponse($response); $this->assertRedirectRouteEquals('account.security', $response); } + + /** + * Return an instance of the controller for testing with mocked dependencies. + * + * @return \Pterodactyl\Http\Controllers\Base\SecurityController + */ + private function getController(): SecurityController + { + return new SecurityController( + $this->alert, + $this->config, + $this->repository, + $this->toggleTwoFactorService, + $this->twoFactorSetupService + ); + } } diff --git a/tests/Unit/Jobs/Schedule/RunTaskJobTest.php b/tests/Unit/Jobs/Schedule/RunTaskJobTest.php index 176eb4d85..c72ab33b5 100644 --- a/tests/Unit/Jobs/Schedule/RunTaskJobTest.php +++ b/tests/Unit/Jobs/Schedule/RunTaskJobTest.php @@ -64,7 +64,7 @@ class RunTaskJobTest extends TestCase { parent::setUp(); Bus::fake(); - Carbon::setTestNow(); + Carbon::setTestNow(Carbon::now()); $this->commandRepository = m::mock(CommandRepositoryInterface::class); $this->config = m::mock(Repository::class); diff --git a/tests/Unit/Services/DaemonKeys/DaemonKeyProviderServiceTest.php b/tests/Unit/Services/DaemonKeys/DaemonKeyProviderServiceTest.php index 7c240b083..87d5f506b 100644 --- a/tests/Unit/Services/DaemonKeys/DaemonKeyProviderServiceTest.php +++ b/tests/Unit/Services/DaemonKeys/DaemonKeyProviderServiceTest.php @@ -44,7 +44,7 @@ class DaemonKeyProviderServiceTest extends TestCase public function setUp() { parent::setUp(); - Carbon::setTestNow(); + Carbon::setTestNow(Carbon::now()); $this->keyCreationService = m::mock(DaemonKeyCreationService::class); $this->keyUpdateService = m::mock(DaemonKeyUpdateService::class); diff --git a/tests/Unit/Services/Servers/StartupModificationServiceTest.php b/tests/Unit/Services/Servers/StartupModificationServiceTest.php index 5d8076ae8..ca1dc33c0 100644 --- a/tests/Unit/Services/Servers/StartupModificationServiceTest.php +++ b/tests/Unit/Services/Servers/StartupModificationServiceTest.php @@ -107,6 +107,7 @@ class StartupModificationServiceTest extends TestCase { $model = factory(Server::class)->make([ 'egg_id' => 123, + 'image' => 'docker:image', ]); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); @@ -121,22 +122,29 @@ class StartupModificationServiceTest extends TestCase 'variable_id' => 1, ], ['variable_value' => 'stored-value'])->once()->andReturnNull(); - $this->environmentService->shouldReceive('handle')->with($model)->once()->andReturn(['env']); - $this->repository->shouldReceive('update')->with($model->id, m::subset([ 'installed' => 0, 'egg_id' => 456, 'pack_id' => 789, + 'image' => 'docker:image', ]))->once()->andReturn($model); - $this->repository->shouldReceive('withColumns->getDaemonServiceData')->with($model->id)->once()->andReturn([]); + $this->repository->shouldReceive('getDaemonServiceData')->with($model, true)->once()->andReturn([ + 'egg' => 'abcd1234', + 'pack' => 'xyz987', + ]); + + $this->environmentService->shouldReceive('handle')->with($model)->once()->andReturn(['env']); $this->daemonServerRepository->shouldReceive('setNode')->with($model->node_id)->once()->andReturnSelf(); $this->daemonServerRepository->shouldReceive('setAccessServer')->with($model->uuid)->once()->andReturnSelf(); $this->daemonServerRepository->shouldReceive('update')->with([ 'build' => [ 'env|overwrite' => ['env'], + 'image' => $model->image, ], 'service' => [ + 'egg' => 'abcd1234', + 'pack' => 'xyz987', 'skip_scripts' => false, ], ])->once()->andReturnSelf(); @@ -145,7 +153,7 @@ class StartupModificationServiceTest extends TestCase $service = $this->getService(); $service->setUserLevel(User::USER_LEVEL_ADMIN); - $service->handle($model, ['egg_id' => 456, 'pack_id' => 789, 'environment' => ['test' => 'abcd1234']]); + $service->handle($model, ['docker_image' => 'docker:image', 'egg_id' => 456, 'pack_id' => 789, 'environment' => ['test' => 'abcd1234']]); $this->assertTrue(true); } diff --git a/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php b/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php index ae45ec8f1..c8d1cc852 100644 --- a/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php +++ b/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php @@ -1,37 +1,42 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Services\Users; use Mockery as m; +use Carbon\Carbon; use Tests\TestCase; use Pterodactyl\Models\User; -use PragmaRX\Google2FA\Contracts\Google2FA; +use PragmaRX\Google2FA\Google2FA; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Services\Users\ToggleTwoFactorService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; class ToggleTwoFactorServiceTest extends TestCase { - /** - * @var \PragmaRX\Google2FA\Contracts\Google2FA - */ - protected $google2FA; + const TEST_WINDOW_INT = 4; + const USER_TOTP_SECRET = 'encryptedValue'; + const DECRYPTED_USER_SECRET = 'decryptedValue'; /** - * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock */ - protected $repository; + private $config; /** - * @var \Pterodactyl\Services\Users\ToggleTwoFactorService + * @var \Illuminate\Contracts\Encryption\Encrypter|\Mockery\Mock */ - protected $service; + private $encrypter; + + /** + * @var \PragmaRX\Google2FA\Google2FA|\Mockery\Mock + */ + private $google2FA; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock + */ + private $repository; /** * Setup tests. @@ -39,11 +44,15 @@ class ToggleTwoFactorServiceTest extends TestCase public function setUp() { parent::setUp(); + Carbon::setTestNow(Carbon::now()); + $this->config = m::mock(Repository::class); + $this->encrypter = m::mock(Encrypter::class); $this->google2FA = m::mock(Google2FA::class); $this->repository = m::mock(UserRepositoryInterface::class); - $this->service = new ToggleTwoFactorService($this->google2FA, $this->repository); + $this->config->shouldReceive('get')->with('pterodactyl.auth.2fa.window')->once()->andReturn(self::TEST_WINDOW_INT); + $this->encrypter->shouldReceive('decrypt')->with(self::USER_TOTP_SECRET)->once()->andReturn(self::DECRYPTED_USER_SECRET); } /** @@ -51,13 +60,15 @@ class ToggleTwoFactorServiceTest extends TestCase */ public function testTwoFactorIsEnabledForUser() { - $model = factory(User::class)->make(['totp_secret' => 'secret', 'use_totp' => false]); + $model = factory(User::class)->make(['totp_secret' => self::USER_TOTP_SECRET, 'use_totp' => false]); - $this->google2FA->shouldReceive('verifyKey')->with($model->totp_secret, 'test-token', 2)->once()->andReturn(true); - $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($model->id, ['use_totp' => true])->once()->andReturnNull(); + $this->google2FA->shouldReceive('verifyKey')->with(self::DECRYPTED_USER_SECRET, 'test-token', self::TEST_WINDOW_INT)->once()->andReturn(true); + $this->repository->shouldReceive('withoutFresh->update')->with($model->id, [ + 'totp_authenticated_at' => Carbon::now(), + 'use_totp' => true, + ])->once()->andReturnNull(); - $this->assertTrue($this->service->handle($model, 'test-token')); + $this->assertTrue($this->getService()->handle($model, 'test-token')); } /** @@ -65,13 +76,15 @@ class ToggleTwoFactorServiceTest extends TestCase */ public function testTwoFactorIsDisabled() { - $model = factory(User::class)->make(['totp_secret' => 'secret', 'use_totp' => true]); + $model = factory(User::class)->make(['totp_secret' => self::USER_TOTP_SECRET, 'use_totp' => true]); - $this->google2FA->shouldReceive('verifyKey')->with($model->totp_secret, 'test-token', 2)->once()->andReturn(true); - $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($model->id, ['use_totp' => false])->once()->andReturnNull(); + $this->google2FA->shouldReceive('verifyKey')->with(self::DECRYPTED_USER_SECRET, 'test-token', self::TEST_WINDOW_INT)->once()->andReturn(true); + $this->repository->shouldReceive('withoutFresh->update')->with($model->id, [ + 'totp_authenticated_at' => Carbon::now(), + 'use_totp' => false, + ])->once()->andReturnNull(); - $this->assertTrue($this->service->handle($model, 'test-token')); + $this->assertTrue($this->getService()->handle($model, 'test-token')); } /** @@ -79,13 +92,15 @@ class ToggleTwoFactorServiceTest extends TestCase */ public function testTwoFactorRemainsDisabledForUser() { - $model = factory(User::class)->make(['totp_secret' => 'secret', 'use_totp' => false]); + $model = factory(User::class)->make(['totp_secret' => self::USER_TOTP_SECRET, 'use_totp' => false]); - $this->google2FA->shouldReceive('verifyKey')->with($model->totp_secret, 'test-token', 2)->once()->andReturn(true); - $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($model->id, ['use_totp' => false])->once()->andReturnNull(); + $this->google2FA->shouldReceive('verifyKey')->with(self::DECRYPTED_USER_SECRET, 'test-token', self::TEST_WINDOW_INT)->once()->andReturn(true); + $this->repository->shouldReceive('withoutFresh->update')->with($model->id, [ + 'totp_authenticated_at' => Carbon::now(), + 'use_totp' => false, + ])->once()->andReturnNull(); - $this->assertTrue($this->service->handle($model, 'test-token', false)); + $this->assertTrue($this->getService()->handle($model, 'test-token', false)); } /** @@ -95,23 +110,19 @@ class ToggleTwoFactorServiceTest extends TestCase */ public function testExceptionIsThrownIfTokenIsInvalid() { - $model = factory(User::class)->make(); + $model = factory(User::class)->make(['totp_secret' => self::USER_TOTP_SECRET]); $this->google2FA->shouldReceive('verifyKey')->once()->andReturn(false); - $this->service->handle($model, 'test-token'); + $this->getService()->handle($model, 'test-token'); } /** - * Test that an integer can be passed in place of a user model. + * Return an instance of the service with mocked dependencies. + * + * @return \Pterodactyl\Services\Users\ToggleTwoFactorService */ - public function testIntegerCanBePassedInPlaceOfUserModel() + private function getService(): ToggleTwoFactorService { - $model = factory(User::class)->make(['totp_secret' => 'secret', 'use_totp' => false]); - - $this->repository->shouldReceive('find')->with($model->id)->once()->andReturn($model); - $this->google2FA->shouldReceive('verifyKey')->once()->andReturn(true); - $this->repository->shouldReceive('withoutFresh->update')->once()->andReturnNull(); - - $this->assertTrue($this->service->handle($model->id, 'test-token')); + return new ToggleTwoFactorService($this->encrypter, $this->google2FA, $this->config, $this->repository); } } diff --git a/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php b/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php index e58d99f2c..d6f5f8b90 100644 --- a/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php +++ b/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php @@ -1,43 +1,37 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Services\Users; use Mockery as m; use Tests\TestCase; use Pterodactyl\Models\User; +use PragmaRX\Google2FA\Google2FA; use Illuminate\Contracts\Config\Repository; -use PragmaRX\Google2FA\Contracts\Google2FA; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Services\Users\TwoFactorSetupService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; class TwoFactorSetupServiceTest extends TestCase { /** - * @var \Illuminate\Contracts\Config\Repository + * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock */ - protected $config; + private $config; /** - * @var \PragmaRX\Google2FA\Contracts\Google2FA + * @var \Illuminate\Contracts\Encryption\Encrypter|\Mockery\Mock */ - protected $google2FA; + private $encrypter; /** - * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + * @var \PragmaRX\Google2FA\Google2FA|\Mockery\Mock */ - protected $repository; + private $google2FA; /** - * @var \Pterodactyl\Services\Users\TwoFactorSetupService + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock */ - protected $service; + private $repository; /** * Setup tests. @@ -47,10 +41,9 @@ class TwoFactorSetupServiceTest extends TestCase parent::setUp(); $this->config = m::mock(Repository::class); + $this->encrypter = m::mock(Encrypter::class); $this->google2FA = m::mock(Google2FA::class); $this->repository = m::mock(UserRepositoryInterface::class); - - $this->service = new TwoFactorSetupService($this->config, $this->google2FA, $this->repository); } /** @@ -60,34 +53,25 @@ class TwoFactorSetupServiceTest extends TestCase { $model = factory(User::class)->make(); - $this->google2FA->shouldReceive('generateSecretKey')->withNoArgs()->once()->andReturn('secretKey'); + $this->config->shouldReceive('get')->with('pterodactyl.auth.2fa.bytes')->once()->andReturn(32); + $this->google2FA->shouldReceive('generateSecretKey')->with(32)->once()->andReturn('secretKey'); $this->config->shouldReceive('get')->with('app.name')->once()->andReturn('CompanyName'); - $this->google2FA->shouldReceive('getQRCodeGoogleUrl')->with('CompanyName', $model->email, 'secretKey') - ->once()->andReturn('http://url.com'); - $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with($model->id, ['totp_secret' => 'secretKey'])->once()->andReturnNull(); + $this->google2FA->shouldReceive('getQRCodeGoogleUrl')->with('CompanyName', $model->email, 'secretKey')->once()->andReturn('http://url.com'); + $this->encrypter->shouldReceive('encrypt')->with('secretKey')->once()->andReturn('encryptedSecret'); + $this->repository->shouldReceive('withoutFresh->update')->with($model->id, ['totp_secret' => 'encryptedSecret'])->once()->andReturnNull(); - $response = $this->service->handle($model); + $response = $this->getService()->handle($model); $this->assertNotEmpty($response); - $this->assertArrayHasKey('qrImage', $response); - $this->assertArrayHasKey('secret', $response); - $this->assertEquals('http://url.com', $response['qrImage']); - $this->assertEquals('secretKey', $response['secret']); + $this->assertSame('http://url.com', $response); } /** - * Test that an integer can be passed in place of the user model. + * Return an instance of the service to test with mocked dependencies. + * + * @return \Pterodactyl\Services\Users\TwoFactorSetupService */ - public function testIntegerCanBePassedInPlaceOfUserModel() + private function getService(): TwoFactorSetupService { - $model = factory(User::class)->make(); - - $this->repository->shouldReceive('find')->with($model->id)->once()->andReturn($model); - $this->google2FA->shouldReceive('generateSecretKey')->withNoArgs()->once()->andReturnNull(); - $this->config->shouldReceive('get')->with('app.name')->once()->andReturnNull(); - $this->google2FA->shouldReceive('getQRCodeGoogleUrl')->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFresh->update')->once()->andReturnNull(); - - $this->assertTrue(is_array($this->service->handle($model->id))); + return new TwoFactorSetupService($this->config, $this->encrypter, $this->google2FA, $this->repository); } }