feat: team permission refine (#4494) (#4498)

* feat: team permission refine (#4402)

* chore: team permission extend

* feat: manage team permission

* chore: api auth

* fix: i18n

* feat: add initv493

* fix: test, org auth manager

* test: app test for refined permission

* update init sh

* fix: add/remove manage permission (#4427)

* fix: add/remove manage permission

* fix: github action fastgpt-test

* fix: mock create model

* fix: team write permission

* fix: ts

* account permission

---------

Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
This commit is contained in:
Archer
2025-04-10 11:11:54 +08:00
committed by GitHub
parent 80f41dd2a9
commit 199f454b6b
51 changed files with 1116 additions and 460 deletions

View File

@@ -1,20 +1,24 @@
import React from 'react';
import { useTranslation } from 'next-i18next';
import { Box, Checkbox, HStack, VStack } from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import PermissionTags from './PermissionTags';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import OrgTags from '../../user/team/OrgTags';
import Tag from '@fastgpt/web/components/common/Tag';
function MemberItemCard({
avatar,
key,
onChange,
onChange: _onChange,
isChecked,
onDelete,
name,
permission,
orgs
orgs,
addOnly,
rightSlot
}: {
avatar: string;
key: string;
@@ -23,44 +27,66 @@ function MemberItemCard({
onDelete?: () => void;
name: string;
permission?: PermissionValueType;
addOnly?: boolean;
orgs?: string[];
rightSlot?: React.ReactNode;
}) {
const isAdded = addOnly && !!permission;
const onChange = () => {
if (!isAdded) _onChange();
};
const { t } = useTranslation();
return (
<>
<HStack
justifyContent="space-between"
alignItems="center"
key={key}
px="3"
py="2"
borderRadius="sm"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer'
}}
onClick={onChange}
>
{isChecked !== undefined && <Checkbox isChecked={isChecked} pointerEvents="none" />}
<Avatar src={avatar} w="1.5rem" borderRadius={'50%'} />
<HStack
justifyContent="space-between"
alignItems="center"
key={key}
px="3"
py="2"
borderRadius="sm"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer'
}}
onClick={onChange}
>
{isChecked !== undefined && (
<Checkbox isChecked={isChecked} pointerEvents="none" disabled={isAdded} />
)}
<Avatar src={avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full">
<Box fontSize={'sm'}>{name}</Box>
<Box lineHeight={1}>{orgs && orgs.length > 0 && <OrgTags orgs={orgs} />}</Box>
<Box w="full">
<Box fontSize={'sm'} className="textEllipsis" maxW="300px">
{name}
</Box>
{permission && <PermissionTags permission={permission} />}
{onDelete !== undefined && (
<MyIcon
name="common/closeLight"
w="1rem"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
onClick={onDelete}
/>
)}
</HStack>
</>
<Box lineHeight={1}>{orgs && orgs.length > 0 && <OrgTags orgs={orgs} />}</Box>
</Box>
{!isAdded && permission && <PermissionTags permission={permission} />}
{isAdded && (
<Tag
mixBlendMode={'multiply'}
colorSchema="blue"
border="none"
py={2}
px={3}
fontSize={'xs'}
>
{t('user:team.collaborator.added')}
</Tag>
)}
{onDelete !== undefined && (
<MyIcon
name="common/closeLight"
w="1rem"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
onClick={onDelete}
/>
)}
{rightSlot}
</HStack>
);
}

View File

@@ -1,17 +1,6 @@
import { useUserStore } from '@/web/support/user/useUserStore';
import { ChevronDownIcon } from '@chakra-ui/icons';
import {
Box,
Button,
Checkbox,
Flex,
Grid,
HStack,
ModalBody,
ModalFooter,
Tag,
Text
} from '@chakra-ui/react';
import { Box, Button, Flex, Grid, HStack, ModalBody, ModalFooter, Text } from '@chakra-ui/react';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import MyAvatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -19,27 +8,26 @@ import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import PermissionSelect from './PermissionSelect';
import PermissionTags from './PermissionTags';
import {
DEFAULT_ORG_AVATAR,
DEFAULT_TEAM_AVATAR,
DEFAULT_USER_AVATAR
} from '@fastgpt/global/common/system/constants';
import Path from '@/components/common/folder/Path';
import { OrgListItemType, OrgType } from '@fastgpt/global/support/user/team/org/type';
import { OrgListItemType } from '@fastgpt/global/support/user/team/org/type';
import { useContextSelector } from 'use-context-selector';
import { CollaboratorContext } from './context';
import { getTeamMembers } from '@/web/support/user/team/api';
import { getGroupList } from '@/web/support/user/team/group/api';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import MemberItemCard from './MemberItemCard';
import { GetSearchUserGroupOrg } from '@/web/support/user/api';
import useOrg from '@/web/support/user/team/org/hooks/useOrg';
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type';
import _ from 'lodash';
import { UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator';
import { ValueOf } from 'next/dist/shared/lib/constants';
const HoverBoxStyle = {
bgColor: 'myGray.50',
@@ -131,8 +119,8 @@ function MemberModal({
members: selectedMemberList.map((item) => item.tmbId),
groups: selectedGroupList.map((item) => item._id),
orgs: selectedOrgList.map((item) => item._id),
permission: selectedPermission!
}),
permission: addOnly ? undefined : selectedPermission!
} as UpdateClbPermissionProps<ValueOf<typeof addOnly>>),
{
successToast: t('common:common.Add Success'),
onSuccess() {
@@ -285,6 +273,7 @@ function MemberModal({
const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId);
return (
<MemberItemCard
addOnly={addOnly}
avatar={member.avatar}
key={member.tmbId}
name={member.memberName}
@@ -321,49 +310,33 @@ function MemberModal({
};
const collaborator = collaboratorList?.find((v) => v.orgId === org._id);
return (
<HStack
justifyContent="space-between"
<MemberItemCard
avatar={org.avatar}
key={org._id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isChecked={!!selectedOrgList.find((v) => v._id === org._id)}
pointerEvents="none"
/>
<MyAvatar src={org.avatar} w="1.5rem" borderRadius={'50%'} />
<HStack w="full">
<Text>{org.name}</Text>
{org.total && (
<>
<Tag size="sm" my="auto">
{org.total}
</Tag>
</>
)}
</HStack>
<PermissionTags permission={collaborator?.permission.value} />
{org.total && (
<MyIcon
name="core/chat/chevronRight"
w="16px"
p="4px"
rounded={'6px'}
_hover={{
bgColor: 'myGray.200'
}}
onClick={(e) => {
onClickOrg(org);
// setPath(getOrgChildrenPath(org));
e.stopPropagation();
}}
/>
)}
</HStack>
name={org.name}
onChange={onChange}
addOnly={addOnly}
permission={collaborator?.permission.value}
isChecked={!!selectedOrgList.find((v) => String(v._id) === String(org._id))}
rightSlot={
org.total && (
<MyIcon
name="core/chat/chevronRight"
w="16px"
p="4px"
rounded={'6px'}
_hover={{
bgColor: 'myGray.200'
}}
onClick={(e) => {
onClickOrg(org);
// setPath(getOrgChildrenPath(org));
e.stopPropagation();
}}
/>
)
}
/>
);
});
return searchKey ? (
@@ -372,6 +345,9 @@ function MemberModal({
<OrgMemberScrollData>
{Orgs}
{orgMembers.map((member) => {
const isChecked = !!selectedMemberList.find(
(v) => v.tmbId === member.tmbId
);
return (
<MemberItemCard
avatar={member.avatar}
@@ -385,7 +361,9 @@ function MemberModal({
return [...state, member];
});
}}
isChecked={!!selectedMemberList.find((v) => v.tmbId === member.tmbId)}
isChecked={isChecked}
permission={member.permission.value}
addOnly={addOnly && !!member.permission.value}
orgs={member.orgs}
/>
);
@@ -414,6 +392,7 @@ function MemberModal({
permission={collaborator?.permission.value}
onChange={onChange}
isChecked={!!selectedGroupList.find((v) => v._id === group._id)}
addOnly={addOnly}
/>
);
})}

View File

@@ -110,7 +110,15 @@ const CollaboratorContextProvider = ({
} = useRequest2(
async () => {
if (feConfigs.isPlus) {
return onGetCollaboratorList();
const data = await onGetCollaboratorList();
return data.map((item) => {
return {
...item,
permission: new Permission({
per: item.permission.value
})
};
});
}
return [];
},

View File

@@ -10,7 +10,14 @@ function OrgTags({ orgs, type = 'simple' }: { orgs?: string[]; type?: 'simple' |
label={
<VStack gap="1" alignItems={'start'}>
{orgs.map((org, index) => (
<Box key={index} fontSize="sm" fontWeight={400} color="myGray.500">
<Box
key={index}
fontSize="sm"
fontWeight={400}
color="myGray.500"
maxW={'300px'}
className="textEllipsis"
>
{org.slice(1)}
</Box>
))}

View File

@@ -91,7 +91,7 @@ const AccountContainer = ({
}
]
: []),
...(userInfo?.team?.permission.hasManagePer
...(userInfo?.team?.permission.hasApikeyCreatePer
? [
{
icon: 'key',

View File

@@ -27,6 +27,9 @@ import Avatar from '@fastgpt/web/components/common/Avatar';
import MemberTag from '../../../../components/support/user/team/Info/MemberTag';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import {
TeamApikeyCreatePermissionVal,
TeamAppCreatePermissionVal,
TeamDatasetCreatePermissionVal,
TeamManagePermissionVal,
TeamPermissionList,
TeamWritePermissionVal
@@ -42,6 +45,9 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { GetSearchUserGroupOrg } from '@/web/support/user/api';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator';
import { Permission } from '@fastgpt/global/support/permission/controller';
function PermissionManage({
Tabs,
@@ -104,19 +110,18 @@ function PermissionManage({
}, [collaboratorList, searchResult, searchKey]);
const { runAsync: onUpdatePermission, loading: addLoading } = useRequest2(
async ({ id, type, per }: { id: string; type: 'add' | 'remove'; per: 'write' | 'manage' }) => {
async ({ id, type, per }: { id: string; type: 'add' | 'remove'; per: PermissionValueType }) => {
const clb = collaboratorList.find(
(clb) => clb.tmbId === id || clb.groupId === id || clb.orgId === id
);
if (!clb) return;
const updatePer = per === 'write' ? TeamWritePermissionVal : TeamManagePermissionVal;
const permission = new TeamPermission({ per: clb.permission.value });
if (type === 'add') {
permission.addPer(updatePer);
permission.addPer(per);
} else {
permission.removePer(updatePer);
permission.removePer(per);
}
return onUpdateCollaborators({
@@ -132,12 +137,48 @@ function PermissionManage({
useRequest2(onDelOneCollaborator);
const userManage = userInfo?.permission.hasManagePer;
const hasDeletePer = (per: TeamPermission) => {
const hasDeletePer = (per: Permission) => {
if (userInfo?.permission.isOwner) return true;
if (userManage && !per.hasManagePer) return true;
return false;
};
function PermissionCheckBox({
isDisabled,
per,
clbPer,
id
}: {
isDisabled: boolean;
per: PermissionValueType;
clbPer: Permission;
id: string;
}) {
return (
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={isDisabled}
isChecked={clbPer.checkPer(per)}
onChange={(e) =>
e.target.checked
? onUpdatePermission({
id,
type: 'add',
per
})
: onUpdatePermission({
id,
type: 'remove',
per
})
}
/>
</Box>
</Td>
);
}
return (
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
@@ -174,13 +215,26 @@ function PermissionManage({
</Th>
<Th bg="myGray.100">
<Box mx="auto" w="fit-content">
{t('user:team.group.permission.write')}
{t('account_team:permission_appCreate')}
<QuestionTip ml="1" label={t('account_team:permission_appCreate_tip')} />
</Box>
</Th>
<Th bg="myGray.100">
<Box mx="auto" w="fit-content">
{t('user:team.group.permission.manage')}
<QuestionTip ml="1" label={t('user:team.group.manage_tip')} />
{t('account_team:permission_datasetCreate')}
<QuestionTip ml="1" label={t('account_team:permission_datasetCreate_Tip')} />
</Box>
</Th>
<Th bg="myGray.100">
<Box mx="auto" w="fit-content">
{t('account_team:permission_apikeyCreate')}
<QuestionTip ml="1" label={t('account_team:permission_apikeyCreate_Tip')} />
</Box>
</Th>
<Th bg="myGray.100">
<Box mx="auto" w="fit-content">
{t('account_team:permission_manage')}
<QuestionTip ml="1" label={t('account_team:permission_manage_tip')} />
</Box>
</Th>
<Th bg="myGray.100" borderRightRadius="md">
@@ -210,48 +264,30 @@ function PermissionManage({
<Box>{member.name}</Box>
</HStack>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={member.permission.isOwner || !userManage}
isChecked={member.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({
id: member.tmbId!,
type: 'add',
per: 'write'
})
: onUpdatePermission({
id: member.tmbId!,
type: 'remove',
per: 'write'
})
}
/>
</Box>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={member.permission.isOwner || !userInfo?.permission.isOwner}
isChecked={member.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({
id: member.tmbId!,
type: 'add',
per: 'manage'
})
: onUpdatePermission({
id: member.tmbId!,
type: 'remove',
per: 'manage'
})
}
/>
</Box>
</Td>
<PermissionCheckBox
isDisabled={member.permission.isOwner || !userManage}
per={TeamAppCreatePermissionVal}
clbPer={member.permission}
id={member.tmbId!}
/>
<PermissionCheckBox
isDisabled={member.permission.isOwner || !userManage}
per={TeamDatasetCreatePermissionVal}
clbPer={member.permission}
id={member.tmbId!}
/>
<PermissionCheckBox
isDisabled={member.permission.isOwner || !userManage}
per={TeamApikeyCreatePermissionVal}
clbPer={member.permission}
id={member.tmbId!}
/>
<PermissionCheckBox
isDisabled={member.permission.isOwner || !userInfo?.permission.isOwner}
per={TeamManagePermissionVal}
clbPer={member.permission}
id={member.tmbId!}
/>
<Td>
{hasDeletePer(member.permission) &&
userInfo?.team.tmbId !== member.tmbId && (
@@ -268,7 +304,6 @@ function PermissionManage({
</Tr>
))}
</>
<>
<Tr borderBottom={'1px solid'} borderColor={'myGray.200'} />
<Tr userSelect={'none'}>
@@ -286,40 +321,30 @@ function PermissionManage({
<Td pl={10}>
<MemberTag name={org.name} avatar={org.avatar} />
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userManage}
isChecked={org.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({ id: org.orgId!, type: 'add', per: 'write' })
: onUpdatePermission({
id: org.orgId!,
type: 'remove',
per: 'write'
})
}
/>
</Box>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userInfo?.permission.isOwner}
isChecked={org.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({ id: org.orgId!, type: 'add', per: 'manage' })
: onUpdatePermission({
id: org.orgId!,
type: 'remove',
per: 'manage'
})
}
/>
</Box>
</Td>
<PermissionCheckBox
isDisabled={org.permission.isOwner || !userManage}
per={TeamAppCreatePermissionVal}
clbPer={org.permission}
id={org.orgId!}
/>
<PermissionCheckBox
isDisabled={org.permission.isOwner || !userManage}
per={TeamDatasetCreatePermissionVal}
clbPer={org.permission}
id={org.orgId!}
/>
<PermissionCheckBox
isDisabled={org.permission.isOwner || !userManage}
per={TeamApikeyCreatePermissionVal}
clbPer={org.permission}
id={org.orgId!}
/>
<PermissionCheckBox
isDisabled={org.permission.isOwner || !userInfo?.permission.isOwner}
per={TeamManagePermissionVal}
clbPer={org.permission}
id={org.orgId!}
/>
<Td>
{hasDeletePer(org.permission) && (
<Box mx="auto" w="fit-content">
@@ -358,48 +383,30 @@ function PermissionManage({
avatar={group.avatar}
/>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userManage}
isChecked={group.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({
id: group.groupId!,
type: 'add',
per: 'write'
})
: onUpdatePermission({
id: group.groupId!,
type: 'remove',
per: 'write'
})
}
/>
</Box>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userInfo?.permission.isOwner}
isChecked={group.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({
id: group.groupId!,
type: 'add',
per: 'manage'
})
: onUpdatePermission({
id: group.groupId!,
type: 'remove',
per: 'manage'
})
}
/>
</Box>
</Td>
<PermissionCheckBox
isDisabled={group.permission.isOwner || !userManage}
per={TeamAppCreatePermissionVal}
clbPer={group.permission}
id={group.groupId!}
/>
<PermissionCheckBox
isDisabled={group.permission.isOwner || !userManage}
per={TeamDatasetCreatePermissionVal}
clbPer={group.permission}
id={group.groupId!}
/>
<PermissionCheckBox
isDisabled={group.permission.isOwner || !userManage}
per={TeamApikeyCreatePermissionVal}
clbPer={group.permission}
id={group.groupId!}
/>
<PermissionCheckBox
isDisabled={group.permission.isOwner || !userInfo?.permission.isOwner}
per={TeamManagePermissionVal}
clbPer={group.permission}
id={group.groupId!}
/>
<Td>
{hasDeletePer(group.permission) && (
<Box mx="auto" w="fit-content">

View File

@@ -36,6 +36,7 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import UserBox from '@fastgpt/web/components/common/UserBox';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
const HttpEditModal = dynamic(() => import('./HttpPluginEditModal'));
const ListItem = () => {
@@ -429,7 +430,7 @@ const ListItem = () => {
members?: string[];
groups?: string[];
orgs?: string[];
permission: number;
permission: PermissionValueType;
}) =>
postUpdateAppCollaborators({
...props,

View File

@@ -4,6 +4,7 @@ import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { onCreateApp } from './create';
import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant';
export type copyAppQuery = {};
@@ -17,19 +18,16 @@ async function handler(
req: ApiRequestProps<copyAppBody, copyAppQuery>,
res: ApiResponseType<any>
): Promise<copyAppResponse> {
const [{ app, tmbId }] = await Promise.all([
authApp({
req,
authToken: true,
per: WritePermissionVal,
appId: req.body.appId
}),
authUserPer({
req,
authToken: true,
per: WritePermissionVal
})
]);
const { app } = await authApp({
req,
authToken: true,
per: WritePermissionVal,
appId: req.body.appId
});
const { tmbId } = app.parentId
? await authApp({ req, appId: app.parentId, per: TeamAppCreatePermissionVal, authToken: true })
: await authUserPer({ req, authToken: true, per: TeamAppCreatePermissionVal });
const appId = await onCreateApp({
parentId: app.parentId,

View File

@@ -17,6 +17,7 @@ import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller';
import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant';
export type CreateAppBody = {
parentId?: ParentIdType;
@@ -36,18 +37,15 @@ async function handler(req: ApiRequestProps<CreateAppBody>) {
}
// 凭证校验
const [{ teamId, tmbId, userId }] = await Promise.all([
authUserPer({ req, authToken: true, per: WritePermissionVal }),
...(parentId
? [authApp({ req, appId: parentId, per: WritePermissionVal, authToken: true })]
: [])
]);
const { teamId, tmbId, userId } = parentId
? await authApp({ req, appId: parentId, per: TeamAppCreatePermissionVal, authToken: true })
: await authUserPer({ req, authToken: true, per: TeamAppCreatePermissionVal });
// 上限校验
await checkTeamAppLimit(teamId);
const tmb = await MongoTeamMember.findById({ _id: tmbId }, 'userId').populate<{
user: { avatar: string; username: string };
}>('user', 'avatar username');
user: { username: string };
}>('user', 'username');
// 创建app
const appId = await onCreateApp({
@@ -60,7 +58,7 @@ async function handler(req: ApiRequestProps<CreateAppBody>) {
chatConfig,
teamId,
tmbId,
userAvatar: tmb?.user?.avatar,
userAvatar: tmb?.avatar,
username: tmb?.user?.username
});

View File

@@ -16,7 +16,7 @@ import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission';
import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller';
import { TeamWritePermissionVal } from '@fastgpt/global/support/permission/user/constant';
import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
export type CreateAppFolderBody = {
@@ -33,21 +33,9 @@ async function handler(req: ApiRequestProps<CreateAppFolderBody>) {
}
// 凭证校验
const { teamId, tmbId } = await authUserPer({
req,
authToken: true,
per: TeamWritePermissionVal
});
if (parentId) {
// if it is not a root folder
await authApp({
req,
appId: parentId,
per: WritePermissionVal,
authToken: true
});
}
const { teamId, tmbId } = parentId
? await authApp({ req, appId: parentId, per: TeamAppCreatePermissionVal, authToken: true })
: await authUserPer({ req, authToken: true, per: TeamAppCreatePermissionVal });
// Create app
await mongoSessionRun(async (session) => {

View File

@@ -9,6 +9,8 @@ import { onCreateApp, type CreateAppBody } from '../create';
import { AppSchema } from '@fastgpt/global/core/app/type';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant';
export type createHttpPluginQuery = {};
@@ -29,11 +31,9 @@ async function handler(
return Promise.reject('缺少参数');
}
const { teamId, tmbId, userId } = await authUserPer({
req,
authToken: true,
per: WritePermissionVal
});
const { teamId, tmbId, userId } = parentId
? await authApp({ req, appId: parentId, per: TeamAppCreatePermissionVal, authToken: true })
: await authUserPer({ req, authToken: true, per: TeamAppCreatePermissionVal });
await mongoSessionRun(async (session) => {
// create http plugin folder

View File

@@ -20,7 +20,7 @@ import { ClientSession } from 'mongoose';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { TeamWritePermissionVal } from '@fastgpt/global/support/permission/user/constant';
import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
@@ -79,7 +79,7 @@ async function handler(req: ApiRequestProps<AppUpdateBody, AppUpdateQuery>) {
await authUserPer({
req,
authToken: true,
per: TeamWritePermissionVal
per: TeamAppCreatePermissionVal
});
}
} else {

View File

@@ -6,11 +6,9 @@ import {
getLLMModel,
getEmbeddingModel,
getDatasetModel,
getDefaultEmbeddingModel,
getVlmModel
getDefaultEmbeddingModel
} from '@fastgpt/service/core/ai/model';
import { checkTeamDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
@@ -18,6 +16,7 @@ import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller';
import { TeamDatasetCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant';
export type DatasetCreateQuery = {};
export type DatasetCreateBody = CreateDatasetParams;
@@ -41,25 +40,20 @@ async function handler(
} = req.body;
// auth
const [{ teamId, tmbId, userId }] = await Promise.all([
authUserPer({
req,
authToken: true,
authApiKey: true,
per: WritePermissionVal
}),
...(parentId
? [
authDataset({
req,
datasetId: parentId,
authToken: true,
authApiKey: true,
per: WritePermissionVal
})
]
: [])
]);
const { teamId, tmbId, userId } = parentId
? await authDataset({
req,
datasetId: parentId,
authToken: true,
authApiKey: true,
per: TeamDatasetCreatePermissionVal
})
: await authUserPer({
req,
authToken: true,
authApiKey: true,
per: TeamDatasetCreatePermissionVal
});
// check model valid
const vectorModelStore = getEmbeddingModel(vectorModel);

View File

@@ -5,8 +5,7 @@ import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import {
OwnerPermissionVal,
PerResourceTypeEnum,
WritePermissionVal
PerResourceTypeEnum
} from '@fastgpt/global/support/permission/constant';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
@@ -16,6 +15,7 @@ import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller';
import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { TeamDatasetCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant';
export type DatasetFolderCreateQuery = {};
export type DatasetFolderCreateBody = {
parentId?: string;
@@ -33,20 +33,20 @@ async function handler(
return Promise.reject(CommonErrEnum.missingParams);
}
const { tmbId, teamId } = await authUserPer({
req,
per: WritePermissionVal,
authToken: true
});
if (parentId) {
await authDataset({
datasetId: parentId,
per: WritePermissionVal,
req,
authToken: true
});
}
const { teamId, tmbId } = parentId
? await authDataset({
req,
datasetId: parentId,
authToken: true,
authApiKey: true,
per: TeamDatasetCreatePermissionVal
})
: await authUserPer({
req,
authToken: true,
authApiKey: true,
per: TeamDatasetCreatePermissionVal
});
await mongoSessionRun(async (session) => {
const dataset = await MongoDataset.create({

View File

@@ -23,7 +23,7 @@ import {
syncCollaborators
} from '@fastgpt/service/support/permission/inheritPermission';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { TeamWritePermissionVal } from '@fastgpt/global/support/permission/user/constant';
import { TeamDatasetCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant';
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
import { MongoDatasetTraining } from '@fastgpt/service/core/dataset/training/schema';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
@@ -104,7 +104,7 @@ async function handler(
await authUserPer({
req,
authToken: true,
per: TeamWritePermissionVal
per: TeamDatasetCreatePermissionVal
});
}
} else {

View File

@@ -4,12 +4,10 @@ import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import {
ManagePermissionVal,
WritePermissionVal
} from '@fastgpt/global/support/permission/constant';
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { OpenApiErrEnum } from '@fastgpt/global/common/error/code/openapi';
import { TeamApikeyCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant';
async function handler(req: ApiRequestProps<EditApiKeyProps>): Promise<string> {
const { appId, name, limit } = req.body;
@@ -19,7 +17,7 @@ async function handler(req: ApiRequestProps<EditApiKeyProps>): Promise<string> {
const { teamId, tmbId } = await authUserPer({
req,
authToken: true,
per: WritePermissionVal
per: TeamApikeyCreatePermissionVal
});
return { teamId, tmbId };
} else {

View File

@@ -31,6 +31,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import TemplateMarketModal from '@/pageComponents/app/list/TemplateMarketModal';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import JsonImportModal from '@/pageComponents/app/list/JsonImportModal';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
const CreateModal = dynamic(() => import('@/pageComponents/app/list/CreateModal'));
const EditFolderModal = dynamic(
@@ -213,7 +214,7 @@ const MyApps = () => {
{(folderDetail
? folderDetail.permission.hasWritePer && folderDetail?.type !== AppTypeEnum.httpPlugin
: userInfo?.team.permission.hasWritePer) && (
: userInfo?.team.permission.hasAppCreatePer) && (
<MyMenu
size="md"
Button={
@@ -327,7 +328,7 @@ const MyApps = () => {
}: {
members?: string[];
groups?: string[];
permission: number;
permission: PermissionValueType;
}) => {
return postUpdateAppCollaborators({
members,

View File

@@ -29,6 +29,7 @@ import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
const EditFolderModal = dynamic(
() => import('@fastgpt/web/components/common/MyModal/EditFolderModal')
@@ -138,7 +139,7 @@ const Dataset = () => {
{(folderDetail
? folderDetail.permission.hasWritePer
: userInfo?.team?.permission.hasWritePer) && (
: userInfo?.team?.permission.hasDatasetCreatePer) && (
<Box pl={[0, 4]}>
<MyMenu
size="md"
@@ -248,7 +249,7 @@ const Dataset = () => {
}: {
members?: string[];
groups?: string[];
permission: number;
permission: PermissionValueType;
}) =>
postUpdateDatasetCollaborators({
members,

View File

@@ -0,0 +1,57 @@
import * as createapi from '@/pages/api/core/app/create';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { getFakeUsers } from '@test/datas/users';
import { Call } from '@test/utils/request';
import { expect, it, describe } from 'vitest';
describe('create api', () => {
it('should return 200 when create app success', async () => {
const users = await getFakeUsers(2);
await MongoResourcePermission.create({
resourceType: 'team',
teamId: users.members[0].teamId,
resourceId: null,
tmbId: users.members[0].tmbId,
permission: TeamAppCreatePermissionVal
});
const res = await Call<createapi.CreateAppBody, {}, {}>(createapi.default, {
auth: users.members[0],
body: {
modules: [],
name: 'testfolder',
type: AppTypeEnum.folder
}
});
expect(res.error).toBeUndefined();
expect(res.code).toBe(200);
const folderId = res.data as string;
const res2 = await Call<createapi.CreateAppBody, {}, {}>(createapi.default, {
auth: users.members[0],
body: {
modules: [],
name: 'testapp',
type: AppTypeEnum.simple,
parentId: String(folderId)
}
});
expect(res2.error).toBeUndefined();
expect(res2.code).toBe(200);
expect(res2.data).toBeDefined();
const res3 = await Call<createapi.CreateAppBody, {}, {}>(createapi.default, {
auth: users.members[1],
body: {
modules: [],
name: 'testapp',
type: AppTypeEnum.simple,
parentId: String(folderId)
}
});
expect(res3.error).toBe(AppErrEnum.unAuthApp);
expect(res3.code).toBe(500);
});
});

View File

@@ -0,0 +1,38 @@
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
import { getRootUser } from '@test/datas/users';
import { Call } from '@test/utils/request';
import { describe, expect, it } from 'vitest';
import handler, {
type versionListBody,
type versionListResponse
} from '@/pages/api/core/app/version/list';
describe('app version list test', () => {
it('should return app version list', async () => {
const root = await getRootUser();
const app = await MongoApp.create({
name: 'test',
tmbId: root.tmbId,
teamId: root.teamId
});
await MongoAppVersion.create(
[...Array(10).keys()].map((i) => ({
tmbId: root.tmbId,
appId: app._id,
versionName: `v${i}`
}))
);
const res = await Call<versionListBody, {}, versionListResponse>(handler, {
auth: root,
body: {
pageSize: 10,
offset: 0,
appId: app._id
}
});
expect(res.code).toBe(200);
expect(res.data.total).toBe(10);
expect(res.data.list.length).toBe(10);
});
});

View File

@@ -0,0 +1,87 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getDatasetCollectionPaths } from '@/pages/api/core/dataset/collection/paths';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
vi.mock('@fastgpt/service/core/dataset/collection/schema', () => ({
MongoDatasetCollection: {
findOne: vi.fn()
},
DatasetColCollectionName: 'dataset_collections'
}));
describe('getDatasetCollectionPaths', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should return empty array for empty parentId', async () => {
const result = await getDatasetCollectionPaths({});
expect(result).toEqual([]);
});
it('should return empty array if collection not found', async () => {
vi.mocked(MongoDatasetCollection.findOne).mockResolvedValueOnce(null);
const result = await getDatasetCollectionPaths({ parentId: 'nonexistent-id' });
expect(result).toEqual([]);
});
it('should return single path for collection without parent', async () => {
vi.mocked(MongoDatasetCollection.findOne).mockResolvedValueOnce({
_id: 'col1',
name: 'Collection 1',
parentId: ''
});
const result = await getDatasetCollectionPaths({ parentId: 'col1' });
expect(result).toEqual([{ parentId: 'col1', parentName: 'Collection 1' }]);
});
it('should return full path for nested collections', async () => {
vi.mocked(MongoDatasetCollection.findOne)
.mockResolvedValueOnce({
_id: 'col3',
name: 'Collection 3',
parentId: 'col2'
})
.mockResolvedValueOnce({
_id: 'col2',
name: 'Collection 2',
parentId: 'col1'
})
.mockResolvedValueOnce({
_id: 'col1',
name: 'Collection 1',
parentId: ''
});
const result = await getDatasetCollectionPaths({ parentId: 'col3' });
expect(result).toEqual([
{ parentId: 'col1', parentName: 'Collection 1' },
{ parentId: 'col2', parentName: 'Collection 2' },
{ parentId: 'col3', parentName: 'Collection 3' }
]);
});
it('should handle circular references gracefully', async () => {
vi.mocked(MongoDatasetCollection.findOne)
.mockResolvedValueOnce({
_id: 'col1',
name: 'Collection 1',
parentId: 'col2'
})
.mockResolvedValueOnce({
_id: 'col2',
name: 'Collection 2',
parentId: 'col1'
});
const result = await getDatasetCollectionPaths({ parentId: 'col1' });
expect(result).toEqual([
{ parentId: 'col2', parentName: 'Collection 2' },
{ parentId: 'col1', parentName: 'Collection 1' }
]);
});
});

View File

@@ -0,0 +1,54 @@
import * as createapi from '@/pages/api/core/dataset/create';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { TeamDatasetCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { getFakeUsers } from '@test/datas/users';
import { Call } from '@test/utils/request';
import { vi, describe, it, expect } from 'vitest';
describe('create dataset', () => {
it('should return 200 when create dataset success', async () => {
const users = await getFakeUsers(2);
await MongoResourcePermission.create({
resourceType: 'team',
teamId: users.members[0].teamId,
resourceId: null,
tmbId: users.members[0].tmbId,
permission: TeamDatasetCreatePermissionVal
});
const res = await Call<
createapi.DatasetCreateBody,
createapi.DatasetCreateQuery,
createapi.DatasetCreateResponse
>(createapi.default, {
auth: users.members[0],
body: {
name: 'folder',
intro: 'intro',
avatar: 'avatar',
type: DatasetTypeEnum.folder
}
});
expect(res.error).toBeUndefined();
expect(res.code).toBe(200);
const folderId = res.data as string;
const res2 = await Call<
createapi.DatasetCreateBody,
createapi.DatasetCreateQuery,
createapi.DatasetCreateResponse
>(createapi.default, {
auth: users.members[0],
body: {
name: 'test',
intro: 'intro',
avatar: 'avatar',
type: DatasetTypeEnum.dataset,
parentId: folderId
}
});
expect(res2.error).toBeUndefined();
expect(res2.code).toBe(200);
});
});

View File

@@ -0,0 +1,83 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getParents } from '@/pages/api/core/dataset/paths';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
vi.mock('@fastgpt/service/core/dataset/schema', () => ({
MongoDataset: {
findById: vi.fn()
},
ChunkSettings: {},
DatasetCollectionName: 'datasets'
}));
describe('getParents', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should return empty array if parentId is undefined', async () => {
const result = await getParents(undefined);
expect(result).toEqual([]);
});
it('should return empty array if parent not found', async () => {
vi.mocked(MongoDataset.findById).mockResolvedValueOnce(null);
const result = await getParents('non-existent-id');
expect(result).toEqual([]);
});
it('should return single parent path if no further parents', async () => {
vi.mocked(MongoDataset.findById).mockResolvedValueOnce({
name: 'Parent1',
parentId: undefined
});
const result = await getParents('parent1-id');
expect(result).toEqual([{ parentId: 'parent1-id', parentName: 'Parent1' }]);
});
it('should return full parent path for nested parents', async () => {
vi.mocked(MongoDataset.findById)
.mockResolvedValueOnce({
name: 'Child',
parentId: 'parent1-id'
})
.mockResolvedValueOnce({
name: 'Parent1',
parentId: 'parent2-id'
})
.mockResolvedValueOnce({
name: 'Parent2',
parentId: undefined
});
const result = await getParents('child-id');
expect(result).toEqual([
{ parentId: 'parent2-id', parentName: 'Parent2' },
{ parentId: 'parent1-id', parentName: 'Parent1' },
{ parentId: 'child-id', parentName: 'Child' }
]);
});
it('should handle circular references gracefully', async () => {
vi.mocked(MongoDataset.findById)
.mockResolvedValueOnce({
name: 'Node1',
parentId: 'node2-id'
})
.mockResolvedValueOnce({
name: 'Node2',
parentId: 'node1-id' // Circular reference
});
const result = await getParents('node1-id');
expect(result).toEqual([
{ parentId: 'node2-id', parentName: 'Node2' },
{ parentId: 'node1-id', parentName: 'Node1' }
]);
});
});

View File

@@ -0,0 +1,64 @@
import { EditApiKeyProps } from '@/global/support/openapi/api';
import * as createapi from '@/pages/api/support/openapi/create';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
import {
TeamApikeyCreatePermissionVal,
TeamDatasetCreatePermissionVal
} from '@fastgpt/global/support/permission/user/constant';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { getFakeUsers } from '@test/datas/users';
import { Call } from '@test/utils/request';
import { describe, it, expect } from 'vitest';
describe('create dataset', () => {
it('should return 200 when create dataset success', async () => {
const users = await getFakeUsers(2);
await MongoResourcePermission.create({
resourceType: 'team',
teamId: users.members[0].teamId,
resourceId: null,
tmbId: users.members[0].tmbId,
permission: TeamApikeyCreatePermissionVal
});
const res = await Call<EditApiKeyProps>(createapi.default, {
auth: users.members[0],
body: {
name: 'test',
limit: {
maxUsagePoints: 1000
}
}
});
expect(res.error).toBeUndefined();
expect(res.code).toBe(200);
await MongoResourcePermission.create({
resourceType: 'app',
teamId: users.members[1].teamId,
resourceId: null,
tmbId: users.members[1].tmbId,
permission: ManagePermissionVal
});
const app = await MongoApp.create({
name: 'a',
type: 'simple',
tmbId: users.members[1].tmbId,
teamId: users.members[1].teamId
});
const res2 = await Call<EditApiKeyProps>(createapi.default, {
auth: users.members[1],
body: {
appId: app._id,
name: 'test',
limit: {
maxUsagePoints: 1000
}
}
});
expect(res2.error).toBeUndefined();
expect(res2.code).toBe(200);
});
});

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"paths": {
"@/*": ["../src/*"],
"@fastgpt/*": ["../../../packages/*"],
"@test/*": ["../../../test/*"]
}
},
"include": ["**/*.test.ts"],
"exclude": ["**/node_modules"]
}