Skip to content

Commit 8bc81c8

Browse files
committed
Update permissions checking code
1 parent 2e9d327 commit 8bc81c8

File tree

7 files changed

+79
-43
lines changed

7 files changed

+79
-43
lines changed

resources/scripts/components/elements/Can.tsx

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React, { useMemo } from 'react';
2-
import { ServerContext } from '@/state/server';
1+
import React from 'react';
2+
import { usePermissions } from '@/plugins/usePermissions';
33

44
interface Props {
55
action: string | string[];
@@ -8,35 +8,11 @@ interface Props {
88
}
99

1010
const Can = ({ action, renderOnError, children }: Props) => {
11-
const userPermissions = ServerContext.useStoreState(state => state.server.permissions);
12-
const actions = Array.isArray(action) ? action : [ action ];
13-
14-
const missingPermissionCount = useMemo(() => {
15-
if (userPermissions[0] === '*') {
16-
return 0;
17-
}
18-
19-
return actions.filter(permission => {
20-
return !(
21-
// Allows checking for any permission matching a name, for example files.*
22-
// will return if the user has any permission under the file.XYZ namespace.
23-
(
24-
permission.endsWith('.*') &&
25-
permission !== 'websocket.*' &&
26-
userPermissions.filter(p => p.startsWith(permission.split('.')[0])).length > 0
27-
)
28-
// Otherwise just check if the entire permission exists in the array or not.
29-
|| userPermissions.indexOf(permission) >= 0);
30-
}).length;
31-
}, [ action, userPermissions ]);
11+
const can = usePermissions(action);
3212

3313
return (
3414
<>
35-
{missingPermissionCount > 0 ?
36-
renderOnError
37-
:
38-
children
39-
}
15+
{can.every(p => p) ? children : renderOnError}
4016
</>
4117
);
4218
};

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

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import createOrUpdateSubuser from '@/api/server/users/createOrUpdateSubuser';
1414
import { ServerContext } from '@/state/server';
1515
import { httpErrorToHuman } from '@/api/http';
1616
import FlashMessageRender from '@/components/FlashMessageRender';
17+
import Can from '@/components/elements/Can';
18+
import { usePermissions } from '@/plugins/usePermissions';
1719

1820
type Props = {
1921
subuser?: Subuser;
@@ -25,21 +27,32 @@ interface Values {
2527
}
2628

2729
const PermissionLabel = styled.label`
28-
${tw`flex items-center border border-transparent rounded p-2 cursor-pointer`};
30+
${tw`flex items-center border border-transparent rounded p-2`};
2931
text-transform: none;
3032
31-
&:hover {
32-
${tw`border-neutral-500 bg-neutral-800`};
33+
&:not(.disabled) {
34+
${tw`cursor-pointer`};
35+
36+
&:hover {
37+
${tw`border-neutral-500 bg-neutral-800`};
38+
}
3339
}
3440
`;
3541

3642
const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...props }, ref) => {
3743
const { values, isSubmitting, setFieldValue } = useFormikContext<Values>();
44+
const [ canEditUser ] = usePermissions([ 'user.update' ]);
3845
const permissions = useStoreState((state: ApplicationStore) => state.permissions.data);
3946

4047
return (
4148
<Modal {...props} showSpinnerOverlay={isSubmitting}>
42-
<h3 ref={ref}>{subuser ? `Modify permissions for ${subuser.email}` : 'Create new subuser'}</h3>
49+
<h3 ref={ref}>
50+
{subuser ?
51+
`${canEditUser ? 'Modify' : 'View'} permissions for ${subuser.email}`
52+
:
53+
'Create new subuser'
54+
}
55+
</h3>
4356
<FlashMessageRender byKey={'user:edit'} className={'mt-4'}/>
4457
{!subuser &&
4558
<div className={'mt-6'}>
@@ -50,13 +63,14 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
5063
/>
5164
</div>
5265
}
53-
<div className={'mt-6'}>
66+
<div className={'my-6'}>
5467
{Object.keys(permissions).filter(key => key !== 'websocket').map((key, index) => (
5568
<TitledGreyBox
5669
key={key}
5770
title={
5871
<div className={'flex items-center'}>
5972
<p className={'text-sm uppercase flex-1'}>{key}</p>
73+
{canEditUser &&
6074
<input
6175
type={'checkbox'}
6276
onClick={e => {
@@ -78,6 +92,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
7892
}
7993
}}
8094
/>
95+
}
8196
</div>
8297
}
8398
className={index !== 0 ? 'mt-4' : undefined}
@@ -87,9 +102,11 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
87102
</p>
88103
{Object.keys(permissions[key].keys).map((pkey, index) => (
89104
<PermissionLabel
105+
key={`permission_${key}_${pkey}`}
90106
htmlFor={`permission_${key}_${pkey}`}
91107
className={classNames('transition-colors duration-75', {
92108
'mt-2': index !== 0,
109+
disabled: !canEditUser,
93110
})}
94111
>
95112
<div className={'p-2'}>
@@ -98,6 +115,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
98115
name={'permissions'}
99116
value={`${key}.${pkey}`}
100117
className={'w-5 h-5 mr-2'}
118+
disabled={!canEditUser}
101119
/>
102120
</div>
103121
<div className={'flex-1'}>
@@ -115,11 +133,13 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
115133
</TitledGreyBox>
116134
))}
117135
</div>
118-
<div className={'mt-6 pb-6 flex justify-end'}>
119-
<button className={'btn btn-primary btn-sm'} type={'submit'}>
120-
{subuser ? 'Save' : 'Invite User'}
121-
</button>
122-
</div>
136+
<Can action={subuser ? 'user.update' : 'user.delete'}>
137+
<div className={'pb-6 flex justify-end'}>
138+
<button className={'btn btn-primary btn-sm'} type={'submit'}>
139+
{subuser ? 'Save' : 'Invite User'}
140+
</button>
141+
</div>
142+
</Can>
123143
</Modal>
124144
);
125145
});

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import EditSubuserModal from '@/components/server/users/EditSubuserModal';
77
import { faUnlockAlt } from '@fortawesome/free-solid-svg-icons/faUnlockAlt';
88
import { faUserLock } from '@fortawesome/free-solid-svg-icons/faUserLock';
99
import classNames from 'classnames';
10+
import Can from '@/components/elements/Can';
1011

1112
interface Props {
1213
subuser: Subuser;
@@ -58,7 +59,9 @@ export default ({ subuser }: Props) => {
5859
>
5960
<FontAwesomeIcon icon={faPencilAlt}/>
6061
</button>
61-
<RemoveSubuserButton subuser={subuser}/>
62+
<Can action={'user.delete'}>
63+
<RemoveSubuserButton subuser={subuser}/>
64+
</Can>
6265
</div>
6366
);
6467
};

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import UserRow from '@/components/server/users/UserRow';
88
import FlashMessageRender from '@/components/FlashMessageRender';
99
import getServerSubusers from '@/api/server/users/getServerSubusers';
1010
import { httpErrorToHuman } from '@/api/http';
11+
import Can from '@/components/elements/Can';
1112

1213
export default () => {
1314
const [ loading, setLoading ] = useState(true);
@@ -53,9 +54,11 @@ export default () => {
5354
<UserRow key={subuser.uuid} subuser={subuser}/>
5455
))
5556
}
56-
<div className={'flex justify-end mt-6'}>
57-
<AddSubuserButton/>
58-
</div>
57+
<Can action={'user.create'}>
58+
<div className={'flex justify-end mt-6'}>
59+
<AddSubuserButton/>
60+
</div>
61+
</Can>
5962
</div>
6063
);
6164
};

