Add ability to create new database through the UI

This commit is contained in:
Dane Everitt 2018-08-22 22:29:20 -07:00
parent 17796fb1c4
commit c28e9c1ab7
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
12 changed files with 240 additions and 37 deletions

View file

@ -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;
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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';
}
}

View file

@ -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}$/',
];
}
}

View file

@ -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));
}
}

View file

@ -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,

View file

@ -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>

View file

@ -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>

View file

@ -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.
*

View file

@ -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;
}

View file

@ -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');
});
});