Skip to content

Commit c28e9c1

Browse files
committed
Add ability to create new database through the UI
1 parent 17796fb commit c28e9c1

File tree

12 files changed

+240
-37
lines changed

12 files changed

+240
-37
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Pterodactyl\Contracts\Http;
4+
5+
interface ClientPermissionsRequest
6+
{
7+
/**
8+
* Returns the permissions string indicating which permission should be used to
9+
* validate that the authenticated user has permission to perform this action aganist
10+
* the given resource (server).
11+
*
12+
* @return string
13+
*/
14+
public function permission(): string;
15+
}

app/Http/Controllers/Api/Client/Servers/DatabaseController.php

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@
44

55
use Pterodactyl\Models\Server;
66
use Pterodactyl\Transformers\Api\Client\DatabaseTransformer;
7+
use Pterodactyl\Services\Databases\DeployServerDatabaseService;
78
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
89
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
9-
use Pterodactyl\Http\Requests\Api\Client\Servers\GetDatabasesRequest;
10+
use Pterodactyl\Http\Requests\Api\Client\Servers\Databases\GetDatabasesRequest;
11+
use Pterodactyl\Http\Requests\Api\Client\Servers\Databases\StoreDatabaseRequest;
1012

1113
class DatabaseController extends ClientApiController
1214
{
15+
/**
16+
* @var \Pterodactyl\Services\Databases\DeployServerDatabaseService
17+
*/
18+
private $deployDatabaseService;
19+
1320
/**
1421
* @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
1522
*/
@@ -19,16 +26,18 @@ class DatabaseController extends ClientApiController
1926
* DatabaseController constructor.
2027
*
2128
* @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
29+
* @param \Pterodactyl\Services\Databases\DeployServerDatabaseService $deployDatabaseService
2230
*/
23-
public function __construct(DatabaseRepositoryInterface $repository)
31+
public function __construct(DatabaseRepositoryInterface $repository, DeployServerDatabaseService $deployDatabaseService)
2432
{
2533
parent::__construct();
2634

35+
$this->deployDatabaseService = $deployDatabaseService;
2736
$this->repository = $repository;
2837
}
2938

3039
/**
31-
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\GetDatabasesRequest $request
40+
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Databases\GetDatabasesRequest $request
3241
* @return array
3342
*/
3443
public function index(GetDatabasesRequest $request): array
@@ -39,4 +48,22 @@ public function index(GetDatabasesRequest $request): array
3948
->transformWith($this->getTransformer(DatabaseTransformer::class))
4049
->toArray();
4150
}
51+
52+
/**
53+
* Create a new database for the given server and return it.
54+
*
55+
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Databases\StoreDatabaseRequest $request
56+
* @return array
57+
*
58+
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
59+
*/
60+
public function store(StoreDatabaseRequest $request): array
61+
{
62+
$database = $this->deployDatabaseService->handle($request->getModel(Server::class), $request->validated());
63+
64+
return $this->fractal->item($database)
65+
->parseIncludes(['password'])
66+
->transformWith($this->getTransformer(DatabaseTransformer::class))
67+
->toArray();
68+
}
4269
}

app/Http/Requests/Api/Client/ClientApiRequest.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,23 @@
22

33
namespace Pterodactyl\Http\Requests\Api\Client;
44

5+
use Pterodactyl\Models\Server;
6+
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
57
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
68

