Merge branch 'release/v0.7.2'

This commit is contained in:
Dane Everitt 2018-02-24 16:49:12 -06:00
commit c0cef1fac2
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
34 changed files with 347 additions and 68 deletions

View file

@ -3,6 +3,19 @@ This file is a running track of new features and fixes to each version of the pa
This project follows [Semantic Versioning](http://semver.org) guidelines. This project follows [Semantic Versioning](http://semver.org) guidelines.
## v0.7.2 (Derelict Dermodactylus)
### Fixed
* Fixes an exception thrown when trying to access the `/nests/:id/eggs/:id` API endpoint.
* Fixes search on server listing page.
* Schedules with no names are now clickable to allow editing.
* Fixes broken permissions check that would deny access to API keys that did in fact have permission.
### Added
* Adds ability to include egg variables on an API request.
* Added `external_id` column to servers that allows for easier linking with external services such as WHMCS.
* Added back the sidebar when viewing servers that allows for quick-switching to a different server.
* Added API endpoint to get a server by external ID.
## v0.7.1 (Derelict Dermodactylus) ## v0.7.1 (Derelict Dermodactylus)
### Fixed ### Fixed
* Fixes an exception when no token is entered on the 2-Factor enable/disable page and the form is submitted. * Fixes an exception when no token is entered on the 2-Factor enable/disable page and the form is submitted.

View file

@ -41,4 +41,4 @@ If you've found what you believe is a security issue please email us at `support
### Where to find Us ### Where to find Us
You can find us in a couple places online. First and foremost, we're active right here on Github. If you encounter a bug or other problem open an issue on here for us to take a look at it. We also accept feature requests here as well. You can find us in a couple places online. First and foremost, we're active right here on Github. If you encounter a bug or other problem open an issue on here for us to take a look at it. We also accept feature requests here as well.
You can also find us on [Discord](https://pterodactyl.io/discord). In the event that you need to get in contact with us privately feel free to contact us at `support@pterodactyl.io`. Try not to email us with requests for support regarding the panel, we'll probably just direct you to our forums or Discord. You can also find us on [Discord](https://pterodactyl.io/discord). In the event that you need to get in contact with us privately feel free to contact us at `support@pterodactyl.io`. Try not to email us with requests for support regarding the panel, we'll probably just direct you to our Discord.

View file

@ -6,7 +6,7 @@
Pterodactyl Panel is the free, open-source, game agnostic, self-hosted control panel for users, networks, and game service providers. Pterodactyl supports games and servers such as Minecraft (including Spigot, Bungeecord, and Sponge), ARK: Evolution Evolved, CS:GO, Team Fortress 2, Insurgency, Teamspeak 3, Mumble, and many more. Control all of your games from one unified interface. Pterodactyl Panel is the free, open-source, game agnostic, self-hosted control panel for users, networks, and game service providers. Pterodactyl supports games and servers such as Minecraft (including Spigot, Bungeecord, and Sponge), ARK: Evolution Evolved, CS:GO, Team Fortress 2, Insurgency, Teamspeak 3, Mumble, and many more. Control all of your games from one unified interface.
## Support & Documentation ## Support & Documentation
Support for using Pterodactyl can be found on our [Documentation Website](https://docs.pterodactyl.io), our [Discord Chat](https://discord.gg/QRDZvVm), or via our [Forums](https://forums.pterodactyl.io). Support for using Pterodactyl can be found on our [Documentation Website](https://docs.pterodactyl.io) or via our [Discord Chat](https://discord.gg/QRDZvVm).
## License ## License
``` ```

View file

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

View file

@ -201,12 +201,13 @@ class ServersController extends Controller
/** /**
* Display the index page with all servers currently on the system. * Display the index page with all servers currently on the system.
* *
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View * @return \Illuminate\View\View
*/ */
public function index() public function index(Request $request)
{ {
return view('admin.servers.index', [ return view('admin.servers.index', [
'servers' => $this->repository->getAllServers( 'servers' => $this->repository->setSearchTerm($request->input('query'))->getAllServers(
$this->config->get('pterodactyl.paginate.admin.servers') $this->config->get('pterodactyl.paginate.admin.servers')
), ),
]); ]);
@ -405,7 +406,7 @@ class ServersController extends Controller
public function setDetails(Request $request, Server $server) public function setDetails(Request $request, Server $server)
{ {
$this->detailsModificationService->handle($server, $request->only([ $this->detailsModificationService->handle($server, $request->only([
'owner_id', 'name', 'description', 'owner_id', 'external_id', 'name', 'description',
])); ]));
$this->alert->success(trans('admin/server.alerts.details_updated'))->flash(); $this->alert->success(trans('admin/server.alerts.details_updated'))->flash();

View file

@ -0,0 +1,23 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Application\Servers;
use Pterodactyl\Transformers\Api\Application\ServerTransformer;
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
use Pterodactyl\Http\Requests\Api\Application\Servers\GetExternalServerRequest;
class ExternalServerController extends ApplicationApiController
{
/**
* Retrieve a specific server from the database using its external ID.
*
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\GetExternalServerRequest $request
* @return array
*/
public function index(GetExternalServerRequest $request): array
{
return $this->fractal->item($request->getServerModel())
->transformWith($this->getTransformer(ServerTransformer::class))
->toArray();
}
}

View file

@ -9,6 +9,7 @@ use Pterodactyl\Services\Servers\ServerCreationService;
use Pterodactyl\Services\Servers\ServerDeletionService; use Pterodactyl\Services\Servers\ServerDeletionService;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Transformers\Api\Application\ServerTransformer; use Pterodactyl\Transformers\Api\Application\ServerTransformer;
use Pterodactyl\Http\Requests\Api\Application\Servers\GetServerRequest;
use Pterodactyl\Http\Requests\Api\Application\Servers\GetServersRequest; use Pterodactyl\Http\Requests\Api\Application\Servers\GetServersRequest;
use Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest; use Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest;
use Pterodactyl\Http\Requests\Api\Application\Servers\StoreServerRequest; use Pterodactyl\Http\Requests\Api\Application\Servers\StoreServerRequest;
@ -91,10 +92,10 @@ class ServerController extends ApplicationApiController
/** /**
* Show a single server transformed for the application API. * Show a single server transformed for the application API.
* *
* @param \Pterodactyl\Http\Requests\Api\Application\Servers\ServerWriteRequest $request * @param \Pterodactyl\Http\Requests\Api\Application\Servers\GetServerRequest $request
* @return array * @return array
*/ */
public function view(ServerWriteRequest $request): array public function view(GetServerRequest $request): array
{ {
return $this->fractal->item($request->getModel(Server::class)) return $this->fractal->item($request->getModel(Server::class))
->transformWith($this->getTransformer(ServerTransformer::class)) ->transformWith($this->getTransformer(ServerTransformer::class))

View file

@ -19,7 +19,7 @@ class AuthenticateUser
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {
if (is_null($request->user()) || ! $request->user()->root_admin) { if (is_null($request->user()) || ! $request->user()->root_admin) {
throw new AccessDeniedHttpException; throw new AccessDeniedHttpException('This account does not have permission to access the API.');
} }
return $next($request); return $next($request);

View file

@ -9,6 +9,7 @@ use Illuminate\Foundation\Http\FormRequest;
use Pterodactyl\Exceptions\PterodactylException; use Pterodactyl\Exceptions\PterodactylException;
use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings; use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Exception\InvalidParameterException;
abstract class ApplicationApiRequest extends FormRequest abstract class ApplicationApiRequest extends FormRequest
{ {
@ -76,22 +77,23 @@ abstract class ApplicationApiRequest extends FormRequest
} }
/** /**
* Grab a model from the route parameters. If no model exists under * Grab a model from the route parameters. If no model is found in the
* the specified key a default response is returned. * binding mappings an exception will be thrown.
* *
* @param string $model * @param string $model
* @param mixed $default
* @return mixed * @return mixed
*
* @throws \Symfony\Component\Routing\Exception\InvalidParameterException
*/ */
public function getModel(string $model, $default = null) public function getModel(string $model)
{ {
$parameterKey = array_get(array_flip(ApiSubstituteBindings::getMappings()), $model); $parameterKey = array_get(array_flip(ApiSubstituteBindings::getMappings()), $model);
if (! is_null($parameterKey)) { if (is_null($parameterKey)) {
$model = $this->route()->parameter($parameterKey); throw new InvalidParameterException;
} }
return $model ?? $default; return $this->route()->parameter($parameterKey);
} }
/* /*

View file

@ -2,6 +2,8 @@
namespace Pterodactyl\Http\Requests\Api\Application\Nests\Eggs; namespace Pterodactyl\Http\Requests\Api\Application\Nests\Eggs;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\Nest;
use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Services\Acl\Api\AdminAcl;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
@ -24,6 +26,6 @@ class GetEggRequest extends ApplicationApiRequest
*/ */
public function resourceExists(): bool public function resourceExists(): bool
{ {
return $this->getModel('nest')->id === $this->getModel('egg')->nest_id; return $this->getModel(Nest::class)->id === $this->getModel(Egg::class)->nest_id;
} }
} }

View file

@ -0,0 +1,57 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Application\Servers;
use Pterodactyl\Models\Server;
use Pterodactyl\Services\Acl\Api\AdminAcl;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
class GetExternalServerRequest extends ApplicationApiRequest
{
/**
* @var \Pterodactyl\Models\Server
*/
private $serverModel;
/**
* @var string
*/
protected $resource = AdminAcl::RESOURCE_SERVERS;
/**
* @var int
*/
protected $permission = AdminAcl::READ;
/**
* Determine if the requested external user exists.
*
* @return bool
*/
public function resourceExists(): bool
{
$repository = $this->container->make(ServerRepositoryInterface::class);
try {
$this->serverModel = $repository->findFirstWhere([
['external_id', '=', $this->route()->parameter('external_id')],
]);
} catch (RecordNotFoundException $exception) {
return false;
}
return true;
}
/**
* Return the server model for the requested external server.
*
* @return \Pterodactyl\Models\Server
*/
public function getServerModel(): Server
{
return $this->serverModel;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Application\Servers;
use Pterodactyl\Services\Acl\Api\AdminAcl;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
class GetServerRequest extends ApplicationApiRequest
{
/**
* @var string
*/
protected $resource = AdminAcl::RESOURCE_SERVERS;
/**
* @var int
*/
protected $permission = AdminAcl::READ;
}

View file

@ -2,21 +2,8 @@
namespace Pterodactyl\Http\Requests\Api\Application\Servers; namespace Pterodactyl\Http\Requests\Api\Application\Servers;
use Pterodactyl\Services\Acl\Api\AdminAcl; class GetServersRequest extends GetServerRequest
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
class GetServersRequest extends ApplicationApiRequest
{ {
/**
* @var string
*/
protected $resource = AdminAcl::RESOURCE_SERVERS;
/**
* @var int
*/
protected $permission = AdminAcl::READ;
/** /**
* @return array * @return array
*/ */

View file

@ -31,6 +31,7 @@ class StoreServerRequest extends ApplicationApiRequest
$rules = Server::getCreateRules(); $rules = Server::getCreateRules();
return [ return [
'external_id' => $rules['external_id'],
'name' => $rules['name'], 'name' => $rules['name'],
'description' => array_merge(['nullable'], $rules['description']), 'description' => array_merge(['nullable'], $rules['description']),
'user' => $rules['owner_id'], 'user' => $rules['owner_id'],

View file

@ -33,7 +33,7 @@ class ScheduleCreationFormRequest extends ServerFormRequest
public function rules() public function rules()
{ {
return [ return [
'name' => 'string|max:255', 'name' => 'nullable|string|max:255',
'cron_day_of_week' => 'required|string', 'cron_day_of_week' => 'required|string',
'cron_day_of_month' => 'required|string', 'cron_day_of_month' => 'required|string',
'cron_hour' => 'required|string', 'cron_hour' => 'required|string',

View file

@ -0,0 +1,51 @@
<?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

@ -157,6 +157,17 @@ class Node extends Model implements CleansAttributes, ValidableContract
'filesystem' => [ 'filesystem' => [
'server_logs' => '/tmp/pterodactyl', 'server_logs' => '/tmp/pterodactyl',
], ],
'internals' => [
'disk_use_seconds' => 30,
'set_permissions_on_boot' => true,
'throttle' => [
'enabled' => true,
'kill_at_count' => 5,
'decay' => 10,
'bytes' => 30720,
'check_interval_ms' => 100,
],
],
'sftp' => [ 'sftp' => [
'path' => $this->daemonBase, 'path' => $this->daemonBase,
'ip' => '0.0.0.0', 'ip' => '0.0.0.0',

View file

@ -53,6 +53,7 @@ class Server extends Model implements CleansAttributes, ValidableContract
* @var array * @var array
*/ */
protected static $applicationRules = [ protected static $applicationRules = [
'external_id' => 'sometimes',
'owner_id' => 'required', 'owner_id' => 'required',
'name' => 'required', 'name' => 'required',
'memory' => 'required', 'memory' => 'required',
@ -74,6 +75,7 @@ class Server extends Model implements CleansAttributes, ValidableContract
* @var array * @var array
*/ */
protected static $dataIntegrityRules = [ protected static $dataIntegrityRules = [
'external_id' => 'nullable|string|between:1,191|unique:servers',
'owner_id' => 'integer|exists:users,id', 'owner_id' => 'integer|exists:users,id',
'name' => 'string|min:1|max:255', 'name' => 'string|min:1|max:255',
'node_id' => 'exists:nodes,id', 'node_id' => 'exists:nodes,id',
@ -122,13 +124,14 @@ class Server extends Model implements CleansAttributes, ValidableContract
* @var array * @var array
*/ */
protected $searchableColumns = [ protected $searchableColumns = [
'name' => 50, 'name' => 100,
'uuidShort' => 10, 'uuid' => 80,
'uuid' => 10, 'uuidShort' => 80,
'pack.name' => 5, 'external_id' => 50,
'user.email' => 20, 'user.email' => 40,
'user.username' => 20, 'user.username' => 30,
'node.name' => 10, 'node.name' => 10,
'pack.name' => 10,
]; ];
/** /**

View file

@ -64,6 +64,7 @@ class User extends Model implements
* @var array * @var array
*/ */
protected $fillable = [ protected $fillable = [
'external_id',
'username', 'username',
'email', 'email',
'name_first', 'name_first',

View file

@ -1,15 +1,9 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Providers; namespace Pterodactyl\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Pterodactyl\Http\ViewComposers\ServerListComposer;
use Pterodactyl\Http\ViewComposers\Server\ServerDataComposer; use Pterodactyl\Http\ViewComposers\Server\ServerDataComposer;
class ViewComposerServiceProvider extends ServiceProvider class ViewComposerServiceProvider extends ServiceProvider
@ -20,5 +14,8 @@ class ViewComposerServiceProvider extends ServiceProvider
public function boot() public function boot()
{ {
$this->app->make('view')->composer('server.*', ServerDataComposer::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

@ -211,11 +211,12 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
* *
* @param \Pterodactyl\Models\User $user * @param \Pterodactyl\Models\User $user
* @param int $level * @param int $level
* @return \Illuminate\Pagination\LengthAwarePaginator * @param bool $paginate
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator|\Illuminate\Database\Eloquent\Collection
*/ */
public function filterUserAccessServers(User $user, int $level): LengthAwarePaginator public function filterUserAccessServers(User $user, int $level, bool $paginate = true)
{ {
$instance = $this->getBuilder()->with(['user']); $instance = $this->getBuilder()->select($this->getColumns())->with(['user']);
// If access level is set to owner, only display servers // If access level is set to owner, only display servers
// that the user owns. // that the user owns.
@ -224,8 +225,9 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
} }
// If set to all, display all servers they can access, including // 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 // those they access as an admin. If set to subuser, only return
// they are owner, or marked as a subuser of the server. // 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) { elseif (($level === User::FILTER_LEVEL_ALL && ! $user->root_admin) || $level === User::FILTER_LEVEL_SUBUSER) {
$instance->whereIn('id', $this->getUserAccessServers($user->id)); $instance->whereIn('id', $this->getUserAccessServers($user->id));
} }
@ -236,7 +238,9 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
$instance->whereNotIn('id', $this->getUserAccessServers($user->id)); $instance->whereNotIn('id', $this->getUserAccessServers($user->id));
} }
return $instance->search($this->getSearchTerm())->paginate(25); $instance->search($this->getSearchTerm());
return $paginate ? $instance->paginate(25) : $instance->get();
} }
/** /**

View file

@ -69,6 +69,7 @@ class DetailsModificationService
$this->connection->beginTransaction(); $this->connection->beginTransaction();
$response = $this->repository->setFreshModel($this->getUpdatedModel())->update($server->id, [ $response = $this->repository->setFreshModel($this->getUpdatedModel())->update($server->id, [
'external_id' => array_get($data, 'external_id'),
'owner_id' => array_get($data, 'owner_id'), 'owner_id' => array_get($data, 'owner_id'),
'name' => array_get($data, 'name'), 'name' => array_get($data, 'name'),
'description' => array_get($data, 'description') ?? '', 'description' => array_get($data, 'description') ?? '',

View file

@ -211,6 +211,7 @@ class ServerCreationService
private function createModel(array $data): Server private function createModel(array $data): Server
{ {
return $this->repository->create([ return $this->repository->create([
'external_id' => array_get($data, 'external_id'),
'uuid' => Uuid::uuid4()->toString(), 'uuid' => Uuid::uuid4()->toString(),
'uuidShort' => str_random(8), 'uuidShort' => str_random(8),
'node_id' => array_get($data, 'node_id'), 'node_id' => array_get($data, 'node_id'),

View file

@ -5,6 +5,7 @@ namespace Pterodactyl\Transformers\Api\Application;
use Pterodactyl\Models\Egg; use Pterodactyl\Models\Egg;
use Pterodactyl\Models\Nest; use Pterodactyl\Models\Nest;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Models\EggVariable;
use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Services\Acl\Api\AdminAcl;
class EggTransformer extends BaseTransformer class EggTransformer extends BaseTransformer
@ -15,7 +16,7 @@ class EggTransformer extends BaseTransformer
* @var array * @var array
*/ */
protected $availableIncludes = [ protected $availableIncludes = [
'nest', 'servers', 'config', 'script', 'nest', 'servers', 'config', 'script', 'variables',
]; ];
/** /**
@ -147,4 +148,25 @@ class EggTransformer extends BaseTransformer
]; ];
}); });
} }
/**
* Include the variables that are defined for this Egg.
*
* @param \Pterodactyl\Models\Egg $model
* @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource
*/
public function includeVariables(Egg $model)
{
if (! $this->authorize(AdminAcl::RESOURCE_EGGS)) {
return $this->null();
}
$model->loadMissing('variables');
return $this->collection(
$model->getRelation('variables'),
$this->makeTransformer(EggVariableTransformer::class),
EggVariable::RESOURCE_NAME
);
}
} }

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Transformers\Api\Application; namespace Pterodactyl\Transformers\Api\Application;
use Cake\Chronos\Chronos;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Services\Acl\Api\AdminAcl; use Pterodactyl\Services\Acl\Api\AdminAcl;
use Pterodactyl\Services\Servers\EnvironmentService; use Pterodactyl\Services\Servers\EnvironmentService;
@ -63,6 +62,7 @@ class ServerTransformer extends BaseTransformer
{ {
return [ return [
'id' => $server->getKey(), 'id' => $server->getKey(),
'external_id' => $server->external_id,
'uuid' => $server->uuid, 'uuid' => $server->uuid,
'identifier' => $server->uuidShort, 'identifier' => $server->uuidShort,
'name' => $server->name, 'name' => $server->name,
@ -87,8 +87,8 @@ class ServerTransformer extends BaseTransformer
'installed' => (int) $server->installed === 1, 'installed' => (int) $server->installed === 1,
'environment' => $this->environmentService->handle($server), 'environment' => $this->environmentService->handle($server),
], ],
'created_at' => Chronos::createFromFormat(Chronos::DEFAULT_TO_STRING_FORMAT, $server->created_at)->setTimezone('UTC')->toIso8601String(), $server->getUpdatedAtColumn() => $this->formatTimestamp($server->updated_at),
'updated_at' => Chronos::createFromFormat(Chronos::DEFAULT_TO_STRING_FORMAT, $server->updated_at)->setTimezone('UTC')->toIso8601String(), $server->getCreatedAtColumn() => $this->formatTimestamp($server->created_at),
]; ];
} }

View file

@ -9,7 +9,7 @@ return [
| change this value if you are not maintaining your own internal versions. | change this value if you are not maintaining your own internal versions.
*/ */
'version' => '0.7.1', 'version' => '0.7.2',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddExternalIdColumnToServersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('servers', function (Blueprint $table) {
$table->string('external_id')->after('id')->nullable()->unique();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('external_id');
});
}
}

View file

@ -47,12 +47,17 @@
<form action="{{ route('admin.servers.view.details', $server->id) }}" method="POST"> <form action="{{ route('admin.servers.view.details', $server->id) }}" method="POST">
<div class="box-body"> <div class="box-body">
<div class="form-group"> <div class="form-group">
<label for="name" class="control-label">Server Name</label> <label for="name" class="control-label">Server Name <span class="field-required"></span></label>
<input type="text" name="name" value="{{ old('name', $server->name) }}" class="form-control" /> <input type="text" name="name" value="{{ old('name', $server->name) }}" class="form-control" />
<p class="text-muted small">Character limits: <code>a-zA-Z0-9_-</code> and <code>[Space]</code> (max 35 characters).</p> <p class="text-muted small">Character limits: <code>a-zA-Z0-9_-</code> and <code>[Space]</code> (max 35 characters).</p>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="pUserId" class="control-label">Server Owner</label> <label for="external_id" class="control-label">External Identifier</label>
<input type="text" name="external_id" value="{{ old('external_id', $server->external_id) }}" class="form-control" />
<p class="text-muted small">Leave empty to not assign an external identifier for this server. The external ID should be unique to this server and not be in use by any other servers.</p>
</div>
<div class="form-group">
<label for="pUserId" class="control-label">Server Owner <span class="field-required"></span></label>
<select name="owner_id" class="form-control" id="pUserId"> <select name="owner_id" class="form-control" id="pUserId">
<option value="{{ $server->owner_id }}" selected>{{ $server->user->email }}</option> <option value="{{ $server->owner_id }}" selected>{{ $server->user->email }}</option>
</select> </select>

View file

@ -47,6 +47,18 @@
</div> </div>
<div class="box-body table-responsive no-padding"> <div class="box-body table-responsive no-padding">
<table class="table table-hover"> <table class="table table-hover">
<tr>
<td>Internal Identifier</td>
<td><code>{{ $server->id }}</code></td>
</tr>
<tr>
<td>External Identifier</td>
@if(is_null($server->external_id))
<td><span class="label label-default">Not Set</span></td>
@else
<td><code>{{ $server->external_id }}</code></td>
@endif
</tr>
<tr> <tr>
<td>UUID / Docker Container ID</td> <td>UUID / Docker Container ID</td>
<td><code>{{ $server->uuid }}</code></td> <td><code>{{ $server->uuid }}</code></td>
@ -127,7 +139,7 @@
<div class="col-sm-12"> <div class="col-sm-12">
<div class="small-box bg-gray"> <div class="small-box bg-gray">
<div class="inner"> <div class="inner">
<h3>{{ str_limit($server->user->username, 8) }}</h3> <h3>{{ str_limit($server->user->username, 16) }}</h3>
<p>Server Owner</p> <p>Server Owner</p>
</div> </div>
<div class="icon"><i class="fa fa-user"></i></div> <div class="icon"><i class="fa fa-user"></i></div>
@ -139,7 +151,7 @@
<div class="col-sm-12"> <div class="col-sm-12">
<div class="small-box bg-gray"> <div class="small-box bg-gray">
<div class="inner"> <div class="inner">
<h3>{{ str_limit($server->node->name, 8) }}</h3> <h3>{{ str_limit($server->node->name, 16) }}</h3>
<p>Server Node</p> <p>Server Node</p>
</div> </div>
<div class="icon"><i class="fa fa-codepen"></i></div> <div class="icon"><i class="fa fa-codepen"></i></div>

View file

@ -60,9 +60,13 @@
<span class="hidden-xs">{{ Auth::user()->name_first }} {{ Auth::user()->name_last }}</span> <span class="hidden-xs">{{ Auth::user()->name_first }} {{ Auth::user()->name_last }}</span>
</a> </a>
</li> </li>
{{--<li>--}} @if(isset($sidebarServerList))
{{--<a href="#" data-action="control-sidebar" data-toggle="tooltip" data-placement="bottom" title="@lang('strings.servers')"><i class="fa fa-server"></i></a>--}} <li>
{{--</li>--}} <a href="#" data-toggle="control-sidebar">
<i class="fa fa-server"></i>
</a>
</li>
@endif
@if(Auth::user()->root_admin) @if(Auth::user()->root_admin)
<li> <li>
<li><a href="{{ route('admin.index') }}" data-toggle="tooltip" data-placement="bottom" title="@lang('strings.admin_cp')"><i class="fa fa-gears"></i></a></li> <li><a href="{{ route('admin.index') }}" data-toggle="tooltip" data-placement="bottom" title="@lang('strings.admin_cp')"><i class="fa fa-gears"></i></a></li>
@ -240,6 +244,29 @@
</div> </div>
Copyright &copy; 2015 - {{ date('Y') }} <a href="https://pterodactyl.io/">Pterodactyl Software</a>. Copyright &copy; 2015 - {{ date('Y') }} <a href="https://pterodactyl.io/">Pterodactyl Software</a>.
</footer> </footer>
@if(isset($sidebarServerList))
<aside class="control-sidebar control-sidebar-dark">
<div class="tab-content">
<ul class="control-sidebar-menu">
@foreach($sidebarServerList as $sidebarServer)
<li>
<a href="{{ route('server.index', $sidebarServer->uuidShort) }}" @if(isset($server) && $sidebarServer->id === $server->id)class="active"@endif>
@if($sidebarServer->owner_id === Auth::user()->id)
<i class="menu-icon fa fa-user bg-blue"></i>
@else
<i class="menu-icon fa fa-user-o bg-gray"></i>
@endif
<div class="menu-info">
<h4 class="control-sidebar-subheading">{{ str_limit($sidebarServer->name, 20) }}</h4>
<p>{{ str_limit($sidebarServer->description, 20) }}</p>
</div>
</a>
</li>
@endforeach
</ul>
</div>
</aside>
@endif
<div class="control-sidebar-bg"></div> <div class="control-sidebar-bg"></div>
</div> </div>
@section('footer-scripts') @section('footer-scripts')

View file

@ -43,7 +43,9 @@
<tr @if(! $schedule->is_active)class="muted muted-hover"@endif> <tr @if(! $schedule->is_active)class="muted muted-hover"@endif>
<td class="middle"> <td class="middle">
@can('edit-schedule', $server) @can('edit-schedule', $server)
<a href="{{ route('server.schedules.view', ['server' => $server->uuidShort, '$schedule' => $schedule->hashid]) }}">{{ $schedule->name }}</a> <a href="{{ route('server.schedules.view', ['server' => $server->uuidShort, '$schedule' => $schedule->hashid]) }}">
{{ $schedule->name ?? trans('server.schedule.unnamed') }}
</a>
@else @else
{{ $schedule->name ?? trans('server.schedule.unnamed') }} {{ $schedule->name ?? trans('server.schedule.unnamed') }}
@endcan @endcan

View file

@ -74,6 +74,7 @@ Route::group(['prefix' => '/locations'], function () {
Route::group(['prefix' => '/servers'], function () { Route::group(['prefix' => '/servers'], function () {
Route::get('/', 'Servers\ServerController@index')->name('api.application.servers'); Route::get('/', 'Servers\ServerController@index')->name('api.application.servers');
Route::get('/{server}', 'Servers\ServerController@view')->name('api.application.servers.view'); Route::get('/{server}', 'Servers\ServerController@view')->name('api.application.servers.view');
Route::get('/external/{external_id}', 'Servers\ExternalServerController@index')->name('api.application.servers.external');
Route::patch('/{server}/details', 'Servers\ServerDetailsController@details')->name('api.application.servers.details'); Route::patch('/{server}/details', 'Servers\ServerDetailsController@details')->name('api.application.servers.details');
Route::patch('/{server}/build', 'Servers\ServerDetailsController@build')->name('api.application.servers.build'); Route::patch('/{server}/build', 'Servers\ServerDetailsController@build')->name('api.application.servers.build');

View file

@ -58,6 +58,7 @@ class DetailsModificationServiceTest extends TestCase
$this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull(); $this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull();
$this->repository->shouldReceive('setFreshModel')->once()->with(false)->andReturnSelf(); $this->repository->shouldReceive('setFreshModel')->once()->with(false)->andReturnSelf();
$this->repository->shouldReceive('update')->once()->with($server->id, [ $this->repository->shouldReceive('update')->once()->with($server->id, [
'external_id' => null,
'owner_id' => $data['owner_id'], 'owner_id' => $data['owner_id'],
'name' => $data['name'], 'name' => $data['name'],
'description' => $data['description'], 'description' => $data['description'],
@ -95,11 +96,12 @@ class DetailsModificationServiceTest extends TestCase
'owner_id' => 1, 'owner_id' => 1,
]); ]);
$data = ['owner_id' => 2, 'name' => 'New Name', 'description' => 'New Description']; $data = ['owner_id' => 2, 'name' => 'New Name', 'description' => 'New Description', 'external_id' => 'abcd1234'];
$this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull(); $this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull();
$this->repository->shouldReceive('setFreshModel')->once()->with(false)->andReturnSelf(); $this->repository->shouldReceive('setFreshModel')->once()->with(false)->andReturnSelf();
$this->repository->shouldReceive('update')->once()->with($server->id, [ $this->repository->shouldReceive('update')->once()->with($server->id, [
'external_id' => 'abcd1234',
'owner_id' => $data['owner_id'], 'owner_id' => $data['owner_id'],
'name' => $data['name'], 'name' => $data['name'],
'description' => $data['description'], 'description' => $data['description'],