mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
Add per-server permissions
This commit is contained in:
@@ -9,8 +9,8 @@ const getUserDetail = async (
|
|||||||
req: TypedRequest<typeof validation.users.detail>,
|
req: TypedRequest<typeof validation.users.detail>,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
const { id } = req.params;
|
const { userId } = req.params;
|
||||||
const user = await service.users.findById(req.authUser, { id });
|
const user = await service.users.findById(req.authUser, { id: userId });
|
||||||
const success = ApiSuccess.ok({ data: toApiModel.users([user])[0] });
|
const success = ApiSuccess.ok({ data: toApiModel.users([user])[0] });
|
||||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { ServerPermission, ServerPermissionType } from '@prisma/client';
|
||||||
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
|
||||||
|
export const authenticateServerAdmin = (
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
if (!req.params.serverId) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: {
|
||||||
|
message: 'Server id is required.',
|
||||||
|
path: req.path,
|
||||||
|
},
|
||||||
|
response: 'Error',
|
||||||
|
statusCode: 403,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.authUser.isAdmin || req.authUser.isSuperAdmin) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const permission = req.authUser.serverPermissions.find(
|
||||||
|
(p: ServerPermission) => p.serverId === req.params.serverId
|
||||||
|
)?.type;
|
||||||
|
|
||||||
|
if (permission !== ServerPermissionType.ADMIN) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: {
|
||||||
|
message: 'This action requires "Admin" server permissions.',
|
||||||
|
path: req.path,
|
||||||
|
},
|
||||||
|
response: 'Error',
|
||||||
|
statusCode: 403,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { ServerPermission, ServerPermissionType } from '@prisma/client';
|
||||||
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
|
||||||
|
export const authenticateServerEditor = (
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
if (!req.params.serverId) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: {
|
||||||
|
message: 'Server id is required.',
|
||||||
|
path: req.path,
|
||||||
|
},
|
||||||
|
response: 'Error',
|
||||||
|
statusCode: 403,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.authUser.isAdmin || req.authUser.isSuperAdmin) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const permission = req.authUser.serverPermissions.find(
|
||||||
|
(p: ServerPermission) => p.serverId === req.params.serverId
|
||||||
|
)?.type;
|
||||||
|
|
||||||
|
if (
|
||||||
|
permission !== ServerPermissionType.EDITOR &&
|
||||||
|
permission !== ServerPermissionType.ADMIN
|
||||||
|
) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: {
|
||||||
|
message: 'This action requires "Editor" server permissions.',
|
||||||
|
path: req.path,
|
||||||
|
},
|
||||||
|
response: 'Error',
|
||||||
|
statusCode: 403,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { ServerPermission, ServerPermissionType } from '@prisma/client';
|
||||||
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
|
||||||
|
export const authenticateServerViewer = (
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
if (!req.params.serverId) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: {
|
||||||
|
message: 'Server id is required.',
|
||||||
|
path: req.path,
|
||||||
|
},
|
||||||
|
response: 'Error',
|
||||||
|
statusCode: 403,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.authUser.isAdmin || req.authUser.isSuperAdmin) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const permission = req.authUser.serverPermissions.find(
|
||||||
|
(p: ServerPermission) => p.serverId === req.params.serverId
|
||||||
|
)?.type;
|
||||||
|
|
||||||
|
if (permission === undefined) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: {
|
||||||
|
message: 'This action requires "Viewer" server permissions.',
|
||||||
|
path: req.path,
|
||||||
|
},
|
||||||
|
response: 'Error',
|
||||||
|
statusCode: 403,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
|
||||||
|
export const authenticateSuperAdmin = (
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
if (!req.authUser.isSuperAdmin) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: {
|
||||||
|
message: 'This action requires an administrator account.',
|
||||||
|
path: req.path,
|
||||||
|
},
|
||||||
|
response: 'Error',
|
||||||
|
statusCode: 403,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
};
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
export * from './error-handler';
|
export * from './error-handler';
|
||||||
export * from './authenticate';
|
export * from './authenticate';
|
||||||
export * from './authenticate-admin';
|
export * from './authenticate-admin';
|
||||||
|
export * from './authenticate-super-admin';
|
||||||
|
export * from './authenticate-server-admin';
|
||||||
|
export * from './authenticate-server-editor';
|
||||||
|
export * from './authenticate-server-viewer';
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import express, { Router } from 'express';
|
import express, { Router } from 'express';
|
||||||
import { controller } from '@controllers/index';
|
import { controller } from '@controllers/index';
|
||||||
import { authenticateAdmin } from '@middleware/authenticate-admin';
|
import { authenticateAdmin } from '@middleware/authenticate-admin';
|
||||||
|
import { authenticateServerAdmin } from '@middleware/authenticate-server-admin';
|
||||||
|
import { authenticateServerEditor } from '@middleware/authenticate-server-editor';
|
||||||
|
import { authenticateServerViewer } from '@middleware/authenticate-server-viewer';
|
||||||
import { service } from '@services/index';
|
import { service } from '@services/index';
|
||||||
import { validateRequest, validation } from '@validations/index';
|
import { validateRequest, validation } from '@validations/index';
|
||||||
|
|
||||||
@@ -25,7 +28,7 @@ router
|
|||||||
controller.servers.getServerDetail
|
controller.servers.getServerDetail
|
||||||
)
|
)
|
||||||
.patch(
|
.patch(
|
||||||
authenticateAdmin,
|
authenticateServerAdmin,
|
||||||
validateRequest(validation.servers.update),
|
validateRequest(validation.servers.update),
|
||||||
controller.servers.updateServer
|
controller.servers.updateServer
|
||||||
)
|
)
|
||||||
@@ -38,7 +41,7 @@ router
|
|||||||
router
|
router
|
||||||
.route('/:serverId/refresh')
|
.route('/:serverId/refresh')
|
||||||
.get(
|
.get(
|
||||||
authenticateAdmin,
|
authenticateServerEditor,
|
||||||
validateRequest(validation.servers.refresh),
|
validateRequest(validation.servers.refresh),
|
||||||
controller.servers.refreshServer
|
controller.servers.refreshServer
|
||||||
);
|
);
|
||||||
@@ -46,23 +49,23 @@ router
|
|||||||
router
|
router
|
||||||
.route('/:serverId/scan')
|
.route('/:serverId/scan')
|
||||||
.post(
|
.post(
|
||||||
|
authenticateServerAdmin,
|
||||||
validateRequest(validation.servers.scan),
|
validateRequest(validation.servers.scan),
|
||||||
authenticateAdmin,
|
|
||||||
controller.servers.quickScanServer
|
controller.servers.quickScanServer
|
||||||
);
|
);
|
||||||
|
|
||||||
router
|
router
|
||||||
.route('/:serverId/full-scan')
|
.route('/:serverId/full-scan')
|
||||||
.post(
|
.post(
|
||||||
|
authenticateServerAdmin,
|
||||||
validateRequest(validation.servers.scan),
|
validateRequest(validation.servers.scan),
|
||||||
authenticateAdmin,
|
|
||||||
controller.servers.fullScanServer
|
controller.servers.fullScanServer
|
||||||
);
|
);
|
||||||
|
|
||||||
router
|
router
|
||||||
.route('/:serverId/url')
|
.route('/:serverId/url')
|
||||||
.post(
|
.post(
|
||||||
authenticateAdmin,
|
authenticateServerEditor,
|
||||||
validateRequest(validation.servers.createUrl),
|
validateRequest(validation.servers.createUrl),
|
||||||
controller.servers.createServerUrl
|
controller.servers.createServerUrl
|
||||||
);
|
);
|
||||||
@@ -75,7 +78,7 @@ router.param('urlId', async (_req, _res, next, urlId) => {
|
|||||||
router
|
router
|
||||||
.route('/:serverId/url/:urlId')
|
.route('/:serverId/url/:urlId')
|
||||||
.delete(
|
.delete(
|
||||||
authenticateAdmin,
|
authenticateServerEditor,
|
||||||
validateRequest(validation.servers.deleteUrl),
|
validateRequest(validation.servers.deleteUrl),
|
||||||
controller.servers.deleteServerUrl
|
controller.servers.deleteServerUrl
|
||||||
);
|
);
|
||||||
@@ -83,6 +86,7 @@ router
|
|||||||
router
|
router
|
||||||
.route('/:serverId/url/:urlId/enable')
|
.route('/:serverId/url/:urlId/enable')
|
||||||
.post(
|
.post(
|
||||||
|
authenticateServerViewer,
|
||||||
validateRequest(validation.servers.enableUrl),
|
validateRequest(validation.servers.enableUrl),
|
||||||
controller.servers.enableServerUrl
|
controller.servers.enableServerUrl
|
||||||
);
|
);
|
||||||
@@ -90,6 +94,7 @@ router
|
|||||||
router
|
router
|
||||||
.route('/:serverId/url/:urlId/disable')
|
.route('/:serverId/url/:urlId/disable')
|
||||||
.post(
|
.post(
|
||||||
|
authenticateServerViewer,
|
||||||
validateRequest(validation.servers.disableUrl),
|
validateRequest(validation.servers.disableUrl),
|
||||||
controller.servers.disableServerUrl
|
controller.servers.disableServerUrl
|
||||||
);
|
);
|
||||||
@@ -102,7 +107,7 @@ router.param('folderId', async (_req, _res, next, folderId) => {
|
|||||||
router
|
router
|
||||||
.route('/:serverId/folder/:folderId')
|
.route('/:serverId/folder/:folderId')
|
||||||
.delete(
|
.delete(
|
||||||
authenticateAdmin,
|
authenticateServerAdmin,
|
||||||
validateRequest(validation.servers.deleteFolder),
|
validateRequest(validation.servers.deleteFolder),
|
||||||
controller.servers.deleteServerFolder
|
controller.servers.deleteServerFolder
|
||||||
);
|
);
|
||||||
@@ -110,6 +115,7 @@ router
|
|||||||
router
|
router
|
||||||
.route('/:serverId/folder/:folderId/enable')
|
.route('/:serverId/folder/:folderId/enable')
|
||||||
.post(
|
.post(
|
||||||
|
authenticateServerAdmin,
|
||||||
validateRequest(validation.servers.enableFolder),
|
validateRequest(validation.servers.enableFolder),
|
||||||
controller.servers.enableServerFolder
|
controller.servers.enableServerFolder
|
||||||
);
|
);
|
||||||
@@ -117,6 +123,7 @@ router
|
|||||||
router
|
router
|
||||||
.route('/:serverId/folder/:folderId/disable')
|
.route('/:serverId/folder/:folderId/disable')
|
||||||
.post(
|
.post(
|
||||||
|
authenticateServerAdmin,
|
||||||
validateRequest(validation.servers.disableFolder),
|
validateRequest(validation.servers.disableFolder),
|
||||||
controller.servers.disableServerFolder
|
controller.servers.disableServerFolder
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const findById = async (user: AuthUser, options: { id: string }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uniqueUser = await prisma.user.findUnique({
|
const uniqueUser = await prisma.user.findUnique({
|
||||||
include: { serverFolderPermissions: true },
|
include: { serverFolderPermissions: true, serverPermissions: true },
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ const createUser = async (
|
|||||||
const createdUser = await prisma.user.create({
|
const createdUser = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
deviceId: `${username}_${randomString(10)}`,
|
deviceId: `${username}_${randomString(10)}`,
|
||||||
enabled: false,
|
enabled: true,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
username,
|
username,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const useLogin = (
|
|||||||
const props = {
|
const props = {
|
||||||
accessToken: res.data.accessToken,
|
accessToken: res.data.accessToken,
|
||||||
permissions: {
|
permissions: {
|
||||||
|
id: res.data.id,
|
||||||
isAdmin: res.data.isAdmin,
|
isAdmin: res.data.isAdmin,
|
||||||
isSuperAdmin: res.data.isSuperAdmin,
|
isSuperAdmin: res.data.isSuperAdmin,
|
||||||
username: res.data.username,
|
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 { useFullScan } from '@/renderer/features/servers/mutations/use-full-scan';
|
||||||
import { useQuickScan } from '@/renderer/features/servers/mutations/use-quick-scan';
|
import { useQuickScan } from '@/renderer/features/servers/mutations/use-quick-scan';
|
||||||
import { useUpdateServer } from '@/renderer/features/servers/mutations/use-update-server';
|
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 { useTaskList } from '@/renderer/features/tasks';
|
||||||
import { useAuthStore } from '@/renderer/store';
|
import { useAuthStore } from '@/renderer/store';
|
||||||
import { Font } from '@/renderer/styles';
|
import { Font } from '@/renderer/styles';
|
||||||
@@ -34,6 +34,10 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
|
|||||||
const [addCredential, addCredentialHandlers] = useDisclosure(false);
|
const [addCredential, addCredentialHandlers] = useDisclosure(false);
|
||||||
|
|
||||||
const permissions = usePermissions();
|
const permissions = usePermissions();
|
||||||
|
const serverPermission = permissions[
|
||||||
|
server.id as keyof typeof permissions
|
||||||
|
] as ServerPermission;
|
||||||
|
|
||||||
const updateServer = useUpdateServer();
|
const updateServer = useUpdateServer();
|
||||||
const enableServerUrl = useEnableServerUrl();
|
const enableServerUrl = useEnableServerUrl();
|
||||||
const disableServerUrl = useDisableServerUrl();
|
const disableServerUrl = useDisableServerUrl();
|
||||||
@@ -148,23 +152,27 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
|
|||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
<Text font={Font.EPILOGUE}>Server details</Text>
|
<Text font={Font.EPILOGUE}>Server details</Text>
|
||||||
<Group spacing="md">
|
<Group spacing="md">
|
||||||
<Button
|
{serverPermission >= ServerPermission.ADMIN && (
|
||||||
compact
|
<Button
|
||||||
disabled={isRunningTask}
|
compact
|
||||||
loading={fullScan.isLoading}
|
disabled={isRunningTask}
|
||||||
variant="subtle"
|
loading={fullScan.isLoading}
|
||||||
onClick={handleFullScan}
|
variant="subtle"
|
||||||
>
|
onClick={handleFullScan}
|
||||||
Full scan
|
>
|
||||||
</Button>
|
Full scan
|
||||||
<Button
|
</Button>
|
||||||
compact
|
)}
|
||||||
disabled={true || isRunningTask}
|
{serverPermission >= ServerPermission.EDITOR && (
|
||||||
variant="subtle"
|
<Button
|
||||||
onClick={handleQuickScan}
|
compact
|
||||||
>
|
disabled={true || isRunningTask}
|
||||||
Quick scan
|
variant="subtle"
|
||||||
</Button>
|
onClick={handleQuickScan}
|
||||||
|
>
|
||||||
|
Quick scan
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
@@ -179,50 +187,58 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
|
|||||||
<Group>
|
<Group>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text>URL</Text>
|
<Text>URL</Text>
|
||||||
<Text>Username</Text>
|
{serverPermission >= ServerPermission.EDITOR && (
|
||||||
|
<Text>Username</Text>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text size="sm">{server.url}</Text>
|
<Text size="sm">{server.url}</Text>
|
||||||
<Text size="sm">{server.username}</Text>
|
{serverPermission >= ServerPermission.EDITOR && (
|
||||||
|
<Text size="sm">{server.username}</Text>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
<Group>
|
{serverPermission >= ServerPermission.ADMIN && (
|
||||||
<Button
|
<Group>
|
||||||
disabled={!permissions.editServer}
|
<Button
|
||||||
tooltip={{ label: 'Edit server details' }}
|
tooltip={{ label: 'Edit server details' }}
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={() => editHandlers.toggle()}
|
onClick={() => editHandlers.toggle()}
|
||||||
>
|
>
|
||||||
<RiEdit2Fill color="white" />
|
<RiEdit2Fill color="white" />
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
</ServerSection>
|
</ServerSection>
|
||||||
|
|
||||||
<ServerSection title="Music Folders">
|
<ServerSection title="Music Folders">
|
||||||
<Stack>
|
<Stack>
|
||||||
{server.serverFolders?.map((folder) => (
|
{server.serverFolders?.map((folder) => (
|
||||||
<Group key={folder.id} position="apart">
|
<Group key={folder.id} position="apart">
|
||||||
|
<Text size="sm">{folder.name}</Text>
|
||||||
<Group>
|
<Group>
|
||||||
<Button
|
<>
|
||||||
compact
|
<Switch
|
||||||
disabled={true || !permissions.deleteServerFolder}
|
checked={folder.enabled}
|
||||||
variant="subtle"
|
disabled={serverPermission < ServerPermission.ADMIN}
|
||||||
>
|
onChange={(e) =>
|
||||||
<RiDeleteBin2Line color="var(--danger-color)" />
|
handleToggleFolder(folder.id, !e.currentTarget.checked)
|
||||||
</Button>
|
}
|
||||||
<Text size="sm">{folder.name}</Text>
|
/>
|
||||||
|
{serverPermission >= ServerPermission.ADMIN && (
|
||||||
|
<Button compact variant="subtle">
|
||||||
|
<RiDeleteBin2Line color="var(--danger-color)" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
</Group>
|
</Group>
|
||||||
<Switch
|
|
||||||
checked={folder.enabled}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleToggleFolder(folder.id, !e.currentTarget.checked)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</ServerSection>
|
</ServerSection>
|
||||||
|
|
||||||
<ServerSection title="Credentials">
|
<ServerSection title="Credentials">
|
||||||
{addCredential ? (
|
{addCredential ? (
|
||||||
<AddServerCredentialForm
|
<AddServerCredentialForm
|
||||||
@@ -234,32 +250,30 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
|
|||||||
<Stack>
|
<Stack>
|
||||||
{serverCredentials?.map((credential) => (
|
{serverCredentials?.map((credential) => (
|
||||||
<Group key={credential.id} position="apart">
|
<Group key={credential.id} position="apart">
|
||||||
|
<Text size="sm">{credential.username}</Text>
|
||||||
<Group>
|
<Group>
|
||||||
|
<Switch
|
||||||
|
checked={credential.enabled}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleToggleCredential(
|
||||||
|
credential.id,
|
||||||
|
!e.currentTarget.checked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
disabled={!permissions.deleteServerCredential}
|
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => handleDeleteCredential(credential.id)}
|
onClick={() => handleDeleteCredential(credential.id)}
|
||||||
>
|
>
|
||||||
<RiDeleteBin2Line color="var(--danger-color)" />
|
<RiDeleteBin2Line color="var(--danger-color)" />
|
||||||
</Button>
|
</Button>
|
||||||
<Text size="sm">{credential.username}</Text>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Switch
|
|
||||||
checked={credential.enabled}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleToggleCredential(
|
|
||||||
credential.id,
|
|
||||||
!e.currentTarget.checked
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
disabled={!permissions.createServerCredential}
|
|
||||||
mt={10}
|
mt={10}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => addCredentialHandlers.open()}
|
onClick={() => addCredentialHandlers.open()}
|
||||||
@@ -269,6 +283,7 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ServerSection>
|
</ServerSection>
|
||||||
|
|
||||||
<ServerSection title="URLs">
|
<ServerSection title="URLs">
|
||||||
{addUrl ? (
|
{addUrl ? (
|
||||||
<AddServerUrlForm
|
<AddServerUrlForm
|
||||||
@@ -280,60 +295,75 @@ export const ServerListItem = ({ server }: ServerListItemProps) => {
|
|||||||
<Stack>
|
<Stack>
|
||||||
{server.serverUrls?.map((serverUrl) => (
|
{server.serverUrls?.map((serverUrl) => (
|
||||||
<Group key={serverUrl.id} position="apart">
|
<Group key={serverUrl.id} position="apart">
|
||||||
|
<Text size="sm">{serverUrl.url}</Text>
|
||||||
<Group>
|
<Group>
|
||||||
<Button
|
<Switch
|
||||||
compact
|
checked={serverUrl.enabled}
|
||||||
disabled={!permissions.deleteServerUrl}
|
onChange={(e) =>
|
||||||
variant="subtle"
|
handleToggleUrl(
|
||||||
onClick={() => handleDeleteUrl(serverUrl.id)}
|
serverUrl.id,
|
||||||
>
|
!e.currentTarget.checked
|
||||||
<RiDeleteBin2Line color="var(--danger-color)" />
|
)
|
||||||
</Button>
|
}
|
||||||
<Text size="sm">{serverUrl.url}</Text>
|
/>
|
||||||
|
{serverPermission >= ServerPermission.EDITOR && (
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => handleDeleteUrl(serverUrl.id)}
|
||||||
|
>
|
||||||
|
<RiDeleteBin2Line color="var(--danger-color)" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
<Switch
|
|
||||||
checked={serverUrl.enabled}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleToggleUrl(serverUrl.id, !e.currentTarget.checked)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Button
|
{serverPermission >= ServerPermission.EDITOR && (
|
||||||
compact
|
<Button
|
||||||
disabled={!permissions.createServerUrl}
|
compact
|
||||||
mt={10}
|
mt={10}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => addUrlHandlers.open()}
|
onClick={() => addUrlHandlers.open()}
|
||||||
>
|
>
|
||||||
Add url
|
Add url
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ServerSection>
|
</ServerSection>
|
||||||
|
|
||||||
<ServerSection title="Danger zone">
|
{serverPermission >= ServerPermission.ADMIN && (
|
||||||
<Group position="apart">
|
<ServerSection title="Danger zone">
|
||||||
<Text size="sm">Require user credentials</Text>
|
<Group position="apart">
|
||||||
<Switch
|
<Text size="sm">Require user credentials</Text>
|
||||||
checked={server.noCredential}
|
<Switch
|
||||||
disabled={!permissions.isAdmin}
|
checked={server.noCredential}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
toggleRequiredCredential(e.currentTarget.checked)
|
toggleRequiredCredential(e.currentTarget.checked)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Divider my="xl" />
|
{permissions.isSuperAdmin && (
|
||||||
<Button
|
<>
|
||||||
compact
|
<Divider my="xl" />
|
||||||
disabled={!permissions.deleteServer}
|
<Button
|
||||||
leftIcon={<RiDeleteBin2Line color="var(--danger-color)" />}
|
compact
|
||||||
variant="default"
|
leftIcon={<RiDeleteBin2Line />}
|
||||||
>
|
size="xs"
|
||||||
Delete server
|
sx={{
|
||||||
</Button>
|
'&:hover': {
|
||||||
</ServerSection>
|
background: 'var(--danger-color)',
|
||||||
|
},
|
||||||
|
background: 'var(--danger-color)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete server
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ServerSection>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,27 +1,63 @@
|
|||||||
import { useMemo } from 'react';
|
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';
|
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 = () => {
|
export const usePermissions = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const userId = useAuthStore((state) => state.permissions.id);
|
||||||
const permissions = useAuthStore((state) => state.permissions);
|
const permissions = useAuthStore((state) => state.permissions);
|
||||||
|
|
||||||
const permissionSet = useMemo(() => {
|
const user = queryClient.getQueryData<UserDetailResponse>(
|
||||||
const set = {
|
queryKeys.users.detail(userId)
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
return set;
|
const servers = queryClient.getQueryData<ServerListResponse>(
|
||||||
}, [permissions]);
|
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;
|
return permissionSet;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { api } from '@/renderer/api';
|
import { api } from '@/renderer/api';
|
||||||
import { queryKeys } from '@/renderer/api/query-keys';
|
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 }) => {
|
export const useUserDetail = (
|
||||||
const { data, error, isLoading } = useQuery({
|
q: { userId: string },
|
||||||
queryFn: () => api.users.getUserDetail({ userId: options.userId }),
|
options?: QueryOptions<UserDetailResponse>
|
||||||
queryKey: queryKeys.users.detail(options.userId),
|
) => {
|
||||||
|
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 {
|
return query;
|
||||||
data,
|
|
||||||
error,
|
|
||||||
isLoading,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface AuthState {
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
currentServer: Server | null;
|
currentServer: Server | null;
|
||||||
permissions: {
|
permissions: {
|
||||||
|
id: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isSuperAdmin: boolean;
|
isSuperAdmin: boolean;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -91,11 +92,17 @@ export const useAuthStore = create<AuthSlice>()(
|
|||||||
logout: () => {
|
logout: () => {
|
||||||
return set({
|
return set({
|
||||||
accessToken: undefined,
|
accessToken: undefined,
|
||||||
permissions: { isAdmin: false, isSuperAdmin: false, username: '' },
|
permissions: {
|
||||||
|
id: '',
|
||||||
|
isAdmin: false,
|
||||||
|
isSuperAdmin: false,
|
||||||
|
username: '',
|
||||||
|
},
|
||||||
refreshToken: undefined,
|
refreshToken: undefined,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
permissions: {
|
permissions: {
|
||||||
|
id: '',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isSuperAdmin: false,
|
isSuperAdmin: false,
|
||||||
username: '',
|
username: '',
|
||||||
|
|||||||
Reference in New Issue
Block a user