Support modifying startup variables for servers
This commit is contained in:
parent
1b69d82daa
commit
91cdbd6c2e
11 changed files with 226 additions and 6 deletions
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||||
|
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
use Pterodactyl\Models\Server;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Pterodactyl\Services\Servers\VariableValidatorService;
|
||||||
|
use Pterodactyl\Repositories\Eloquent\ServerVariableRepository;
|
||||||
|
use Pterodactyl\Transformers\Api\Client\EggVariableTransformer;
|
||||||
|
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
use Pterodactyl\Http\Requests\Api\Client\Servers\Startup\UpdateStartupVariableRequest;
|
||||||
|
|
||||||
|
class StartupController extends ClientApiController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var \Pterodactyl\Services\Servers\VariableValidatorService
|
||||||
|
*/
|
||||||
|
private $service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Pterodactyl\Repositories\Eloquent\ServerVariableRepository
|
||||||
|
*/
|
||||||
|
private $repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StartupController constructor.
|
||||||
|
*
|
||||||
|
* @param \Pterodactyl\Services\Servers\VariableValidatorService $service
|
||||||
|
* @param \Pterodactyl\Repositories\Eloquent\ServerVariableRepository $repository
|
||||||
|
*/
|
||||||
|
public function __construct(VariableValidatorService $service, ServerVariableRepository $repository)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Startup;
|
||||||
|
|
||||||
|
use Pterodactyl\Models\Permission;
|
||||||
|
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||||
|
|
||||||
|
class UpdateStartupVariableRequest extends ClientApiRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function permission()
|
||||||
|
{
|
||||||
|
return Permission::ACTION_STARTUP_UPDATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actual validation of the variable's value will happen inside the controller.
|
||||||
|
*
|
||||||
|
* @return array|string[]
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'key' => 'required|string',
|
||||||
|
'value' => 'present|string',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,6 +55,9 @@ class Permission extends Model
|
||||||
const ACTION_FILE_ARCHIVE = 'file.archive';
|
const ACTION_FILE_ARCHIVE = 'file.archive';
|
||||||
const ACTION_FILE_SFTP = 'file.sftp';
|
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_RENAME = 'settings.rename';
|
||||||
const ACTION_SETTINGS_REINSTALL = 'settings.reinstall';
|
const ACTION_SETTINGS_REINSTALL = 'settings.reinstall';
|
||||||
|
|
||||||
|
@ -169,8 +172,8 @@ class Permission extends Model
|
||||||
'startup' => [
|
'startup' => [
|
||||||
'description' => 'Permissions that control a user\'s ability to view this server\'s startup parameters.',
|
'description' => 'Permissions that control a user\'s ability to view this server\'s startup parameters.',
|
||||||
'keys' => [
|
'keys' => [
|
||||||
'read' => '',
|
'read' => 'Allows a user to view the startup variables for a server.',
|
||||||
'update' => '',
|
'update' => 'Allows a user to modify the startup variables for the server.',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,8 @@ rules:
|
||||||
comma-dangle:
|
comma-dangle:
|
||||||
- warn
|
- warn
|
||||||
- always-multiline
|
- always-multiline
|
||||||
|
spaced-comment:
|
||||||
|
- warn
|
||||||
array-bracket-spacing:
|
array-bracket-spacing:
|
||||||
- warn
|
- warn
|
||||||
- always
|
- always
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import http, { FractalResponseData, FractalResponseList } from '@/api/http';
|
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 {
|
export interface Allocation {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -21,7 +22,6 @@ export interface Server {
|
||||||
};
|
};
|
||||||
invocation: string;
|
invocation: string;
|
||||||
description: string;
|
description: string;
|
||||||
allocations: Allocation[];
|
|
||||||
limits: {
|
limits: {
|
||||||
memory: number;
|
memory: number;
|
||||||
swap: number;
|
swap: number;
|
||||||
|
@ -37,6 +37,8 @@ export interface Server {
|
||||||
};
|
};
|
||||||
isSuspended: boolean;
|
isSuspended: boolean;
|
||||||
isInstalling: boolean;
|
isInstalling: boolean;
|
||||||
|
variables: ServerEggVariable[];
|
||||||
|
allocations: Allocation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rawDataToServerObject = ({ attributes: data }: FractalResponseData): Server => ({
|
export const rawDataToServerObject = ({ attributes: data }: FractalResponseData): Server => ({
|
||||||
|
@ -54,6 +56,7 @@ export const rawDataToServerObject = ({ attributes: data }: FractalResponseData)
|
||||||
featureLimits: { ...data.feature_limits },
|
featureLimits: { ...data.feature_limits },
|
||||||
isSuspended: data.is_suspended,
|
isSuspended: data.is_suspended,
|
||||||
isInstalling: data.is_installing,
|
isInstalling: data.is_installing,
|
||||||
|
variables: ((data.relationships?.variables as FractalResponseList | undefined)?.data || []).map(rawDataToServerEggVariable),
|
||||||
allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(rawDataToServerAllocation),
|
allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(rawDataToServerAllocation),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
10
resources/scripts/api/server/types.d.ts
vendored
10
resources/scripts/api/server/types.d.ts
vendored
|
@ -8,3 +8,13 @@ export interface ServerBackup {
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
completedAt: Date | null;
|
completedAt: Date | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ServerEggVariable {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
envVariable: string;
|
||||||
|
defaultValue: string;
|
||||||
|
serverValue: string;
|
||||||
|
isEditable: boolean;
|
||||||
|
rules: string[];
|
||||||
|
}
|
||||||
|
|
9
resources/scripts/api/server/updateStartupVariable.ts
Normal file
9
resources/scripts/api/server/updateStartupVariable.ts
Normal file
|
@ -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<ServerEggVariable> => {
|
||||||
|
const { data } = await http.put(`/api/client/servers/${uuid}/startup/variable`, { key, value });
|
||||||
|
|
||||||
|
return rawDataToServerEggVariable(data);
|
||||||
|
};
|
|
@ -1,7 +1,7 @@
|
||||||
import { Allocation } from '@/api/server/getServer';
|
import { Allocation } from '@/api/server/getServer';
|
||||||
import { FractalResponseData } from '@/api/http';
|
import { FractalResponseData } from '@/api/http';
|
||||||
import { FileObject } from '@/api/server/files/loadDirectory';
|
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 => ({
|
export const rawDataToServerAllocation = (data: FractalResponseData): Allocation => ({
|
||||||
id: data.attributes.id,
|
id: data.attributes.id,
|
||||||
|
@ -51,3 +51,13 @@ export const rawDataToServerBackup = ({ attributes }: FractalResponseData): Serv
|
||||||
createdAt: new Date(attributes.created_at),
|
createdAt: new Date(attributes.created_at),
|
||||||
completedAt: attributes.completed_at ? new Date(attributes.completed_at) : null,
|
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('|'),
|
||||||
|
});
|
||||||
|
|
|
@ -3,9 +3,10 @@ import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||||
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
||||||
import useServer from '@/plugins/useServer';
|
import useServer from '@/plugins/useServer';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
|
import VariableBox from '@/components/server/startup/VariableBox';
|
||||||
|
|
||||||
const StartupContainer = () => {
|
const StartupContainer = () => {
|
||||||
const { invocation } = useServer();
|
const { invocation, variables } = useServer();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock title={'Startup Settings'} showFlashKey={'server:startup'}>
|
<PageContentBlock title={'Startup Settings'} showFlashKey={'server:startup'}>
|
||||||
|
@ -16,6 +17,9 @@ const StartupContainer = () => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</TitledGreyBox>
|
</TitledGreyBox>
|
||||||
|
<div css={tw`grid gap-8 grid-cols-2 mt-10`}>
|
||||||
|
{variables.map(variable => <VariableBox key={variable.envVariable} variable={variable}/>)}
|
||||||
|
</div>
|
||||||
</PageContentBlock>
|
</PageContentBlock>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
64
resources/scripts/components/server/startup/VariableBox.tsx
Normal file
64
resources/scripts/components/server/startup/VariableBox.tsx
Normal file
|
@ -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 (
|
||||||
|
<TitledGreyBox title={variable.name}>
|
||||||
|
<FlashMessageRender byKey={FLASH_KEY} css={tw`mb-4`}/>
|
||||||
|
<InputSpinner visible={loading}>
|
||||||
|
<Input
|
||||||
|
onKeyUp={e => setVariableValue(e.currentTarget.value)}
|
||||||
|
readOnly={!canEdit}
|
||||||
|
name={variable.envVariable}
|
||||||
|
defaultValue={variable.serverValue}
|
||||||
|
placeholder={variable.defaultValue}
|
||||||
|
/>
|
||||||
|
</InputSpinner>
|
||||||
|
<p css={tw`mt-1 text-xs text-neutral-400`}>
|
||||||
|
{variable.description}
|
||||||
|
</p>
|
||||||
|
</TitledGreyBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VariableBox;
|
|
@ -101,6 +101,10 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ
|
||||||
Route::delete('/{backup}', 'Servers\BackupController@delete');
|
Route::delete('/{backup}', 'Servers\BackupController@delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::group(['prefix' => '/startup'], function () {
|
||||||
|
Route::put('/variable', 'Servers\StartupController@update');
|
||||||
|
});
|
||||||
|
|
||||||
Route::group(['prefix' => '/settings'], function () {
|
Route::group(['prefix' => '/settings'], function () {
|
||||||
Route::post('/rename', 'Servers\SettingsController@rename');
|
Route::post('/rename', 'Servers\SettingsController@rename');
|
||||||
Route::post('/reinstall', 'Servers\SettingsController@reinstall');
|
Route::post('/reinstall', 'Servers\SettingsController@reinstall');
|
||||||
|
|
Loading…
Reference in a new issue