diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index 8851c579..13a93176 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Contracts\Repository; -use Pterodactyl\Models\User; use Pterodactyl\Models\Server; use Illuminate\Support\Collection; use Illuminate\Contracts\Pagination\LengthAwarePaginator; @@ -107,16 +106,6 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter */ public function getDaemonServiceData(Server $server, bool $refresh = false): array; - /** - * Return a paginated list of servers that a user can access at a given level. - * - * @param \Pterodactyl\Models\User $user - * @param int $level - * @param bool|int $paginate - * @return \Illuminate\Pagination\LengthAwarePaginator|\Illuminate\Database\Eloquent\Collection - */ - public function filterUserAccessServers(User $user, int $level, $paginate = 25); - /** * Return a server by UUID. * diff --git a/app/Http/Controllers/Api/Client/ClientApiController.php b/app/Http/Controllers/Api/Client/ClientApiController.php index 7e304617..b3d0d0ea 100644 --- a/app/Http/Controllers/Api/Client/ClientApiController.php +++ b/app/Http/Controllers/Api/Client/ClientApiController.php @@ -10,6 +10,38 @@ use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; abstract class ClientApiController extends ApplicationApiController { + /** + * Returns only the includes which are valid for the given transformer. + * + * @param \Pterodactyl\Transformers\Api\Client\BaseClientTransformer $transformer + * @param array $merge + * @return string[] + */ + protected function getIncludesForTransformer(BaseClientTransformer $transformer, array $merge = []) + { + $filtered = array_filter($this->parseIncludes(), function ($datum) use ($transformer) { + return in_array($datum, $transformer->getAvailableIncludes()); + }); + + return array_merge($filtered, $merge); + } + + /** + * Returns the parsed includes for this request. + */ + protected function parseIncludes() + { + $includes = $this->request->query('include'); + + if (! is_string($includes)) { + return $includes; + } + + return array_map(function ($item) { + return trim($item); + }, explode(',', $includes)); + } + /** * Return an instance of an application transformer. * diff --git a/app/Http/Controllers/Api/Client/ClientController.php b/app/Http/Controllers/Api/Client/ClientController.php index b673ac5b..300770aa 100644 --- a/app/Http/Controllers/Api/Client/ClientController.php +++ b/app/Http/Controllers/Api/Client/ClientController.php @@ -3,7 +3,9 @@ namespace Pterodactyl\Http\Controllers\Api\Client; use Pterodactyl\Models\User; +use Pterodactyl\Models\Server; use Pterodactyl\Models\Permission; +use Spatie\QueryBuilder\QueryBuilder; use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Transformers\Api\Client\ServerTransformer; use Pterodactyl\Http\Requests\Api\Client\GetServersRequest; @@ -36,32 +38,36 @@ class ClientController extends ClientApiController */ public function index(GetServersRequest $request): array { - // Check for the filter parameter on the request. - switch ($request->input('filter')) { - case 'all': - $filter = User::FILTER_LEVEL_ALL; - break; - case 'admin': - $filter = User::FILTER_LEVEL_ADMIN; - break; - case 'owner': - $filter = User::FILTER_LEVEL_OWNER; - break; - case 'subuser-of': - default: - $filter = User::FILTER_LEVEL_SUBUSER; - break; + $user = $request->user(); + $level = $request->getFilterLevel(); + $transformer = $this->getTransformer(ServerTransformer::class); + + // Start the query builder and ensure we eager load any requested relationships from the request. + $builder = Server::query()->with($this->getIncludesForTransformer($transformer, ['node'])); + + if ($level === User::FILTER_LEVEL_OWNER) { + $builder = $builder->where('owner_id', $request->user()->id); + } + // If set to all, display all servers they can access, including those they access as an + // admin. If set to subuser, only return the servers they can access because they are owner, + // or marked as a subuser of the server. + elseif (($level === User::FILTER_LEVEL_ALL && ! $user->root_admin) || $level === User::FILTER_LEVEL_SUBUSER) { + $builder = $builder->whereIn('id', $user->accessibleServers()->pluck('id')->all()); + } + // If set to admin, only display the servers a user can access because they are an administrator. + // This means only servers the user would not have access to if they were not an admin (because they + // are not an owner or subuser) are returned. + elseif ($level === User::FILTER_LEVEL_ADMIN && $user->root_admin) { + $builder = $builder->whereNotIn('id', $user->accessibleServers()->pluck('id')->all()); } - $servers = $this->repository - ->setSearchTerm($request->input('query')) - ->filterUserAccessServers( - $request->user(), $filter, config('pterodactyl.paginate.frontend.servers') - ); + $builder = QueryBuilder::for($builder)->allowedFilters( + 'uuid', 'name', 'external_id' + ); - return $this->fractal->collection($servers) - ->transformWith($this->getTransformer(ServerTransformer::class)) - ->toArray(); + $servers = $builder->paginate(min($request->query('per_page', 50), 100))->appends($request->query()); + + return $this->fractal->transformWith($transformer)->collection($servers)->toArray(); } /** diff --git a/app/Http/Controllers/Base/IndexController.php b/app/Http/Controllers/Base/IndexController.php index f62de118..41ff988f 100644 --- a/app/Http/Controllers/Base/IndexController.php +++ b/app/Http/Controllers/Base/IndexController.php @@ -2,8 +2,6 @@ namespace Pterodactyl\Http\Controllers\Base; -use Illuminate\Http\Request; -use Pterodactyl\Models\User; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; @@ -27,15 +25,10 @@ class IndexController extends Controller /** * Returns listing of user's servers. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function index(Request $request) + public function index() { - $servers = $this->repository->setSearchTerm($request->input('query'))->filterUserAccessServers( - $request->user(), User::FILTER_LEVEL_ALL, config('pterodactyl.paginate.frontend.servers') - ); - - return view('templates/base.core', ['servers' => $servers]); + return view('templates/base.core'); } } diff --git a/app/Http/Requests/Api/Client/GetServersRequest.php b/app/Http/Requests/Api/Client/GetServersRequest.php index 9b4601f2..c28f0a94 100644 --- a/app/Http/Requests/Api/Client/GetServersRequest.php +++ b/app/Http/Requests/Api/Client/GetServersRequest.php @@ -2,6 +2,8 @@ namespace Pterodactyl\Http\Requests\Api\Client; +use Pterodactyl\Models\User; + class GetServersRequest extends ClientApiRequest { /** @@ -11,4 +13,28 @@ class GetServersRequest extends ClientApiRequest { return true; } + + /** + * Return the filtering method for servers when the client base endpoint is requested. + * + * @return int + */ + public function getFilterLevel(): int + { + switch ($this->input('type')) { + case 'all': + return User::FILTER_LEVEL_ALL; + break; + case 'admin': + return User::FILTER_LEVEL_ADMIN; + break; + case 'owner': + return User::FILTER_LEVEL_OWNER; + break; + case 'subuser-of': + default: + return User::FILTER_LEVEL_SUBUSER; + break; + } + } } diff --git a/app/Http/ViewComposers/Server/ServerDataComposer.php b/app/Http/ViewComposers/Server/ServerDataComposer.php deleted file mode 100644 index 9e185864..00000000 --- a/app/Http/ViewComposers/Server/ServerDataComposer.php +++ /dev/null @@ -1,38 +0,0 @@ -request = $request; - } - - /** - * Attach server data to a view automatically. - * - * @param \Illuminate\View\View $view - */ - public function compose(View $view) - { - $server = $this->request->get('server'); - - $view->with('server', $server); - $view->with('node', object_get($server, 'node')); - $view->with('daemon_token', $this->request->get('server_token')); - } -} diff --git a/app/Http/ViewComposers/ServerListComposer.php b/app/Http/ViewComposers/ServerListComposer.php deleted file mode 100644 index 9b57884a..00000000 --- a/app/Http/ViewComposers/ServerListComposer.php +++ /dev/null @@ -1,51 +0,0 @@ -request = $request; - $this->repository = $repository; - } - - /** - * Attach a list of servers the user can access to the view. - * - * @param \Illuminate\View\View $view - */ - public function compose(View $view) - { - if (! $this->request->user()) { - return; - } - - $servers = $this->repository - ->setColumns(['id', 'owner_id', 'uuidShort', 'name', 'description']) - ->filterUserAccessServers($this->request->user(), User::FILTER_LEVEL_SUBUSER, false); - - $view->with('sidebarServerList', $servers); - } -} diff --git a/app/Models/User.php b/app/Models/User.php index 408ceead..360fa391 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -7,6 +7,7 @@ use Illuminate\Support\Collection; use Illuminate\Validation\Rules\In; use Illuminate\Auth\Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Database\Eloquent\Builder; use Pterodactyl\Models\Traits\Searchable; use Illuminate\Auth\Passwords\CanResetPassword; use Pterodactyl\Traits\Helpers\AvailableLanguages; @@ -260,4 +261,21 @@ class User extends Model implements { return $this->hasMany(RecoveryToken::class); } + + /** + * Returns all of the servers that a user can access by way of being the owner of the + * server, or because they are assigned as a subuser for that server. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function accessibleServers() + { + return $this->hasMany(Server::class, 'owner_id') + ->select('servers.*') + ->leftJoin('subusers', 'subusers.server_id', '=', 'servers.id') + ->where(function (Builder $builder) { + $builder->where('servers.owner_id', $this->id)->orWhere('subusers.user_id', $this->id); + }) + ->groupBy('servers.id'); + } } diff --git a/app/Providers/ViewComposerServiceProvider.php b/app/Providers/ViewComposerServiceProvider.php index 9490234f..9f484e00 100644 --- a/app/Providers/ViewComposerServiceProvider.php +++ b/app/Providers/ViewComposerServiceProvider.php @@ -4,8 +4,6 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; use Pterodactyl\Http\ViewComposers\AssetComposer; -use Pterodactyl\Http\ViewComposers\ServerListComposer; -use Pterodactyl\Http\ViewComposers\Server\ServerDataComposer; class ViewComposerServiceProvider extends ServiceProvider { @@ -15,10 +13,5 @@ class ViewComposerServiceProvider extends ServiceProvider public function boot() { $this->app->make('view')->composer('*', AssetComposer::class); - - $this->app->make('view')->composer('server.*', ServerDataComposer::class); - - // Add data to make the sidebar work when viewing a server. - $this->app->make('view')->composer(['server.*'], ServerListComposer::class); } } diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index 73e671cc..34b5247a 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -2,9 +2,11 @@ namespace Pterodactyl\Repositories\Eloquent; +use Illuminate\Http\Request; use Webmozart\Assert\Assert; use Illuminate\Support\Collection; use Pterodactyl\Repositories\Repository; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Query\Expression; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Contracts\Pagination\LengthAwarePaginator; @@ -15,6 +17,53 @@ use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; abstract class EloquentRepository extends Repository implements RepositoryInterface { + /** + * @var bool + */ + protected $useRequestFilters = false; + + /** + * Determines if the repository function should use filters off the request object + * present when returning results. This allows repository methods to be called in API + * context's such that we can pass through ?filter[name]=Dane&sort=desc for example. + * + * @param bool $usingFilters + * @return $this + */ + public function usingRequestFilters($usingFilters = true) + { + $this->useRequestFilters = $usingFilters; + + return $this; + } + + /** + * Returns the request instance. + * + * @return \Illuminate\Http\Request + */ + protected function request() + { + return $this->app->make(Request::class); + } + + /** + * Paginate the response data based on the page para. + * + * @param \Illuminate\Database\Eloquent\Builder $instance + * @param int $default + * + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + protected function paginate(Builder $instance, int $default = 50) + { + if (! $this->useRequestFilters) { + return $instance->paginate($default); + } + + return $instance->paginate($this->request()->query('per_page', $default)); + } + /** * Return an instance of the eloquent model bound to this * repository instance. @@ -236,6 +285,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf * Return all records associated with the given model. * * @return \Illuminate\Support\Collection + * @deprecated Just use the model */ public function all(): Collection { @@ -313,6 +363,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf * Get the amount of entries in the database. * * @return int + * @deprecated just use the count method off a model */ public function count(): int { diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index e3e398d5..a64f68db 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Repositories\Eloquent; -use Pterodactyl\Models\User; use Pterodactyl\Models\Server; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Builder; @@ -226,43 +225,6 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt ]; } - /** - * Return a paginated list of servers that a user can access at a given level. - * - * @param \Pterodactyl\Models\User $user - * @param int $level - * @param bool|int $paginate - * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator|\Illuminate\Database\Eloquent\Collection - */ - public function filterUserAccessServers(User $user, int $level, $paginate = 25) - { - $instance = $this->getBuilder()->select($this->getColumns())->with(['user', 'node', 'allocation']); - - // If access level is set to owner, only display servers - // that the user owns. - if ($level === User::FILTER_LEVEL_OWNER) { - $instance->where('owner_id', $user->id); - } - - // If set to all, display all servers they can access, including - // those they access as an admin. If set to subuser, only return - // the servers they can access because they are owner, or marked - // as a subuser of the server. - elseif (($level === User::FILTER_LEVEL_ALL && ! $user->root_admin) || $level === User::FILTER_LEVEL_SUBUSER) { - $instance->whereIn('id', $this->getUserAccessServers($user->id)); - } - - // If set to admin, only display the servers a user can access - // as an administrator (leaves out owned and subuser of). - elseif ($level === User::FILTER_LEVEL_ADMIN && $user->root_admin) { - $instance->whereNotIn('id', $this->getUserAccessServers($user->id)); - } - - $instance->search($this->getSearchTerm()); - - return $paginate ? $instance->paginate($paginate) : $instance->get(); - } - /** * Return a server by UUID. * @@ -339,20 +301,6 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt return ! $this->getBuilder()->where('uuid', '=', $uuid)->orWhere('uuidShort', '=', $short)->exists(); } - /** - * Return an array of server IDs that a given user can access based - * on owner and subuser permissions. - * - * @param int $user - * @return int[] - */ - private function getUserAccessServers(int $user): array - { - return $this->getBuilder()->select('id')->where('owner_id', $user)->union( - $this->app->make(SubuserRepository::class)->getBuilder()->select('server_id')->where('user_id', $user) - )->pluck('id')->all(); - } - /** * Get the amount of servers that are suspended. * diff --git a/composer.json b/composer.json index 0ccfab91..a07a9ae8 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "psy/psysh": "^0.10.4", "s1lentium/iptools": "^1.1", "spatie/laravel-fractal": "^5.7", + "spatie/laravel-query-builder": "^2.8", "staudenmeir/belongs-to-through": "^2.10", "symfony/yaml": "^4.4", "webmozart/assert": "^1.9" diff --git a/composer.lock b/composer.lock index 7593ee4b..6df479de 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "155b8e930e604c0476fa975b1084ca3f", + "content-hash": "d05ab995e4aff4b847ff2a027924065c", "packages": [ { "name": "appstract/laravel-blade-directives", @@ -3361,6 +3361,70 @@ ], "time": "2020-03-02T18:40:49+00:00" }, + { + "name": "spatie/laravel-query-builder", + "version": "2.8.2", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-query-builder.git", + "reference": "2737b2298e8bfeb632a80013646943307bf31775" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-query-builder/zipball/2737b2298e8bfeb632a80013646943307bf31775", + "reference": "2737b2298e8bfeb632a80013646943307bf31775", + "shasum": "" + }, + "require": { + "illuminate/database": "~5.6.34|~5.7.0|~5.8.0|^6.0|^7.0", + "illuminate/http": "~5.6.34|~5.7.0|~5.8.0|^6.0|^7.0", + "illuminate/support": "~5.6.34|~5.7.0|~5.8.0|^6.0|^7.0", + "php": "^7.1" + }, + "require-dev": { + "ext-json": "*", + "orchestra/testbench": "~3.6.0|~3.7.0|~3.8.0|^4.0|^5.0", + "phpunit/phpunit": "^7.0|^8.0|^9.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\QueryBuilder\\QueryBuilderServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\QueryBuilder\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Vanderbist", + "email": "alex@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Easily build Eloquent queries from API requests", + "homepage": "https://github.com/spatie/laravel-query-builder", + "keywords": [ + "laravel-query-builder", + "spatie" + ], + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + } + ], + "time": "2020-05-25T09:36:37+00:00" + }, { "name": "staudenmeir/belongs-to-through", "version": "v2.10", diff --git a/resources/scripts/api/getServers.ts b/resources/scripts/api/getServers.ts index 42b2d501..1be6fe9f 100644 --- a/resources/scripts/api/getServers.ts +++ b/resources/scripts/api/getServers.ts @@ -6,8 +6,8 @@ export default (query?: string, includeAdmin?: boolean): Promise resolve({