From c28e9c1ab7ea70d26b4e02c1327babc6182b9aa9 Mon Sep 17 00:00:00 2001 From: Dane Everitt <dane@daneeveritt.com> Date: Wed, 22 Aug 2018 22:29:20 -0700 Subject: [PATCH] Add ability to create new database through the UI --- .../Http/ClientPermissionsRequest.php | 15 ++++ .../Api/Client/Servers/DatabaseController.php | 33 +++++++- .../Requests/Api/Client/ClientApiRequest.php | 9 ++- .../Servers/Databases/GetDatabasesRequest.php | 17 ++++ .../Databases/StoreDatabaseRequest.php | 28 +++++++ .../Client/Servers/GetDatabasesRequest.php | 20 ----- resources/assets/scripts/components/Flash.vue | 16 ++-- .../assets/scripts/components/MessageBox.vue | 16 ++++ .../server/components/CreateDatabaseModal.vue | 81 +++++++++++++++++++ .../components/server/subpages/Databases.vue | 37 ++++++++- .../assets/styles/components/buttons.css | 4 + routes/api-client.php | 1 + 12 files changed, 240 insertions(+), 37 deletions(-) create mode 100644 app/Contracts/Http/ClientPermissionsRequest.php create mode 100644 app/Http/Requests/Api/Client/Servers/Databases/GetDatabasesRequest.php create mode 100644 app/Http/Requests/Api/Client/Servers/Databases/StoreDatabaseRequest.php delete mode 100644 app/Http/Requests/Api/Client/Servers/GetDatabasesRequest.php create mode 100644 resources/assets/scripts/components/MessageBox.vue create mode 100644 resources/assets/scripts/components/server/components/CreateDatabaseModal.vue diff --git a/app/Contracts/Http/ClientPermissionsRequest.php b/app/Contracts/Http/ClientPermissionsRequest.php new file mode 100644 index 00000000..6b863fce --- /dev/null +++ b/app/Contracts/Http/ClientPermissionsRequest.php @@ -0,0 +1,15 @@ +<?php + +namespace Pterodactyl\Contracts\Http; + +interface ClientPermissionsRequest +{ + /** + * Returns the permissions string indicating which permission should be used to + * validate that the authenticated user has permission to perform this action aganist + * the given resource (server). + * + * @return string + */ + public function permission(): string; +} diff --git a/app/Http/Controllers/Api/Client/Servers/DatabaseController.php b/app/Http/Controllers/Api/Client/Servers/DatabaseController.php index 34f0c0e9..72605e88 100644 --- a/app/Http/Controllers/Api/Client/Servers/DatabaseController.php +++ b/app/Http/Controllers/Api/Client/Servers/DatabaseController.php @@ -4,12 +4,19 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers; use Pterodactyl\Models\Server; use Pterodactyl\Transformers\Api\Client\DatabaseTransformer; +use Pterodactyl\Services\Databases\DeployServerDatabaseService; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; -use Pterodactyl\Http\Requests\Api\Client\Servers\GetDatabasesRequest; +use Pterodactyl\Http\Requests\Api\Client\Servers\Databases\GetDatabasesRequest; +use Pterodactyl\Http\Requests\Api\Client\Servers\Databases\StoreDatabaseRequest; class DatabaseController extends ClientApiController { + /** + * @var \Pterodactyl\Services\Databases\DeployServerDatabaseService + */ + private $deployDatabaseService; + /** * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface */ @@ -19,16 +26,18 @@ class DatabaseController extends ClientApiController * DatabaseController constructor. * * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository + * @param \Pterodactyl\Services\Databases\DeployServerDatabaseService $deployDatabaseService */ - public function __construct(DatabaseRepositoryInterface $repository) + public function __construct(DatabaseRepositoryInterface $repository, DeployServerDatabaseService $deployDatabaseService) { parent::__construct(); + $this->deployDatabaseService = $deployDatabaseService; $this->repository = $repository; } /** - * @param \Pterodactyl\Http\Requests\Api\Client\Servers\GetDatabasesRequest $request + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Databases\GetDatabasesRequest $request * @return array */ public function index(GetDatabasesRequest $request): array @@ -39,4 +48,22 @@ class DatabaseController extends ClientApiController ->transformWith($this->getTransformer(DatabaseTransformer::class)) ->toArray(); } + + /** + * Create a new database for the given server and return it. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Databases\StoreDatabaseRequest $request + * @return array + * + * @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException + */ + public function store(StoreDatabaseRequest $request): array + { + $database = $this->deployDatabaseService->handle($request->getModel(Server::class), $request->validated()); + + return $this->fractal->item($database) + ->parseIncludes(['password']) + ->transformWith($this->getTransformer(DatabaseTransformer::class)) + ->toArray(); + } } diff --git a/app/Http/Requests/Api/Client/ClientApiRequest.php b/app/Http/Requests/Api/Client/ClientApiRequest.php index 92402e51..468b294f 100644 --- a/app/Http/Requests/Api/Client/ClientApiRequest.php +++ b/app/Http/Requests/Api/Client/ClientApiRequest.php @@ -2,18 +2,23 @@ namespace Pterodactyl\Http\Requests\Api\Client; +use Pterodactyl\Models\Server; +use Pterodactyl\Contracts\Http\ClientPermissionsRequest; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; abstract class ClientApiRequest extends ApplicationApiRequest { /** - * Determine if the current user is authorized to perform - * the requested action against the API. + * Determine if the current user is authorized to perform the requested action against the API. * * @return bool */ public function authorize(): bool { + if ($this instanceof ClientPermissionsRequest || method_exists($this, 'permission')) { + return $this->user()->can($this->permission(), $this->getModel(Server::class)); + } + return true; } } diff --git a/app/Http/Requests/Api/Client/Servers/Databases/GetDatabasesRequest.php b/app/Http/Requests/Api/Client/Servers/Databases/GetDatabasesRequest.php new file mode 100644 index 00000000..0e90d80b --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Databases/GetDatabasesRequest.php @@ -0,0 +1,17 @@ +<?php + +namespace Pterodactyl\Http\Requests\Api\Client\Servers\Databases; + +use Pterodactyl\Contracts\Http\ClientPermissionsRequest; +use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest; + +class GetDatabasesRequest extends ClientApiRequest implements ClientPermissionsRequest +{ + /** + * @return string + */ + public function permission(): string + { + return 'view-databases'; + } +} diff --git a/app/Http/Requests/Api/Client/Servers/Databases/StoreDatabaseRequest.php b/app/Http/Requests/Api/Client/Servers/Databases/StoreDatabaseRequest.php new file mode 100644 index 00000000..d8fc7849 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Databases/StoreDatabaseRequest.php @@ -0,0 +1,28 @@ +<?php + +namespace Pterodactyl\Http\Requests\Api\Client\Servers\Databases; + +use Pterodactyl\Contracts\Http\ClientPermissionsRequest; +use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest; + +class StoreDatabaseRequest extends ClientApiRequest implements ClientPermissionsRequest +{ + /** + * @return string + */ + public function permission(): string + { + return 'create-database'; + } + + /** + * @return array + */ + public function rules(): array + { + return [ + 'database' => 'required|alpha_dash|min:1|max:100', + 'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/', + ]; + } +} diff --git a/app/Http/Requests/Api/Client/Servers/GetDatabasesRequest.php b/app/Http/Requests/Api/Client/Servers/GetDatabasesRequest.php deleted file mode 100644 index 1c8b201a..00000000 --- a/app/Http/Requests/Api/Client/Servers/GetDatabasesRequest.php +++ /dev/null @@ -1,20 +0,0 @@ -<?php - -namespace Pterodactyl\Http\Requests\Api\Client\Servers; - -use Pterodactyl\Models\Server; -use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest; - -class GetDatabasesRequest extends ClientApiRequest -{ - /** - * Determine if this user has permission to view all of the databases available - * to this server. - * - * @return bool - */ - public function authorize(): bool - { - return $this->user()->can('view-databases', $this->getModel(Server::class)); - } -} diff --git a/resources/assets/scripts/components/Flash.vue b/resources/assets/scripts/components/Flash.vue index b42da3b1..5a776c19 100644 --- a/resources/assets/scripts/components/Flash.vue +++ b/resources/assets/scripts/components/Flash.vue @@ -1,22 +1,22 @@ <template> <div v-if="notifications.length > 0" :class="this.container"> <transition-group tag="div" name="fade"> - <div class="lg:inline-flex" role="alert" v-for="(item, index) in notifications" - :key="index" - :class="[item.class, { - 'mb-2': index < notifications.length - 1 - }]" - > - <span class="title" v-html="item.title" v-if="item.title.length > 0"></span> - <span class="message" v-html="item.message"></span> + <div v-for="(item, index) in notifications" :key="index"> + <message-box + :class="[item.class, {'mb-2': index < notifications.length - 1}]" + :title="item.title" + :message="item.message" + /> </div> </transition-group> </div> </template> <script> + import MessageBox from './MessageBox'; export default { name: 'flash', + components: {MessageBox}, props: { container: { type: String, diff --git a/resources/assets/scripts/components/MessageBox.vue b/resources/assets/scripts/components/MessageBox.vue new file mode 100644 index 00000000..b552f774 --- /dev/null +++ b/resources/assets/scripts/components/MessageBox.vue @@ -0,0 +1,16 @@ +<template> + <div class="lg:inline-flex" role="alert"> + <span class="title" v-html="title" v-if="title && title.length > 0"></span> + <span class="message" v-html="message"></span> + </div> +</template> + +<script> + export default { + name: 'message-box', + props: { + title: {type: String, required: false}, + message: {type: String, required: true} + }, + }; +</script> diff --git a/resources/assets/scripts/components/server/components/CreateDatabaseModal.vue b/resources/assets/scripts/components/server/components/CreateDatabaseModal.vue new file mode 100644 index 00000000..81cefad1 --- /dev/null +++ b/resources/assets/scripts/components/server/components/CreateDatabaseModal.vue @@ -0,0 +1,81 @@ +<template> + <div> + <message-box class="alert error mb-6" :message="errorMessage" v-show="errorMessage.length"/> + <h2 class="font-medium text-grey-darkest mb-6">Create a new database</h2> + <div class="mb-6"> + <label class="input-label" for="grid-database-name">Database name</label> + <input id="grid-database-name" type="text" class="input" name="database_name" required + v-model="database" + v-validate="{ alpha_dash: true, max: 100 }" + :class="{ error: errors.has('database_name') }" + > + <p class="input-help error" v-show="errors.has('database_name')">{{ errors.first('database_name') }}</p> + </div> + <div class="mb-6"> + <label class="input-label" for="grid-database-remote">Allow connections from</label> + <input id="grid-database-remote" type="text" class="input" name="remote" required + v-model="remote" + v-validate="{ regex: /^[0-9%.]{1,15}$/ }" + :class="{ error: errors.has('remote') }" + > + <p class="input-help error" v-show="errors.has('remote')">{{ errors.first('remote') }}</p> + </div> + <div class="text-right"> + <button class="btn btn-secondary btn-sm mr-2" v-on:click.once="$emit('close')">Cancel</button> + <button class="btn btn-green btn-sm" + :disabled="errors.any() || !canSubmit" + v-on:click="submit" + >Create</button> + </div> + </div> +</template> + +<script> + import MessageBox from '../../MessageBox'; + import get from 'lodash/get'; + + export default { + name: 'create-database-modal', + components: {MessageBox}, + data: function () { + return { + loading: false, + database: '', + remote: '%', + errorMessage: '', + }; + }, + + computed: { + canSubmit: function () { + return this.database.length && this.remote.length; + }, + }, + + methods: { + submit: function () { + this.errorMessage = ''; + this.loading = true; + + window.axios.post(this.route('api.client.servers.databases', { + server: this.$route.params.id, + }), { + database: this.database, + remote: this.remote, + }).then(response => { + this.$emit('database', response.data.attributes); + this.$emit('close'); + }).catch(err => { + if (get(err, 'response.data.errors[0]')) { + this.errorMessage = err.response.data.errors[0].detail; + } + + console.error('A network error was encountered while processing this request.', err.response); + }).then(() => { + this.loading = false; + }) + } + } + }; +</script> + diff --git a/resources/assets/scripts/components/server/subpages/Databases.vue b/resources/assets/scripts/components/server/subpages/Databases.vue index 01cf69c3..a18af988 100644 --- a/resources/assets/scripts/components/server/subpages/Databases.vue +++ b/resources/assets/scripts/components/server/subpages/Databases.vue @@ -3,7 +3,7 @@ <div v-if="loading"> <div class="spinner spinner-xl blue"></div> </div> - <div class="bg-white p-6 rounded border border-grey-light" v-else-if="!databases.length"> + <div class="context-box" v-else-if="!databases.length"> <div class="flex items-center"> <database-icon class="flex-none text-grey-darker"></database-icon> <div class="flex-1 px-4 text-grey-darker"> @@ -12,7 +12,7 @@ </div> </div> <div v-else> - <div class="bg-white p-6 rounded border border-grey-light mb-6" v-for="database in databases" :key="database.name"> + <div class="content-box mb-6" v-for="database in databases" :key="database.name"> <div class="flex items-center text-grey-darker"> <database-icon class="flex-none text-green"></database-icon> <div class="flex-1 px-4"> @@ -40,22 +40,35 @@ </div> </div> </div> + <div> + <button class="btn btn-blue btn-lg" v-on:click="showCreateModal = true">Create new database</button> + </div> </div> + <modal :show="showCreateModal" v-on:close="showCreateModal = false"> + <create-database-modal + v-on:close="showCreateModal = false" + v-on:database="handleModalCallback" + v-if="showCreateModal" + /> + </modal> </div> </template> <script> import { DatabaseIcon, LockIcon } from 'vue-feather-icons'; import map from 'lodash/map'; + import Modal from '../../core/Modal'; + import CreateDatabaseModal from '../components/CreateDatabaseModal'; export default { name: 'databases-page', - components: { DatabaseIcon, LockIcon }, + components: {CreateDatabaseModal, Modal, DatabaseIcon, LockIcon }, data: function () { return { - loading: true, databases: [], + loading: true, + showCreateModal: false, }; }, @@ -95,6 +108,22 @@ }); }, + /** + * Add the database to the list of existing databases automatically when the modal + * is closed with a successful callback. + */ + handleModalCallback: function (object) { + console.log('handle', object); + + const data = object; + data.password = data.relationships.password.attributes.password; + data.showPassword = false; + + delete data.relationships; + + this.databases.push(data); + }, + /** * Show the password for a given database object. * diff --git a/resources/assets/styles/components/buttons.css b/resources/assets/styles/components/buttons.css index 0171adda..e457a5db 100644 --- a/resources/assets/styles/components/buttons.css +++ b/resources/assets/styles/components/buttons.css @@ -43,6 +43,10 @@ @apply .p-4 .w-full .uppercase .tracking-wide .text-sm; } + &.btn-lg { + @apply .p-4 .uppercase .tracking-wide .text-sm; + } + &.btn-sm { @apply .px-6 .py-3 .uppercase .tracking-wide .text-sm; } diff --git a/routes/api-client.php b/routes/api-client.php index 14f783eb..d13c103e 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -37,5 +37,6 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateClie Route::group(['prefix' => '/databases'], function () { Route::get('/', 'Servers\DatabaseController@index')->name('api.client.servers.databases'); + Route::post('/', 'Servers\DatabaseController@store'); }); });