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