From 51c5cf4dbb89323e37a3db1411f61744a9ad8541 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 25 Mar 2020 21:58:37 -0700 Subject: [PATCH] Get basic modal view for editing/creating a new subuser working --- .../Api/Client/ClientController.php | 9 +- app/Models/Permission.php | 131 ++++++++--------- resources/scripts/api/getSystemPermissions.ts | 4 +- .../dashboard/AccountApiContainer.tsx | 2 +- .../components/elements/ContentBox.tsx | 5 +- .../components/elements/TitledGreyBox.tsx | 12 +- .../server/schedules/ScheduleTaskRow.tsx | 4 +- .../server/users/AddSubuserButton.tsx | 21 +++ .../server/users/EditSubuserModal.tsx | 139 ++++++++++++++++++ .../server/users/UsersContainer.tsx | 124 +++++----------- resources/scripts/routers/ServerRouter.tsx | 5 +- resources/scripts/state/permissions.ts | 14 +- resources/styles/components/modal.css | 9 +- 13 files changed, 293 insertions(+), 186 deletions(-) create mode 100644 resources/scripts/components/server/users/AddSubuserButton.tsx create mode 100644 resources/scripts/components/server/users/EditSubuserModal.tsx diff --git a/app/Http/Controllers/Api/Client/ClientController.php b/app/Http/Controllers/Api/Client/ClientController.php index f11d1ac1..b673ac5b 100644 --- a/app/Http/Controllers/Api/Client/ClientController.php +++ b/app/Http/Controllers/Api/Client/ClientController.php @@ -3,7 +3,6 @@ namespace Pterodactyl\Http\Controllers\Api\Client; use Pterodactyl\Models\User; -use Illuminate\Support\Collection; use Pterodactyl\Models\Permission; use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Transformers\Api\Client\ServerTransformer; @@ -72,16 +71,10 @@ class ClientController extends ClientApiController */ public function permissions() { - $permissions = Permission::permissions()->map(function ($values, $key) { - return Collection::make($values)->map(function ($permission) use ($key) { - return $key . '.' . $permission; - })->values()->toArray(); - })->flatten(); - return [ 'object' => 'system_permissions', 'attributes' => [ - 'permissions' => $permissions, + 'permissions' => Permission::permissions(), ], ]; } diff --git a/app/Models/Permission.php b/app/Models/Permission.php index 11db3454..9a834ff7 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -98,105 +98,90 @@ class Permission extends Validable */ protected static $permissions = [ 'websocket' => [ - // Allows the user to connect to the server websocket, this will give them - // access to view the console output as well as realtime server stats (CPU - // and Memory usage). - '*', + 'description' => 'Allows the user to connect to the server websocket, giving them access to view console output and realtime server stats.', + 'keys' => [ + '*' => 'Gives user full read access to the websocket.', + ], ], 'control' => [ - // Allows the user to send data to the server console process. A user with this - // permission will not be able to stop the server directly by issuing the specified - // stop command for the Egg, however depending on plugins and server configuration - // they may still be able to control the server power state. - 'console', // power.send-command - - // Allows the user to start/stop/restart/kill the server process. - 'start', // power.power-start - 'stop', // power.power-stop - 'restart', // power.power-restart - 'kill', // power.power-kill + 'description' => 'Permissions that control a user\'s ability to control the power state of a server, or send commands.', + 'keys' => [ + 'console' => 'Allows a user to send commands to the server instance via the console.', + 'start' => 'Allows a user to start the server if it is stopped.', + 'stop' => 'Allows a user to stop a server if it is running.', + 'restart' => 'Allows a user to perform a server restart. This allows them to start the server if it is offline, but not put the server in a completely stopped state.', + 'kill' => 'Allows a user to terminate a server process.', + ], ], 'user' => [ - // Allows a user to create a new user assigned to the server. They will not be able - // to assign any permissions they do not already have on their account as well. - 'create', // subuser.create-subuser - 'read', // subuser.list-subusers, subuser.view-subuser - 'update', // subuser.edit-subuser - 'delete', // subuser.delete-subuser + 'description' => 'Permissions that allow a user to manage other subusers on a server. They will never be able to edit their own account, or assign permissions they do not have themselves.', + 'keys' => [ + 'create' => 'Allows a user to create new subusers for the server.', + 'read' => 'Allows the user to view subusers and their permissions for the server.', + 'update' => 'Allows a user to modify other subusers.', + 'delete' => 'Allows a user to delete a subuser from the server.', + ], ], 'file' => [ - // Allows a user to create additional files and folders either via the Panel, - // or via a direct upload. - 'create', // files.create-files, files.upload-files, files.copy-files, files.move-files - - // Allows a user to view the contents of a directory as well as read the contents - // of a given file. A user with this permission will be able to download files - // as well. - 'read', // files.list-files, files.download-files - - // Allows a user to update the contents of an existing file or directory. - 'update', // files.edit-files, files.save-files - - // Allows a user to delete a file or directory. - 'delete', // files.delete-files - - // Allows a user to archive the contents of a directory as well as decompress existing - // archives on the system. - 'archive', // files.compress-files, files.decompress-files - - // Allows the user to connect and manage server files using their account - // credentials and a SFTP client. - 'sftp', // files.access-sftp + 'description' => 'Permissions that control a user\'s ability to modify the filesystem for this server.', + 'keys' => [ + 'create' => 'Allows a user to create additional files and folders via the Panel or direct upload.', + 'read' => 'Allows a user to view the contents of a directory and read the contents of a file. Users with this permission can also download files.', + 'update' => 'Allows a user to update the contents of an existing file or directory.', + 'delete' => 'Allows a user to delete files or directories.', + 'archive' => 'Allows a user to archive the contents of a directory as well as decompress existing archives on the system.', + 'sftp' => 'Allows a user to connect to SFTP and manage server files using the other assigned file permissions.', + ], ], // Controls permissions for editing or viewing a server's allocations. 'allocation' => [ - 'read', // server.view-allocations - 'update', // server.edit-allocation + 'description' => 'Permissions that control a user\'s ability to modify the port allocations for this server.', + 'keys' => [ + 'read' => 'Allows a user to view the allocations assigned to this server.', + 'update' => 'Allows a user to modify the allocations assigned to this server.', + ], ], // Controls permissions for editing or viewing a server's startup parameters. 'startup' => [ - 'read', // server.view-startup - 'update', // server.edit-startup + 'description' => 'Permissions that control a user\'s ability to view this server\'s startup parameters.', + 'keys' => [ + 'read' => '', + 'update' => '', + ], ], 'database' => [ - // Allows a user to create a new database for a server. - 'create', // database.create-database - - // Allows a user to view the databases associated with the server. If they do not also - // have the view_password permission they will only be able to see the connection address - // and the name of the user. - 'read', // database.view-databases - - // Allows a user to rotate the password on a database instance. If the user does not - // alow have the view_password permission they will not be able to see the updated password - // anywhere, but it will still be rotated. - 'update', // database.reset-db-password - - // Allows a user to delete a database instance. - 'delete', // database.delete-database - - // Allows a user to view the password associated with a database instance for the - // server. Note that a user without this permission may still be able to access these - // credentials by viewing files or the console. - 'view_password', // database.reset-db-password + 'description' => 'Permissions that control a user\'s access to the database management for this server.', + 'keys' => [ + 'create' => 'Allows a user to create a new database for this server.', + 'read' => 'Allows a user to view the database associated with this server.', + 'update' => 'Allows a user to rotate the password on a database instance. If the user does not have the view_password permission they will not see the updated password.', + 'delete' => 'Allows a user to remove a database instance from this server.', + 'view_password' => 'Allows a user to view the password associated with a database instance for this server.', + ], ], 'schedule' => [ - 'create', // task.create-schedule - 'read', // task.view-schedule, task.list-schedules - 'update', // task.edit-schedule, task.queue-schedule, task.toggle-schedule - 'delete', // task.delete-schedule + 'description' => 'Permissions that control a user\'s access to the schedule management for this server.', + 'keys' => [ + 'create' => '', // task.create-schedule + 'read' => '', // task.view-schedule, task.list-schedules + 'update' => '', // task.edit-schedule, task.queue-schedule, task.toggle-schedule + 'delete' => '', // task.delete-schedule + ], ], 'settings' => [ - 'rename', - 'reinstall', + 'description' => 'Permissions that control a user\'s access to the settings for this server.', + 'keys' => [ + 'rename' => '', + 'reinstall' => '', + ], ], ]; diff --git a/resources/scripts/api/getSystemPermissions.ts b/resources/scripts/api/getSystemPermissions.ts index 47016b3c..69fb5679 100644 --- a/resources/scripts/api/getSystemPermissions.ts +++ b/resources/scripts/api/getSystemPermissions.ts @@ -1,7 +1,7 @@ -import { SubuserPermission } from '@/state/server/subusers'; +import { PanelPermissions } from '@/state/permissions'; import http from '@/api/http'; -export default (): Promise => { +export default (): Promise => { return new Promise((resolve, reject) => { http.get(`/api/client/permissions`) .then(({ data }) => resolve(data.attributes.permissions)) diff --git a/resources/scripts/components/dashboard/AccountApiContainer.tsx b/resources/scripts/components/dashboard/AccountApiContainer.tsx index 7fbe2162..b6503328 100644 --- a/resources/scripts/components/dashboard/AccountApiContainer.tsx +++ b/resources/scripts/components/dashboard/AccountApiContainer.tsx @@ -95,7 +95,7 @@ export default () => { > diff --git a/resources/scripts/components/elements/ContentBox.tsx b/resources/scripts/components/elements/ContentBox.tsx index b03503e6..94c9ad62 100644 --- a/resources/scripts/components/elements/ContentBox.tsx +++ b/resources/scripts/components/elements/ContentBox.tsx @@ -1,14 +1,16 @@ import * as React from 'react'; import classNames from 'classnames'; import FlashMessageRender from '@/components/FlashMessageRender'; +import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; type Props = Readonly, HTMLDivElement> & { title?: string; borderColor?: string; showFlashes?: string | boolean; + showLoadingOverlay?: boolean; }>; -const ContentBox = ({ title, borderColor, showFlashes, children, ...props }: Props) => ( +const ContentBox = ({ title, borderColor, showFlashes, showLoadingOverlay, children, ...props }: Props) => (
{title &&

{title}

} {showFlashes && @@ -20,6 +22,7 @@ const ContentBox = ({ title, borderColor, showFlashes, children, ...props }: Pro
+ {children}
diff --git a/resources/scripts/components/elements/TitledGreyBox.tsx b/resources/scripts/components/elements/TitledGreyBox.tsx index 6116feb2..7e5bb161 100644 --- a/resources/scripts/components/elements/TitledGreyBox.tsx +++ b/resources/scripts/components/elements/TitledGreyBox.tsx @@ -4,7 +4,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; interface Props { icon?: IconProp; - title: string; + title: string | React.ReactNode; className?: string; children: React.ReactNode; } @@ -12,9 +12,13 @@ interface Props { const TitledGreyBox = ({ icon, title, children, className }: Props) => (
-

- {icon && }{title} -

+ {typeof title === 'string' ? +

+ {icon && }{title} +

+ : + title + }
{children} diff --git a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx index 773308e2..12e675eb 100644 --- a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx +++ b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx @@ -78,7 +78,7 @@ export default ({ schedule, task, onTaskUpdated, onTaskRemoved }: Props) => { + + ); +}; diff --git a/resources/scripts/components/server/users/EditSubuserModal.tsx b/resources/scripts/components/server/users/EditSubuserModal.tsx new file mode 100644 index 00000000..65ff2aaa --- /dev/null +++ b/resources/scripts/components/server/users/EditSubuserModal.tsx @@ -0,0 +1,139 @@ +import React from 'react'; +import { Subuser } from '@/state/server/subusers'; +import { Formik, FormikHelpers, useFormikContext } from 'formik'; +import { array, object, string } from 'yup'; +import Modal, { RequiredModalProps } from '@/components/elements/Modal'; +import Field from '@/components/elements/Field'; +import { useStoreState } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; +import TitledGreyBox from '@/components/elements/TitledGreyBox'; +import Checkbox from '@/components/elements/Checkbox'; +import styled from 'styled-components'; +import classNames from 'classnames'; + +type Props = { + subuser?: Subuser; +} & RequiredModalProps; + +interface Values { + email: string; + permissions: string[]; +} + +const PermissionLabel = styled.label` + ${tw`flex items-center border border-transparent rounded p-2 cursor-pointer`}; + text-transform: none; + + &:hover { + ${tw`border-neutral-500 bg-neutral-800`}; + } +`; + +const EditSubuserModal = ({ subuser, ...props }: Props) => { + const { values, isSubmitting, setFieldValue } = useFormikContext(); + const permissions = useStoreState((state: ApplicationStore) => state.permissions.data); + + return ( + +

{subuser ? 'Edit subuser' : 'Create new subuser'}

+
+ +
+
+ {Object.keys(permissions).filter(key => key !== 'websocket').map((key, index) => ( + +

{key}

+ { + if (e.currentTarget.checked) { + setFieldValue('permissions', [ + ...values.permissions, + ...Object.keys(permissions[key].keys) + .map(pkey => `${key}.${pkey}`) + .filter(permission => values.permissions.indexOf(permission) === -1), + ]); + } else { + setFieldValue('permissions', [ + ...values.permissions.filter( + permission => Object.keys(permissions[key].keys) + .map(pkey => `${key}.${pkey}`) + .indexOf(permission) < 0, + ), + ]); + } + }} + /> +
+ } + className={index !== 0 ? 'mt-4' : undefined} + > +

+ {permissions[key].description} +

+ {Object.keys(permissions[key].keys).map((pkey, index) => ( + +
+ +
+
+ + {pkey} + + {permissions[key].keys[pkey].length > 0 && +

+ {permissions[key].keys[pkey]} +

+ } +
+
+ ))} + + ))} +
+
+ +
+ + ); +}; + +export default (props: Props) => { + const submit = (values: Values, helpers: FormikHelpers) => { + }; + + return ( + + + + ); +}; diff --git a/resources/scripts/components/server/users/UsersContainer.tsx b/resources/scripts/components/server/users/UsersContainer.tsx index 01e42f74..ff685ec0 100644 --- a/resources/scripts/components/server/users/UsersContainer.tsx +++ b/resources/scripts/components/server/users/UsersContainer.tsx @@ -1,19 +1,12 @@ import React, { useEffect, useState } from 'react'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faUserPlus } from '@fortawesome/free-solid-svg-icons/faUserPlus'; import { ServerContext } from '@/state/server'; -import Spinner from '@/components/elements/Spinner'; -import { Subuser } from '@/state/server/subusers'; -import { CSSTransition } from 'react-transition-group'; -import classNames from 'classnames'; -import PermissionEditor from '@/components/server/users/PermissionEditor'; import { Actions, useStoreActions, useStoreState } from 'easy-peasy'; import { ApplicationStore } from '@/state'; -import { faArrowLeft } from '@fortawesome/free-solid-svg-icons/faArrowLeft'; +import Spinner from '@/components/elements/Spinner'; +import AddSubuserButton from '@/components/server/users/AddSubuserButton'; export default () => { const [ loading, setLoading ] = useState(true); - const [ editSubuser, setEditSubuser ] = useState(null); const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const subusers = ServerContext.useStoreState(state => state.subusers.data); @@ -23,10 +16,8 @@ export default () => { const getPermissions = useStoreActions((actions: Actions) => actions.permissions.getPermissions); useEffect(() => { - if (!permissions.length) { - getPermissions().catch(error => console.error(error)); - } - }, [ permissions, getPermissions ]); + getPermissions().catch(error => console.error(error)); + }, []); useEffect(() => { getSubusers(uuid) @@ -37,84 +28,47 @@ export default () => { }, [ uuid, getSubusers ]); useEffect(() => { - if (subusers.length > 0) { - setLoading(false); - } + setLoading(!subusers); }, [ subusers ]); + if (loading || !Object.keys(permissions).length) { + return ; + } + return ( -
-
-

Subusers

-
- {(loading || !permissions.length) ? -
- +
+ {!subusers.length ? +

+ It looks like you don't have any subusers. +

+ : + subusers.map(subuser => ( +
+ +
+

{subuser.email}

+
+
+ +
- : - !subusers.length ? -

It looks like you don't have any subusers.

- : - subusers.map(subuser => ( -
- -
-

{subuser.email}

-
-
- - -
-
- )) - } -
-
- -
-
- {editSubuser && - -
-

- setEditSubuser(null)}> - - - Edit {editSubuser.email} -

-
- - -
-
-
+ )) } +
+ +
); }; diff --git a/resources/scripts/routers/ServerRouter.tsx b/resources/scripts/routers/ServerRouter.tsx index 2998c6ee..8735c140 100644 --- a/resources/scripts/routers/ServerRouter.tsx +++ b/resources/scripts/routers/ServerRouter.tsx @@ -14,6 +14,7 @@ import FileEditContainer from '@/components/server/files/FileEditContainer'; import SettingsContainer from '@/components/server/settings/SettingsContainer'; import ScheduleContainer from '@/components/server/schedules/ScheduleContainer'; import ScheduleEditContainer from '@/components/server/schedules/ScheduleEditContainer'; +import UsersContainer from '@/components/server/users/UsersContainer'; const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => { const server = ServerContext.useStoreState(state => state.server.data); @@ -35,8 +36,8 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) Console File Manager Databases - {/* User Management */} Schedules + Users Settings
@@ -62,9 +63,9 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) exact /> - {/* */} + diff --git a/resources/scripts/state/permissions.ts b/resources/scripts/state/permissions.ts index e275e38f..07a42800 100644 --- a/resources/scripts/state/permissions.ts +++ b/resources/scripts/state/permissions.ts @@ -1,15 +1,21 @@ -import { SubuserPermission } from '@/state/server/subusers'; import { action, Action, thunk, Thunk } from 'easy-peasy'; import getSystemPermissions from '@/api/getSystemPermissions'; +export interface PanelPermissions { + [key: string]: { + description: string; + keys: { [k: string]: string }; + }; +} + export interface GloablPermissionsStore { - data: SubuserPermission[]; - setPermissions: Action; + data: PanelPermissions; + setPermissions: Action; getPermissions: Thunk>; } const permissions: GloablPermissionsStore = { - data: [], + data: {}, setPermissions: action((state, payload) => { state.data = payload; diff --git a/resources/styles/components/modal.css b/resources/styles/components/modal.css index 7c3a6ae9..03e41eb4 100644 --- a/resources/styles/components/modal.css +++ b/resources/styles/components/modal.css @@ -6,9 +6,9 @@ & > .modal-container { @apply .relative .w-full .max-w-md .m-auto .flex-col .flex; - &.top { + /*&.top { margin-top: 10%; - } + }*/ & > .modal-close-icon { @apply .absolute .pin-r .p-2 .text-white .cursor-pointer .opacity-50; @@ -22,7 +22,8 @@ } & > .modal-content { - @apply .bg-neutral-800 .rounded .shadow-md; + @apply .bg-neutral-800 .rounded .shadow-md .overflow-y-scroll; + max-height: calc(100vh - 16rem); transition: all 250ms ease; } @@ -39,7 +40,7 @@ } & > .modal-container.full-screen { - @apply .w-3/4 .mt-32; + @apply .w-3/4; height: calc(100vh - 16rem); max-width: none; }