79
abstract class ClientApiRequest extends ApplicationApiRequest
810
{
911
/**
10-
* Determine if the current user is authorized to perform
11-
* the requested action against the API.
12+
* Determine if the current user is authorized to perform the requested action against the API.
1213
*
1314
* @return bool
1415
*/
1516
public function authorize(): bool
1617
{
18+
if ($this instanceof ClientPermissionsRequest || method_exists($this, 'permission')) {
19+
return $this->user()->can($this->permission(), $this->getModel(Server::class));
20+
}
21+
1722
return true;
1823
}
1924
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Databases;
4+
5+
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
6+
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
7+
8+
class GetDatabasesRequest extends ClientApiRequest implements ClientPermissionsRequest
9+
{
10+
/**
11+
* @return string
12+
*/
13+
public function permission(): string
14+
{
15+
return 'view-databases';
16+
}
17+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Databases;
4+
5+
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
6+
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
7+
8+
class StoreDatabaseRequest extends ClientApiRequest implements ClientPermissionsRequest
9+
{
10+
/**
11+
* @return string
12+
*/
13+
public function permission(): string
14+
{
15+
return 'create-database';
16+
}
17+
18+
/**
19+
* @return array
20+
*/
21+
public function rules(): array
22+
{
23+
return [
24+
'database' => 'required|alpha_dash|min:1|max:100',
25+
'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/',
26+
];
27+
}
28+
}

app/Http/Requests/Api/Client/Servers/GetDatabasesRequest.php

Lines changed: 0 additions & 20 deletions
This file was deleted.

resources/assets/scripts/components/Flash.vue

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
<template>
22
<div v-if="notifications.length > 0" :class="this.container">
33
<transition-group tag="div" name="fade">
4-
<div class="lg:inline-flex" role="alert" v-for="(item, index) in notifications"
5-
:key="index"
6-
:class="[item.class, {
7-
'mb-2': index < notifications.length - 1
8-
}]"
9-
>
10-
<span class="title" v-html="item.title" v-if="item.title.length > 0"></span>
11-
<span class="message" v-html="item.message"></span>
4+
<div v-for="(item, index) in notifications" :key="index">
5+
<message-box
6+
:class="[item.class, {'mb-2': index < notifications.length - 1}]"
7+
:title="item.title"
8+
:message="item.message"
9+
/>
1210
</div>
1311
</transition-group>
1412
</div>
1513
</template>
1614

