Code cleanup & fix frontend searching servers; closes #2100

This commit is contained in:
Dane Everitt 2020-07-06 21:25:00 -07:00
parent f0e18ba6f7
commit d3c749ac56
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
14 changed files with 226 additions and 194 deletions

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Contracts\Repository; namespace Pterodactyl\Contracts\Repository;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Contracts\Pagination\LengthAwarePaginator;
@ -107,16 +106,6 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter
*/ */
public function getDaemonServiceData(Server $server, bool $refresh = false): array; 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. * Return a server by UUID.
* *

View file

@ -10,6 +10,38 @@ use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
abstract class ClientApiController extends 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. * Return an instance of an application transformer.
* *

View file

@ -3,7 +3,9 @@
namespace Pterodactyl\Http\Controllers\Api\Client; namespace Pterodactyl\Http\Controllers\Api\Client;
use Pterodactyl\Models\User; use Pterodactyl\Models\User;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Permission; use Pterodactyl\Models\Permission;
use Spatie\QueryBuilder\QueryBuilder;
use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Transformers\Api\Client\ServerTransformer; use Pterodactyl\Transformers\Api\Client\ServerTransformer;
use Pterodactyl\Http\Requests\Api\Client\GetServersRequest; use Pterodactyl\Http\Requests\Api\Client\GetServersRequest;
@ -36,32 +38,36 @@ class ClientController extends ClientApiController
*/ */
public function index(GetServersRequest $request): array public function index(GetServersRequest $request): array
{ {
// Check for the filter parameter on the request. $user = $request->user();
switch ($request->input('filter')) { $level = $request->getFilterLevel();
case 'all': $transformer = $this->getTransformer(ServerTransformer::class);
$filter = User::FILTER_LEVEL_ALL;
break; // Start the query builder and ensure we eager load any requested relationships from the request.
case 'admin': $builder = Server::query()->with($this->getIncludesForTransformer($transformer, ['node']));
$filter = User::FILTER_LEVEL_ADMIN;
break; if ($level === User::FILTER_LEVEL_OWNER) {
case 'owner': $builder = $builder->where('owner_id', $request->user()->id);
$filter = User::FILTER_LEVEL_OWNER; }
break; // If set to all, display all servers they can access, including those they access as an
case 'subuser-of': // admin. If set to subuser, only return the servers they can access because they are owner,
default: // or marked as a subuser of the server.
$filter = User::FILTER_LEVEL_SUBUSER; elseif (($level === User::FILTER_LEVEL_ALL && ! $user->root_admin) || $level === User::FILTER_LEVEL_SUBUSER) {
break; $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 $builder = QueryBuilder::for($builder)->allowedFilters(
->setSearchTerm($request->input('query')) 'uuid', 'name', 'external_id'
->filterUserAccessServers(
$request->user(), $filter, config('pterodactyl.paginate.frontend.servers')
); );
return $this->fractal->collection($servers) $servers = $builder->paginate(min($request->query('per_page', 50), 100))->appends($request->query());
->transformWith($this->getTransformer(ServerTransformer::class))
->toArray(); return $this->fractal->transformWith($transformer)->collection($servers)->toArray();
} }
/** /**

View file

@ -2,8 +2,6 @@
namespace Pterodactyl\Http\Controllers\Base; namespace Pterodactyl\Http\Controllers\Base;
use Illuminate\Http\Request;
use Pterodactyl\Models\User;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
@ -27,15 +25,10 @@ class IndexController extends Controller
/** /**
* Returns listing of user's servers. * Returns listing of user's servers.
* *
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View * @return \Illuminate\View\View
*/ */
public function index(Request $request) public function index()
{ {
$servers = $this->repository->setSearchTerm($request->input('query'))->filterUserAccessServers( return view('templates/base.core');
$request->user(), User::FILTER_LEVEL_ALL, config('pterodactyl.paginate.frontend.servers')
);
return view('templates/base.core', ['servers' => $servers]);
} }
} }

View file

@ -2,6 +2,8 @@
namespace Pterodactyl\Http\Requests\Api\Client; namespace Pterodactyl\Http\Requests\Api\Client;
use Pterodactyl\Models\User;
class GetServersRequest extends ClientApiRequest class GetServersRequest extends ClientApiRequest
{ {
/** /**
@ -11,4 +13,28 @@ class GetServersRequest extends ClientApiRequest
{ {
return true; 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;
}
}
} }

View file

@ -1,38 +0,0 @@
<?php
namespace Pterodactyl\Http\ViewComposers\Server;
use Illuminate\View\View;
use Illuminate\Http\Request;
class ServerDataComposer
{
/**
* @var \Illuminate\Http\Request
*/
protected $request;
/**
* ServerDataComposer constructor.
*
* @param \Illuminate\Http\Request $request
*/
public function __construct(Request $request)
{
$this->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'));
}
}

View file

@ -1,51 +0,0 @@
<?php
namespace Pterodactyl\Http\ViewComposers;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Pterodactyl\Models\User;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class ServerListComposer
{
/**
* @var \Illuminate\Http\Request
*/
private $request;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
private $repository;
/**
* ServerListComposer constructor.
*
* @param \Illuminate\Http\Request $request
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
*/
public function __construct(Request $request, ServerRepositoryInterface $repository)
{
$this->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);
}
}

