forked from Alex/Pterodactyl-Panel
Merge branch 'feature/user-databases' into develop
This commit is contained in:
commit
060c64263b
@ -18,6 +18,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines.
|
||||
* Adds back client API for sending commands or power toggles to a server though the Panel API: `/api/client/servers/<identifier>`
|
||||
* Added proper transformer for Packs and re-enabled missing includes on server.
|
||||
* Added support for using Filesystem as a caching driver, although not recommended.
|
||||
* Added support for user management of server databases.
|
||||
|
||||
## v0.7.3 (Derelict Dermodactylus)
|
||||
### Fixed
|
||||
|
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Exceptions\Service\Database;
|
||||
|
||||
use Pterodactyl\Exceptions\PterodactylException;
|
||||
|
||||
class DatabaseClientFeatureNotEnabledException extends PterodactylException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('Client database creation is not enabled in this Panel.');
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Exceptions\Service\Database;
|
||||
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
|
||||
class NoSuitableDatabaseHostException extends DisplayException
|
||||
{
|
||||
/**
|
||||
* NoSuitableDatabaseHostException constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('No database host was found that meets the requirements for this server.');
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Exceptions\Service\Database;
|
||||
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
|
||||
class TooManyDatabasesException extends DisplayException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('Operation aborted: creating a new database would put this server over the defined limit.');
|
||||
}
|
||||
}
|
@ -4,34 +4,76 @@ namespace Pterodactyl\Http\Controllers\Server;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Pterodactyl\Traits\Controllers\JavascriptInjection;
|
||||
use Pterodactyl\Services\Databases\DatabasePasswordService;
|
||||
use Pterodactyl\Services\Databases\DatabaseManagementService;
|
||||
use Pterodactyl\Services\Databases\DeployServerDatabaseService;
|
||||
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
|
||||
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
|
||||
use Pterodactyl\Http\Requests\Server\Database\StoreServerDatabaseRequest;
|
||||
use Pterodactyl\Http\Requests\Server\Database\DeleteServerDatabaseRequest;
|
||||
|
||||
class DatabaseController extends Controller
|
||||
{
|
||||
use JavascriptInjection;
|
||||
|
||||
/**
|
||||
* @var \Prologue\Alerts\AlertsMessageBag
|
||||
*/
|
||||
private $alert;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Databases\DeployServerDatabaseService
|
||||
*/
|
||||
private $deployServerDatabaseService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface
|
||||
*/
|
||||
private $databaseHostRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Databases\DatabaseManagementService
|
||||
*/
|
||||
private $managementService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Databases\DatabasePasswordService
|
||||
*/
|
||||
protected $passwordService;
|
||||
private $passwordService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
|
||||
*/
|
||||
protected $repository;
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* DatabaseController constructor.
|
||||
*
|
||||
* @param \Prologue\Alerts\AlertsMessageBag $alert
|
||||
* @param \Pterodactyl\Services\Databases\DeployServerDatabaseService $deployServerDatabaseService
|
||||
* @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $databaseHostRepository
|
||||
* @param \Pterodactyl\Services\Databases\DatabaseManagementService $managementService
|
||||
* @param \Pterodactyl\Services\Databases\DatabasePasswordService $passwordService
|
||||
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
|
||||
*/
|
||||
public function __construct(DatabasePasswordService $passwordService, DatabaseRepositoryInterface $repository)
|
||||
{
|
||||
public function __construct(
|
||||
AlertsMessageBag $alert,
|
||||
DeployServerDatabaseService $deployServerDatabaseService,
|
||||
DatabaseHostRepositoryInterface $databaseHostRepository,
|
||||
DatabaseManagementService $managementService,
|
||||
DatabasePasswordService $passwordService,
|
||||
DatabaseRepositoryInterface $repository
|
||||
) {
|
||||
$this->alert = $alert;
|
||||
$this->databaseHostRepository = $databaseHostRepository;
|
||||
$this->deployServerDatabaseService = $deployServerDatabaseService;
|
||||
$this->managementService = $managementService;
|
||||
$this->passwordService = $passwordService;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
@ -50,11 +92,42 @@ class DatabaseController extends Controller
|
||||
$this->authorize('view-databases', $server);
|
||||
$this->setRequest($request)->injectJavascript();
|
||||
|
||||
$canCreateDatabase = config('pterodactyl.client_features.databases.enabled');
|
||||
$allowRandom = config('pterodactyl.client_features.databases.allow_random');
|
||||
|
||||
if ($this->databaseHostRepository->findCountWhere([['node_id', '=', $server->node_id]]) === 0) {
|
||||
if ($canCreateDatabase && ! $allowRandom) {
|
||||
$canCreateDatabase = false;
|
||||
}
|
||||
}
|
||||
|
||||
$databases = $this->repository->getDatabasesForServer($server->id);
|
||||
|
||||
return view('server.databases.index', [
|
||||
'databases' => $this->repository->getDatabasesForServer($server->id),
|
||||
'allowCreation' => $canCreateDatabase,
|
||||
'overLimit' => ! is_null($server->database_limit) && count($databases) >= $server->database_limit,
|
||||
'databases' => $databases,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a request from a user to create a new database for the server.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Server\Database\StoreServerDatabaseRequest $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
|
||||
*/
|
||||
public function store(StoreServerDatabaseRequest $request): RedirectResponse
|
||||
{
|
||||
$this->deployServerDatabaseService->handle($request->getServer(), $request->validated());
|
||||
|
||||
$this->alert->success('Successfully created a new database.')->flash();
|
||||
|
||||
return redirect()->route('server.databases.index', $request->getServer()->uuidShort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a request to update the password for a specific database.
|
||||
*
|
||||
@ -74,4 +147,19 @@ class DatabaseController extends Controller
|
||||
|
||||
return response()->json(['password' => $password]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a database for this server from the SQL server and Panel database.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Server\Database\DeleteServerDatabaseRequest $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function delete(DeleteServerDatabaseRequest $request): Response
|
||||
{
|
||||
$this->managementService->delete($request->attributes->get('database')->id);
|
||||
|
||||
return response('', Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
|
@ -38,8 +38,13 @@ class DatabaseBelongsToServer
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$server = $request->attributes->get('server');
|
||||
$database = $request->input('database') ?? $request->route()->parameter('database');
|
||||
|
||||
$database = $this->repository->find($request->input('database'));
|
||||
if (! is_digit($database)) {
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
$database = $this->repository->find($database);
|
||||
if (is_null($database) || $database->server_id !== $server->id) {
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = Server::getUpdateRulesForId($this->route()->parameter('server')->id);
|
||||
$rules = Server::getUpdateRulesForId($this->getModel(Server::class)->id);
|
||||
|
||||
return [
|
||||
'allocation' => $rules['allocation_id'],
|
||||
@ -26,6 +26,9 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
|
||||
'add_allocations.*' => 'integer',
|
||||
'remove_allocations' => 'bail|array',
|
||||
'remove_allocations.*' => 'integer',
|
||||
'feature_limits' => 'required|array',
|
||||
'feature_limits.databases' => $rules['database_limit'],
|
||||
'feature_limits.allocations' => $rules['allocation_limit'],
|
||||
];
|
||||
}
|
||||
|
||||
@ -39,7 +42,9 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
|
||||
$data = parent::validated();
|
||||
|
||||
$data['allocation_id'] = $data['allocation'];
|
||||
unset($data['allocation']);
|
||||
$data['database_limit'] = $data['feature_limits']['databases'];
|
||||
$data['allocation_limit'] = $data['feature_limits']['allocations'];
|
||||
unset($data['allocation'], $data['feature_limits']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
@ -56,6 +61,8 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
|
||||
'remove_allocations' => 'allocations to remove',
|
||||
'add_allocations.*' => 'allocation to add',
|
||||
'remove_allocations.*' => 'allocation to remove',
|
||||
'feature_limits.databases' => 'Database Limit',
|
||||
'feature_limits.allocations' => 'Allocation Limit',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Server\Database;
|
||||
|
||||
use Pterodactyl\Http\Requests\Server\ServerFormRequest;
|
||||
|
||||
class DeleteServerDatabaseRequest extends ServerFormRequest
|
||||
{
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
if (! parent::authorize()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return config('pterodactyl.client_features.databases.enabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user permission to validate this request aganist.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function permission(): string
|
||||
{
|
||||
return 'delete-database';
|
||||
}
|
||||
|
||||
/**
|
||||
* Rules to validate this request aganist.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Server\Database;
|
||||
|
||||
use Pterodactyl\Http\Requests\Server\ServerFormRequest;
|
||||
|
||||
class StoreServerDatabaseRequest extends ServerFormRequest
|
||||
{
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
if (! parent::authorize()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return config('pterodactyl.client_features.databases.enabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user permission to validate this request aganist.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function permission(): string
|
||||
{
|
||||
return 'create-database';
|
||||
}
|
||||
|
||||
/**
|
||||
* Rules to validate this request aganist.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'database' => 'required|string|min:1',
|
||||
'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/',
|
||||
];
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Server;
|
||||
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Http\Requests\FrontendUserFormRequest;
|
||||
|
||||
abstract class ServerFormRequest extends FrontendUserFormRequest
|
||||
@ -24,6 +25,11 @@ abstract class ServerFormRequest extends FrontendUserFormRequest
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->user()->can($this->permission(), $this->attributes->get('server'));
|
||||
return $this->user()->can($this->permission(), $this->getServer());
|
||||
}
|
||||
|
||||
public function getServer(): Server
|
||||
{
|
||||
return $this->attributes->get('server');
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +93,8 @@ class Permission extends Model implements CleansAttributes, ValidableContract
|
||||
'database' => [
|
||||
'view-databases' => null,
|
||||
'reset-db-password' => null,
|
||||
'delete-database' => null,
|
||||
'create-database' => null,
|
||||
],
|
||||
'file' => [
|
||||
'access-sftp' => null,
|
||||
|
@ -69,6 +69,8 @@ class Server extends Model implements CleansAttributes, ValidableContract
|
||||
'skip_scripts' => 'sometimes',
|
||||
'image' => 'required',
|
||||
'startup' => 'required',
|
||||
'database_limit' => 'present',
|
||||
'allocation_limit' => 'present',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -93,6 +95,8 @@ class Server extends Model implements CleansAttributes, ValidableContract
|
||||
'skip_scripts' => 'boolean',
|
||||
'image' => 'string|max:255',
|
||||
'installed' => 'boolean',
|
||||
'database_limit' => 'nullable|integer|min:0',
|
||||
'allocation_limit' => 'nullable|integer|min:0',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -116,6 +120,8 @@ class Server extends Model implements CleansAttributes, ValidableContract
|
||||
'egg_id' => 'integer',
|
||||
'pack_id' => 'integer',
|
||||
'installed' => 'integer',
|
||||
'database_limit' => 'integer',
|
||||
'allocation_limit' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -13,22 +13,27 @@ class DatabaseManagementService
|
||||
/**
|
||||
* @var \Illuminate\Database\DatabaseManager
|
||||
*/
|
||||
protected $database;
|
||||
private $database;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection
|
||||
*/
|
||||
protected $dynamic;
|
||||
private $dynamic;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Encryption\Encrypter
|
||||
*/
|
||||
protected $encrypter;
|
||||
private $encrypter;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
|
||||
*/
|
||||
protected $repository;
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $useRandomHost = false;
|
||||
|
||||
/**
|
||||
* CreationService constructor.
|
||||
@ -55,7 +60,7 @@ class DatabaseManagementService
|
||||
*
|
||||
* @param int $server
|
||||
* @param array $data
|
||||
* @return \Illuminate\Database\Eloquent\Model
|
||||
* @return \Pterodactyl\Models\Database
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
|
90
app/Services/Databases/DeployServerDatabaseService.php
Normal file
90
app/Services/Databases/DeployServerDatabaseService.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Services\Databases;
|
||||
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\Database;
|
||||
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
|
||||
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
|
||||
use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException;
|
||||
use Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException;
|
||||
use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;
|
||||
|
||||
class DeployServerDatabaseService
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface
|
||||
*/
|
||||
private $databaseHostRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Databases\DatabaseManagementService
|
||||
*/
|
||||
private $managementService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* ServerDatabaseCreationService constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
|
||||
* @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $databaseHostRepository
|
||||
* @param \Pterodactyl\Services\Databases\DatabaseManagementService $managementService
|
||||
*/
|
||||
public function __construct(
|
||||
DatabaseRepositoryInterface $repository,
|
||||
DatabaseHostRepositoryInterface $databaseHostRepository,
|
||||
DatabaseManagementService $managementService
|
||||
) {
|
||||
$this->databaseHostRepository = $databaseHostRepository;
|
||||
$this->managementService = $managementService;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @param array $data
|
||||
* @return \Pterodactyl\Models\Database
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function handle(Server $server, array $data): Database
|
||||
{
|
||||
if (! config('pterodactyl.client_features.databases.enabled')) {
|
||||
throw new DatabaseClientFeatureNotEnabledException;
|
||||
}
|
||||
|
||||
$databases = $this->repository->findCountWhere([['server_id', '=', $server->id]]);
|
||||
if (! is_null($server->database_limit) && $databases >= $server->database_limit) {
|
||||
throw new TooManyDatabasesException;
|
||||
}
|
||||
|
||||
$allowRandom = config('pterodactyl.client_features.databases.allow_random');
|
||||
$hosts = $this->databaseHostRepository->setColumns(['id'])->findWhere([
|
||||
['node_id', '=', $server->node_id],
|
||||
]);
|
||||
|
||||
if ($hosts->isEmpty() && ! $allowRandom) {
|
||||
throw new NoSuitableDatabaseHostException;
|
||||
}
|
||||
|
||||
if ($hosts->isEmpty()) {
|
||||
$hosts = $this->databaseHostRepository->setColumns(['id'])->all();
|
||||
if ($hosts->isEmpty()) {
|
||||
throw new NoSuitableDatabaseHostException;
|
||||
}
|
||||
}
|
||||
|
||||
$host = $hosts->random();
|
||||
|
||||
return $this->managementService->create($server->id, [
|
||||
'database_host_id' => $host->id,
|
||||
'database' => array_get($data, 'database'),
|
||||
'remote' => array_get($data, 'remote'),
|
||||
]);
|
||||
}
|
||||
}
|
@ -91,6 +91,8 @@ class BuildModificationService
|
||||
'cpu' => array_get($data, 'cpu'),
|
||||
'disk' => array_get($data, 'disk'),
|
||||
'allocation_id' => array_get($data, 'allocation_id'),
|
||||
'database_limit' => array_get($data, 'database_limit'),
|
||||
'allocation_limit' => array_get($data, 'allocation_limit'),
|
||||
]);
|
||||
|
||||
$allocations = $this->allocationRepository->findWhere([['server_id', '=', $server->id]]);
|
||||
|
@ -75,6 +75,10 @@ class ServerTransformer extends BaseTransformer
|
||||
'io' => $server->io,
|
||||
'cpu' => $server->cpu,
|
||||
],
|
||||
'feature_limits' => [
|
||||
'databases' => $server->database_limit,
|
||||
'allocations' => $server->allocation_limit,
|
||||
],
|
||||
'user' => $server->owner_id,
|
||||
'node' => $server->node_id,
|
||||
'allocation' => $server->allocation_id,
|
||||
|
@ -36,6 +36,10 @@ class ServerTransformer extends BaseClientTransformer
|
||||
'io' => $server->io,
|
||||
'cpu' => $server->cpu,
|
||||
],
|
||||
'feature_limits' => [
|
||||
'databases' => $server->database_limit,
|
||||
'allocations' => $server->allocation_limit,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -163,6 +163,21 @@ return [
|
||||
'in_context' => env('PHRASE_IN_CONTEXT', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Language Editor
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set `PHRASE_IN_CONTEXT` to true to enable the PhaseApp in-context editor
|
||||
| on this site which allows you to translate the panel, from the panel.
|
||||
*/
|
||||
'client_features' => [
|
||||
'databases' => [
|
||||
'enabled' => env('PTERODACTYL_CLIENT_DATABASES_ENABLED', true),
|
||||
'allow_random' => env('PTERODACTYL_CLIENT_DATABASES_ALLOW_RANDOM', true),
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| File Editor
|
||||
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddDatabaseAndPortLimitColumnsToServersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->unsignedInteger('database_limit')->after('installed')->nullable()->default(0);
|
||||
$table->unsignedInteger('allocation_limit')->after('installed')->nullable()->default(0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropColumn(['database_limit', 'allocation_limit']);
|
||||
});
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -248,6 +248,14 @@ return [
|
||||
'title' => 'Reset Database Password',
|
||||
'description' => 'Allows a user to reset passwords for databases.',
|
||||
],
|
||||
'delete_database' => [
|
||||
'title' => 'Delete Databases',
|
||||
'description' => 'Allows a user to delete databases for this server from the Panel.',
|
||||
],
|
||||
'create_database' => [
|
||||
'title' => 'Create Database',
|
||||
'description' => 'Allows a user to create additional databases for this server.',
|
||||
],
|
||||
],
|
||||
],
|
||||
'files' => [
|
||||
|
@ -111,7 +111,7 @@
|
||||
<div class="form-group col-sm-4">
|
||||
<label for="pSwap">Swap</label>
|
||||
<div class="input-group">
|
||||
<input type="text" value="{{ old('swap') }}" class="form-control" name="swap" id="pSwap" />
|
||||
<input type="text" value="{{ old('swap', 0) }}" class="form-control" name="swap" id="pSwap" />
|
||||
<span class="input-group-addon">MB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -89,6 +89,33 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-7">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Application Feature Limits</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="form-group col-xs-6">
|
||||
<label for="cpu" class="control-label">Database Limit</label>
|
||||
<div>
|
||||
<input type="text" name="database_limit" class="form-control" value="{{ old('database_limit', $server->database_limit) }}"/>
|
||||
</div>
|
||||
<p class="text-muted small">The total number of databases a user is allowed to create for this server. Leave blank to allow unlimmited.</p>
|
||||
</div>
|
||||
<div class="form-group col-xs-6">
|
||||
<label for="cpu" class="control-label">Allocation Limit</label>
|
||||
<div>
|
||||
<input type="text" name="allocation_limit" class="form-control" value="{{ old('allocation_limit', $server->allocation_limit) }}"/>
|
||||
</div>
|
||||
<p class="text-muted small"><strong>This feature is not currently implemented.</strong> The total number of allocations a user is allowed to create for this server. Leave blank to allow unlimited.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Allocation Management</h3>
|
||||
@ -136,6 +163,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
@ -21,15 +21,10 @@
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="{{ $allowCreation && Gate::allows('create-database', $server) ? 'col-xs-12 col-sm-8' : 'col-xs-12' }}">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">@lang('server.config.database.your_dbs')</h3>
|
||||
@if(auth()->user()->root_admin)
|
||||
<div class="box-tools">
|
||||
<a href="{{ route('admin.servers.view.database', ['server' => $server->id]) }}" target="_blank" class="btn btn-sm btn-success">Create New</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@if(count($databases) > 0)
|
||||
<div class="box-body table-responsive no-padding">
|
||||
@ -55,11 +50,20 @@
|
||||
</code>
|
||||
</td>
|
||||
<td class="middle"><code>{{ $database->host->host }}:{{ $database->host->port }}</code></td>
|
||||
@can('reset-db-password', $server)
|
||||
@if(Gate::allows('reset-db-password', $server) || Gate::allows('delete-database', $server))
|
||||
<td>
|
||||
<button class="btn btn-xs btn-primary pull-right" data-action="reset-password" data-id="{{ $database->id }}"><i class="fa fa-fw fa-refresh"></i> @lang('server.config.database.reset_password')</button>
|
||||
</td>
|
||||
@can('delete-database', $server)
|
||||
<button class="btn btn-xs btn-danger pull-right" data-action="delete-database" data-id="{{ $database->id }}">
|
||||
<i class="fa fa-fw fa-trash-o"></i>
|
||||
</button>
|
||||
@endcan
|
||||
@can('reset-db-password', $server)
|
||||
<button class="btn btn-xs btn-primary pull-right" style="margin-right:10px;" data-action="reset-password" data-id="{{ $database->id }}">
|
||||
<i class="fa fa-fw fa-refresh"></i> @lang('server.config.database.reset_password')
|
||||
</button>
|
||||
@endcan
|
||||
</td>
|
||||
@endif
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
@ -69,17 +73,49 @@
|
||||
<div class="box-body">
|
||||
<div class="alert alert-info no-margin-bottom">
|
||||
@lang('server.config.database.no_dbs')
|
||||
@if(Auth::user()->root_admin === 1)
|
||||
<a href="{{ route('admin.servers.view', [
|
||||
'id' => $server->id,
|
||||
'tab' => 'tab_database'
|
||||
]) }}" target="_blank">@lang('server.config.database.add_db')</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if($allowCreation && Gate::allows('create-database', $server))
|
||||
<div class="col-xs-12 col-sm-4">
|
||||
<div class="box box-success">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Create New Database</h3>
|
||||
</div>
|
||||
@if($overLimit)
|
||||
<div class="box-body">
|
||||
<div class="alert alert-danger no-margin">
|
||||
You are currently using <strong>{{ count($databases) }}</strong> of your <strong>{{ $server->database_limit ?? '∞' }}</strong> allowed databases.
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<form action="{{ route('server.databases.new', $server->uuidShort) }}" method="POST">
|
||||
<div class="box-body">
|
||||
<div class="form-group">
|
||||
<label for="pDatabaseName" class="control-label">Database</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">s{{ $server->id }}_</span>
|
||||
<input id="pDatabaseName" type="text" name="database" class="form-control" placeholder="database" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="pRemote" class="control-label">Connections</label>
|
||||
<input id="pRemote" type="text" name="remote" class="form-control" value="%" />
|
||||
<p class="text-muted small">This should reflect the IP address that connections are allowed from. Uses standard MySQL notation. If unsure leave as <code>%</code>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
{!! csrf_field() !!}
|
||||
<p class="text-muted small">You are currently using <strong>{{ count($databases) }}</strong> of <strong>{{ $server->database_limit ?? '∞' }}</strong> databases. A username and password for this database will be randomly generated after form submission.</p>
|
||||
<input type="submit" class="btn btn-sm btn-success pull-right" value="Create Database" />
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@ -126,5 +162,37 @@
|
||||
});
|
||||
});
|
||||
@endcan
|
||||
@can('delete-database', $server)
|
||||
$('[data-action="delete-database"]').click(function (event) {
|
||||
event.preventDefault();
|
||||
var self = $(this);
|
||||
swal({
|
||||
title: '',
|
||||
type: 'warning',
|
||||
text: 'Are you sure that you want to delete this database? There is no going back, all data will immediately be removed.',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Delete',
|
||||
confirmButtonColor: '#d9534f',
|
||||
closeOnConfirm: false,
|
||||
showLoaderOnConfirm: true,
|
||||
}, function () {
|
||||
$.ajax({
|
||||
method: 'DELETE',
|
||||
url: Router.route('server.databases.delete', { server: '{{ $server->uuidShort }}', database: self.data('id') }),
|
||||
headers: { 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content') },
|
||||
}).done(function () {
|
||||
self.parent().parent().slideUp();
|
||||
swal.close();
|
||||
}).fail(function (jqXHR) {
|
||||
console.error(jqXHR);
|
||||
swal({
|
||||
type: 'error',
|
||||
title: 'Whoops!',
|
||||
text: (typeof jqXHR.responseJSON.error !== 'undefined') ? jqXHR.responseJSON.error : 'An error occured while processing this request.'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@endcan
|
||||
</script>
|
||||
@endsection
|
||||
|
@ -38,7 +38,11 @@ Route::group(['prefix' => 'settings'], function () {
|
||||
Route::group(['prefix' => 'databases'], function () {
|
||||
Route::get('/', 'DatabaseController@index')->name('server.databases.index');
|
||||
|
||||
Route::post('/new', 'DatabaseController@store')->name('server.databases.new');
|
||||
|
||||
Route::patch('/password', 'DatabaseController@update')->middleware('server..database')->name('server.databases.password');
|
||||
|
||||
Route::delete('/delete/{database}', 'DatabaseController@delete')->middleware('server..database')->name('server.databases.delete');
|
||||
});
|
||||
|
||||
/*
|
||||
|
@ -1,17 +1,10 @@
|
||||
<?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 Tests\Unit\Http\Controllers\Server\Files;
|
||||
|
||||
use Mockery as m;
|
||||
use phpmock\phpunit\PHPMock;
|
||||
use Pterodactyl\Models\Node;
|
||||
use Tests\Traits\MocksUuids;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Illuminate\Cache\Repository;
|
||||
use Tests\Unit\Http\Controllers\ControllerTestCase;
|
||||
@ -19,7 +12,7 @@ use Pterodactyl\Http\Controllers\Server\Files\DownloadController;
|
||||
|
||||
class DownloadControllerTest extends ControllerTestCase
|
||||
{
|
||||
use PHPMock;
|
||||
use MocksUuids;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Cache\Repository|\Mockery\Mock
|
||||
@ -48,16 +41,20 @@ class DownloadControllerTest extends ControllerTestCase
|
||||
$this->setRequestAttribute('server', $server);
|
||||
|
||||
$controller->shouldReceive('authorize')->with('download-files', $server)->once()->andReturnNull();
|
||||
$this->getFunctionMock('\\Pterodactyl\\Http\\Controllers\\Server\\Files', 'str_random')
|
||||
->expects($this->once())->willReturn('randomString');
|
||||
|
||||
$this->cache->shouldReceive('tags')->with(['Server:Downloads'])->once()->andReturnSelf();
|
||||
$this->cache->shouldReceive('put')->with('randomString', ['server' => $server->uuid, 'path' => '/my/file.txt'], 5)->once()->andReturnNull();
|
||||
$this->cache->shouldReceive('put')
|
||||
->once()
|
||||
->with('Server:Downloads:' . $this->getKnownUuid(), ['server' => $server->uuid, 'path' => '/my/file.txt'], 5)
|
||||
->andReturnNull();
|
||||
|
||||
$response = $controller->index($this->request, $server->uuidShort, '/my/file.txt');
|
||||
$this->assertIsRedirectResponse($response);
|
||||
$this->assertRedirectUrlEquals(sprintf(
|
||||
'%s://%s:%s/v1/server/file/download/%s', $server->node->scheme, $server->node->fqdn, $server->node->daemonListen, 'randomString'
|
||||
'%s://%s:%s/v1/server/file/download/%s',
|
||||
$server->node->scheme,
|
||||
$server->node->fqdn,
|
||||
$server->node->daemonListen,
|
||||
$this->getKnownUuid()
|
||||
), $response);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\Databases;
|
||||
|
||||
use Mockery as m;
|
||||
use Tests\TestCase;
|
||||
use Pterodactyl\Models\Server;
|
||||
use Pterodactyl\Models\Database;
|
||||
use Pterodactyl\Services\Databases\DatabaseManagementService;
|
||||
use Pterodactyl\Services\Databases\DeployServerDatabaseService;
|
||||
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
|
||||
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
|
||||
|
||||
class DeployServerDatabaseServiceTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface|\Mockery\Mock
|
||||
*/
|
||||
private $databaseHostRepository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Databases\DatabaseManagementService|\Mockery\Mock
|
||||
*/
|
||||
private $managementService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface|\Mockery\Mock
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* Setup tests.
|
||||
*/
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->databaseHostRepository = m::mock(DatabaseHostRepositoryInterface::class);
|
||||
$this->managementService = m::mock(DatabaseManagementService::class);
|
||||
$this->repository = m::mock(DatabaseRepositoryInterface::class);
|
||||
|
||||
// Set configs for testing instances.
|
||||
config()->set('pterodactyl.client_features.databases.enabled', true);
|
||||
config()->set('pterodactyl.client_features.databases.allow_random', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test handling of non-random hosts when a host is found.
|
||||
*
|
||||
* @dataProvider databaseLimitDataProvider
|
||||
*/
|
||||
public function testNonRandomFoundHost($limit, $count)
|
||||
{
|
||||
config()->set('pterodactyl.client_features.databases.allow_random', false);
|
||||
|
||||
$server = factory(Server::class)->make(['database_limit' => $limit]);
|
||||
$model = factory(Database::class)->make();
|
||||
|
||||
$this->repository->shouldReceive('findCountWhere')
|
||||
->once()
|
||||
->with([['server_id', '=', $server->id]])
|
||||
->andReturn($count);
|
||||
|
||||
$this->databaseHostRepository->shouldReceive('setColumns->findWhere')
|
||||
->once()
|
||||
->with([['node_id', '=', $server->node_id]])
|
||||
->andReturn(collect([$model]));
|
||||
|
||||
$this->managementService->shouldReceive('create')
|
||||
->once()
|
||||
->with($server->id, [
|
||||
'database_host_id' => $model->id,
|
||||
'database' => 'testdb',
|
||||
'remote' => null,
|
||||
])
|
||||
->andReturn($model);
|
||||
|
||||
$response = $this->getService()->handle($server, ['database' => 'testdb']);
|
||||
|
||||
$this->assertInstanceOf(Database::class, $response);
|
||||
$this->assertSame($model, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an exception is thrown if in non-random mode and no host is found.
|
||||
*
|
||||
* @expectedException \Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException
|
||||
*/
|
||||
public function testNonRandomNoHost()
|
||||
{
|
||||
config()->set('pterodactyl.client_features.databases.allow_random', false);
|
||||
|
||||
$server = factory(Server::class)->make(['database_limit' => 1]);
|
||||
|
||||
$this->repository->shouldReceive('findCountWhere')
|
||||
->once()
|
||||
->with([['server_id', '=', $server->id]])
|
||||
->andReturn(0);
|
||||
|
||||
$this->databaseHostRepository->shouldReceive('setColumns->findWhere')
|
||||
->once()
|
||||
->with([['node_id', '=', $server->node_id]])
|
||||
->andReturn(collect());
|
||||
|
||||
$this->getService()->handle($server, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test handling of random host selection.
|
||||
*/
|
||||
public function testRandomFoundHost()
|
||||
{
|
||||
$server = factory(Server::class)->make(['database_limit' => 1]);
|
||||
$model = factory(Database::class)->make();
|
||||
|
||||
$this->repository->shouldReceive('findCountWhere')
|
||||
->once()
|
||||
->with([['server_id', '=', $server->id]])
|
||||
->andReturn(0);
|
||||
|
||||
$this->databaseHostRepository->shouldReceive('setColumns->findWhere')
|
||||
->once()
|
||||
->with([['node_id', '=', $server->node_id]])
|
||||
->andReturn(collect());
|
||||
|
||||
$this->databaseHostRepository->shouldReceive('setColumns->all')
|
||||
->once()
|
||||
->andReturn(collect([$model]));
|
||||
|
||||
$this->managementService->shouldReceive('create')
|
||||
->once()
|
||||
->with($server->id, [
|
||||
'database_host_id' => $model->id,
|
||||
'database' => 'testdb',
|
||||
'remote' => null,
|
||||
])
|
||||
->andReturn($model);
|
||||
|
||||
$response = $this->getService()->handle($server, ['database' => 'testdb']);
|
||||
|
||||
$this->assertInstanceOf(Database::class, $response);
|
||||
$this->assertSame($model, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an exception is thrown when no host is found and random is allowed.
|
||||
*
|
||||
* @expectedException \Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException
|
||||
*/
|
||||
public function testRandomNoHost()
|
||||
{
|
||||
$server = factory(Server::class)->make(['database_limit' => 1]);
|
||||
|
||||
$this->repository->shouldReceive('findCountWhere')
|
||||
->once()
|
||||
->with([['server_id', '=', $server->id]])
|
||||
->andReturn(0);
|
||||
|
||||
$this->databaseHostRepository->shouldReceive('setColumns->findWhere')
|
||||
->once()
|
||||
->with([['node_id', '=', $server->node_id]])
|
||||
->andReturn(collect());
|
||||
|
||||
$this->databaseHostRepository->shouldReceive('setColumns->all')
|
||||
->once()
|
||||
->andReturn(collect());
|
||||
|
||||
$this->getService()->handle($server, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a server over the database limit throws an exception.
|
||||
*
|
||||
* @dataProvider databaseExceedingLimitDataProvider
|
||||
* @expectedException \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException
|
||||
*/
|
||||
public function testServerOverDatabaseLimit($limit, $count)
|
||||
{
|
||||
$server = factory(Server::class)->make(['database_limit' => $limit]);
|
||||
|
||||
$this->repository->shouldReceive('findCountWhere')
|
||||
->once()
|
||||
->with([['server_id', '=', $server->id]])
|
||||
->andReturn($count);
|
||||
|
||||
$this->getService()->handle($server, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an exception is thrown if the feature is not enabled.
|
||||
*
|
||||
* @expectedException \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
|
||||
*/
|
||||
public function testFeatureNotEnabled()
|
||||
{
|
||||
config()->set('pterodactyl.client_features.databases.enabled', false);
|
||||
|
||||
$this->getService()->handle(factory(Server::class)->make(), []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide limits and current database counts for testing.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function databaseLimitDataProvider(): array
|
||||
{
|
||||
return [
|
||||
[null, 10],
|
||||
[1, 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide data for servers over their database limit.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function databaseExceedingLimitDataProvider(): array
|
||||
{
|
||||
return [
|
||||
[2, 2],
|
||||
[2, 3],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an instance of the service with mocked dependencies for testing.
|
||||
*
|
||||
* @return \Pterodactyl\Services\Databases\DeployServerDatabaseService
|
||||
*/
|
||||
private function getService(): DeployServerDatabaseService
|
||||
{
|
||||
return new DeployServerDatabaseService($this->repository, $this->databaseHostRepository, $this->managementService);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user