From 3ebb6eadbfdbb827cdad782cf4edf7df4e003ae6 Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Sun, 12 Jul 2020 15:20:37 -0600 Subject: [PATCH 01/54] Add upload button and drag&drop modal --- public/assets/svgs/file_upload.svg | 1 + .../server/files/FileManagerContainer.tsx | 2 + .../components/server/files/UploadButton.tsx | 60 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 public/assets/svgs/file_upload.svg create mode 100644 resources/scripts/components/server/files/UploadButton.tsx diff --git a/public/assets/svgs/file_upload.svg b/public/assets/svgs/file_upload.svg new file mode 100644 index 000000000..f11a922ff --- /dev/null +++ b/public/assets/svgs/file_upload.svg @@ -0,0 +1 @@ +going up \ No newline at end of file diff --git a/resources/scripts/components/server/files/FileManagerContainer.tsx b/resources/scripts/components/server/files/FileManagerContainer.tsx index 520365104..1e3e29ce6 100644 --- a/resources/scripts/components/server/files/FileManagerContainer.tsx +++ b/resources/scripts/components/server/files/FileManagerContainer.tsx @@ -16,6 +16,7 @@ import useServer from '@/plugins/useServer'; import { ServerContext } from '@/state/server'; import useFileManagerSwr from '@/plugins/useFileManagerSwr'; import MassActionsBar from '@/components/server/files/MassActionsBar'; +import UploadButton from '@/components/server/files/UploadButton'; const sortFiles = (files: FileObject[]): FileObject[] => { return files.sort((a, b) => a.name.localeCompare(b.name)) @@ -79,6 +80,7 @@ export default () => {
+ + + ); +}; From 1d2acbd5b4b7a4de2afb13b7ba23b219aa50cf93 Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Sun, 12 Jul 2020 16:42:32 -0600 Subject: [PATCH 02/54] Get basic file upload functionality working --- .../Servers/DownloadBackupController.php | 2 +- .../Client/Servers/FileUploadController.php | 73 +++++++++++++++++++ .../Servers/Files/UploadFileRequest.php | 17 +++++ .../api/server/files/getFileUploadUrl.ts | 9 +++ .../components/server/files/UploadButton.tsx | 59 +++++++++++++-- routes/api-client.php | 1 + 6 files changed, 152 insertions(+), 9 deletions(-) create mode 100644 app/Http/Controllers/Api/Client/Servers/FileUploadController.php create mode 100644 app/Http/Requests/Api/Client/Servers/Files/UploadFileRequest.php create mode 100644 resources/scripts/api/server/files/getFileUploadUrl.ts diff --git a/app/Http/Controllers/Api/Client/Servers/DownloadBackupController.php b/app/Http/Controllers/Api/Client/Servers/DownloadBackupController.php index 1d22b7c35..4c8b16a25 100644 --- a/app/Http/Controllers/Api/Client/Servers/DownloadBackupController.php +++ b/app/Http/Controllers/Api/Client/Servers/DownloadBackupController.php @@ -82,7 +82,7 @@ class DownloadBackupController extends ClientApiController throw new BadRequestHttpException; } - return JsonResponse::create([ + return new JsonResponse([ 'object' => 'signed_url', 'attributes' => [ 'url' => $url, diff --git a/app/Http/Controllers/Api/Client/Servers/FileUploadController.php b/app/Http/Controllers/Api/Client/Servers/FileUploadController.php new file mode 100644 index 000000000..e8f5ad080 --- /dev/null +++ b/app/Http/Controllers/Api/Client/Servers/FileUploadController.php @@ -0,0 +1,73 @@ +jwtService = $jwtService; + } + + /** + * Returns a url where files can be uploaded to. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\UploadFileRequest $request + * @param \Pterodactyl\Models\Server $server + * + * @return \Illuminate\Http\JsonResponse + */ + public function __invoke(UploadFileRequest $request, Server $server) + { + return new JsonResponse([ + 'object' => 'signed_url', + 'attributes' => [ + 'url' => $this->getUploadUrl($server, $request->user()), + ], + ]); + } + + /** + * Returns a url where files can be uploaded to. + * + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\User $user + * @return string + */ + protected function getUploadUrl(Server $server, User $user) + { + $token = $this->jwtService + ->setExpiresAt(CarbonImmutable::now()->addMinutes(15)) + ->setClaims([ + 'server_uuid' => $server->uuid, + ]) + ->handle($server->node, $user->id . $server->uuid); + + return sprintf( + '%s/upload/file?token=%s', + $server->node->getConnectionAddress(), + $token->__toString() + ); + } +} diff --git a/app/Http/Requests/Api/Client/Servers/Files/UploadFileRequest.php b/app/Http/Requests/Api/Client/Servers/Files/UploadFileRequest.php new file mode 100644 index 000000000..6808a5497 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Files/UploadFileRequest.php @@ -0,0 +1,17 @@ + => { + return new Promise((resolve, reject) => { + http.get(`/api/client/servers/${uuid}/files/upload`) + .then(({ data }) => resolve(data.attributes.url)) + .catch(reject); + }); +}; diff --git a/resources/scripts/components/server/files/UploadButton.tsx b/resources/scripts/components/server/files/UploadButton.tsx index 16a44d39d..c3cc78355 100644 --- a/resources/scripts/components/server/files/UploadButton.tsx +++ b/resources/scripts/components/server/files/UploadButton.tsx @@ -1,6 +1,9 @@ +import axios from 'axios'; +import getFileUploadUrl from '@/api/server/files/getFileUploadUrl'; +import useServer from '@/plugins/useServer'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components/macro'; const ModalMask = styled.div` @@ -9,31 +12,71 @@ const ModalMask = styled.div` `; export default () => { + const { uuid } = useServer(); const [ visible, setVisible ] = useState(false); + const handleEscapeEvent = (e: KeyboardEvent) => { + setVisible(false); + }; + + useEffect(() => { + window.addEventListener('keydown', handleEscapeEvent); + + return () => window.removeEventListener('keydown', handleEscapeEvent); + }, [ visible ]); + const onDragOver = (e: any) => { e.preventDefault(); - - //console.log(e); }; const onDragEnter = (e: any) => { e.preventDefault(); - - //console.log(e); }; const onDragLeave = (e: any) => { e.preventDefault(); - - //console.log(e); }; const onFileDrop = (e: any) => { e.preventDefault(); - const files = e.dataTransfer.files; + if (e.dataTransfer === undefined || e.dataTransfer === null) { + return; + } + + const files: FileList = e.dataTransfer.files; console.log(files); + + const formData = new FormData(); + + for (let i = 0; i < files.length; i++) { + console.log(files[i]); + // @ts-ignore + formData.append('files', files[i]); + } + + console.log('getFileUploadUrl'); + getFileUploadUrl(uuid) + .then(url => { + console.log(url); + + // `${url}&directory=` + axios.post(url, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + .then(res => { + console.log(res); + setVisible(false); + }) + .catch(error => { + console.error(error); + }); + }) + .catch(error => { + console.error(error); + }); }; return ( diff --git a/routes/api-client.php b/routes/api-client.php index 9b57cf0c4..eb01ed85f 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -62,6 +62,7 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ Route::post('/compress', 'Servers\FileController@compress'); Route::post('/delete', 'Servers\FileController@delete'); Route::post('/create-folder', 'Servers\FileController@create'); + Route::get('/upload', 'Servers\FileUploadController'); }); Route::group(['prefix' => '/schedules'], function () { From 67ba3baff0ef166a032d4dc7856faa58973c86ce Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Sun, 12 Jul 2020 16:47:00 -0600 Subject: [PATCH 03/54] Fix lint warnings --- resources/scripts/components/server/files/UploadButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/scripts/components/server/files/UploadButton.tsx b/resources/scripts/components/server/files/UploadButton.tsx index c3cc78355..e328c5dc8 100644 --- a/resources/scripts/components/server/files/UploadButton.tsx +++ b/resources/scripts/components/server/files/UploadButton.tsx @@ -15,7 +15,7 @@ export default () => { const { uuid } = useServer(); const [ visible, setVisible ] = useState(false); - const handleEscapeEvent = (e: KeyboardEvent) => { + const handleEscapeEvent = () => { setVisible(false); }; From 1ced8da735fb95b6cfe1b094c3c900573d4cfa2d Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 17 Aug 2020 19:04:23 -0700 Subject: [PATCH 04/54] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index d788fb7c0..eae7dfc65 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ in becoming a sponsor?](https://github.com/sponsors/DaneEveritt) > DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance > and giving you the best performance money can buy. +### [Skynode](https://skynode.com) +> Skynode provides blazing fast game servers along with a top notch user experience. Whatever our clients are looking +> for, we're able to provide it! ## Support & Documentation Support for using Pterodactyl can be found on our [Documentation Website](https://pterodactyl.io/project/introduction.html), [Guides Website](https://pterodactyl.io/community/about.html), or via our [Discord Chat](https://discord.gg/QRDZvVm). From a6cc53793d45322e0bd966382ffdc456e09c58c0 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 17 Aug 2020 19:05:01 -0700 Subject: [PATCH 05/54] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eae7dfc65..84d428ee0 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ in becoming a sponsor?](https://github.com/sponsors/DaneEveritt) > DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance > and giving you the best performance money can buy. -### [Skynode](https://skynode.com) +#### [Skynode](https://www.skynode.pro/) > Skynode provides blazing fast game servers along with a top notch user experience. Whatever our clients are looking > for, we're able to provide it! From d41b86f0ea77eafbaff8b79f663497f8e02240cc Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 17 Aug 2020 19:48:51 -0700 Subject: [PATCH 06/54] Correctly pass along allowed IPs for client API keys, closes #2244 --- .../Requests/Api/Client/Account/StoreApiKeyRequest.php | 10 ++++++++++ .../components/dashboard/forms/CreateApiKeyForm.tsx | 10 +++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/Http/Requests/Api/Client/Account/StoreApiKeyRequest.php b/app/Http/Requests/Api/Client/Account/StoreApiKeyRequest.php index 00197388a..a82db1ec0 100644 --- a/app/Http/Requests/Api/Client/Account/StoreApiKeyRequest.php +++ b/app/Http/Requests/Api/Client/Account/StoreApiKeyRequest.php @@ -17,4 +17,14 @@ class StoreApiKeyRequest extends ClientApiRequest 'allowed_ips.*' => 'ip', ]; } + + /** + * @return array|string[] + */ + public function messages() + { + return [ + 'allowed_ips.*' => 'All of the IP addresses entered must be valid IPv4 addresses.', + ]; + } } diff --git a/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx b/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx index 4e8fae1d5..3e52e68ae 100644 --- a/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx +++ b/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx @@ -12,12 +12,15 @@ import { ApiKey } from '@/api/account/getApiKeys'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; import Input, { Textarea } from '@/components/elements/Input'; +import styled from 'styled-components/macro'; interface Values { description: string; allowedIps: string; } +const CustomTextarea = styled(Textarea)`${tw`h-32`}`; + export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => { const [ apiKey, setApiKey ] = useState(''); const { addError, clearFlashes } = useStoreActions((actions: Actions) => actions.flashes); @@ -66,10 +69,7 @@ export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => { void }) => { name={'allowedIps'} description={'Leave blank to allow any IP address to use this API key, otherwise provide each IP address on a new line.'} > - +
From c28cba92e2192b1be8c18f8923fcb95225c8b02c Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 17 Aug 2020 21:35:11 -0700 Subject: [PATCH 07/54] Make modals programatically controllable via a HOC This allows entire components to be unmounted when the modal is hidden without affecting the fade in/out of the modal itself. This also makes it easier to programatically dismiss a modal without having to copy the visibility all over the place, and makes working with props much simpler in those modal components --- .../dashboard/AccountApiContainer.tsx | 6 +- .../components/dashboard/ApiKeyModal.tsx | 38 +++++++++ .../dashboard/forms/CreateApiKeyForm.tsx | 28 ++----- .../components/elements/ConfirmationModal.tsx | 52 ++++++------ .../scripts/components/elements/Fade.tsx | 6 +- .../scripts/components/elements/Modal.tsx | 12 ++- .../server/backups/BackupContextMenu.tsx | 6 +- .../server/files/MassActionsBar.tsx | 2 +- .../server/schedules/DeleteScheduleButton.tsx | 6 +- .../server/schedules/ScheduleTaskRow.tsx | 2 +- .../server/settings/ReinstallServerBox.tsx | 4 +- .../server/users/RemoveSubuserButton.tsx | 4 +- resources/scripts/context/ModalContext.ts | 15 ++++ resources/scripts/hoc/asModal.tsx | 81 +++++++++++++++++++ 14 files changed, 192 insertions(+), 70 deletions(-) create mode 100644 resources/scripts/components/dashboard/ApiKeyModal.tsx create mode 100644 resources/scripts/context/ModalContext.ts create mode 100644 resources/scripts/hoc/asModal.tsx diff --git a/resources/scripts/components/dashboard/AccountApiContainer.tsx b/resources/scripts/components/dashboard/AccountApiContainer.tsx index c80a51a20..304fe5630 100644 --- a/resources/scripts/components/dashboard/AccountApiContainer.tsx +++ b/resources/scripts/components/dashboard/AccountApiContainer.tsx @@ -61,21 +61,19 @@ export default () => { - {deleteIdentifier && { doDeletion(deleteIdentifier); setDeleteIdentifier(''); }} - onDismissed={() => setDeleteIdentifier('')} + onModalDismissed={() => setDeleteIdentifier('')} > Are you sure you wish to delete this API key? All requests using it will immediately be invalidated and will fail. - } { keys.length === 0 ?

diff --git a/resources/scripts/components/dashboard/ApiKeyModal.tsx b/resources/scripts/components/dashboard/ApiKeyModal.tsx new file mode 100644 index 000000000..db511edb8 --- /dev/null +++ b/resources/scripts/components/dashboard/ApiKeyModal.tsx @@ -0,0 +1,38 @@ +import React, { useContext } from 'react'; +import tw from 'twin.macro'; +import Button from '@/components/elements/Button'; +import asModal from '@/hoc/asModal'; +import ModalContext from '@/context/ModalContext'; + +interface Props { + apiKey: string; +} + +const ApiKeyModal = ({ apiKey }: Props) => { + const { dismiss } = useContext(ModalContext); + + return ( + <> +

Your API Key

+

+ The API key you have requested is shown below. Please store this in a safe location, it will not be + shown again. +

+
+                {apiKey}
+            
+
+ +
+ + ); +}; + +ApiKeyModal.displayName = 'ApiKeyModal'; + +export default asModal({ + closeOnEscape: false, + closeOnBackground: false, +})(ApiKeyModal); diff --git a/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx b/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx index 3e52e68ae..9022ae6c8 100644 --- a/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx +++ b/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import { Field, Form, Formik, FormikHelpers } from 'formik'; import { object, string } from 'yup'; import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper'; -import Modal from '@/components/elements/Modal'; import createApiKey from '@/api/account/createApiKey'; import { Actions, useStoreActions } from 'easy-peasy'; import { ApplicationStore } from '@/state'; @@ -13,6 +12,7 @@ import tw from 'twin.macro'; import Button from '@/components/elements/Button'; import Input, { Textarea } from '@/components/elements/Input'; import styled from 'styled-components/macro'; +import ApiKeyModal from '@/components/dashboard/ApiKeyModal'; interface Values { description: string; @@ -44,29 +44,11 @@ export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => { return ( <> - 0} - onDismissed={() => setApiKey('')} - closeOnEscape={false} - closeOnBackground={false} - > -

Your API Key

-

- The API key you have requested is shown below. Please store this in a safe location, it will not be - shown again. -

-
-                    {apiKey}
-                
-
- -
-
+ onModalDismissed={() => setApiKey('')} + apiKey={apiKey} + /> void; showSpinnerOverlay?: boolean; -} & RequiredModalProps; +}; -const ConfirmationModal = ({ title, appear, children, visible, buttonText, onConfirmed, showSpinnerOverlay, onDismissed }: Props) => ( - onDismissed()} - > -

{title}

-

{children}

-
- - -
-
-); +const ConfirmationModal = ({ title, children, buttonText, onConfirmed, showSpinnerOverlay }: Props) => { + const { dismiss, toggleSpinner } = useContext(ModalContext); -export default ConfirmationModal; + useEffect(() => { + toggleSpinner(showSpinnerOverlay); + }, [ showSpinnerOverlay ]); + + return ( + <> +

{title}

+

{children}

+
+ + +
+ + ); +}; + +ConfirmationModal.displayName = 'ConfirmationModal'; + +export default asModal()(ConfirmationModal); diff --git a/resources/scripts/components/elements/Fade.tsx b/resources/scripts/components/elements/Fade.tsx index 62850283e..2b9c3efa8 100644 --- a/resources/scripts/components/elements/Fade.tsx +++ b/resources/scripts/components/elements/Fade.tsx @@ -8,14 +8,14 @@ interface Props extends Omit { } const Container = styled.div<{ timeout: number }>` - .fade-enter, .fade-exit { + .fade-enter, .fade-exit, .fade-appear { will-change: opacity; } - .fade-enter { + .fade-enter, .fade-appear { ${tw`opacity-0`}; - &.fade-enter-active { + &.fade-enter-active, &.fade-appear-active { ${tw`opacity-100 transition-opacity ease-in`}; transition-duration: ${props => props.timeout}ms; } diff --git a/resources/scripts/components/elements/Modal.tsx b/resources/scripts/components/elements/Modal.tsx index f242fbacc..de0be05e6 100644 --- a/resources/scripts/components/elements/Modal.tsx +++ b/resources/scripts/components/elements/Modal.tsx @@ -13,7 +13,7 @@ export interface RequiredModalProps { top?: boolean; } -interface Props extends RequiredModalProps { +export interface ModalProps extends RequiredModalProps { dismissable?: boolean; closeOnEscape?: boolean; closeOnBackground?: boolean; @@ -40,7 +40,7 @@ const ModalContainer = styled.div<{ alignTop?: boolean }>` } `; -const Modal: React.FC = ({ visible, appear, dismissable, showSpinnerOverlay, top = true, closeOnBackground = true, closeOnEscape = true, onDismissed, children }) => { +const Modal: React.FC = ({ visible, appear, dismissable, showSpinnerOverlay, top = true, closeOnBackground = true, closeOnEscape = true, onDismissed, children }) => { const [ render, setRender ] = useState(visible); const isDismissable = useMemo(() => { @@ -62,7 +62,13 @@ const Modal: React.FC = ({ visible, appear, dismissable, showSpinnerOverl }, [ render ]); return ( - + onDismissed()} + > { if (isDismissable && closeOnBackground) { diff --git a/resources/scripts/components/server/backups/BackupContextMenu.tsx b/resources/scripts/components/server/backups/BackupContextMenu.tsx index 54a45b9d3..d9c74150d 100644 --- a/resources/scripts/components/server/backups/BackupContextMenu.tsx +++ b/resources/scripts/components/server/backups/BackupContextMenu.tsx @@ -65,18 +65,16 @@ export default ({ backup }: Props) => { checksum={backup.sha256Hash} /> } - {deleteVisible && doDeletion()} - visible={deleteVisible} - onDismissed={() => setDeleteVisible(false)} + onModalDismissed={() => setDeleteVisible(false)} > Are you sure you wish to delete this backup? This is a permanent operation and the backup cannot be recovered once deleted. - } ( diff --git a/resources/scripts/components/server/files/MassActionsBar.tsx b/resources/scripts/components/server/files/MassActionsBar.tsx index 6df43ecca..40067d039 100644 --- a/resources/scripts/components/server/files/MassActionsBar.tsx +++ b/resources/scripts/components/server/files/MassActionsBar.tsx @@ -72,7 +72,7 @@ const MassActionsBar = () => { title={'Delete these files?'} buttonText={'Yes, Delete Files'} onConfirmed={onClickConfirmDeletion} - onDismissed={() => setShowConfirm(false)} + onModalDismissed={() => setShowConfirm(false)} > Deleting files is a permanent operation, you cannot undo this action. diff --git a/resources/scripts/components/server/schedules/DeleteScheduleButton.tsx b/resources/scripts/components/server/schedules/DeleteScheduleButton.tsx index 26d86652d..198060388 100644 --- a/resources/scripts/components/server/schedules/DeleteScheduleButton.tsx +++ b/resources/scripts/components/server/schedules/DeleteScheduleButton.tsx @@ -39,12 +39,12 @@ export default ({ scheduleId, onDeleted }: Props) => { return ( <> setVisible(false)} + showSpinnerOverlay={isLoading} + onModalDismissed={() => setVisible(false)} > Are you sure you want to delete this schedule? All tasks will be removed and any running processes will be terminated. diff --git a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx index fb1136f73..b14a24ea3 100644 --- a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx +++ b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx @@ -69,7 +69,7 @@ export default ({ schedule, task }: Props) => { buttonText={'Delete Task'} onConfirmed={onConfirmDeletion} visible={visible} - onDismissed={() => setVisible(false)} + onModalDismissed={() => setVisible(false)} > Are you sure you want to delete this task? This action cannot be undone. diff --git a/resources/scripts/components/server/settings/ReinstallServerBox.tsx b/resources/scripts/components/server/settings/ReinstallServerBox.tsx index 1b7b44de7..0c1ce8ec7 100644 --- a/resources/scripts/components/server/settings/ReinstallServerBox.tsx +++ b/resources/scripts/components/server/settings/ReinstallServerBox.tsx @@ -46,10 +46,10 @@ export default () => { reinstall()} + onConfirmed={reinstall} showSpinnerOverlay={isSubmitting} visible={modalVisible} - onDismissed={() => setModalVisible(false)} + onModalDismissed={() => setModalVisible(false)} > Your server will be stopped and some files may be deleted or modified during this process, are you sure you wish to continue? diff --git a/resources/scripts/components/server/users/RemoveSubuserButton.tsx b/resources/scripts/components/server/users/RemoveSubuserButton.tsx index 976c64170..f6481dfc4 100644 --- a/resources/scripts/components/server/users/RemoveSubuserButton.tsx +++ b/resources/scripts/components/server/users/RemoveSubuserButton.tsx @@ -35,19 +35,17 @@ export default ({ subuser }: { subuser: Subuser }) => { return ( <> - {showConfirmation && doDeletion()} - onDismissed={() => setShowConfirmation(false)} + onModalDismissed={() => setShowConfirmation(false)} > Are you sure you wish to remove this subuser? They will have all access to this server revoked immediately. - }
} {showSpinnerOverlay && -
- -
+ +
+ +
+
}
{children} diff --git a/resources/scripts/hoc/asModal.tsx b/resources/scripts/hoc/asModal.tsx index 2311b1441..7db437c14 100644 --- a/resources/scripts/hoc/asModal.tsx +++ b/resources/scripts/hoc/asModal.tsx @@ -1,6 +1,7 @@ import React from 'react'; import Modal, { ModalProps } from '@/components/elements/Modal'; import ModalContext from '@/context/ModalContext'; +import isEqual from 'react-fast-compare'; export interface AsModalProps { visible: boolean; @@ -12,26 +13,34 @@ type ExtendedModalProps = Omit interface State { render: boolean; visible: boolean; - showSpinnerOverlay: boolean; + modalProps: ExtendedModalProps | undefined; } -function asModal (modalProps?: ExtendedModalProps) { - // eslint-disable-next-line @typescript-eslint/ban-types - return function (Component: React.ComponentType) { - return class extends React.PureComponent { +type ExtendedComponentType = (C: React.ComponentType) => React.ComponentType; + +// eslint-disable-next-line @typescript-eslint/ban-types +function asModal

(modalProps?: ExtendedModalProps | ((props: P) => ExtendedModalProps)): ExtendedComponentType

{ + return function (Component) { + return class extends React.PureComponent

{ static displayName = `asModal(${Component.displayName})`; - constructor (props: T & AsModalProps) { + constructor (props: P & AsModalProps) { super(props); this.state = { render: props.visible, visible: props.visible, - showSpinnerOverlay: modalProps?.showSpinnerOverlay || false, + modalProps: typeof modalProps === 'function' ? modalProps(this.props) : modalProps, }; } - componentDidUpdate (prevProps: Readonly) { + componentDidUpdate (prevProps: Readonly

) { + const mapped = typeof modalProps === 'function' ? modalProps(this.props) : modalProps; + if (!isEqual(this.state.modalProps, mapped)) { + // noinspection JSPotentiallyInvalidUsageOfThis + this.setState({ modalProps: mapped }); + } + if (prevProps.visible && !this.props.visible) { // noinspection JSPotentiallyInvalidUsageOfThis this.setState({ visible: false }); @@ -43,7 +52,12 @@ function asModal (modalProps?: ExtendedModalProps) { dismiss = () => this.setState({ visible: false }); - toggleSpinner = (value?: boolean) => this.setState({ showSpinnerOverlay: value || false }); + toggleSpinner = (value?: boolean) => this.setState(s => ({ + modalProps: { + ...s.modalProps, + showSpinnerOverlay: value || false, + }, + })); render () { return ( @@ -58,13 +72,12 @@ function asModal (modalProps?: ExtendedModalProps) { this.setState({ render: false }, () => { if (typeof this.props.onModalDismissed === 'function') { this.props.onModalDismissed(); } })} - {...modalProps} + {...this.state.modalProps} > From 57bb652d8190a34da80144dd509e55f868871726 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 18 Aug 2020 20:16:13 -0700 Subject: [PATCH 09/54] Whoops, don't always show this modal --- .../scripts/components/server/users/RemoveSubuserButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/scripts/components/server/users/RemoveSubuserButton.tsx b/resources/scripts/components/server/users/RemoveSubuserButton.tsx index f6481dfc4..a7fb4ce67 100644 --- a/resources/scripts/components/server/users/RemoveSubuserButton.tsx +++ b/resources/scripts/components/server/users/RemoveSubuserButton.tsx @@ -38,7 +38,7 @@ export default ({ subuser }: { subuser: Subuser }) => { doDeletion()} onModalDismissed={() => setShowConfirmation(false)} From 61e977133318a1888d79d2048841660650f250d1 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 19 Aug 2020 20:21:12 -0700 Subject: [PATCH 10/54] Code cleanup for subuser API endpoints; closes #2247 --- app/Exceptions/Handler.php | 7 ++ .../Api/Client/Servers/SubuserController.php | 34 ++++++-- .../Client/Server/SubuserBelongsToServer.php | 37 +++++++++ .../Client/SubstituteClientApiBindings.php | 5 ++ .../Servers/Subusers/SubuserRequest.php | 78 +++---------------- app/Models/Server.php | 2 +- .../Eloquent/SubuserRepository.php | 24 ------ .../Servers/GetUserPermissionsService.php | 2 +- routes/api-client.php | 9 ++- 9 files changed, 94 insertions(+), 104 deletions(-) create mode 100644 app/Http/Middleware/Api/Client/Server/SubuserBelongsToServer.php diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 50ac1a960..d278ce0bc 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -213,6 +213,13 @@ class Handler extends ExceptionHandler 'detail' => 'An error was encountered while processing this request.', ]; + if ($exception instanceof ModelNotFoundException || $exception->getPrevious() instanceof ModelNotFoundException) { + // Show a nicer error message compared to the standard "No query results for model" + // response that is normally returned. If we are in debug mode this will get overwritten + // with a more specific error message to help narrow down things. + $error['detail'] = 'The requested resource could not be found on the server.'; + } + if (config('app.debug')) { $error = array_merge($error, [ 'detail' => $exception->getMessage(), diff --git a/app/Http/Controllers/Api/Client/Servers/SubuserController.php b/app/Http/Controllers/Api/Client/Servers/SubuserController.php index da6fee428..d8bdcc40a 100644 --- a/app/Http/Controllers/Api/Client/Servers/SubuserController.php +++ b/app/Http/Controllers/Api/Client/Servers/SubuserController.php @@ -3,7 +3,9 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers; use Illuminate\Http\Request; +use Pterodactyl\Models\User; use Pterodactyl\Models\Server; +use Pterodactyl\Models\Subuser; use Illuminate\Http\JsonResponse; use Pterodactyl\Models\Permission; use Pterodactyl\Repositories\Eloquent\SubuserRepository; @@ -57,6 +59,21 @@ class SubuserController extends ClientApiController ->toArray(); } + /** + * Returns a single subuser associated with this server instance. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\GetSubuserRequest $request + * @return array + */ + public function view(GetSubuserRequest $request) + { + $subuser = $request->attributes->get('subuser'); + + return $this->fractal->item($subuser) + ->transformWith($this->getTransformer(SubuserTransformer::class)) + ->toArray(); + } + /** * Create a new subuser for the given server. * @@ -84,15 +101,16 @@ class SubuserController extends ClientApiController * Update a given subuser in the system for the server. * * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\UpdateSubuserRequest $request - * @param \Pterodactyl\Models\Server $server * @return array * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function update(UpdateSubuserRequest $request, Server $server): array + public function update(UpdateSubuserRequest $request): array { - $subuser = $request->endpointSubuser(); + /** @var \Pterodactyl\Models\Subuser $subuser */ + $subuser = $request->attributes->get('subuser'); + $this->repository->update($subuser->id, [ 'permissions' => $this->getDefaultPermissions($request), ]); @@ -106,14 +124,16 @@ class SubuserController extends ClientApiController * Removes a subusers from a server's assignment. * * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\DeleteSubuserRequest $request - * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\JsonResponse */ - public function delete(DeleteSubuserRequest $request, Server $server) + public function delete(DeleteSubuserRequest $request) { - $this->repository->delete($request->endpointSubuser()->id); + /** @var \Pterodactyl\Models\Subuser $subuser */ + $subuser = $request->attributes->get('subuser'); - return JsonResponse::create([], JsonResponse::HTTP_NO_CONTENT); + $this->repository->delete($subuser->id); + + return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); } /** diff --git a/app/Http/Middleware/Api/Client/Server/SubuserBelongsToServer.php b/app/Http/Middleware/Api/Client/Server/SubuserBelongsToServer.php new file mode 100644 index 000000000..894d6b000 --- /dev/null +++ b/app/Http/Middleware/Api/Client/Server/SubuserBelongsToServer.php @@ -0,0 +1,37 @@ +route()->parameter('server'); + /** @var \Pterodactyl\Models\User $user */ + $user = $request->route()->parameter('user'); + + // Don't do anything if there isn't a user present in the request. + if (is_null($user)) { + return $next($request); + } + + $request->attributes->set('subuser', $server->subusers()->where('user_id', $user->id)->firstOrFail()); + + return $next($request); + } +} diff --git a/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php index 0bd40eee5..77879c97f 100644 --- a/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php +++ b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Http\Middleware\Api\Client; use Closure; +use Pterodactyl\Models\User; use Pterodactyl\Models\Backup; use Pterodactyl\Models\Database; use Illuminate\Container\Container; @@ -52,6 +53,10 @@ class SubstituteClientApiBindings extends ApiSubstituteBindings return Backup::query()->where('uuid', $value)->firstOrFail(); }); + $this->router->model('user', User::class, function ($value) { + return User::query()->where('uuid', $value)->firstOrFail(); + }); + return parent::handle($request, $next); } } diff --git a/app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php b/app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php index e43b7178e..98d0d9643 100644 --- a/app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php @@ -3,12 +3,10 @@ namespace Pterodactyl\Http\Requests\Api\Client\Servers\Subusers; use Illuminate\Http\Request; -use Pterodactyl\Models\Server; +use Pterodactyl\Models\User; use Pterodactyl\Exceptions\Http\HttpForbiddenException; -use Pterodactyl\Repositories\Eloquent\SubuserRepository; use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest; -use Pterodactyl\Exceptions\Repository\RecordNotFoundException; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Pterodactyl\Services\Servers\GetUserPermissionsService; abstract class SubuserRequest extends ClientApiRequest { @@ -30,10 +28,10 @@ abstract class SubuserRequest extends ClientApiRequest return false; } - // If there is a subuser present in the URL, validate that it is not the same as the - // current request user. You're not allowed to modify yourself. - if ($this->route()->hasParameter('subuser')) { - if ($this->endpointSubuser()->user_id === $this->user()->id) { + $user = $this->route()->parameter('user'); + // Don't allow a user to edit themselves on the server. + if ($user instanceof User) { + if ($user->uuid === $this->user()->uuid) { return false; } } @@ -71,68 +69,14 @@ abstract class SubuserRequest extends ClientApiRequest // Otherwise, get the current subuser's permission set, and ensure that the // permissions they are trying to assign are not _more_ than the ones they // already have. - if (count(array_diff($permissions, $this->currentUserPermissions())) > 0) { + /** @var \Pterodactyl\Models\Subuser|null $subuser */ + /** @var \Pterodactyl\Services\Servers\GetUserPermissionsService $service */ + $service = $this->container->make(GetUserPermissionsService::class); + + if (count(array_diff($permissions, $service->handle($server, $user))) > 0) { throw new HttpForbiddenException( 'Cannot assign permissions to a subuser that your account does not actively possess.' ); } } - - /** - * Returns the currently authenticated user's permissions. - * - * @return array - * - * @throws \Illuminate\Contracts\Container\BindingResolutionException - */ - public function currentUserPermissions(): array - { - /** @var \Pterodactyl\Repositories\Eloquent\SubuserRepository $repository */ - $repository = $this->container->make(SubuserRepository::class); - - /* @var \Pterodactyl\Models\Subuser $model */ - try { - $model = $repository->findFirstWhere([ - ['server_id', $this->route()->parameter('server')->id], - ['user_id', $this->user()->id], - ]); - } catch (RecordNotFoundException $exception) { - return []; - } - - return $model->permissions; - } - - /** - * Return the subuser model for the given request which can then be validated. If - * required request parameters are missing a 404 error will be returned, otherwise - * a model exception will be returned if the model is not found. - * - * This returns the subuser based on the endpoint being hit, not the actual subuser - * for the account making the request. - * - * @return \Pterodactyl\Models\Subuser - * - * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - * @throws \Illuminate\Contracts\Container\BindingResolutionException - */ - public function endpointSubuser() - { - /** @var \Pterodactyl\Repositories\Eloquent\SubuserRepository $repository */ - $repository = $this->container->make(SubuserRepository::class); - - $parameters = $this->route()->parameters(); - if ( - ! isset($parameters['server'], $parameters['server']) - || ! is_string($parameters['subuser']) - || ! $parameters['server'] instanceof Server - ) { - throw new NotFoundHttpException; - } - - return $this->model ?: $this->model = $repository->getUserForServer( - $parameters['server']->id, $parameters['subuser'] - ); - } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 8f15bfcf1..8894a4d60 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -38,7 +38,7 @@ use Znck\Eloquent\Traits\BelongsToThrough; * @property \Carbon\Carbon $updated_at * * @property \Pterodactyl\Models\User $user - * @property \Pterodactyl\Models\User[]|\Illuminate\Database\Eloquent\Collection $subusers + * @property \Pterodactyl\Models\Subuser[]|\Illuminate\Database\Eloquent\Collection $subusers * @property \Pterodactyl\Models\Allocation $allocation * @property \Pterodactyl\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations * @property \Pterodactyl\Models\Pack|null $pack diff --git a/app/Repositories/Eloquent/SubuserRepository.php b/app/Repositories/Eloquent/SubuserRepository.php index e00d825e7..c0fb930a6 100644 --- a/app/Repositories/Eloquent/SubuserRepository.php +++ b/app/Repositories/Eloquent/SubuserRepository.php @@ -18,30 +18,6 @@ class SubuserRepository extends EloquentRepository implements SubuserRepositoryI return Subuser::class; } - /** - * Returns a subuser model for the given user and server combination. If no record - * exists an exception will be thrown. - * - * @param int $server - * @param string $uuid - * @return \Pterodactyl\Models\Subuser - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - */ - public function getUserForServer(int $server, string $uuid): Subuser - { - /** @var \Pterodactyl\Models\Subuser $model */ - $model = $this->getBuilder() - ->with('server', 'user') - ->select('subusers.*') - ->join('users', 'users.id', '=', 'subusers.user_id') - ->where('subusers.server_id', $server) - ->where('users.uuid', $uuid) - ->firstOrFail(); - - return $model; - } - /** * Return a subuser with the associated server relationship. * diff --git a/app/Services/Servers/GetUserPermissionsService.php b/app/Services/Servers/GetUserPermissionsService.php index 98dcf6c34..e0ea20373 100644 --- a/app/Services/Servers/GetUserPermissionsService.php +++ b/app/Services/Servers/GetUserPermissionsService.php @@ -30,7 +30,7 @@ class GetUserPermissionsService } /** @var \Pterodactyl\Models\Subuser|null $subuserPermissions */ - $subuserPermissions = $server->subusers->where('user_id', $user->id)->first(); + $subuserPermissions = $server->subusers()->where('user_id', $user->id)->first(); return $subuserPermissions ? $subuserPermissions->permissions : []; } diff --git a/routes/api-client.php b/routes/api-client.php index f92ba6ed9..c9ec16097 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -1,6 +1,7 @@ '/servers/{server}', 'middleware' => [AuthenticateServ Route::delete('/allocations/{allocation}', 'Servers\NetworkAllocationController@delete'); }); - Route::group(['prefix' => '/users'], function () { + Route::group(['prefix' => '/users', 'middleware' => [SubuserBelongsToServer::class]], function () { Route::get('/', 'Servers\SubuserController@index'); Route::post('/', 'Servers\SubuserController@store'); - Route::get('/{subuser}', 'Servers\SubuserController@view'); - Route::post('/{subuser}', 'Servers\SubuserController@update'); - Route::delete('/{subuser}', 'Servers\SubuserController@delete'); + Route::get('/{user}', 'Servers\SubuserController@view'); + Route::post('/{user}', 'Servers\SubuserController@update'); + Route::delete('/{user}', 'Servers\SubuserController@delete'); }); Route::group(['prefix' => '/backups'], function () { From 540cc82e3dc28e983fba3388cc942ef9d6217ee9 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 19 Aug 2020 20:38:51 -0700 Subject: [PATCH 11/54] Don't resolve database hosts; closes #2237 --- .../Client/Server/SubuserBelongsToServer.php | 1 - .../Admin/DatabaseHostFormRequest.php | 4 -- app/Models/DatabaseHost.php | 16 +++++- app/Rules/ResolvesToIPAddress.php | 49 +++++++++++++++++++ 4 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 app/Rules/ResolvesToIPAddress.php diff --git a/app/Http/Middleware/Api/Client/Server/SubuserBelongsToServer.php b/app/Http/Middleware/Api/Client/Server/SubuserBelongsToServer.php index 894d6b000..a80f6eefd 100644 --- a/app/Http/Middleware/Api/Client/Server/SubuserBelongsToServer.php +++ b/app/Http/Middleware/Api/Client/Server/SubuserBelongsToServer.php @@ -3,7 +3,6 @@ namespace Pterodactyl\Http\Middleware\Api\Client\Server; use Closure; -use Exception; use Illuminate\Http\Request; class SubuserBelongsToServer diff --git a/app/Http/Requests/Admin/DatabaseHostFormRequest.php b/app/Http/Requests/Admin/DatabaseHostFormRequest.php index 54d3bd0cc..c6b2468a7 100644 --- a/app/Http/Requests/Admin/DatabaseHostFormRequest.php +++ b/app/Http/Requests/Admin/DatabaseHostFormRequest.php @@ -29,10 +29,6 @@ class DatabaseHostFormRequest extends AdminFormRequest $this->merge(['node_id' => null]); } - $this->merge([ - 'host' => gethostbyname($this->input('host')), - ]); - return parent::getValidatorInstance(); } } diff --git a/app/Models/DatabaseHost.php b/app/Models/DatabaseHost.php index 6fafce2f0..d76fed494 100644 --- a/app/Models/DatabaseHost.php +++ b/app/Models/DatabaseHost.php @@ -2,6 +2,8 @@ namespace Pterodactyl\Models; +use Pterodactyl\Rules\ResolvesToIPAddress; + class DatabaseHost extends Model { /** @@ -51,13 +53,25 @@ class DatabaseHost extends Model */ public static $validationRules = [ 'name' => 'required|string|max:255', - 'host' => 'required|unique:database_hosts,host', + 'host' => 'required|string', 'port' => 'required|numeric|between:1,65535', 'username' => 'required|string|max:32', 'password' => 'nullable|string', 'node_id' => 'sometimes|nullable|integer|exists:nodes,id', ]; + /** + * @return array + */ + public static function getRules() + { + $rules = parent::getRules(); + + $rules['host'] = array_merge($rules['host'], [ new ResolvesToIPAddress() ]); + + return $rules; + } + /** * Gets the node associated with a database host. * diff --git a/app/Rules/ResolvesToIPAddress.php b/app/Rules/ResolvesToIPAddress.php new file mode 100644 index 000000000..e1421b52c --- /dev/null +++ b/app/Rules/ResolvesToIPAddress.php @@ -0,0 +1,49 @@ + Date: Wed, 19 Aug 2020 21:11:29 -0700 Subject: [PATCH 12/54] Move the file selector out of the editor itself; closes #2147 --- .../scripts/components/elements/AceEditor.tsx | 46 ++++++------------- .../server/files/FileEditContainer.tsx | 30 +++++++----- resources/scripts/modes.d.ts | 3 ++ resources/scripts/modes.js | 1 + 4 files changed, 36 insertions(+), 44 deletions(-) create mode 100644 resources/scripts/modes.d.ts diff --git a/resources/scripts/components/elements/AceEditor.tsx b/resources/scripts/components/elements/AceEditor.tsx index 0b4ebca95..fbea88b8f 100644 --- a/resources/scripts/components/elements/AceEditor.tsx +++ b/resources/scripts/components/elements/AceEditor.tsx @@ -2,8 +2,6 @@ import React, { useCallback, useEffect, useState } from 'react'; import ace, { Editor } from 'brace'; import styled from 'styled-components/macro'; import tw from 'twin.macro'; -import Select from '@/components/elements/Select'; -// @ts-ignore import modes from '@/modes'; // @ts-ignore @@ -21,42 +19,38 @@ const EditorContainer = styled.div` `; Object.keys(modes).forEach(mode => require(`brace/mode/${mode}`)); +const modelist = ace.acequire('ace/ext/modelist'); export interface Props { style?: React.CSSProperties; initialContent?: string; - initialModePath?: string; + mode: string; + filename?: string; + onModeChanged: (mode: string) => void; fetchContent: (callback: () => Promise) => void; onContentSaved: (content: string) => void; } -export default ({ style, initialContent, initialModePath, fetchContent, onContentSaved }: Props) => { - const [ mode, setMode ] = useState('ace/mode/plain_text'); - +export default ({ style, initialContent, filename, mode, fetchContent, onContentSaved, onModeChanged }: Props) => { const [ editor, setEditor ] = useState(); const ref = useCallback(node => { - if (node) { - setEditor(ace.edit('editor')); - } + if (node) setEditor(ace.edit('editor')); }, []); useEffect(() => { - editor && editor.session.setMode(mode); + if (modelist && filename) { + onModeChanged(modelist.getModeForPath(filename).mode.replace(/^ace\/mode\//, '')); + } + }, [ filename ]); + + useEffect(() => { + editor && editor.session.setMode(`ace/mode/${mode}`); }, [ editor, mode ]); useEffect(() => { editor && editor.session.setValue(initialContent || ''); }, [ editor, initialContent ]); - useEffect(() => { - if (initialModePath) { - const modelist = ace.acequire('ace/ext/modelist'); - if (modelist) { - setMode(modelist.getModeForPath(initialModePath).mode); - } - } - }, [ initialModePath ]); - useEffect(() => { if (!editor) { fetchContent(() => Promise.reject(new Error('no editor session has been configured'))); @@ -85,20 +79,6 @@ export default ({ style, initialContent, initialModePath, fetchContent, onConten return (

-
-
- -
-
); }; diff --git a/resources/scripts/components/server/files/FileEditContainer.tsx b/resources/scripts/components/server/files/FileEditContainer.tsx index fdde9bb1b..4106ad53d 100644 --- a/resources/scripts/components/server/files/FileEditContainer.tsx +++ b/resources/scripts/components/server/files/FileEditContainer.tsx @@ -1,8 +1,5 @@ import React, { lazy, useEffect, useState } from 'react'; -import { ServerContext } from '@/state/server'; import getFileContents from '@/api/server/files/getFileContents'; -import { Actions, useStoreActions } from 'easy-peasy'; -import { ApplicationStore } from '@/state'; import { httpErrorToHuman } from '@/api/http'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import saveFileContents from '@/api/server/files/saveFileContents'; @@ -15,6 +12,10 @@ import PageContentBlock from '@/components/elements/PageContentBlock'; import ServerError from '@/components/screens/ServerError'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; +import Select from '@/components/elements/Select'; +import modes from '@/modes'; +import useServer from '@/plugins/useServer'; +import useFlash from '@/plugins/useFlash'; const LazyAceEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/AceEditor')); @@ -24,12 +25,13 @@ export default () => { const [ loading, setLoading ] = useState(action === 'edit'); const [ content, setContent ] = useState(''); const [ modalVisible, setModalVisible ] = useState(false); + const [ mode, setMode ] = useState('plain_text'); const history = useHistory(); const { hash } = useLocation(); - const { id, uuid } = ServerContext.useStoreState(state => state.server.data!); - const { addError, clearFlashes } = useStoreActions((actions: Actions) => actions.flashes); + const { id, uuid } = useServer(); + const { addError, clearFlashes } = useFlash(); let fetchFileContent: null | (() => Promise) = null; @@ -75,10 +77,7 @@ export default () => { if (error) { return ( - history.goBack()} - /> + history.goBack()}/> ); } @@ -109,15 +108,24 @@ export default () => {
{ fetchFileContent = value; }} - onContentSaved={() => save()} + onContentSaved={save} />
+
+ +
{action === 'edit' ? - )} - > -
- - doDownload()}> - - Download + {backup.isSuccessful ? + ( + + )} + > +
+ + doDownload()}> + + Download + + + setVisible(true)}> + + Checksum - - setVisible(true)}> - - Checksum - - - setDeleteVisible(true)}> - - Delete - - -
-
+ + setDeleteVisible(true)}> + + Delete + + +
+ + : + + } ); }; diff --git a/resources/scripts/components/server/backups/BackupRow.tsx b/resources/scripts/components/server/backups/BackupRow.tsx index 2a7b625bc..e6a16a2f1 100644 --- a/resources/scripts/components/server/backups/BackupRow.tsx +++ b/resources/scripts/components/server/backups/BackupRow.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { ServerBackup } from '@/api/server/backups/getServerBackups'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faArchive, faEllipsisH } from '@fortawesome/free-solid-svg-icons'; import { format, formatDistanceToNow } from 'date-fns'; @@ -7,10 +6,11 @@ import Spinner from '@/components/elements/Spinner'; import { bytesToHuman } from '@/helpers'; import Can from '@/components/elements/Can'; import useWebsocketEvent from '@/plugins/useWebsocketEvent'; -import { ServerContext } from '@/state/server'; import BackupContextMenu from '@/components/server/backups/BackupContextMenu'; import tw from 'twin.macro'; import GreyRowBox from '@/components/elements/GreyRowBox'; +import getServerBackups from '@/api/swr/getServerBackups'; +import { ServerBackup } from '@/api/server/types'; interface Props { backup: ServerBackup; @@ -18,17 +18,22 @@ interface Props { } export default ({ backup, className }: Props) => { - const appendBackup = ServerContext.useStoreActions(actions => actions.backups.appendBackup); + const { mutate } = getServerBackups(); useWebsocketEvent(`backup completed:${backup.uuid}`, data => { try { const parsed = JSON.parse(data); - appendBackup({ - ...backup, - sha256Hash: parsed.sha256_hash || '', - bytes: parsed.file_size || 0, - completedAt: new Date(), - }); + + mutate(data => ({ + ...data, + items: data.items.map(b => b.uuid !== backup.uuid ? b : ({ + ...b, + isSuccessful: parsed.is_successful || true, + sha256Hash: parsed.sha256_hash || '', + bytes: parsed.file_size || 0, + completedAt: new Date(), + })), + }), false); } catch (e) { console.warn(e); } @@ -45,8 +50,13 @@ export default ({ backup, className }: Props) => {

+ {!backup.isSuccessful && + + Failed + + } {backup.name} - {backup.completedAt && + {(backup.completedAt && backup.isSuccessful) && {bytesToHuman(backup.bytes)} }

diff --git a/resources/scripts/components/server/backups/CreateBackupButton.tsx b/resources/scripts/components/server/backups/CreateBackupButton.tsx index 3d7834fa9..3fd53403a 100644 --- a/resources/scripts/components/server/backups/CreateBackupButton.tsx +++ b/resources/scripts/components/server/backups/CreateBackupButton.tsx @@ -7,12 +7,11 @@ import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper'; import useFlash from '@/plugins/useFlash'; import useServer from '@/plugins/useServer'; import createServerBackup from '@/api/server/backups/createServerBackup'; -import { httpErrorToHuman } from '@/api/http'; import FlashMessageRender from '@/components/FlashMessageRender'; -import { ServerContext } from '@/state/server'; import Button from '@/components/elements/Button'; import tw from 'twin.macro'; import { Textarea } from '@/components/elements/Input'; +import getServerBackups from '@/api/swr/getServerBackups'; interface Values { name: string; @@ -60,10 +59,9 @@ const ModalContent = ({ ...props }: RequiredModalProps) => { export default () => { const { uuid } = useServer(); - const { addError, clearFlashes } = useFlash(); + const { clearFlashes, clearAndAddHttpError } = useFlash(); const [ visible, setVisible ] = useState(false); - - const appendBackup = ServerContext.useStoreActions(actions => actions.backups.appendBackup); + const { mutate } = getServerBackups(); useEffect(() => { clearFlashes('backups:create'); @@ -73,12 +71,11 @@ export default () => { clearFlashes('backups:create'); createServerBackup(uuid, name, ignored) .then(backup => { - appendBackup(backup); + mutate(data => ({ ...data, items: data.items.concat(backup) }), false); setVisible(false); }) .catch(error => { - console.error(error); - addError({ key: 'backups:create', message: httpErrorToHuman(error) }); + clearAndAddHttpError({ key: 'backups:create', error }); setSubmitting(false); }); }; diff --git a/resources/scripts/state/server/backups.ts b/resources/scripts/state/server/backups.ts deleted file mode 100644 index aa24bdf7f..000000000 --- a/resources/scripts/state/server/backups.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ServerBackup } from '@/api/server/backups/getServerBackups'; -import { action, Action } from 'easy-peasy'; - -export interface ServerBackupStore { - data: ServerBackup[]; - setBackups: Action; - appendBackup: Action; - removeBackup: Action; -} - -const backups: ServerBackupStore = { - data: [], - - setBackups: action((state, payload) => { - state.data = payload; - }), - - appendBackup: action((state, payload) => { - if (state.data.find(backup => backup.uuid === payload.uuid)) { - state.data = state.data.map(backup => backup.uuid === payload.uuid ? payload : backup); - } else { - state.data = [ ...state.data, payload ]; - } - }), - - removeBackup: action((state, payload) => { - state.data = [ ...state.data.filter(backup => backup.uuid !== payload) ]; - }), -}; - -export default backups; diff --git a/resources/scripts/state/server/index.ts b/resources/scripts/state/server/index.ts index febf0951a..87023a020 100644 --- a/resources/scripts/state/server/index.ts +++ b/resources/scripts/state/server/index.ts @@ -4,7 +4,6 @@ import socket, { SocketStore } from './socket'; import files, { ServerFileStore } from '@/state/server/files'; import subusers, { ServerSubuserStore } from '@/state/server/subusers'; import { composeWithDevTools } from 'redux-devtools-extension'; -import backups, { ServerBackupStore } from '@/state/server/backups'; import schedules, { ServerScheduleStore } from '@/state/server/schedules'; import databases, { ServerDatabaseStore } from '@/state/server/databases'; @@ -56,7 +55,6 @@ export interface ServerStore { databases: ServerDatabaseStore; files: ServerFileStore; schedules: ServerScheduleStore; - backups: ServerBackupStore; socket: SocketStore; status: ServerStatusStore; clearServerState: Action; @@ -69,7 +67,6 @@ export const ServerContext = createContextStore({ databases, files, subusers, - backups, schedules, clearServerState: action(state => { state.server.data = undefined; @@ -78,7 +75,6 @@ export const ServerContext = createContextStore({ state.subusers.data = []; state.files.directory = '/'; state.files.selectedFiles = []; - state.backups.data = []; state.schedules.data = []; if (state.socket.instance) { From b5713ff7b7383156b94881cc400a775090d8857d Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 21 Aug 2020 15:10:17 +0200 Subject: [PATCH 17/54] Fix schedules in Dockerfile The wrong directory to run PHP in, so schedules will not run --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0525e5b39..f00d54d5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,7 @@ RUN cp docker/default.conf /etc/nginx/conf.d/default.conf \ && cat docker/www.conf > /usr/local/etc/php-fpm.d/www.conf \ && rm /usr/local/etc/php-fpm.d/www.conf.default \ && cat docker/supervisord.conf > /etc/supervisord.conf \ - && echo "* * * * * /usr/bin/php /app/artisan schedule:run >> /dev/null 2>&1" >> /var/spool/cron/crontabs/root \ + && echo "* * * * * /usr/local/bin/php /app/artisan schedule:run >> /dev/null 2>&1" >> /var/spool/cron/crontabs/root \ && sed -i s/ssl_session_cache/#ssl_session_cache/g /etc/nginx/nginx.conf \ && mkdir -p /var/run/php /var/run/nginx @@ -33,4 +33,4 @@ EXPOSE 80 443 ENTRYPOINT ["/bin/ash", "docker/entrypoint.sh"] -CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ] \ No newline at end of file +CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ] From 3a2c60ce316e109501890e5428b158bddc202de0 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 22 Aug 2020 13:26:03 -0700 Subject: [PATCH 18/54] Store bytes as unsigned bigint; closes #2245 --- ...132500_update_bytes_to_unsigned_bigint.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 database/migrations/2020_08_22_132500_update_bytes_to_unsigned_bigint.php diff --git a/database/migrations/2020_08_22_132500_update_bytes_to_unsigned_bigint.php b/database/migrations/2020_08_22_132500_update_bytes_to_unsigned_bigint.php new file mode 100644 index 000000000..802994ebe --- /dev/null +++ b/database/migrations/2020_08_22_132500_update_bytes_to_unsigned_bigint.php @@ -0,0 +1,32 @@ +unsignedBigInteger('bytes')->default(0)->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('backups', function (Blueprint $table) { + $table->integer('bytes')->default(0)->change(); + }); + } +} From cae604e79dc580e63fced328e59e88253551007e Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 22 Aug 2020 15:43:28 -0700 Subject: [PATCH 19/54] Include egg variables in the output from the API --- app/Models/EggVariable.php | 43 ++++++++++-- app/Models/Server.php | 6 +- .../Eloquent/ServerRepository.php | 4 ++ .../Servers/StartupCommandService.php | 27 ++++++++ .../Servers/StartupCommandViewService.php | 56 ---------------- .../Api/Client/EggVariableTransformer.php | 33 ++++++++++ .../Api/Client/ServerTransformer.php | 24 ++++++- resources/scripts/api/server/getServer.ts | 2 + .../components/elements/PageContentBlock.tsx | 65 ++++++++++++------- .../server/startup/StartupContainer.tsx | 23 +++++++ resources/scripts/routers/ServerRouter.tsx | 5 ++ .../Servers/StartupCommandViewServiceTest.php | 8 +-- 12 files changed, 204 insertions(+), 92 deletions(-) create mode 100644 app/Services/Servers/StartupCommandService.php delete mode 100644 app/Services/Servers/StartupCommandViewService.php create mode 100644 app/Transformers/Api/Client/EggVariableTransformer.php create mode 100644 resources/scripts/components/server/startup/StartupContainer.tsx diff --git a/app/Models/EggVariable.php b/app/Models/EggVariable.php index 2db891dc9..c6cc45b56 100644 --- a/app/Models/EggVariable.php +++ b/app/Models/EggVariable.php @@ -2,6 +2,27 @@ namespace Pterodactyl\Models; +/** + * @property int $id + * @property int $egg_id + * @property string $name + * @property string $description + * @property string $env_variable + * @property string $default_value + * @property bool $user_viewable + * @property bool $user_editable + * @property string $rules + * @property \Carbon\CarbonImmutable $created_at + * @property \Carbon\CarbonImmutable $updated_at + * + * @property bool $required + * @property \Pterodactyl\Models\Egg $egg + * @property \Pterodactyl\Models\ServerVariable $serverVariable + * + * The "server_value" variable is only present on the object if you've loaded this model + * using the server relationship. + * @property string|null $server_value + */ class EggVariable extends Model { /** @@ -17,6 +38,11 @@ class EggVariable extends Model */ const RESERVED_ENV_NAMES = 'SERVER_MEMORY,SERVER_IP,SERVER_PORT,ENV,HOME,USER,STARTUP,SERVER_UUID,UUID'; + /** + * @var bool + */ + protected $immutableDates = true; + /** * The table associated with the model. * @@ -38,8 +64,8 @@ class EggVariable extends Model */ protected $casts = [ 'egg_id' => 'integer', - 'user_viewable' => 'integer', - 'user_editable' => 'integer', + 'user_viewable' => 'bool', + 'user_editable' => 'bool', ]; /** @@ -65,12 +91,19 @@ class EggVariable extends Model ]; /** - * @param $value * @return bool */ - public function getRequiredAttribute($value) + public function getRequiredAttribute() { - return $this->rules === 'required' || str_contains($this->rules, ['required|', '|required']); + return in_array('required', explode('|', $this->rules)); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\HasOne + */ + public function egg() + { + return $this->hasOne(Egg::class); } /** diff --git a/app/Models/Server.php b/app/Models/Server.php index 8894a4d60..e6e9bca72 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -45,7 +45,7 @@ use Znck\Eloquent\Traits\BelongsToThrough; * @property \Pterodactyl\Models\Node $node * @property \Pterodactyl\Models\Nest $nest * @property \Pterodactyl\Models\Egg $egg - * @property \Pterodactyl\Models\ServerVariable[]|\Illuminate\Database\Eloquent\Collection $variables + * @property \Pterodactyl\Models\EggVariable[]|\Illuminate\Database\Eloquent\Collection $variables * @property \Pterodactyl\Models\Schedule[]|\Illuminate\Database\Eloquent\Collection $schedule * @property \Pterodactyl\Models\Database[]|\Illuminate\Database\Eloquent\Collection $databases * @property \Pterodactyl\Models\Location $location @@ -270,7 +270,9 @@ class Server extends Model */ public function variables() { - return $this->hasMany(ServerVariable::class); + return $this->hasMany(EggVariable::class, 'egg_id', 'egg_id') + ->select(['egg_variables.*', 'server_variables.variable_value as server_value']) + ->leftJoin('server_variables', 'server_variables.variable_id', '=', 'egg_variables.id'); } /** diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index a64f68db9..0f7919305 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -143,6 +143,10 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt */ public function getVariablesWithValues(int $id, bool $returnAsObject = false) { + $this->getBuilder() + ->with('variables', 'egg.variables') + ->findOrFail($id); + try { $instance = $this->getBuilder()->with('variables', 'egg.variables')->find($id, $this->getColumns()); } catch (ModelNotFoundException $exception) { diff --git a/app/Services/Servers/StartupCommandService.php b/app/Services/Servers/StartupCommandService.php new file mode 100644 index 000000000..5ee170aa0 --- /dev/null +++ b/app/Services/Servers/StartupCommandService.php @@ -0,0 +1,27 @@ +memory, $server->allocation->ip, $server->allocation->port]; + + foreach ($server->variables as $variable) { + $find[] = '{{' . $variable->env_variable . '}}'; + $replace[] = $variable->user_viewable ? ($variable->server_value ?? $variable->default_value) : '[hidden]'; + } + + return str_replace($find, $replace, $server->startup); + } +} diff --git a/app/Services/Servers/StartupCommandViewService.php b/app/Services/Servers/StartupCommandViewService.php deleted file mode 100644 index d3cda3143..000000000 --- a/app/Services/Servers/StartupCommandViewService.php +++ /dev/null @@ -1,56 +0,0 @@ -repository = $repository; - } - - /** - * Generate a startup command for a server and return all of the user-viewable variables - * as well as their assigned values. - * - * @param int $server - * @return \Illuminate\Support\Collection - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function handle(int $server): Collection - { - $response = $this->repository->getVariablesWithValues($server, true); - $server = $this->repository->getPrimaryAllocation($response->server); - - $find = ['{{SERVER_MEMORY}}', '{{SERVER_IP}}', '{{SERVER_PORT}}']; - $replace = [$server->memory, $server->getRelation('allocation')->ip, $server->getRelation('allocation')->port]; - - $variables = $server->getRelation('egg')->getRelation('variables') - ->each(function ($variable) use (&$find, &$replace, $response) { - $find[] = '{{' . $variable->env_variable . '}}'; - $replace[] = $variable->user_viewable ? $response->data[$variable->env_variable] : '[hidden]'; - })->filter(function ($variable) { - return $variable->user_viewable === 1; - }); - - return collect([ - 'startup' => str_replace($find, $replace, $server->startup), - 'variables' => $variables, - 'server_values' => $response->data, - ]); - } -} diff --git a/app/Transformers/Api/Client/EggVariableTransformer.php b/app/Transformers/Api/Client/EggVariableTransformer.php new file mode 100644 index 000000000..62be843f2 --- /dev/null +++ b/app/Transformers/Api/Client/EggVariableTransformer.php @@ -0,0 +1,33 @@ + $variable->name, + 'description' => $variable->description, + 'env_variable' => $variable->env_variable, + 'default_value' => $variable->default_value, + 'server_value' => $variable->server_value, + 'is_editable' => $variable->user_editable, + 'rules' => $variable->rules, + ]; + } +} diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php index 148fd8990..e1e7f529e 100644 --- a/app/Transformers/Api/Client/ServerTransformer.php +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -6,13 +6,17 @@ use Pterodactyl\Models\Egg; use Pterodactyl\Models\Server; use Pterodactyl\Models\Subuser; use Pterodactyl\Models\Allocation; +use Illuminate\Container\Container; +use Pterodactyl\Models\EggVariable; +use Pterodactyl\Services\Servers\StartupCommandService; +use Pterodactyl\Transformers\Api\Client\EggVariableTransformer; class ServerTransformer extends BaseClientTransformer { /** * @var string[] */ - protected $defaultIncludes = ['allocations']; + protected $defaultIncludes = ['allocations', 'variables']; /** * @var array @@ -36,6 +40,9 @@ class ServerTransformer extends BaseClientTransformer */ public function transform(Server $server): array { + /** @var \Pterodactyl\Services\Servers\StartupCommandService $service */ + $service = Container::getInstance()->make(StartupCommandService::class); + return [ 'server_owner' => $this->getKey()->user_id === $server->owner_id, 'identifier' => $server->uuidShort, @@ -54,6 +61,7 @@ class ServerTransformer extends BaseClientTransformer 'io' => $server->io, 'cpu' => $server->cpu, ], + 'invocation' => $service->handle($server), 'feature_limits' => [ 'databases' => $server->database_limit, 'allocations' => $server->allocation_limit, @@ -80,6 +88,20 @@ class ServerTransformer extends BaseClientTransformer ); } + /** + * @param \Pterodactyl\Models\Server $server + * @return \League\Fractal\Resource\Collection + * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException + */ + public function includeVariables(Server $server) + { + return $this->collection( + $server->variables->where('user_viewable', true), + $this->makeTransformer(EggVariableTransformer::class), + EggVariable::RESOURCE_NAME + ); + } + /** * Returns the egg associated with this server. * diff --git a/resources/scripts/api/server/getServer.ts b/resources/scripts/api/server/getServer.ts index 7072033f1..36dcffda9 100644 --- a/resources/scripts/api/server/getServer.ts +++ b/resources/scripts/api/server/getServer.ts @@ -19,6 +19,7 @@ export interface Server { ip: string; port: number; }; + invocation: string; description: string; allocations: Allocation[]; limits: { @@ -43,6 +44,7 @@ export const rawDataToServerObject = ({ attributes: data }: FractalResponseData) uuid: data.uuid, name: data.name, node: data.node, + invocation: data.invocation, sftpDetails: { ip: data.sftp_details.ip, port: data.sftp_details.port, diff --git a/resources/scripts/components/elements/PageContentBlock.tsx b/resources/scripts/components/elements/PageContentBlock.tsx index f32c42ce2..392cffb8d 100644 --- a/resources/scripts/components/elements/PageContentBlock.tsx +++ b/resources/scripts/components/elements/PageContentBlock.tsx @@ -3,31 +3,48 @@ import ContentContainer from '@/components/elements/ContentContainer'; import { CSSTransition } from 'react-transition-group'; import tw from 'twin.macro'; import FlashMessageRender from '@/components/FlashMessageRender'; +import { Helmet } from 'react-helmet'; +import useServer from '@/plugins/useServer'; -const PageContentBlock: React.FC<{ showFlashKey?: string; className?: string }> = ({ children, showFlashKey, className }) => ( - - <> - - {showFlashKey && - +interface Props { + title?: string; + className?: string; + showFlashKey?: string; +} + +const PageContentBlock: React.FC = ({ title, showFlashKey, className, children }) => { + const { name } = useServer(); + + return ( + + <> + {!!title && + + {name} | {title} + } - {children} - - -

- © 2015 - 2020  - - Pterodactyl Software - -

-
- -
-); + + {showFlashKey && + + } + {children} + + +

+ © 2015 - 2020  + + Pterodactyl Software + +

+
+ + + ); +}; export default PageContentBlock; diff --git a/resources/scripts/components/server/startup/StartupContainer.tsx b/resources/scripts/components/server/startup/StartupContainer.tsx new file mode 100644 index 000000000..e689b4982 --- /dev/null +++ b/resources/scripts/components/server/startup/StartupContainer.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import PageContentBlock from '@/components/elements/PageContentBlock'; +import TitledGreyBox from '@/components/elements/TitledGreyBox'; +import useServer from '@/plugins/useServer'; +import tw from 'twin.macro'; + +const StartupContainer = () => { + const { invocation } = useServer(); + + return ( + + +
+

+ {invocation} +

+
+
+
+ ); +}; + +export default StartupContainer; diff --git a/resources/scripts/routers/ServerRouter.tsx b/resources/scripts/routers/ServerRouter.tsx index 3fa5a9ff4..22e701fa9 100644 --- a/resources/scripts/routers/ServerRouter.tsx +++ b/resources/scripts/routers/ServerRouter.tsx @@ -27,6 +27,7 @@ import ScreenBlock from '@/components/screens/ScreenBlock'; import SubNavigation from '@/components/elements/SubNavigation'; import NetworkContainer from '@/components/server/network/NetworkContainer'; import InstallListener from '@/components/server/InstallListener'; +import StartupContainer from '@/components/server/startup/StartupContainer'; const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => { const { rootAdmin } = useStoreState(state => state.user.data!); @@ -98,6 +99,9 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) Network + + Startup + Settings @@ -137,6 +141,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) + diff --git a/tests/Unit/Services/Servers/StartupCommandViewServiceTest.php b/tests/Unit/Services/Servers/StartupCommandViewServiceTest.php index 5bb436122..a16eb3865 100644 --- a/tests/Unit/Services/Servers/StartupCommandViewServiceTest.php +++ b/tests/Unit/Services/Servers/StartupCommandViewServiceTest.php @@ -9,7 +9,7 @@ use Pterodactyl\Models\Server; use Illuminate\Support\Collection; use Pterodactyl\Models\Allocation; use Pterodactyl\Models\EggVariable; -use Pterodactyl\Services\Servers\StartupCommandViewService; +use Pterodactyl\Services\Servers\StartupCommandService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class StartupCommandViewServiceTest extends TestCase @@ -76,10 +76,10 @@ class StartupCommandViewServiceTest extends TestCase /** * Return an instance of the service with mocked dependencies. * - * @return \Pterodactyl\Services\Servers\StartupCommandViewService + * @return \Pterodactyl\Services\Servers\StartupCommandService */ - private function getService(): StartupCommandViewService + private function getService(): StartupCommandService { - return new StartupCommandViewService($this->repository); + return new StartupCommandService($this->repository); } } From 9b16f5883c755af5584266390e8d5a52110c5221 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 22 Aug 2020 15:46:13 -0700 Subject: [PATCH 20/54] Refactor to a single transformer file --- .../api/server/backups/createServerBackup.ts | 2 +- resources/scripts/api/server/transformers.ts | 13 ------------- resources/scripts/api/swr/getServerBackups.ts | 2 +- resources/scripts/api/transformers.ts | 12 ++++++++++++ 4 files changed, 14 insertions(+), 15 deletions(-) delete mode 100644 resources/scripts/api/server/transformers.ts diff --git a/resources/scripts/api/server/backups/createServerBackup.ts b/resources/scripts/api/server/backups/createServerBackup.ts index f86088994..a27d5d146 100644 --- a/resources/scripts/api/server/backups/createServerBackup.ts +++ b/resources/scripts/api/server/backups/createServerBackup.ts @@ -1,6 +1,6 @@ import http from '@/api/http'; import { ServerBackup } from '@/api/server/types'; -import { rawDataToServerBackup } from '@/api/server/transformers'; +import { rawDataToServerBackup } from '@/api/transformers'; export default (uuid: string, name?: string, ignored?: string): Promise => { return new Promise((resolve, reject) => { diff --git a/resources/scripts/api/server/transformers.ts b/resources/scripts/api/server/transformers.ts deleted file mode 100644 index f6f98e054..000000000 --- a/resources/scripts/api/server/transformers.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { FractalResponseData } from '@/api/http'; -import { ServerBackup } from '@/api/server/types'; - -export const rawDataToServerBackup = ({ attributes }: FractalResponseData): ServerBackup => ({ - uuid: attributes.uuid, - isSuccessful: attributes.is_successful, - name: attributes.name, - ignoredFiles: attributes.ignored_files, - sha256Hash: attributes.sha256_hash, - bytes: attributes.bytes, - createdAt: new Date(attributes.created_at), - completedAt: attributes.completed_at ? new Date(attributes.completed_at) : null, -}); diff --git a/resources/scripts/api/swr/getServerBackups.ts b/resources/scripts/api/swr/getServerBackups.ts index b07a5bea3..d7487fde3 100644 --- a/resources/scripts/api/swr/getServerBackups.ts +++ b/resources/scripts/api/swr/getServerBackups.ts @@ -1,7 +1,7 @@ import useSWR from 'swr'; import http, { getPaginationSet, PaginatedResult } from '@/api/http'; import { ServerBackup } from '@/api/server/types'; -import { rawDataToServerBackup } from '@/api/server/transformers'; +import { rawDataToServerBackup } from '@/api/transformers'; import useServer from '@/plugins/useServer'; export default (page?: number | string) => { diff --git a/resources/scripts/api/transformers.ts b/resources/scripts/api/transformers.ts index 6ac0ba1dd..5f9d337ae 100644 --- a/resources/scripts/api/transformers.ts +++ b/resources/scripts/api/transformers.ts @@ -1,6 +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'; export const rawDataToServerAllocation = (data: FractalResponseData): Allocation => ({ id: data.attributes.id, @@ -39,3 +40,14 @@ export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({ ].indexOf(this.mimetype) >= 0; }, }); + +export const rawDataToServerBackup = ({ attributes }: FractalResponseData): ServerBackup => ({ + uuid: attributes.uuid, + isSuccessful: attributes.is_successful, + name: attributes.name, + ignoredFiles: attributes.ignored_files, + sha256Hash: attributes.sha256_hash, + bytes: attributes.bytes, + createdAt: new Date(attributes.created_at), + completedAt: attributes.completed_at ? new Date(attributes.completed_at) : null, +}); From 1b69d82daac34d4c58ff9a26cefd8f292e119e10 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 22 Aug 2020 16:54:12 -0700 Subject: [PATCH 21/54] Don't return things a user shouldn't be able to see via the API includes --- .../Api/Client/DatabaseTransformer.php | 13 +++++++---- .../Api/Client/ServerTransformer.php | 23 +++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/app/Transformers/Api/Client/DatabaseTransformer.php b/app/Transformers/Api/Client/DatabaseTransformer.php index 8d420ea83..ddf02af10 100644 --- a/app/Transformers/Api/Client/DatabaseTransformer.php +++ b/app/Transformers/Api/Client/DatabaseTransformer.php @@ -4,6 +4,7 @@ namespace Pterodactyl\Transformers\Api\Client; use Pterodactyl\Models\Database; use League\Fractal\Resource\Item; +use Pterodactyl\Models\Permission; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Contracts\Extensions\HashidsInterface; @@ -65,12 +66,16 @@ class DatabaseTransformer extends BaseClientTransformer /** * Include the database password in the request. * - * @param \Pterodactyl\Models\Database $model - * @return \League\Fractal\Resource\Item + * @param \Pterodactyl\Models\Database $database + * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource */ - public function includePassword(Database $model): Item + public function includePassword(Database $database): Item { - return $this->item($model, function (Database $model) { + if (!$this->getUser()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $database->server)) { + return $this->null(); + } + + return $this->item($database, function (Database $model) { return [ 'password' => $this->encrypter->decrypt($model->password), ]; diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php index e1e7f529e..6d5b86ac5 100644 --- a/app/Transformers/Api/Client/ServerTransformer.php +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -6,10 +6,10 @@ use Pterodactyl\Models\Egg; use Pterodactyl\Models\Server; use Pterodactyl\Models\Subuser; use Pterodactyl\Models\Allocation; +use Pterodactyl\Models\Permission; use Illuminate\Container\Container; use Pterodactyl\Models\EggVariable; use Pterodactyl\Services\Servers\StartupCommandService; -use Pterodactyl\Transformers\Api\Client\EggVariableTransformer; class ServerTransformer extends BaseClientTransformer { @@ -76,11 +76,16 @@ class ServerTransformer extends BaseClientTransformer * Returns the allocations associated with this server. * * @param \Pterodactyl\Models\Server $server - * @return \League\Fractal\Resource\Collection + * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + * * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeAllocations(Server $server) { + if (! $this->getUser()->can(Permission::ACTION_ALLOCATION_READ, $server)) { + return $this->null(); + } + return $this->collection( $server->allocations, $this->makeTransformer(AllocationTransformer::class), @@ -90,11 +95,16 @@ class ServerTransformer extends BaseClientTransformer /** * @param \Pterodactyl\Models\Server $server - * @return \League\Fractal\Resource\Collection + * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + * * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeVariables(Server $server) { + if (! $this->getUser()->can(Permission::ACTION_STARTUP_READ, $server)) { + return $this->null(); + } + return $this->collection( $server->variables->where('user_viewable', true), $this->makeTransformer(EggVariableTransformer::class), @@ -118,11 +128,16 @@ class ServerTransformer extends BaseClientTransformer * Returns the subusers associated with this server. * * @param \Pterodactyl\Models\Server $server - * @return \League\Fractal\Resource\Collection + * @return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource + * * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ public function includeSubusers(Server $server) { + if (! $this->getUser()->can(Permission::ACTION_USER_READ, $server)) { + return $this->null(); + } + return $this->collection($server->subusers, $this->makeTransformer(SubuserTransformer::class), Subuser::RESOURCE_NAME); } } From 91cdbd6c2e807b15a4d4748d464311a416b5c132 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 22 Aug 2020 18:13:59 -0700 Subject: [PATCH 22/54] 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'); From 9ae3c179130fc3de4aaab6a02d468ddc32b132a0 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 22 Aug 2020 19:01:29 -0700 Subject: [PATCH 23/54] Don't even render components if the user doesn't have permission --- .../components/screens/ScreenBlock.tsx | 4 +-- .../server/network/NetworkContainer.tsx | 7 ++--- .../scripts/hoc/requireServerPermission.tsx | 27 +++++++++++++++++++ resources/scripts/routers/ServerRouter.tsx | 19 ++++++++----- 4 files changed, 43 insertions(+), 14 deletions(-) create mode 100644 resources/scripts/hoc/requireServerPermission.tsx diff --git a/resources/scripts/components/screens/ScreenBlock.tsx b/resources/scripts/components/screens/ScreenBlock.tsx index 55e1e70a0..38cad46cb 100644 --- a/resources/scripts/components/screens/ScreenBlock.tsx +++ b/resources/scripts/components/screens/ScreenBlock.tsx @@ -52,8 +52,8 @@ export default ({ title, image, message, onBack, onRetry }: Props) => (
} - -

{title}

+ +

{title}

{message}

diff --git a/resources/scripts/components/server/network/NetworkContainer.tsx b/resources/scripts/components/server/network/NetworkContainer.tsx index a330685b1..64d69aec0 100644 --- a/resources/scripts/components/server/network/NetworkContainer.tsx +++ b/resources/scripts/components/server/network/NetworkContainer.tsx @@ -24,7 +24,7 @@ const Code = styled.code`${tw`font-mono py-1 px-2 bg-neutral-900 rounded text-sm const Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`; const NetworkContainer = () => { - const { uuid, allocations, name: serverName } = useServer(); + const { uuid, allocations } = useServer(); const { clearFlashes, clearAndAddHttpError } = useFlash(); const [ loading, setLoading ] = useState(false); const { data, error, mutate } = useSWR(uuid, key => getServerAllocations(key), { initialData: allocations }); @@ -61,10 +61,7 @@ const NetworkContainer = () => { }, [ error ]); return ( - - - {serverName} | Network - + {!data ? : diff --git a/resources/scripts/hoc/requireServerPermission.tsx b/resources/scripts/hoc/requireServerPermission.tsx new file mode 100644 index 000000000..88ff05e20 --- /dev/null +++ b/resources/scripts/hoc/requireServerPermission.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import Can from '@/components/elements/Can'; +import NotFound from '@/components/screens/NotFound'; +import ScreenBlock from '@/components/screens/ScreenBlock'; + +const requireServerPermission = (Component: React.ComponentType, permissions: string | string[]) => { + return class extends React.Component { + render () { + return ( + + } + > + + + ); + } + }; +}; + +export default requireServerPermission; diff --git a/resources/scripts/routers/ServerRouter.tsx b/resources/scripts/routers/ServerRouter.tsx index 22e701fa9..7e0e30685 100644 --- a/resources/scripts/routers/ServerRouter.tsx +++ b/resources/scripts/routers/ServerRouter.tsx @@ -28,6 +28,7 @@ import SubNavigation from '@/components/elements/SubNavigation'; import NetworkContainer from '@/components/server/network/NetworkContainer'; import InstallListener from '@/components/server/InstallListener'; import StartupContainer from '@/components/server/startup/StartupContainer'; +import requireServerPermission from '@/hoc/requireServerPermission'; const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => { const { rootAdmin } = useStoreState(state => state.user.data!); @@ -121,7 +122,11 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) - + ( @@ -131,17 +136,17 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) )} exact /> - - + + - - - - + + + + From 813a6715711a8baf5376e6233df022334bcdce08 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 22 Aug 2020 19:01:42 -0700 Subject: [PATCH 24/54] Hide startup line from API response if user doesn't have startup permissions --- app/Services/Servers/StartupCommandService.php | 5 +++-- app/Transformers/Api/Client/ServerTransformer.php | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/Services/Servers/StartupCommandService.php b/app/Services/Servers/StartupCommandService.php index 5ee170aa0..bf31763cc 100644 --- a/app/Services/Servers/StartupCommandService.php +++ b/app/Services/Servers/StartupCommandService.php @@ -10,16 +10,17 @@ class StartupCommandService * Generates a startup command for a given server instance. * * @param \Pterodactyl\Models\Server $server + * @param bool $hideAllValues * @return string */ - public function handle(Server $server): string + public function handle(Server $server, bool $hideAllValues = false): string { $find = ['{{SERVER_MEMORY}}', '{{SERVER_IP}}', '{{SERVER_PORT}}']; $replace = [$server->memory, $server->allocation->ip, $server->allocation->port]; foreach ($server->variables as $variable) { $find[] = '{{' . $variable->env_variable . '}}'; - $replace[] = $variable->user_viewable ? ($variable->server_value ?? $variable->default_value) : '[hidden]'; + $replace[] = ($variable->user_viewable && !$hideAllValues) ? ($variable->server_value ?? $variable->default_value) : '[hidden]'; } return str_replace($find, $replace, $server->startup); diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php index 6d5b86ac5..d40bfd3f6 100644 --- a/app/Transformers/Api/Client/ServerTransformer.php +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -61,7 +61,7 @@ class ServerTransformer extends BaseClientTransformer 'io' => $server->io, 'cpu' => $server->cpu, ], - 'invocation' => $service->handle($server), + 'invocation' => $service->handle($server, ! $this->getUser()->can(Permission::ACTION_STARTUP_READ, $server)), 'feature_limits' => [ 'databases' => $server->database_limit, 'allocations' => $server->allocation_limit, From 56475d89bbde9be04e4934ca697bca2fc4625df2 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 22 Aug 2020 19:05:43 -0700 Subject: [PATCH 25/54] Fix rendering when trying to access server from state --- .../components/elements/PageContentBlock.tsx | 11 ++++------- .../elements/ServerContentBlock.tsx | 19 +++++++++++++++++++ .../server/network/NetworkContainer.tsx | 7 +++---- .../server/startup/StartupContainer.tsx | 6 +++--- 4 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 resources/scripts/components/elements/ServerContentBlock.tsx diff --git a/resources/scripts/components/elements/PageContentBlock.tsx b/resources/scripts/components/elements/PageContentBlock.tsx index 392cffb8d..bcbead377 100644 --- a/resources/scripts/components/elements/PageContentBlock.tsx +++ b/resources/scripts/components/elements/PageContentBlock.tsx @@ -4,23 +4,20 @@ import { CSSTransition } from 'react-transition-group'; import tw from 'twin.macro'; import FlashMessageRender from '@/components/FlashMessageRender'; import { Helmet } from 'react-helmet'; -import useServer from '@/plugins/useServer'; -interface Props { +export interface PageContentBlockProps { title?: string; className?: string; showFlashKey?: string; } -const PageContentBlock: React.FC = ({ title, showFlashKey, className, children }) => { - const { name } = useServer(); - +const PageContentBlock: React.FC = ({ title, showFlashKey, className, children }) => { return ( <> - {!!title && + {title && - {name} | {title} + {title} } diff --git a/resources/scripts/components/elements/ServerContentBlock.tsx b/resources/scripts/components/elements/ServerContentBlock.tsx new file mode 100644 index 000000000..d2e9bebc3 --- /dev/null +++ b/resources/scripts/components/elements/ServerContentBlock.tsx @@ -0,0 +1,19 @@ +import useServer from '@/plugins/useServer'; +import PageContentBlock, { PageContentBlockProps } from '@/components/elements/PageContentBlock'; +import React from 'react'; + +interface Props extends PageContentBlockProps { + title: string; +} + +const ServerContentBlock: React.FC = ({ title, children, ...props }) => { + const { name } = useServer(); + + return ( + + {children} + + ); +}; + +export default ServerContentBlock; diff --git a/resources/scripts/components/server/network/NetworkContainer.tsx b/resources/scripts/components/server/network/NetworkContainer.tsx index 64d69aec0..d0682b8dd 100644 --- a/resources/scripts/components/server/network/NetworkContainer.tsx +++ b/resources/scripts/components/server/network/NetworkContainer.tsx @@ -1,10 +1,8 @@ import React, { useEffect, useState } from 'react'; -import { Helmet } from 'react-helmet'; import tw from 'twin.macro'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faNetworkWired } from '@fortawesome/free-solid-svg-icons'; import styled from 'styled-components/macro'; -import PageContentBlock from '@/components/elements/PageContentBlock'; import GreyRowBox from '@/components/elements/GreyRowBox'; import Button from '@/components/elements/Button'; import Can from '@/components/elements/Can'; @@ -19,6 +17,7 @@ import { Textarea } from '@/components/elements/Input'; import setServerAllocationNotes from '@/api/server/network/setServerAllocationNotes'; import { debounce } from 'debounce'; import InputSpinner from '@/components/elements/InputSpinner'; +import ServerContentBlock from '@/components/elements/ServerContentBlock'; const Code = styled.code`${tw`font-mono py-1 px-2 bg-neutral-900 rounded text-sm block`}`; const Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`; @@ -61,7 +60,7 @@ const NetworkContainer = () => { }, [ error ]); return ( - + {!data ? : @@ -109,7 +108,7 @@ const NetworkContainer = () => { )) } - + ); }; diff --git a/resources/scripts/components/server/startup/StartupContainer.tsx b/resources/scripts/components/server/startup/StartupContainer.tsx index 481293145..2df1e44e0 100644 --- a/resources/scripts/components/server/startup/StartupContainer.tsx +++ b/resources/scripts/components/server/startup/StartupContainer.tsx @@ -1,15 +1,15 @@ import React from 'react'; -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'; +import ServerContentBlock from '@/components/elements/ServerContentBlock'; const StartupContainer = () => { const { invocation, variables } = useServer(); return ( - +

@@ -20,7 +20,7 @@ const StartupContainer = () => {

{variables.map(variable => )}
- + ); }; From f561089cad3deafb869845a6fa11dc7f3cbb35f4 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 22 Aug 2020 22:10:16 -0700 Subject: [PATCH 26/54] Fix the unholy hell that is drag events --- .../scripts/components/elements/Modal.tsx | 2 +- .../components/server/files/UploadButton.tsx | 78 ++++++++++--------- resources/scripts/plugins/useEventListener.ts | 23 +++--- 3 files changed, 53 insertions(+), 50 deletions(-) diff --git a/resources/scripts/components/elements/Modal.tsx b/resources/scripts/components/elements/Modal.tsx index 395f7a5a1..b62918050 100644 --- a/resources/scripts/components/elements/Modal.tsx +++ b/resources/scripts/components/elements/Modal.tsx @@ -20,7 +20,7 @@ export interface ModalProps extends RequiredModalProps { showSpinnerOverlay?: boolean; } -const ModalMask = styled.div` +export const ModalMask = styled.div` ${tw`fixed z-50 overflow-auto flex w-full inset-0`}; background: rgba(0, 0, 0, 0.70); `; diff --git a/resources/scripts/components/server/files/UploadButton.tsx b/resources/scripts/components/server/files/UploadButton.tsx index e328c5dc8..979b0a600 100644 --- a/resources/scripts/components/server/files/UploadButton.tsx +++ b/resources/scripts/components/server/files/UploadButton.tsx @@ -5,39 +5,41 @@ import tw from 'twin.macro'; import Button from '@/components/elements/Button'; import React, { useEffect, useState } from 'react'; import styled from 'styled-components/macro'; +import { ModalMask } from '@/components/elements/Modal'; +import Fade from '@/components/elements/Fade'; +import useEventListener from '@/plugins/useEventListener'; -const ModalMask = styled.div` - ${tw`fixed z-50 overflow-auto flex w-full inset-0`}; - background: rgba(0, 0, 0, 0.70); +const InnerContainer = styled.div` + max-width: 600px; + ${tw`bg-black w-full border-4 border-primary-500 border-dashed rounded p-10 mx-10`} `; export default () => { const { uuid } = useServer(); const [ visible, setVisible ] = useState(false); - const handleEscapeEvent = () => { + useEventListener('dragenter', e => { + e.stopPropagation(); + setVisible(true); + }, true); + + useEventListener('dragexit', e => { + e.stopPropagation(); setVisible(false); - }; + }, true); useEffect(() => { - window.addEventListener('keydown', handleEscapeEvent); + if (!visible) return; - return () => window.removeEventListener('keydown', handleEscapeEvent); + const hide = () => setVisible(false); + + window.addEventListener('keydown', hide); + return () => { + window.removeEventListener('keydown', hide); + }; }, [ visible ]); - const onDragOver = (e: any) => { - e.preventDefault(); - }; - - const onDragEnter = (e: any) => { - e.preventDefault(); - }; - - const onDragLeave = (e: any) => { - e.preventDefault(); - }; - - const onFileDrop = (e: any) => { + const onFileDrop = (e: React.DragEvent) => { e.preventDefault(); if (e.dataTransfer === undefined || e.dataTransfer === null) { @@ -55,12 +57,10 @@ export default () => { formData.append('files', files[i]); } - console.log('getFileUploadUrl'); getFileUploadUrl(uuid) .then(url => { console.log(url); - // `${url}&directory=` axios.post(url, formData, { headers: { 'Content-Type': 'multipart/form-data', @@ -81,20 +81,26 @@ export default () => { return ( <> - { - visible ? - -
-
- -

Drag and drop files to upload

-
-
-
- : - null - } - + + setVisible(false)} + onDrop={onFileDrop} + > +
+ +

+ Drag and drop files to upload. +

+
+
+
+
diff --git a/resources/scripts/plugins/useEventListener.ts b/resources/scripts/plugins/useEventListener.ts index 1328fffff..969549339 100644 --- a/resources/scripts/plugins/useEventListener.ts +++ b/resources/scripts/plugins/useEventListener.ts @@ -1,23 +1,20 @@ import { useEffect, useRef } from 'react'; -export default (eventName: string, handler: (e: Event | CustomEvent | UIEvent | any) => void, element: any = window) => { +export default (eventName: string, handler: (e: Event | CustomEvent | UIEvent | any) => void, options?: boolean | EventListenerOptions) => { const savedHandler = useRef(null); useEffect(() => { savedHandler.current = handler; }, [ handler ]); - useEffect( - () => { - const isSupported = element && element.addEventListener; - if (!isSupported) return; + useEffect(() => { + const isSupported = document && document.addEventListener; + if (!isSupported) return; - const eventListener = (event: any) => savedHandler.current(event); - element.addEventListener(eventName, eventListener); - return () => { - element.removeEventListener(eventName, eventListener); - }; - }, - [ eventName, element ], - ); + const eventListener = (event: any) => savedHandler.current(event); + document.addEventListener(eventName, eventListener, options); + return () => { + document.removeEventListener(eventName, eventListener); + }; + }, [ eventName, document ]); }; From b4c64d3dc0d5efacef3cefb04c07197a05d75fa7 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 22 Aug 2020 22:35:53 -0700 Subject: [PATCH 27/54] Better handling of file uploads --- resources/scripts/api/http.ts | 5 ++ .../components/server/files/UploadButton.tsx | 53 ++++++++----------- resources/scripts/plugins/useEventListener.ts | 8 +-- 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/resources/scripts/api/http.ts b/resources/scripts/api/http.ts index 9ac1b64f8..a642bb16e 100644 --- a/resources/scripts/api/http.ts +++ b/resources/scripts/api/http.ts @@ -66,6 +66,11 @@ export function httpErrorToHuman (error: any): string { if (data.errors && data.errors[0] && data.errors[0].detail) { return data.errors[0].detail; } + + // Errors from wings directory, mostly just for file uploads. + if (data.error && typeof data.error === 'string') { + return data.error; + } } return error.message; diff --git a/resources/scripts/components/server/files/UploadButton.tsx b/resources/scripts/components/server/files/UploadButton.tsx index 979b0a600..8b48052bd 100644 --- a/resources/scripts/components/server/files/UploadButton.tsx +++ b/resources/scripts/components/server/files/UploadButton.tsx @@ -8,6 +8,8 @@ import styled from 'styled-components/macro'; import { ModalMask } from '@/components/elements/Modal'; import Fade from '@/components/elements/Fade'; import useEventListener from '@/plugins/useEventListener'; +import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; +import useFlash from '@/plugins/useFlash'; const InnerContainer = styled.div` max-width: 600px; @@ -17,6 +19,8 @@ const InnerContainer = styled.div` export default () => { const { uuid } = useServer(); const [ visible, setVisible ] = useState(false); + const [ loading, setLoading ] = useState(false); + const { clearFlashes, clearAndAddHttpError } = useFlash(); useEventListener('dragenter', e => { e.stopPropagation(); @@ -41,42 +45,33 @@ export default () => { const onFileDrop = (e: React.DragEvent) => { e.preventDefault(); + e.stopPropagation(); + setVisible(false); if (e.dataTransfer === undefined || e.dataTransfer === null) { return; } - const files: FileList = e.dataTransfer.files; - console.log(files); - - const formData = new FormData(); - - for (let i = 0; i < files.length; i++) { - console.log(files[i]); - // @ts-ignore - formData.append('files', files[i]); - } + const form = new FormData(); + Array.from(e.dataTransfer.files).forEach(file => form.append('files', file)); + setLoading(true); + clearFlashes('files'); getFileUploadUrl(uuid) - .then(url => { - console.log(url); - - axios.post(url, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }) - .then(res => { - console.log(res); - setVisible(false); - }) - .catch(error => { - console.error(error); - }); + .then(url => axios.post(url, form, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + })) + .then(res => { + console.log(res); }) .catch(error => { console.error(error); - }); + clearAndAddHttpError({ error, key: 'files' }); + }) + .then(() => setVisible(false)) + .then(() => setLoading(false)); }; return ( @@ -88,10 +83,7 @@ export default () => { key={'upload_modal_mask'} unmountOnExit > - setVisible(false)} - onDrop={onFileDrop} - > + setVisible(false)} onDrop={onFileDrop} onDragOver={e => e.preventDefault()}>

@@ -101,6 +93,7 @@ export default () => {

+ diff --git a/resources/scripts/plugins/useEventListener.ts b/resources/scripts/plugins/useEventListener.ts index 969549339..f73374bf7 100644 --- a/resources/scripts/plugins/useEventListener.ts +++ b/resources/scripts/plugins/useEventListener.ts @@ -8,13 +8,13 @@ export default (eventName: string, handler: (e: Event | CustomEvent | UIEvent | }, [ handler ]); useEffect(() => { - const isSupported = document && document.addEventListener; + const isSupported = window && window.addEventListener; if (!isSupported) return; const eventListener = (event: any) => savedHandler.current(event); - document.addEventListener(eventName, eventListener, options); + window.addEventListener(eventName, eventListener, options); return () => { - document.removeEventListener(eventName, eventListener); + window.removeEventListener(eventName, eventListener); }; - }, [ eventName, document ]); + }, [ eventName, window ]); }; From f21aca20b2a6718194d2d91d05aacb5373db6cc1 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 22 Aug 2020 22:36:53 -0700 Subject: [PATCH 28/54] Mutate the store after uploading --- resources/scripts/components/server/files/UploadButton.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/scripts/components/server/files/UploadButton.tsx b/resources/scripts/components/server/files/UploadButton.tsx index 8b48052bd..f119b0d91 100644 --- a/resources/scripts/components/server/files/UploadButton.tsx +++ b/resources/scripts/components/server/files/UploadButton.tsx @@ -10,6 +10,7 @@ import Fade from '@/components/elements/Fade'; import useEventListener from '@/plugins/useEventListener'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import useFlash from '@/plugins/useFlash'; +import useFileManagerSwr from '@/plugins/useFileManagerSwr'; const InnerContainer = styled.div` max-width: 600px; @@ -20,6 +21,7 @@ export default () => { const { uuid } = useServer(); const [ visible, setVisible ] = useState(false); const [ loading, setLoading ] = useState(false); + const { mutate } = useFileManagerSwr(); const { clearFlashes, clearAndAddHttpError } = useFlash(); useEventListener('dragenter', e => { @@ -63,9 +65,7 @@ export default () => { 'Content-Type': 'multipart/form-data', }, })) - .then(res => { - console.log(res); - }) + .then(() => mutate()) .catch(error => { console.error(error); clearAndAddHttpError({ error, key: 'files' }); From 92929c45d542b100993da28054ff1086a1082458 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 23 Aug 2020 08:45:39 -0700 Subject: [PATCH 29/54] Fix query bug returning _all_ variables; closes #2250 --- app/Models/Server.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/Models/Server.php b/app/Models/Server.php index e6e9bca72..91ba9621a 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -4,6 +4,7 @@ namespace Pterodactyl\Models; use Illuminate\Notifications\Notifiable; use Pterodactyl\Models\Traits\Searchable; +use Illuminate\Database\Query\JoinClause; use Znck\Eloquent\Traits\BelongsToThrough; /** @@ -272,7 +273,15 @@ class Server extends Model { return $this->hasMany(EggVariable::class, 'egg_id', 'egg_id') ->select(['egg_variables.*', 'server_variables.variable_value as server_value']) - ->leftJoin('server_variables', 'server_variables.variable_id', '=', 'egg_variables.id'); + ->leftJoin('server_variables', function (JoinClause $join) { + // Don't forget to join against the server ID as well since the way we're using this relationship + // would actually return all of the variables and their values for _all_ servers using that egg,\ + // rather than only the server for this model. + // + // @see https://github.com/pterodactyl/panel/issues/2250 + $join->on('server_variables.variable_id', 'egg_variables.id') + ->where('server_variables.server_id', $this->id); + }); } /** From 5173f1f7e8023ad82a3c42c30833cb380f499a51 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 23 Aug 2020 14:56:05 -0700 Subject: [PATCH 30/54] Don't allow editing read only values; closes #2252 --- .../Api/Client/Servers/StartupController.php | 8 ++++++-- .../components/server/startup/VariableBox.tsx | 19 ++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Api/Client/Servers/StartupController.php b/app/Http/Controllers/Api/Client/Servers/StartupController.php index 6eb1df0ad..92961e1c9 100644 --- a/app/Http/Controllers/Api/Client/Servers/StartupController.php +++ b/app/Http/Controllers/Api/Client/Servers/StartupController.php @@ -55,9 +55,13 @@ class StartupController extends ClientApiController /** @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) { + if (is_null($variable) || !$variable->user_viewable) { throw new BadRequestHttpException( - "The environment variable you are trying to edit [\"{$request->input('key')}\"] does not exist." + "The environment variable you are trying to edit does not exist." + ); + } else if (! $variable->user_editable) { + throw new BadRequestHttpException( + "The environment variable you are trying to edit is read-only." ); } diff --git a/resources/scripts/components/server/startup/VariableBox.tsx b/resources/scripts/components/server/startup/VariableBox.tsx index e9e7b58f0..ffc589329 100644 --- a/resources/scripts/components/server/startup/VariableBox.tsx +++ b/resources/scripts/components/server/startup/VariableBox.tsx @@ -43,12 +43,25 @@ const VariableBox = ({ variable }: Props) => { }, 500); return ( - + + {!variable.isEditable && + Read Only + } + {variable.name} +

+ } + > setVariableValue(e.currentTarget.value)} - readOnly={!canEdit} + onKeyUp={e => { + if (canEdit && variable.isEditable) { + setVariableValue(e.currentTarget.value); + } + }} + readOnly={!canEdit || !variable.isEditable} name={variable.envVariable} defaultValue={variable.serverValue} placeholder={variable.defaultValue} From 89b18cbcacfbb2658da1402c557ff3adc7f8c72e Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 23 Aug 2020 15:55:10 -0700 Subject: [PATCH 31/54] Fix pages not rendering --- resources/scripts/hoc/requireServerPermission.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/scripts/hoc/requireServerPermission.tsx b/resources/scripts/hoc/requireServerPermission.tsx index 88ff05e20..fc3917da3 100644 --- a/resources/scripts/hoc/requireServerPermission.tsx +++ b/resources/scripts/hoc/requireServerPermission.tsx @@ -1,6 +1,5 @@ import React from 'react'; import Can from '@/components/elements/Can'; -import NotFound from '@/components/screens/NotFound'; import ScreenBlock from '@/components/screens/ScreenBlock'; const requireServerPermission = (Component: React.ComponentType, permissions: string | string[]) => { @@ -17,7 +16,7 @@ const requireServerPermission = (Component: React.ComponentType, permission /> } > - + ); } From 55cd7d4d390bd77f2b598b96df3a9556118f28de Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 23 Aug 2020 15:55:26 -0700 Subject: [PATCH 32/54] Allow editing files up to 4MB --- config/pterodactyl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/pterodactyl.php b/config/pterodactyl.php index b37790cbc..671a64fd3 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -177,7 +177,7 @@ return [ | This array includes the MIME filetypes that can be edited via the web. */ 'files' => [ - 'max_edit_size' => env('PTERODACTYL_FILES_MAX_EDIT_SIZE', 1024 * 512), + 'max_edit_size' => env('PTERODACTYL_FILES_MAX_EDIT_SIZE', 1024 * 1024 * 4), 'editable' => [ 'application/json', 'application/javascript', From 4cd44d20255941909d652e547c44f3c80c23cfbc Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 23 Aug 2020 16:03:54 -0700 Subject: [PATCH 33/54] Hide checkbox when editing/creating files --- .../components/server/files/FileManagerBreadcrumbs.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx b/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx index 15a5f39c4..5ec1efa8a 100644 --- a/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx +++ b/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { ServerContext } from '@/state/server'; -import { NavLink } from 'react-router-dom'; +import { NavLink, useRouteMatch } from 'react-router-dom'; import { cleanDirectoryPath } from '@/helpers'; import tw from 'twin.macro'; import { FileActionCheckbox } from '@/components/server/files/SelectFileCheckbox'; @@ -13,6 +13,7 @@ interface Props { export default ({ withinFileEditor, isNewFile }: Props) => { const [ file, setFile ] = useState(null); + const { params } = useRouteMatch>(); const id = ServerContext.useStoreState(state => state.server.data!.id); const directory = ServerContext.useStoreState(state => state.files.directory); @@ -44,7 +45,7 @@ export default ({ withinFileEditor, isNewFile }: Props) => { return (
- {!!(files && files.length) && + {(files && files.length && !params?.action) && Date: Sun, 23 Aug 2020 18:06:47 -0700 Subject: [PATCH 34/54] Use checksum more broadly, not specifically SHA256 --- app/Models/Backup.php | 6 +-- .../Api/Client/BackupTransformer.php | 2 +- ...31_modify_checksums_column_for_backups.php | 41 +++++++++++++++++++ resources/scripts/api/server/types.d.ts | 2 +- resources/scripts/api/transformers.ts | 2 +- .../server/backups/BackupContextMenu.tsx | 2 +- .../components/server/backups/BackupRow.tsx | 2 +- .../server/backups/ChecksumModal.tsx | 2 +- 8 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 database/migrations/2020_08_23_175331_modify_checksums_column_for_backups.php diff --git a/app/Models/Backup.php b/app/Models/Backup.php index e164b1ae3..5a8ab28e3 100644 --- a/app/Models/Backup.php +++ b/app/Models/Backup.php @@ -12,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @property string $name * @property string[] $ignored_files * @property string $disk - * @property string|null $sha256_hash + * @property string|null $checksum * @property int $bytes * @property \Carbon\CarbonImmutable|null $completed_at * @property \Carbon\CarbonImmutable $created_at @@ -62,7 +62,7 @@ class Backup extends Model */ protected $attributes = [ 'is_successful' => true, - 'sha256_hash' => null, + 'checksum' => null, 'bytes' => 0, ]; @@ -76,7 +76,7 @@ class Backup extends Model 'name' => 'required|string', 'ignored_files' => 'array', 'disk' => 'required|string', - 'sha256_hash' => 'nullable|string', + 'checksum' => 'nullable|string', 'bytes' => 'numeric', ]; diff --git a/app/Transformers/Api/Client/BackupTransformer.php b/app/Transformers/Api/Client/BackupTransformer.php index 15d6b357f..d5acd41fe 100644 --- a/app/Transformers/Api/Client/BackupTransformer.php +++ b/app/Transformers/Api/Client/BackupTransformer.php @@ -25,7 +25,7 @@ class BackupTransformer extends BaseClientTransformer 'is_successful' => $backup->is_successful, 'name' => $backup->name, 'ignored_files' => $backup->ignored_files, - 'sha256_hash' => $backup->sha256_hash, + 'checksum' => $backup->checksum, 'bytes' => $backup->bytes, 'created_at' => $backup->created_at->toIso8601String(), 'completed_at' => $backup->completed_at ? $backup->completed_at->toIso8601String() : null, diff --git a/database/migrations/2020_08_23_175331_modify_checksums_column_for_backups.php b/database/migrations/2020_08_23_175331_modify_checksums_column_for_backups.php new file mode 100644 index 000000000..8a9013cde --- /dev/null +++ b/database/migrations/2020_08_23_175331_modify_checksums_column_for_backups.php @@ -0,0 +1,41 @@ +renameColumn('sha256_hash', 'checksum'); + }); + + Schema::table('backups', function (Blueprint $table) { + DB::update('UPDATE backups SET checksum = CONCAT(\'sha256:\', checksum)'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('backups', function (Blueprint $table) { + $table->renameColumn('checksum', 'sha256_hash'); + }); + + Schema::table('backups', function (Blueprint $table) { + DB::update('UPDATE backups SET sha256_hash = SUBSTRING(sha256_hash, 8)'); + }); + } +} diff --git a/resources/scripts/api/server/types.d.ts b/resources/scripts/api/server/types.d.ts index e11a39c45..b37fae402 100644 --- a/resources/scripts/api/server/types.d.ts +++ b/resources/scripts/api/server/types.d.ts @@ -3,7 +3,7 @@ export interface ServerBackup { isSuccessful: boolean; name: string; ignoredFiles: string; - sha256Hash: string; + checksum: string; bytes: number; createdAt: Date; completedAt: Date | null; diff --git a/resources/scripts/api/transformers.ts b/resources/scripts/api/transformers.ts index 53ee514ed..595f2b9c8 100644 --- a/resources/scripts/api/transformers.ts +++ b/resources/scripts/api/transformers.ts @@ -46,7 +46,7 @@ export const rawDataToServerBackup = ({ attributes }: FractalResponseData): Serv isSuccessful: attributes.is_successful, name: attributes.name, ignoredFiles: attributes.ignored_files, - sha256Hash: attributes.sha256_hash, + checksum: attributes.checksum, bytes: attributes.bytes, createdAt: new Date(attributes.created_at), completedAt: attributes.completed_at ? new Date(attributes.completed_at) : null, diff --git a/resources/scripts/components/server/backups/BackupContextMenu.tsx b/resources/scripts/components/server/backups/BackupContextMenu.tsx index 9542389cc..422155cf9 100644 --- a/resources/scripts/components/server/backups/BackupContextMenu.tsx +++ b/resources/scripts/components/server/backups/BackupContextMenu.tsx @@ -66,7 +66,7 @@ export default ({ backup }: Props) => { appear visible={visible} onDismissed={() => setVisible(false)} - checksum={backup.sha256Hash} + checksum={backup.checksum} /> } { items: data.items.map(b => b.uuid !== backup.uuid ? b : ({ ...b, isSuccessful: parsed.is_successful || true, - sha256Hash: parsed.sha256_hash || '', + checksum: parsed.checksum || '', bytes: parsed.file_size || 0, completedAt: new Date(), })), diff --git a/resources/scripts/components/server/backups/ChecksumModal.tsx b/resources/scripts/components/server/backups/ChecksumModal.tsx index 91b275904..f37774f59 100644 --- a/resources/scripts/components/server/backups/ChecksumModal.tsx +++ b/resources/scripts/components/server/backups/ChecksumModal.tsx @@ -6,7 +6,7 @@ const ChecksumModal = ({ checksum, ...props }: RequiredModalProps & { checksum:

Verify file checksum

- The SHA256 checksum of this file is: + The checksum of this file is:

             {checksum}

From 1e58e108ba71ec6adcfcc61648d02f9f8c6bcd12 Mon Sep 17 00:00:00 2001
From: Dane Everitt 
Date: Sun, 23 Aug 2020 20:23:42 -0700
Subject: [PATCH 35/54] Update last location using wrong column name

---
 .../Controllers/Api/Remote/Backups/BackupStatusController.php   | 2 +-
 app/Http/Requests/Api/Remote/ReportBackupCompleteRequest.php    | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php b/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php
index be971d605..e658a8012 100644
--- a/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php
+++ b/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php
@@ -37,7 +37,7 @@ class BackupStatusController extends Controller
     {
         $this->repository->updateWhere([['uuid', '=', $backup]], [
             'is_successful' => $request->input('successful') ? true : false,
-            'sha256_hash' => $request->input('checksum'),
+            'checksum' => $request->input('checksum_type') . ':' . $request->input('checksum'),
             'bytes' => $request->input('size'),
             'completed_at' => CarbonImmutable::now(),
         ]);
diff --git a/app/Http/Requests/Api/Remote/ReportBackupCompleteRequest.php b/app/Http/Requests/Api/Remote/ReportBackupCompleteRequest.php
index edf744dc9..709961b71 100644
--- a/app/Http/Requests/Api/Remote/ReportBackupCompleteRequest.php
+++ b/app/Http/Requests/Api/Remote/ReportBackupCompleteRequest.php
@@ -14,6 +14,7 @@ class ReportBackupCompleteRequest extends FormRequest
         return [
             'successful' => 'boolean',
             'checksum' => 'nullable|string|required_if:successful,true',
+            'checksum_type' => 'string|required_if:successful,true',
             'size' => 'nullable|numeric|required_if:successful,true',
         ];
     }

From 74cf92b2e1ea807238efffcfc09618ba41c882d3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9mi=20Laberg=C3=A8re?= 
Date: Mon, 24 Aug 2020 18:50:25 +0200
Subject: [PATCH 36/54] Fix default values not applied in server creation

---
 app/Services/Servers/ServerCreationService.php | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php
index 09638547b..6d70d23e4 100644
--- a/app/Services/Servers/ServerCreationService.php
+++ b/app/Services/Servers/ServerCreationService.php
@@ -242,16 +242,16 @@ class ServerCreationService
             'io' => Arr::get($data, 'io'),
             'cpu' => Arr::get($data, 'cpu'),
             'threads' => Arr::get($data, 'threads'),
-            'oom_disabled' => Arr::get($data, 'oom_disabled', true),
+            'oom_disabled' => Arr::get($data, 'oom_disabled') ?? true,
             'allocation_id' => Arr::get($data, 'allocation_id'),
             'nest_id' => Arr::get($data, 'nest_id'),
             'egg_id' => Arr::get($data, 'egg_id'),
             'pack_id' => empty($data['pack_id']) ? null : $data['pack_id'],
             'startup' => Arr::get($data, 'startup'),
             'image' => Arr::get($data, 'image'),
-            'database_limit' => Arr::get($data, 'database_limit', 0),
-            'allocation_limit' => Arr::get($data, 'allocation_limit', 0),
-            'backup_limit' => Arr::get($data, 'backup_limit', 0),
+            'database_limit' => Arr::get($data, 'database_limit') ?? 0,
+            'allocation_limit' => Arr::get($data, 'allocation_limit') ?? 0,
+            'backup_limit' => Arr::get($data, 'backup_limit') ?? 0,
         ]);
 
         return $model;

From 773c42e4f28a2e2ab3764a28ccb08db2d4e1af00 Mon Sep 17 00:00:00 2001
From: Matthew Penner 
Date: Mon, 24 Aug 2020 11:26:05 -0600
Subject: [PATCH 37/54] Fix #2253

---
 public/assets/svgs/file_upload.svg                            | 1 -
 .../components/server/files/FileManagerBreadcrumbs.tsx        | 2 +-
 resources/scripts/components/server/files/UploadButton.tsx    | 4 +++-
 3 files changed, 4 insertions(+), 3 deletions(-)
 delete mode 100644 public/assets/svgs/file_upload.svg

diff --git a/public/assets/svgs/file_upload.svg b/public/assets/svgs/file_upload.svg
deleted file mode 100644
index f11a922ff..000000000
--- a/public/assets/svgs/file_upload.svg
+++ /dev/null
@@ -1 +0,0 @@
-going up
\ No newline at end of file
diff --git a/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx b/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx
index 5ec1efa8a..1cdcb05a4 100644
--- a/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx
+++ b/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx
@@ -45,7 +45,7 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
 
     return (
         
- {(files && files.length && !params?.action) && + {(files && files.length > 0 && !params?.action) && { const [ loading, setLoading ] = useState(false); const { mutate } = useFileManagerSwr(); const { clearFlashes, clearAndAddHttpError } = useFlash(); + const directory = ServerContext.useStoreState(state => state.files.directory); useEventListener('dragenter', e => { e.stopPropagation(); @@ -60,7 +62,7 @@ export default () => { setLoading(true); clearFlashes('files'); getFileUploadUrl(uuid) - .then(url => axios.post(url, form, { + .then(url => axios.post(`${url}&directory=${directory}`, form, { headers: { 'Content-Type': 'multipart/form-data', }, From 9e60cf9f5399511957794e3498e31adb79162164 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 24 Aug 2020 21:17:15 -0700 Subject: [PATCH 38/54] Show server status --- resources/scripts/components/dashboard/ServerRow.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/resources/scripts/components/dashboard/ServerRow.tsx b/resources/scripts/components/dashboard/ServerRow.tsx index d68744c49..a504f1951 100644 --- a/resources/scripts/components/dashboard/ServerRow.tsx +++ b/resources/scripts/components/dashboard/ServerRow.tsx @@ -59,7 +59,17 @@ export default ({ server, className }: { server: Server; className?: string }) =
-

{server.name}

+
+
+

{server.name}

+
From 96fef94578df5e9c1e4f7b75d63fe52f32d08a5b Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 25 Aug 2020 18:47:26 -0700 Subject: [PATCH 39/54] Don't try to use the contents of the file as the file name when using the save shortcut... closes #2266 --- .../scripts/components/elements/AceEditor.tsx | 4 +-- .../server/files/FileEditContainer.tsx | 26 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/resources/scripts/components/elements/AceEditor.tsx b/resources/scripts/components/elements/AceEditor.tsx index fbea88b8f..47fba4edb 100644 --- a/resources/scripts/components/elements/AceEditor.tsx +++ b/resources/scripts/components/elements/AceEditor.tsx @@ -28,7 +28,7 @@ export interface Props { filename?: string; onModeChanged: (mode: string) => void; fetchContent: (callback: () => Promise) => void; - onContentSaved: (content: string) => void; + onContentSaved: () => void; } export default ({ style, initialContent, filename, mode, fetchContent, onContentSaved, onModeChanged }: Props) => { @@ -70,7 +70,7 @@ export default ({ style, initialContent, filename, mode, fetchContent, onContent editor.commands.addCommand({ name: 'Save', bindKey: { win: 'Ctrl-s', mac: 'Command-s' }, - exec: (editor: Editor) => onContentSaved(editor.session.getValue()), + exec: () => onContentSaved(), }); fetchContent(() => Promise.resolve(editor.session.getValue())); diff --git a/resources/scripts/components/server/files/FileEditContainer.tsx b/resources/scripts/components/server/files/FileEditContainer.tsx index 4106ad53d..8ad40f72d 100644 --- a/resources/scripts/components/server/files/FileEditContainer.tsx +++ b/resources/scripts/components/server/files/FileEditContainer.tsx @@ -35,19 +35,19 @@ export default () => { let fetchFileContent: null | (() => Promise) = null; - if (action !== 'new') { - useEffect(() => { - setLoading(true); - setError(''); - getFileContents(uuid, hash.replace(/^#/, '')) - .then(setContent) - .catch(error => { - console.error(error); - setError(httpErrorToHuman(error)); - }) - .then(() => setLoading(false)); - }, [ uuid, hash ]); - } + useEffect(() => { + if (action === 'new') return; + + setLoading(true); + setError(''); + getFileContents(uuid, hash.replace(/^#/, '')) + .then(setContent) + .catch(error => { + console.error(error); + setError(httpErrorToHuman(error)); + }) + .then(() => setLoading(false)); + }, [ action, uuid, hash ]); const save = (name?: string) => { if (!fetchFileContent) { From 9d95c5ab32277cb5115b7c86b7cb2ef626d50ae2 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 25 Aug 2020 19:01:08 -0700 Subject: [PATCH 40/54] Less obtuse mounting code --- app/Models/Mount.php | 18 ++++----- app/Models/MountNode.php | 39 +++++++++++++++++++ app/Models/MountServer.php | 39 +++++++++++++++++++ .../ServerConfigurationStructureService.php | 16 ++------ 4 files changed, 89 insertions(+), 23 deletions(-) create mode 100644 app/Models/MountNode.php create mode 100644 app/Models/MountServer.php diff --git a/app/Models/Mount.php b/app/Models/Mount.php index ac0b5da9a..81e9a57c1 100644 --- a/app/Models/Mount.php +++ b/app/Models/Mount.php @@ -2,6 +2,9 @@ namespace Pterodactyl\Models; +use MountNode; +use MountServer; + /** * @property int $id * @property string $uuid @@ -45,11 +48,6 @@ class Mount extends Model */ protected $attributes = [ 'id' => 'int', - 'uuid' => 'string', - 'name' => 'string', - 'description' => 'string', - 'source' => 'string', - 'target' => 'string', 'read_only' => 'bool', 'user_mountable' => 'bool', ]; @@ -89,20 +87,18 @@ class Mount extends Model /** * Returns all nodes that have this mount assigned. * - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough */ public function nodes() { - return $this->belongsToMany(Node::class); + return $this->hasManyThrough(Server::class, MountNode::class); } /** - * Returns all servers that have this mount assigned. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough */ public function servers() { - return $this->belongsToMany(Server::class); + return $this->hasManyThrough(Server::class, MountServer::class); } } diff --git a/app/Models/MountNode.php b/app/Models/MountNode.php new file mode 100644 index 000000000..77f8bf3d5 --- /dev/null +++ b/app/Models/MountNode.php @@ -0,0 +1,39 @@ +belongsTo(Node::class); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function mount() + { + return $this->belongsTo(Mount::class); + } +} diff --git a/app/Models/MountServer.php b/app/Models/MountServer.php new file mode 100644 index 000000000..77b60208c --- /dev/null +++ b/app/Models/MountServer.php @@ -0,0 +1,39 @@ +belongsTo(Server::class); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function mount() + { + return $this->belongsTo(Mount::class); + } +} diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index fea2eaac0..ec8bbf560 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -9,6 +9,7 @@ namespace Pterodactyl\Services\Servers; +use Pterodactyl\Models\Mount; use Pterodactyl\Models\Server; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; @@ -71,17 +72,6 @@ class ServerConfigurationStructureService */ protected function returnCurrentFormat(Server $server) { - $mounts = $server->mounts; - foreach ($mounts as $mount) { - unset($mount->id); - unset($mount->uuid); - unset($mount->name); - unset($mount->description); - $mount->read_only = $mount->read_only == 1; - unset($mount->user_mountable); - unset($mount->pivot); - } - return [ 'uuid' => $server->uuid, 'suspended' => (bool) $server->suspended, @@ -112,7 +102,9 @@ class ServerConfigurationStructureService ], 'mappings' => $server->getAllocationMappings(), ], - 'mounts' => $mounts, + 'mounts' => $server->mounts->map(function (Mount $mount) { + return $mount->only('uuid', 'source', 'description', 'read_only'); + })->toArray(), ]; } From d58fd72bf55330a369c07e9e9c2a28ddeb2e0fb3 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 25 Aug 2020 19:11:25 -0700 Subject: [PATCH 41/54] Correctly pass along startup variables for a server; closes #2255 --- .../Repository/ServerRepositoryInterface.php | 12 ------ .../Admin/Servers/ServerViewController.php | 15 +++++-- .../Eloquent/ServerRepository.php | 39 ------------------- app/Services/Servers/EnvironmentService.php | 21 +++++----- 4 files changed, 22 insertions(+), 65 deletions(-) diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index 13a931764..f56582858 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -65,18 +65,6 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter */ public function getPrimaryAllocation(Server $server, bool $refresh = false): Server; - /** - * Return all of the server variables possible and default to the variable - * default if there is no value defined for the specific server requested. - * - * @param int $id - * @param bool $returnAsObject - * @return array|object - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function getVariablesWithValues(int $id, bool $returnAsObject = false); - /** * Return enough data to be used for the creation of a server via the daemon. * diff --git a/app/Http/Controllers/Admin/Servers/ServerViewController.php b/app/Http/Controllers/Admin/Servers/ServerViewController.php index 67531fa5f..5c2440b24 100644 --- a/app/Http/Controllers/Admin/Servers/ServerViewController.php +++ b/app/Http/Controllers/Admin/Servers/ServerViewController.php @@ -9,6 +9,7 @@ use Pterodactyl\Models\Server; use Illuminate\Contracts\View\Factory; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Services\Servers\EnvironmentService; use Pterodactyl\Repositories\Eloquent\NestRepository; use Pterodactyl\Repositories\Eloquent\NodeRepository; use Pterodactyl\Repositories\Eloquent\MountRepository; @@ -56,6 +57,11 @@ class ServerViewController extends Controller */ private $nodeRepository; + /** + * @var \Pterodactyl\Services\Servers\EnvironmentService + */ + private $environmentService; + /** * ServerViewController constructor. * @@ -66,6 +72,7 @@ class ServerViewController extends Controller * @param \Pterodactyl\Repositories\Eloquent\NestRepository $nestRepository * @param \Pterodactyl\Repositories\Eloquent\NodeRepository $nodeRepository * @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository + * @param \Pterodactyl\Services\Servers\EnvironmentService $environmentService */ public function __construct( Factory $view, @@ -74,7 +81,8 @@ class ServerViewController extends Controller MountRepository $mountRepository, NestRepository $nestRepository, NodeRepository $nodeRepository, - ServerRepository $repository + ServerRepository $repository, + EnvironmentService $environmentService ) { $this->view = $view; $this->databaseHostRepository = $databaseHostRepository; @@ -83,6 +91,7 @@ class ServerViewController extends Controller $this->nestRepository = $nestRepository; $this->nodeRepository = $nodeRepository; $this->repository = $repository; + $this->environmentService = $environmentService; } /** @@ -138,12 +147,12 @@ class ServerViewController extends Controller */ public function startup(Request $request, Server $server) { - $parameters = $this->repository->getVariablesWithValues($server->id, true); $nests = $this->nestRepository->getWithEggs(); + $variables = $this->environmentService->handle($server); $this->plainInject([ 'server' => $server, - 'server_variables' => $parameters->data, + 'server_variables' => $variables, 'nests' => $nests->map(function (Nest $item) { return array_merge($item->toArray(), [ 'eggs' => $item->eggs->keyBy('id')->toArray(), diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index 0f7919305..f749d0d18 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -131,45 +131,6 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt return $server; } - /** - * Return all of the server variables possible and default to the variable - * default if there is no value defined for the specific server requested. - * - * @param int $id - * @param bool $returnAsObject - * @return array|object - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function getVariablesWithValues(int $id, bool $returnAsObject = false) - { - $this->getBuilder() - ->with('variables', 'egg.variables') - ->findOrFail($id); - - try { - $instance = $this->getBuilder()->with('variables', 'egg.variables')->find($id, $this->getColumns()); - } catch (ModelNotFoundException $exception) { - throw new RecordNotFoundException; - } - - $data = []; - $instance->getRelation('egg')->getRelation('variables')->each(function ($item) use (&$data, $instance) { - $display = $instance->getRelation('variables')->where('variable_id', $item->id)->pluck('variable_value')->first(); - - $data[$item->env_variable] = $display ?? $item->default_value; - }); - - if ($returnAsObject) { - return (object) [ - 'data' => $data, - 'server' => $instance, - ]; - } - - return $data; - } - /** * Return enough data to be used for the creation of a server via the daemon. * diff --git a/app/Services/Servers/EnvironmentService.php b/app/Services/Servers/EnvironmentService.php index 68ae68dc4..8aab214d1 100644 --- a/app/Services/Servers/EnvironmentService.php +++ b/app/Services/Servers/EnvironmentService.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Services\Servers; use Pterodactyl\Models\Server; +use Pterodactyl\Models\EggVariable; use Illuminate\Contracts\Config\Repository as ConfigRepository; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; @@ -63,35 +64,33 @@ class EnvironmentService * * @param \Pterodactyl\Models\Server $server * @return array - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function handle(Server $server): array { - $variables = $this->repository->getVariablesWithValues($server->id); + $variables = $server->variables->toBase()->mapWithKeys(function (EggVariable $variable) { + return [$variable->env_variable => $variable->server_value ?? $variable->default_value]; + }); // Process environment variables defined in this file. This is done first // in order to allow run-time and config defined variables to take // priority over built-in values. foreach ($this->getEnvironmentMappings() as $key => $object) { - $variables[$key] = object_get($server, $object); + $variables->put($key, object_get($server, $object)); } // Process variables set in the configuration file. foreach ($this->config->get('pterodactyl.environment_variables', []) as $key => $object) { - if (is_callable($object)) { - $variables[$key] = call_user_func($object, $server); - } else { - $variables[$key] = object_get($server, $object); - } + $variables->put( + $key, is_callable($object) ? call_user_func($object, $server) : object_get($server, $object) + ); } // Process dynamically included environment variables. foreach ($this->additional as $key => $closure) { - $variables[$key] = call_user_func($closure, $server); + $variables->put($key, call_user_func($closure, $server)); } - return $variables; + return $variables->toArray(); } /** From 179885b546f3d818b60bd6497588c567c79001de Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 25 Aug 2020 19:22:17 -0700 Subject: [PATCH 42/54] Add endpoint to return startup variables; send back modified startup when a variable is edited --- .../Api/Client/Servers/StartupController.php | 37 ++++++++++++++++++- .../Servers/Startup/GetStartupRequest.php | 17 +++++++++ routes/api-client.php | 1 + 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 app/Http/Requests/Api/Client/Servers/Startup/GetStartupRequest.php diff --git a/app/Http/Controllers/Api/Client/Servers/StartupController.php b/app/Http/Controllers/Api/Client/Servers/StartupController.php index 92961e1c9..16975a1be 100644 --- a/app/Http/Controllers/Api/Client/Servers/StartupController.php +++ b/app/Http/Controllers/Api/Client/Servers/StartupController.php @@ -5,12 +5,14 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers; use Carbon\CarbonImmutable; use Pterodactyl\Models\Server; use Illuminate\Http\JsonResponse; +use Pterodactyl\Services\Servers\StartupCommandService; 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\GetStartupRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Startup\UpdateStartupVariableRequest; class StartupController extends ClientApiController @@ -25,18 +27,45 @@ class StartupController extends ClientApiController */ private $repository; + /** + * @var \Pterodactyl\Services\Servers\StartupCommandService + */ + private $startupCommandService; + /** * StartupController constructor. * * @param \Pterodactyl\Services\Servers\VariableValidatorService $service + * @param \Pterodactyl\Services\Servers\StartupCommandService $startupCommandService * @param \Pterodactyl\Repositories\Eloquent\ServerVariableRepository $repository */ - public function __construct(VariableValidatorService $service, ServerVariableRepository $repository) + public function __construct(VariableValidatorService $service, StartupCommandService $startupCommandService, ServerVariableRepository $repository) { parent::__construct(); $this->service = $service; $this->repository = $repository; + $this->startupCommandService = $startupCommandService; + } + + /** + * Returns the startup information for the server including all of the variables. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Startup\GetStartupRequest $request + * @param \Pterodactyl\Models\Server $server + * @return array + */ + public function index(GetStartupRequest $request, Server $server) + { + $startup = $this->startupCommandService->handle($server, false); + + return $this->fractal->collection($server->variables) + ->transformWith($this->getTransformer(EggVariableTransformer::class)) + ->addMeta([ + 'startup_command' => $startup, + 'raw_startup_command' => $server->startup, + ]) + ->toArray(); } /** @@ -78,8 +107,14 @@ class StartupController extends ClientApiController $variable = $variable->refresh(); $variable->server_value = $request->input('value'); + $startup = $this->startupCommandService->handle($server, false); + return $this->fractal->item($variable) ->transformWith($this->getTransformer(EggVariableTransformer::class)) + ->addMeta([ + 'startup_command' => $startup, + 'raw_startup_command' => $server->startup, + ]) ->toArray(); } } diff --git a/app/Http/Requests/Api/Client/Servers/Startup/GetStartupRequest.php b/app/Http/Requests/Api/Client/Servers/Startup/GetStartupRequest.php new file mode 100644 index 000000000..25ab2ce21 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Startup/GetStartupRequest.php @@ -0,0 +1,17 @@ + '/servers/{server}', 'middleware' => [AuthenticateServ }); Route::group(['prefix' => '/startup'], function () { + Route::get('/', 'Servers\StartupController@index'); Route::put('/variable', 'Servers\StartupController@update'); }); From 0e0b58ba5e29b9c68aad6d8dca1d80b07ea5663a Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 25 Aug 2020 21:00:43 -0700 Subject: [PATCH 43/54] Update wrapper.blade.php --- resources/views/templates/wrapper.blade.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/resources/views/templates/wrapper.blade.php b/resources/views/templates/wrapper.blade.php index 333a0f986..213f09358 100644 --- a/resources/views/templates/wrapper.blade.php +++ b/resources/views/templates/wrapper.blade.php @@ -35,21 +35,11 @@ @import url('//fonts.googleapis.com/css?family=IBM+Plex+Mono|IBM+Plex+Sans:500&display=swap'); - @section('assets') -{{-- {!! $asset->css('main.css') !!}--}} - @show + @yield('assets') @include('layouts.scripts') - @if(\Illuminate\Support\Str::contains(config('app.version'), ['-alpha', '-beta'])) -
-

- You are running a pre-release version of Pterodactyl. Please report any issues - via GitHub. -

-
- @endif @section('content') @yield('above-container') @yield('container') From c4418640eb4dae92e265cfdf1098e866a349f146 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 25 Aug 2020 21:25:31 -0700 Subject: [PATCH 44/54] Start cleaning up the mess of useServer; make startup page update in real time --- .../api/server/updateStartupVariable.ts | 4 +- resources/scripts/api/swr/getServerStartup.ts | 18 +++++ resources/scripts/components/elements/Can.tsx | 5 +- .../elements/ServerContentBlock.tsx | 4 +- .../scripts/components/elements/Spinner.tsx | 6 ++ .../server/startup/StartupContainer.tsx | 66 +++++++++++++++---- .../components/server/startup/VariableBox.tsx | 23 +++---- .../server/users/EditSubuserModal.tsx | 4 +- .../scripts/hoc/requireServerPermission.tsx | 5 ++ .../scripts/plugins/useDeepCompareEffect.ts | 5 ++ .../scripts/plugins/useDeepCompareMemo.ts | 5 ++ resources/scripts/plugins/useDeepMemo.ts | 12 ---- resources/scripts/plugins/useDeepMemoize.ts | 12 ++++ resources/scripts/plugins/usePermissions.ts | 4 +- resources/scripts/routers/ServerRouter.tsx | 46 +++++++++---- resources/scripts/state/server/index.ts | 17 ++++- 16 files changed, 175 insertions(+), 61 deletions(-) create mode 100644 resources/scripts/api/swr/getServerStartup.ts create mode 100644 resources/scripts/plugins/useDeepCompareEffect.ts create mode 100644 resources/scripts/plugins/useDeepCompareMemo.ts delete mode 100644 resources/scripts/plugins/useDeepMemo.ts create mode 100644 resources/scripts/plugins/useDeepMemoize.ts diff --git a/resources/scripts/api/server/updateStartupVariable.ts b/resources/scripts/api/server/updateStartupVariable.ts index 88231eccc..74498caa8 100644 --- a/resources/scripts/api/server/updateStartupVariable.ts +++ b/resources/scripts/api/server/updateStartupVariable.ts @@ -2,8 +2,8 @@ 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 => { +export default async (uuid: string, key: string, value: string): Promise<[ ServerEggVariable, string ]> => { const { data } = await http.put(`/api/client/servers/${uuid}/startup/variable`, { key, value }); - return rawDataToServerEggVariable(data); + return [ rawDataToServerEggVariable(data), data.meta.startup_command ]; }; diff --git a/resources/scripts/api/swr/getServerStartup.ts b/resources/scripts/api/swr/getServerStartup.ts new file mode 100644 index 000000000..fff0263f9 --- /dev/null +++ b/resources/scripts/api/swr/getServerStartup.ts @@ -0,0 +1,18 @@ +import useSWR from 'swr'; +import http, { FractalResponseList } from '@/api/http'; +import { rawDataToServerEggVariable } from '@/api/transformers'; +import { ServerEggVariable } from '@/api/server/types'; + +interface Response { + invocation: string; + variables: ServerEggVariable[]; +} + +export default (uuid: string, initialData?: Response) => useSWR([ uuid, '/startup' ], async (): Promise => { + console.log('firing getServerStartup'); + const { data } = await http.get(`/api/client/servers/${uuid}/startup`); + + const variables = ((data as FractalResponseList).data || []).map(rawDataToServerEggVariable); + + return { invocation: data.meta.startup_command, variables }; +}, { initialData, errorRetryCount: 3 }); diff --git a/resources/scripts/components/elements/Can.tsx b/resources/scripts/components/elements/Can.tsx index 4ea140d3e..dd9d4845f 100644 --- a/resources/scripts/components/elements/Can.tsx +++ b/resources/scripts/components/elements/Can.tsx @@ -1,5 +1,6 @@ -import React from 'react'; +import React, { memo } from 'react'; import { usePermissions } from '@/plugins/usePermissions'; +import isEqual from 'react-fast-compare'; interface Props { action: string | string[]; @@ -23,4 +24,4 @@ const Can = ({ action, matchAny = false, renderOnError, children }: Props) => { ); }; -export default Can; +export default memo(Can, isEqual); diff --git a/resources/scripts/components/elements/ServerContentBlock.tsx b/resources/scripts/components/elements/ServerContentBlock.tsx index d2e9bebc3..0457d34a5 100644 --- a/resources/scripts/components/elements/ServerContentBlock.tsx +++ b/resources/scripts/components/elements/ServerContentBlock.tsx @@ -1,13 +1,13 @@ -import useServer from '@/plugins/useServer'; import PageContentBlock, { PageContentBlockProps } from '@/components/elements/PageContentBlock'; import React from 'react'; +import { ServerContext } from '@/state/server'; interface Props extends PageContentBlockProps { title: string; } const ServerContentBlock: React.FC = ({ title, children, ...props }) => { - const { name } = useServer(); + const name = ServerContext.useStoreState(state => state.server.data!.name); return ( diff --git a/resources/scripts/components/elements/Spinner.tsx b/resources/scripts/components/elements/Spinner.tsx index 127ab6524..d2f7ffb24 100644 --- a/resources/scripts/components/elements/Spinner.tsx +++ b/resources/scripts/components/elements/Spinner.tsx @@ -45,4 +45,10 @@ const Spinner = ({ centered, ...props }: Props) => ( ); Spinner.DisplayName = 'Spinner'; +Spinner.Size = { + SMALL: 'small' as SpinnerSize, + BASE: 'base' as SpinnerSize, + LARGE: 'large' as SpinnerSize, +}; + export default Spinner; diff --git a/resources/scripts/components/server/startup/StartupContainer.tsx b/resources/scripts/components/server/startup/StartupContainer.tsx index 2df1e44e0..bf3b7651c 100644 --- a/resources/scripts/components/server/startup/StartupContainer.tsx +++ b/resources/scripts/components/server/startup/StartupContainer.tsx @@ -1,26 +1,64 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import TitledGreyBox from '@/components/elements/TitledGreyBox'; -import useServer from '@/plugins/useServer'; import tw from 'twin.macro'; import VariableBox from '@/components/server/startup/VariableBox'; import ServerContentBlock from '@/components/elements/ServerContentBlock'; +import getServerStartup from '@/api/swr/getServerStartup'; +import Spinner from '@/components/elements/Spinner'; +import ServerError from '@/components/screens/ServerError'; +import { httpErrorToHuman } from '@/api/http'; +import { ServerContext } from '@/state/server'; +import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect'; const StartupContainer = () => { - const { invocation, variables } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); + const invocation = ServerContext.useStoreState(state => state.server.data!.invocation); + const variables = ServerContext.useStoreState(state => state.server.data!.variables); + + const { data, error, isValidating, mutate } = getServerStartup(uuid, { invocation, variables }); + + const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState); + + useEffect(() => { + // Since we're passing in initial data this will not trigger on mount automatically. We + // want to always fetch fresh information from the API however when we're loading the startup + // information. + mutate(); + }, []); + + useDeepCompareEffect(() => { + if (!data) return; + + setServerFromState(s => ({ + ...s, + invocation: data.invocation, + variables: data.variables, + })); + }, [ data ]); return ( - - -
-

- {invocation} -

+ !data ? + (!error || (error && isValidating)) ? + + : + mutate()} + /> + : + + +
+

+ {data.invocation} +

+
+
+
+ {data.variables.map(variable => )}
- -
- {variables.map(variable => )} -
-
+ ); }; diff --git a/resources/scripts/components/server/startup/VariableBox.tsx b/resources/scripts/components/server/startup/VariableBox.tsx index ffc589329..27c2cfa8f 100644 --- a/resources/scripts/components/server/startup/VariableBox.tsx +++ b/resources/scripts/components/server/startup/VariableBox.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { memo, useState } from 'react'; import { ServerEggVariable } from '@/api/server/types'; import TitledGreyBox from '@/components/elements/TitledGreyBox'; import { usePermissions } from '@/plugins/usePermissions'; @@ -8,9 +8,11 @@ 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'; +import getServerStartup from '@/api/swr/getServerStartup'; +import isEqual from 'react-fast-compare'; +import { ServerContext } from '@/state/server'; interface Props { variable: ServerEggVariable; @@ -19,22 +21,21 @@ interface Props { const VariableBox = ({ variable }: Props) => { const FLASH_KEY = `server:startup:${variable.envVariable}`; - const server = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const [ loading, setLoading ] = useState(false); const [ canEdit ] = usePermissions([ 'startup.update' ]); const { clearFlashes, clearAndAddHttpError } = useFlash(); - - const setServer = ServerContext.useStoreActions(actions => actions.server.setServer); + const { mutate } = getServerStartup(uuid); 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), - })) + updateStartupVariable(uuid, variable.envVariable, value) + .then(([ response, invocation ]) => mutate(data => ({ + invocation, + variables: data.variables.map(v => v.envVariable === response.envVariable ? response : v), + }), false)) .catch(error => { console.error(error); clearAndAddHttpError({ error, key: FLASH_KEY }); @@ -74,4 +75,4 @@ const VariableBox = ({ variable }: Props) => { ); }; -export default VariableBox; +export default memo(VariableBox, isEqual); diff --git a/resources/scripts/components/server/users/EditSubuserModal.tsx b/resources/scripts/components/server/users/EditSubuserModal.tsx index 02776932e..f3e0f81c7 100644 --- a/resources/scripts/components/server/users/EditSubuserModal.tsx +++ b/resources/scripts/components/server/users/EditSubuserModal.tsx @@ -15,7 +15,7 @@ import { httpErrorToHuman } from '@/api/http'; import FlashMessageRender from '@/components/FlashMessageRender'; import Can from '@/components/elements/Can'; import { usePermissions } from '@/plugins/usePermissions'; -import { useDeepMemo } from '@/plugins/useDeepMemo'; +import { useDeepCompareMemo } from '@/plugins/useDeepCompareMemo'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; import Label from '@/components/elements/Label'; @@ -63,7 +63,7 @@ const EditSubuserModal = forwardRef(({ subuser, ...pr const loggedInPermissions = ServerContext.useStoreState(state => state.server.permissions); // The permissions that can be modified by this user. - const editablePermissions = useDeepMemo(() => { + const editablePermissions = useDeepCompareMemo(() => { const cleaned = Object.keys(permissions) .map(key => Object.keys(permissions[key].keys).map(pkey => `${key}.${pkey}`)); diff --git a/resources/scripts/hoc/requireServerPermission.tsx b/resources/scripts/hoc/requireServerPermission.tsx index fc3917da3..2cafa8fcb 100644 --- a/resources/scripts/hoc/requireServerPermission.tsx +++ b/resources/scripts/hoc/requireServerPermission.tsx @@ -1,9 +1,14 @@ import React from 'react'; import Can from '@/components/elements/Can'; import ScreenBlock from '@/components/screens/ScreenBlock'; +import isEqual from 'react-fast-compare'; const requireServerPermission = (Component: React.ComponentType, permissions: string | string[]) => { return class extends React.Component { + shouldComponentUpdate (nextProps: Readonly) { + return !isEqual(nextProps, this.props); + } + render () { return ( + useEffect(callback, useDeepMemoize(dependencies)); diff --git a/resources/scripts/plugins/useDeepCompareMemo.ts b/resources/scripts/plugins/useDeepCompareMemo.ts new file mode 100644 index 000000000..635f09fe2 --- /dev/null +++ b/resources/scripts/plugins/useDeepCompareMemo.ts @@ -0,0 +1,5 @@ +import { DependencyList, useMemo } from 'react'; +import { useDeepMemoize } from '@/plugins/useDeepMemoize'; + +export const useDeepCompareMemo = (callback: () => T, dependencies: DependencyList) => + useMemo(callback, useDeepMemoize(dependencies)); diff --git a/resources/scripts/plugins/useDeepMemo.ts b/resources/scripts/plugins/useDeepMemo.ts deleted file mode 100644 index ccf602853..000000000 --- a/resources/scripts/plugins/useDeepMemo.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useRef } from 'react'; -import isEqual from 'react-fast-compare'; - -export const useDeepMemo = (fn: () => T, key: K): T => { - const ref = useRef<{ key: K, value: T }>(); - - if (!ref.current || !isEqual(key, ref.current.key)) { - ref.current = { key, value: fn() }; - } - - return ref.current.value; -}; diff --git a/resources/scripts/plugins/useDeepMemoize.ts b/resources/scripts/plugins/useDeepMemoize.ts new file mode 100644 index 000000000..228c402aa --- /dev/null +++ b/resources/scripts/plugins/useDeepMemoize.ts @@ -0,0 +1,12 @@ +import { DependencyList, MutableRefObject, useRef } from 'react'; +import isEqual from 'react-fast-compare'; + +export const useDeepMemoize = (value: T): T => { + const ref: MutableRefObject = useRef(); + + if (!isEqual(value, ref.current)) { + ref.current = value; + } + + return ref.current as T; +}; diff --git a/resources/scripts/plugins/usePermissions.ts b/resources/scripts/plugins/usePermissions.ts index 1b383114c..09e1a705d 100644 --- a/resources/scripts/plugins/usePermissions.ts +++ b/resources/scripts/plugins/usePermissions.ts @@ -1,10 +1,10 @@ import { ServerContext } from '@/state/server'; -import { useDeepMemo } from '@/plugins/useDeepMemo'; +import { useDeepCompareMemo } from '@/plugins/useDeepCompareMemo'; export const usePermissions = (action: string | string[]): boolean[] => { const userPermissions = ServerContext.useStoreState(state => state.server.permissions); - return useDeepMemo(() => { + return useDeepCompareMemo(() => { if (userPermissions[0] === '*') { return Array(Array.isArray(action) ? action.length : 1).fill(true); } diff --git a/resources/scripts/routers/ServerRouter.tsx b/resources/scripts/routers/ServerRouter.tsx index 7e0e30685..b830a3c7a 100644 --- a/resources/scripts/routers/ServerRouter.tsx +++ b/resources/scripts/routers/ServerRouter.tsx @@ -22,7 +22,6 @@ import ServerError from '@/components/screens/ServerError'; import { httpErrorToHuman } from '@/api/http'; import NotFound from '@/components/screens/NotFound'; import { useStoreState } from 'easy-peasy'; -import useServer from '@/plugins/useServer'; import ScreenBlock from '@/components/screens/ScreenBlock'; import SubNavigation from '@/components/elements/SubNavigation'; import NetworkContainer from '@/components/server/network/NetworkContainer'; @@ -34,7 +33,10 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) const { rootAdmin } = useStoreState(state => state.user.data!); const [ error, setError ] = useState(''); const [ installing, setInstalling ] = useState(false); - const server = useServer(); + + const id = ServerContext.useStoreState(state => state.server.data?.id); + const uuid = ServerContext.useStoreState(state => state.server.data?.uuid); + const isInstalling = ServerContext.useStoreState(state => state.server.data?.isInstalling); const getServer = ServerContext.useStoreActions(actions => actions.server.getServer); const clearServerState = ServerContext.useStoreActions(actions => actions.clearServerState); @@ -43,8 +45,8 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) }, []); useEffect(() => { - setInstalling(server?.isInstalling !== false); - }, [ server?.isInstalling ]); + setInstalling(!!isInstalling); + }, [ isInstalling ]); useEffect(() => { setError(''); @@ -71,7 +73,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) return ( - {!server ? + {(!uuid || !id) ? error ? : @@ -111,7 +113,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) - {(installing && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${server.id}`)))) ? + {(installing && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${id}`)))) ? ) )} exact /> - - + + - - - - + + + + diff --git a/resources/scripts/state/server/index.ts b/resources/scripts/state/server/index.ts index 87023a020..f6c8136e4 100644 --- a/resources/scripts/state/server/index.ts +++ b/resources/scripts/state/server/index.ts @@ -6,6 +6,7 @@ import subusers, { ServerSubuserStore } from '@/state/server/subusers'; import { composeWithDevTools } from 'redux-devtools-extension'; import schedules, { ServerScheduleStore } from '@/state/server/schedules'; import databases, { ServerDatabaseStore } from '@/state/server/databases'; +import isEqual from 'react-fast-compare'; export type ServerStatus = 'offline' | 'starting' | 'stopping' | 'running'; @@ -15,6 +16,7 @@ interface ServerDataStore { getServer: Thunk, ServerStore, Promise>; setServer: Action; + setServerFromState: Action Server>; setPermissions: Action; } @@ -29,11 +31,22 @@ const server: ServerDataStore = { }), setServer: action((state, payload) => { - state.data = payload; + if (!isEqual(payload, state.data)) { + state.data = payload; + } + }), + + setServerFromState: action((state, payload) => { + const output = payload(state.data!); + if (!isEqual(output, state.data)) { + state.data = output; + } }), setPermissions: action((state, payload) => { - state.permissions = payload; + if (!isEqual(payload, state.permissions)) { + state.permissions = payload; + } }), }; From 1598dac6f8458c252076ad503d818868dbbeb3b0 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 25 Aug 2020 21:39:00 -0700 Subject: [PATCH 45/54] Remove more references to useServer --- resources/scripts/api/swr/getServerBackups.ts | 4 +-- .../scripts/components/NavigationBar.tsx | 4 +-- .../components/server/InstallListener.tsx | 9 +++---- .../components/server/ServerConsole.tsx | 25 +++++++++---------- .../components/server/StopOrKillButton.tsx | 5 ++-- .../server/files/FileDropdownMenu.tsx | 3 +-- .../server/files/FileEditContainer.tsx | 5 ++-- .../server/files/FileManagerContainer.tsx | 13 +++------- .../server/files/MassActionsBar.tsx | 4 +-- .../server/files/NewDirectoryButton.tsx | 3 +-- .../server/files/RenameFileModal.tsx | 3 +-- .../components/server/files/UploadButton.tsx | 3 +-- .../server/settings/SettingsContainer.tsx | 24 ++++++++---------- .../components/server/startup/VariableBox.tsx | 1 - .../scripts/plugins/useFileManagerSwr.ts | 3 +-- 15 files changed, 47 insertions(+), 62 deletions(-) diff --git a/resources/scripts/api/swr/getServerBackups.ts b/resources/scripts/api/swr/getServerBackups.ts index d7487fde3..0c38cd278 100644 --- a/resources/scripts/api/swr/getServerBackups.ts +++ b/resources/scripts/api/swr/getServerBackups.ts @@ -2,10 +2,10 @@ import useSWR from 'swr'; import http, { getPaginationSet, PaginatedResult } from '@/api/http'; import { ServerBackup } from '@/api/server/types'; import { rawDataToServerBackup } from '@/api/transformers'; -import useServer from '@/plugins/useServer'; +import { ServerContext } from '@/state/server'; export default (page?: number | string) => { - const { uuid } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); return useSWR>([ 'server:backups', uuid, page ], async () => { const { data } = await http.get(`/api/client/servers/${uuid}/backups`, { params: { page } }); diff --git a/resources/scripts/components/NavigationBar.tsx b/resources/scripts/components/NavigationBar.tsx index 206e035c3..4f122a82d 100644 --- a/resources/scripts/components/NavigationBar.tsx +++ b/resources/scripts/components/NavigationBar.tsx @@ -43,8 +43,8 @@ const RightNavigation = styled.div` `; export default () => { - const user = useStoreState((state: ApplicationStore) => state.user.data!); const name = useStoreState((state: ApplicationStore) => state.settings.data!.name); + const rootAdmin = useStoreState((state: ApplicationStore) => state.user.data!.rootAdmin); return ( @@ -62,7 +62,7 @@ export default () => { - {user.rootAdmin && + {rootAdmin && diff --git a/resources/scripts/components/server/InstallListener.tsx b/resources/scripts/components/server/InstallListener.tsx index 8bc85778a..782b9da3f 100644 --- a/resources/scripts/components/server/InstallListener.tsx +++ b/resources/scripts/components/server/InstallListener.tsx @@ -1,23 +1,22 @@ import useWebsocketEvent from '@/plugins/useWebsocketEvent'; import { ServerContext } from '@/state/server'; -import useServer from '@/plugins/useServer'; const InstallListener = () => { - const server = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const getServer = ServerContext.useStoreActions(actions => actions.server.getServer); - const setServer = ServerContext.useStoreActions(actions => actions.server.setServer); + const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState); // Listen for the installation completion event and then fire off a request to fetch the updated // server information. This allows the server to automatically become available to the user if they // just sit on the page. useWebsocketEvent('install completed', () => { - getServer(server.uuid).catch(error => console.error(error)); + getServer(uuid).catch(error => console.error(error)); }); // When we see the install started event immediately update the state to indicate such so that the // screens automatically update. useWebsocketEvent('install started', () => { - setServer({ ...server, isInstalling: true }); + setServerFromState(s => ({ ...s, isInstalling: true })); }); return null; diff --git a/resources/scripts/components/server/ServerConsole.tsx b/resources/scripts/components/server/ServerConsole.tsx index 74ba4d750..253cb05e2 100644 --- a/resources/scripts/components/server/ServerConsole.tsx +++ b/resources/scripts/components/server/ServerConsole.tsx @@ -1,5 +1,4 @@ import React, { lazy, useEffect, useState } from 'react'; -import { Helmet } from 'react-helmet'; import { ServerContext } from '@/state/server'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCircle, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons'; @@ -7,11 +6,11 @@ import { bytesToHuman, megabytesToHuman } from '@/helpers'; import SuspenseSpinner from '@/components/elements/SuspenseSpinner'; import TitledGreyBox from '@/components/elements/TitledGreyBox'; import Can from '@/components/elements/Can'; -import PageContentBlock from '@/components/elements/PageContentBlock'; import ContentContainer from '@/components/elements/ContentContainer'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; import StopOrKillButton from '@/components/server/StopOrKillButton'; +import ServerContentBlock from '@/components/elements/ServerContentBlock'; export type PowerAction = 'start' | 'stop' | 'restart' | 'kill'; @@ -23,10 +22,13 @@ export default () => { const [ cpu, setCpu ] = useState(0); const [ disk, setDisk ] = useState(0); - const server = ServerContext.useStoreState(state => state.server.data!); + const name = ServerContext.useStoreState(state => state.server.data!.name); + const limits = ServerContext.useStoreState(state => state.server.data!.limits); + const isInstalling = ServerContext.useStoreState(state => state.server.data!.isInstalling); const status = ServerContext.useStoreState(state => state.status.value); - const { connected, instance } = ServerContext.useStoreState(state => state.socket); + const connected = ServerContext.useStoreState(state => state.socket.connected); + const instance = ServerContext.useStoreState(state => state.socket.instance); const statsListener = (data: string) => { let stats: any = {}; @@ -57,16 +59,13 @@ export default () => { }; }, [ instance, connected ]); - const disklimit = server.limits.disk ? megabytesToHuman(server.limits.disk) : 'Unlimited'; - const memorylimit = server.limits.memory ? megabytesToHuman(server.limits.memory) : 'Unlimited'; + const disklimit = limits.disk ? megabytesToHuman(limits.disk) : 'Unlimited'; + const memorylimit = limits.memory ? megabytesToHuman(limits.memory) : 'Unlimited'; return ( - - - {server.name} | Console - +
- +

{ / {disklimit}

- {!server.isInstalling ? + {!isInstalling ?
@@ -143,6 +142,6 @@ export default () => {
- + ); }; diff --git a/resources/scripts/components/server/StopOrKillButton.tsx b/resources/scripts/components/server/StopOrKillButton.tsx index fc8490655..ee9d40d2d 100644 --- a/resources/scripts/components/server/StopOrKillButton.tsx +++ b/resources/scripts/components/server/StopOrKillButton.tsx @@ -1,7 +1,8 @@ -import React, { useEffect, useState } from 'react'; +import React, { memo, useEffect, useState } from 'react'; import { ServerContext } from '@/state/server'; import { PowerAction } from '@/components/server/ServerConsole'; import Button from '@/components/elements/Button'; +import isEqual from 'react-fast-compare'; const StopOrKillButton = ({ onPress }: { onPress: (action: PowerAction) => void }) => { const [ clicked, setClicked ] = useState(false); @@ -27,4 +28,4 @@ const StopOrKillButton = ({ onPress }: { onPress: (action: PowerAction) => void ); }; -export default StopOrKillButton; +export default memo(StopOrKillButton, isEqual); diff --git a/resources/scripts/components/server/files/FileDropdownMenu.tsx b/resources/scripts/components/server/files/FileDropdownMenu.tsx index e64dd3d84..66b2fbb32 100644 --- a/resources/scripts/components/server/files/FileDropdownMenu.tsx +++ b/resources/scripts/components/server/files/FileDropdownMenu.tsx @@ -19,7 +19,6 @@ import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import copyFile from '@/api/server/files/copyFile'; import Can from '@/components/elements/Can'; import getFileDownloadUrl from '@/api/server/files/getFileDownloadUrl'; -import useServer from '@/plugins/useServer'; import useFlash from '@/plugins/useFlash'; import tw from 'twin.macro'; import { FileObject } from '@/api/server/files/loadDirectory'; @@ -56,7 +55,7 @@ const FileDropdownMenu = ({ file }: { file: FileObject }) => { const [ showSpinner, setShowSpinner ] = useState(false); const [ modal, setModal ] = useState(null); - const { uuid } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const { mutate } = useFileManagerSwr(); const { clearAndAddHttpError, clearFlashes } = useFlash(); const directory = ServerContext.useStoreState(state => state.files.directory); diff --git a/resources/scripts/components/server/files/FileEditContainer.tsx b/resources/scripts/components/server/files/FileEditContainer.tsx index 8ad40f72d..4b3d73a80 100644 --- a/resources/scripts/components/server/files/FileEditContainer.tsx +++ b/resources/scripts/components/server/files/FileEditContainer.tsx @@ -14,8 +14,8 @@ import tw from 'twin.macro'; import Button from '@/components/elements/Button'; import Select from '@/components/elements/Select'; import modes from '@/modes'; -import useServer from '@/plugins/useServer'; import useFlash from '@/plugins/useFlash'; +import { ServerContext } from '@/state/server'; const LazyAceEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/AceEditor')); @@ -30,7 +30,8 @@ export default () => { const history = useHistory(); const { hash } = useLocation(); - const { id, uuid } = useServer(); + const id = ServerContext.useStoreState(state => state.server.data!.id); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const { addError, clearFlashes } = useFlash(); let fetchFileContent: null | (() => Promise) = null; diff --git a/resources/scripts/components/server/files/FileManagerContainer.tsx b/resources/scripts/components/server/files/FileManagerContainer.tsx index de2b0df11..2c90e9925 100644 --- a/resources/scripts/components/server/files/FileManagerContainer.tsx +++ b/resources/scripts/components/server/files/FileManagerContainer.tsx @@ -1,5 +1,4 @@ import React, { useEffect } from 'react'; -import { Helmet } from 'react-helmet'; import { httpErrorToHuman } from '@/api/http'; import { CSSTransition } from 'react-transition-group'; import Spinner from '@/components/elements/Spinner'; @@ -9,15 +8,14 @@ import { FileObject } from '@/api/server/files/loadDirectory'; import NewDirectoryButton from '@/components/server/files/NewDirectoryButton'; import { Link, useLocation } from 'react-router-dom'; import Can from '@/components/elements/Can'; -import PageContentBlock from '@/components/elements/PageContentBlock'; import ServerError from '@/components/screens/ServerError'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; -import useServer from '@/plugins/useServer'; import { ServerContext } from '@/state/server'; import useFileManagerSwr from '@/plugins/useFileManagerSwr'; import MassActionsBar from '@/components/server/files/MassActionsBar'; import UploadButton from '@/components/server/files/UploadButton'; +import ServerContentBlock from '@/components/elements/ServerContentBlock'; const sortFiles = (files: FileObject[]): FileObject[] => { return files.sort((a, b) => a.name.localeCompare(b.name)) @@ -25,7 +23,7 @@ const sortFiles = (files: FileObject[]): FileObject[] => { }; export default () => { - const { id, name: serverName } = useServer(); + const id = ServerContext.useStoreState(state => state.server.data!.id); const { hash } = useLocation(); const { data: files, error, mutate } = useFileManagerSwr(); @@ -44,10 +42,7 @@ export default () => { } return ( - - - {serverName} | File Manager - + { !files ? @@ -93,6 +88,6 @@ export default () => {
} - + ); }; diff --git a/resources/scripts/components/server/files/MassActionsBar.tsx b/resources/scripts/components/server/files/MassActionsBar.tsx index 40067d039..b5dceb81a 100644 --- a/resources/scripts/components/server/files/MassActionsBar.tsx +++ b/resources/scripts/components/server/files/MassActionsBar.tsx @@ -8,14 +8,14 @@ import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import useFileManagerSwr from '@/plugins/useFileManagerSwr'; import useFlash from '@/plugins/useFlash'; import compressFiles from '@/api/server/files/compressFiles'; -import useServer from '@/plugins/useServer'; import { ServerContext } from '@/state/server'; import ConfirmationModal from '@/components/elements/ConfirmationModal'; import deleteFiles from '@/api/server/files/deleteFiles'; import RenameFileModal from '@/components/server/files/RenameFileModal'; const MassActionsBar = () => { - const { uuid } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); + const { mutate } = useFileManagerSwr(); const { clearFlashes, clearAndAddHttpError } = useFlash(); const [ loading, setLoading ] = useState(false); diff --git a/resources/scripts/components/server/files/NewDirectoryButton.tsx b/resources/scripts/components/server/files/NewDirectoryButton.tsx index 0a6a2b07f..709fdd5e6 100644 --- a/resources/scripts/components/server/files/NewDirectoryButton.tsx +++ b/resources/scripts/components/server/files/NewDirectoryButton.tsx @@ -8,7 +8,6 @@ import { object, string } from 'yup'; import createDirectory from '@/api/server/files/createDirectory'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; -import useServer from '@/plugins/useServer'; import { FileObject } from '@/api/server/files/loadDirectory'; import useFlash from '@/plugins/useFlash'; import useFileManagerSwr from '@/plugins/useFileManagerSwr'; @@ -36,7 +35,7 @@ const generateDirectoryData = (name: string): FileObject => ({ }); export default () => { - const { uuid } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const { clearAndAddHttpError } = useFlash(); const [ visible, setVisible ] = useState(false); diff --git a/resources/scripts/components/server/files/RenameFileModal.tsx b/resources/scripts/components/server/files/RenameFileModal.tsx index fb3c0620d..b81087948 100644 --- a/resources/scripts/components/server/files/RenameFileModal.tsx +++ b/resources/scripts/components/server/files/RenameFileModal.tsx @@ -7,7 +7,6 @@ import renameFiles from '@/api/server/files/renameFiles'; import { ServerContext } from '@/state/server'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; -import useServer from '@/plugins/useServer'; import useFileManagerSwr from '@/plugins/useFileManagerSwr'; import useFlash from '@/plugins/useFlash'; @@ -18,7 +17,7 @@ interface FormikValues { type OwnProps = RequiredModalProps & { files: string[]; useMoveTerminology?: boolean }; const RenameFileModal = ({ files, useMoveTerminology, ...props }: OwnProps) => { - const { uuid } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const { mutate } = useFileManagerSwr(); const { clearFlashes, clearAndAddHttpError } = useFlash(); const directory = ServerContext.useStoreState(state => state.files.directory); diff --git a/resources/scripts/components/server/files/UploadButton.tsx b/resources/scripts/components/server/files/UploadButton.tsx index 6fe275f90..5b399ef1d 100644 --- a/resources/scripts/components/server/files/UploadButton.tsx +++ b/resources/scripts/components/server/files/UploadButton.tsx @@ -1,6 +1,5 @@ import axios from 'axios'; import getFileUploadUrl from '@/api/server/files/getFileUploadUrl'; -import useServer from '@/plugins/useServer'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; import React, { useEffect, useState } from 'react'; @@ -19,7 +18,7 @@ const InnerContainer = styled.div` `; export default () => { - const { uuid } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const [ visible, setVisible ] = useState(false); const [ loading, setLoading ] = useState(false); const { mutate } = useFileManagerSwr(); diff --git a/resources/scripts/components/server/settings/SettingsContainer.tsx b/resources/scripts/components/server/settings/SettingsContainer.tsx index edaa3503f..f8934d075 100644 --- a/resources/scripts/components/server/settings/SettingsContainer.tsx +++ b/resources/scripts/components/server/settings/SettingsContainer.tsx @@ -1,29 +1,25 @@ import React from 'react'; -import { Helmet } from 'react-helmet'; import TitledGreyBox from '@/components/elements/TitledGreyBox'; import { ServerContext } from '@/state/server'; import { useStoreState } from 'easy-peasy'; -import { ApplicationStore } from '@/state'; -import { UserData } from '@/state/user'; import RenameServerBox from '@/components/server/settings/RenameServerBox'; import FlashMessageRender from '@/components/FlashMessageRender'; import Can from '@/components/elements/Can'; import ReinstallServerBox from '@/components/server/settings/ReinstallServerBox'; -import PageContentBlock from '@/components/elements/PageContentBlock'; import tw from 'twin.macro'; import Input from '@/components/elements/Input'; import Label from '@/components/elements/Label'; import { LinkButton } from '@/components/elements/Button'; +import ServerContentBlock from '@/components/elements/ServerContentBlock'; export default () => { - const user = useStoreState(state => state.user.data!); - const server = ServerContext.useStoreState(state => state.server.data!); + const username = useStoreState(state => state.user.data!.username); + const id = ServerContext.useStoreState(state => state.server.data!.id); + const sftpIp = ServerContext.useStoreState(state => state.server.data!.sftpDetails.ip); + const sftpPort = ServerContext.useStoreState(state => state.server.data!.sftpDetails.port); return ( - - - {server.name} | Settings - +
@@ -33,7 +29,7 @@ export default () => {
@@ -41,7 +37,7 @@ export default () => {
@@ -56,7 +52,7 @@ export default () => {
Launch SFTP @@ -76,6 +72,6 @@ export default () => {
-
+ ); }; diff --git a/resources/scripts/components/server/startup/VariableBox.tsx b/resources/scripts/components/server/startup/VariableBox.tsx index 27c2cfa8f..f238614ec 100644 --- a/resources/scripts/components/server/startup/VariableBox.tsx +++ b/resources/scripts/components/server/startup/VariableBox.tsx @@ -7,7 +7,6 @@ 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 useFlash from '@/plugins/useFlash'; import FlashMessageRender from '@/components/FlashMessageRender'; import getServerStartup from '@/api/swr/getServerStartup'; diff --git a/resources/scripts/plugins/useFileManagerSwr.ts b/resources/scripts/plugins/useFileManagerSwr.ts index 16721e72c..eb36848bf 100644 --- a/resources/scripts/plugins/useFileManagerSwr.ts +++ b/resources/scripts/plugins/useFileManagerSwr.ts @@ -1,11 +1,10 @@ import useSWR from 'swr'; import loadDirectory, { FileObject } from '@/api/server/files/loadDirectory'; import { cleanDirectoryPath } from '@/helpers'; -import useServer from '@/plugins/useServer'; import { ServerContext } from '@/state/server'; export default () => { - const { uuid } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const directory = ServerContext.useStoreState(state => state.files.directory); return useSWR( From ac1559de5e8170bcd97ae0f22812fd46b8141b69 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 25 Aug 2020 21:54:41 -0700 Subject: [PATCH 46/54] Revert "Less obtuse mounting code" This reverts commit 9d95c5ab32277cb5115b7c86b7cb2ef626d50ae2. --- app/Models/Mount.php | 18 +++++---- app/Models/MountNode.php | 39 ------------------- app/Models/MountServer.php | 39 ------------------- .../ServerConfigurationStructureService.php | 16 ++++++-- 4 files changed, 23 insertions(+), 89 deletions(-) delete mode 100644 app/Models/MountNode.php delete mode 100644 app/Models/MountServer.php diff --git a/app/Models/Mount.php b/app/Models/Mount.php index 81e9a57c1..ac0b5da9a 100644 --- a/app/Models/Mount.php +++ b/app/Models/Mount.php @@ -2,9 +2,6 @@ namespace Pterodactyl\Models; -use MountNode; -use MountServer; - /** * @property int $id * @property string $uuid @@ -48,6 +45,11 @@ class Mount extends Model */ protected $attributes = [ 'id' => 'int', + 'uuid' => 'string', + 'name' => 'string', + 'description' => 'string', + 'source' => 'string', + 'target' => 'string', 'read_only' => 'bool', 'user_mountable' => 'bool', ]; @@ -87,18 +89,20 @@ class Mount extends Model /** * Returns all nodes that have this mount assigned. * - * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function nodes() { - return $this->hasManyThrough(Server::class, MountNode::class); + return $this->belongsToMany(Node::class); } /** - * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough + * Returns all servers that have this mount assigned. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ public function servers() { - return $this->hasManyThrough(Server::class, MountServer::class); + return $this->belongsToMany(Server::class); } } diff --git a/app/Models/MountNode.php b/app/Models/MountNode.php deleted file mode 100644 index 77f8bf3d5..000000000 --- a/app/Models/MountNode.php +++ /dev/null @@ -1,39 +0,0 @@ -belongsTo(Node::class); - } - - /** - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function mount() - { - return $this->belongsTo(Mount::class); - } -} diff --git a/app/Models/MountServer.php b/app/Models/MountServer.php deleted file mode 100644 index 77b60208c..000000000 --- a/app/Models/MountServer.php +++ /dev/null @@ -1,39 +0,0 @@ -belongsTo(Server::class); - } - - /** - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function mount() - { - return $this->belongsTo(Mount::class); - } -} diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index ec8bbf560..fea2eaac0 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -9,7 +9,6 @@ namespace Pterodactyl\Services\Servers; -use Pterodactyl\Models\Mount; use Pterodactyl\Models\Server; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; @@ -72,6 +71,17 @@ class ServerConfigurationStructureService */ protected function returnCurrentFormat(Server $server) { + $mounts = $server->mounts; + foreach ($mounts as $mount) { + unset($mount->id); + unset($mount->uuid); + unset($mount->name); + unset($mount->description); + $mount->read_only = $mount->read_only == 1; + unset($mount->user_mountable); + unset($mount->pivot); + } + return [ 'uuid' => $server->uuid, 'suspended' => (bool) $server->suspended, @@ -102,9 +112,7 @@ class ServerConfigurationStructureService ], 'mappings' => $server->getAllocationMappings(), ], - 'mounts' => $server->mounts->map(function (Mount $mount) { - return $mount->only('uuid', 'source', 'description', 'read_only'); - })->toArray(), + 'mounts' => $mounts, ]; } From 8c98264eedbbf35e4dd38f082f8765f40aa01652 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 25 Aug 2020 22:09:54 -0700 Subject: [PATCH 47/54] Remove last calls to useServer --- .../server/backups/BackupContainer.tsx | 24 +++++++---------- .../server/backups/BackupContextMenu.tsx | 4 +-- .../server/backups/CreateBackupButton.tsx | 4 +-- .../server/databases/CreateDatabaseButton.tsx | 3 +-- .../server/databases/DatabaseRow.tsx | 5 ++-- .../server/databases/DatabasesContainer.tsx | 26 +++++++++---------- .../server/network/NetworkContainer.tsx | 7 +++-- .../server/schedules/EditScheduleModal.tsx | 3 +-- .../server/schedules/ScheduleContainer.tsx | 13 +++------- .../schedules/ScheduleEditContainer.tsx | 5 ++-- .../server/schedules/ScheduleTaskRow.tsx | 3 +-- .../server/schedules/TaskDetailsModal.tsx | 3 +-- resources/scripts/plugins/useServer.ts | 8 ------ 13 files changed, 44 insertions(+), 64 deletions(-) delete mode 100644 resources/scripts/plugins/useServer.ts diff --git a/resources/scripts/components/server/backups/BackupContainer.tsx b/resources/scripts/components/server/backups/BackupContainer.tsx index c7504b164..b3bfdc945 100644 --- a/resources/scripts/components/server/backups/BackupContainer.tsx +++ b/resources/scripts/components/server/backups/BackupContainer.tsx @@ -1,22 +1,21 @@ import React, { useEffect } from 'react'; -import { Helmet } from 'react-helmet'; import Spinner from '@/components/elements/Spinner'; -import useServer from '@/plugins/useServer'; import useFlash from '@/plugins/useFlash'; import Can from '@/components/elements/Can'; import CreateBackupButton from '@/components/server/backups/CreateBackupButton'; import FlashMessageRender from '@/components/FlashMessageRender'; import BackupRow from '@/components/server/backups/BackupRow'; -import PageContentBlock from '@/components/elements/PageContentBlock'; import tw from 'twin.macro'; import getServerBackups from '@/api/swr/getServerBackups'; +import { ServerContext } from '@/state/server'; +import ServerContentBlock from '@/components/elements/ServerContentBlock'; export default () => { const { clearFlashes, clearAndAddHttpError } = useFlash(); - const { featureLimits, name: serverName } = useServer(); - const { data: backups, error, isValidating } = getServerBackups(); + const backupLimit = ServerContext.useStoreState(state => state.server.data!.featureLimits.backups); + useEffect(() => { if (!error) { clearFlashes('backups'); @@ -32,10 +31,7 @@ export default () => { } return ( - - - {serverName} | Backups - + {!backups.items.length ?

@@ -50,23 +46,23 @@ export default () => { />)}

} - {featureLimits.backups === 0 && + {backupLimit === 0 &&

Backups cannot be created for this server.

} - {(featureLimits.backups > 0 && backups.items.length > 0) && + {(backupLimit > 0 && backups.items.length > 0) &&

- {backups.items.length} of {featureLimits.backups} backups have been created for this server. + {backups.items.length} of {backupLimit} backups have been created for this server.

} - {featureLimits.backups > 0 && featureLimits.backups !== backups.items.length && + {backupLimit > 0 && backupLimit !== backups.items.length &&
}
-
+ ); }; diff --git a/resources/scripts/components/server/backups/BackupContextMenu.tsx b/resources/scripts/components/server/backups/BackupContextMenu.tsx index 422155cf9..7c3ba07ac 100644 --- a/resources/scripts/components/server/backups/BackupContextMenu.tsx +++ b/resources/scripts/components/server/backups/BackupContextMenu.tsx @@ -6,20 +6,20 @@ import getBackupDownloadUrl from '@/api/server/backups/getBackupDownloadUrl'; import useFlash from '@/plugins/useFlash'; import ChecksumModal from '@/components/server/backups/ChecksumModal'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; -import useServer from '@/plugins/useServer'; import deleteBackup from '@/api/server/backups/deleteBackup'; import ConfirmationModal from '@/components/elements/ConfirmationModal'; import Can from '@/components/elements/Can'; import tw from 'twin.macro'; import getServerBackups from '@/api/swr/getServerBackups'; import { ServerBackup } from '@/api/server/types'; +import { ServerContext } from '@/state/server'; interface Props { backup: ServerBackup; } export default ({ backup }: Props) => { - const { uuid } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const [ loading, setLoading ] = useState(false); const [ visible, setVisible ] = useState(false); const [ deleteVisible, setDeleteVisible ] = useState(false); diff --git a/resources/scripts/components/server/backups/CreateBackupButton.tsx b/resources/scripts/components/server/backups/CreateBackupButton.tsx index 3fd53403a..e505fd251 100644 --- a/resources/scripts/components/server/backups/CreateBackupButton.tsx +++ b/resources/scripts/components/server/backups/CreateBackupButton.tsx @@ -5,13 +5,13 @@ import { object, string } from 'yup'; import Field from '@/components/elements/Field'; import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper'; import useFlash from '@/plugins/useFlash'; -import useServer from '@/plugins/useServer'; import createServerBackup from '@/api/server/backups/createServerBackup'; import FlashMessageRender from '@/components/FlashMessageRender'; import Button from '@/components/elements/Button'; import tw from 'twin.macro'; import { Textarea } from '@/components/elements/Input'; import getServerBackups from '@/api/swr/getServerBackups'; +import { ServerContext } from '@/state/server'; interface Values { name: string; @@ -58,7 +58,7 @@ const ModalContent = ({ ...props }: RequiredModalProps) => { }; export default () => { - const { uuid } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const { clearFlashes, clearAndAddHttpError } = useFlash(); const [ visible, setVisible ] = useState(false); const { mutate } = getServerBackups(); diff --git a/resources/scripts/components/server/databases/CreateDatabaseButton.tsx b/resources/scripts/components/server/databases/CreateDatabaseButton.tsx index 2b035ce2c..0fff36d25 100644 --- a/resources/scripts/components/server/databases/CreateDatabaseButton.tsx +++ b/resources/scripts/components/server/databases/CreateDatabaseButton.tsx @@ -8,7 +8,6 @@ import { ServerContext } from '@/state/server'; import { httpErrorToHuman } from '@/api/http'; import FlashMessageRender from '@/components/FlashMessageRender'; import useFlash from '@/plugins/useFlash'; -import useServer from '@/plugins/useServer'; import Button from '@/components/elements/Button'; import tw from 'twin.macro'; @@ -29,7 +28,7 @@ const schema = object().shape({ }); export default () => { - const { uuid } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const { addError, clearFlashes } = useFlash(); const [ visible, setVisible ] = useState(false); diff --git a/resources/scripts/components/server/databases/DatabaseRow.tsx b/resources/scripts/components/server/databases/DatabaseRow.tsx index 4cad11611..e0e3b038a 100644 --- a/resources/scripts/components/server/databases/DatabaseRow.tsx +++ b/resources/scripts/components/server/databases/DatabaseRow.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faDatabase, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons'; +import { faDatabase, faEye, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import Modal from '@/components/elements/Modal'; import { Form, Formik, FormikHelpers } from 'formik'; import Field from '@/components/elements/Field'; @@ -12,7 +12,6 @@ import { httpErrorToHuman } from '@/api/http'; import RotatePasswordButton from '@/components/server/databases/RotatePasswordButton'; import Can from '@/components/elements/Can'; import { ServerDatabase } from '@/api/server/getServerDatabases'; -import useServer from '@/plugins/useServer'; import useFlash from '@/plugins/useFlash'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; @@ -26,7 +25,7 @@ interface Props { } export default ({ database, className }: Props) => { - const { uuid } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const { addError, clearFlashes } = useFlash(); const [ visible, setVisible ] = useState(false); const [ connectionVisible, setConnectionVisible ] = useState(false); diff --git a/resources/scripts/components/server/databases/DatabasesContainer.tsx b/resources/scripts/components/server/databases/DatabasesContainer.tsx index 922f0a364..8ee4b3129 100644 --- a/resources/scripts/components/server/databases/DatabasesContainer.tsx +++ b/resources/scripts/components/server/databases/DatabasesContainer.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from 'react'; -import { Helmet } from 'react-helmet'; import getServerDatabases from '@/api/server/getServerDatabases'; import { ServerContext } from '@/state/server'; import { httpErrorToHuman } from '@/api/http'; @@ -9,17 +8,19 @@ import Spinner from '@/components/elements/Spinner'; import CreateDatabaseButton from '@/components/server/databases/CreateDatabaseButton'; import Can from '@/components/elements/Can'; import useFlash from '@/plugins/useFlash'; -import useServer from '@/plugins/useServer'; -import PageContentBlock from '@/components/elements/PageContentBlock'; import tw from 'twin.macro'; import Fade from '@/components/elements/Fade'; +import ServerContentBlock from '@/components/elements/ServerContentBlock'; +import { useDeepMemoize } from '@/plugins/useDeepMemoize'; export default () => { - const { uuid, featureLimits, name: serverName } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); + const databaseLimit = ServerContext.useStoreState(state => state.server.data!.featureLimits.databases); + const { addError, clearFlashes } = useFlash(); const [ loading, setLoading ] = useState(true); - const databases = ServerContext.useStoreState(state => state.databases.data); + const databases = useDeepMemoize(ServerContext.useStoreState(state => state.databases.data)); const setDatabases = ServerContext.useStoreActions(state => state.databases.setDatabases); useEffect(() => { @@ -36,10 +37,7 @@ export default () => { }, []); return ( - - - {serverName} | Databases - + {(!databases.length && loading) ? @@ -56,7 +54,7 @@ export default () => { )) :

- {featureLimits.databases > 0 ? + {databaseLimit > 0 ? 'It looks like you have no databases.' : 'Databases cannot be created for this server.' @@ -64,13 +62,13 @@ export default () => {

} - {(featureLimits.databases > 0 && databases.length > 0) && + {(databaseLimit > 0 && databases.length > 0) &&

- {databases.length} of {featureLimits.databases} databases have been allocated to this + {databases.length} of {databaseLimit} databases have been allocated to this server.

} - {featureLimits.databases > 0 && featureLimits.databases !== databases.length && + {databaseLimit > 0 && databaseLimit !== databases.length &&
@@ -79,6 +77,6 @@ export default () => { } -
+ ); }; diff --git a/resources/scripts/components/server/network/NetworkContainer.tsx b/resources/scripts/components/server/network/NetworkContainer.tsx index d0682b8dd..14ae10597 100644 --- a/resources/scripts/components/server/network/NetworkContainer.tsx +++ b/resources/scripts/components/server/network/NetworkContainer.tsx @@ -6,7 +6,6 @@ import styled from 'styled-components/macro'; import GreyRowBox from '@/components/elements/GreyRowBox'; import Button from '@/components/elements/Button'; import Can from '@/components/elements/Can'; -import useServer from '@/plugins/useServer'; import useSWR from 'swr'; import getServerAllocations from '@/api/server/network/getServerAllocations'; import { Allocation } from '@/api/server/getServer'; @@ -18,12 +17,16 @@ import setServerAllocationNotes from '@/api/server/network/setServerAllocationNo import { debounce } from 'debounce'; import InputSpinner from '@/components/elements/InputSpinner'; import ServerContentBlock from '@/components/elements/ServerContentBlock'; +import { ServerContext } from '@/state/server'; +import { useDeepMemoize } from '@/plugins/useDeepMemoize'; const Code = styled.code`${tw`font-mono py-1 px-2 bg-neutral-900 rounded text-sm block`}`; const Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`; const NetworkContainer = () => { - const { uuid, allocations } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); + const allocations = useDeepMemoize(ServerContext.useStoreState(state => state.server.data!.allocations)); + const { clearFlashes, clearAndAddHttpError } = useFlash(); const [ loading, setLoading ] = useState(false); const { data, error, mutate } = useSWR(uuid, key => getServerAllocations(key), { initialData: allocations }); diff --git a/resources/scripts/components/server/schedules/EditScheduleModal.tsx b/resources/scripts/components/server/schedules/EditScheduleModal.tsx index 1cf9eea3a..5d42888f3 100644 --- a/resources/scripts/components/server/schedules/EditScheduleModal.tsx +++ b/resources/scripts/components/server/schedules/EditScheduleModal.tsx @@ -8,7 +8,6 @@ import createOrUpdateSchedule from '@/api/server/schedules/createOrUpdateSchedul import { ServerContext } from '@/state/server'; import { httpErrorToHuman } from '@/api/http'; import FlashMessageRender from '@/components/FlashMessageRender'; -import useServer from '@/plugins/useServer'; import useFlash from '@/plugins/useFlash'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; @@ -75,7 +74,7 @@ const EditScheduleModal = ({ schedule, ...props }: Omit { - const { uuid } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const { addError, clearFlashes } = useFlash(); const [ modalVisible, setModalVisible ] = useState(visible); diff --git a/resources/scripts/components/server/schedules/ScheduleContainer.tsx b/resources/scripts/components/server/schedules/ScheduleContainer.tsx index 77e31b590..b9c1f0340 100644 --- a/resources/scripts/components/server/schedules/ScheduleContainer.tsx +++ b/resources/scripts/components/server/schedules/ScheduleContainer.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from 'react'; -import { Helmet } from 'react-helmet'; import getServerSchedules from '@/api/server/schedules/getServerSchedules'; import { ServerContext } from '@/state/server'; import Spinner from '@/components/elements/Spinner'; @@ -9,15 +8,14 @@ import ScheduleRow from '@/components/server/schedules/ScheduleRow'; import { httpErrorToHuman } from '@/api/http'; import EditScheduleModal from '@/components/server/schedules/EditScheduleModal'; import Can from '@/components/elements/Can'; -import useServer from '@/plugins/useServer'; import useFlash from '@/plugins/useFlash'; -import PageContentBlock from '@/components/elements/PageContentBlock'; import tw from 'twin.macro'; import GreyRowBox from '@/components/elements/GreyRowBox'; import Button from '@/components/elements/Button'; +import ServerContentBlock from '@/components/elements/ServerContentBlock'; export default ({ match, history }: RouteComponentProps) => { - const { uuid, name: serverName } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const { clearFlashes, addError } = useFlash(); const [ loading, setLoading ] = useState(true); const [ visible, setVisible ] = useState(false); @@ -37,10 +35,7 @@ export default ({ match, history }: RouteComponentProps) => { }, []); return ( - - - {serverName} | Schedules - + {(!schedules.length && loading) ? @@ -77,6 +72,6 @@ export default ({ match, history }: RouteComponentProps) => { } - + ); }; diff --git a/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx b/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx index 6fcaca784..9573d307b 100644 --- a/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx +++ b/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx @@ -11,7 +11,6 @@ import EditScheduleModal from '@/components/server/schedules/EditScheduleModal'; import NewTaskButton from '@/components/server/schedules/NewTaskButton'; import DeleteScheduleButton from '@/components/server/schedules/DeleteScheduleButton'; import Can from '@/components/elements/Can'; -import useServer from '@/plugins/useServer'; import useFlash from '@/plugins/useFlash'; import { ServerContext } from '@/state/server'; import PageContentBlock from '@/components/elements/PageContentBlock'; @@ -28,7 +27,9 @@ interface State { } export default ({ match, history, location: { state } }: RouteComponentProps, State>) => { - const { id, uuid } = useServer(); + const id = ServerContext.useStoreState(state => state.server.data!.id); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); + const { clearFlashes, addError } = useFlash(); const [ isLoading, setIsLoading ] = useState(true); const [ showEditModal, setShowEditModal ] = useState(false); diff --git a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx index b14a24ea3..5f101415e 100644 --- a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx +++ b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx @@ -7,7 +7,6 @@ import { httpErrorToHuman } from '@/api/http'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import TaskDetailsModal from '@/components/server/schedules/TaskDetailsModal'; import Can from '@/components/elements/Can'; -import useServer from '@/plugins/useServer'; import useFlash from '@/plugins/useFlash'; import { ServerContext } from '@/state/server'; import tw from 'twin.macro'; @@ -32,7 +31,7 @@ const getActionDetails = (action: string): [ string, any ] => { }; export default ({ schedule, task }: Props) => { - const { uuid } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const { clearFlashes, addError } = useFlash(); const [ visible, setVisible ] = useState(false); const [ isLoading, setIsLoading ] = useState(false); diff --git a/resources/scripts/components/server/schedules/TaskDetailsModal.tsx b/resources/scripts/components/server/schedules/TaskDetailsModal.tsx index 00457a4ec..1ef64c138 100644 --- a/resources/scripts/components/server/schedules/TaskDetailsModal.tsx +++ b/resources/scripts/components/server/schedules/TaskDetailsModal.tsx @@ -9,7 +9,6 @@ import Field from '@/components/elements/Field'; import FlashMessageRender from '@/components/FlashMessageRender'; import { number, object, string } from 'yup'; import useFlash from '@/plugins/useFlash'; -import useServer from '@/plugins/useServer'; import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper'; import tw from 'twin.macro'; import Label from '@/components/elements/Label'; @@ -108,7 +107,7 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => { }; export default ({ task, schedule, onDismissed }: Props) => { - const { uuid } = useServer(); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const { clearFlashes, addError } = useFlash(); const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule); diff --git a/resources/scripts/plugins/useServer.ts b/resources/scripts/plugins/useServer.ts deleted file mode 100644 index 8014ced58..000000000 --- a/resources/scripts/plugins/useServer.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ServerContext } from '@/state/server'; -import { Server } from '@/api/server/getServer'; - -const useServer = (dependencies?: any[] | undefined): Server => { - return ServerContext.useStoreState(state => state.server.data!, dependencies); -}; - -export default useServer; From e863683582013448645cef4856c17055bfbdec0f Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 27 Aug 2020 19:26:52 -0700 Subject: [PATCH 48/54] Treat validation errors the same as a normal error; match the output exactly --- app/Exceptions/Handler.php | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index d278ce0bc..47e7be8ae 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -169,25 +169,14 @@ class Handler extends ExceptionHandler */ public function invalidJson($request, ValidationException $exception) { - $codes = collect($exception->validator->failed())->mapWithKeys(function ($reasons, $field) { - $cleaned = []; - foreach ($reasons as $reason => $attrs) { - $cleaned[] = snake_case($reason); - } - - return [str_replace('.', '_', $field) => $cleaned]; - })->toArray(); - - $errors = collect($exception->errors())->map(function ($errors, $field) use ($codes) { + $errors = collect($exception->errors())->map(function ($errors, $field) use ($exception) { $response = []; foreach ($errors as $key => $error) { - $response[] = [ - 'code' => str_replace(self::PTERODACTYL_RULE_STRING, 'p_', array_get( - $codes, str_replace('.', '_', $field) . '.' . $key - )), + $converted = self::convertToArray($exception)['errors'][0]; + $response[] = array_merge($converted, [ 'detail' => $error, - 'source' => ['field' => $field], - ]; + 'source_field' => $field, + ]); } return $response; @@ -209,7 +198,9 @@ class Handler extends ExceptionHandler { $error = [ 'code' => class_basename($exception), - 'status' => method_exists($exception, 'getStatusCode') ? strval($exception->getStatusCode()) : '500', + 'status' => method_exists($exception, 'getStatusCode') + ? strval($exception->getStatusCode()) + : ($exception instanceof ValidationException ? '422' : '500'), 'detail' => 'An error was encountered while processing this request.', ]; From 4b919cabd2cddd73662e2304f0c3ac7b95539e5c Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 27 Aug 2020 19:35:22 -0700 Subject: [PATCH 49/54] Correctly validation API calls to mark a backup as completed Also block modifying a backup that is already marked as completed via the endpoint --- .../Api/Remote/Backups/BackupStatusController.php | 15 ++++++++++++++- .../Api/Remote/ReportBackupCompleteRequest.php | 4 ++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php b/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php index e658a8012..57d1f3b4b 100644 --- a/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php +++ b/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php @@ -7,6 +7,8 @@ use Carbon\CarbonImmutable; use Illuminate\Http\JsonResponse; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Repositories\Eloquent\BackupRepository; +use Pterodactyl\Exceptions\Http\HttpForbiddenException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Pterodactyl\Http\Requests\Api\Remote\ReportBackupCompleteRequest; class BackupStatusController extends Controller @@ -32,10 +34,21 @@ class BackupStatusController extends Controller * @param \Pterodactyl\Http\Requests\Api\Remote\ReportBackupCompleteRequest $request * @param string $backup * @return \Illuminate\Http\JsonResponse + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function __invoke(ReportBackupCompleteRequest $request, string $backup) { - $this->repository->updateWhere([['uuid', '=', $backup]], [ + /** @var \Pterodactyl\Models\Backup $model */ + $model = $this->repository->findFirstWhere([[ 'uuid', '=', $backup ]]); + + if (!is_null($model->completed_at)) { + throw new BadRequestHttpException( + 'Cannot update the status of a backup that is already marked as completed.' + ); + } + + $model->update([ 'is_successful' => $request->input('successful') ? true : false, 'checksum' => $request->input('checksum_type') . ':' . $request->input('checksum'), 'bytes' => $request->input('size'), diff --git a/app/Http/Requests/Api/Remote/ReportBackupCompleteRequest.php b/app/Http/Requests/Api/Remote/ReportBackupCompleteRequest.php index 709961b71..a90a2b2b9 100644 --- a/app/Http/Requests/Api/Remote/ReportBackupCompleteRequest.php +++ b/app/Http/Requests/Api/Remote/ReportBackupCompleteRequest.php @@ -12,9 +12,9 @@ class ReportBackupCompleteRequest extends FormRequest public function rules() { return [ - 'successful' => 'boolean', + 'successful' => 'present|boolean', 'checksum' => 'nullable|string|required_if:successful,true', - 'checksum_type' => 'string|required_if:successful,true', + 'checksum_type' => 'nullable|string|required_if:successful,true', 'size' => 'nullable|numeric|required_if:successful,true', ]; } From 1967e3f7fdfae798da41c367193e9d0229636a62 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 27 Aug 2020 19:59:01 -0700 Subject: [PATCH 50/54] Better backup storage --- .../Api/Remote/Backups/BackupStatusController.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php b/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php index 57d1f3b4b..3f568882a 100644 --- a/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php +++ b/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php @@ -48,12 +48,13 @@ class BackupStatusController extends Controller ); } - $model->update([ - 'is_successful' => $request->input('successful') ? true : false, - 'checksum' => $request->input('checksum_type') . ':' . $request->input('checksum'), - 'bytes' => $request->input('size'), + $successful = $request->input('successful') ? true : false; + $model->forceFill([ + 'is_successful' => $successful, + 'checksum' => $successful ? ($request->input('checksum_type') . ':' . $request->input('checksum')) : null, + 'bytes' => $successful ? $request->input('size') : 0, 'completed_at' => CarbonImmutable::now(), - ]); + ])->save(); return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); } From f293c739772bc0ccb39c301fd7507b1d4cff4438 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 27 Aug 2020 21:15:23 -0700 Subject: [PATCH 51/54] Return the broken rule in the validation error response --- app/Exceptions/Handler.php | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 47e7be8ae..f63e5a37f 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -169,14 +169,30 @@ class Handler extends ExceptionHandler */ public function invalidJson($request, ValidationException $exception) { - $errors = collect($exception->errors())->map(function ($errors, $field) use ($exception) { + $codes = collect($exception->validator->failed())->mapWithKeys(function ($reasons, $field) { + $cleaned = []; + foreach ($reasons as $reason => $attrs) { + $cleaned[] = snake_case($reason); + } + + return [str_replace('.', '_', $field) => $cleaned]; + })->toArray(); + + $errors = collect($exception->errors())->map(function ($errors, $field) use ($codes, $exception) { $response = []; foreach ($errors as $key => $error) { - $converted = self::convertToArray($exception)['errors'][0]; - $response[] = array_merge($converted, [ - 'detail' => $error, + $meta = [ 'source_field' => $field, - ]); + 'rule' => str_replace(self::PTERODACTYL_RULE_STRING, 'p_', array_get( + $codes, str_replace('.', '_', $field) . '.' . $key + )), + ]; + + $converted = self::convertToArray($exception)['errors'][0]; + $converted['detail'] = $error; + $converted['meta'] = is_array($converted['meta']) ? array_merge($converted['meta'], $meta) : $meta; + + $response[] = $converted; } return $response; From 1e19e023983698d852afcef3ac7d6d032f9d501b Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 27 Aug 2020 21:19:01 -0700 Subject: [PATCH 52/54] Fix integration tests to properly account for the rule position in the API --- tests/Integration/Api/Client/AccountControllerTest.php | 6 +++--- tests/Integration/Api/Client/ApiKeyControllerTest.php | 2 +- .../Integration/Api/Client/Server/CommandControllerTest.php | 2 +- .../Api/Client/Server/NetworkAllocationControllerTest.php | 2 +- tests/Integration/Api/Client/Server/PowerControllerTest.php | 2 +- .../Api/Client/Server/Schedule/CreateServerScheduleTest.php | 2 +- .../Server/ScheduleTask/CreateServerScheduleTaskTest.php | 6 +++--- .../Api/Client/Server/SettingsControllerTest.php | 2 +- tests/Integration/Api/Client/TwoFactorControllerTest.php | 3 ++- 9 files changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/Integration/Api/Client/AccountControllerTest.php b/tests/Integration/Api/Client/AccountControllerTest.php index 4fbef8749..75b152090 100644 --- a/tests/Integration/Api/Client/AccountControllerTest.php +++ b/tests/Integration/Api/Client/AccountControllerTest.php @@ -85,7 +85,7 @@ class AccountControllerTest extends ClientApiIntegrationTestCase ]); $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); - $response->assertJsonPath('errors.0.code', 'required'); + $response->assertJsonPath('errors.0.meta.rule', 'required'); $response->assertJsonPath('errors.0.detail', 'The email field is required.'); $response = $this->actingAs($user)->putJson('/api/client/account/email', [ @@ -94,7 +94,7 @@ class AccountControllerTest extends ClientApiIntegrationTestCase ]); $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); - $response->assertJsonPath('errors.0.code', 'email'); + $response->assertJsonPath('errors.0.meta.rule', 'email'); $response->assertJsonPath('errors.0.detail', 'The email must be a valid email address.'); } @@ -156,7 +156,7 @@ class AccountControllerTest extends ClientApiIntegrationTestCase ]); $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); - $response->assertJsonPath('errors.0.code', 'confirmed'); + $response->assertJsonPath('errors.0.meta.rule', 'confirmed'); $response->assertJsonPath('errors.0.detail', 'The password confirmation does not match.'); } } diff --git a/tests/Integration/Api/Client/ApiKeyControllerTest.php b/tests/Integration/Api/Client/ApiKeyControllerTest.php index 13a0b9c84..833562392 100644 --- a/tests/Integration/Api/Client/ApiKeyControllerTest.php +++ b/tests/Integration/Api/Client/ApiKeyControllerTest.php @@ -133,7 +133,7 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase ]); $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); - $response->assertJsonPath('errors.0.code', 'required'); + $response->assertJsonPath('errors.0.meta.rule', 'required'); $response->assertJsonPath('errors.0.detail', 'The description field is required.'); } diff --git a/tests/Integration/Api/Client/Server/CommandControllerTest.php b/tests/Integration/Api/Client/Server/CommandControllerTest.php index de3dacc85..f3e9df7b2 100644 --- a/tests/Integration/Api/Client/Server/CommandControllerTest.php +++ b/tests/Integration/Api/Client/Server/CommandControllerTest.php @@ -41,7 +41,7 @@ class CommandControllerTest extends ClientApiIntegrationTestCase ]); $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); - $response->assertJsonPath('errors.0.code', 'required'); + $response->assertJsonPath('errors.0.meta.rule', 'required'); } /** diff --git a/tests/Integration/Api/Client/Server/NetworkAllocationControllerTest.php b/tests/Integration/Api/Client/Server/NetworkAllocationControllerTest.php index b019b3e62..ffebc83b8 100644 --- a/tests/Integration/Api/Client/Server/NetworkAllocationControllerTest.php +++ b/tests/Integration/Api/Client/Server/NetworkAllocationControllerTest.php @@ -63,7 +63,7 @@ class NetworkAllocationControllerTest extends ClientApiIntegrationTestCase $this->actingAs($user)->postJson($this->link($allocation), []) ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) - ->assertJsonPath('errors.0.code', 'present'); + ->assertJsonPath('errors.0.meta.rule', 'present'); $this->actingAs($user)->postJson($this->link($allocation), ['notes' => 'Test notes']) ->assertOk() diff --git a/tests/Integration/Api/Client/Server/PowerControllerTest.php b/tests/Integration/Api/Client/Server/PowerControllerTest.php index 80f58010a..96911cf37 100644 --- a/tests/Integration/Api/Client/Server/PowerControllerTest.php +++ b/tests/Integration/Api/Client/Server/PowerControllerTest.php @@ -40,7 +40,7 @@ class PowerControllerTest extends ClientApiIntegrationTestCase ]); $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); - $response->assertJsonPath('errors.0.code', 'in'); + $response->assertJsonPath('errors.0.meta.rule', 'in'); $response->assertJsonPath('errors.0.detail', 'The selected signal is invalid.'); } diff --git a/tests/Integration/Api/Client/Server/Schedule/CreateServerScheduleTest.php b/tests/Integration/Api/Client/Server/Schedule/CreateServerScheduleTest.php index 5b45e7fe7..78f6253ed 100644 --- a/tests/Integration/Api/Client/Server/Schedule/CreateServerScheduleTest.php +++ b/tests/Integration/Api/Client/Server/Schedule/CreateServerScheduleTest.php @@ -71,7 +71,7 @@ class CreateServerScheduleTest extends ClientApiIntegrationTestCase 'day_of_week' => '*', ]) ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) - ->assertJsonPath('errors.0.code', 'boolean'); + ->assertJsonPath('errors.0.meta.rule', 'boolean'); } /** diff --git a/tests/Integration/Api/Client/Server/ScheduleTask/CreateServerScheduleTaskTest.php b/tests/Integration/Api/Client/Server/ScheduleTask/CreateServerScheduleTaskTest.php index 2044221e5..8dba137b5 100644 --- a/tests/Integration/Api/Client/Server/ScheduleTask/CreateServerScheduleTaskTest.php +++ b/tests/Integration/Api/Client/Server/ScheduleTask/CreateServerScheduleTaskTest.php @@ -66,7 +66,7 @@ class CreateServerScheduleTaskTest extends ClientApiIntegrationTestCase 'time_offset' => 0, ]) ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) - ->assertJsonPath('errors.0.code', 'in') + ->assertJsonPath('errors.0.meta.rule', 'in') ->assertJsonPath('errors.0.source.field', 'action'); $this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [ @@ -74,7 +74,7 @@ class CreateServerScheduleTaskTest extends ClientApiIntegrationTestCase 'time_offset' => 0, ]) ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) - ->assertJsonPath('errors.0.code', 'required_unless') + ->assertJsonPath('errors.0.meta.rule', 'required_unless') ->assertJsonPath('errors.0.source.field', 'payload'); $this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [ @@ -84,7 +84,7 @@ class CreateServerScheduleTaskTest extends ClientApiIntegrationTestCase 'sequence_id' => 'hodor', ]) ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) - ->assertJsonPath('errors.0.code', 'numeric') + ->assertJsonPath('errors.0.meta.rule', 'numeric') ->assertJsonPath('errors.0.source.field', 'sequence_id'); } diff --git a/tests/Integration/Api/Client/Server/SettingsControllerTest.php b/tests/Integration/Api/Client/Server/SettingsControllerTest.php index 99c32a56a..44a850a7c 100644 --- a/tests/Integration/Api/Client/Server/SettingsControllerTest.php +++ b/tests/Integration/Api/Client/Server/SettingsControllerTest.php @@ -28,7 +28,7 @@ class SettingsControllerTest extends ClientApiIntegrationTestCase ]); $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); - $response->assertJsonPath('errors.0.code', 'required'); + $response->assertJsonPath('errors.0.meta.rule', 'required'); $server = $server->refresh(); $this->assertSame($originalName, $server->name); diff --git a/tests/Integration/Api/Client/TwoFactorControllerTest.php b/tests/Integration/Api/Client/TwoFactorControllerTest.php index 189a94fa1..1c9b467f6 100644 --- a/tests/Integration/Api/Client/TwoFactorControllerTest.php +++ b/tests/Integration/Api/Client/TwoFactorControllerTest.php @@ -64,7 +64,8 @@ class TwoFactorControllerTest extends ClientApiIntegrationTestCase ]); $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); - $response->assertJsonPath('errors.0.code', 'required'); + $response->assertJsonPath('errors.0.code', 'ValidationException'); + $response->assertJsonPath('errors.0.meta.rule', 'required'); } /** From 90ba76c237ca9a91e30b3333129ccfb590443cc0 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 27 Aug 2020 21:22:05 -0700 Subject: [PATCH 53/54] Return integration tests to passing state --- .../Server/Schedule/CreateServerScheduleTest.php | 5 +++-- .../ScheduleTask/CreateServerScheduleTaskTest.php | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/Integration/Api/Client/Server/Schedule/CreateServerScheduleTest.php b/tests/Integration/Api/Client/Server/Schedule/CreateServerScheduleTest.php index 78f6253ed..51345fa79 100644 --- a/tests/Integration/Api/Client/Server/Schedule/CreateServerScheduleTest.php +++ b/tests/Integration/Api/Client/Server/Schedule/CreateServerScheduleTest.php @@ -57,8 +57,9 @@ class CreateServerScheduleTest extends ClientApiIntegrationTestCase $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); foreach (['name', 'minute', 'hour', 'day_of_month', 'day_of_week'] as $i => $field) { - $response->assertJsonPath("errors.{$i}.code", 'required'); - $response->assertJsonPath("errors.{$i}.source.field", $field); + $response->assertJsonPath("errors.{$i}.code", 'ValidationException'); + $response->assertJsonPath("errors.{$i}.meta.rule", 'required'); + $response->assertJsonPath("errors.{$i}.meta.source_field", $field); } $this->actingAs($user) diff --git a/tests/Integration/Api/Client/Server/ScheduleTask/CreateServerScheduleTaskTest.php b/tests/Integration/Api/Client/Server/ScheduleTask/CreateServerScheduleTaskTest.php index 8dba137b5..9f263acd7 100644 --- a/tests/Integration/Api/Client/Server/ScheduleTask/CreateServerScheduleTaskTest.php +++ b/tests/Integration/Api/Client/Server/ScheduleTask/CreateServerScheduleTaskTest.php @@ -56,8 +56,8 @@ class CreateServerScheduleTaskTest extends ClientApiIntegrationTestCase $response = $this->actingAs($user)->postJson($this->link($schedule, '/tasks'))->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); foreach (['action', 'payload', 'time_offset'] as $i => $field) { - $response->assertJsonPath("errors.{$i}.code", $field === 'payload' ? 'required_unless' : 'required'); - $response->assertJsonPath("errors.{$i}.source.field", $field); + $response->assertJsonPath("errors.{$i}.meta.rule", $field === 'payload' ? 'required_unless' : 'required'); + $response->assertJsonPath("errors.{$i}.meta.source_field", $field); } $this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [ @@ -67,7 +67,7 @@ class CreateServerScheduleTaskTest extends ClientApiIntegrationTestCase ]) ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) ->assertJsonPath('errors.0.meta.rule', 'in') - ->assertJsonPath('errors.0.source.field', 'action'); + ->assertJsonPath('errors.0.meta.source_field', 'action'); $this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [ 'action' => 'command', @@ -75,7 +75,7 @@ class CreateServerScheduleTaskTest extends ClientApiIntegrationTestCase ]) ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) ->assertJsonPath('errors.0.meta.rule', 'required_unless') - ->assertJsonPath('errors.0.source.field', 'payload'); + ->assertJsonPath('errors.0.meta.source_field', 'payload'); $this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [ 'action' => 'command', @@ -85,7 +85,7 @@ class CreateServerScheduleTaskTest extends ClientApiIntegrationTestCase ]) ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) ->assertJsonPath('errors.0.meta.rule', 'numeric') - ->assertJsonPath('errors.0.source.field', 'sequence_id'); + ->assertJsonPath('errors.0.meta.source_field', 'sequence_id'); } /** From d73590912499252e29c16d15237b73a7a46eb274 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 28 Aug 2020 08:23:44 -0700 Subject: [PATCH 54/54] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 84d428ee0..3fab319b8 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,9 @@ in becoming a sponsor?](https://github.com/sponsors/DaneEveritt) > Skynode provides blazing fast game servers along with a top notch user experience. Whatever our clients are looking > for, we're able to provide it! +#### [XCORE-SERVER.de](https://xcore-server.de) +> XCORE-SERVER.de offers High-End Servers for hosting and gaming since 2012. Fast, excellent and well known for eSports Gaming. + ## Support & Documentation Support for using Pterodactyl can be found on our [Documentation Website](https://pterodactyl.io/project/introduction.html), [Guides Website](https://pterodactyl.io/community/about.html), or via our [Discord Chat](https://discord.gg/QRDZvVm).