mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
Add server permission management
This commit is contained in:
@@ -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<typeof validation.servers.deleteServerFolderPermission>,
|
||||
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));
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 } },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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<NullResponse>(
|
||||
`/servers/${query.serverId}/permissions`,
|
||||
body
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
const deleteServerPermission = async (query: {
|
||||
permissionId: string;
|
||||
serverId: string;
|
||||
}) => {
|
||||
const { data } = await ax.delete<NullResponse>(
|
||||
`/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<NullResponse>(
|
||||
`/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<NullResponse>(
|
||||
`/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<NullResponse>(
|
||||
`/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,
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<ApiError>,
|
||||
{
|
||||
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;
|
||||
};
|
||||
@@ -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<ApiError>,
|
||||
{
|
||||
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;
|
||||
};
|
||||
@@ -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<ApiError>,
|
||||
{
|
||||
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;
|
||||
};
|
||||
@@ -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<ApiError>,
|
||||
{ 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;
|
||||
};
|
||||
@@ -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<ApiError>,
|
||||
{
|
||||
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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
</DropdownMenu.Item>
|
||||
{permissions.isAdmin && (
|
||||
{(permissions.isAdmin || permissions.isMusicServerAdmin) && (
|
||||
<DropdownMenu.Item
|
||||
rightSection={<RiUserAddLine />}
|
||||
onClick={handleManageUsersModal}
|
||||
|
||||
@@ -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 (
|
||||
<Stack m={5}>
|
||||
<Accordion variant="contained">
|
||||
{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 (
|
||||
<Accordion.Item key={`server-permission-${s.id}`} value={s.name}>
|
||||
<Accordion.Control icon={<RiServerFill />}>
|
||||
<Group>
|
||||
<Text>
|
||||
{s.name} ({titleCase(s.type)})
|
||||
</Text>
|
||||
</Group>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Stack>
|
||||
<Select
|
||||
data={permissionTypeOptions}
|
||||
defaultValue={currentServerPermission?.type || 'none'}
|
||||
disabled={isPermissionTypeDisabled}
|
||||
label="Permission Type"
|
||||
width={150}
|
||||
onChange={handleChangeServerPermission}
|
||||
/>
|
||||
<Group>
|
||||
<Tooltip label="Allows the user to trigger full scans and edit user permissions for this server">
|
||||
<span>
|
||||
<Text $secondary size="xs">
|
||||
Admin
|
||||
</Text>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label="Allows the user to trigger quick scans and edit server urls">
|
||||
<span>
|
||||
<Text $secondary size="xs">
|
||||
Editor
|
||||
</Text>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label="Allows the user to view the server">
|
||||
<span>
|
||||
<Text $secondary size="xs">
|
||||
Viewer
|
||||
</Text>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
<Divider my={5} />
|
||||
<Stack spacing={0}>
|
||||
<Text>Music Folders</Text>
|
||||
<Text $secondary size="xs">
|
||||
Server admins have access to all music folders by default.
|
||||
</Text>
|
||||
</Stack>
|
||||
{s.serverFolders?.map((f) => {
|
||||
const currentFolderPermission =
|
||||
user?.data.serverFolderPermissions?.find(
|
||||
(p) => p.serverFolderId === f.id
|
||||
);
|
||||
|
||||
const handleToggleMusicFolderPermission = async (
|
||||
e: ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (!user) return;
|
||||
const { checked } = e.target;
|
||||
const serverId = s.id;
|
||||
|
||||
if (checked) {
|
||||
createServerFolderPermissionMutation.mutate(
|
||||
{
|
||||
body: {
|
||||
userId: user.data.id,
|
||||
},
|
||||
query: {
|
||||
folderId: f.id,
|
||||
serverId,
|
||||
},
|
||||
},
|
||||
{
|
||||
onError: (err) =>
|
||||
toast.error({
|
||||
message: err?.response?.data.error.message,
|
||||
title: 'Error creating folder permission',
|
||||
}),
|
||||
}
|
||||
);
|
||||
} else if (currentFolderPermission) {
|
||||
deleteServerFolderPermissionMutation.mutate(
|
||||
{
|
||||
query: {
|
||||
folderId: f.id,
|
||||
folderPermissionId: currentFolderPermission.id,
|
||||
serverId,
|
||||
},
|
||||
userId: user.data.id,
|
||||
},
|
||||
{
|
||||
onError: (err) =>
|
||||
toast.error({
|
||||
message: err?.response?.data.error.message,
|
||||
title: 'Error removing folder permission',
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch
|
||||
key={`server-folder-permission-${f.id}`}
|
||||
defaultChecked={user?.data.serverFolderPermissions.some(
|
||||
(p) => p.serverFolderId === f.id
|
||||
)}
|
||||
disabled={isServerAdminEditingOtherAdmin}
|
||||
label={f.name}
|
||||
onChange={handleToggleMusicFolderPermission}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
<Group mt={10} position="right">
|
||||
<Button variant="subtle" onClick={onCancel}>
|
||||
Go back
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { usePermissions } from '@/renderer/features/shared';
|
||||
import { AddUserForm } from '@/renderer/features/users/components/add-user-form';
|
||||
import { EditUserForm } from '@/renderer/features/users/components/edit-user-form';
|
||||
import { EditUserPermissionsForm } from '@/renderer/features/users/components/edit-user-permissions-form';
|
||||
import { useDeleteUser } from '../mutations/delete-user';
|
||||
import { useUserList } from '../queries/get-user-list';
|
||||
|
||||
@@ -57,7 +58,7 @@ export const UserList = () => {
|
||||
modal: 'base',
|
||||
overflow: 'inside',
|
||||
size: 'lg',
|
||||
title: `Edit User`,
|
||||
title: `Edit User (${user.username})`,
|
||||
transition: 'slide-down',
|
||||
});
|
||||
};
|
||||
@@ -79,6 +80,25 @@ export const UserList = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleEdituserPermissionsModal = (user: User) => {
|
||||
openContextModal({
|
||||
centered: true,
|
||||
exitTransitionDuration: 300,
|
||||
innerProps: {
|
||||
modalBody: (vars: ContextModalVars) => (
|
||||
<EditUserPermissionsForm
|
||||
userId={user.id}
|
||||
onCancel={() => vars.context.closeModal(vars.id)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
modal: 'base',
|
||||
overflow: 'inside',
|
||||
title: `Edit Permissions (${user.username})`,
|
||||
transition: 'slide-down',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Group
|
||||
@@ -103,41 +123,50 @@ export const UserList = () => {
|
||||
<React.Fragment key={u.id}>
|
||||
<Group
|
||||
noWrap
|
||||
p={5}
|
||||
position="apart"
|
||||
sx={{
|
||||
'&:hover': {
|
||||
background: 'rgba(125, 125, 125, 0.1)',
|
||||
},
|
||||
transition: 'background 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<Group>
|
||||
<Group noWrap>
|
||||
<Avatar radius="xl" src={u.avatarUrl} />
|
||||
<Stack spacing="xs">
|
||||
<Text overflow="hidden">
|
||||
{u.username}
|
||||
<Text overflow="hidden" sx={{ maxWidth: '15rem' }}>
|
||||
{(u.isSuperAdmin || u.isAdmin) && (
|
||||
<Tooltip label={u.isSuperAdmin ? 'Super Admin' : 'Admin'}>
|
||||
<span>
|
||||
<span style={{ marginRight: '.5rem' }}>
|
||||
<RiAdminLine />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{u.username}
|
||||
</Text>
|
||||
<Text $secondary size="xs">
|
||||
<Text
|
||||
$secondary
|
||||
overflow="hidden"
|
||||
size="xs"
|
||||
sx={{ maxWidth: '15rem' }}
|
||||
>
|
||||
{u.displayName}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Group>
|
||||
<Button
|
||||
compact
|
||||
disabled={!permissions.isAdmin}
|
||||
leftIcon={<RiEdit2Line />}
|
||||
variant="subtle"
|
||||
onClick={() => handleEditUserModal(u)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Group noWrap>
|
||||
{!u.isAdmin && (
|
||||
<Button
|
||||
compact
|
||||
disabled={u.isAdmin}
|
||||
leftIcon={<RiEdit2Line />}
|
||||
variant="subtle"
|
||||
onClick={() => handleEdituserPermissionsModal(u)}
|
||||
>
|
||||
Permissions
|
||||
</Button>
|
||||
)}
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
@@ -149,6 +178,12 @@ export const UserList = () => {
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Item
|
||||
rightSection={<RiEdit2Line />}
|
||||
onClick={() => handleEditUserModal(u)}
|
||||
>
|
||||
Edit profile
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
rightSection={
|
||||
<RiDeleteBin2Line color="var(--danger-color)" />
|
||||
|
||||
Reference in New Issue
Block a user