diff --git a/app/Http/Controllers/Api/Client/Servers/DatabaseController.php b/app/Http/Controllers/Api/Client/Servers/DatabaseController.php index d440f3216..b2a808c15 100644 --- a/app/Http/Controllers/Api/Client/Servers/DatabaseController.php +++ b/app/Http/Controllers/Api/Client/Servers/DatabaseController.php @@ -2,9 +2,11 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers; +use Illuminate\Support\Str; use Illuminate\Http\Response; use Pterodactyl\Models\Server; use Pterodactyl\Models\Database; +use Pterodactyl\Services\Databases\DatabasePasswordService; use Pterodactyl\Transformers\Api\Client\DatabaseTransformer; use Pterodactyl\Services\Databases\DatabaseManagementService; use Pterodactyl\Services\Databases\DeployServerDatabaseService; @@ -13,6 +15,7 @@ use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Http\Requests\Api\Client\Servers\Databases\GetDatabasesRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Databases\StoreDatabaseRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Databases\DeleteDatabaseRequest; +use Pterodactyl\Http\Requests\Api\Client\Servers\Databases\RotatePasswordRequest; class DatabaseController extends ClientApiController { @@ -31,15 +34,22 @@ class DatabaseController extends ClientApiController */ private $managementService; + /** + * @var \Pterodactyl\Services\Databases\DatabasePasswordService + */ + private $passwordService; + /** * DatabaseController constructor. * * @param \Pterodactyl\Services\Databases\DatabaseManagementService $managementService + * @param \Pterodactyl\Services\Databases\DatabasePasswordService $passwordService * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository * @param \Pterodactyl\Services\Databases\DeployServerDatabaseService $deployDatabaseService */ public function __construct( DatabaseManagementService $managementService, + DatabasePasswordService $passwordService, DatabaseRepositoryInterface $repository, DeployServerDatabaseService $deployDatabaseService ) { @@ -48,6 +58,7 @@ class DatabaseController extends ClientApiController $this->deployDatabaseService = $deployDatabaseService; $this->repository = $repository; $this->managementService = $managementService; + $this->passwordService = $passwordService; } /** @@ -81,6 +92,30 @@ class DatabaseController extends ClientApiController ->toArray(); } + /** + * Rotates the password for the given server model and returns a fresh instance to + * the caller. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Databases\RotatePasswordRequest $request + * @return array + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function rotatePassword(RotatePasswordRequest $request) + { + $database = $request->getModel(Database::class); + + $this->passwordService->handle($database, Str::random(24)); + + $database->refresh(); + + return $this->fractal->item($database) + ->parseIncludes(['password']) + ->transformWith($this->getTransformer(DatabaseTransformer::class)) + ->toArray(); + } + /** * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Databases\DeleteDatabaseRequest $request * @return \Illuminate\Http\Response diff --git a/app/Http/Requests/Api/Client/ClientApiRequest.php b/app/Http/Requests/Api/Client/ClientApiRequest.php index 468b294f7..c80ae1a1e 100644 --- a/app/Http/Requests/Api/Client/ClientApiRequest.php +++ b/app/Http/Requests/Api/Client/ClientApiRequest.php @@ -6,6 +6,9 @@ use Pterodactyl\Models\Server; use Pterodactyl\Contracts\Http\ClientPermissionsRequest; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; +/** + * @method \Pterodactyl\Models\User user($guard = null) + */ abstract class ClientApiRequest extends ApplicationApiRequest { /** diff --git a/app/Http/Requests/Api/Client/Servers/Databases/RotatePasswordRequest.php b/app/Http/Requests/Api/Client/Servers/Databases/RotatePasswordRequest.php new file mode 100644 index 000000000..424e3460c --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Databases/RotatePasswordRequest.php @@ -0,0 +1,19 @@ +user()->can('reset-db-password', $this->getModel(Server::class)); + } +} diff --git a/app/Services/Databases/DatabasePasswordService.php b/app/Services/Databases/DatabasePasswordService.php index 8f8a9582d..04543abb0 100644 --- a/app/Services/Databases/DatabasePasswordService.php +++ b/app/Services/Databases/DatabasePasswordService.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Services\Databases; +use Webmozart\Assert\Assert; use Pterodactyl\Models\Database; use Illuminate\Database\ConnectionInterface; use Illuminate\Contracts\Encryption\Encrypter; @@ -63,6 +64,8 @@ class DatabasePasswordService public function handle($database, string $password): bool { if (! $database instanceof Database) { + Assert::integerish($database); + $database = $this->repository->find($database); } diff --git a/resources/scripts/api/server/rotateDatabasePassword.ts b/resources/scripts/api/server/rotateDatabasePassword.ts new file mode 100644 index 000000000..c6c9e8aef --- /dev/null +++ b/resources/scripts/api/server/rotateDatabasePassword.ts @@ -0,0 +1,10 @@ +import { rawDataToServerDatabase, ServerDatabase } from '@/api/server/getServerDatabases'; +import http from '@/api/http'; + +export default (uuid: string, database: string): Promise => { + return new Promise((resolve, reject) => { + http.post(`/api/client/servers/${uuid}/databases/${database}/rotate-password`) + .then((response) => resolve(rawDataToServerDatabase(response.data.attributes))) + .catch(reject); + }); +}; diff --git a/resources/scripts/components/elements/Button.tsx b/resources/scripts/components/elements/Button.tsx new file mode 100644 index 000000000..04bb13a81 --- /dev/null +++ b/resources/scripts/components/elements/Button.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import classNames from 'classnames'; + +type Props = { isLoading?: boolean } & React.DetailedHTMLProps, HTMLButtonElement>; + +export default ({ isLoading, children, className, ...props }: Props) => ( + +); diff --git a/resources/scripts/components/server/databases/DatabaseRow.tsx b/resources/scripts/components/server/databases/DatabaseRow.tsx index 3cad11755..0092c7cb7 100644 --- a/resources/scripts/components/server/databases/DatabaseRow.tsx +++ b/resources/scripts/components/server/databases/DatabaseRow.tsx @@ -15,19 +15,26 @@ import { ApplicationStore } from '@/state'; import { ServerContext } from '@/state/server'; import deleteServerDatabase from '@/api/server/deleteServerDatabase'; import { httpErrorToHuman } from '@/api/http'; +import RotatePasswordButton from '@/components/server/databases/RotatePasswordButton'; interface Props { - database: ServerDatabase; + databaseId: string | number; className?: string; onDelete: () => void; } -export default ({ database, className, onDelete }: Props) => { +export default ({ databaseId, className, onDelete }: Props) => { const [visible, setVisible] = useState(false); + const database = ServerContext.useStoreState(state => state.databases.items.find(item => item.id === databaseId)); + const appendDatabase = ServerContext.useStoreActions(actions => actions.databases.appendDatabase); const [connectionVisible, setConnectionVisible] = useState(false); const { addFlash, clearFlashes } = useStoreActions((actions: Actions) => actions.flashes); const server = ServerContext.useStoreState(state => state.server.data!); + if (!database) { + return null; + } + const schema = object().shape({ confirm: string() .required('The database name must be provided.') @@ -104,6 +111,7 @@ export default ({ database, className, onDelete }: Props) => { } setConnectionVisible(false)}> +

Database connection details

@@ -119,6 +127,7 @@ export default ({ database, className, onDelete }: Props) => { />
+ diff --git a/resources/scripts/components/server/databases/DatabasesContainer.tsx b/resources/scripts/components/server/databases/DatabasesContainer.tsx index a7c2c1d72..105db5828 100644 --- a/resources/scripts/components/server/databases/DatabasesContainer.tsx +++ b/resources/scripts/components/server/databases/DatabasesContainer.tsx @@ -12,12 +12,15 @@ import CreateDatabaseButton from '@/components/server/databases/CreateDatabaseBu export default () => { const [ loading, setLoading ] = useState(true); - const [ databases, setDatabases ] = useState([]); const server = ServerContext.useStoreState(state => state.server.data!); + const databases = ServerContext.useStoreState(state => state.databases.items); + const { setDatabases, appendDatabase, removeDatabase } = ServerContext.useStoreActions(state => state.databases); const { addFlash, clearFlashes } = useStoreActions((actions: Actions) => actions.flashes); useEffect(() => { + setLoading(!databases.length); clearFlashes('databases'); + getServerDatabases(server.uuid) .then(databases => { setDatabases(databases); @@ -43,8 +46,8 @@ export default () => { databases.map((database, index) => ( setDatabases(s => [ ...s.filter(d => d.id !== database.id) ])} + databaseId={database.id} + onDelete={() => removeDatabase(database)} className={index > 0 ? 'mt-1' : undefined} /> )) @@ -54,7 +57,7 @@ export default () => {

}
- setDatabases(s => [ ...s, database ])}/> +
diff --git a/resources/scripts/components/server/databases/RotatePasswordButton.tsx b/resources/scripts/components/server/databases/RotatePasswordButton.tsx new file mode 100644 index 000000000..fdb31a1e2 --- /dev/null +++ b/resources/scripts/components/server/databases/RotatePasswordButton.tsx @@ -0,0 +1,45 @@ +import React, { useState } from 'react'; +import rotateDatabasePassword from '@/api/server/rotateDatabasePassword'; +import { Actions, useStoreActions } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; +import { ServerContext } from '@/state/server'; +import { ServerDatabase } from '@/api/server/getServerDatabases'; +import { httpErrorToHuman } from '@/api/http'; +import Button from '@/components/elements/Button'; + +export default ({ databaseId, onUpdate }: { + databaseId: string; + onUpdate: (database: ServerDatabase) => void; +}) => { + const [ loading, setLoading ] = useState(false); + const { addFlash, clearFlashes } = useStoreActions((actions: Actions) => actions.flashes); + const server = ServerContext.useStoreState(state => state.server.data!); + + if (!databaseId) { + return null; + } + + const rotate = () => { + setLoading(true); + clearFlashes(); + + rotateDatabasePassword(server.uuid, databaseId) + .then(database => onUpdate(database)) + .catch(error => { + console.error(error); + addFlash({ + type: 'error', + title: 'Error', + message: httpErrorToHuman(error), + key: 'database-connection-modal', + }); + }) + .then(() => setLoading(false)); + }; + + return ( + + ); +}; diff --git a/resources/scripts/state/server/index.ts b/resources/scripts/state/server/index.ts index c29e61bf8..5c6314fc5 100644 --- a/resources/scripts/state/server/index.ts +++ b/resources/scripts/state/server/index.ts @@ -1,6 +1,7 @@ import getServer, { Server } from '@/api/server/getServer'; import { action, Action, createContextStore, thunk, Thunk } from 'easy-peasy'; import socket, { SocketStore } from './socket'; +import { ServerDatabase } from '@/api/server/getServerDatabases'; export type ServerStatus = 'offline' | 'starting' | 'stopping' | 'running'; @@ -32,8 +33,29 @@ const status: ServerStatusStore = { }), }; +interface ServerDatabaseStore { + items: ServerDatabase[]; + setDatabases: Action; + appendDatabase: Action; + removeDatabase: Action; +} + +const databases: ServerDatabaseStore = { + items: [], + setDatabases: action((state, payload) => { + state.items = payload; + }), + appendDatabase: action((state, payload) => { + state.items = state.items.filter(item => item.id !== payload.id).concat(payload); + }), + removeDatabase: action((state, payload) => { + state.items = state.items.filter(item => item.id !== payload.id); + }), +}; + export interface ServerStore { server: ServerDataStore; + databases: ServerDatabaseStore; socket: SocketStore; status: ServerStatusStore; clearServerState: Action; @@ -43,8 +65,10 @@ export const ServerContext = createContextStore({ server, socket, status, + databases, clearServerState: action(state => { state.server.data = undefined; + state.databases.items = []; if (state.socket.instance) { state.socket.instance.removeAllListeners(); diff --git a/routes/api-client.php b/routes/api-client.php index 7bf174ef1..ed5286935 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -38,6 +38,7 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ Route::group(['prefix' => '/databases'], function () { Route::get('/', 'Servers\DatabaseController@index')->name('api.client.servers.databases'); Route::post('/', 'Servers\DatabaseController@store'); + Route::post('/{database}/rotate-password', 'Servers\DatabaseController@rotatePassword'); Route::delete('/{database}', 'Servers\DatabaseController@delete')->name('api.client.servers.databases.delete'); });