Skip to content

Commit 1270e51

Browse files
committed
Add support for deleting a subuser from a server
1 parent a6f46d3 commit 1270e51

File tree

11 files changed

+158
-107
lines changed

11 files changed

+158
-107
lines changed

app/Http/Controllers/Api/Client/Servers/SubuserController.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,13 @@ public function store(StoreSubuserRequest $request, Server $server)
8383
* Update a given subuser in the system for the server.
8484
*
8585
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\UpdateSubuserRequest $request
86-
*
86+
* @param \Pterodactyl\Models\Server $server
8787
* @return array
88+
*
8889
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
8990
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
9091
*/
91-
public function update(UpdateSubuserRequest $request)
92+
public function update(UpdateSubuserRequest $request, Server $server): array
9293
{
9394
$subuser = $request->subuser();
9495
$this->repository->update($subuser->id, [
@@ -104,9 +105,10 @@ public function update(UpdateSubuserRequest $request)
104105
* Removes a subusers from a server's assignment.
105106
*
106107
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\DeleteSubuserRequest $request
108+
* @param \Pterodactyl\Models\Server $server
107109
* @return \Illuminate\Http\JsonResponse
108110
*/
109-
public function delete(DeleteSubuserRequest $request)
111+
public function delete(DeleteSubuserRequest $request, Server $server)
110112
{
111113
$this->repository->delete($request->subuser()->id);
112114

app/Http/Requests/Api/Client/Servers/Subusers/AbstractSubuserRequest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public function subuser()
5757
}
5858

5959
return $this->model ?: $this->model = $repository->getUserForServer(
60-
$this->route()->parameter('subuser'), $this->route()->parameter('server')->id
60+
$parameters['server']->id, $parameters['subuser']
6161
);
6262
}
6363
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import http from '@/api/http';
2+
3+
export default (uuid: string, userId: string): Promise<void> => {
4+
return new Promise((resolve, reject) => {
5+
http.delete(`/api/client/servers/${uuid}/users/${userId}`)
6+
.then(() => resolve())
7+
.catch(reject);
8+
});
9+
};

resources/scripts/components/dashboard/AccountApiContainer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export default () => {
6262
doDeletion(deleteIdentifier);
6363
setDeleteIdentifier('');
6464
}}
65-
onCanceled={() => setDeleteIdentifier('')}
65+
onDismissed={() => setDeleteIdentifier('')}
6666
>
6767
Are you sure you wish to delete this API key? All requests using it will immediately be
6868
invalidated and will fail.

resources/scripts/components/elements/ConfirmationModal.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
import React from 'react';
2-
import Modal from '@/components/elements/Modal';
2+
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
33

4-
interface Props {
4+
type Props = {
55
title: string;
66
buttonText: string;
77
children: string;
8-
visible: boolean;
98
onConfirmed: () => void;
10-
onCanceled: () => void;
11-
}
9+
showSpinnerOverlay?: boolean;
10+
} & RequiredModalProps;
1211

13-
const ConfirmationModal = ({ title, children, visible, buttonText, onConfirmed, onCanceled }: Props) => (
12+
const ConfirmationModal = ({ title, appear, children, visible, buttonText, onConfirmed, showSpinnerOverlay, onDismissed }: Props) => (
1413
<Modal
15-
appear={true}
14+
appear={appear || true}
1615
visible={visible}
17-
onDismissed={() => onCanceled()}
16+
showSpinnerOverlay={showSpinnerOverlay}
17+
onDismissed={() => onDismissed()}
1818
>
1919
<h3 className={'mb-6'}>{title}</h3>
2020
<p className={'text-sm'}>{children}</p>
2121
<div className={'flex items-center justify-end mt-8'}>
22-
<button className={'btn btn-secondary btn-sm'} onClick={() => onCanceled()}>
22+
<button className={'btn btn-secondary btn-sm'} onClick={() => onDismissed()}>
2323
Cancel
2424
</button>
2525
<button className={'btn btn-red btn-sm ml-4'} onClick={() => onConfirmed()}>

resources/scripts/components/elements/Modal.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState } from 'react';
1+
import React, { useEffect, useMemo, useState } from 'react';
22
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
33
import { faTimes } from '@fortawesome/free-solid-svg-icons/faTimes';
44
import { CSSTransition } from 'react-transition-group';
@@ -18,16 +18,22 @@ type Props = RequiredModalProps & {
1818
children: React.ReactNode;
1919
}
2020

21-
export default (props: Props) => {
22-
const [render, setRender] = useState(props.visible);
21+
export default ({ visible, appear, dismissable, showSpinnerOverlay, closeOnBackground = true, closeOnEscape = true, onDismissed, children }: Props) => {
22+
const [render, setRender] = useState(visible);
23+
24+
const isDismissable = useMemo(() => {
25+
return (dismissable || true) && !(showSpinnerOverlay || false);
26+
}, [dismissable, showSpinnerOverlay]);
2327

2428
const handleEscapeEvent = (e: KeyboardEvent) => {
25-
if (props.dismissable !== false && props.closeOnEscape !== false && e.key === 'Escape') {
29+
if (isDismissable && closeOnEscape && e.key === 'Escape') {
2630
setRender(false);
2731
}
2832
};
2933

30-
useEffect(() => setRender(props.visible), [props.visible]);
34+
useEffect(() => {
35+
setRender(visible);
36+
}, [visible]);
3137

3238
useEffect(() => {
3339
window.addEventListener('keydown', handleEscapeEvent);
@@ -39,26 +45,26 @@ export default (props: Props) => {
3945
<CSSTransition
4046
timeout={250}
4147
classNames={'fade'}
42-
appear={props.appear}
48+
appear={appear}
4349
in={render}
4450
unmountOnExit={true}
45-
onExited={() => props.onDismissed()}
51+
onExited={() => onDismissed()}
4652
>
4753
<div className={'modal-mask'} onClick={e => {
48-
if (props.dismissable !== false && props.closeOnBackground !== false) {
54+
if (isDismissable && closeOnBackground) {
4955
e.stopPropagation();
5056
if (e.target === e.currentTarget) {
5157
setRender(false);
5258
}
5359
}
5460
}}>
5561
<div className={'modal-container top'}>
56-
{props.dismissable !== false &&
62+
{isDismissable &&
5763
<div className={'modal-close-icon'} onClick={() => setRender(false)}>
5864
<FontAwesomeIcon icon={faTimes}/>
5965
</div>
6066
}
61-
{props.showSpinnerOverlay &&
67+
{showSpinnerOverlay &&
6268
<div
6369
className={'absolute w-full h-full rounded flex items-center justify-center'}
6470
style={{ background: 'hsla(211, 10%, 53%, 0.25)' }}
@@ -67,7 +73,7 @@ export default (props: Props) => {
6773
</div>
6874
}
6975
<div className={'modal-content p-6'}>
70-
{props.children}
76+
{children}
7177
</div>
7278
</div>
7379
</div>

resources/scripts/components/server/users/PermissionEditor.tsx

Lines changed: 0 additions & 46 deletions
This file was deleted.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React, { useState } from 'react';
2+
import ConfirmationModal from '@/components/elements/ConfirmationModal';
3+
import { ServerContext } from '@/state/server';
4+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5+
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt';
6+
import { Subuser } from '@/state/server/subusers';
7+
import deleteSubuser from '@/api/server/users/deleteSubuser';
8+
import { Actions, useStoreActions } from 'easy-peasy';
9+
import { ApplicationStore } from '@/state';
10+
import { httpErrorToHuman } from '@/api/http';
11+
12+
export default ({ subuser }: { subuser: Subuser }) => {
13+
const [ loading, setLoading ] = useState(false);
14+
const [ showConfirmation, setShowConfirmation ] = useState(false);
15+
16+
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
17+
const removeSubuser = ServerContext.useStoreActions(actions => actions.subusers.removeSubuser);
18+
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
19+
20+
const doDeletion = () => {
21+
setLoading(true);
22+
clearFlashes('users');
23+
deleteSubuser(uuid, subuser.uuid)
24+
.then(() => {
25+
setLoading(false);
26+
removeSubuser(subuser.uuid);
27+
})
28+
.catch(error => {
29+
console.error(error);
30+
addError({ key: 'users', message: httpErrorToHuman(error) });
31+
setShowConfirmation(false);
32+
});
33+
}
34+
35+
return (
36+
<>
37+
{showConfirmation &&
38+
<ConfirmationModal
39+
title={'Delete this subuser?'}
40+
buttonText={'Yes, remove subuser'}
41+
visible={true}
42+
showSpinnerOverlay={loading}
43+
onConfirmed={() => doDeletion()}
44+
onDismissed={() => setShowConfirmation(false)}
45+
>
46+
Are you sure you wish to remove this subuser? They will have all access to this server revoked
47+
immediately.
48+
</ConfirmationModal>
49+
}
50+
<button
51+
type={'button'}
52+
aria-label={'Delete subuser'}
53+
className={'block text-sm p-2 text-neutral-500 hover:text-red-600 transition-colors duration-150'}
54+
onClick={() => setShowConfirmation(true)}
55+
>
56+
<FontAwesomeIcon icon={faTrashAlt}/>
57+
</button>
58+
</>
59+
);
60+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React, { useState } from 'react';
2+
import { Subuser } from '@/state/server/subusers';
3+
import { ServerContext } from '@/state/server';
4+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5+
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons/faPencilAlt';
6+
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt';
7+
import ConfirmationModal from '@/components/elements/ConfirmationModal';
8+
import RemoveSubuserButton from '@/components/server/users/RemoveSubuserButton';
9+
10+
interface Props {
11+
subuser: Subuser;
12+
}
13+
14+
export default ({ subuser }: Props) => {
15+
const appendSubuser = ServerContext.useStoreActions(actions => actions.subusers.appendSubuser);
16+
17+
return (
18+
<div className={'grey-row-box mb-2'}>
19+
<div className={'w-10 h-10 rounded-full bg-white border-2 border-inset border-neutral-800 overflow-hidden'}>
20+
<img className={'f-full h-full'} src={`${subuser.image}?s=400`}/>
21+
</div>
22+
<div className={'ml-4 flex-1'}>
23+
<p className={'text-sm'}>{subuser.email}</p>
24+
</div>
25+
<button
26+
type={'button'}
27+
aria-label={'Edit subuser'}
28+
className={'block text-sm p-2 text-neutral-500 hover:text-neutral-100 transition-colors duration-150 mr-4'}
29+
onClick={() => null}
30+
>
31+
<FontAwesomeIcon icon={faPencilAlt}/>
32+
</button>
33+
<RemoveSubuserButton subuser={subuser}/>
34+
</div>
35+
);
36+
};

resources/scripts/components/server/users/UsersContainer.tsx

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,66 +4,53 @@ import { Actions, useStoreActions, useStoreState } from 'easy-peasy';
44
import { ApplicationStore } from '@/state';
55
import Spinner from '@/components/elements/Spinner';
66
import AddSubuserButton from '@/components/server/users/AddSubuserButton';
7+
import UserRow from '@/components/server/users/UserRow';
8+
import FlashMessageRender from '@/components/FlashMessageRender';
9+
import getServerSubusers from '@/api/server/users/getServerSubusers';
10+
import { httpErrorToHuman } from '@/api/http';
711

812
export default () => {
913
const [ loading, setLoading ] = useState(true);
1014

1115
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
1216
const subusers = ServerContext.useStoreState(state => state.subusers.data);
13-
const getSubusers = ServerContext.useStoreActions(actions => actions.subusers.getSubusers);
17+
const setSubusers = ServerContext.useStoreActions(actions => actions.subusers.setSubusers);
1418

1519
const permissions = useStoreState((state: ApplicationStore) => state.permissions.data);
1620
const getPermissions = useStoreActions((actions: Actions<ApplicationStore>) => actions.permissions.getPermissions);
21+
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
1722

1823
useEffect(() => {
1924
getPermissions().catch(error => console.error(error));
2025
}, []);
2126

2227
useEffect(() => {
23-
getSubusers(uuid)
24-
.then(() => setLoading(false))
28+
clearFlashes('users');
29+
getServerSubusers(uuid)
30+
.then(subusers => {
31+
setSubusers(subusers);
32+
setLoading(false);
33+
})
2534
.catch(error => {
2635
console.error(error);
36+
addError({ key: 'users', message: httpErrorToHuman(error) });
2737
});
28-
}, [ uuid, getSubusers ]);
29-
30-
useEffect(() => {
31-
setLoading(!subusers);
32-
}, [ subusers ]);
38+
}, []);
3339

3440
if (loading || !Object.keys(permissions).length) {
3541
return <Spinner size={'large'} centered={true}/>;
3642
}
3743

3844
return (
3945
<div className={'my-10'}>
46+
<FlashMessageRender byKey={'users'} className={'mb-4'}/>
4047
{!subusers.length ?
4148
<p className={'text-center text-sm text-neutral-400'}>
4249
It looks like you don't have any subusers.
4350
</p>
4451
:
4552
subusers.map(subuser => (
46-
<div key={subuser.uuid} className={'flex items-center w-full'}>
47-
<img
48-
className={'w-10 h-10 rounded-full bg-white border-2 border-inset border-neutral-800'}
49-
src={`${subuser.image}?s=400`}
50-
/>
51-
<div className={'ml-4 flex-1'}>
52-
<p className={'text-sm'}>{subuser.email}</p>
53-
</div>
54-
<div className={'ml-4'}>
55-
<button
56-
className={'btn btn-xs btn-primary'}
57-
>
58-
Edit
59-
</button>
60-
<button
61-
className={'ml-2 btn btn-xs btn-red btn-secondary'}
62-
>
63-
Remove
64-
</button>
65-
</div>
66-
</div>
53+
<UserRow key={subuser.uuid} subuser={subuser}/>
6754
))
6855
}
6956
<div className={'flex justify-end mt-6'}>

0 commit comments

Comments
 (0)