diff --git a/app/Extensions/DynamicDatabaseConnection.php b/app/Extensions/DynamicDatabaseConnection.php new file mode 100644 index 000000000..862336f41 --- /dev/null +++ b/app/Extensions/DynamicDatabaseConnection.php @@ -0,0 +1,93 @@ +. + * + * 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\Extensions; + +use Illuminate\Config\Repository as ConfigRepository; +use Illuminate\Contracts\Encryption\Encrypter; +use Pterodactyl\Models\DatabaseHost; + +class DynamicDatabaseConnection +{ + const DB_CHARSET = 'utf8'; + const DB_COLLATION = 'utf8_unicode_ci'; + const DB_DRIVER = 'mysql'; + + /** + * @var \Illuminate\Config\Repository + */ + protected $config; + + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + protected $encrypter; + + /** + * @var \Pterodactyl\Models\DatabaseHost + */ + protected $model; + + /** + * DynamicDatabaseConnection constructor. + * + * @param \Illuminate\Config\Repository $config + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \Pterodactyl\Models\DatabaseHost $model + */ + public function __construct( + ConfigRepository $config, + Encrypter $encrypter, + DatabaseHost $model + ) { + $this->config = $config; + $this->encrypter = $encrypter; + $this->model = $model; + } + + /** + * Adds a dynamic database connection entry to the runtime config. + * + * @param string $connection + * @param \Pterodactyl\Models\DatabaseHost|int $host + * @param string $database + */ + public function set($connection, $host, $database = 'mysql') + { + if (! $host instanceof DatabaseHost) { + $host = $this->model->findOrFail($host); + } + + $this->config->set('database.connections.' . $connection, [ + 'driver' => self::DB_DRIVER, + 'host' => $host->host, + 'port' => $host->port, + 'database' => $database, + 'username' => $host->username, + 'password' => $this->encrypter->decrypt($host->password), + 'charset' => self::DB_CHARSET, + 'collation' => self::DB_COLLATION, + ]); + } +} diff --git a/app/Http/Controllers/Admin/BaseController.php b/app/Http/Controllers/Admin/BaseController.php index 8c5719827..d4dd5dc82 100644 --- a/app/Http/Controllers/Admin/BaseController.php +++ b/app/Http/Controllers/Admin/BaseController.php @@ -24,21 +24,35 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Alert; -use Settings; -use Validator; -use Illuminate\Http\Request; +use Krucas\Settings\Settings; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Http\Requests\Admin\BaseFormRequest; class BaseController extends Controller { + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Krucas\Settings\Settings + */ + protected $settings; + + public function __construct(AlertsMessageBag $alert, Settings $settings) + { + $this->alert = $alert; + $this->settings = $settings; + } + /** * Return the admin index view. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function getIndex(Request $request) + public function getIndex() { return view('admin.index'); } @@ -46,10 +60,9 @@ class BaseController extends Controller /** * Return the admin settings view. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function getSettings(Request $request) + public function getSettings() { return view('admin.settings'); } @@ -57,24 +70,14 @@ class BaseController extends Controller /** * Handle settings post request. * - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Http\Requests\Admin\BaseFormRequest $request * @return \Illuminate\Http\RedirectResponse */ - public function postSettings(Request $request) + public function postSettings(BaseFormRequest $request) { - $validator = Validator::make($request->all(), [ - 'company' => 'required|between:1,256', - // 'default_language' => 'required|alpha_dash|min:2|max:5', - ]); + $this->settings->set('company', $request->input('company')); - if ($validator->fails()) { - return redirect()->route('admin.settings')->withErrors($validator->errors())->withInput(); - } - - Settings::set('company', $request->input('company')); - // Settings::set('default_language', $request->input('default_language')); - - Alert::success('Settings have been successfully updated.')->flash(); + $this->alert->success('Settings have been successfully updated.')->flash(); return redirect()->route('admin.settings'); } diff --git a/app/Http/Controllers/Admin/DatabaseController.php b/app/Http/Controllers/Admin/DatabaseController.php index 6b4e12a1d..94d60a0c6 100644 --- a/app/Http/Controllers/Admin/DatabaseController.php +++ b/app/Http/Controllers/Admin/DatabaseController.php @@ -24,112 +24,144 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Log; -use Alert; -use Illuminate\Http\Request; -use Pterodactyl\Models\Database; use Pterodactyl\Models\Location; use Pterodactyl\Models\DatabaseHost; -use Pterodactyl\Exceptions\DisplayException; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\DatabaseRepository; -use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Services\DatabaseHostService; +use Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest; class DatabaseController extends Controller { + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Pterodactyl\Models\DatabaseHost + */ + protected $hostModel; + + /** + * @var \Pterodactyl\Models\Location + */ + protected $locationModel; + + /** + * @var \Pterodactyl\Services\DatabaseHostService + */ + protected $service; + + /** + * DatabaseController constructor. + * + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Models\DatabaseHost $hostModel + * @param \Pterodactyl\Models\Location $locationModel + * @param \Pterodactyl\Services\DatabaseHostService $service + */ + public function __construct( + AlertsMessageBag $alert, + DatabaseHost $hostModel, + Location $locationModel, + DatabaseHostService $service + ) { + $this->alert = $alert; + $this->hostModel = $hostModel; + $this->locationModel = $locationModel; + $this->service = $service; + } + /** * Display database host index. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function index(Request $request) + public function index() { return view('admin.databases.index', [ - 'locations' => Location::with('nodes')->get(), - 'hosts' => DatabaseHost::withCount('databases')->with('node')->get(), + 'locations' => $this->locationModel->with('nodes')->get(), + 'hosts' => $this->hostModel->withCount('databases')->with('node')->get(), ]); } /** * Display database host to user. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\DatabaseHost $host * @return \Illuminate\View\View */ - public function view(Request $request, $id) + public function view(DatabaseHost $host) { + $host->load('databases.server'); + return view('admin.databases.view', [ - 'locations' => Location::with('nodes')->get(), - 'host' => DatabaseHost::with('databases.server')->findOrFail($id), + 'locations' => $this->locationModel->with('nodes')->get(), + 'host' => $host, ]); } /** - * Handle post request to create database host. + * Handle request to create a new database host. * - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest $request * @return \Illuminate\Http\RedirectResponse + * + * @throws \Throwable */ - public function create(Request $request) + public function create(DatabaseHostFormRequest $request) { - $repo = new DatabaseRepository; - try { - $host = $repo->add($request->intersect([ - 'name', 'username', 'password', - 'host', 'port', 'node_id', - ])); - Alert::success('Successfully created new database host on the system.')->flash(); + $host = $this->service->create($request->normalize()); + $this->alert->success('Successfully created a new database host on the system.')->flash(); return redirect()->route('admin.databases.view', $host->id); } catch (\PDOException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.databases')->withErrors(json_decode($ex->getMessage())); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error was encountered while trying to process this request. This error has been logged.')->flash(); + $this->alert->danger($ex->getMessage())->flash(); } return redirect()->route('admin.databases'); } /** - * Handle post request to update a database host. + * Handle updating database host. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest $request + * @param \Pterodactyl\Models\DatabaseHost $host * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException */ - public function update(Request $request, $id) + public function update(DatabaseHostFormRequest $request, DatabaseHost $host) { - $repo = new DatabaseRepository; - - try { - if ($request->input('action') !== 'delete') { - $host = $repo->update($id, $request->intersect([ - 'name', 'username', 'password', - 'host', 'port', 'node_id', - ])); - Alert::success('Database host was updated successfully.')->flash(); - } else { - $repo->delete($id); - - return redirect()->route('admin.databases'); - } - } catch (\PDOException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.databases.view', $id)->withErrors(json_decode($ex->getMessage())); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error was encountered while trying to process this request. This error has been logged.')->flash(); + if ($request->input('action') === 'delete') { + return $this->delete($host); } - return redirect()->route('admin.databases.view', $id); + try { + $host = $this->service->update($host->id, $request->normalize()); + $this->alert->success('Database host was updated successfully.')->flash(); + } catch (\PDOException $ex) { + $this->alert->danger($ex->getMessage())->flash(); + } + + return redirect()->route('admin.databases.view', $host->id); + } + + /** + * Handle request to delete a database host. + * + * @param \Pterodactyl\Models\DatabaseHost $host + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function delete(DatabaseHost $host) + { + $this->service->delete($host->id); + $this->alert->success('The requested database host has been deleted from the system.')->flash(); + + return redirect()->route('admin.databases'); } } diff --git a/app/Http/Requests/Admin/BaseFormRequest.php b/app/Http/Requests/Admin/BaseFormRequest.php new file mode 100644 index 000000000..0d9884254 --- /dev/null +++ b/app/Http/Requests/Admin/BaseFormRequest.php @@ -0,0 +1,35 @@ +. + * + * 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\Http\Requests\Admin; + +class BaseFormRequest extends AdminFormRequest +{ + public function rules() + { + return [ + 'company' => 'required|between:1,256', + ]; + } +} diff --git a/app/Http/Requests/Admin/DatabaseHostFormRequest.php b/app/Http/Requests/Admin/DatabaseHostFormRequest.php new file mode 100644 index 000000000..42052f5bd --- /dev/null +++ b/app/Http/Requests/Admin/DatabaseHostFormRequest.php @@ -0,0 +1,49 @@ +. + * + * 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\Http\Requests\Admin; + +use Pterodactyl\Models\DatabaseHost; + +class DatabaseHostFormRequest extends AdminFormRequest +{ + /** + * @return mixed + */ + public function rules() + { + $this->merge([ + 'node_id' => ($this->input('node_id') < 1) ? null : $this->input('node_id'), + 'host' => gethostbyname($this->input('host')), + ]); + + $rules = app()->make(DatabaseHost::class)->getRules(); + + if ($this->method() === 'PATCH') { + $rules['host'] = $rules['host'] . ',' . $this->route()->parameter('host')->id; + } + + return $rules; + } +} diff --git a/app/Models/DatabaseHost.php b/app/Models/DatabaseHost.php index 165a99c5a..a6599cc46 100644 --- a/app/Models/DatabaseHost.php +++ b/app/Models/DatabaseHost.php @@ -24,12 +24,13 @@ namespace Pterodactyl\Models; -use Crypt; -use Config; +use Watson\Validating\ValidatingTrait; use Illuminate\Database\Eloquent\Model; class DatabaseHost extends Model { + use ValidatingTrait; + /** * The table associated with the model. * @@ -65,24 +66,18 @@ class DatabaseHost extends Model ]; /** - * Sets the database connection name with the details of the host. + * Validation rules to assign to this model. * - * @param string $connection - * @return void + * @var array */ - public function setDynamicConnection($connection = 'dynamic') - { - Config::set('database.connections.' . $connection, [ - 'driver' => 'mysql', - 'host' => $this->host, - 'port' => $this->port, - 'database' => 'mysql', - 'username' => $this->username, - 'password' => Crypt::decrypt($this->password), - 'charset' => 'utf8', - 'collation' => 'utf8_unicode_ci', - ]); - } + 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', + ]; /** * Gets the node associated with a database host. diff --git a/app/Models/Location.php b/app/Models/Location.php index bb1529403..e7a7cd3f6 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -46,7 +46,7 @@ class Location extends Model protected $guarded = ['id', 'created_at', 'updated_at']; /** - * Validation rules to apply when attempting to save a model to the DB. + * Validation rules to apply to this model. * * @var array */ diff --git a/app/Services/DatabaseHostService.php b/app/Services/DatabaseHostService.php new file mode 100644 index 000000000..ed2850201 --- /dev/null +++ b/app/Services/DatabaseHostService.php @@ -0,0 +1,151 @@ +. + * + * 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; + +use Pterodactyl\Models\DatabaseHost; +use Illuminate\Database\DatabaseManager; +use Pterodactyl\Exceptions\DisplayException; +use Illuminate\Contracts\Encryption\Encrypter; +use Pterodactyl\Extensions\DynamicDatabaseConnection; + +class DatabaseHostService +{ + /** + * @var \Illuminate\Database\DatabaseManager + */ + protected $database; + + /** + * @var \Pterodactyl\Extensions\DynamicDatabaseConnection + */ + protected $dynamic; + + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + protected $encrypter; + + /** + * @var \Pterodactyl\Models\DatabaseHost + */ + protected $model; + + /** + * DatabaseHostService constructor. + * + * @param \Illuminate\Database\DatabaseManager $database + * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \Pterodactyl\Models\DatabaseHost $model + */ + public function __construct( + DatabaseManager $database, + DynamicDatabaseConnection $dynamic, + Encrypter $encrypter, + DatabaseHost $model + ) { + $this->database = $database; + $this->dynamic = $dynamic; + $this->encrypter = $encrypter; + $this->model = $model; + } + + /** + * Create a new database host and persist it to the database. + * + * @param array $data + * @return \Pterodactyl\Models\DatabaseHost + * + * @throws \Throwable + * @throws \PDOException + */ + public function create(array $data) + { + $instance = $this->model->newInstance(); + $instance->password = $this->encrypter->encrypt(array_get($data, 'password')); + + $instance->fill([ + 'name' => array_get($data, 'name'), + 'host' => array_get($data, 'host'), + 'port' => array_get($data, 'port'), + 'username' => array_get($data, 'username'), + 'max_databases' => null, + 'node_id' => array_get($data, 'node_id'), + ]); + + // Check Access + $this->dynamic->set('dynamic', $instance); + $this->database->connection('dynamic')->select('SELECT 1 FROM dual'); + + $instance->saveOrFail(); + + return $instance; + } + + /** + * Update a database host and persist to the database. + * + * @param int $id + * @param array $data + * @return mixed + * + * @throws \PDOException + */ + public function update($id, array $data) + { + $model = $this->model->findOrFail($id); + + if (! empty(array_get($data, 'password'))) { + $model->password = $this->encrypter->encrypt($data['password']); + } + + $model->fill($data); + $this->dynamic->set('dynamic', $model); + $this->database->connection('dynamic')->select('SELECT 1 FROM dual'); + + $model->saveOrFail(); + + return $model; + } + + /** + * Delete a database host if it has no active databases attached to it. + * + * @param int $id + * @return bool|null + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function delete($id) + { + $model = $this->model->withCount('databases')->findOrFail($id); + + if ($model->databases_count > 0) { + throw new DisplayException('Cannot delete a database host that has active databases attached to it.'); + } + + return $model->delete(); + } +} diff --git a/resources/themes/pterodactyl/admin/databases/view.blade.php b/resources/themes/pterodactyl/admin/databases/view.blade.php index ab331921c..bade35b42 100644 --- a/resources/themes/pterodactyl/admin/databases/view.blade.php +++ b/resources/themes/pterodactyl/admin/databases/view.blade.php @@ -93,6 +93,7 @@ diff --git a/routes/admin.php b/routes/admin.php index 6d2730ce1..5d67b45bd 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -49,10 +49,10 @@ Route::group(['prefix' => 'locations'], function () { */ Route::group(['prefix' => 'databases'], function () { Route::get('/', 'DatabaseController@index')->name('admin.databases'); - Route::get('/view/{id}', 'DatabaseController@view')->name('admin.databases.view'); + Route::get('/view/{host}', 'DatabaseController@view')->name('admin.databases.view'); Route::post('/', 'DatabaseController@create'); - Route::post('/view/{id}', 'DatabaseController@update'); + Route::patch('/view/{host}', 'DatabaseController@update'); }); /*