From 91cdbd6c2e807b15a4d4748d464311a416b5c132 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 22 Aug 2020 18:13:59 -0700 Subject: [PATCH] Support modifying startup variables for servers --- .../Api/Client/Servers/StartupController.php | 81 +++++++++++++++++++ .../Startup/UpdateStartupVariableRequest.php | 30 +++++++ app/Models/Permission.php | 7 +- resources/scripts/.eslintrc.yml | 2 + resources/scripts/api/server/getServer.ts | 7 +- resources/scripts/api/server/types.d.ts | 10 +++ .../api/server/updateStartupVariable.ts | 9 +++ resources/scripts/api/transformers.ts | 12 ++- .../server/startup/StartupContainer.tsx | 6 +- .../components/server/startup/VariableBox.tsx | 64 +++++++++++++++ routes/api-client.php | 4 + 11 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 app/Http/Controllers/Api/Client/Servers/StartupController.php create mode 100644 app/Http/Requests/Api/Client/Servers/Startup/UpdateStartupVariableRequest.php create mode 100644 resources/scripts/api/server/updateStartupVariable.ts create mode 100644 resources/scripts/components/server/startup/VariableBox.tsx diff --git a/app/Http/Controllers/Api/Client/Servers/StartupController.php b/app/Http/Controllers/Api/Client/Servers/StartupController.php new file mode 100644 index 000000000..6eb1df0ad --- /dev/null +++ b/app/Http/Controllers/Api/Client/Servers/StartupController.php @@ -0,0 +1,81 @@ +service = $service; + $this->repository = $repository; + } + + /** + * Updates a single variable for a server. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Startup\UpdateStartupVariableRequest $request + * @param \Pterodactyl\Models\Server $server + * @return array + * + * @throws \Illuminate\Validation\ValidationException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function update(UpdateStartupVariableRequest $request, Server $server) + { + /** @var \Pterodactyl\Models\EggVariable $variable */ + $variable = $server->variables()->where('env_variable', $request->input('key'))->first(); + + if (is_null($variable) || !$variable->user_viewable || !$variable->user_editable) { + throw new BadRequestHttpException( + "The environment variable you are trying to edit [\"{$request->input('key')}\"] does not exist." + ); + } + + // Revalidate the variable value using the egg variable specific validation rules for it. + $this->validate($request, ['value' => $variable->rules]); + + $this->repository->updateOrCreate([ + 'server_id' => $server->id, + 'variable_id' => $variable->id, + ], [ + 'variable_value' => $request->input('value'), + ]); + + $variable = $variable->refresh(); + $variable->server_value = $request->input('value'); + + return $this->fractal->item($variable) + ->transformWith($this->getTransformer(EggVariableTransformer::class)) + ->toArray(); + } +} diff --git a/app/Http/Requests/Api/Client/Servers/Startup/UpdateStartupVariableRequest.php b/app/Http/Requests/Api/Client/Servers/Startup/UpdateStartupVariableRequest.php new file mode 100644 index 000000000..63005c78b --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Startup/UpdateStartupVariableRequest.php @@ -0,0 +1,30 @@ + 'required|string', + 'value' => 'present|string', + ]; + } +} diff --git a/app/Models/Permission.php b/app/Models/Permission.php index af3dc5cf9..a7eb2709b 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -55,6 +55,9 @@ class Permission extends Model const ACTION_FILE_ARCHIVE = 'file.archive'; const ACTION_FILE_SFTP = 'file.sftp'; + const ACTION_STARTUP_READ = 'startup.read'; + const ACTION_STARTUP_UPDATE = 'startup.update'; + const ACTION_SETTINGS_RENAME = 'settings.rename'; const ACTION_SETTINGS_REINSTALL = 'settings.reinstall'; @@ -169,8 +172,8 @@ class Permission extends Model 'startup' => [ 'description' => 'Permissions that control a user\'s ability to view this server\'s startup parameters.', 'keys' => [ - 'read' => '', - 'update' => '', + 'read' => 'Allows a user to view the startup variables for a server.', + 'update' => 'Allows a user to modify the startup variables for the server.', ], ], diff --git a/resources/scripts/.eslintrc.yml b/resources/scripts/.eslintrc.yml index 0e22c8f66..b18f90af9 100644 --- a/resources/scripts/.eslintrc.yml +++ b/resources/scripts/.eslintrc.yml @@ -39,6 +39,8 @@ rules: comma-dangle: - warn - always-multiline + spaced-comment: + - warn array-bracket-spacing: - warn - always diff --git a/resources/scripts/api/server/getServer.ts b/resources/scripts/api/server/getServer.ts index 36dcffda9..278b21e17 100644 --- a/resources/scripts/api/server/getServer.ts +++ b/resources/scripts/api/server/getServer.ts @@ -1,5 +1,6 @@ import http, { FractalResponseData, FractalResponseList } from '@/api/http'; -import { rawDataToServerAllocation } from '@/api/transformers'; +import { rawDataToServerAllocation, rawDataToServerEggVariable } from '@/api/transformers'; +import { ServerEggVariable } from '@/api/server/types'; export interface Allocation { id: number; @@ -21,7 +22,6 @@ export interface Server { }; invocation: string; description: string; - allocations: Allocation[]; limits: { memory: number; swap: number; @@ -37,6 +37,8 @@ export interface Server { }; isSuspended: boolean; isInstalling: boolean; + variables: ServerEggVariable[]; + allocations: Allocation[]; } export const rawDataToServerObject = ({ attributes: data }: FractalResponseData): Server => ({ @@ -54,6 +56,7 @@ export const rawDataToServerObject = ({ attributes: data }: FractalResponseData) featureLimits: { ...data.feature_limits }, isSuspended: data.is_suspended, isInstalling: data.is_installing, + variables: ((data.relationships?.variables as FractalResponseList | undefined)?.data || []).map(rawDataToServerEggVariable), allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(rawDataToServerAllocation), }); diff --git a/resources/scripts/api/server/types.d.ts b/resources/scripts/api/server/types.d.ts index bcdd7416d..e11a39c45 100644 --- a/resources/scripts/api/server/types.d.ts +++ b/resources/scripts/api/server/types.d.ts @@ -8,3 +8,13 @@ export interface ServerBackup { createdAt: Date; completedAt: Date | null; } + +export interface ServerEggVariable { + name: string; + description: string; + envVariable: string; + defaultValue: string; + serverValue: string; + isEditable: boolean; + rules: string[]; +} diff --git a/resources/scripts/api/server/updateStartupVariable.ts b/resources/scripts/api/server/updateStartupVariable.ts new file mode 100644 index 000000000..88231eccc --- /dev/null +++ b/resources/scripts/api/server/updateStartupVariable.ts @@ -0,0 +1,9 @@ +import http from '@/api/http'; +import { ServerEggVariable } from '@/api/server/types'; +import { rawDataToServerEggVariable } from '@/api/transformers'; + +export default async (uuid: string, key: string, value: string): Promise => { + const { data } = await http.put(`/api/client/servers/${uuid}/startup/variable`, { key, value }); + + return rawDataToServerEggVariable(data); +}; diff --git a/resources/scripts/api/transformers.ts b/resources/scripts/api/transformers.ts index 5f9d337ae..53ee514ed 100644 --- a/resources/scripts/api/transformers.ts +++ b/resources/scripts/api/transformers.ts @@ -1,7 +1,7 @@ import { Allocation } from '@/api/server/getServer'; import { FractalResponseData } from '@/api/http'; import { FileObject } from '@/api/server/files/loadDirectory'; -import { ServerBackup } from '@/api/server/types'; +import { ServerBackup, ServerEggVariable } from '@/api/server/types'; export const rawDataToServerAllocation = (data: FractalResponseData): Allocation => ({ id: data.attributes.id, @@ -51,3 +51,13 @@ export const rawDataToServerBackup = ({ attributes }: FractalResponseData): Serv createdAt: new Date(attributes.created_at), completedAt: attributes.completed_at ? new Date(attributes.completed_at) : null, }); + +export const rawDataToServerEggVariable = ({ attributes }: FractalResponseData): ServerEggVariable => ({ + name: attributes.name, + description: attributes.description, + envVariable: attributes.env_variable, + defaultValue: attributes.default_value, + serverValue: attributes.server_value, + isEditable: attributes.is_editable, + rules: attributes.rules.split('|'), +}); diff --git a/resources/scripts/components/server/startup/StartupContainer.tsx b/resources/scripts/components/server/startup/StartupContainer.tsx index e689b4982..481293145 100644 --- a/resources/scripts/components/server/startup/StartupContainer.tsx +++ b/resources/scripts/components/server/startup/StartupContainer.tsx @@ -3,9 +3,10 @@ import PageContentBlock from '@/components/elements/PageContentBlock'; import TitledGreyBox from '@/components/elements/TitledGreyBox'; import useServer from '@/plugins/useServer'; import tw from 'twin.macro'; +import VariableBox from '@/components/server/startup/VariableBox'; const StartupContainer = () => { - const { invocation } = useServer(); + const { invocation, variables } = useServer(); return ( @@ -16,6 +17,9 @@ const StartupContainer = () => {

