Merge pull request #3 from pterodactyl/develop

Pull from upstream
This commit is contained in:
Caleb 2020-09-16 11:23:13 -04:00 committed by GitHub
commit edeaa69dff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
168 changed files with 1269 additions and 4343 deletions

View File

@ -1,53 +1,44 @@
[![Logo Image](https://cdn.pterodactyl.io/logos/new/pterodactyl_logo.png)](https://pterodactyl.io)
[![Build status](https://img.shields.io/travis/pterodactyl/panel/develop.svg?style=flat-square)](https://travis-ci.org/pterodactyl/panel)
[![StyleCI](https://styleci.io/repos/47508644/shield?branch=develop)](https://styleci.io/repos/47508644)
[![Codecov](https://img.shields.io/codecov/c/github/pterodactyl/panel/develop.svg?style=flat-square)](https://codecov.io/gh/Pterodactyl/Panel)
[![Discord](https://img.shields.io/discord/122900397965705216.svg?style=flat-square&label=Discord)](https://pterodactyl.io/discord)
# Pterodactyl Panel
Pterodactyl is an open-source game server management panel built with PHP 7, React, and Go. Designed with security
in mind, Pterodactyl runs all game servers in isolated Docker container while exposing a beautiful and intuitive
UI to end users.
Pterodactyl is the open-source game server management panel built with PHP7, Nodejs, and Go. Designed with security in mind, Pterodactyl runs all game servers in isolated Docker containers while exposing a beautiful and intuitive UI to administrators and users.
What more are you waiting for? Make game servers a first class citizen on your platform today.
Stop settling for less. Make game servers a first class citizen on your platform.
![Image](https://cdn.pterodactyl.io/site-assets/mockup-macbook-grey.png)
![Image](https://cdn.pterodactyl.io/site-assets/pterodactyl_v1_demo.gif)
## Sponsors
I would like to extend my sincere thanks to the following sponsors for funding Pterodactyl's developement. [Interested
in becoming a sponsor?](https://github.com/sponsors/DaneEveritt)
I would like to extend my sincere thanks to the following sponsors for helping find Pterodactyl's developement.
[Interested in becoming a sponsor?](https://github.com/sponsors/DaneEveritt)
#### [BloomVPS](https://bloomvps.com)
> BloomVPS offers dedicated core VPS and Minecraft hosting with Ryzen 9 processors. With owned-hardware, we offer truly
> unbeatable prices on high-performance hosting.
| Company | About |
| ------- | ----- |
| [**Bloom.host**](https://bloom.host) | Bloom.host offers dedicated core VPS and Minecraft hosting with Ryzen 9 processors. With owned-hardware, we offer truly unbeatable prices on high-performance hosting. |
| [**VersatileNode**](https://versatilenode.com/) | Looking to host a minecraft server, vps, or a website? VersatileNode is one of the most affordable hosting providers to provide quality yet cheap services with incredible support. |
| [**MineStrator**](https://minestrator.com/) | Looking for a French highend hosting company for you minecraft server? More than 14,000 members on our discord, trust us. |
| [**DedicatedMC**](https://dedicatedmc.io/) | DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance and giving you the best performance money can buy. |
| [**Skynode**](https://www.skynode.pro/) | Skynode provides blazing fast game servers along with a top-notch user experience. Whatever our clients are looking for, we're able to provide it! |
| [**XCORE-SERVER.de**](https://xcore-server.de/) | XCORE-SERVER.de offers High-End Servers for hosting and gaming since 2012. Fast, excellent and well-known for eSports Gaming. |
#### [VersatileNode](https://versatilenode.com/)
> Looking to host a minecraft server, vps, or a website? VersatileNode is one of the most affordable hosting providers
> to provide quality yet cheap services with incredible support.
#### [MineStrator](https://minestrator.com/)
> Looking for a French highend hosting company for you minecraft server? More than 14,000 members on our discord
> trust us.
#### [DedicatedMC](https://dedicatedmc.io/)
> DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance
> and giving you the best performance money can buy.
#### [Skynode](https://www.skynode.pro/)
> Skynode provides blazing fast game servers along with a top notch user experience. Whatever our clients are looking
> for, we're able to provide it!
#### [XCORE-SERVER.de](https://xcore-server.de)
> XCORE-SERVER.de offers High-End Servers for hosting and gaming since 2012. Fast, excellent and well known for eSports Gaming.
## Support & Documentation
Support for using Pterodactyl can be found on our [Documentation Website](https://pterodactyl.io/project/introduction.html), [Guides Website](https://pterodactyl.io/community/about.html), or via our [Discord Chat](https://discord.gg/QRDZvVm).
## Documentation
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
* [Wings Documentation](https://pterodactyl.io/wings/1.0/installing.html)
* [Community Guides](https://pterodactyl.io/community/about.html)
* Or, get additional help [via Discord](https://discord.gg/pterodactyl)
### Supported Games
We support a huge variety of games by utilizing Docker containers to isolate each instance, giving you the power to host your games across the world without having to bloat each physical machine with additional dependencies.
We support a huge variety of games by utilizing Docker containers to isolate each instance, giving you the power to
host your games across the world without having to bloat each physical machine with additional dependencies.
Some of our core supported games include:
* Minecraft — including Spigot, Sponge, Bungeecord, Waterfall, and more
* Minecraft — including Paper, Sponge, Bungeecord, Waterfall, and more
* Rust
* Terraria
* Teamspeak
@ -57,7 +48,8 @@ Some of our core supported games include:
* Garry's Mod
* ARK: Survival Evolved
In addition to our standard nest of supported games, our community is constantly pushing the limits of this software and there are plenty more games available provided by the community. Some of these games include:
In addition to our standard nest of supported games, our community is constantly pushing the limits of this software
and there are plenty more games available provided by the community. Some of these games include:
* Factorio
* San Andreas: MP
@ -65,22 +57,13 @@ In addition to our standard nest of supported games, our community is constantly
* Squad
* FiveM
* Xonotic
* Discord ATLBot
## Credits
This software would not be possible without the work of other open-source authors who provide tools such as:
[Ace Editor](https://ace.c9.io), [AdminLTE](https://adminlte.io), [Animate.css](http://daneden.github.io/animate.css/), [AnsiUp](https://github.com/drudru/ansi_up), [Async.js](https://github.com/caolan/async),
[Bootstrap](http://getbootstrap.com), [Bootstrap Notify](http://bootstrap-notify.remabledesigns.com), [Chart.js](http://www.chartjs.org), [FontAwesome](http://fontawesome.io),
[FontAwesome Animations](https://github.com/l-lin/font-awesome-animation), [jQuery](http://jquery.com), [Laravel](https://laravel.com), [Lodash](https://lodash.com),
[Select2](https://select2.github.io), [Socket.io](http://socket.io), [Socket.io File Upload](https://github.com/vote539/socketio-file-upload), [SweetAlert](http://t4t5.github.io/sweetalert),
[Typeahead](https://github.com/bassjobsen/Bootstrap-3-Typeahead), and [Particles.js](http://vincentgarreau.com/particles.js).
Some Javascript and CSS used within the panel is licensed under a `MIT` or `Apache 2.0` license. Please check their respective header files for more information.
* Starmade
* Discord ATLBot, and most other Node.js/Python discord bots
* [and many more...](https://github.com/parkervcp/eggs)
## License
```
Copyright (c) 2015 - 2018 Dane Everitt <dane@daneeveritt.com>.
Copyright (c) 2015 - 2020 Dane Everitt <dane@daneeveritt.com> & Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -100,3 +83,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
Some Javascript and CSS used within the panel are licensed under a `MIT` or `Apache 2.0` license. Please check their
respective header files for more information.

View File

@ -10,6 +10,7 @@
namespace Pterodactyl\Console\Commands\User;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\User;
use Illuminate\Console\Command;
use Pterodactyl\Services\Users\UserDeletionService;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
@ -40,16 +41,11 @@ class DeleteUserCommand extends Command
* DeleteUserCommand constructor.
*
* @param \Pterodactyl\Services\Users\UserDeletionService $deletionService
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository
*/
public function __construct(
UserDeletionService $deletionService,
UserRepositoryInterface $repository
) {
public function __construct(UserDeletionService $deletionService) {
parent::__construct();
$this->deletionService = $deletionService;
$this->repository = $repository;
}
/**
@ -59,9 +55,13 @@ class DeleteUserCommand extends Command
public function handle()
{
$search = $this->option('user') ?? $this->ask(trans('command/messages.user.search_users'));
Assert::notEmpty($search, 'Search term must be a non-null value, received %s.');
Assert::notEmpty($search, 'Search term should be an email address, got: %s.');
$results = User::query()
->where('email', 'LIKE', "$search%")
->where('username', 'LIKE', "$search%")
->get();
$results = $this->repository->setSearchTerm($search)->all();
if (count($results) < 1) {
$this->error(trans('command/messages.user.no_users_found'));
if ($this->input->isInteractive()) {
@ -95,5 +95,7 @@ class DeleteUserCommand extends Command
$this->deletionService->handle($deleteUser);
$this->info(trans('command/messages.user.deleted'));
}
return;
}
}

View File

@ -6,14 +6,6 @@ use Illuminate\Support\Collection;
interface AllocationRepositoryInterface extends RepositoryInterface
{
/**
* Return all of the unique IPs that exist for a given node.
*
* @param int $node
* @return \Illuminate\Support\Collection
*/
public function getUniqueAllocationIpsForNode(int $node): Collection;
/**
* Return all of the allocations that exist for a node that are not currently
* allocated.
@ -23,27 +15,6 @@ interface AllocationRepositoryInterface extends RepositoryInterface
*/
public function getUnassignedAllocationIds(int $node): array;
/**
* Get an array of all allocations that are currently assigned to a given server.
*
* @param int $server
* @return array
*/
public function getAssignedAllocationIds(int $server): array;
/**
* Return a concatenated result set of node ips that already have at least one
* server assigned to that IP. This allows for filtering out sets for
* dedicated allocation IPs.
*
* If an array of nodes is passed the results will be limited to allocations
* in those nodes.
*
* @param array $nodes
* @return array
*/
public function getDiscardableDedicatedAllocations(array $nodes = []): array;
/**
* Return a single allocation from those meeting the requirements.
*

View File

@ -1,38 +0,0 @@
<?php
namespace Pterodactyl\Contracts\Repository\Attributes;
interface SearchableInterface
{
/**
* Set the search term.
*
* @param string|null $term
* @return $this
* @deprecated
*/
public function search($term);
/**
* Set the search term to use when requesting all records from
* the model.
*
* @param string|null $term
* @return $this
*/
public function setSearchTerm(string $term = null);
/**
* Determine if a valid search term is set on this repository.
*
* @return bool
*/
public function hasSearchTerm(): bool;
/**
* Return the search term.
*
* @return string|null
*/
public function getSearchTerm();
}

View File

@ -1,52 +0,0 @@
<?php
namespace Pterodactyl\Contracts\Repository;
use Pterodactyl\Models\User;
use Pterodactyl\Models\DaemonKey;
use Illuminate\Support\Collection;
interface DaemonKeyRepositoryInterface extends RepositoryInterface
{
/**
* String prepended to keys to identify that they are managed internally and not part of the user API.
*/
const INTERNAL_KEY_IDENTIFIER = 'i_';
/**
* Load the server and user relations onto a key model.
*
* @param \Pterodactyl\Models\DaemonKey $key
* @param bool $refresh
* @return \Pterodactyl\Models\DaemonKey
*/
public function loadServerAndUserRelations(DaemonKey $key, bool $refresh = false): DaemonKey;
/**
* Return a daemon key with the associated server relation attached.
*
* @param string $key
* @return \Pterodactyl\Models\DaemonKey
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getKeyWithServer(string $key): DaemonKey;
/**
* Get all of the keys for a specific user including the information needed
* from their server relation for revocation on the daemon.
*
* @param \Pterodactyl\Models\User $user
* @return \Illuminate\Support\Collection
*/
public function getKeysForRevocation(User $user): Collection;
/**
* Delete an array of daemon keys from the database. Used primarily in
* conjunction with getKeysForRevocation.
*
* @param array $ids
* @return bool|int
*/
public function deleteKeys(array $ids);
}

View File

@ -4,9 +4,8 @@ namespace Pterodactyl\Contracts\Repository;
use Pterodactyl\Models\Location;
use Illuminate\Support\Collection;
use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface;
interface LocationRepositoryInterface extends RepositoryInterface, SearchableInterface
interface LocationRepositoryInterface extends RepositoryInterface
{
/**
* Return locations with a count of nodes and servers attached to it.

View File

@ -14,7 +14,7 @@ use Pterodactyl\Models\Nest;
interface NestRepositoryInterface extends RepositoryInterface
{
/**
* Return a nest or all nests with their associated eggs, variables, and packs.
* Return a nest or all nests with their associated eggs and variables.
*
* @param int $id
* @return \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Nest
@ -24,7 +24,7 @@ interface NestRepositoryInterface extends RepositoryInterface
public function getWithEggs(int $id = null);
/**
* Return a nest or all nests and the count of eggs, packs, and servers for that nest.
* Return a nest or all nests and the count of eggs and servers for that nest.
*
* @param int|null $id
* @return \Pterodactyl\Models\Nest|\Illuminate\Database\Eloquent\Collection

View File

@ -8,7 +8,7 @@ use Illuminate\Support\LazyCollection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface;
interface NodeRepositoryInterface extends RepositoryInterface, SearchableInterface
interface NodeRepositoryInterface extends RepositoryInterface
{
const THRESHOLD_PERCENTAGE_LOW = 75;
const THRESHOLD_PERCENTAGE_MEDIUM = 90;
@ -29,13 +29,6 @@ interface NodeRepositoryInterface extends RepositoryInterface, SearchableInterfa
*/
public function getUsageStatsRaw(Node $node): array;
/**
* Return all available nodes with a searchable interface.
*
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function getNodeListingData(): LengthAwarePaginator;
/**
* Return a single node with location and server information.
*

View File

@ -1,26 +0,0 @@
<?php
namespace Pterodactyl\Contracts\Repository;
use Pterodactyl\Models\Pack;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface;
interface PackRepositoryInterface extends RepositoryInterface, SearchableInterface
{
/**
* Return a pack with the associated server models attached to it.
*
* @param \Pterodactyl\Models\Pack $pack
* @param bool $refresh
* @return \Pterodactyl\Models\Pack
*/
public function loadServerData(Pack $pack, bool $refresh = false): Pack;
/**
* Return a paginated listing of packs with their associated egg and server count.
*
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function paginateWithEggAndServerCount(): LengthAwarePaginator;
}

View File

@ -5,18 +5,9 @@ namespace Pterodactyl\Contracts\Repository;
use Pterodactyl\Models\Server;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface;
interface ServerRepositoryInterface extends RepositoryInterface, SearchableInterface
interface ServerRepositoryInterface extends RepositoryInterface
{
/**
* Returns a listing of all servers that exist including relationships.
*
* @param int $paginate
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function getAllServers(int $paginate): LengthAwarePaginator;
/**
* Load the egg relations onto the server model.
*
@ -85,7 +76,7 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter
/**
* 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
* the egg which is used for build and rebuild. Only loads relations
* if they are missing, or refresh is set to true.
*
* @param \Pterodactyl\Models\Server $server

View File

@ -2,32 +2,6 @@
namespace Pterodactyl\Contracts\Repository;
use Illuminate\Support\Collection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface;
interface UserRepositoryInterface extends RepositoryInterface, SearchableInterface
interface UserRepositoryInterface extends RepositoryInterface
{
/**
* Return all users with counts of servers and subusers of servers.
*
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function getAllUsersWithCounts(): LengthAwarePaginator;
/**
* Return all matching models for a user in a format that can be used for dropdowns.
*
* @param string|null $query
* @return \Illuminate\Support\Collection
*/
public function filterUsersByQuery(?string $query): Collection;
/**
* Returns a user with the given id in a format that can be used for dropdowns.
*
* @param int $id
* @return \Pterodactyl\Models\Model
*/
public function filterById(int $id): \Pterodactyl\Models\Model;
}

View File

@ -1,16 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Exceptions\Service\Pack;
use Pterodactyl\Exceptions\DisplayException;
class InvalidFileMimeTypeException extends DisplayException
{
}

View File

@ -1,16 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Exceptions\Service\Pack;
use Pterodactyl\Exceptions\DisplayException;
class InvalidPackArchiveFormatException extends DisplayException
{
}

View File

@ -1,16 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Exceptions\Service\Pack;
use Pterodactyl\Exceptions\DisplayException;
class UnreadableZipArchiveException extends DisplayException
{
}

View File

@ -1,16 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Exceptions\Service\Pack;
use Exception;
class ZipArchiveCreationException extends Exception
{
}

View File

@ -1,16 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Exceptions\Service\Pack;
use Pterodactyl\Exceptions\DisplayException;
class ZipExtractionException extends DisplayException
{
}

View File

@ -3,6 +3,8 @@
namespace Pterodactyl\Http\Controllers\Admin\Nodes;
use Illuminate\Http\Request;
use Pterodactyl\Models\Node;
use Spatie\QueryBuilder\QueryBuilder;
use Illuminate\Contracts\View\Factory;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
@ -39,10 +41,13 @@ class NodeController extends Controller
*/
public function index(Request $request)
{
$nodes = $this->repository
->setSearchTerm($request->input('query'))
->getNodeListingData();
$nodes = QueryBuilder::for(
Node::query()->with('location')->withCount('servers')
)
->allowedFilters(['uuid', 'name'])
->allowedSorts(['id'])
->paginate(25);
return $this->view->make('admin.nodes.index', compact('nodes'));
return $this->view->make('admin.nodes.index', ['nodes' => $nodes]);
}
}

View File

@ -5,6 +5,7 @@ namespace Pterodactyl\Http\Controllers\Admin\Nodes;
use Illuminate\Http\Request;
use Pterodactyl\Models\Node;
use Illuminate\Support\Collection;
use Pterodactyl\Models\Allocation;
use Illuminate\Contracts\View\Factory;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
@ -134,7 +135,10 @@ class NodeViewController extends Controller
return $this->view->make('admin.nodes.view.allocation', [
'node' => $node,
'allocations' => $this->allocationRepository->setColumns(['ip'])->getUniqueAllocationIpsForNode($node->id),
'allocations' => Allocation::query()->where('node_id', $node->id)
->groupBy('ip')
->orderByRaw('INET_ATON(ip) ASC')
->get(['ip']),
]);
}

View File

@ -1,250 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Http\Controllers\Admin;
use Illuminate\Http\Request;
use Pterodactyl\Models\Pack;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Packs\ExportPackService;
use Pterodactyl\Services\Packs\PackUpdateService;
use Pterodactyl\Services\Packs\PackCreationService;
use Pterodactyl\Services\Packs\PackDeletionService;
use Pterodactyl\Http\Requests\Admin\PackFormRequest;
use Pterodactyl\Services\Packs\TemplateUploadService;
use Pterodactyl\Contracts\Repository\NestRepositoryInterface;
use Pterodactyl\Contracts\Repository\PackRepositoryInterface;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
class PackController extends Controller
{
/**
* @var \Prologue\Alerts\AlertsMessageBag
*/
protected $alert;
/**
* @var \Illuminate\Contracts\Config\Repository
*/
protected $config;
/**
* @var \Pterodactyl\Services\Packs\PackCreationService
*/
protected $creationService;
/**
* @var \Pterodactyl\Services\Packs\PackDeletionService
*/
protected $deletionService;
/**
* @var \Pterodactyl\Services\Packs\ExportPackService
*/
protected $exportService;
/**
* @var \Pterodactyl\Contracts\Repository\PackRepositoryInterface
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Packs\PackUpdateService
*/
protected $updateService;
/**
* @var \Pterodactyl\Contracts\Repository\NestRepositoryInterface
*/
protected $serviceRepository;
/**
* @var \Pterodactyl\Services\Packs\TemplateUploadService
*/
protected $templateUploadService;
/**
* PackController constructor.
*
* @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Illuminate\Contracts\Config\Repository $config
* @param \Pterodactyl\Services\Packs\ExportPackService $exportService
* @param \Pterodactyl\Services\Packs\PackCreationService $creationService
* @param \Pterodactyl\Services\Packs\PackDeletionService $deletionService
* @param \Pterodactyl\Contracts\Repository\PackRepositoryInterface $repository
* @param \Pterodactyl\Services\Packs\PackUpdateService $updateService
* @param \Pterodactyl\Contracts\Repository\NestRepositoryInterface $serviceRepository
* @param \Pterodactyl\Services\Packs\TemplateUploadService $templateUploadService
*/
public function __construct(
AlertsMessageBag $alert,
ConfigRepository $config,
ExportPackService $exportService,
PackCreationService $creationService,
PackDeletionService $deletionService,
PackRepositoryInterface $repository,
PackUpdateService $updateService,
NestRepositoryInterface $serviceRepository,
TemplateUploadService $templateUploadService
) {
$this->alert = $alert;
$this->config = $config;
$this->creationService = $creationService;
$this->deletionService = $deletionService;
$this->exportService = $exportService;
$this->repository = $repository;
$this->updateService = $updateService;
$this->serviceRepository = $serviceRepository;
$this->templateUploadService = $templateUploadService;
}
/**
* Display listing of all packs on the system.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*/
public function index(Request $request)
{
return view('admin.packs.index', [
'packs' => $this->repository->setSearchTerm($request->input('query'))->paginateWithEggAndServerCount(),
]);
}
/**
* Display new pack creation form.
*
* @return \Illuminate\View\View
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function create()
{
return view('admin.packs.new', [
'nests' => $this->serviceRepository->getWithEggs(),
]);
}
/**
* Display new pack creation modal for use with template upload.
*
* @return \Illuminate\View\View
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function newTemplate()
{
return view('admin.packs.modal', [
'nests' => $this->serviceRepository->getWithEggs(),
]);
}
/**
* Handle create pack request and route user to location.
*
* @param \Pterodactyl\Http\Requests\Admin\PackFormRequest $request
* @return \Illuminate\View\View
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
* @throws \Pterodactyl\Exceptions\Service\Pack\InvalidPackArchiveFormatException
* @throws \Pterodactyl\Exceptions\Service\Pack\UnreadableZipArchiveException
* @throws \Pterodactyl\Exceptions\Service\Pack\ZipExtractionException
*/
public function store(PackFormRequest $request)
{
if ($request->filled('from_template')) {
$pack = $this->templateUploadService->handle($request->input('egg_id'), $request->file('file_upload'));
} else {
$pack = $this->creationService->handle($request->normalize(), $request->file('file_upload'));
}
$this->alert->success(trans('admin/pack.notices.pack_created'))->flash();
return redirect()->route('admin.packs.view', $pack->id);
}
/**
* Display pack view template to user.
*
* @param \Pterodactyl\Models\Pack $pack
* @return \Illuminate\View\View
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function view(Pack $pack)
{
return view('admin.packs.view', [
'pack' => $this->repository->loadServerData($pack),
'nests' => $this->serviceRepository->getWithEggs(),
]);
}
/**
* Handle updating or deleting pack information.
*
* @param \Pterodactyl\Http\Requests\Admin\PackFormRequest $request
* @param \Pterodactyl\Models\Pack $pack
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\HasActiveServersException
*/
public function update(PackFormRequest $request, Pack $pack)
{
$this->updateService->handle($pack, $request->normalize());
$this->alert->success(trans('admin/pack.notices.pack_updated'))->flash();
return redirect()->route('admin.packs.view', $pack->id);
}
/**
* Delete a pack if no servers are attached to it currently.
*
* @param \Pterodactyl\Models\Pack $pack
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\HasActiveServersException
*/
public function destroy(Pack $pack)
{
$this->deletionService->handle($pack->id);
$this->alert->success(trans('admin/pack.notices.pack_deleted', [
'name' => $pack->name,
]))->flash();
return redirect()->route('admin.packs');
}
/**
* Creates an archive of the pack and downloads it to the browser.
*
* @param \Pterodactyl\Models\Pack $pack
* @param bool|string $files
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Pack\ZipArchiveCreationException
*/
public function export(Pack $pack, $files = false)
{
$filename = $this->exportService->handle($pack, is_string($files));
if (is_string($files)) {
return response()->download($filename, 'pack-' . $pack->name . '.zip')->deleteFileAfterSend(true);
}
return response()->download($filename, 'pack-' . $pack->name . '.json', [
'Content-Type' => 'application/json',
])->deleteFileAfterSend(true);
}
}

View File

@ -3,6 +3,8 @@
namespace Pterodactyl\Http\Controllers\Admin\Servers;
use Illuminate\Http\Request;
use Pterodactyl\Models\Server;
use Spatie\QueryBuilder\QueryBuilder;
use Illuminate\Contracts\View\Factory;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
@ -42,10 +44,11 @@ class ServerController extends Controller
*/
public function index(Request $request)
{
return $this->view->make('admin.servers.index', [
'servers' => $this->repository->setSearchTerm($request->input('query'))->getAllServers(
config()->get('pterodactyl.paginate.admin.servers')
),
]);
$servers = QueryBuilder::for(Server::query()->with('node', 'user', 'allocation'))
->allowedFilters(['uuid', 'name', 'image'])
->allowedSorts(['id', 'uuid'])
->paginate(config()->get('pterodactyl.paginate.admin.servers'));
return $this->view->make('admin.servers.index', ['servers' => $servers]);
}
}

View File

@ -240,7 +240,7 @@ class ServersController extends Controller
}
/**
* Reinstalls the server with the currently assigned pack and service.
* Reinstalls the server with the currently assigned service.
*
* @param \Pterodactyl\Models\Server $server
* @return \Illuminate\Http\RedirectResponse

View File

@ -5,6 +5,7 @@ namespace Pterodactyl\Http\Controllers\Admin;
use Illuminate\Http\Request;
use Pterodactyl\Models\User;
use Prologue\Alerts\AlertsMessageBag;
use Spatie\QueryBuilder\QueryBuilder;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\Translation\Translator;
@ -83,7 +84,10 @@ class UserController extends Controller
*/
public function index(Request $request)
{
$users = $this->repository->setSearchTerm($request->input('query'))->getAllUsersWithCounts();
$users = QueryBuilder::for(User::query()->withCount('servers'))
->allowedFilters(['username', 'email', 'uuid'])
->allowedSorts(['id', 'uuid'])
->paginate(50);
return view('admin.users.index', ['users' => $users]);
}
@ -181,11 +185,20 @@ class UserController extends Controller
*/
public function json(Request $request)
{
$users = QueryBuilder::for(User::query())->allowedFilters(['email'])->paginate(25);
// Handle single user requests.
if ($request->query('user_id')) {
return $this->repository->filterById($request->input('user_id'));
$user = User::query()->findOrFail($request->input('user_id'));
$user->md5 = md5(strtolower($user->email));
return $user;
}
return $this->repository->filterUsersByQuery($request->input('q'));
return $users->map(function ($item) {
$item->md5 = md5(strtolower($item->email));
return $item;
});
}
}

View File

@ -5,6 +5,7 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Locations;
use Illuminate\Http\Response;
use Pterodactyl\Models\Location;
use Illuminate\Http\JsonResponse;
use Spatie\QueryBuilder\QueryBuilder;
use Pterodactyl\Services\Locations\LocationUpdateService;
use Pterodactyl\Services\Locations\LocationCreationService;
use Pterodactyl\Services\Locations\LocationDeletionService;
@ -69,7 +70,10 @@ class LocationController extends ApplicationApiController
*/
public function index(GetLocationsRequest $request): array
{
$locations = $this->repository->setSearchTerm($request->input('search'))->paginated(50);
$locations = QueryBuilder::for(Location::query())
->allowedFilters(['short', 'long'])
->allowedSorts(['id'])
->paginate(100);
return $this->fractal->collection($locations)
->transformWith($this->getTransformer(LocationTransformer::class))

View File

@ -3,8 +3,8 @@
namespace Pterodactyl\Http\Controllers\Api\Application\Nodes;
use Pterodactyl\Models\Node;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Spatie\QueryBuilder\QueryBuilder;
use Pterodactyl\Services\Nodes\NodeUpdateService;
use Pterodactyl\Services\Nodes\NodeCreationService;
use Pterodactyl\Services\Nodes\NodeDeletionService;
@ -69,7 +69,10 @@ class NodeController extends ApplicationApiController
*/
public function index(GetNodesRequest $request): array
{
$nodes = $this->repository->setSearchTerm($request->input('search'))->paginated(50);
$nodes = QueryBuilder::for(Node::query())
->allowedFilters(['uuid', 'name', 'fqdn', 'daemon_token_id'])
->allowedSorts(['id', 'uuid', 'memory', 'disk'])
->paginate(100);
return $this->fractal->collection($nodes)
->transformWith($this->getTransformer(NodeTransformer::class))
@ -80,11 +83,12 @@ class NodeController extends ApplicationApiController
* Return data for a single instance of a node.
*
* @param \Pterodactyl\Http\Requests\Api\Application\Nodes\GetNodeRequest $request
* @param \Pterodactyl\Models\Node $node
* @return array
*/
public function view(GetNodeRequest $request): array
public function view(GetNodeRequest $request, Node $node): array
{
return $this->fractal->item($request->getModel(Node::class))
return $this->fractal->item($node)
->transformWith($this->getTransformer(NodeTransformer::class))
->toArray();
}
@ -116,16 +120,15 @@ class NodeController extends ApplicationApiController
* Update an existing node on the Panel.
*
* @param \Pterodactyl\Http\Requests\Api\Application\Nodes\UpdateNodeRequest $request
* @param \Pterodactyl\Models\Node $node
* @return array
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Throwable
*/
public function update(UpdateNodeRequest $request): array
public function update(UpdateNodeRequest $request, Node $node): array
{
$node = $this->updateService->handle(
$request->getModel(Node::class), $request->validated(), $request->input('reset_secret') === true
$node, $request->validated(), $request->input('reset_secret') === true
);
return $this->fractal->item($node)
@ -138,14 +141,15 @@ class NodeController extends ApplicationApiController
* currently attached to it.
*
* @param \Pterodactyl\Http\Requests\Api\Application\Nodes\DeleteNodeRequest $request
* @return \Illuminate\Http\Response
* @param \Pterodactyl\Models\Node $node
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\Service\HasActiveServersException
*/
public function delete(DeleteNodeRequest $request): Response
public function delete(DeleteNodeRequest $request, Node $node): JsonResponse
{
$this->deletionService->handle($request->getModel(Node::class));
$this->deletionService->handle($node);
return response('', 204);
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
}

View File

@ -5,6 +5,7 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Servers;
use Illuminate\Http\Response;
use Pterodactyl\Models\Server;
use Illuminate\Http\JsonResponse;
use Spatie\QueryBuilder\QueryBuilder;
use Pterodactyl\Services\Servers\ServerCreationService;
use Pterodactyl\Services\Servers\ServerDeletionService;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
@ -59,7 +60,10 @@ class ServerController extends ApplicationApiController
*/
public function index(GetServersRequest $request): array
{
$servers = $this->repository->setSearchTerm($request->input('search'))->paginated(50);
$servers = QueryBuilder::for(Server::query())
->allowedFilters(['uuid', 'name', 'image', 'external_id'])
->allowedSorts(['id', 'uuid'])
->paginate(100);
return $this->fractal->collection($servers)
->transformWith($this->getTransformer(ServerTransformer::class))

View File

@ -5,6 +5,7 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Users;
use Pterodactyl\Models\User;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Spatie\QueryBuilder\QueryBuilder;
use Pterodactyl\Services\Users\UserUpdateService;
use Pterodactyl\Services\Users\UserCreationService;
use Pterodactyl\Services\Users\UserDeletionService;
@ -70,7 +71,10 @@ class UserController extends ApplicationApiController
*/
public function index(GetUsersRequest $request): array
{
$users = $this->repository->setSearchTerm($request->input('search'))->paginated(50);
$users = QueryBuilder::for(User::query())
->allowedFilters(['email', 'uuid', 'username', 'external_id'])
->allowedSorts(['id', 'uuid'])
->paginate(100);
return $this->fractal->collection($users)
->transformWith($this->getTransformer(UserTransformer::class))
@ -82,11 +86,12 @@ class UserController extends ApplicationApiController
* were defined in the request.
*
* @param \Pterodactyl\Http\Requests\Api\Application\Users\GetUsersRequest $request
* @param \Pterodactyl\Models\User $user
* @return array
*/
public function view(GetUsersRequest $request): array
public function view(GetUsersRequest $request, User $user): array
{
return $this->fractal->item($request->getModel(User::class))
return $this->fractal->item($user)
->transformWith($this->getTransformer(UserTransformer::class))
->toArray();
}
@ -146,14 +151,15 @@ class UserController extends ApplicationApiController
* on successful deletion.
*
* @param \Pterodactyl\Http\Requests\Api\Application\Users\DeleteUserRequest $request
* @return \Illuminate\Http\Response
* @param \Pterodactyl\Models\User $user
* @return \Illuminate\Http\JsonResponse
*
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public function delete(DeleteUserRequest $request): Response
public function delete(DeleteUserRequest $request, User $user): JsonResponse
{
$this->deletionService->handle($request->getModel(User::class));
$this->deletionService->handle($user);
return response('', 204);
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
}

View File

@ -1,50 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Remote;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class FileDownloadController extends Controller
{
/**
* @var \Illuminate\Contracts\Cache\Repository
*/
private $cache;
/**
* FileDownloadController constructor.
*
* @param \Illuminate\Contracts\Cache\Repository $cache
*/
public function __construct(CacheRepository $cache)
{
$this->cache = $cache;
}
/**
* Handle a request to authenticate a download using a token and return
* the path of the file to the daemon.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function index(Request $request): JsonResponse
{
$download = $this->cache->pull('Server:Downloads:' . $request->input('token', ''));
if (is_null($download)) {
throw new NotFoundHttpException('No file was found using the token provided.');
}
return response()->json([
'path' => array_get($download, 'path'),
'server' => array_get($download, 'server'),
]);
}
}

View File

@ -14,6 +14,7 @@ use Pterodactyl\Services\Servers\GetUserPermissionsService;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Pterodactyl\Http\Requests\Api\Remote\SftpAuthenticationFormRequest;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
class SftpAuthenticationController extends Controller
{
@ -71,11 +72,12 @@ class SftpAuthenticationController extends Controller
'server' => strrev(array_get($parts, 0)),
];
$this->incrementLoginAttempts($request);
if ($this->hasTooManyLoginAttempts($request)) {
return JsonResponse::create([
'error' => 'Too many logins attempted too quickly.',
], JsonResponse::HTTP_TOO_MANY_REQUESTS);
$seconds = $this->limiter()->availableIn($this->throttleKey($request));
throw new TooManyRequestsHttpException(
$seconds, "Too many login attempts for this account, please try again in {$seconds} seconds."
);
}
/** @var \Pterodactyl\Models\Node $node */
@ -91,6 +93,8 @@ class SftpAuthenticationController extends Controller
$server = $this->serverRepository->getByUuid($connection['server'] ?? '');
if (! password_verify($request->input('password'), $user->password) || $server->node_id !== $node->id) {
$this->incrementLoginAttempts($request);
throw new HttpForbiddenException(
'Authorization credentials were not correct, please try again.'
);

View File

@ -8,6 +8,7 @@ use Pterodactyl\Models\Server;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class AuthenticateServerAccess
@ -64,8 +65,10 @@ class AuthenticateServerAccess
}
}
if ($server->suspended) {
throw new AccessDeniedHttpException('This server is currently suspended and the functionality requested is unavailable.');
if ($server->suspended && !$request->routeIs('api:client:server.resources')) {
throw new BadRequestHttpException(
'This server is currently suspended and the functionality requested is unavailable.'
);
}
if (! $server->isInstalled()) {

View File

@ -1,49 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Http\Requests\Admin;
use Pterodactyl\Models\Pack;
use Pterodactyl\Services\Packs\PackCreationService;
class PackFormRequest extends AdminFormRequest
{
/**
* @return array
*/
public function rules()
{
if ($this->method() === 'PATCH') {
return Pack::getRulesForUpdate($this->route()->parameter('pack')->id);
}
return Pack::getRules();
}
/**
* Run validation after the rules above have been applied.
*
* @param \Illuminate\Validation\Validator $validator
*/
public function withValidator($validator)
{
if ($this->method() !== 'POST') {
return;
}
$validator->after(function ($validator) {
$mimetypes = implode(',', PackCreationService::VALID_UPLOAD_TYPES);
/* @var $validator \Illuminate\Validation\Validator */
$validator->sometimes('file_upload', 'sometimes|required|file|mimetypes:' . $mimetypes, function () {
return true;
});
});
}
}

View File

@ -62,15 +62,6 @@ class ServerFormRequest extends AdminFormRequest
], function ($input) {
return ! ($input->auto_deploy);
});
$validator->sometimes('pack_id', [
Rule::exists('packs', 'id')->where(function ($query) {
$query->where('selectable', 1);
$query->where('egg_id', $this->input('egg_id'));
}),
], function ($input) {
return $input->pack_id !== 0 && $input->pack_id !== null;
});
});
}
}

View File

@ -36,7 +36,6 @@ class StoreServerRequest extends ApplicationApiRequest
'description' => array_merge(['nullable'], $rules['description']),
'user' => $rules['owner_id'],
'egg' => $rules['egg_id'],
'pack' => $rules['pack_id'],
'docker_image' => $rules['image'],
'startup' => $rules['startup'],
'environment' => 'present|array',
@ -88,7 +87,6 @@ class StoreServerRequest extends ApplicationApiRequest
'description' => array_get($data, 'description'),
'owner_id' => array_get($data, 'user'),
'egg_id' => array_get($data, 'egg'),
'pack_id' => array_get($data, 'pack'),
'image' => array_get($data, 'docker_image'),
'startup' => array_get($data, 'startup'),
'environment' => array_get($data, 'environment'),

View File

@ -31,7 +31,6 @@ class UpdateServerStartupRequest extends ApplicationApiRequest
'startup' => $data['startup'],
'environment' => 'present|array',
'egg' => $data['egg_id'],
'pack' => $data['pack_id'],
'image' => $data['image'],
'skip_scripts' => 'present|boolean',
];
@ -48,7 +47,6 @@ class UpdateServerStartupRequest extends ApplicationApiRequest
return collect($data)->only(['startup', 'environment', 'skip_scripts'])->merge([
'egg_id' => array_get($data, 'egg'),
'pack_id' => array_get($data, 'pack'),
'docker_image' => array_get($data, 'image'),
])->toArray();
}

View File

@ -17,7 +17,7 @@ class GetFileContentsRequest extends ClientApiRequest implements ClientPermissio
*/
public function permission(): string
{
return Permission::ACTION_FILE_READ;
return Permission::ACTION_FILE_READ_CONTENT;
}
/**

View File

@ -63,7 +63,6 @@ class ApiKey extends Model
'r_' . AdminAcl::RESOURCE_LOCATIONS => 'int',
'r_' . AdminAcl::RESOURCE_NESTS => 'int',
'r_' . AdminAcl::RESOURCE_NODES => 'int',
'r_' . AdminAcl::RESOURCE_PACKS => 'int',
'r_' . AdminAcl::RESOURCE_SERVERS => 'int',
];
@ -110,7 +109,6 @@ class ApiKey extends Model
'r_' . AdminAcl::RESOURCE_LOCATIONS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_NESTS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_NODES => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_PACKS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_SERVERS => 'integer|min:0|max:3',
];

View File

@ -1,78 +0,0 @@
<?php
namespace Pterodactyl\Models;
use Znck\Eloquent\Traits\BelongsToThrough;
class DaemonKey extends Model
{
use BelongsToThrough;
/**
* @var string
*/
protected $table = 'daemon_keys';
/**
* @var array
*/
protected $casts = [
'user_id' => 'integer',
'server_id' => 'integer',
];
/**
* @var array
*/
protected $dates = [
self::CREATED_AT,
self::UPDATED_AT,
'expires_at',
];
/**
* @var array
*/
protected $fillable = ['user_id', 'server_id', 'secret', 'expires_at'];
/**
* @var array
*/
public static $validationRules = [
'user_id' => 'required|numeric|exists:users,id',
'server_id' => 'required|numeric|exists:servers,id',
'secret' => 'required|string|min:20',
'expires_at' => 'required|date',
];
/**
* Return the server relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function server()
{
return $this->belongsTo(Server::class);
}
/**
* Return the node relation.
*
* @return \Znck\Eloquent\Relations\BelongsToThrough
* @throws \Exception
*/
public function node()
{
return $this->belongsToThrough(Node::class, Server::class);
}
/**
* Return the user relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@ -2,8 +2,6 @@
namespace Pterodactyl\Models;
use Pterodactyl\Rules\ResolvesToIPAddress;
class DatabaseHost extends Model
{
/**
@ -60,18 +58,6 @@ class DatabaseHost extends Model
'node_id' => 'sometimes|nullable|integer|exists:nodes,id',
];
/**
* @return array
*/
public static function getRules()
{
$rules = parent::getRules();
$rules['host'] = array_merge($rules['host'], [ new ResolvesToIPAddress() ]);
return $rules;
}
/**
* Gets the node associated with a database host.
*

View File

@ -35,7 +35,6 @@ namespace Pterodactyl\Models;
* @property \Pterodactyl\Models\Nest $nest
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Server[] $servers
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\EggVariable[] $variables
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Pack[] $packs
* @property \Pterodactyl\Models\Egg|null $scriptFrom
* @property \Pterodactyl\Models\Egg|null $configFrom
*/
@ -247,16 +246,6 @@ class Egg extends Model
return $this->hasMany(EggVariable::class, 'egg_id');
}
/**
* Gets all packs associated with this egg.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function packs()
{
return $this->hasMany(Pack::class, 'egg_id');
}
/**
* Get the parent egg from which to copy scripts.
*

View File

@ -43,13 +43,8 @@ class Mount extends Model
*
* @var array
*/
protected $attributes = [
protected $casts = [
'id' => 'int',
'uuid' => 'string',
'name' => 'string',
'description' => 'string',
'source' => 'string',
'target' => 'string',
'read_only' => 'bool',
'user_mountable' => 'bool',
];
@ -60,7 +55,6 @@ class Mount extends Model
* @var string
*/
public static $validationRules = [
// 'uuid' => 'required|string|size:36|unique:mounts,uuid',
'name' => 'required|string|min:2|max:64|unique:mounts,name',
'description' => 'nullable|string|max:255',
'source' => 'required|string',

23
app/Models/MountNode.php Normal file
View File

@ -0,0 +1,23 @@
<?php
namespace Pterodactyl\Models;
use Illuminate\Database\Eloquent\Model;
class MountNode extends Model
{
/**
* @var string
*/
protected $table = 'mount_node';
/**
* @var null
*/
protected $primaryKey = null;
/**
* @var bool
*/
public $incrementing = false;
}

View File

@ -0,0 +1,23 @@
<?php
namespace Pterodactyl\Models;
use Illuminate\Database\Eloquent\Model;
class MountServer extends Model
{
/**
* @var string
*/
protected $table = 'mount_server';
/**
* @var null
*/
protected $primaryKey = null;
/**
* @var bool
*/
public $incrementing = false;
}

View File

@ -13,7 +13,6 @@ namespace Pterodactyl\Models;
*
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Server[] $servers
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Egg[] $eggs
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Pack[] $packs
*/
class Nest extends Model
{
@ -59,16 +58,6 @@ class Nest extends Model
return $this->hasMany(Egg::class);
}
/**
* Returns all of the packs associated with a nest, regardless of the egg.
*
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
*/
public function packs()
{
return $this->hasManyThrough(Pack::class, Egg::class, 'nest_id', 'egg_id');
}
/**
* Gets all servers associated with this nest.
*

View File

@ -5,7 +5,6 @@ namespace Pterodactyl\Models;
use Symfony\Component\Yaml\Yaml;
use Illuminate\Container\Container;
use Illuminate\Notifications\Notifiable;
use Pterodactyl\Models\Traits\Searchable;
use Illuminate\Contracts\Encryption\Encrypter;
/**
@ -33,13 +32,13 @@ use Illuminate\Contracts\Encryption\Encrypter;
* @property \Carbon\Carbon $updated_at
*
* @property \Pterodactyl\Models\Location $location
* @property \Pterodactyl\Models\Mount[]|\Illuminate\Database\Eloquent\Collection $mounts
* @property \Pterodactyl\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers
* @property \Pterodactyl\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations
*/
class Node extends Model
{
use Notifiable;
use Searchable;
/**
* The resource name for this model when it is transformed into an
@ -94,18 +93,6 @@ class Node extends Model
'description', 'maintenance_mode',
];
/**
* Fields that are searchable.
*
* @var array
*/
protected $searchableColumns = [
'name' => 10,
'fqdn' => 8,
'location.short' => 4,
'location.long' => 4,
];
/**
* @var array
*/
@ -182,6 +169,7 @@ class Node extends Model
'bind_port' => $this->daemonSFTP,
],
],
'allowed_mounts' => $this->mounts->pluck('source')->toArray(),
'remote' => route('index'),
];
}
@ -214,11 +202,19 @@ class Node extends Model
*/
public function getDecryptedKey(): string
{
return (string) Container::getInstance()->make(Encrypter::class)->decrypt(
return (string)Container::getInstance()->make(Encrypter::class)->decrypt(
$this->daemon_token
);
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
*/
public function mounts()
{
return $this->hasManyThrough(Mount::class, MountNode::class, 'node_id', 'id', 'id', 'mount_id');
}
/**
* Gets the location associated with a node.
*

View File

@ -1,106 +0,0 @@
<?php
namespace Pterodactyl\Models;
use Pterodactyl\Models\Traits\Searchable;
/**
* @property int $id
* @property int $egg_id
* @property string $uuid
* @property string $name
* @property string $version
* @property string $description
* @property bool $selectable
* @property bool $visible
* @property bool $locked
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*
* @property \Pterodactyl\Models\Egg|null $egg
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Server[] $servers
*/
class Pack extends Model
{
use Searchable;
/**
* The resource name for this model when it is transformed into an
* API representation using fractal.
*/
const RESOURCE_NAME = 'pack';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'packs';
/**
* Fields that are mass assignable.
*
* @var array
*/
protected $fillable = [
'egg_id', 'uuid', 'name', 'version', 'description', 'selectable', 'visible', 'locked',
];
/**
* @var array
*/
public static $validationRules = [
'name' => 'required|string',
'version' => 'required|string',
'description' => 'sometimes|nullable|string',
'selectable' => 'sometimes|required|boolean',
'visible' => 'sometimes|required|boolean',
'locked' => 'sometimes|required|boolean',
'egg_id' => 'required|exists:eggs,id',
];
/**
* Cast values to correct type.
*
* @var array
*/
protected $casts = [
'egg_id' => 'integer',
'selectable' => 'boolean',
'visible' => 'boolean',
'locked' => 'boolean',
];
/**
* Parameters for search querying.
*
* @var array
*/
protected $searchableColumns = [
'name' => 10,
'uuid' => 8,
'egg.name' => 6,
'egg.docker_image' => 5,
'version' => 2,
];
/**
* Gets egg associated with a service pack.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function egg()
{
return $this->belongsTo(Egg::class);
}
/**
* Gets servers associated with a pack.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function servers()
{
return $this->hasMany(Server::class);
}
}

View File

@ -49,6 +49,7 @@ class Permission extends Model
const ACTION_ALLOCATION_DELETE = 'allocation.delete';
const ACTION_FILE_READ = 'file.read';
const ACTION_FILE_READ_CONTENT = 'file.read-content';
const ACTION_FILE_CREATE = 'file.create';
const ACTION_FILE_UPDATE = 'file.update';
const ACTION_FILE_DELETE = 'file.delete';
@ -138,7 +139,8 @@ class Permission extends Model
'description' => 'Permissions that control a user\'s ability to modify the filesystem for this server.',
'keys' => [
'create' => 'Allows a user to create additional files and folders via the Panel or direct upload.',
'read' => 'Allows a user to view the contents of a directory and read the contents of a file. Users with this permission can also download files.',
'read' => 'Allows a user to view the contents of a directory, but not view the contents of or download files.',
'read-content' => 'Allows a user to view the contents of a given file. This will also allow the user to download files.',
'update' => 'Allows a user to update the contents of an existing file or directory.',
'delete' => 'Allows a user to delete files or directories.',
'archive' => 'Allows a user to archive the contents of a directory as well as decompress existing archives on the system.',

View File

@ -3,7 +3,6 @@
namespace Pterodactyl\Models;
use Illuminate\Notifications\Notifiable;
use Pterodactyl\Models\Traits\Searchable;
use Illuminate\Database\Query\JoinClause;
use Znck\Eloquent\Traits\BelongsToThrough;
@ -28,7 +27,6 @@ use Znck\Eloquent\Traits\BelongsToThrough;
* @property int $allocation_id
* @property int $nest_id
* @property int $egg_id
* @property int|null $pack_id
* @property string $startup
* @property string $image
* @property int $installed
@ -42,7 +40,6 @@ use Znck\Eloquent\Traits\BelongsToThrough;
* @property \Pterodactyl\Models\Subuser[]|\Illuminate\Database\Eloquent\Collection $subusers
* @property \Pterodactyl\Models\Allocation $allocation
* @property \Pterodactyl\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations
* @property \Pterodactyl\Models\Pack|null $pack
* @property \Pterodactyl\Models\Node $node
* @property \Pterodactyl\Models\Nest $nest
* @property \Pterodactyl\Models\Egg $egg
@ -50,8 +47,6 @@ use Znck\Eloquent\Traits\BelongsToThrough;
* @property \Pterodactyl\Models\Schedule[]|\Illuminate\Database\Eloquent\Collection $schedule
* @property \Pterodactyl\Models\Database[]|\Illuminate\Database\Eloquent\Collection $databases
* @property \Pterodactyl\Models\Location $location
* @property \Pterodactyl\Models\DaemonKey $key
* @property \Pterodactyl\Models\DaemonKey[]|\Illuminate\Database\Eloquent\Collection $keys
* @property \Pterodactyl\Models\ServerTransfer $transfer
* @property \Pterodactyl\Models\Backup[]|\Illuminate\Database\Eloquent\Collection $backups
* @property \Pterodactyl\Models\Mount[]|\Illuminate\Database\Eloquent\Collection $mounts
@ -60,7 +55,6 @@ class Server extends Model
{
use BelongsToThrough;
use Notifiable;
use Searchable;
/**
* The resource name for this model when it is transformed into an
@ -122,7 +116,6 @@ class Server extends Model
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
'nest_id' => 'required|exists:nests,id',
'egg_id' => 'required|exists:eggs,id',
'pack_id' => 'sometimes|nullable|numeric|min:0',
'startup' => 'required|string',
'skip_scripts' => 'sometimes|boolean',
'image' => 'required|string|max:255',
@ -151,29 +144,12 @@ class Server extends Model
'allocation_id' => 'integer',
'nest_id' => 'integer',
'egg_id' => 'integer',
'pack_id' => 'integer',
'installed' => 'integer',
'database_limit' => 'integer',
'allocation_limit' => 'integer',
'backup_limit' => 'integer',
];
/**
* Parameters for search querying.
*
* @var array
*/
protected $searchableColumns = [
'name' => 100,
'uuid' => 80,
'uuidShort' => 80,
'external_id' => 50,
'user.email' => 40,
'user.username' => 30,
'node.name' => 10,
'pack.name' => 10,
];
/**
* Returns the format for server allocations when communicating with the Daemon.
*
@ -234,16 +210,6 @@ class Server extends Model
return $this->hasMany(Allocation::class, 'server_id');
}
/**
* Gets information for the pack associated with this server.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function pack()
{
return $this->belongsTo(Pack::class);
}
/**
* Gets information for the nest associated with this server.
*
@ -326,26 +292,6 @@ class Server extends Model
return $this->belongsToThrough(Location::class, Node::class);
}
/**
* Return the key belonging to the server owner.
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function key()
{
return $this->hasOne(DaemonKey::class, 'user_id', 'owner_id');
}
/**
* Returns all of the daemon keys belonging to this server.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function keys()
{
return $this->hasMany(DaemonKey::class);
}
/**
* Returns the associated server transfer.
*
@ -367,10 +313,10 @@ class Server extends Model
/**
* Returns all mounts that have this server has mounted.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
*/
public function mounts()
{
return $this->belongsToMany(Mount::class);
return $this->hasManyThrough(Mount::class, MountServer::class, 'server_id', 'id', 'id', 'mount_id');
}
}

View File

@ -99,14 +99,4 @@ class Subuser extends Model
{
return $this->hasMany(Permission::class);
}
/**
* Return the key that belongs to this subuser for the server.
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function key()
{
return $this->hasOne(DaemonKey::class, 'server_id', 'server_id')->where('daemon_keys.user_id', '=', $this->user_id);
}
}

View File

@ -1,13 +0,0 @@
<?php
namespace Pterodactyl\Models\Traits;
use Pterodactyl\Extensions\Illuminate\Database\Eloquent\Builder;
trait Searchable
{
public function newEloquentBuilder($query)
{
return new Builder($query);
}
}

View File

@ -8,7 +8,6 @@ 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;
use Illuminate\Foundation\Auth\Access\Authorizable;
@ -39,7 +38,6 @@ use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification;
* @property string $name
* @property \Pterodactyl\Models\ApiKey[]|\Illuminate\Database\Eloquent\Collection $apiKeys
* @property \Pterodactyl\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers
* @property \Pterodactyl\Models\DaemonKey[]|\Illuminate\Database\Eloquent\Collection $keys
* @property \Pterodactyl\Models\RecoveryToken[]|\Illuminate\Database\Eloquent\Collection $recoveryTokens
*/
class User extends Model implements
@ -52,7 +50,6 @@ class User extends Model implements
use AvailableLanguages;
use CanResetPassword;
use Notifiable;
use Searchable;
const USER_LEVEL_USER = 0;
const USER_LEVEL_ADMIN = 1;
@ -120,20 +117,6 @@ class User extends Model implements
*/
protected $hidden = ['password', 'remember_token', 'totp_secret', 'totp_authenticated_at'];
/**
* Parameters for search querying.
*
* @var array
*/
protected $searchableColumns = [
'username' => 100,
'email' => 100,
'external_id' => 80,
'uuid' => 80,
'name_first' => 40,
'name_last' => 40,
];
/**
* Default values for specific fields in the database.
*
@ -230,16 +213,6 @@ class User extends Model implements
return $this->hasMany(Server::class, 'owner_id');
}
/**
* Return all of the daemon keys that a user belongs to.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function keys()
{
return $this->hasMany(DaemonKey::class);
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/

View File

@ -6,7 +6,6 @@ use Illuminate\Support\ServiceProvider;
use Pterodactyl\Repositories\Eloquent\EggRepository;
use Pterodactyl\Repositories\Eloquent\NestRepository;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Repositories\Eloquent\PackRepository;
use Pterodactyl\Repositories\Eloquent\TaskRepository;
use Pterodactyl\Repositories\Eloquent\UserRepository;
use Pterodactyl\Repositories\Eloquent\ApiKeyRepository;
@ -17,13 +16,11 @@ use Pterodactyl\Repositories\Eloquent\DatabaseRepository;
use Pterodactyl\Repositories\Eloquent\LocationRepository;
use Pterodactyl\Repositories\Eloquent\ScheduleRepository;
use Pterodactyl\Repositories\Eloquent\SettingsRepository;
use Pterodactyl\Repositories\Eloquent\DaemonKeyRepository;
use Pterodactyl\Repositories\Eloquent\AllocationRepository;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Repositories\Eloquent\EggVariableRepository;
use Pterodactyl\Contracts\Repository\NestRepositoryInterface;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Contracts\Repository\PackRepositoryInterface;
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository;
@ -36,7 +33,6 @@ use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
@ -52,7 +48,6 @@ class RepositoryServiceProvider extends ServiceProvider
// Eloquent Repositories
$this->app->bind(AllocationRepositoryInterface::class, AllocationRepository::class);
$this->app->bind(ApiKeyRepositoryInterface::class, ApiKeyRepository::class);
$this->app->bind(DaemonKeyRepositoryInterface::class, DaemonKeyRepository::class);
$this->app->bind(DatabaseRepositoryInterface::class, DatabaseRepository::class);
$this->app->bind(DatabaseHostRepositoryInterface::class, DatabaseHostRepository::class);
$this->app->bind(EggRepositoryInterface::class, EggRepository::class);
@ -60,7 +55,6 @@ class RepositoryServiceProvider extends ServiceProvider
$this->app->bind(LocationRepositoryInterface::class, LocationRepository::class);
$this->app->bind(NestRepositoryInterface::class, NestRepository::class);
$this->app->bind(NodeRepositoryInterface::class, NodeRepository::class);
$this->app->bind(PackRepositoryInterface::class, PackRepository::class);
$this->app->bind(ScheduleRepositoryInterface::class, ScheduleRepository::class);
$this->app->bind(ServerRepositoryInterface::class, ServerRepository::class);
$this->app->bind(ServerVariableRepositoryInterface::class, ServerVariableRepository::class);

View File

@ -1,64 +0,0 @@
<?php
namespace Pterodactyl\Repositories\Concerns;
trait Searchable
{
/**
* The search term to use when filtering results.
*
* @var string|null
*/
protected $searchTerm;
/**
* Set the search term.
*
* @param string|null $term
* @return $this
* @deprecated
*/
public function search($term)
{
return $this->setSearchTerm($term);
}
/**
* Set the search term to use when requesting all records from
* the model.
*
* @param string|null $term
* @return $this
*/
public function setSearchTerm(string $term = null)
{
if (empty($term)) {
return $this;
}
$clone = clone $this;
$clone->searchTerm = $term;
return $clone;
}
/**
* Determine if a valid search term is set on this repository.
*
* @return bool
*/
public function hasSearchTerm(): bool
{
return ! empty($this->searchTerm);
}
/**
* Return the search term.
*
* @return string|null
*/
public function getSearchTerm()
{
return $this->searchTerm;
}
}

View File

@ -2,7 +2,6 @@
namespace Pterodactyl\Repositories\Eloquent;
use Illuminate\Support\Collection;
use Pterodactyl\Models\Allocation;
use Illuminate\Database\Eloquent\Builder;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
@ -19,20 +18,6 @@ class AllocationRepository extends EloquentRepository implements AllocationRepos
return Allocation::class;
}
/**
* Return all of the unique IPs that exist for a given node.
*
* @param int $node
* @return \Illuminate\Support\Collection
*/
public function getUniqueAllocationIpsForNode(int $node): Collection
{
return $this->getBuilder()->where('node_id', $node)
->groupBy('ip')
->orderByRaw('INET_ATON(ip) ASC')
->get($this->getColumns());
}
/**
* Return all of the allocations that exist for a node that are not currently
* allocated.
@ -42,22 +27,12 @@ class AllocationRepository extends EloquentRepository implements AllocationRepos
*/
public function getUnassignedAllocationIds(int $node): array
{
$results = $this->getBuilder()->select('id')->whereNull('server_id')->where('node_id', $node)->get();
return $results->pluck('id')->toArray();
}
/**
* Get an array of all allocations that are currently assigned to a given server.
*
* @param int $server
* @return array
*/
public function getAssignedAllocationIds(int $server): array
{
$results = $this->getBuilder()->select('id')->where('server_id', $server)->get();
return $results->pluck('id')->toArray();
return Allocation::query()->select('id')
->whereNull('server_id')
->where('node_id', $node)
->get()
->pluck('id')
->toArray();
}
/**
@ -71,21 +46,19 @@ class AllocationRepository extends EloquentRepository implements AllocationRepos
* @param array $nodes
* @return array
*/
public function getDiscardableDedicatedAllocations(array $nodes = []): array
protected function getDiscardableDedicatedAllocations(array $nodes = []): array
{
$instance = $this->getBuilder()->select(
$this->getBuilder()->raw('CONCAT_WS("-", node_id, ip) as result')
);
$query = Allocation::query()->selectRaw('CONCAT_WS("-", node_id, ip) as result');
if (! empty($nodes)) {
$instance->whereIn('node_id', $nodes);
$query->whereIn('node_id', $nodes);
}
$results = $instance->whereNotNull('server_id')
->groupBy($this->getBuilder()->raw('CONCAT(node_id, ip)'))
->get();
return $results->pluck('result')->toArray();
return $query->whereNotNull('server_id')
->groupByRaw('CONCAT(node_id, ip)')
->get()
->pluck('result')
->toArray();
}
/**
@ -98,18 +71,18 @@ class AllocationRepository extends EloquentRepository implements AllocationRepos
*/
public function getRandomAllocation(array $nodes, array $ports, bool $dedicated = false)
{
$instance = $this->getBuilder()->whereNull('server_id');
$query = Allocation::query()->whereNull('server_id');
if (! empty($nodes)) {
$instance->whereIn('node_id', $nodes);
$query->whereIn('node_id', $nodes);
}
if (! empty($ports)) {
$instance->where(function (Builder $query) use ($ports) {
$query->where(function (Builder $inner) use ($ports) {
$whereIn = [];
foreach ($ports as $port) {
if (is_array($port)) {
$query->orWhereBetween('port', $port);
$inner->orWhereBetween('port', $port);
continue;
}
@ -117,7 +90,7 @@ class AllocationRepository extends EloquentRepository implements AllocationRepos
}
if (! empty($whereIn)) {
$query->orWhereIn('port', $whereIn);
$inner->orWhereIn('port', $whereIn);
}
});
}
@ -128,12 +101,12 @@ class AllocationRepository extends EloquentRepository implements AllocationRepos
$discard = $this->getDiscardableDedicatedAllocations($nodes);
if (! empty($discard)) {
$instance->whereNotIn(
$query->whereNotIn(
$this->getBuilder()->raw('CONCAT_WS("-", node_id, ip)'), $discard
);
}
}
return $instance->inRandomOrder()->first();
return $query->inRandomOrder()->first();
}
}

View File

@ -1,87 +0,0 @@
<?php
namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\User;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\DaemonKey;
use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
class DaemonKeyRepository extends EloquentRepository implements DaemonKeyRepositoryInterface
{
/**
* Return the model backing this repository.
*
* @return string
*/
public function model()
{
return DaemonKey::class;
}
/**
* Load the server and user relations onto a key model.
*
* @param \Pterodactyl\Models\DaemonKey $key
* @param bool $refresh
* @return \Pterodactyl\Models\DaemonKey
*/
public function loadServerAndUserRelations(DaemonKey $key, bool $refresh = false): DaemonKey
{
if (! $key->relationLoaded('server') || $refresh) {
$key->load('server');
}
if (! $key->relationLoaded('user') || $refresh) {
$key->load('user');
}
return $key;
}
/**
* Return a daemon key with the associated server relation attached.
*
* @param string $key
* @return \Pterodactyl\Models\DaemonKey
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getKeyWithServer(string $key): DaemonKey
{
Assert::notEmpty($key, 'Expected non-empty string as first argument passed to ' . __METHOD__);
try {
return $this->getBuilder()->with('server')->where('secret', '=', $key)->firstOrFail($this->getColumns());
} catch (ModelNotFoundException $exception) {
throw new RecordNotFoundException;
}
}
/**
* Get all of the keys for a specific user including the information needed
* from their server relation for revocation on the daemon.
*
* @param \Pterodactyl\Models\User $user
* @return \Illuminate\Support\Collection
*/
public function getKeysForRevocation(User $user): Collection
{
return $this->getBuilder()->with('node')->where('user_id', $user->id)->get($this->getColumns());
}
/**
* Delete an array of daemon keys from the database. Used primarily in
* conjunction with getKeysForRevocation.
*
* @param array $ids
* @return bool|int
*/
public function deleteKeys(array $ids)
{
return $this->getBuilder()->whereIn('id', $ids)->delete();
}
}

View File

@ -289,12 +289,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
*/
public function all(): Collection
{
$instance = $this->getBuilder();
if (is_subclass_of(get_called_class(), SearchableInterface::class) && $this->hasSearchTerm()) {
$instance = $instance->search($this->getSearchTerm());
}
return $instance->get($this->getColumns());
return $this->getBuilder()->get($this->getColumns());
}
/**
@ -305,12 +300,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf
*/
public function paginated(int $perPage): LengthAwarePaginator
{
$instance = $this->getBuilder();
if (is_subclass_of(get_called_class(), SearchableInterface::class) && $this->hasSearchTerm()) {
$instance = $instance->search($this->getSearchTerm());
}
return $instance->paginate($perPage, $this->getColumns());
return $this->getBuilder()->paginate($perPage, $this->getColumns());
}
/**

View File

@ -4,15 +4,12 @@ namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\Location;
use Illuminate\Support\Collection;
use Pterodactyl\Repositories\Concerns\Searchable;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
class LocationRepository extends EloquentRepository implements LocationRepositoryInterface
{
use Searchable;
/**
* Return the model backing this repository.
*

View File

@ -5,14 +5,11 @@ namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\Mount;
use Pterodactyl\Models\Server;
use Illuminate\Support\Collection;
use Pterodactyl\Repositories\Concerns\Searchable;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
class MountRepository extends EloquentRepository
{
use Searchable;
/**
* Return the model backing this repository.
*

View File

@ -26,7 +26,7 @@ class NestRepository extends EloquentRepository implements NestRepositoryInterfa
}
/**
* Return a nest or all nests with their associated eggs, variables, and packs.
* Return a nest or all nests with their associated eggs and variables.
*
* @param int $id
* @return \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Nest
@ -35,7 +35,7 @@ class NestRepository extends EloquentRepository implements NestRepositoryInterfa
*/
public function getWithEggs(int $id = null)
{
$instance = $this->getBuilder()->with('eggs.packs', 'eggs.variables');
$instance = $this->getBuilder()->with('eggs', 'eggs.variables');
if (! is_null($id)) {
$instance = $instance->find($id, $this->getColumns());
@ -50,7 +50,7 @@ class NestRepository extends EloquentRepository implements NestRepositoryInterfa
}
/**
* Return a nest or all nests and the count of eggs, packs, and servers for that nest.
* Return a nest or all nests and the count of eggs and servers for that nest.
*
* @param int|null $id
* @return \Pterodactyl\Models\Nest|\Illuminate\Database\Eloquent\Collection
@ -59,7 +59,7 @@ class NestRepository extends EloquentRepository implements NestRepositoryInterfa
*/
public function getWithCounts(int $id = null)
{
$instance = $this->getBuilder()->withCount(['eggs', 'packs', 'servers']);
$instance = $this->getBuilder()->withCount(['eggs', 'servers']);
if (! is_null($id)) {
$instance = $instance->find($id, $this->getColumns());

View File

@ -5,14 +5,11 @@ namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\Node;
use Illuminate\Support\Collection;
use Illuminate\Support\LazyCollection;
use Pterodactyl\Repositories\Concerns\Searchable;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
class NodeRepository extends EloquentRepository implements NodeRepositoryInterface
{
use Searchable;
/**
* Return the model backing this repository.
*
@ -85,22 +82,6 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa
})->toArray();
}
/**
* Return all available nodes with a searchable interface.
*
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function getNodeListingData(): LengthAwarePaginator
{
$instance = $this->getBuilder()->with('location')->withCount('servers');
if ($this->hasSearchTerm()) {
$instance->search($this->getSearchTerm());
}
return $instance->paginate(25, $this->getColumns());
}
/**
* Return a single node with location and server information.
*

View File

@ -1,53 +0,0 @@
<?php
namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\Pack;
use Pterodactyl\Repositories\Concerns\Searchable;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Pterodactyl\Contracts\Repository\PackRepositoryInterface;
class PackRepository extends EloquentRepository implements PackRepositoryInterface
{
use Searchable;
/**
* Return the model backing this repository.
*
* @return string
*/
public function model()
{
return Pack::class;
}
/**
* Return a pack with the associated server models attached to it.
*
* @param \Pterodactyl\Models\Pack $pack
* @param bool $refresh
* @return \Pterodactyl\Models\Pack
*/
public function loadServerData(Pack $pack, bool $refresh = false): Pack
{
if ($refresh) {
$pack->load(['servers.node', 'servers.user']);
}
$pack->loadMissing(['servers.node', 'servers.user']);
return $pack;
}
/**
* Return a paginated listing of packs with their associated egg and server count.
*
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function paginateWithEggAndServerCount(): LengthAwarePaginator
{
return $this->getBuilder()->with('egg')->withCount('servers')
->search($this->getSearchTerm())
->paginate(50, $this->getColumns());
}
}

View File

@ -5,7 +5,6 @@ namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\Server;
use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\Builder;
use Pterodactyl\Repositories\Concerns\Searchable;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
@ -13,8 +12,6 @@ use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class ServerRepository extends EloquentRepository implements ServerRepositoryInterface
{
use Searchable;
/**
* Return the model backing this repository.
*
@ -25,19 +22,6 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
return Server::class;
}
/**
* Returns a listing of all servers that exist including relationships.
*
* @param int $paginate
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function getAllServers(int $paginate): LengthAwarePaginator
{
$instance = $this->getBuilder()->with('node', 'user', 'allocation')->search($this->getSearchTerm());
return $instance->paginate($paginate, $this->getColumns());
}
/**
* Load the egg relations onto the server model.
*
@ -63,11 +47,11 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
*/
public function getDataForRebuild(int $server = null, int $node = null): Collection
{
$instance = $this->getBuilder()->with(['allocation', 'allocations', 'pack', 'egg', 'node']);
$instance = $this->getBuilder()->with(['allocation', 'allocations', 'egg', 'node']);
if (! is_null($server) && is_null($node)) {
$instance = $instance->where('id', '=', $server);
} elseif (is_null($server) && ! is_null($node)) {
} else if (is_null($server) && ! is_null($node)) {
$instance = $instance->where('node_id', '=', $node);
}
@ -83,11 +67,11 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
*/
public function getDataForReinstall(int $server = null, int $node = null): Collection
{
$instance = $this->getBuilder()->with(['allocation', 'allocations', 'pack', 'egg', 'node']);
$instance = $this->getBuilder()->with(['allocation', 'allocations', 'egg', 'node']);
if (! is_null($server) && is_null($node)) {
$instance = $instance->where('id', '=', $server);
} elseif (is_null($server) && ! is_null($node)) {
} else if (is_null($server) && ! is_null($node)) {
$instance = $instance->where('node_id', '=', $node);
}
@ -140,7 +124,7 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
*/
public function getDataForCreation(Server $server, bool $refresh = false): Server
{
foreach (['allocation', 'allocations', 'pack', 'egg'] as $relation) {
foreach (['allocation', 'allocations', 'egg'] as $relation) {
if (! $server->relationLoaded($relation) || $refresh) {
$server->load($relation);
}
@ -167,7 +151,7 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
/**
* 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
* the egg which is used for build and rebuild. Only loads relations
* if they are missing, or refresh is set to true.
*
* @param \Pterodactyl\Models\Server $server
@ -180,13 +164,8 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
$server->load('egg');
}
if (! $server->relationLoaded('pack') || $refresh) {
$server->load('pack');
}
return [
'egg' => $server->getRelation('egg')->uuid,
'pack' => is_null($server->getRelation('pack')) ? null : $server->getRelation('pack')->uuid,
];
}
@ -229,9 +208,9 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt
if (! empty($nodes) && ! empty($servers)) {
$instance->whereIn('id', $servers)->orWhereIn('node_id', $nodes);
} elseif (empty($nodes) && ! empty($servers)) {
} else if (empty($nodes) && ! empty($servers)) {
$instance->whereIn('id', $servers);
} elseif (! empty($nodes) && empty($servers)) {
} else if (! empty($nodes) && empty($servers)) {
$instance->whereIn('node_id', $nodes);
}

View File

@ -3,15 +3,10 @@
namespace Pterodactyl\Repositories\Eloquent;
use Pterodactyl\Models\User;
use Illuminate\Support\Collection;
use Pterodactyl\Repositories\Concerns\Searchable;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
class UserRepository extends EloquentRepository implements UserRepositoryInterface
{
use Searchable;
/**
* Return the model backing this repository.
*
@ -21,55 +16,4 @@ class UserRepository extends EloquentRepository implements UserRepositoryInterfa
{
return User::class;
}
/**
* Return all users with counts of servers and subusers of servers.
*
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*/
public function getAllUsersWithCounts(): LengthAwarePaginator
{
return $this->getBuilder()->withCount('servers')
->search($this->getSearchTerm())
->paginate(50, $this->getColumns());
}
/**
* Return all matching models for a user in a format that can be used for dropdowns.
*
* @param string|null $query
* @return \Illuminate\Support\Collection
*/
public function filterUsersByQuery(?string $query): Collection
{
$this->setColumns([
'id', 'email', 'username', 'name_first', 'name_last',
]);
$instance = $this->getBuilder()->search($query)->get($this->getColumns());
return $instance->transform(function ($item) {
$item->md5 = md5(strtolower($item->email));
return $item;
});
}
/**
* Returns a user with the given id in a format that can be used for dropdowns.
*
* @param int $id
* @return \Pterodactyl\Models\Model
*/
public function filterById(int $id): \Pterodactyl\Models\Model
{
$this->setColumns([
'id', 'email', 'username', 'name_first', 'name_last',
]);
$model = $this->getBuilder()->findOrFail($id, $this->getColumns())->getModel();
$model->md5 = md5(strtolower($model->email));
return $model;
}
}

View File

@ -1,49 +0,0 @@
<?php
namespace Pterodactyl\Rules;
use Illuminate\Contracts\Validation\Rule;
class ResolvesToIPAddress implements Rule
{
/**
* Validate that a given string can correctly resolve to a valid IPv4 address.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value): bool
{
// inet_pton returns false if the value passed through is not a valid IP address, so we'll just
// use that a nice ugly PHP hack to determine if we should pass this off to the gethostbyname
// call below.
$isIP = inet_pton($attribute) !== false;
// If the value received is not an IP address try to look it up using the gethostbyname() call.
// If that returns the same value that we passed in then it means it did not resolve to anything
// and we should fail this validation call.
return $isIP || gethostbyname($value) !== $value;
}
/**
* Return a validation message for use when this rule fails.
*
* @return string
*/
public function message(): string
{
return 'The :attribute must be a valid IPv4 address or hostname that resolves to a valid IPv4 address.';
}
/**
* Convert the rule to a validation string. This is necessary to avoid
* issues with Eloquence which tries to use this rule as a string.
*
* @return string
*/
public function __toString()
{
return 'p_resolves_to_ip_address';
}
}

View File

@ -34,7 +34,6 @@ class AdminAcl
const RESOURCE_EGGS = 'eggs';
const RESOURCE_DATABASE_HOSTS = 'database_hosts';
const RESOURCE_SERVER_DATABASES = 'server_databases';
const RESOURCE_PACKS = 'packs';
/**
* Determine if an API key has permission to perform a specific read/write operation.

View File

@ -82,12 +82,24 @@ class AssetHashService
*/
public function css(string $resource): string
{
return '<link href="' . $this->url($resource) . '"
rel="stylesheet preload"
as="style"
crossorigin="anonymous"
integrity="' . $this->integrity($resource) . '"
referrerpolicy="no-referrer">';
$attributes = [
'href' => $this->url($resource),
'rel' => 'stylesheet preload',
'as' => 'style',
'crossorigin' => 'anonymous',
'referrerpolicy' => 'no-referrer',
];
if (config('pterodactyl.assets.use_hash')) {
$attributes['integrity'] = $this->integrity($resource);
}
$output = '<link';
foreach ($attributes as $key => $value) {
$output .= " $key=\"$value\"";
}
return $output . '>';
}
/**
@ -100,9 +112,21 @@ class AssetHashService
*/
public function js(string $resource): string
{
return '<script src="' . $this->url($resource) . '"
integrity="' . $this->integrity($resource) . '"
crossorigin="anonymous"></script>';
$attributes = [
'src' => $this->url($resource),
'crossorigin' => 'anonymous',
];
if (config('pterodactyl.assets.use_hash')) {
$attributes['integrity'] = $this->integrity($resource);
}
$output = '<script';
foreach ($attributes as $key => $value) {
$output .= " $key=\"$value\"";
}
return $output . '></script>';
}
/**

View File

@ -1,97 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Services\Packs;
use ZipArchive;
use Pterodactyl\Models\Pack;
use Pterodactyl\Contracts\Repository\PackRepositoryInterface;
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
use Pterodactyl\Exceptions\Service\Pack\ZipArchiveCreationException;
class ExportPackService
{
/**
* @var \ZipArchive
*/
protected $archive;
/**
* @var \Pterodactyl\Contracts\Repository\PackRepositoryInterface
*/
protected $repository;
/**
* @var \Illuminate\Contracts\Filesystem\Factory
*/
protected $storage;
/**
* ExportPackService constructor.
*
* @param \Illuminate\Contracts\Filesystem\Factory $storage
* @param \Pterodactyl\Contracts\Repository\PackRepositoryInterface $repository
* @param \ZipArchive $archive
*/
public function __construct(
FilesystemFactory $storage,
PackRepositoryInterface $repository,
ZipArchive $archive
) {
$this->archive = $archive;
$this->repository = $repository;
$this->storage = $storage;
}
/**
* Prepare a pack for export.
*
* @param int|\Pterodactyl\Models\Pack $pack
* @param bool $files
* @return string
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Pack\ZipArchiveCreationException
*/
public function handle($pack, $files = false)
{
if (! $pack instanceof Pack) {
$pack = $this->repository->find($pack);
}
$json = [
'name' => $pack->name,
'version' => $pack->version,
'description' => $pack->description,
'selectable' => $pack->selectable,
'visible' => $pack->visible,
'locked' => $pack->locked,
];
$filename = tempnam(sys_get_temp_dir(), 'pterodactyl_');
if ($files) {
if (! $this->archive->open($filename, $this->archive::CREATE)) {
throw new ZipArchiveCreationException;
}
foreach ($this->storage->disk()->files('packs/' . $pack->uuid) as $file) {
$this->archive->addFile(storage_path('app/' . $file), basename(storage_path('app/' . $file)));
}
$this->archive->addFromString('import.json', json_encode($json, JSON_PRETTY_PRINT));
$this->archive->close();
} else {
$fp = fopen($filename, 'a+');
fwrite($fp, json_encode($json, JSON_PRETTY_PRINT));
fclose($fp);
}
return $filename;
}
}

View File

@ -1,104 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Services\Packs;
use Ramsey\Uuid\Uuid;
use Illuminate\Http\UploadedFile;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Contracts\Repository\PackRepositoryInterface;
use Pterodactyl\Exceptions\Service\InvalidFileUploadException;
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
use Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException;
class PackCreationService
{
const VALID_UPLOAD_TYPES = [
'application/gzip',
'application/x-gzip',
];
/**
* @var \Illuminate\Database\ConnectionInterface
*/
protected $connection;
/**
* @var \Pterodactyl\Contracts\Repository\PackRepositoryInterface
*/
protected $repository;
/**
* @var \Illuminate\Contracts\Filesystem\Factory
*/
protected $storage;
/**
* PackCreationService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Contracts\Filesystem\Factory $storage
* @param \Pterodactyl\Contracts\Repository\PackRepositoryInterface $repository
*/
public function __construct(
ConnectionInterface $connection,
FilesystemFactory $storage,
PackRepositoryInterface $repository
) {
$this->connection = $connection;
$this->repository = $repository;
$this->storage = $storage;
}
/**
* Add a new service pack to the system.
*
* @param array $data
* @param \Illuminate\Http\UploadedFile|null $file
* @return \Pterodactyl\Models\Pack
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
*/
public function handle(array $data, UploadedFile $file = null)
{
if (! is_null($file)) {
if (! $file->isValid()) {
throw new InvalidFileUploadException(trans('exceptions.packs.invalid_upload'));
}
if (! in_array($file->getMimeType(), self::VALID_UPLOAD_TYPES)) {
throw new InvalidFileMimeTypeException(trans('exceptions.packs.invalid_mime', [
'type' => implode(', ', self::VALID_UPLOAD_TYPES),
]));
}
}
// Transform values to boolean
$data['selectable'] = isset($data['selectable']);
$data['visible'] = isset($data['visible']);
$data['locked'] = isset($data['locked']);
$this->connection->beginTransaction();
$pack = $this->repository->create(array_merge(
['uuid' => Uuid::uuid4()],
$data
));
$this->storage->disk()->makeDirectory('packs/' . $pack->uuid);
if (! is_null($file)) {
$file->storeAs('packs/' . $pack->uuid, 'archive.tar.gz');
}
$this->connection->commit();
return $pack;
}
}

View File

@ -1,85 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Services\Packs;
use Pterodactyl\Models\Pack;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Contracts\Repository\PackRepositoryInterface;
use Pterodactyl\Exceptions\Service\HasActiveServersException;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
class PackDeletionService
{
/**
* @var \Illuminate\Database\ConnectionInterface
*/
protected $connection;
/**
* @var \Pterodactyl\Contracts\Repository\PackRepositoryInterface
*/
protected $repository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
protected $serverRepository;
/**
* @var \Illuminate\Contracts\Filesystem\Factory
*/
protected $storage;
/**
* PackDeletionService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Contracts\Filesystem\Factory $storage
* @param \Pterodactyl\Contracts\Repository\PackRepositoryInterface $repository
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository
*/
public function __construct(
ConnectionInterface $connection,
FilesystemFactory $storage,
PackRepositoryInterface $repository,
ServerRepositoryInterface $serverRepository
) {
$this->connection = $connection;
$this->repository = $repository;
$this->serverRepository = $serverRepository;
$this->storage = $storage;
}
/**
* Delete a pack from the database as well as the archive stored on the server.
*
* @param int|\Pterodactyl\Models\Pack $pack
*
* @throws \Pterodactyl\Exceptions\Service\HasActiveServersException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle($pack)
{
if (! $pack instanceof Pack) {
$pack = $this->repository->setColumns(['id', 'uuid'])->find($pack);
}
$count = $this->serverRepository->findCountWhere([['pack_id', '=', $pack->id]]);
if ($count !== 0) {
throw new HasActiveServersException(trans('exceptions.packs.delete_has_servers'));
}
$this->connection->beginTransaction();
$this->repository->delete($pack->id);
$this->storage->disk()->deleteDirectory('packs/' . $pack->uuid);
$this->connection->commit();
}
}

View File

@ -1,75 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Services\Packs;
use Pterodactyl\Models\Pack;
use Pterodactyl\Contracts\Repository\PackRepositoryInterface;
use Pterodactyl\Exceptions\Service\HasActiveServersException;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class PackUpdateService
{
/**
* @var \Pterodactyl\Contracts\Repository\PackRepositoryInterface
*/
protected $repository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
protected $serverRepository;
/**
* PackUpdateService constructor.
*
* @param \Pterodactyl\Contracts\Repository\PackRepositoryInterface $repository
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository
*/
public function __construct(
PackRepositoryInterface $repository,
ServerRepositoryInterface $serverRepository
) {
$this->repository = $repository;
$this->serverRepository = $serverRepository;
}
/**
* Update a pack.
*
* @param int|\Pterodactyl\Models\Pack $pack
* @param array $data
* @return bool
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Service\HasActiveServersException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle($pack, array $data)
{
if (! $pack instanceof Pack) {
$pack = $this->repository->setColumns(['id', 'egg_id'])->find($pack);
}
if ((int) array_get($data, 'egg_id', $pack->egg_id) !== $pack->egg_id) {
$count = $this->serverRepository->findCountWhere([['pack_id', '=', $pack->id]]);
if ($count !== 0) {
throw new HasActiveServersException(trans('exceptions.packs.update_has_servers'));
}
}
// Transform values to boolean
$data['selectable'] = isset($data['selectable']);
$data['visible'] = isset($data['visible']);
$data['locked'] = isset($data['locked']);
return $this->repository->withoutFreshModel()->update($pack->id, $data);
}
}

View File

@ -1,125 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Services\Packs;
use ZipArchive;
use Illuminate\Http\UploadedFile;
use Pterodactyl\Exceptions\Service\InvalidFileUploadException;
use Pterodactyl\Exceptions\Service\Pack\ZipExtractionException;
use Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException;
use Pterodactyl\Exceptions\Service\Pack\UnreadableZipArchiveException;
use Pterodactyl\Exceptions\Service\Pack\InvalidPackArchiveFormatException;
class TemplateUploadService
{
const VALID_UPLOAD_TYPES = [
'application/zip',
'text/plain',
'application/json',
];
/**
* @var \ZipArchive
*/
protected $archive;
/**
* @var \Pterodactyl\Services\Packs\PackCreationService
*/
protected $creationService;
/**
* TemplateUploadService constructor.
*
* @param \Pterodactyl\Services\Packs\PackCreationService $creationService
* @param \ZipArchive $archive
*/
public function __construct(
PackCreationService $creationService,
ZipArchive $archive
) {
$this->archive = $archive;
$this->creationService = $creationService;
}
/**
* Process an uploaded file to create a new pack from a JSON or ZIP format.
*
* @param int $egg
* @param \Illuminate\Http\UploadedFile $file
* @return \Pterodactyl\Models\Pack
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Service\Pack\ZipExtractionException
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
* @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException
* @throws \Pterodactyl\Exceptions\Service\Pack\UnreadableZipArchiveException
* @throws \Pterodactyl\Exceptions\Service\Pack\InvalidPackArchiveFormatException
*/
public function handle($egg, UploadedFile $file)
{
if (! $file->isValid()) {
throw new InvalidFileUploadException(trans('exceptions.packs.invalid_upload'));
}
if (! in_array($file->getMimeType(), self::VALID_UPLOAD_TYPES)) {
throw new InvalidFileMimeTypeException(trans('exceptions.packs.invalid_mime', [
'type' => implode(', ', self::VALID_UPLOAD_TYPES),
]));
}
if ($file->getMimeType() === 'application/zip') {
return $this->handleArchive($egg, $file);
} else {
$json = json_decode($file->openFile()->fread($file->getSize()), true);
$json['egg_id'] = $egg;
return $this->creationService->handle($json);
}
}
/**
* Process a ZIP file to create a pack and stored archive.
*
* @param int $egg
* @param \Illuminate\Http\UploadedFile $file
* @return \Pterodactyl\Models\Pack
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Service\Pack\ZipExtractionException
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
* @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException
* @throws \Pterodactyl\Exceptions\Service\Pack\UnreadableZipArchiveException
* @throws \Pterodactyl\Exceptions\Service\Pack\InvalidPackArchiveFormatException
*/
protected function handleArchive($egg, $file)
{
if (! $this->archive->open($file->getRealPath())) {
throw new UnreadableZipArchiveException(trans('exceptions.packs.unreadable'));
}
if (! $this->archive->locateName('import.json') || ! $this->archive->locateName('archive.tar.gz')) {
throw new InvalidPackArchiveFormatException(trans('exceptions.packs.invalid_archive_exception'));
}
$json = json_decode($this->archive->getFromName('import.json'), true);
$json['egg_id'] = $egg;
$pack = $this->creationService->handle($json);
if (! $this->archive->extractTo(storage_path('app/packs/' . $pack->uuid), 'archive.tar.gz')) {
// @todo delete the pack that was created.
throw new ZipExtractionException(trans('exceptions.packs.zip_extraction'));
}
$this->archive->close();
return $pack;
}
}

View File

@ -159,7 +159,7 @@ class BuildModificationService
// Handle removal of allocations from this server.
if (array_key_exists('remove_allocations', $data) && ! empty($data['remove_allocations'])) {
$assigned = $this->allocationRepository->getAssignedAllocationIds($server->id);
$assigned = $server->allocations->pluck('id')->toArray();
$updateIds = [];
foreach ($data['remove_allocations'] as $allocation) {

View File

@ -9,12 +9,13 @@
namespace Pterodactyl\Services\Servers;
use Pterodactyl\Models\Mount;
use Pterodactyl\Models\Server;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class ServerConfigurationStructureService
{
const REQUIRED_RELATIONS = ['allocation', 'allocations', 'pack', 'egg'];
const REQUIRED_RELATIONS = ['allocation', 'allocations', 'egg'];
/**
* @var \Pterodactyl\Services\Servers\EnvironmentService
@ -66,27 +67,15 @@ class ServerConfigurationStructureService
*
* @param \Pterodactyl\Models\Server $server
* @return array
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
protected function returnCurrentFormat(Server $server)
{
$mounts = $server->mounts;
foreach ($mounts as $mount) {
unset($mount->id);
unset($mount->uuid);
unset($mount->name);
unset($mount->description);
$mount->read_only = $mount->read_only == 1;
unset($mount->user_mountable);
unset($mount->pivot);
}
return [
'uuid' => $server->uuid,
'suspended' => (bool) $server->suspended,
'environment' => $this->environment->handle($server),
'invocation' => $server->startup,
'skip_egg_scripts' => $server->skip_scripts,
'build' => [
'memory_limit' => $server->memory,
'swap' => $server->swap,
@ -95,11 +84,6 @@ class ServerConfigurationStructureService
'threads' => $server->threads,
'disk_space' => $server->disk,
],
'service' => [
'egg' => $server->egg->uuid,
'pack' => $server->pack ? $server->pack->uuid : null,
'skip_scripts' => $server->skip_scripts,
],
'container' => [
'image' => $server->image,
'oom_disabled' => $server->oom_disabled,
@ -112,7 +96,13 @@ class ServerConfigurationStructureService
],
'mappings' => $server->getAllocationMappings(),
],
'mounts' => $mounts,
'mounts' => $server->mounts->map(function (Mount $mount) {
return [
'source' => $mount->source,
'target' => $mount->target,
'read_only' => $mount->read_only,
];
}),
];
}
@ -149,7 +139,6 @@ class ServerConfigurationStructureService
],
'service' => [
'egg' => $server->egg->uuid,
'pack' => $server->pack ? $server->pack->uuid : null,
'skip_scripts' => $server->skip_scripts,
],
'rebuild' => false,

View File

@ -246,7 +246,6 @@ class ServerCreationService
'allocation_id' => Arr::get($data, 'allocation_id'),
'nest_id' => Arr::get($data, 'nest_id'),
'egg_id' => Arr::get($data, 'egg_id'),
'pack_id' => empty($data['pack_id']) ? null : $data['pack_id'],
'startup' => Arr::get($data, 'startup'),
'image' => Arr::get($data, 'image'),
'database_limit' => Arr::get($data, 'database_limit') ?? 0,

View File

@ -140,7 +140,6 @@ class StartupModificationService
'startup' => array_get($data, 'startup', $server->startup),
'nest_id' => array_get($data, 'nest_id', $server->nest_id),
'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' => array_get($data, 'skip_scripts') ?? isset($data['skip_scripts']),
'image' => array_get($data, 'docker_image', $server->image),
]);

View File

@ -30,7 +30,10 @@ trait EnvironmentWriterTrait
$saveContents = file_get_contents($path);
collect($values)->each(function ($value, $key) use (&$saveContents) {
$key = strtoupper($key);
if (str_contains($value, ' ') && ! preg_match('/\"(.*)\"/', $value)) {
// If the key value is not sorrounded by quotation marks, and contains anything that could reasonably
// cause environment parsing issues, wrap it in quotes before writing it. This also adds slashes to the
// value to ensure quotes within it don't cause us issues.
if (! preg_match('/^\"(.*)\"$/', $value) && preg_match('/([^\w.\-+\/])+/', $value)) {
$value = sprintf('"%s"', addslashes($value));
}

View File

@ -25,7 +25,7 @@ class LocationTransformer extends BaseTransformer
}
/**
* Return a generic transformed pack array.
* Return a generic transformed location array.
*
* @param \Pterodactyl\Models\Location $location
* @return array

View File

@ -1,40 +0,0 @@
<?php
namespace Pterodactyl\Transformers\Api\Application;
use Pterodactyl\Models\Pack;
class PackTransformer extends BaseTransformer
{
/**
* Return the resource name for the JSONAPI output.
*
* @return string
*/
public function getResourceName(): string
{
return Pack::RESOURCE_NAME;
}
/**
* Return a transformed User model that can be consumed by external services.
*
* @param \Pterodactyl\Models\Pack $pack
* @return array
*/
public function transform(Pack $pack): array
{
return [
'id' => $pack->id,
'uuid' => $pack->uuid,
'egg' => $pack->egg_id,
'name' => $pack->name,
'description' => $pack->description,
'is_selectable' => (bool) $pack->selectable,
'is_visible' => (bool) $pack->visible,
'is_locked' => (bool) $pack->locked,
'created_at' => $this->formatTimestamp($pack->created_at),
'updated_at' => $this->formatTimestamp($pack->updated_at),
];
}
}

View File

@ -22,7 +22,6 @@ class ServerTransformer extends BaseTransformer
'allocations',
'user',
'subusers',
'pack',
'nest',
'egg',
'variables',
@ -87,7 +86,6 @@ class ServerTransformer extends BaseTransformer
'allocation' => $server->allocation_id,
'nest' => $server->nest_id,
'egg' => $server->egg_id,
'pack' => $server->pack_id,
'container' => [
'startup_command' => $server->startup,
'image' => $server->image,
@ -156,28 +154,6 @@ class ServerTransformer extends BaseTransformer
return $this->item($server->getRelation('user'), $this->makeTransformer(UserTransformer::class), 'user');
}
/**
* Return a generic array with pack information for this server.
*
* @param \Pterodactyl\Models\Server $server
* @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource
*
* @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException
*/
public function includePack(Server $server)
{
if (! $this->authorize(AdminAcl::RESOURCE_PACKS)) {
return $this->null();
}
$server->loadMissing('pack');
if (is_null($server->getRelation('pack'))) {
return $this->null();
}
return $this->item($server->getRelation('pack'), $this->makeTransformer(PackTransformer::class), 'pack');
}
/**
* Return a generic array with nest information for this server.
*
@ -236,7 +212,7 @@ class ServerTransformer extends BaseTransformer
}
/**
* Return a generic array with pack information for this server.
* Return a generic array with location information for this server.
*
* @param \Pterodactyl\Models\Server $server
* @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource
@ -255,7 +231,7 @@ class ServerTransformer extends BaseTransformer
}
/**
* Return a generic array with pack information for this server.
* Return a generic array with node information for this server.
*
* @param \Pterodactyl\Models\Server $server
* @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource

View File

@ -1,76 +0,0 @@
<?php
namespace Pterodactyl\Transformers\Daemon;
use Carbon\Carbon;
use Pterodactyl\Models\DaemonKey;
use Pterodactyl\Models\Permission;
use League\Fractal\TransformerAbstract;
use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
class ApiKeyTransformer extends TransformerAbstract
{
/**
* @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface
*/
private $keyRepository;
/**
* @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface
*/
private $repository;
/**
* ApiKeyTransformer constructor.
*
* @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $keyRepository
* @param \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface $repository
*/
public function __construct(DaemonKeyRepositoryInterface $keyRepository, SubuserRepositoryInterface $repository)
{
$this->repository = $repository;
$this->keyRepository = $keyRepository;
}
/**
* Return a listing of servers that a daemon key can access.
*
* @param \Pterodactyl\Models\DaemonKey $key
* @return array
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function transform(DaemonKey $key)
{
$this->keyRepository->loadServerAndUserRelations($key);
if ($key->user_id === $key->getRelation('server')->owner_id || $key->getRelation('user')->root_admin) {
return [
'id' => $key->getRelation('server')->uuid,
'is_temporary' => true,
'expires_in' => max(Carbon::now()->diffInSeconds($key->expires_at, false), 0),
'permissions' => ['s:*'],
];
}
$subuser = $this->repository->getWithPermissionsUsingUserAndServer($key->user_id, $key->server_id);
$permissions = $subuser->getRelation('permissions')->pluck('permission')->toArray();
$mappings = Permission::getPermissions(true);
$daemonPermissions = ['s:console'];
foreach ($permissions as $permission) {
if (! is_null(array_get($mappings, $permission))) {
$daemonPermissions[] = array_get($mappings, $permission);
}
}
return [
'id' => $key->getRelation('server')->uuid,
'is_temporary' => true,
'expires_in' => max(Carbon::now()->diffInSeconds($key->expires_at, false), 0),
'permissions' => $daemonPermissions,
];
}
}

View File

@ -14,14 +14,6 @@ class FileObjectTransformer extends BaseDaemonTransformer
*/
private $editable = [];
/**
* FileObjectTransformer constructor.
*/
public function __construct()
{
$this->editable = config('pterodactyl.files.editable', []);
}
/**
* Transform a file object response from the daemon into a standardized response.
*
@ -36,8 +28,7 @@ class FileObjectTransformer extends BaseDaemonTransformer
'size' => Arr::get($item, 'size'),
'is_file' => Arr::get($item, 'file', true),
'is_symlink' => Arr::get($item, 'symlink', false),
'is_editable' => in_array(Arr::get($item, 'mime', ''), $this->editable),
'mimetype' => Arr::get($item, 'mime'),
'mimetype' => Arr::get($item, 'mime', 'application/octet-stream'),
'created_at' => Carbon::parse(Arr::get($item, 'created', ''))->toIso8601String(),
'modified_at' => Carbon::parse(Arr::get($item, 'modified', ''))->toIso8601String(),
];

View File

@ -96,6 +96,8 @@ return [
'client' => 'predis',
'default' => [
'scheme' => env('REDIS_SCHEME', 'tcp'),
'path' => env('REDIS_PATH', '/run/redis/redis.sock'),
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
@ -103,6 +105,8 @@ return [
],
'sessions' => [
'scheme' => env('REDIS_SCHEME', 'tcp'),
'path' => env('REDIS_PATH', '/run/redis/redis.sock'),
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),

View File

@ -10,7 +10,7 @@ return [
| setup on the panel. When set to true, configurations stored in the
| database will not be applied.
*/
'load_environment_only' => (bool) env('APP_ENVIRONMENT_ONLY', false),
'load_environment_only' => (bool)env('APP_ENVIRONMENT_ONLY', false),
/*
|--------------------------------------------------------------------------
@ -56,7 +56,6 @@ return [
'admin' => [
'servers' => env('APP_PAGINATE_ADMIN_SERVERS', 25),
'users' => env('APP_PAGINATE_ADMIN_USERS', 25),
'packs' => env('APP_PAGINATE_ADMIN_PACKS', 50),
],
'api' => [
'nodes' => env('APP_PAGINATE_API_NODES', 25),
@ -102,29 +101,6 @@ return [
'high' => env('QUEUE_HIGH', 'high'),
],
/*
|--------------------------------------------------------------------------
| Console Configuration
|--------------------------------------------------------------------------
|
| Configure the speed at which data is rendered to the console.
*/
'console' => [
'count' => env('CONSOLE_PUSH_COUNT', 10),
'frequency' => env('CONSOLE_PUSH_FREQ', 200),
],
/*
|--------------------------------------------------------------------------
| Daemon Connection Details
|--------------------------------------------------------------------------
|
| Configuration for support of the new Golang based daemon.
*/
'daemon' => [
'use_new_daemon' => (bool) env('APP_USE_NEW_DAEMON', false),
],
/*
|--------------------------------------------------------------------------
| Task Timers
@ -178,20 +154,6 @@ return [
*/
'files' => [
'max_edit_size' => env('PTERODACTYL_FILES_MAX_EDIT_SIZE', 1024 * 1024 * 4),
'editable' => [
'application/json',
'application/javascript',
'application/xml',
'application/xhtml+xml',
'inode/x-empty',
'text/xml',
'text/css',
'text/html',
'text/plain',
'text/x-perl',
'text/x-shellscript',
'text/x-python',
],
],
/*
@ -226,4 +188,15 @@ return [
'environment_variables' => [
'P_SERVER_ALLOCATION_LIMIT' => 'allocation_limit',
],
/*
|--------------------------------------------------------------------------
| Asset Verification
|--------------------------------------------------------------------------
|
| This section controls the output format for JS & CSS assets.
*/
'assets' => [
'use_hash' => env('PTERODACTYL_USE_ASSET_HASH', false),
],
];

View File

@ -32,7 +32,6 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker $faker) {
'io' => 500,
'cpu' => 0,
'oom_disabled' => 0,
'pack_id' => null,
'installed' => 1,
'database_limit' => null,
'allocation_limit' => null,
@ -132,18 +131,6 @@ $factory->state(Pterodactyl\Models\EggVariable::class, 'editable', function () {
return ['user_editable' => 1];
});
$factory->define(Pterodactyl\Models\Pack::class, function (Faker $faker) {
return [
'uuid' => $faker->uuid,
'name' => $faker->word,
'description' => null,
'version' => $faker->randomNumber(),
'selectable' => 1,
'visible' => 1,
'locked' => 0,
];
});
$factory->define(Pterodactyl\Models\Subuser::class, function (Faker $faker) {
return [];
});
@ -194,13 +181,6 @@ $factory->define(Pterodactyl\Models\Task::class, function (Faker $faker) {
];
});
$factory->define(Pterodactyl\Models\DaemonKey::class, function (Faker $faker) {
return [
'secret' => 'i_' . str_random(40),
'expires_at' => \Carbon\Carbon::now()->addMinutes(10)->toDateTimeString(),
];
});
$factory->define(Pterodactyl\Models\ApiKey::class, function (Faker $faker) {
static $token;

View File

@ -1,5 +1,6 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
@ -13,6 +14,21 @@ class CreateBackupsTable extends Migration
*/
public function up()
{
$db = config('database.default');
// There exists a backups plugin for the 0.7 version of the Panel. However, it didn't properly
// namespace itself so now we have to deal with these tables being in the way of tables we're trying
// to use. For now, just rename them to maintain the data.
$results = DB::select('SELECT TABLE_NAME FROM information_schema.tables WHERE table_schema = ? AND table_name LIKE ? AND table_name NOT LIKE \'%_plugin_bak\'', [
config("database.connections.{$db}.database"),
'backup%'
]);
// Take any of the results, most likely "backups" and "backup_logs" and rename them to have a
// suffix so data isn't completely lost, but they're no longer in the way of this migration...
foreach ($results as $result) {
Schema::rename($result->TABLE_NAME, $result->TABLE_NAME. '_plugin_bak');
}
Schema::create('backups', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedInteger('server_id');

View File

@ -1,5 +1,6 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
@ -13,9 +14,23 @@ class AddBackupLimitToServers extends Migration
*/
public function up()
{
Schema::table('servers', function (Blueprint $table) {
$table->unsignedInteger('backup_limit')->default(0)->after('database_limit');
});
$db = config('database.default');
// Same as in the backups migration, we need to handle that plugin messing with the data structure
// here. If we find a result we'll actually keep the column around since we can maintain that backup
// limit, but we need to correct the column definition a bit.
$results = DB::select('SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = \'servers\' AND COLUMN_NAME = \'backup_limit\'', [
config("database.connections.{$db}.database")
]);
if (count($results) === 1) {
Schema::table('servers', function (Blueprint $table) {
$table->unsignedInteger('backup_limit')->default(0)->change();
});
} else {
Schema::table('servers', function (Blueprint $table) {
$table->unsignedInteger('backup_limit')->default(0)->after('database_limit');
});
}
}
/**

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class DropPacksFromServers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('servers', function (Blueprint $table) {
$table->dropForeign(['pack_id']);
$table->dropColumn('pack_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('servers', function (Blueprint $table) {
$table->unsignedInteger('pack_id')->after('egg_id')->nullable();
$table->foreign('pack_id')->references('id')->on('packs');
});
}
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class DropPacksFromApiKeyPermissions extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('api_keys', function (Blueprint $table) {
$table->dropColumn('r_packs');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('api_keys', function (Blueprint $table) {
$table->unsignedTinyInteger('r_packs')->default(0);
});
}
}

View File

@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class DropPacksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::dropIfExists('packs');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::create('packs', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('egg_id');
$table->char('uuid', 36)->unique();
$table->string('name');
$table->string('version');
$table->text('description')->nullable();
$table->tinyInteger('selectable')->default(1);
$table->tinyInteger('visible')->default(1);
$table->tinyInteger('locked')->default(0);
$table->timestamps();
});
Schema::table('packs', function (Blueprint $table) {
$table->foreign('egg_id')->references('id')->on('eggs')->cascadeOnDelete();
});
}
}

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class DropDaemonKeyTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::dropIfExists('daemon_keys');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::create('daemon_keys', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('server_id');
$table->unsignedInteger('user_id');
$table->string('secret')->unique();
$table->timestamp('expires_at');
$table->timestamps();
});
Schema::table('daemon_keys', function (Blueprint $table) {
$table->foreign('server_id')->references('id')->on('servers')->cascadeOnDelete();
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
});
}
}

View File

@ -5,9 +5,8 @@
"@fortawesome/free-solid-svg-icons": "^5.9.0",
"@fortawesome/react-fontawesome": "0.1.4",
"axios": "^0.19.2",
"ayu-ace": "^2.0.4",
"brace": "^0.11.1",
"chart.js": "^2.8.0",
"codemirror": "^5.57.0",
"date-fns": "^2.14.0",
"debounce": "^1.2.0",
"deepmerge": "^4.2.2",
@ -57,6 +56,7 @@
"@babel/preset-typescript": "^7.7.4",
"@babel/runtime": "^7.7.5",
"@types/chart.js": "^2.8.5",
"@types/codemirror": "^0.0.98",
"@types/debounce": "^1.2.0",
"@types/events": "^3.0.0",
"@types/node": "^12.6.9",

View File

@ -161,7 +161,7 @@ function initUserIdSelect(data) {
data: function (params) {
return {
q: params.term, // search term
filter: { email: params.term },
page: params.page,
};
},

View File

@ -1,16 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
return [
'notices' => [
'pack_updated' => 'Pack has been successfully updated.',
'pack_deleted' => 'Successfully deleted the pack ":name" from the system.',
'pack_created' => 'A new pack was successfully created on the system and is now available for deployment to servers.',
],
];

View File

@ -4,7 +4,7 @@ return [
'daemon_connection_failed' => 'There was an exception while attempting to communicate with the daemon resulting in a HTTP/:code response code. This exception has been logged.',
'node' => [
'servers_attached' => 'A node must have no servers linked to it in order to be deleted.',
'daemon_off_config_updated' => 'The daemon configuration <strong>has been updated</strong>, however there was an error encountered while attempting to automatically update the configuration file on the Daemon. You will need to manually update the configuration file (core.json) for the daemon to apply these changes.',
'daemon_off_config_updated' => 'The daemon configuration <strong>has been updated</strong>, however there was an error encountered while attempting to automatically update the configuration file on the Daemon. You will need to manually update the configuration file (config.yml) for the daemon to apply these changes.',
],
'allocations' => [
'server_using' => 'A server is currently assigned to this allocation. An allocation can only be deleted if no server is currently assigned.',
@ -32,15 +32,6 @@ return [
'invalid_json_provided' => 'The JSON file provided is not in a format that can be recognized.',
],
],
'packs' => [
'delete_has_servers' => 'Cannot delete a pack that is attached to active servers.',
'update_has_servers' => 'Cannot modify the associated option ID when servers are currently attached to a pack.',
'invalid_upload' => 'The file provided does not appear to be valid.',
'invalid_mime' => 'The file provided does not meet the required type :type',
'unreadable' => 'The archive provided could not be opened by the server.',
'zip_extraction' => 'An exception was encountered while attempting to extract the archive provided onto the server.',
'invalid_archive_exception' => 'The pack archive provided appears to be missing a required archive.tar.gz or import.json file in the base directory.',
],
'subusers' => [
'editing_self' => 'Editing your own subuser account is not permitted.',
'user_is_owner' => 'You cannot add the server owner as a subuser for this server.',

View File

@ -8,11 +8,11 @@ export interface FileObject {
size: number;
isFile: boolean;
isSymlink: boolean;
isEditable: boolean;
mimetype: string;
createdAt: Date;
modifiedAt: Date;
isArchiveType: () => boolean;
isEditable: () => boolean;
}
export default async (uuid: string, directory?: string): Promise<FileObject[]> => {

View File

@ -19,7 +19,6 @@ export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({
size: Number(data.attributes.size),
isFile: data.attributes.is_file,
isSymlink: data.attributes.is_symlink,
isEditable: data.attributes.is_editable,
mimetype: data.attributes.mimetype,
createdAt: new Date(data.attributes.created_at),
modifiedAt: new Date(data.attributes.modified_at),
@ -39,6 +38,19 @@ export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({
'application/zip', // .zip
].indexOf(this.mimetype) >= 0;
},
isEditable: function () {
if (this.isArchiveType() || !this.isFile) return false;
const matches = [
'application/jar',
'application/octet-stream',
'inode/directory',
/^image\//,
];
return matches.every(m => !this.mimetype.match(m));
},
});
export const rawDataToServerBackup = ({ attributes }: FractalResponseData): ServerBackup => ({

View File

@ -11,7 +11,7 @@ import styled from 'styled-components/macro';
import * as config from '@/../../tailwind.config.js';
const Navigation = styled.div`
${tw`w-full bg-neutral-900 shadow-md`};
${tw`w-full bg-neutral-900 shadow-md overflow-x-auto`};
& > div {
${tw`mx-auto w-full flex items-center`};

View File

@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import ContentBox from '@/components/elements/ContentBox';
import CreateApiKeyForm from '@/components/dashboard/forms/CreateApiKeyForm';
import getApiKeys, { ApiKey } from '@/api/account/getApiKeys';
@ -8,21 +7,38 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faKey, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import ConfirmationModal from '@/components/elements/ConfirmationModal';
import deleteApiKey from '@/api/account/deleteApiKey';
import { Actions, useStoreActions, useStoreState } from 'easy-peasy';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import FlashMessageRender from '@/components/FlashMessageRender';
import { httpErrorToHuman } from '@/api/http';
import { format } from 'date-fns';
import PageContentBlock from '@/components/elements/PageContentBlock';
import tw from 'twin.macro';
import { breakpoint } from '@/theme';
import styled from 'styled-components/macro';
import GreyRowBox from '@/components/elements/GreyRowBox';
const Container = styled.div`
${tw`flex flex-wrap my-10`};
& > div {
${tw`w-full`};
${breakpoint('md')`
width: calc(50% - 1rem);
`}
${breakpoint('xl')`
${tw`w-auto flex-1`};
`}
}
`;
export default () => {
const [ deleteIdentifier, setDeleteIdentifier ] = useState('');
const [ keys, setKeys ] = useState<ApiKey[]>([]);
const [ loading, setLoading ] = useState(true);
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
useEffect(() => {
clearFlashes('account');
@ -50,16 +66,13 @@ export default () => {
};
return (
<PageContentBlock>
<Helmet>
<title> {name} | API</title>
</Helmet>
<PageContentBlock title={'Account API'}>
<FlashMessageRender byKey={'account'} css={tw`mb-4`}/>
<div css={tw`flex`}>
<ContentBox title={'Create API Key'} css={tw`flex-1`}>
<Container>
<ContentBox title={'Create API Key'}>
<CreateApiKeyForm onKeyCreated={key => setKeys(s => ([ ...s!, key ]))}/>
</ContentBox>
<ContentBox title={'API Keys'} css={tw`ml-10 flex-1`}>
<ContentBox title={'API Keys'} css={tw`mt-8 md:mt-0 md:ml-8`}>
<SpinnerOverlay visible={loading}/>
<ConfirmationModal
visible={!!deleteIdentifier}
@ -111,7 +124,7 @@ export default () => {
))
}
</ContentBox>
</div>
</Container>
</PageContentBlock>
);
};

View File

@ -1,6 +1,4 @@
import * as React from 'react';
import { Helmet } from 'react-helmet';
import { ApplicationStore } from '@/state';
import ContentBox from '@/components/elements/ContentBox';
import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm';
import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm';
@ -9,7 +7,6 @@ import PageContentBlock from '@/components/elements/PageContentBlock';
import tw from 'twin.macro';
import { breakpoint } from '@/theme';
import styled from 'styled-components/macro';
import { useStoreState } from 'easy-peasy';
const Container = styled.div`
${tw`flex flex-wrap my-10`};
@ -28,12 +25,8 @@ const Container = styled.div`
`;
export default () => {
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
return (
<PageContentBlock>
<Helmet>
<title> {name} | Account Overview</title>
</Helmet>
<PageContentBlock title={'Account Overview'}>
<Container>
<ContentBox title={'Update Password'} showFlashes={'account:password'}>
<UpdatePasswordForm/>

View File

@ -1,7 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import { Server } from '@/api/server/getServer';
import { ApplicationStore } from '@/state';
import getServers from '@/api/getServers';
import ServerRow from '@/components/dashboard/ServerRow';
import Spinner from '@/components/elements/Spinner';
@ -20,7 +18,6 @@ export default () => {
const [ page, setPage ] = useState(1);
const { rootAdmin } = useStoreState(state => state.user.data!);
const [ showOnlyAdmin, setShowOnlyAdmin ] = usePersistedState('show_all_servers', false);
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
const { data: servers, error } = useSWR<PaginatedResult<Server>>(
[ '/api/client/servers', showOnlyAdmin, page ],
@ -33,10 +30,7 @@ export default () => {
}, [ error ]);
return (
<PageContentBlock showFlashKey={'dashboard'}>
<Helmet>
<title> {name} | Dashboard</title>
</Helmet>
<PageContentBlock title={'Dashboard'} showFlashKey={'dashboard'}>
{rootAdmin &&
<div css={tw`mb-2 flex justify-end items-center`}>
<p css={tw`uppercase text-xs text-neutral-400 mr-2`}>

View File

@ -55,24 +55,17 @@ export default ({ server, className }: { server: Server; className?: string }) =
return (
<GreyRowBox as={Link} to={`/server/${server.id}`} className={className}>
<div className={'icon'}>
<div className={'icon'} css={tw`hidden md:block`}>
<FontAwesomeIcon icon={faServer}/>
</div>
<div css={tw`flex-1 ml-4`}>
<div css={tw`flex items-center`}>
<div
css={[
tw`w-3 h-3 rounded-full mr-2`,
(!stats?.status || stats?.status === 'offline')
? tw`bg-red-500`
: (stats?.status === 'running' ? tw`bg-green-500` : tw`bg-yellow-500`),
]}
/>
<p css={tw`text-lg`}>{server.name}</p>
</div>
<div css={tw`flex-1 md:ml-4`}>
<p css={tw`text-lg`}>{server.name}</p>
{!!server.description &&
<p css={tw`text-sm text-neutral-300`}>{server.description}</p>
}
</div>
<div css={tw`w-1/4 overflow-hidden`}>
<div css={tw`flex ml-4`}>
<div css={tw`w-48 overflow-hidden self-start hidden lg:block`}>
<div css={tw`flex ml-4 justify-end`}>
<FontAwesomeIcon icon={faEthernet} css={tw`text-neutral-500`}/>
<p css={tw`text-sm text-neutral-400 ml-2`}>
{
@ -83,7 +76,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
</p>
</div>
</div>
<div css={tw`w-1/3 flex items-baseline justify-center relative`}>
<div css={tw`w-1/3 sm:w-1/2 lg:w-1/3 flex items-baseline justify-center relative`}>
{!stats ?
!statsError ?
<Spinner size={'small'}/>
@ -102,7 +95,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
</div>
:
<React.Fragment>
<div css={tw`flex-1 flex ml-4 justify-center`}>
<div css={tw`flex-1 flex md:ml-4 sm:flex hidden justify-center`}>
<FontAwesomeIcon
icon={faMicrochip}
css={[
@ -120,7 +113,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
{stats.cpuUsagePercent} %
</p>
</div>
<div css={tw`flex-1 ml-4`}>
<div css={tw`flex-1 ml-4 sm:block hidden`}>
<div css={tw`flex justify-center`}>
<FontAwesomeIcon
icon={faMemory}
@ -141,7 +134,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
</div>
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {memorylimit}</p>
</div>
<div css={tw`flex-1 ml-4`}>
<div css={tw`flex-1 ml-4 sm:block hidden`}>
<div css={tw`flex justify-center`}>
<FontAwesomeIcon
icon={faHdd}
@ -162,6 +155,18 @@ export default ({ server, className }: { server: Server; className?: string }) =
</div>
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {disklimit}</p>
</div>
<div css={tw`flex-1 flex justify-end sm:hidden`}>
<div css={tw`flex items-end text-right`}>
<div
css={[
tw`w-3 h-3 rounded-full`,
(!stats?.status || stats?.status === 'offline')
? tw`bg-red-500`
: (stats?.status === 'running' ? tw`bg-green-500` : tw`bg-yellow-500`),
]}
/>
</div>
</div>
</React.Fragment>
}
</div>

View File

@ -1,84 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import ace, { Editor } from 'brace';
import styled from 'styled-components/macro';
import tw from 'twin.macro';
import modes from '@/modes';
// @ts-ignore
require('brace/ext/modelist');
require('ayu-ace/mirage');
const EditorContainer = styled.div`
min-height: 16rem;
height: calc(100vh - 20rem);
${tw`relative`};
#editor {
${tw`rounded h-full`};
}
`;
Object.keys(modes).forEach(mode => require(`brace/mode/${mode}`));
const modelist = ace.acequire('ace/ext/modelist');
export interface Props {
style?: React.CSSProperties;
initialContent?: string;
mode: string;
filename?: string;
onModeChanged: (mode: string) => void;
fetchContent: (callback: () => Promise<string>) => void;
onContentSaved: () => void;
}
export default ({ style, initialContent, filename, mode, fetchContent, onContentSaved, onModeChanged }: Props) => {
const [ editor, setEditor ] = useState<Editor>();
const ref = useCallback(node => {
if (node) setEditor(ace.edit('editor'));
}, []);
useEffect(() => {
if (modelist && filename) {
onModeChanged(modelist.getModeForPath(filename).mode.replace(/^ace\/mode\//, ''));
}
}, [ filename ]);
useEffect(() => {
editor && editor.session.setMode(`ace/mode/${mode}`);
}, [ editor, mode ]);
useEffect(() => {
editor && editor.session.setValue(initialContent || '');
}, [ editor, initialContent ]);
useEffect(() => {
if (!editor) {
fetchContent(() => Promise.reject(new Error('no editor session has been configured')));
return;
}
editor.setTheme('ace/theme/ayu-mirage');
editor.$blockScrolling = Infinity;
editor.container.style.lineHeight = '1.375rem';
editor.container.style.fontWeight = '500';
editor.renderer.updateFontSize();
editor.renderer.setShowPrintMargin(false);
editor.session.setTabSize(4);
editor.session.setUseSoftTabs(true);
editor.commands.addCommand({
name: 'Save',
bindKey: { win: 'Ctrl-s', mac: 'Command-s' },
exec: () => onContentSaved(),
});
fetchContent(() => Promise.resolve(editor.session.getValue()));
}, [ editor, fetchContent, onContentSaved ]);
return (
<EditorContainer style={style}>
<div id={'editor'} ref={ref}/>
</EditorContainer>
);
};

Some files were not shown because too many files have changed in this diff Show More