1715
<script>
16+
import MessageBox from './MessageBox';
1817
export default {
1918
name: 'flash',
19+
components: {MessageBox},
2020
props: {
2121
container: {
2222
type: String,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<template>
2+
<div class="lg:inline-flex" role="alert">
3+
<span class="title" v-html="title" v-if="title && title.length > 0"></span>
4+
<span class="message" v-html="message"></span>
5+
</div>
6+
</template>
7+
8+
<script>
9+
export default {
10+
name: 'message-box',
11+
props: {
12+
title: {type: String, required: false},
13+
message: {type: String, required: true}
14+
},
15+
};
16+
</script>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<template>
2+
<div>
3+
<message-box class="alert error mb-6" :message="errorMessage" v-show="errorMessage.length"/>
4+
<h2 class="font-medium text-grey-darkest mb-6">Create a new database</h2>
5+
<div class="mb-6">
6+
<label class="input-label" for="grid-database-name">Database name</label>
7+
<input id="grid-database-name" type="text" class="input" name="database_name" required
8+
v-model="database"
9+
v-validate="{ alpha_dash: true, max: 100 }"
10+
:class="{ error: errors.has('database_name') }"
11+
>
12+
<p class="input-help error" v-show="errors.has('database_name')">{{ errors.first('database_name') }}</p>
13+
</div>
14+
<div class="mb-6">
15+
<label class="input-label" for="grid-database-remote">Allow connections from</label>
16+
<input id="grid-database-remote" type="text" class="input" name="remote" required
17+
v-model="remote"
18+
v-validate="{ regex: /^[0-9%.]{1,15}$/ }"
19+
:class="{ error: errors.has('remote') }"
20+
>
21+
<p class="input-help error" v-show="errors.has('remote')">{{ errors.first('remote') }}</p>
22+
</div>
23+
<div class="text-right">
24+
<button class="btn btn-secondary btn-sm mr-2" v-on:click.once="$emit('close')">Cancel</button>
25+
<button class="btn btn-green btn-sm"
26+
:disabled="errors.any() || !canSubmit"
27+
v-on:click="submit"
28+
>Create</button>
29+
</div>
30+
</div>
31+
</template>
32+
33+
<script>
34+
import MessageBox from '../../MessageBox';
35+
import get from 'lodash/get';
36+
37+
export default {
38+
name: 'create-database-modal',
39+
components: {MessageBox},
40+
data: function () {
41+
return {
42+
loading: false,
43+
database: '',
44+
remote: '%',
45+
errorMessage: '',
46+
};
47+
},
48+
49+
computed: {
50+
canSubmit: function () {
51+
return this.database.length && this.remote.length;
52+
},
53+
},
54+
55+
methods: {
56+
submit: function () {
57+
this.errorMessage = '';
58+
this.loading = true;
59+
60+
window.axios.post(this.route('api.client.servers.databases', {
61+
server: this.$route.params.id,
62+
}), {
63+
database: this.database,
64+
remote: this.remote,
65+
}).then(response => {
66+
this.$emit('database', response.data.attributes);
67+
this.$emit('close');
68+
}).catch(err => {
69+
if (get(err, 'response.data.errors[0]')) {
70+
this.errorMessage = err.response.data.errors[0].detail;
71+
}
72+
73+
console.error('A network error was encountered while processing this request.', err.response);
74+
}).then(() => {
75+
this.loading = false;
76+
})
77+
}
78+
}
79+
};
80+
</script>
81+

resources/assets/scripts/components/server/subpages/Databases.vue

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<div v-if="loading">
44
<div class="spinner spinner-xl blue"></div>
55
</div>
6-
<div class="bg-white p-6 rounded border border-grey-light" v-else-if="!databases.length">
6+
<div class="context-box" v-else-if="!databases.length">
77
<div class="flex items-center">
88
<database-icon class="flex-none text-grey-darker"></database-icon>
99
<div class="flex-1 px-4 text-grey-darker">
@@ -12,7 +12,7 @@
1212
</div>
1313
</div>
1414
<div v-else>
15-
<div class="bg-white p-6 rounded border border-grey-light mb-6" v-for="database in databases" :key="database.name">
15+
<div class="content-box mb-6" v-for="database in databases" :key="database.name">
1616
<div class="flex items-center text-grey-darker">
1717
<database-icon class="flex-none text-green"></database-icon>
1818
<div class="flex-1 px-4">
@@ -40,22 +40,35 @@
4040
</div>
4141
</div>
4242
</div>
43+
<div>
44+
<button class="btn btn-blue btn-lg" v-on:click="showCreateModal = true">Create new database</button>
45+
</div>
4346
</div>
47+
<modal :show="showCreateModal" v-on:close="showCreateModal = false">
48+
<create-database-modal
49+
v-on:close="showCreateModal = false"
50+
v-on:database="handleModalCallback"
51+
v-if="showCreateModal"
52+
/>
53+
</modal>
4454
</div>
4555
</template>
4656

4757
<script>
4858
import { DatabaseIcon, LockIcon } from 'vue-feather-icons';
4959
import map from 'lodash/map';
60+
import Modal from '../../core/Modal';
61+
import CreateDatabaseModal from '../components/CreateDatabaseModal';
5062
5163
export default {
5264
name: 'databases-page',
53-
components: { DatabaseIcon, LockIcon },
65+
components: {CreateDatabaseModal, Modal, DatabaseIcon, LockIcon },
5466
5567
data: function () {
5668
return {
57-
loading: true,
5869
databases: [],
70+
loading: true,
71+
showCreateModal: false,
5972
};
6073
},
6174
@@ -95,6 +108,22 @@
95108
});
96109
},
97110
111+
/**
112+
* Add the database to the list of existing databases automatically when the modal
113+
* is closed with a successful callback.
114+
*/
115+
handleModalCallback: function (object) {
116+
console.log('handle', object);
117+
118+
const data = object;
119+
data.password = data.relationships.password.attributes.password;
120+
data.showPassword = false;
121+
122+
delete data.relationships;
123+
124+
this.databases.push(data);
125+
},
126+
98127
/**
99128
* Show the password for a given database object.
100129
*

0 commit comments

Comments
 (0)