+
+ {variables.map(variable => )} +
); }; diff --git a/resources/scripts/components/server/startup/VariableBox.tsx b/resources/scripts/components/server/startup/VariableBox.tsx new file mode 100644 index 000000000..e9e7b58f0 --- /dev/null +++ b/resources/scripts/components/server/startup/VariableBox.tsx @@ -0,0 +1,64 @@ +import React, { useState } from 'react'; +import { ServerEggVariable } from '@/api/server/types'; +import TitledGreyBox from '@/components/elements/TitledGreyBox'; +import { usePermissions } from '@/plugins/usePermissions'; +import InputSpinner from '@/components/elements/InputSpinner'; +import Input from '@/components/elements/Input'; +import tw from 'twin.macro'; +import { debounce } from 'debounce'; +import updateStartupVariable from '@/api/server/updateStartupVariable'; +import useServer from '@/plugins/useServer'; +import { ServerContext } from '@/state/server'; +import useFlash from '@/plugins/useFlash'; +import FlashMessageRender from '@/components/FlashMessageRender'; + +interface Props { + variable: ServerEggVariable; +} + +const VariableBox = ({ variable }: Props) => { + const FLASH_KEY = `server:startup:${variable.envVariable}`; + + const server = useServer(); + const [ loading, setLoading ] = useState(false); + const [ canEdit ] = usePermissions([ 'startup.update' ]); + const { clearFlashes, clearAndAddHttpError } = useFlash(); + + const setServer = ServerContext.useStoreActions(actions => actions.server.setServer); + + const setVariableValue = debounce((value: string) => { + setLoading(true); + clearFlashes(FLASH_KEY); + + updateStartupVariable(server.uuid, variable.envVariable, value) + .then(response => setServer({ + ...server, + variables: server.variables.map(v => v.envVariable === response.envVariable ? response : v), + })) + .catch(error => { + console.error(error); + clearAndAddHttpError({ error, key: FLASH_KEY }); + }) + .then(() => setLoading(false)); + }, 500); + + return ( + + + + setVariableValue(e.currentTarget.value)} + readOnly={!canEdit} + name={variable.envVariable} + defaultValue={variable.serverValue} + placeholder={variable.defaultValue} + /> + +

+ {variable.description} +

+
+ ); +}; + +export default VariableBox; diff --git a/routes/api-client.php b/routes/api-client.php index c9ec16097..c3dfefd83 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -101,6 +101,10 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ Route::delete('/{backup}', 'Servers\BackupController@delete'); }); + Route::group(['prefix' => '/startup'], function () { + Route::put('/variable', 'Servers\StartupController@update'); + }); + Route::group(['prefix' => '/settings'], function () { Route::post('/rename', 'Servers\SettingsController@rename'); Route::post('/reinstall', 'Servers\SettingsController@reinstall');