View file

@ -7,6 +7,7 @@ use Illuminate\Support\Collection;
use Illuminate\Validation\Rules\In; use Illuminate\Validation\Rules\In;
use Illuminate\Auth\Authenticatable; use Illuminate\Auth\Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Builder;
use Pterodactyl\Models\Traits\Searchable; use Pterodactyl\Models\Traits\Searchable;
use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Auth\Passwords\CanResetPassword;
use Pterodactyl\Traits\Helpers\AvailableLanguages; use Pterodactyl\Traits\Helpers\AvailableLanguages;
@ -260,4 +261,21 @@ class User extends Model implements
{ {
return $this->hasMany(RecoveryToken::class); 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');
}
} }

View file

@ -4,8 +4,6 @@ namespace Pterodactyl\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Pterodactyl\Http\ViewComposers\AssetComposer; use Pterodactyl\Http\ViewComposers\AssetComposer;
use Pterodactyl\Http\ViewComposers\ServerListComposer;
use Pterodactyl\Http\ViewComposers\Server\ServerDataComposer;
class ViewComposerServiceProvider extends ServiceProvider class ViewComposerServiceProvider extends ServiceProvider
{ {
@ -15,10 +13,5 @@ class ViewComposerServiceProvider extends ServiceProvider
public function boot() public function boot()
{ {
$this->app->make('view')->composer('*', AssetComposer::class); $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);
} }
} }

View file

@ -2,9 +2,11 @@
namespace Pterodactyl\Repositories\Eloquent; namespace Pterodactyl\Repositories\Eloquent;
use Illuminate\Http\Request;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Pterodactyl\Repositories\Repository; use Pterodactyl\Repositories\Repository;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Expression; use Illuminate\Database\Query\Expression;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Contracts\Pagination\LengthAwarePaginator;
@ -15,6 +17,53 @@ use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface;
abstract class EloquentRepository extends Repository implements RepositoryInterface 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 * Return an instance of the eloquent model bound to this
* repository instance. * repository instance.
@ -236,6 +285,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
* Return all records associated with the given model. * Return all records associated with the given model.
* *
* @return \Illuminate\Support\Collection * @return \Illuminate\Support\Collection
* @deprecated Just use the model
*/ */
public function all(): Collection public function all(): Collection
{ {
@ -313,6 +363,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
* Get the amount of entries in the database. * Get the amount of entries in the database.
* *
* @return int * @return int
* @deprecated just use the count method off a model
*/ */
public function count(): int public function count(): int
{ {

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Repositories\Eloquent; namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\Builder; 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. * 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 ! $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. * Get the amount of servers that are suspended.
* *

View file

@ -37,6 +37,7 @@
"psy/psysh": "^0.10.4", "psy/psysh": "^0.10.4",
"s1lentium/iptools": "^1.1", "s1lentium/iptools": "^1.1",
"spatie/laravel-fractal": "^5.7", "spatie/laravel-fractal": "^5.7",
"spatie/laravel-query-builder": "^2.8",
"staudenmeir/belongs-to-through": "^2.10", "staudenmeir/belongs-to-through": "^2.10",
"symfony/yaml": "^4.4", "symfony/yaml": "^4.4",
"webmozart/assert": "^1.9" "webmozart/assert": "^1.9"

66
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "155b8e930e604c0476fa975b1084ca3f", "content-hash": "d05ab995e4aff4b847ff2a027924065c",
"packages": [ "packages": [
{ {
"name": "appstract/laravel-blade-directives", "name": "appstract/laravel-blade-directives",
@ -3361,6 +3361,70 @@
], ],
"time": "2020-03-02T18:40:49+00:00" "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", "name": "staudenmeir/belongs-to-through",
"version": "v2.10", "version": "v2.10",

View file

@ -6,8 +6,8 @@ export default (query?: string, includeAdmin?: boolean): Promise<PaginatedResult
http.get('/api/client', { http.get('/api/client', {
params: { params: {
include: [ 'allocation' ], include: [ 'allocation' ],
filter: includeAdmin ? 'all' : undefined, type: includeAdmin ? 'all' : undefined,
query, 'filter[name]': query,
}, },
}) })
.then(({ data }) => resolve({ .then(({ data }) => resolve({