From c54eea4382ec7ee8f8937544e889d718a2f2b8a9 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Mon, 14 Nov 2022 01:13:54 -0800 Subject: [PATCH] Add server permission management --- server/controllers/servers.controller.ts | 22 +- server/routes/servers.route.ts | 14 +- server/routes/users.route.ts | 6 +- server/services/servers.service.ts | 5 +- server/services/users.service.ts | 14 +- src/renderer/api/servers.api.ts | 83 ++++++ src/renderer/features/servers/index.ts | 5 + .../create-server-folder-permission.ts | 31 ++ .../mutations/create-server-permission.ts | 30 ++ .../delete-server-folder-permission.ts | 26 ++ .../mutations/delete-server-permission.ts | 23 ++ .../mutations/update-server-permission.ts | 29 ++ .../features/shared/hooks/use-permissions.ts | 7 + .../features/titlebar/components/app-menu.tsx | 3 +- .../components/edit-user-permissions-form.tsx | 272 ++++++++++++++++++ .../features/users/components/user-list.tsx | 67 +++-- 16 files changed, 594 insertions(+), 43 deletions(-) create mode 100644 src/renderer/features/servers/mutations/create-server-folder-permission.ts create mode 100644 src/renderer/features/servers/mutations/create-server-permission.ts create mode 100644 src/renderer/features/servers/mutations/delete-server-folder-permission.ts create mode 100644 src/renderer/features/servers/mutations/delete-server-permission.ts create mode 100644 src/renderer/features/servers/mutations/update-server-permission.ts create mode 100644 src/renderer/features/users/components/edit-user-permissions-form.tsx diff --git a/server/controllers/servers.controller.ts b/server/controllers/servers.controller.ts index bd7e6ed9a..23e7cc17f 100644 --- a/server/controllers/servers.controller.ts +++ b/server/controllers/servers.controller.ts @@ -194,7 +194,7 @@ const deleteServerUrl = async ( id: urlId, }); - const success = ApiSuccess.noContent({ data: null }); + const success = ApiSuccess.ok({ data: null }); return res.status(success.statusCode).json(getSuccessResponse(success)); }; @@ -209,7 +209,7 @@ const enableServerUrl = async ( serverId, }); - const success = ApiSuccess.noContent({ data: null }); + const success = ApiSuccess.ok({ data: null }); return res.status(success.statusCode).json(getSuccessResponse(success)); }; @@ -219,7 +219,7 @@ const disableServerUrl = async ( ) => { await service.servers.disableUrlById(req.authUser); - const success = ApiSuccess.noContent({ data: null }); + const success = ApiSuccess.ok({ data: null }); return res.status(success.statusCode).json(getSuccessResponse(success)); }; @@ -231,7 +231,7 @@ const deleteServerFolder = async ( await service.servers.deleteFolderById({ id: folderId }); - const success = ApiSuccess.noContent({ data: null }); + const success = ApiSuccess.ok({ data: null }); return res.status(success.statusCode).json(getSuccessResponse(success)); }; @@ -243,7 +243,7 @@ const enableServerFolder = async ( await service.servers.enableFolderById({ id: folderId }); - const success = ApiSuccess.noContent({ data: null }); + const success = ApiSuccess.ok({ data: null }); return res.status(success.statusCode).json(getSuccessResponse(success)); }; @@ -255,7 +255,7 @@ const disableServerFolder = async ( await service.servers.disableFolderById({ id: folderId }); - const success = ApiSuccess.noContent({ data: null }); + const success = ApiSuccess.ok({ data: null }); return res.status(success.statusCode).json(getSuccessResponse(success)); }; @@ -286,7 +286,7 @@ const deleteServerPermission = async ( id: permissionId, }); - const success = ApiSuccess.noContent({ data: null }); + const success = ApiSuccess.ok({ data: null }); return res.status(success.statusCode).json(getSuccessResponse(success)); }; @@ -302,7 +302,7 @@ const updateServerPermission = async ( type, }); - const success = ApiSuccess.noContent({ data: null }); + const success = ApiSuccess.ok({ data: null }); return res.status(success.statusCode).json(getSuccessResponse(success)); }; @@ -326,11 +326,11 @@ const deleteServerFolderPermission = async ( req: TypedRequest, res: Response ) => { - const { permissionId } = req.params; + const { folderPermissionId } = req.params; - await service.servers.deleteFolderPermission({ id: permissionId }); + await service.servers.deleteFolderPermission({ id: folderPermissionId }); - const success = ApiSuccess.noContent({ data: null }); + const success = ApiSuccess.ok({ data: null }); return res.status(success.statusCode).json(getSuccessResponse(success)); }; diff --git a/server/routes/servers.route.ts b/server/routes/servers.route.ts index b59110455..1696704fd 100644 --- a/server/routes/servers.route.ts +++ b/server/routes/servers.route.ts @@ -111,11 +111,13 @@ router .route('/:serverId/permissions/:permissionId') .patch( authenticateServerAdmin, - validateRequest(validation.servers.updateServerPermission) + validateRequest(validation.servers.updateServerPermission), + controller.servers.updateServerPermission ) .delete( authenticateServerAdmin, - validateRequest(validation.servers.deleteServerPermission) + validateRequest(validation.servers.deleteServerPermission), + controller.servers.deleteServerPermission ); router.param('folderId', async (_req, _res, next, folderId) => { @@ -149,9 +151,11 @@ router router .route('/:serverId/folder/:folderId/permissions') - .post(authenticateServerAdmin); + .post(authenticateServerAdmin, controller.servers.addServerFolderPermission); router .route('/:serverId/folder/:folderId/permissions/:folderPermissionId') - .patch(authenticateServerAdmin) - .delete(authenticateServerAdmin); + .delete( + authenticateServerAdmin, + controller.servers.deleteServerFolderPermission + ); diff --git a/server/routes/users.route.ts b/server/routes/users.route.ts index 9f07b8d64..7f43023f8 100644 --- a/server/routes/users.route.ts +++ b/server/routes/users.route.ts @@ -13,11 +13,7 @@ export const router: Router = express.Router({ mergeParams: true }); router .route('/') - .get( - authenticateAdmin, - validateRequest(validation.users.list), - controller.users.getUserList - ) + .get(validateRequest(validation.users.list), controller.users.getUserList) .post( authenticateAdmin, validateRequest(validation.users.createUser), diff --git a/server/services/servers.service.ts b/server/services/servers.service.ts index b82613874..44f1b37b1 100644 --- a/server/services/servers.service.ts +++ b/server/services/servers.service.ts @@ -138,7 +138,10 @@ const findMany = async (user: AuthUser, options?: { enabled?: boolean }) => { }, // If not admin, only show folders the user has permissions for { serverFolderPermissions: { some: { userId: user.id } } }, - { enabled: options?.enabled ? true : undefined }, + { + enabled: options?.enabled ? true : undefined, + serverFolderPermissions: { some: { userId: user.id } }, + }, ], }, }, diff --git a/server/services/users.service.ts b/server/services/users.service.ts index 45147b6ee..431aa8645 100644 --- a/server/services/users.service.ts +++ b/server/services/users.service.ts @@ -11,9 +11,10 @@ import { SortOrder } from '../types/types'; const findById = async (user: AuthUser, options: { id: string }) => { const { id } = options; - if (!user.isAdmin && user.id !== id) { - throw ApiError.forbidden(); - } + // Possibly restrict detail later if additional sensitive user data is added + // if (!user.isAdmin && user.id !== id) { + // throw ApiError.forbidden(); + // } const uniqueUser = await prisma.user.findUnique({ include: { @@ -33,9 +34,14 @@ const findById = async (user: AuthUser, options: { id: string }) => { const findMany = async () => { const users = await prisma.user.findMany({ - include: { files: true }, + include: { + files: true, + serverFolderPermissions: true, + serverPermissions: true, + }, orderBy: [{ isAdmin: SortOrder.DESC }, { username: SortOrder.ASC }], }); + return users; }; diff --git a/src/renderer/api/servers.api.ts b/src/renderer/api/servers.api.ts index 87972a084..71ddaff1a 100644 --- a/src/renderer/api/servers.api.ts +++ b/src/renderer/api/servers.api.ts @@ -2,6 +2,7 @@ import { BaseResponse, NullResponse, Server, + ServerPermissionType, ServerType, ServerUrl, } from '@/renderer/api/types'; @@ -145,10 +146,91 @@ const fullScan = async (options: { return data; }; +export type CreateServerPermissionBody = { + type: ServerPermissionType; + userId: string; +}; + +const createServerPermission = async ( + query: { serverId: string }, + body: CreateServerPermissionBody +) => { + const { data } = await ax.post( + `/servers/${query.serverId}/permissions`, + body + ); + return data; +}; + +const deleteServerPermission = async (query: { + permissionId: string; + serverId: string; +}) => { + const { data } = await ax.delete( + `/servers/${query.serverId}/permissions/${query.permissionId}` + ); + + return data; +}; + +export type UpdateServerPermissionBody = { + type: ServerPermissionType; +}; + +const updateServerPermission = async ( + query: { + permissionId: string; + serverId: string; + }, + body: UpdateServerPermissionBody +) => { + const { data } = await ax.patch( + `/servers/${query.serverId}/permissions/${query.permissionId}`, + body + ); + + return data; +}; + +export type CreateServerFolderPermissionBody = { + userId: string; +}; + +const createServerFolderPermission = async ( + query: { + folderId: string; + serverId: string; + }, + body: CreateServerFolderPermissionBody +) => { + const { data } = await ax.post( + `/servers/${query.serverId}/folder/${query.folderId}/permissions`, + body + ); + + return data; +}; + +const deleteServerFolderPermission = async (query: { + folderId: string; + folderPermissionId: string; + serverId: string; +}) => { + const { data } = await ax.delete( + `/servers/${query.serverId}/folder/${query.folderId}/permissions/${query.folderPermissionId}` + ); + + return data; +}; + export const serversApi = { createServer, + createServerFolderPermission, + createServerPermission, createUrl, deleteServer, + deleteServerFolderPermission, + deleteServerPermission, deleteUrl, disableFolder, disableUrl, @@ -158,4 +240,5 @@ export const serversApi = { getServerList, quickScan, updateServer, + updateServerPermission, }; diff --git a/src/renderer/features/servers/index.ts b/src/renderer/features/servers/index.ts index 13bb92ce8..0803bbfe5 100644 --- a/src/renderer/features/servers/index.ts +++ b/src/renderer/features/servers/index.ts @@ -2,3 +2,8 @@ export * from './mutations/use-create-server'; export * from './components/add-server-form'; export * from './components/server-list'; export * from './queries/use-server-list'; +export * from './mutations/create-server-folder-permission'; +export * from './mutations/delete-server-folder-permission'; +export * from './mutations/update-server-permission'; +export * from './mutations/create-server-permission'; +export * from './mutations/delete-server-permission'; diff --git a/src/renderer/features/servers/mutations/create-server-folder-permission.ts b/src/renderer/features/servers/mutations/create-server-folder-permission.ts new file mode 100644 index 000000000..2a6430b8e --- /dev/null +++ b/src/renderer/features/servers/mutations/create-server-folder-permission.ts @@ -0,0 +1,31 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { api } from '@/renderer/api'; +import { queryKeys } from '@/renderer/api/query-keys'; +import { CreateServerFolderPermissionBody } from '@/renderer/api/servers.api'; +import { ApiError, NullResponse } from '@/renderer/api/types'; +import { UserListResponse } from '@/renderer/api/users.api'; + +export const useCreateServerFolderPermission = () => { + const queryClient = useQueryClient(); + + const mutation = useMutation< + NullResponse, + AxiosError, + { + body: CreateServerFolderPermissionBody; + query: { folderId: string; serverId: string }; + }, + { previous: UserListResponse | undefined } + >({ + mutationFn: ({ query, body }) => + api.servers.createServerFolderPermission(query, body), + onSuccess: (_data, variables) => { + queryClient.invalidateQueries( + queryKeys.users.detail(variables.body.userId) + ); + }, + }); + + return mutation; +}; diff --git a/src/renderer/features/servers/mutations/create-server-permission.ts b/src/renderer/features/servers/mutations/create-server-permission.ts new file mode 100644 index 000000000..4c6222f99 --- /dev/null +++ b/src/renderer/features/servers/mutations/create-server-permission.ts @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { api } from '@/renderer/api'; +import { queryKeys } from '@/renderer/api/query-keys'; +import { CreateServerPermissionBody } from '@/renderer/api/servers.api'; +import { ApiError, NullResponse } from '@/renderer/api/types'; + +export const useCreateServerPermission = () => { + const queryClient = useQueryClient(); + + const mutation = useMutation< + NullResponse, + AxiosError, + { + body: CreateServerPermissionBody; + query: { serverId: string }; + }, + undefined + >({ + mutationFn: ({ query, body }) => + api.servers.createServerPermission(query, body), + onSuccess: (_data, variables) => { + queryClient.invalidateQueries( + queryKeys.users.detail(variables.body.userId) + ); + }, + }); + + return mutation; +}; diff --git a/src/renderer/features/servers/mutations/delete-server-folder-permission.ts b/src/renderer/features/servers/mutations/delete-server-folder-permission.ts new file mode 100644 index 000000000..cb92d1445 --- /dev/null +++ b/src/renderer/features/servers/mutations/delete-server-folder-permission.ts @@ -0,0 +1,26 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { api } from '@/renderer/api'; +import { queryKeys } from '@/renderer/api/query-keys'; +import { ApiError, NullResponse } from '@/renderer/api/types'; + +export const useDeleteServerFolderPermission = () => { + const queryClient = useQueryClient(); + + const mutation = useMutation< + NullResponse, + AxiosError, + { + query: { folderId: string; folderPermissionId: string; serverId: string }; + userId: string; + }, + undefined + >({ + mutationFn: ({ query }) => api.servers.deleteServerFolderPermission(query), + onSuccess: (_data, variables) => { + queryClient.invalidateQueries(queryKeys.users.detail(variables.userId)); + }, + }); + + return mutation; +}; diff --git a/src/renderer/features/servers/mutations/delete-server-permission.ts b/src/renderer/features/servers/mutations/delete-server-permission.ts new file mode 100644 index 000000000..0573e1895 --- /dev/null +++ b/src/renderer/features/servers/mutations/delete-server-permission.ts @@ -0,0 +1,23 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { api } from '@/renderer/api'; +import { queryKeys } from '@/renderer/api/query-keys'; +import { ApiError, NullResponse } from '@/renderer/api/types'; + +export const useDeleteServerPermission = () => { + const queryClient = useQueryClient(); + + const mutation = useMutation< + NullResponse, + AxiosError, + { query: { permissionId: string; serverId: string }; userId: string }, + undefined + >({ + mutationFn: ({ query }) => api.servers.deleteServerPermission(query), + onSuccess: (_data, variables) => { + queryClient.invalidateQueries(queryKeys.users.detail(variables.userId)); + }, + }); + + return mutation; +}; diff --git a/src/renderer/features/servers/mutations/update-server-permission.ts b/src/renderer/features/servers/mutations/update-server-permission.ts new file mode 100644 index 000000000..f7f285c39 --- /dev/null +++ b/src/renderer/features/servers/mutations/update-server-permission.ts @@ -0,0 +1,29 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { api } from '@/renderer/api'; +import { queryKeys } from '@/renderer/api/query-keys'; +import { UpdateServerPermissionBody } from '@/renderer/api/servers.api'; +import { ApiError, NullResponse } from '@/renderer/api/types'; + +export const useUpdateServerPermission = () => { + const queryClient = useQueryClient(); + + const mutation = useMutation< + NullResponse, + AxiosError, + { + body: UpdateServerPermissionBody; + query: { permissionId: string; serverId: string }; + userId: string; + }, + undefined + >({ + mutationFn: ({ query, body }) => + api.servers.updateServerPermission(query, body), + onSuccess: (_data, variables) => { + queryClient.invalidateQueries(queryKeys.users.detail(variables.userId)); + }, + }); + + return mutation; +}; diff --git a/src/renderer/features/shared/hooks/use-permissions.ts b/src/renderer/features/shared/hooks/use-permissions.ts index 74670560c..260355331 100644 --- a/src/renderer/features/shared/hooks/use-permissions.ts +++ b/src/renderer/features/shared/hooks/use-permissions.ts @@ -47,9 +47,15 @@ export const usePermissions = () => { : -1; }); + const isMusicServerAdmin = Object.keys(serverPermissions).some((key) => { + return serverPermissions[key] === ServerPermission.ADMIN; + }); + return { isAdmin: permissions.isAdmin || permissions.isSuperAdmin, + isMusicServerAdmin, isSuperAdmin: permissions.isSuperAdmin, + userId, ...serverPermissions, }; }, [ @@ -57,6 +63,7 @@ export const usePermissions = () => { permissions.isSuperAdmin, servers?.data, user?.data?.serverPermissions, + userId, ]); return permissionSet; diff --git a/src/renderer/features/titlebar/components/app-menu.tsx b/src/renderer/features/titlebar/components/app-menu.tsx index f65ffe84e..f680a924a 100644 --- a/src/renderer/features/titlebar/components/app-menu.tsx +++ b/src/renderer/features/titlebar/components/app-menu.tsx @@ -72,6 +72,7 @@ export const AppMenu = () => { ), exitTransitionDuration: 300, overflow: 'inside', + size: 'lg', title: 'Edit Profile', transition: 'slide-down', }); @@ -159,7 +160,7 @@ export const AppMenu = () => { > Edit profile - {permissions.isAdmin && ( + {(permissions.isAdmin || permissions.isMusicServerAdmin) && ( } onClick={handleManageUsersModal} diff --git a/src/renderer/features/users/components/edit-user-permissions-form.tsx b/src/renderer/features/users/components/edit-user-permissions-form.tsx new file mode 100644 index 000000000..464747b32 --- /dev/null +++ b/src/renderer/features/users/components/edit-user-permissions-form.tsx @@ -0,0 +1,272 @@ +import { ChangeEvent } from 'react'; +import { Stack, Group, Divider } from '@mantine/core'; +import { RiServerFill } from 'react-icons/ri'; +import { ServerPermissionType } from '@/renderer/api/types'; +import { + Accordion, + Button, + Select, + Switch, + Text, + toast, + Tooltip, +} from '@/renderer/components'; +import { + useCreateServerPermission, + useServerList, + useUpdateServerPermission, + useCreateServerFolderPermission, + useDeleteServerFolderPermission, + useDeleteServerPermission, +} from '@/renderer/features/servers'; +import { ServerPermission, usePermissions } from '@/renderer/features/shared'; +import { useUserDetail } from '@/renderer/features/users/queries/get-user-detail'; +import { titleCase } from '@/renderer/utils'; + +interface EditUserPermissionsFormProps { + onCancel: () => void; + userId: string; +} + +export const PERMISSION_TYPE_OPTIONS = [ + { label: 'None', value: '' }, + { label: 'Viewer', value: ServerPermissionType.VIEWER }, + { label: 'Editor', value: ServerPermissionType.EDITOR }, + { label: 'Editor', value: ServerPermissionType.EDITOR }, +]; + +export const EditUserPermissionsForm = ({ + userId, + onCancel, +}: EditUserPermissionsFormProps) => { + const permissions = usePermissions(); + const { data: servers } = useServerList(); + const { data: user } = useUserDetail({ userId }); + const createServerPermissionMutation = useCreateServerPermission(); + const deleteServerPermissionMutation = useDeleteServerPermission(); + const updateServerPermissionMutation = useUpdateServerPermission(); + const createServerFolderPermissionMutation = + useCreateServerFolderPermission(); + const deleteServerFolderPermissionMutation = + useDeleteServerFolderPermission(); + + const permissionTypeOptions = [ + { label: 'None', value: 'none' }, + { label: 'Viewer', value: ServerPermissionType.VIEWER }, + { label: 'Editor', value: ServerPermissionType.EDITOR }, + { + disabled: !permissions.isAdmin, + label: 'Admin', + value: ServerPermissionType.ADMIN, + }, + ]; + + return ( + + + {servers?.data?.map((s) => { + const currentServerPermission = user?.data?.serverPermissions?.find( + (p) => p.serverId === s.id + ); + + const isServerAdminEditingSelf = + permissions[s.id] >= ServerPermission.ADMIN && + user?.data.id === permissions.userId; + + const isServerAdminEditingOtherAdmin = + !permissions.isAdmin && + currentServerPermission?.type === ServerPermissionType.ADMIN; + + const isPermissionTypeDisabled = + isServerAdminEditingSelf || isServerAdminEditingOtherAdmin; + + const handleChangeServerPermission = async (e: string | null) => { + if (!e || !user) return; + + if (e === 'none' && currentServerPermission) { + deleteServerPermissionMutation.mutate( + { + query: { + permissionId: currentServerPermission.id, + serverId: s.id, + }, + userId: user.data.id, + }, + { + onError: (err) => + toast.error({ + message: err?.response?.data.error.message, + title: 'Error deleting folder permission', + }), + } + ); + } else if (currentServerPermission) { + updateServerPermissionMutation.mutate( + { + body: { + type: e as ServerPermissionType, + }, + query: { + permissionId: currentServerPermission.id, + serverId: s.id, + }, + userId: user.data.id, + }, + { + onError: (err) => + toast.error({ + message: err?.response?.data.error.message, + title: 'Error updating folder permission', + }), + } + ); + } else { + createServerPermissionMutation.mutate( + { + body: { + type: e as ServerPermissionType, + userId: user.data.id, + }, + query: { + serverId: s.id, + }, + }, + { + onError: (err) => + toast.error({ + message: err?.response?.data.error.message, + title: 'Error creating server permission', + }), + } + ); + } + }; + + return ( + + }> + + + {s.name} ({titleCase(s.type)}) + + + + + +