Skip to content

Commit 91cdbd6

Browse files
committed
Support modifying startup variables for servers
1 parent 1b69d82 commit 91cdbd6

File tree

11 files changed

+226
-6
lines changed

11 files changed

+226
-6
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
4+
5+
use Carbon\CarbonImmutable;
6+
use Pterodactyl\Models\Server;
7+
use Illuminate\Http\JsonResponse;
8+
use Pterodactyl\Services\Servers\VariableValidatorService;
9+
use Pterodactyl\Repositories\Eloquent\ServerVariableRepository;
10+
use Pterodactyl\Transformers\Api\Client\EggVariableTransformer;
11+
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
12+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
13+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
14+
use Pterodactyl\Http\Requests\Api\Client\Servers\Startup\UpdateStartupVariableRequest;
15+
16+
class StartupController extends ClientApiController
17+
{
18+
/**
19+
* @var \Pterodactyl\Services\Servers\VariableValidatorService
20+
*/
21+
private $service;
22+
23+
/**
24+
* @var \Pterodactyl\Repositories\Eloquent\ServerVariableRepository
25+
*/
26+
private $repository;
27+
28+
/**
29+
* StartupController constructor.
30+
*
31+
* @param \Pterodactyl\Services\Servers\VariableValidatorService $service
32+
* @param \Pterodactyl\Repositories\Eloquent\ServerVariableRepository $repository
33+
*/
34+
public function __construct(VariableValidatorService $service, ServerVariableRepository $repository)
35+
{
36+
parent::__construct();
37+
38+
$this->service = $service;
39+
$this->repository = $repository;
40+
}
41+
42+
/**
43+
* Updates a single variable for a server.
44+
*
45+
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Startup\UpdateStartupVariableRequest $request
46+
* @param \Pterodactyl\Models\Server $server
47+
* @return array
48+
*
49+
* @throws \Illuminate\Validation\ValidationException
50+
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
51+
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
52+
*/
53+
public function update(UpdateStartupVariableRequest $request, Server $server)
54+
{
55+
/** @var \Pterodactyl\Models\EggVariable $variable */
56+
$variable = $server->variables()->where('env_variable', $request->input('key'))->first();
57+
58+
if (is_null($variable) || !$variable->user_viewable || !$variable->user_editable) {
59+
throw new BadRequestHttpException(
60+
"The environment variable you are trying to edit [\"{$request->input('key')}\"] does not exist."
61+
);
62+
}
63+
64+
// Revalidate the variable value using the egg variable specific validation rules for it.
65+
$this->validate($request, ['value' => $variable->rules]);
66+
67+
$this->repository->updateOrCreate([
68+
'server_id' => $server->id,
69+
'variable_id' => $variable->id,
70+
], [
71+
'variable_value' => $request->input('value'),
72+
]);
73+
74+
$variable = $variable->refresh();
75+
$variable->server_value = $request->input('value');
76+
77+
return $this->fractal->item($variable)
78+
->transformWith($this->getTransformer(EggVariableTransformer::class))
79+
->toArray();
80+
}
81+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Startup;
4+
5+
use Pterodactyl\Models\Permission;
6+
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
7+
8+
class UpdateStartupVariableRequest extends ClientApiRequest
9+
{
10+
/**
11+
* @return string
12+
*/
13+
public function permission()
14+
{
15+
return Permission::ACTION_STARTUP_UPDATE;
16+
}
17+
18+
/**
19+
* The actual validation of the variable's value will happen inside the controller.
20+
*
21+
* @return array|string[]
22+
*/
23+
public function rules(): array
24+
{
25+
return [
26+
'key' => 'required|string',
27+
'value' => 'present|string',
28+
];
29+
}
30+
}

app/Models/Permission.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ class Permission extends Model
5555
const ACTION_FILE_ARCHIVE = 'file.archive';
5656
const ACTION_FILE_SFTP = 'file.sftp';
5757

58+
const ACTION_STARTUP_READ = 'startup.read';
59+
const ACTION_STARTUP_UPDATE = 'startup.update';
60+
5861
const ACTION_SETTINGS_RENAME = 'settings.rename';
5962
const ACTION_SETTINGS_REINSTALL = 'settings.reinstall';
6063

@@ -169,8 +172,8 @@ class Permission extends Model
169172
'startup' => [
170173
'description' => 'Permissions that control a user\'s ability to view this server\'s startup parameters.',
171174
'keys' => [
172-
'read' => '',
173-
'update' => '',
175+
'read' => 'Allows a user to view the startup variables for a server.',
176+
'update' => 'Allows a user to modify the startup variables for the server.',
174177
],
175178
],
176179

resources/scripts/.eslintrc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ rules:
3939
comma-dangle:
4040
- warn
4141
- always-multiline
42+
spaced-comment:
43+
- warn
4244
array-bracket-spacing:
4345
- warn
4446
- always

resources/scripts/api/server/getServer.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import http, { FractalResponseData, FractalResponseList } from '@/api/http';
2-
import { rawDataToServerAllocation } from '@/api/transformers';
2+
import { rawDataToServerAllocation, rawDataToServerEggVariable } from '@/api/transformers';
3+
import { ServerEggVariable } from '@/api/server/types';
34

45
export interface Allocation {
56
id: number;
@@ -21,7 +22,6 @@ export interface Server {
2122
};
2223
invocation: string;
2324
description: string;
24-
allocations: Allocation[];
2525
limits: {
2626
memory: number;
2727
swap: number;
@@ -37,6 +37,8 @@ export interface Server {
3737
};
3838
isSuspended: boolean;
3939
isInstalling: boolean;
40+
variables: ServerEggVariable[];
41+
allocations: Allocation[];
4042
}
4143

4244
export const rawDataToServerObject = ({ attributes: data }: FractalResponseData): Server => ({
@@ -54,6 +56,7 @@ export const rawDataToServerObject = ({ attributes: data }: FractalResponseData)
5456
featureLimits: { ...data.feature_limits },
5557
isSuspended: data.is_suspended,
5658
isInstalling: data.is_installing,
59+
variables: ((data.relationships?.variables as FractalResponseList | undefined)?.data || []).map(rawDataToServerEggVariable),
5760
allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(rawDataToServerAllocation),
5861
});
5962

resources/scripts/api/server/types.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,13 @@ export interface ServerBackup {
88
createdAt: Date;
99
completedAt: Date | null;
1010
}
11+
12+
export interface ServerEggVariable {
13+
name: string;
14+
description: string;
15+
envVariable: string;
16+
defaultValue: string;
17+
serverValue: string;
18+
isEditable: boolean;
19+
rules: string[];
20+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import http from '@/api/http';
2+
import { ServerEggVariable } from '@/api/server/types';
3+
import { rawDataToServerEggVariable } from '@/api/transformers';
4+
5+
export default async (uuid: string, key: string, value: string): Promise<ServerEggVariable> => {
6+
const { data } = await http.put(`/api/client/servers/${uuid}/startup/variable`, { key, value });
7+
8+
return rawDataToServerEggVariable(data);
9+
};

resources/scripts/api/transformers.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Allocation } from '@/api/server/getServer';
22
import { FractalResponseData } from '@/api/http';
33
import { FileObject } from '@/api/server/files/loadDirectory';
4-
import { ServerBackup } from '@/api/server/types';
4+
import { ServerBackup, ServerEggVariable } from '@/api/server/types';
55

66
export const rawDataToServerAllocation = (data: FractalResponseData): Allocation => ({
77
id: data.attributes.id,
@@ -51,3 +51,13 @@ export const rawDataToServerBackup = ({ attributes }: FractalResponseData): Serv
5151
createdAt: new Date(attributes.created_at),
5252
completedAt: attributes.completed_at ? new Date(attributes.completed_at) : null,
5353
});
54+
55+
export const rawDataToServerEggVariable = ({ attributes }: FractalResponseData): ServerEggVariable => ({
56+
name: attributes.name,
57+
description: attributes.description,
58+
envVariable: attributes.env_variable,
59+
defaultValue: attributes.default_value,
60+
serverValue: attributes.server_value,
61+
isEditable: attributes.is_editable,
62+
rules: attributes.rules.split('|'),
63+
});

resources/scripts/components/server/startup/StartupContainer.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import PageContentBlock from '@/components/elements/PageContentBlock';
33
import TitledGreyBox from '@/components/elements/TitledGreyBox';
44
import useServer from '@/plugins/useServer';
55
import tw from 'twin.macro';
6+
import VariableBox from '@/components/server/startup/VariableBox';
67

78
const StartupContainer = () => {
8-
const { invocation } = useServer();
9+
const { invocation, variables } = useServer();
910

1011
return (
1112
<PageContentBlock title={'Startup Settings'} showFlashKey={'server:startup'}>
@@ -16,6 +17,9 @@ const StartupContainer = () => {
1617
</p>
1718
</div>
1819
</TitledGreyBox>
20+
<div css={tw`grid gap-8 grid-cols-2 mt-10`}>
21+
{variables.map(variable => <VariableBox key={variable.envVariable} variable={variable}/>)}
22+
</div>
1923
</PageContentBlock>
2024
);
2125
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React, { useState } from 'react';
2+
import { ServerEggVariable } from '@/api/server/types';
3+
import TitledGreyBox from '@/components/elements/TitledGreyBox';
4+
import { usePermissions } from '@/plugins/usePermissions';
5+
import InputSpinner from '@/components/elements/InputSpinner';
6+
import Input from '@/components/elements/Input';
7+
import tw from 'twin.macro';
8+
import { debounce } from 'debounce';
9+
import updateStartupVariable from '@/api/server/updateStartupVariable';
10+
import useServer from '@/plugins/useServer';
11+
import { ServerContext } from '@/state/server';
12+
import useFlash from '@/plugins/useFlash';
13+
import FlashMessageRender from '@/components/FlashMessageRender';
14+
15+
interface Props {
16+
variable: ServerEggVariable;
17+
}
18+
19+
const VariableBox = ({ variable }: Props) => {
20+
const FLASH_KEY = `server:startup:${variable.envVariable}`;
21+
22+
const server = useServer();
23+
const [ loading, setLoading ] = useState(false);
24+
const [ canEdit ] = usePermissions([ 'startup.update' ]);
25+
const { clearFlashes, clearAndAddHttpError } = useFlash();
26+
27+
const setServer = ServerContext.useStoreActions(actions => actions.server.setServer);
28+
29+
const setVariableValue = debounce((value: string) => {
30+
setLoading(true);
31+
clearFlashes(FLASH_KEY);
32+
33+
updateStartupVariable(server.uuid, variable.envVariable, value)
34+
.then(response => setServer({
35+
...server,
36+
variables: server.variables.map(v => v.envVariable === response.envVariable ? response : v),
37+
}))
38+
.catch(error => {
39+
console.error(error);
40+
clearAndAddHttpError({ error, key: FLASH_KEY });
41+
})
42+
.then(() => setLoading(false));
43+
}, 500);
44+
45+
return (
46+
<TitledGreyBox title={variable.name}>
47+
<FlashMessageRender byKey={FLASH_KEY} css={tw`mb-4`}/>
48+
<InputSpinner visible={loading}>
49+
<Input
50+
onKeyUp={e => setVariableValue(e.currentTarget.value)}
51+
readOnly={!canEdit}
52+
name={variable.envVariable}
53+
defaultValue={variable.serverValue}
54+
placeholder={variable.defaultValue}
55+
/>
56+
</InputSpinner>
57+
<p css={tw`mt-1 text-xs text-neutral-400`}>
58+
{variable.description}
59+
</p>
60+
</TitledGreyBox>
61+
);
62+
};
63+
64+
export default VariableBox;

0 commit comments

Comments
 (0)