resources/scripts/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export function bytesToHuman (bytes: number): string {
55

66
const i = Math.floor(Math.log(bytes) / Math.log(1000));
77
// @ts-ignore
8-
return `${(bytes / Math.pow(1000, i)).toFixed(2) * 1} ${['Bytes', 'kB', 'MB', 'GB', 'TB'][i]}`;
8+
return `${(bytes / Math.pow(1000, i)).toFixed(2) * 1} ${[ 'Bytes', 'kB', 'MB', 'GB', 'TB' ][i]}`;
99
}
1010

1111
export const bytesToMegabytes = (bytes: number) => Math.floor(bytes / 1000 / 1000);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useRef } from 'react';
2+
import isEqual from 'lodash-es/isEqual';
3+
4+
export const useDeepMemo = <T, K> (fn: () => T, key: K): T => {
5+
const ref = useRef<{ key: K, value: T }>();
6+
7+
if (!ref.current || !isEqual(key, ref.current.key)) {
8+
ref.current = { key, value: fn() };
9+
}
10+
11+
return ref.current.value;
12+
};
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ServerContext } from '@/state/server';
2+
import { useDeepMemo } from '@/plugins/useDeepMemo';
3+
4+
export const usePermissions = (action: string | string[]): boolean[] => {
5+
const userPermissions = ServerContext.useStoreState(state => state.server.permissions);
6+
7+
return useDeepMemo(() => {
8+
return (Array.isArray(action) ? action : [ action ])
9+
.map(permission => (
10+
// Allows checking for any permission matching a name, for example files.*
11+
// will return if the user has any permission under the file.XYZ namespace.
12+
(
13+
permission.endsWith('.*') &&
14+
permission !== 'websocket.*' &&
15+
userPermissions.filter(p => p.startsWith(permission.split('.')[0])).length > 0
16+
) ||
17+
// Otherwise just check if the entire permission exists in the array or not.
18+
userPermissions.indexOf(permission) >= 0
19+
),
20+
);
21+
}, [ action, userPermissions ]);
22+
};

0 commit comments

Comments
 (0)