Add per-server permissions

This commit is contained in:
jeffvli
2022-11-09 01:52:08 -08:00
parent 73e6002cc7
commit 581ef32845
13 changed files with 371 additions and 139 deletions
@@ -19,6 +19,7 @@ export const useLogin = (
const props = {
accessToken: res.data.accessToken,
permissions: {
id: res.data.id,
isAdmin: res.data.isAdmin,
isSuperAdmin: res.data.isSuperAdmin,
username: res.data.username,
@@ -18,7 +18,7 @@ import { useEnableServerUrl } from '@/renderer/features/servers/mutations/use-en
import { useFullScan } from '@/renderer/features/servers/mutations/use-full-scan';
import { useQuickScan } from '@/renderer/features/servers/mutations/use-quick-scan';
import { useUpdateServer } from '@/renderer/features/servers/mutations/use-update-server';
import { usePermissions } from '@/renderer/features/shared';
import { ServerPermission, usePermissions } from '@/renderer/features/shared';
import { useTaskList } from '@/renderer/features/tasks';
import { useAuthStore } from '@/renderer/store';
import { Font } from '@/renderer/styles';
@@ -34,6 +34,10 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
const [addCredential, addCredentialHandlers] = useDisclosure(false);
const permissions = usePermissions();
const serverPermission = permissions[
server.id as keyof typeof permissions
] as ServerPermission;
const updateServer = useUpdateServer();
const enableServerUrl = useEnableServerUrl();
const disableServerUrl = useDisableServerUrl();
@@ -148,23 +152,27 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
<Group position="apart">
<Text font={Font.EPILOGUE}>Server details</Text>
<Group spacing="md">
<Button
compact
disabled={isRunningTask}
loading={fullScan.isLoading}
variant="subtle"
onClick={handleFullScan}
>
Full scan
</Button>
<Button
compact
disabled={true || isRunningTask}
variant="subtle"
onClick={handleQuickScan}
>
Quick scan
</Button>
{serverPermission >= ServerPermission.ADMIN && (
<Button
compact
disabled={isRunningTask}
loading={fullScan.isLoading}
variant="subtle"
onClick={handleFullScan}
>
Full scan
</Button>
)}
{serverPermission >= ServerPermission.EDITOR && (
<Button
compact
disabled={true || isRunningTask}
variant="subtle"
onClick={handleQuickScan}
>
Quick scan
</Button>
)}
</Group>
</Group>
}
@@ -179,50 +187,58 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
<Group>
<Stack>
<Text>URL</Text>
<Text>Username</Text>
{serverPermission >= ServerPermission.EDITOR && (
<Text>Username</Text>
)}
</Stack>
<Stack>
<Text size="sm">{server.url}</Text>
<Text size="sm">{server.username}</Text>
{serverPermission >= ServerPermission.EDITOR && (
<Text size="sm">{server.username}</Text>
)}
</Stack>
</Group>
<Group>
<Button
disabled={!permissions.editServer}
tooltip={{ label: 'Edit server details' }}
variant="default"
onClick={() => editHandlers.toggle()}
>
<RiEdit2Fill color="white" />
</Button>
</Group>
{serverPermission >= ServerPermission.ADMIN && (
<Group>
<Button
tooltip={{ label: 'Edit server details' }}
variant="default"
onClick={() => editHandlers.toggle()}
>
<RiEdit2Fill color="white" />
</Button>
</Group>
)}
</Group>
)}
</ServerSection>
<ServerSection title="Music Folders">
<Stack>
{server.serverFolders?.map((folder) => (
<Group key={folder.id} position="apart">
<Text size="sm">{folder.name}</Text>
<Group>
<Button
compact
disabled={true || !permissions.deleteServerFolder}
variant="subtle"
>
<RiDeleteBin2Line color="var(--danger-color)" />
</Button>
<Text size="sm">{folder.name}</Text>
<>
<Switch
checked={folder.enabled}
disabled={serverPermission < ServerPermission.ADMIN}
onChange={(e) =>
handleToggleFolder(folder.id, !e.currentTarget.checked)
}
/>
{serverPermission >= ServerPermission.ADMIN && (
<Button compact variant="subtle">
<RiDeleteBin2Line color="var(--danger-color)" />
</Button>
)}
</>
</Group>
<Switch
checked={folder.enabled}
onChange={(e) =>
handleToggleFolder(folder.id, !e.currentTarget.checked)
}
/>
</Group>
))}
</Stack>
</ServerSection>
<ServerSection title="Credentials">
{addCredential ? (
<AddServerCredentialForm
@@ -234,32 +250,30 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
<Stack>
{serverCredentials?.map((credential) => (
<Group key={credential.id} position="apart">
<Text size="sm">{credential.username}</Text>
<Group>
<Switch
checked={credential.enabled}
onChange={(e) =>
handleToggleCredential(
credential.id,
!e.currentTarget.checked
)
}
/>
<Button
compact
disabled={!permissions.deleteServerCredential}
variant="subtle"
onClick={() => handleDeleteCredential(credential.id)}
>
<RiDeleteBin2Line color="var(--danger-color)" />
</Button>
<Text size="sm">{credential.username}</Text>
</Group>
<Switch
checked={credential.enabled}
onChange={(e) =>
handleToggleCredential(
credential.id,
!e.currentTarget.checked
)
}
/>
</Group>
))}
</Stack>
<Button
compact
disabled={!permissions.createServerCredential}
mt={10}
variant="subtle"
onClick={() => addCredentialHandlers.open()}
@@ -269,6 +283,7 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
</>
)}
</ServerSection>
<ServerSection title="URLs">
{addUrl ? (
<AddServerUrlForm
@@ -280,60 +295,75 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
<Stack>
{server.serverUrls?.map((serverUrl) => (
<Group key={serverUrl.id} position="apart">
<Text size="sm">{serverUrl.url}</Text>
<Group>
<Button
compact
disabled={!permissions.deleteServerUrl}
variant="subtle"
onClick={() => handleDeleteUrl(serverUrl.id)}
>
<RiDeleteBin2Line color="var(--danger-color)" />
</Button>
<Text size="sm">{serverUrl.url}</Text>
<Switch
checked={serverUrl.enabled}
onChange={(e) =>
handleToggleUrl(
serverUrl.id,
!e.currentTarget.checked
)
}
/>
{serverPermission >= ServerPermission.EDITOR && (
<Button
compact
variant="subtle"
onClick={() => handleDeleteUrl(serverUrl.id)}
>
<RiDeleteBin2Line color="var(--danger-color)" />
</Button>
)}
</Group>
<Switch
checked={serverUrl.enabled}
onChange={(e) =>
handleToggleUrl(serverUrl.id, !e.currentTarget.checked)
}
/>
</Group>
))}
</Stack>
<Button
compact
disabled={!permissions.createServerUrl}
mt={10}
variant="subtle"
onClick={() => addUrlHandlers.open()}
>
Add url
</Button>
{serverPermission >= ServerPermission.EDITOR && (
<Button
compact
mt={10}
variant="subtle"
onClick={() => addUrlHandlers.open()}
>
Add url
</Button>
)}
</>
)}
</ServerSection>
<ServerSection title="Danger zone">
<Group position="apart">
<Text size="sm">Require user credentials</Text>
<Switch
checked={server.noCredential}
disabled={!permissions.isAdmin}
onChange={(e) =>
toggleRequiredCredential(e.currentTarget.checked)
}
/>
</Group>
<Divider my="xl" />
<Button
compact
disabled={!permissions.deleteServer}
leftIcon={<RiDeleteBin2Line color="var(--danger-color)" />}
variant="default"
>
Delete server
</Button>
</ServerSection>
{serverPermission >= ServerPermission.ADMIN && (
<ServerSection title="Danger zone">
<Group position="apart">
<Text size="sm">Require user credentials</Text>
<Switch
checked={server.noCredential}
onChange={(e) =>
toggleRequiredCredential(e.currentTarget.checked)
}
/>
</Group>
{permissions.isSuperAdmin && (
<>
<Divider my="xl" />
<Button
compact
leftIcon={<RiDeleteBin2Line />}
size="xs"
sx={{
'&:hover': {
background: 'var(--danger-color)',
},
background: 'var(--danger-color)',
}}
>
Delete server
</Button>
</>
)}
</ServerSection>
)}
</Stack>
</>
);
@@ -1,27 +1,63 @@
import { useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/renderer/api/query-keys';
import { ServerListResponse } from '@/renderer/api/servers.api';
import { ServerPermissionType } from '@/renderer/api/types';
import { UserDetailResponse } from '@/renderer/api/users.api';
import { useAuthStore } from '@/renderer/store';
export enum ServerPermission {
VIEWER = 0,
EDITOR = 1,
ADMIN = 2,
}
const SERVER_PERMISSION_MAP = {
[ServerPermissionType.VIEWER]: 0,
[ServerPermissionType.EDITOR]: 1,
[ServerPermissionType.ADMIN]: 2,
};
export const usePermissions = () => {
const queryClient = useQueryClient();
const userId = useAuthStore((state) => state.permissions.id);
const permissions = useAuthStore((state) => state.permissions);
const permissionSet = useMemo(() => {
const set = {
createServer: permissions.isAdmin,
createServerCredential: true,
createServerUrl: permissions.isAdmin,
deleteServer: permissions.isAdmin,
deleteServerCredential: true,
deleteServerFolder: permissions.isAdmin,
deleteServerUrl: permissions.isAdmin,
editServer: permissions.isAdmin,
editServerFolder: permissions.isAdmin,
isAdmin: permissions.isAdmin,
isSuperAdmin: permissions.isSuperAdmin,
manageUsers: permissions.isAdmin,
};
const user = queryClient.getQueryData<UserDetailResponse>(
queryKeys.users.detail(userId)
);
return set;
}, [permissions]);
const servers = queryClient.getQueryData<ServerListResponse>(
queryKeys.servers.list()
);
const permissionSet: { [key: string]: any } = useMemo(() => {
const serverPermissions: { [key: string]: ServerPermission } = {};
servers?.data?.forEach((server) => {
const permission = user?.data?.serverPermissions?.find(
(p) => p.serverId === server.id
)?.type;
serverPermissions[server.id] =
permissions.isAdmin || permissions.isSuperAdmin
? ServerPermission.ADMIN
: permission
? SERVER_PERMISSION_MAP[permission]
: -1;
});
return {
isAdmin: permissions.isAdmin || permissions.isSuperAdmin,
isSuperAdmin: permissions.isSuperAdmin,
...serverPermissions,
};
}, [
permissions.isAdmin,
permissions.isSuperAdmin,
servers?.data,
user?.data?.serverPermissions,
]);
return permissionSet;
};
@@ -1,16 +1,20 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys';
import { UserDetailResponse } from '@/renderer/api/users.api';
import { QueryOptions } from '@/renderer/lib/react-query';
export const useUserDetail = (options: { userId: string }) => {
const { data, error, isLoading } = useQuery({
queryFn: () => api.users.getUserDetail({ userId: options.userId }),
queryKey: queryKeys.users.detail(options.userId),
export const useUserDetail = (
q: { userId: string },
options?: QueryOptions<UserDetailResponse>
) => {
const query = useQuery({
cacheTime: Infinity,
queryFn: () => api.users.getUserDetail({ userId: q.userId }),
queryKey: queryKeys.users.detail(q.userId),
staleTime: 1000 * 60 * 60 * 24,
...options,
});
return {
data,
error,
isLoading,
};
return query;
};
+8 -1
View File
@@ -8,6 +8,7 @@ export interface AuthState {
accessToken: string;
currentServer: Server | null;
permissions: {
id: string;
isAdmin: boolean;
isSuperAdmin: boolean;
username: string;
@@ -91,11 +92,17 @@ export const useAuthStore = create<AuthSlice>()(
logout: () => {
return set({
accessToken: undefined,
permissions: { isAdmin: false, isSuperAdmin: false, username: '' },
permissions: {
id: '',
isAdmin: false,
isSuperAdmin: false,
username: '',
},
refreshToken: undefined,
});
},
permissions: {
id: '',
isAdmin: false,
isSuperAdmin: false,
username: '',