diff --git a/app/Contracts/Repository/DatabaseHostInterface.php b/app/Contracts/Repository/DatabaseHostRepositoryInterface.php similarity index 95% rename from app/Contracts/Repository/DatabaseHostInterface.php rename to app/Contracts/Repository/DatabaseHostRepositoryInterface.php index 2fd26b167..eeb24fdc0 100644 --- a/app/Contracts/Repository/DatabaseHostInterface.php +++ b/app/Contracts/Repository/DatabaseHostRepositoryInterface.php @@ -24,7 +24,7 @@ namespace Pterodactyl\Contracts\Repository; -interface DatabaseHostInterface extends RepositoryInterface +interface DatabaseHostRepositoryInterface extends RepositoryInterface { /** * Delete a database host from the DB if there are no databases using it. diff --git a/app/Contracts/Repository/DatabaseRepositoryInterface.php b/app/Contracts/Repository/DatabaseRepositoryInterface.php new file mode 100644 index 000000000..1f49a54d5 --- /dev/null +++ b/app/Contracts/Repository/DatabaseRepositoryInterface.php @@ -0,0 +1,98 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * 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. + */ + +namespace Pterodactyl\Contracts\Repository; + +interface DatabaseRepositoryInterface extends RepositoryInterface +{ + /** + * Create a new database if it does not already exist on the host with + * the provided details. + * + * @param array $data + * @return mixed + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function createIfNotExists(array $data); + + /** + * Create a new database on a given connection. + * + * @param string $database + * @param null|string $connection + * @return bool + */ + public function createDatabase($database, $connection = null); + + /** + * Create a new database user on a given connection. + * + * @param string $username + * @param string $remote + * @param string $password + * @param null|string $connection + * @return bool + */ + public function createUser($username, $remote, $password, $connection = null); + + /** + * Give a specific user access to a given database. + * + * @param string $database + * @param string $username + * @param string $remote + * @param null|string $connection + * @return bool + */ + public function assignUserToDatabase($database, $username, $remote, $connection = null); + + /** + * Flush the privileges for a given connection. + * + * @param null|string $connection + * @return mixed + */ + public function flush($connection = null); + + /** + * Drop a given database on a specific connection. + * + * @param string $database + * @param null|string $connection + * @return bool + */ + public function dropDatabase($database, $connection = null); + + /** + * Drop a given user on a specific connection. + * + * @param string $username + * @param string $remote + * @param null|string $connection + * @return mixed + */ + public function dropUser($username, $remote, $connection = null); +} diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php index 747fb03f1..2a19810fd 100644 --- a/app/Contracts/Repository/RepositoryInterface.php +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -138,4 +138,11 @@ interface RepositoryInterface * @return mixed */ public function massUpdate(array $where, array $fields); + + /** + * Return all records from the model. + * + * @return mixed + */ + public function all(); } diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php new file mode 100644 index 000000000..5619e3fdd --- /dev/null +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -0,0 +1,38 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * 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. + */ + +namespace Pterodactyl\Contracts\Repository; + +use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; + +interface ServerRepositoryInterface extends RepositoryInterface, SearchableInterface +{ + /** + * Returns a listing of all servers that exist including relationships. + * + * @param int $paginate + * @return mixed + */ + public function getAllServers($paginate); +} diff --git a/app/Contracts/Repository/ServiceRepositoryInterface.php b/app/Contracts/Repository/ServiceRepositoryInterface.php new file mode 100644 index 000000000..def30c956 --- /dev/null +++ b/app/Contracts/Repository/ServiceRepositoryInterface.php @@ -0,0 +1,36 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * 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. + */ + +namespace Pterodactyl\Contracts\Repository; + +interface ServiceRepositoryInterface extends RepositoryInterface +{ + /** + * Return a service or all services with their associated options, variables, and packs. + * + * @param int $id + * @return \Illuminate\Support\Collection + */ + public function getWithOptions($id = null); +} diff --git a/app/Extensions/DynamicDatabaseConnection.php b/app/Extensions/DynamicDatabaseConnection.php index 01308190d..68081df25 100644 --- a/app/Extensions/DynamicDatabaseConnection.php +++ b/app/Extensions/DynamicDatabaseConnection.php @@ -24,7 +24,7 @@ namespace Pterodactyl\Extensions; -use Pterodactyl\Contracts\Repository\DatabaseHostInterface; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; use Pterodactyl\Models\DatabaseHost; use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Config\Repository as ConfigRepository; @@ -46,20 +46,20 @@ class DynamicDatabaseConnection protected $encrypter; /** - * @var \Pterodactyl\Contracts\Repository\DatabaseHostInterface + * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface */ protected $repository; /** * DynamicDatabaseConnection constructor. * - * @param \Illuminate\Config\Repository $config - * @param \Pterodactyl\Contracts\Repository\DatabaseHostInterface $repository - * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \Illuminate\Config\Repository $config + * @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $repository + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter */ public function __construct( ConfigRepository $config, - DatabaseHostInterface $repository, + DatabaseHostRepositoryInterface $repository, Encrypter $encrypter ) { $this->config = $config; diff --git a/app/Http/Controllers/Admin/DatabaseController.php b/app/Http/Controllers/Admin/DatabaseController.php index 0f48987f6..a383558be 100644 --- a/app/Http/Controllers/Admin/DatabaseController.php +++ b/app/Http/Controllers/Admin/DatabaseController.php @@ -28,7 +28,7 @@ use Pterodactyl\Models\Location; use Pterodactyl\Models\DatabaseHost; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Services\Administrative\DatabaseHostService; +use Pterodactyl\Services\Database\DatabaseHostService; use Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest; class DatabaseController extends Controller @@ -49,7 +49,7 @@ class DatabaseController extends Controller protected $locationModel; /** - * @var \Pterodactyl\Services\Administrative\DatabaseHostService + * @var \Pterodactyl\Services\Database\DatabaseHostService */ protected $service; @@ -59,7 +59,7 @@ class DatabaseController extends Controller * @param \Prologue\Alerts\AlertsMessageBag $alert * @param \Pterodactyl\Models\DatabaseHost $hostModel * @param \Pterodactyl\Models\Location $locationModel - * @param \Pterodactyl\Services\Administrative\DatabaseHostService $service + * @param \Pterodactyl\Services\Database\DatabaseHostService $service */ public function __construct( AlertsMessageBag $alert, diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 76715c84c..0c27bd59a 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -24,9 +24,14 @@ namespace Pterodactyl\Http\Controllers\Admin; +use Illuminate\Contracts\Config\Repository as ConfigRepository; use Log; use Alert; use Javascript; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; +use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; use Pterodactyl\Models; use Illuminate\Http\Request; use GuzzleHttp\Exception\TransferException; @@ -39,34 +44,70 @@ use Pterodactyl\Exceptions\DisplayValidationException; class ServersController extends Controller { + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface + */ + protected $databaseRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface + */ + protected $locationRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $serviceRepository; + + public function __construct( + ConfigRepository $config, + DatabaseRepositoryInterface $databaseRepository, + LocationRepositoryInterface $locationRepository, + ServerRepositoryInterface $repository, + ServiceRepositoryInterface $serviceRepository + ) { + $this->config = $config; + $this->databaseRepository = $databaseRepository; + $this->locationRepository = $locationRepository; + $this->repository = $repository; + $this->serviceRepository = $serviceRepository; + } + /** * Display the index page with all servers currently on the system. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function index(Request $request) + public function index() { - $servers = Models\Server::with('node', 'user', 'allocation'); - - if (! is_null($request->input('query'))) { - $servers->search($request->input('query')); - } - return view('admin.servers.index', [ - 'servers' => $servers->paginate(25), + 'servers' => $this->repository->getAllServers( + $this->config->get('pterodactyl.paginate.admin.servers') + ), ]); } /** * Display create new server page. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View + * + * @throws \Exception */ - public function create(Request $request) + public function create() { - $services = Models\Service::with('options.packs', 'options.variables')->get(); + $services = $this->serviceRepository->getWithOptions(); + Javascript::put([ 'services' => $services->map(function ($item) { return array_merge($item->toArray(), [ @@ -76,7 +117,7 @@ class ServersController extends Controller ]); return view('admin.servers.new', [ - 'locations' => Models\Location::all(), + 'locations' => $this->locationRepository->all(), 'services' => $services, ]); } @@ -115,7 +156,7 @@ class ServersController extends Controller * Returns a tree of all avaliable nodes in a given location. * * @param \Illuminate\Http\Request $request - * @return array + * @return \Illuminate\Support\Collection */ public function nodes(Request $request) { diff --git a/app/Models/Database.php b/app/Models/Database.php index 20ad3c1b0..a7ee970e2 100644 --- a/app/Models/Database.php +++ b/app/Models/Database.php @@ -25,9 +25,13 @@ namespace Pterodactyl\Models; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; class Database extends Model { + use Eloquence, Validable; + /** * The table associated with the model. * @@ -61,6 +65,22 @@ class Database extends Model 'database_host_id' => 'integer', ]; + protected static $applicationRules = [ + 'server_id' => 'required', + 'database_host_id' => 'required', + 'database' => 'required', + 'remote' => 'required', + ]; + + protected static $dataIntegrityRules = [ + 'server_id' => 'numeric|exists:servers,id', + 'database_host_id' => 'exists:database_hosts,id', + 'database' => 'string|alpha_dash|between:3,100', + 'username' => 'string|alpha_dash|between:3,100', + 'remote' => 'string|regex:/^[0-9%.]{1,15}$/', + 'password' => 'string', + ]; + /** * Gets the host database server associated with a database. * diff --git a/app/Models/DatabaseHost.php b/app/Models/DatabaseHost.php index a6599cc46..54a16d3ec 100644 --- a/app/Models/DatabaseHost.php +++ b/app/Models/DatabaseHost.php @@ -24,12 +24,13 @@ namespace Pterodactyl\Models; -use Watson\Validating\ValidatingTrait; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; class DatabaseHost extends Model { - use ValidatingTrait; + use Eloquence, Validable; /** * The table associated with the model. @@ -65,18 +66,31 @@ class DatabaseHost extends Model 'node_id' => 'integer', ]; + /** + * Application validation rules. + * + * @var array + */ + protected static $applicationRules = [ + 'name' => 'required', + 'host' => 'required', + 'port' => 'required', + 'username' => 'required', + 'node_id' => 'sometimes|required', + ]; + /** * Validation rules to assign to this model. * * @var array */ - protected $rules = [ - 'name' => 'required|string|max:255', - 'host' => 'required|ip|unique:database_hosts,host', - 'port' => 'required|numeric|between:1,65535', - 'username' => 'required|string|max:32', - 'password' => 'sometimes|nullable|string', - 'node_id' => 'sometimes|required|nullable|exists:nodes,id', + protected static $dataIntegrityRules = [ + 'name' => 'string|max:255', + 'host' => 'ip|unique:database_hosts,host', + 'port' => 'numeric|between:1,65535', + 'username' => 'string|max:32', + 'password' => 'nullable|string', + 'node_id' => 'nullable|exists:nodes,id', ]; /** diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index 9d0f59b35..189ca6eb6 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -27,12 +27,18 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; -use Pterodactyl\Contracts\Repository\DatabaseHostInterface; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository; use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; +use Pterodactyl\Repositories\Eloquent\DatabaseRepository; use Pterodactyl\Repositories\Eloquent\LocationRepository; +use Pterodactyl\Repositories\Eloquent\ServerRepository; +use Pterodactyl\Repositories\Eloquent\ServiceRepository; use Pterodactyl\Repositories\Eloquent\UserRepository; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; @@ -45,8 +51,11 @@ class RepositoryServiceProvider extends ServiceProvider { $this->app->bind(ApiKeyRepositoryInterface::class, ApiKeyRepository::class); $this->app->bind(ApiPermissionRepositoryInterface::class, ApiPermissionRepository::class); - $this->app->bind(DatabaseHostInterface::class, DatabaseHostRepository::class); + $this->app->bind(DatabaseRepositoryInterface::class, DatabaseRepository::class); + $this->app->bind(DatabaseHostRepositoryInterface::class, DatabaseHostRepository::class); $this->app->bind(LocationRepositoryInterface::class, LocationRepository::class); + $this->app->bind(ServerRepositoryInterface::class, ServerRepository::class); + $this->app->bind(ServiceRepositoryInterface::class, ServiceRepository::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class); } } diff --git a/app/Repositories/Eloquent/Attributes/SearchableRepository.php b/app/Repositories/Eloquent/Attributes/SearchableRepository.php new file mode 100644 index 000000000..5965d1f5b --- /dev/null +++ b/app/Repositories/Eloquent/Attributes/SearchableRepository.php @@ -0,0 +1,51 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * 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. + */ + +namespace Pterodactyl\Repositories\Eloquent\Attributes; + +use Pterodactyl\Repositories\Eloquent\EloquentRepository; +use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; + +abstract class SearchableRepository extends EloquentRepository implements SearchableInterface +{ + /** + * @var bool|string + */ + protected $searchTerm = false; + + /** + * {@inheritdoc} + */ + public function search($term) + { + if (empty($term)) { + return $this; + } + + $clone = clone $this; + $clone->searchTerm = $term; + + return $clone; + } +} diff --git a/app/Repositories/Eloquent/DatabaseHostRepository.php b/app/Repositories/Eloquent/DatabaseHostRepository.php index 76c572123..8a4d7b3c7 100644 --- a/app/Repositories/Eloquent/DatabaseHostRepository.php +++ b/app/Repositories/Eloquent/DatabaseHostRepository.php @@ -24,12 +24,12 @@ namespace Pterodactyl\Repositories\Eloquent; -use Pterodactyl\Contracts\Repository\DatabaseHostInterface; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Models\DatabaseHost; -class DatabaseHostRepository extends EloquentRepository implements DatabaseHostInterface +class DatabaseHostRepository extends EloquentRepository implements DatabaseHostRepositoryInterface { /** * {@inheritdoc} diff --git a/app/Repositories/Eloquent/DatabaseRepository.php b/app/Repositories/Eloquent/DatabaseRepository.php new file mode 100644 index 000000000..3a2a589c2 --- /dev/null +++ b/app/Repositories/Eloquent/DatabaseRepository.php @@ -0,0 +1,155 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * 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. + */ + +namespace Pterodactyl\Repositories\Eloquent; + +use Pterodactyl\Models\Database; +use Illuminate\Foundation\Application; +use Illuminate\Database\ConnectionResolver; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; + +class DatabaseRepository extends EloquentRepository implements DatabaseRepositoryInterface +{ + /** + * @var \Illuminate\Database\ConnectionResolverInterface + */ + protected $connection; + + /** + * DatabaseRepository constructor. + * + * @param \Illuminate\Foundation\Application $application + * @param \Illuminate\Database\ConnectionResolver $connection + */ + public function __construct( + Application $application, + ConnectionResolver $connection + ) { + parent::__construct($application); + + $this->connection = $connection; + } + + /** + * {@inheritdoc} + */ + public function model() + { + return Database::class; + } + + /** + * {@inheritdoc} + * @return bool|\Illuminate\Database\Eloquent\Model + */ + public function createIfNotExists(array $data) + { + $instance = $this->getBuilder()->where([ + ['server_id', $data['server_id']], + ['database_host_id', $data['database_host_id']], + ['database', $data['database']], + ])->count(); + + if ($instance > 0) { + throw new DisplayException('A database with those details already exists for the specified server.'); + } + + return $this->create($data); + } + + /** + * {@inheritdoc} + */ + public function createDatabase($database, $connection = null) + { + return $this->runStatement( + sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $database), $connection + ); + } + + /** + * {@inheritdoc} + */ + public function createUser($username, $remote, $password, $connection = null) + { + return $this->runStatement( + sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', $username, $remote, $password), $connection + ); + } + + /** + * {@inheritdoc} + */ + public function assignUserToDatabase($database, $username, $remote, $connection = null) + { + return $this->runStatement( + sprintf( + 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX ON `%s`.* TO `%s`@`%s`', + $database, $username, $remote + ), + $connection + ); + } + + /** + * {@inheritdoc} + */ + public function flush($connection = null) + { + return $this->runStatement('FLUSH PRIVILEGES', $connection); + } + + /** + * {@inheritdoc} + */ + public function dropDatabase($database, $connection = null) + { + return $this->runStatement( + sprintf('DROP DATABASE IF EXISTS `%s`', $database), $connection + ); + } + + /** + * {@inheritdoc} + */ + public function dropUser($username, $remote, $connection = null) + { + return $this->runStatement( + sprintf('DROP USER IF EXISTS `%s`@`%s`', $username, $remote), $connection + ); + } + + /** + * Run the provided statement against the database on a given connection. + * + * @param string $statement + * @param null|string $connection + * @return bool + */ + protected function runStatement($statement, $connection = null) + { + return $this->connection->connection($connection)->statement($statement); + } +} diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index b6af22f75..793291bb9 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -152,4 +152,12 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf { // TODO: Implement massUpdate() method. } + + /** + * {@inheritdoc} + */ + public function all() + { + return $this->getBuilder()->get($this->getColumns()); + } } diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php new file mode 100644 index 000000000..4f605b64d --- /dev/null +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -0,0 +1,54 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * 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. + */ + +namespace Pterodactyl\Repositories\Eloquent; + +use Pterodactyl\Models\Server; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; + +class ServerRepository extends SearchableRepository implements ServerRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return Server::class; + } + + /** + * {@inheritdoc} + */ + public function getAllServers($paginate = 25) + { + $instance = $this->getBuilder()->with('node', 'user', 'allocation'); + + if ($this->searchTerm) { + $instance->search($this->searchTerm); + } + + return $instance->paginate($paginate); + } +} diff --git a/app/Repositories/Eloquent/ServiceRepository.php b/app/Repositories/Eloquent/ServiceRepository.php new file mode 100644 index 000000000..6fd8a4bc1 --- /dev/null +++ b/app/Repositories/Eloquent/ServiceRepository.php @@ -0,0 +1,60 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * 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. + */ + +namespace Pterodactyl\Repositories\Eloquent; + +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; +use Pterodactyl\Models\Service; + +class ServiceRepository extends EloquentRepository implements ServiceRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return Service::class; + } + + /** + * {@inheritdoc} + */ + public function getWithOptions($id = null) + { + $instance = $this->getBuilder()->with('options.packs', 'options.variables'); + + if (! is_null($id)) { + $instance = $instance->find($id, $this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + return $instance; + } + + return $instance->get($this->getColumns()); + } +} diff --git a/app/Repositories/Eloquent/UserRepository.php b/app/Repositories/Eloquent/UserRepository.php index 00776bb70..4ad33419e 100644 --- a/app/Repositories/Eloquent/UserRepository.php +++ b/app/Repositories/Eloquent/UserRepository.php @@ -30,19 +30,15 @@ use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Models\User; +use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; -class UserRepository extends EloquentRepository implements UserRepositoryInterface +class UserRepository extends SearchableRepository implements UserRepositoryInterface { /** * @var \Illuminate\Contracts\Config\Repository */ protected $config; - /** - * @var bool|array - */ - protected $searchTerm = false; - /** * UserRepository constructor. * @@ -64,21 +60,6 @@ class UserRepository extends EloquentRepository implements UserRepositoryInterfa return User::class; } - /** - * {@inheritdoc} - */ - public function search($term) - { - if (empty($term)) { - return $this; - } - - $clone = clone $this; - $clone->searchTerm = $term; - - return $clone; - } - /** * {@inheritdoc} */ diff --git a/app/Repositories/Old/DatabaseRepository.php b/app/Repositories/Old/DatabaseRepository.php index f7f428a6f..1e4bc75af 100644 --- a/app/Repositories/Old/DatabaseRepository.php +++ b/app/Repositories/Old/DatabaseRepository.php @@ -170,115 +170,4 @@ class DatabaseRepository $database->delete(); }); } - - /** - * Deletes a database host from the system if it has no associated databases. - * - * @param int $id - * @return void - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function delete($id) - { - $host = DatabaseHost::withCount('databases')->findOrFail($id); - - if ($host->databases_count > 0) { - throw new DisplayException('You cannot delete a database host that has active databases attached to it.'); - } - - $host->delete(); - } - - /** - * Adds a new Database Host to the system. - * - * @param array $data - * @return \Pterodactyl\Models\DatabaseHost - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function add(array $data) - { - if (isset($data['host'])) { - $data['host'] = gethostbyname($data['host']); - } - - $validator = Validator::make($data, [ - 'name' => 'required|string|max:255', - 'host' => 'required|ip|unique:database_hosts,host', - 'port' => 'required|numeric|between:1,65535', - 'username' => 'required|string|max:32', - 'password' => 'required|string', - 'node_id' => 'sometimes|required|exists:nodes,id', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - return DB::transaction(function () use ($data) { - $host = new DatabaseHost; - $host->password = Crypt::encrypt($data['password']); - - $host->fill([ - 'name' => $data['name'], - 'host' => $data['host'], - 'port' => $data['port'], - 'username' => $data['username'], - 'max_databases' => null, - 'node_id' => (isset($data['node_id'])) ? $data['node_id'] : null, - ])->save(); - - // Allows us to check that we can connect to things. - $host->setDynamicConnection(); - DB::connection('dynamic')->select('SELECT 1 FROM dual'); - - return $host; - }); - } - - /** - * Updates a Database Host on the system. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\DatabaseHost - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function update($id, array $data) - { - $host = DatabaseHost::findOrFail($id); - - if (isset($data['host'])) { - $data['host'] = gethostbyname($data['host']); - } - - $validator = Validator::make($data, [ - 'name' => 'sometimes|required|string|max:255', - 'host' => 'sometimes|required|ip|unique:database_hosts,host,' . $host->id, - 'port' => 'sometimes|required|numeric|between:1,65535', - 'username' => 'sometimes|required|string|max:32', - 'password' => 'sometimes|required|string', - 'node_id' => 'sometimes|required|exists:nodes,id', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - return DB::transaction(function () use ($data, $host) { - if (isset($data['password'])) { - $host->password = Crypt::encrypt($data['password']); - } - $host->fill($data)->save(); - - // Check that we can still connect with these details. - $host->setDynamicConnection(); - DB::connection('dynamic')->select('SELECT 1 FROM dual'); - - return $host; - }); - } } diff --git a/app/Services/Database/CreationService.php b/app/Services/Database/CreationService.php new file mode 100644 index 000000000..b48874537 --- /dev/null +++ b/app/Services/Database/CreationService.php @@ -0,0 +1,188 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * 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. + */ + +namespace Pterodactyl\Services\Database; + +use Illuminate\Database\ConnectionResolver; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Contracts\Encryption\Encrypter; +use Pterodactyl\Extensions\DynamicDatabaseConnection; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; + +class CreationService +{ + /** + * @var \Illuminate\Database\ConnectionResolver + */ + protected $connection; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $database; + + /** + * @var \Pterodactyl\Extensions\DynamicDatabaseConnection + */ + protected $dynamic; + + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + protected $encrypter; + + /** + * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface + */ + protected $repository; + + /** + * CreationService constructor. + * + * @param \Illuminate\Database\ConnectionInterface $database + * @param \Illuminate\Database\ConnectionResolver $connection + * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic + * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + */ + public function __construct( + ConnectionInterface $database, + ConnectionResolver $connection, + DynamicDatabaseConnection $dynamic, + DatabaseRepositoryInterface $repository, + Encrypter $encrypter + ) { + $this->connection = $connection; + $this->database = $database; + $this->dynamic = $dynamic; + $this->encrypter = $encrypter; + $this->repository = $repository; + } + + /** + * Create a new database that is linked to a specific host. + * + * @param array $data + * @return \Illuminate\Database\Eloquent\Model + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function create(array $data) + { + $data['database'] = sprintf('d%d_%s', $data['server_id'], $data['database']); + $data['username'] = sprintf('u%d_%s', $data['server_id'], str_random(10)); + $data['password'] = $this->encrypter->encrypt(str_random(16)); + + $this->database->beginTransaction(); + try { + $database = $this->repository->createIfNotExists($data); + $this->dynamic->set('dynamic', $data['database_host_id']); + + $this->repository->createDatabase($database->database, 'dynamic'); + $this->repository->createUser( + $database->username, $database->remote, $this->encrypter->decrypt($database->password), 'dynamic' + ); + $this->repository->assignUserToDatabase( + $database->database, $database->username, $database->remote, 'dynamic' + ); + $this->repository->flush('dynamic'); + + $this->database->commit(); + } catch (\Exception $ex) { + try { + if (isset($database)) { + $this->repository->dropDatabase($database->database, 'dynamic'); + $this->repository->dropUser($database->username, $database->remote, 'dynamic'); + $this->repository->flush('dynamic'); + } + } catch (\Exception $ex) { + // ignore an exception + } + + $this->database->rollBack(); + throw $ex; + } + + return $database; + } + + /** + * Change the password for a specific user and database combination. + * + * @param int $id + * @param string $password + * @return bool + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function changePassword($id, $password) + { + $database = $this->repository->find($id); + $this->dynamic->set('dynamic', $database->database_host_id); + + $this->database->beginTransaction(); + + try { + $updated = $this->repository->withoutFresh()->update($id, [ + 'password' => $this->encrypter->encrypt($password), + ]); + + $this->repository->dropUser($database->username, $database->remote, 'dynamic'); + $this->repository->createUser($database->username, $database->remote, $password); + $this->repository->assignUserToDatabase( + $database->database, $database->username, $database->remote, 'dynamic' + ); + $this->repository->flush(); + + $this->database->commit(); + } catch (\Exception $ex) { + $this->database->rollBack(); + throw $ex; + } + + return $updated; + } + + /** + * Delete a database from the given host server. + * + * @param int $id + * @return bool|null + */ + public function delete($id) + { + $database = $this->repository->find($id); + $this->dynamic->set('dynamic', $database->database_host_id); + + $this->repository->dropDatabase($database->database, 'dynamic'); + $this->repository->dropUser($database->username, $database->remote, 'dynamic'); + $this->repository->flush('dynamic'); + + return $this->repository->delete($id); + } +} diff --git a/app/Services/Database/DatabaseHostService.php b/app/Services/Database/DatabaseHostService.php index b0dbb157d..ad54a1cdb 100644 --- a/app/Services/Database/DatabaseHostService.php +++ b/app/Services/Database/DatabaseHostService.php @@ -27,7 +27,7 @@ namespace Pterodactyl\Services\Database; use Illuminate\Database\DatabaseManager; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; -use Pterodactyl\Contracts\Repository\DatabaseHostInterface; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; class DatabaseHostService { @@ -47,20 +47,20 @@ class DatabaseHostService protected $encrypter; /** - * @var \Pterodactyl\Contracts\Repository\DatabaseHostInterface + * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface */ protected $repository; /** * DatabaseHostService constructor. * - * @param \Pterodactyl\Contracts\Repository\DatabaseHostInterface $repository - * @param \Illuminate\Database\DatabaseManager $database - * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic - * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $repository + * @param \Illuminate\Database\DatabaseManager $database + * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter */ public function __construct( - DatabaseHostInterface $repository, + DatabaseHostRepositoryInterface $repository, DatabaseManager $database, DynamicDatabaseConnection $dynamic, Encrypter $encrypter diff --git a/config/pterodactyl.php b/config/pterodactyl.php index d1e74ae0c..29d8bbe72 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -40,6 +40,7 @@ return [ 'servers' => env('APP_PAGINATE_FRONT_SERVERS', 15), ], 'admin' => [ + 'servers' => env('APP_PAGINATE_ADMIN_SERVERS', 25), 'users' => env('APP_PAGINATE_ADMIN_USERS', 25), ], 'api' => [ diff --git a/tests/Unit/Services/Database/DatabaseHostServiceTest.php b/tests/Unit/Services/Database/DatabaseHostServiceTest.php index 55a97ae33..e728d467b 100644 --- a/tests/Unit/Services/Database/DatabaseHostServiceTest.php +++ b/tests/Unit/Services/Database/DatabaseHostServiceTest.php @@ -29,7 +29,7 @@ use Tests\TestCase; use Illuminate\Database\DatabaseManager; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; -use Pterodactyl\Contracts\Repository\DatabaseHostInterface; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; use Pterodactyl\Services\Database\DatabaseHostService; class DatabaseHostServiceTest extends TestCase @@ -50,7 +50,7 @@ class DatabaseHostServiceTest extends TestCase protected $encrypter; /** - * @var \Pterodactyl\Contracts\Repository\DatabaseHostInterface + * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface */ protected $repository; @@ -69,7 +69,7 @@ class DatabaseHostServiceTest extends TestCase $this->database = m::mock(DatabaseManager::class); $this->dynamic = m::mock(DynamicDatabaseConnection::class); $this->encrypter = m::mock(Encrypter::class); - $this->repository = m::mock(DatabaseHostInterface::class); + $this->repository = m::mock(DatabaseHostRepositoryInterface::class); $this->service = new DatabaseHostService( $this->repository,