mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
Add server permission management
This commit is contained in:
@@ -194,7 +194,7 @@ const deleteServerUrl = async (
|
|||||||
id: urlId,
|
id: urlId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const success = ApiSuccess.noContent({ data: null });
|
const success = ApiSuccess.ok({ data: null });
|
||||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ const enableServerUrl = async (
|
|||||||
serverId,
|
serverId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const success = ApiSuccess.noContent({ data: null });
|
const success = ApiSuccess.ok({ data: null });
|
||||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ const disableServerUrl = async (
|
|||||||
) => {
|
) => {
|
||||||
await service.servers.disableUrlById(req.authUser);
|
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));
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -231,7 +231,7 @@ const deleteServerFolder = async (
|
|||||||
|
|
||||||
await service.servers.deleteFolderById({ id: folderId });
|
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));
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -243,7 +243,7 @@ const enableServerFolder = async (
|
|||||||
|
|
||||||
await service.servers.enableFolderById({ id: folderId });
|
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));
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -255,7 +255,7 @@ const disableServerFolder = async (
|
|||||||
|
|
||||||
await service.servers.disableFolderById({ id: folderId });
|
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));
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -286,7 +286,7 @@ const deleteServerPermission = async (
|
|||||||
id: permissionId,
|
id: permissionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const success = ApiSuccess.noContent({ data: null });
|
const success = ApiSuccess.ok({ data: null });
|
||||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@ const updateServerPermission = async (
|
|||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
|
|
||||||
const success = ApiSuccess.noContent({ data: null });
|
const success = ApiSuccess.ok({ data: null });
|
||||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -326,11 +326,11 @@ const deleteServerFolderPermission = async (
|
|||||||
req: TypedRequest<typeof validation.servers.deleteServerFolderPermission>,
|
req: TypedRequest<typeof validation.servers.deleteServerFolderPermission>,
|
||||||
res: Response
|
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));
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -111,11 +111,13 @@ router
|
|||||||
.route('/:serverId/permissions/:permissionId')
|
.route('/:serverId/permissions/:permissionId')
|
||||||
.patch(
|
.patch(
|
||||||
authenticateServerAdmin,
|
authenticateServerAdmin,
|
||||||
validateRequest(validation.servers.updateServerPermission)
|
validateRequest(validation.servers.updateServerPermission),
|
||||||
|
controller.servers.updateServerPermission
|
||||||
)
|
)
|
||||||
.delete(
|
.delete(
|
||||||
authenticateServerAdmin,
|
authenticateServerAdmin,
|
||||||
validateRequest(validation.servers.deleteServerPermission)
|
validateRequest(validation.servers.deleteServerPermission),
|
||||||
|
controller.servers.deleteServerPermission
|
||||||
);
|
);
|
||||||
|
|
||||||
router.param('folderId', async (_req, _res, next, folderId) => {
|
router.param('folderId', async (_req, _res, next, folderId) => {
|
||||||
@@ -149,9 +151,11 @@ router
|
|||||||
|
|
||||||
router
|
router
|
||||||
.route('/:serverId/folder/:folderId/permissions')
|
.route('/:serverId/folder/:folderId/permissions')
|
||||||
.post(authenticateServerAdmin);
|
.post(authenticateServerAdmin, controller.servers.addServerFolderPermission);
|
||||||
|
|
||||||
router
|
router
|
||||||
.route('/:serverId/folder/:folderId/permissions/:folderPermissionId')
|
.route('/:serverId/folder/:folderId/permissions/:folderPermissionId')
|
||||||
.patch(authenticateServerAdmin)
|
.delete(
|
||||||
.delete(authenticateServerAdmin);
|
authenticateServerAdmin,
|
||||||
|
controller.servers.deleteServerFolderPermission
|
||||||
|
);
|
||||||
|
|||||||
@@ -13,11 +13,7 @@ export const router: Router = express.Router({ mergeParams: true });
|
|||||||
|
|
||||||
router
|
router
|
||||||
.route('/')
|
.route('/')
|
||||||
.get(
|
.get(validateRequest(validation.users.list), controller.users.getUserList)
|
||||||
authenticateAdmin,
|
|
||||||
validateRequest(validation.users.list),
|
|
||||||
controller.users.getUserList
|
|
||||||
)
|
|
||||||
.post(
|
.post(
|
||||||
authenticateAdmin,
|
authenticateAdmin,
|
||||||
validateRequest(validation.users.createUser),
|
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
|
// If not admin, only show folders the user has permissions for
|
||||||
{ serverFolderPermissions: { some: { userId: user.id } } },
|
{ 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 findById = async (user: AuthUser, options: { id: string }) => {
|
||||||
const { id } = options;
|
const { id } = options;
|
||||||
|
|
||||||
if (!user.isAdmin && user.id !== id) {
|
// Possibly restrict detail later if additional sensitive user data is added
|
||||||
throw ApiError.forbidden();
|
// if (!user.isAdmin && user.id !== id) {
|
||||||
}
|
// throw ApiError.forbidden();
|
||||||
|
// }
|
||||||
|
|
||||||
const uniqueUser = await prisma.user.findUnique({
|
const uniqueUser = await prisma.user.findUnique({
|
||||||
include: {
|
include: {
|
||||||
@@ -33,9 +34,14 @@ const findById = async (user: AuthUser, options: { id: string }) => {
|
|||||||
|
|
||||||
const findMany = async () => {
|
const findMany = async () => {
|
||||||
const users = await prisma.user.findMany({
|
const users = await prisma.user.findMany({
|
||||||
include: { files: true },
|
include: {
|
||||||
|
files: true,
|
||||||
|
serverFolderPermissions: true,
|
||||||
|
serverPermissions: true,
|
||||||
|
},
|
||||||
orderBy: [{ isAdmin: SortOrder.DESC }, { username: SortOrder.ASC }],
|
orderBy: [{ isAdmin: SortOrder.DESC }, { username: SortOrder.ASC }],
|
||||||
});
|
});
|
||||||
|
|
||||||
return users;
|
return users;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
BaseResponse,
|
BaseResponse,
|
||||||
NullResponse,
|
NullResponse,
|
||||||
Server,
|
Server,
|
||||||
|
ServerPermissionType,
|
||||||
ServerType,
|
ServerType,
|
||||||
ServerUrl,
|
ServerUrl,
|
||||||
} from '@/renderer/api/types';
|
} from '@/renderer/api/types';
|
||||||
@@ -145,10 +146,91 @@ const fullScan = async (options: {
|
|||||||
return data;
|
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 = {
|
export const serversApi = {
|
||||||
createServer,
|
createServer,
|
||||||
|
createServerFolderPermission,
|
||||||
|
createServerPermission,
|
||||||
createUrl,
|
createUrl,
|
||||||
deleteServer,
|
deleteServer,
|
||||||
|
deleteServerFolderPermission,
|
||||||
|
deleteServerPermission,
|
||||||
deleteUrl,
|
deleteUrl,
|
||||||
disableFolder,
|
disableFolder,
|
||||||
disableUrl,
|
disableUrl,
|
||||||
@@ -158,4 +240,5 @@ export const serversApi = {
|
|||||||
getServerList,
|
getServerList,
|
||||||
quickScan,
|
quickScan,
|
||||||
updateServer,
|
updateServer,
|
||||||
|
updateServerPermission,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,3 +2,8 @@ export * from './mutations/use-create-server';
|
|||||||
export * from './components/add-server-form';
|
export * from './components/add-server-form';
|
||||||
export * from './components/server-list';
|
export * from './components/server-list';
|
||||||
export * from './queries/use-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;
|
: -1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isMusicServerAdmin = Object.keys(serverPermissions).some((key) => {
|
||||||
|
return serverPermissions[key] === ServerPermission.ADMIN;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isAdmin: permissions.isAdmin || permissions.isSuperAdmin,
|
isAdmin: permissions.isAdmin || permissions.isSuperAdmin,
|
||||||
|
isMusicServerAdmin,
|
||||||
isSuperAdmin: permissions.isSuperAdmin,
|
isSuperAdmin: permissions.isSuperAdmin,
|
||||||
|
userId,
|
||||||
...serverPermissions,
|
...serverPermissions,
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
@@ -57,6 +63,7 @@ export const usePermissions = () => {
|
|||||||
permissions.isSuperAdmin,
|
permissions.isSuperAdmin,
|
||||||
servers?.data,
|
servers?.data,
|
||||||
user?.data?.serverPermissions,
|
user?.data?.serverPermissions,
|
||||||
|
userId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return permissionSet;
|
return permissionSet;
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export const AppMenu = () => {
|
|||||||
),
|
),
|
||||||
exitTransitionDuration: 300,
|
exitTransitionDuration: 300,
|
||||||
overflow: 'inside',
|
overflow: 'inside',
|
||||||
|
size: 'lg',
|
||||||
title: 'Edit Profile',
|
title: 'Edit Profile',
|
||||||
transition: 'slide-down',
|
transition: 'slide-down',
|
||||||
});
|
});
|
||||||
@@ -159,7 +160,7 @@ export const AppMenu = () => {
|
|||||||
>
|
>
|
||||||
Edit profile
|
Edit profile
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{permissions.isAdmin && (
|
{(permissions.isAdmin || permissions.isMusicServerAdmin) && (
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
rightSection={<RiUserAddLine />}
|
rightSection={<RiUserAddLine />}
|
||||||
onClick={handleManageUsersModal}
|
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 { usePermissions } from '@/renderer/features/shared';
|
||||||
import { AddUserForm } from '@/renderer/features/users/components/add-user-form';
|
import { AddUserForm } from '@/renderer/features/users/components/add-user-form';
|
||||||
import { EditUserForm } from '@/renderer/features/users/components/edit-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 { useDeleteUser } from '../mutations/delete-user';
|
||||||
import { useUserList } from '../queries/get-user-list';
|
import { useUserList } from '../queries/get-user-list';
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ export const UserList = () => {
|
|||||||
modal: 'base',
|
modal: 'base',
|
||||||
overflow: 'inside',
|
overflow: 'inside',
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
title: `Edit User`,
|
title: `Edit User (${user.username})`,
|
||||||
transition: 'slide-down',
|
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 (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Group
|
<Group
|
||||||
@@ -103,41 +123,50 @@ export const UserList = () => {
|
|||||||
<React.Fragment key={u.id}>
|
<React.Fragment key={u.id}>
|
||||||
<Group
|
<Group
|
||||||
noWrap
|
noWrap
|
||||||
|
p={5}
|
||||||
position="apart"
|
position="apart"
|
||||||
sx={{
|
sx={{
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
background: 'rgba(125, 125, 125, 0.1)',
|
background: 'rgba(125, 125, 125, 0.1)',
|
||||||
},
|
},
|
||||||
|
transition: 'background 0.2s ease',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group>
|
<Group noWrap>
|
||||||
<Avatar radius="xl" src={u.avatarUrl} />
|
<Avatar radius="xl" src={u.avatarUrl} />
|
||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
<Text overflow="hidden">
|
<Text overflow="hidden" sx={{ maxWidth: '15rem' }}>
|
||||||
{u.username}
|
|
||||||
{(u.isSuperAdmin || u.isAdmin) && (
|
{(u.isSuperAdmin || u.isAdmin) && (
|
||||||
<Tooltip label={u.isSuperAdmin ? 'Super Admin' : 'Admin'}>
|
<Tooltip label={u.isSuperAdmin ? 'Super Admin' : 'Admin'}>
|
||||||
<span>
|
<span style={{ marginRight: '.5rem' }}>
|
||||||
<RiAdminLine />
|
<RiAdminLine />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
{u.username}
|
||||||
</Text>
|
</Text>
|
||||||
<Text $secondary size="xs">
|
<Text
|
||||||
|
$secondary
|
||||||
|
overflow="hidden"
|
||||||
|
size="xs"
|
||||||
|
sx={{ maxWidth: '15rem' }}
|
||||||
|
>
|
||||||
{u.displayName}
|
{u.displayName}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
<Group>
|
<Group noWrap>
|
||||||
|
{!u.isAdmin && (
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
disabled={!permissions.isAdmin}
|
disabled={u.isAdmin}
|
||||||
leftIcon={<RiEdit2Line />}
|
leftIcon={<RiEdit2Line />}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => handleEditUserModal(u)}
|
onClick={() => handleEdituserPermissionsModal(u)}
|
||||||
>
|
>
|
||||||
Edit
|
Permissions
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
<DropdownMenu position="bottom-start">
|
<DropdownMenu position="bottom-start">
|
||||||
<DropdownMenu.Target>
|
<DropdownMenu.Target>
|
||||||
<Button
|
<Button
|
||||||
@@ -149,6 +178,12 @@ export const UserList = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.Target>
|
</DropdownMenu.Target>
|
||||||
<DropdownMenu.Dropdown>
|
<DropdownMenu.Dropdown>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
rightSection={<RiEdit2Line />}
|
||||||
|
onClick={() => handleEditUserModal(u)}
|
||||||
|
>
|
||||||
|
Edit profile
|
||||||
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
rightSection={
|
rightSection={
|
||||||
<RiDeleteBin2Line color="var(--danger-color)" />
|
<RiDeleteBin2Line color="var(--danger-color)" />
|
||||||
|
|||||||
Reference in New Issue
Block a user