From 78ed343a345be92b36de11db87621ccb9d840b0e Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 18 Mar 2020 21:08:32 -0700 Subject: [PATCH] Support deleting a task from a schedule --- .../Http/HttpForbiddenException.php | 20 +++++++ .../Client/Servers/ScheduleTaskController.php | 59 +++++++++++++++++++ .../Requests/Api/Client/ClientApiRequest.php | 4 +- app/Models/Permission.php | 8 +++ .../server/schedules/deleteScheduleTask.ts | 9 +++ .../components/elements/SpinnerOverlay.tsx | 4 +- .../schedules/ConfirmTaskDeletionModal.tsx | 26 ++++++++ .../schedules/ScheduleEditContainer.tsx | 9 ++- .../server/schedules/ScheduleTaskRow.tsx | 40 ++++++++++++- routes/api-client.php | 1 + 10 files changed, 172 insertions(+), 8 deletions(-) create mode 100644 app/Exceptions/Http/HttpForbiddenException.php create mode 100644 app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php create mode 100644 resources/scripts/api/server/schedules/deleteScheduleTask.ts create mode 100644 resources/scripts/components/server/schedules/ConfirmTaskDeletionModal.tsx diff --git a/app/Exceptions/Http/HttpForbiddenException.php b/app/Exceptions/Http/HttpForbiddenException.php new file mode 100644 index 000000000..fa2aae9be --- /dev/null +++ b/app/Exceptions/Http/HttpForbiddenException.php @@ -0,0 +1,20 @@ +repository = $repository; + } + + /** + * Determines if a user can delete the task for a given server. + * + * @param \Pterodactyl\Http\Requests\Api\Client\ClientApiRequest $request + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Schedule $schedule + * @param \Pterodactyl\Models\Task $task + * @return \Illuminate\Http\JsonResponse + */ + public function delete(ClientApiRequest $request, Server $server, Schedule $schedule, Task $task) + { + if ($task->schedule_id !== $schedule->id || $schedule->server_id !== $server->id) { + throw new NotFoundHttpException; + } + + if (! $request->user()->can(Permission::ACTION_SCHEDULE_UPDATE, $server)) { + throw new HttpForbiddenException('You do not have permission to perform this action.'); + } + + $this->repository->delete($task->id); + + return JsonResponse::create(null, Response::HTTP_NO_CONTENT); + } +} diff --git a/app/Http/Requests/Api/Client/ClientApiRequest.php b/app/Http/Requests/Api/Client/ClientApiRequest.php index 85ea452ad..ea9688bc9 100644 --- a/app/Http/Requests/Api/Client/ClientApiRequest.php +++ b/app/Http/Requests/Api/Client/ClientApiRequest.php @@ -8,9 +8,9 @@ use Pterodactyl\Contracts\Http\ClientPermissionsRequest; use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest; /** - * @method User user($guard = null) + * @method \Pterodactyl\Models\User user($guard = null) */ -abstract class ClientApiRequest extends ApplicationApiRequest +class ClientApiRequest extends ApplicationApiRequest { /** * Determine if the current user is authorized to perform the requested action against the API. diff --git a/app/Models/Permission.php b/app/Models/Permission.php index 06e52fa16..14c36b788 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -12,6 +12,14 @@ class Permission extends Validable */ const RESOURCE_NAME = 'subuser_permission'; + /** + * Constants defining different permissions available. + */ + const ACTION_SCHEDULE_READ = 'schedule.read'; + const ACTION_SCHEDULE_CREATE = 'schedule.create'; + const ACTION_SCHEDULE_UPDATE = 'schedule.update'; + const ACTION_SCHEDULE_DELETE = 'schedule.delete'; + /** * Should timestamps be used on this model. * diff --git a/resources/scripts/api/server/schedules/deleteScheduleTask.ts b/resources/scripts/api/server/schedules/deleteScheduleTask.ts new file mode 100644 index 000000000..4b5a33296 --- /dev/null +++ b/resources/scripts/api/server/schedules/deleteScheduleTask.ts @@ -0,0 +1,9 @@ +import http from '@/api/http'; + +export default (uuid: string, scheduleId: number, taskId: number): Promise => { + return new Promise((resolve, reject) => { + http.delete(`/api/client/servers/${uuid}/schedules/${scheduleId}/tasks/${taskId}`) + .then(() => resolve()) + .catch(reject); + }) +}; diff --git a/resources/scripts/components/elements/SpinnerOverlay.tsx b/resources/scripts/components/elements/SpinnerOverlay.tsx index 7b58bdc06..87f66bc66 100644 --- a/resources/scripts/components/elements/SpinnerOverlay.tsx +++ b/resources/scripts/components/elements/SpinnerOverlay.tsx @@ -13,11 +13,11 @@ interface Props { const SpinnerOverlay = ({ size, fixed, visible, backgroundOpacity }: Props) => (
diff --git a/resources/scripts/components/server/schedules/ConfirmTaskDeletionModal.tsx b/resources/scripts/components/server/schedules/ConfirmTaskDeletionModal.tsx new file mode 100644 index 000000000..abce80cde --- /dev/null +++ b/resources/scripts/components/server/schedules/ConfirmTaskDeletionModal.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import Modal, { RequiredModalProps } from '@/components/elements/Modal'; + +type Props = RequiredModalProps & { + onConfirmed: () => void; +} + +export default ({ onConfirmed, ...props }: Props) => ( + +

Confirm task deletion

+

+ Are you sure you want to delete this task? This action cannot be undone. +

+
+ + +
+
+); diff --git a/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx b/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx index 77a38f027..caa742217 100644 --- a/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx +++ b/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx @@ -79,7 +79,14 @@ export default ({ match, location: { state } }: RouteComponentProps - + setSchedule(s => ({ + ...s!, + tasks: s!.tasks.filter(t => t.id !== task.id), + }))} + /> )) } diff --git a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx index b7d4e5d33..9c4cf5cca 100644 --- a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx +++ b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx @@ -1,17 +1,49 @@ -import React from 'react'; -import { Task } from '@/api/server/schedules/getServerSchedules'; +import React, { useState } from 'react'; +import { Schedule, Task } from '@/api/server/schedules/getServerSchedules'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt'; import { faCode } from '@fortawesome/free-solid-svg-icons/faCode'; import { faToggleOn } from '@fortawesome/free-solid-svg-icons/faToggleOn'; +import ConfirmTaskDeletionModal from '@/components/server/schedules/ConfirmTaskDeletionModal'; +import { ServerContext } from '@/state/server'; +import { Actions, useStoreActions } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; +import deleteScheduleTask from '@/api/server/schedules/deleteScheduleTask'; +import { httpErrorToHuman } from '@/api/http'; +import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; interface Props { + schedule: number; task: Task; + onTaskRemoved: () => void; } -export default ({ task }: Props) => { +export default ({ schedule, task, onTaskRemoved }: Props) => { + const [visible, setVisible] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); + const { clearFlashes, addError } = useStoreActions((actions: Actions) => actions.flashes); + + const onConfirmDeletion = () => { + setIsLoading(true); + clearFlashes('schedules'); + deleteScheduleTask(uuid, schedule, task.id) + .then(() => onTaskRemoved()) + .catch(error => { + console.error(error); + setIsLoading(false); + addError({ message: httpErrorToHuman(error), key: 'schedules' }); + }); + }; + return (
+ + setVisible(false)} + onConfirmed={() => onConfirmDeletion()} + />

@@ -34,7 +66,9 @@ export default ({ task }: Props) => {

setVisible(true)} > diff --git a/routes/api-client.php b/routes/api-client.php index 54937a876..545798ff5 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -62,6 +62,7 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ Route::group(['prefix' => '/schedules'], function () { Route::get('/', 'Servers\ScheduleController@index'); Route::get('/{schedule}', 'Servers\ScheduleController@view'); + Route::delete('/{schedule}/tasks/{task}', 'Servers\ScheduleTaskController@delete'); }); Route::group(['prefix' => '/network'], function () {